mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +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 (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
|
||||
"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/gin-gonic/gin"
|
||||
)
|
||||
@@ -60,7 +62,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
// @Summary Operate firewall
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/operate [post]
|
||||
@@ -71,12 +73,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateFirewall(request); err != nil {
|
||||
result, err := firewallService.QueueFirewallOperation(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
@@ -367,7 +370,8 @@ func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @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
|
||||
// @Param request body dto.FirewallRuleDelete true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||
@@ -453,6 +457,8 @@ func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
switch {
|
||||
case errors.Is(err, filter.ErrProtectedRule):
|
||||
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)
|
||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
||||
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)
|
||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||
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),
|
||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||
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):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||
default:
|
||||
@@ -491,26 +500,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall port whitelist update
|
||||
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
|
||||
// @Summary Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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) {
|
||||
var request dto.FirewallPortWhitelistUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
|
||||
if err != nil {
|
||||
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
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
|
||||
@@ -528,14 +580,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
|
||||
helper.ErrorWithBusinessCode(
|
||||
c,
|
||||
http.StatusConflict,
|
||||
"FW_BACKEND_CLEANUP_REQUIRED",
|
||||
"ErrInvalidParams",
|
||||
err,
|
||||
)
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
@@ -664,19 +712,26 @@ func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
||||
}
|
||||
|
||||
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
|
||||
return
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) {
|
||||
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) {
|
||||
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) {
|
||||
if errors.Is(err, docker.ErrUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
// @Summary Ws local terminal
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @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
|
||||
// @Security ApiKeyAuth
|
||||
// @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"))
|
||||
}
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
Persistent: c.Query("terminalPersistent") == "true",
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
}
|
||||
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
||||
client, err := connect()
|
||||
@@ -240,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
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 {
|
||||
|
||||
+45
-18
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||
)
|
||||
@@ -18,6 +19,7 @@ type FirewallSubsystemStatus struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SyncError string `json:"syncError,omitempty"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
@@ -27,6 +29,11 @@ type FirewallLifecycleOperation struct {
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallLifecycleOperationResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallBackendOption struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
@@ -42,10 +49,12 @@ type FirewallBackendOption struct {
|
||||
}
|
||||
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallBackendGroup struct {
|
||||
@@ -55,15 +64,26 @@ type FirewallBackendGroup struct {
|
||||
}
|
||||
|
||||
type FirewallSettings struct {
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist string `json:"portWhiteList"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
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 {
|
||||
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 {
|
||||
@@ -87,11 +107,7 @@ type FirewallInitializationTask struct {
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort struct {
|
||||
Family string
|
||||
Port string
|
||||
Protocol string
|
||||
}
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
@@ -114,6 +130,7 @@ type FirewallRuleReset struct {
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
Refresh bool `json:"refresh,omitempty"`
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
@@ -225,8 +242,10 @@ type DockerPortGuardOperation struct {
|
||||
}
|
||||
|
||||
type FirewallRuleAdopt struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
Marker string `json:"marker,omitempty" validate:"max=256"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
@@ -313,10 +332,18 @@ type FirewallRuleSyncFailure 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 {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||
|
||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
UseServerFilename bool `json:"useServerFilename"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
|
||||
+27
-198
@@ -1,222 +1,51 @@
|
||||
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 {
|
||||
BaseModel
|
||||
|
||||
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
|
||||
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||
Mode string `gorm:"size:32;not null" json:"mode"`
|
||||
UUID string `gorm:"uniqueIndex" json:"uuid"`
|
||||
ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
||||
Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||
HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||
HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||
Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||
Mode string `json:"mode"`
|
||||
Sources string `gorm:"type:text" json:"-"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
|
||||
NativeAction string `gorm:"default:''" json:"-"`
|
||||
NativeRules string `gorm:"type:text" json:"-"`
|
||||
Sequence int64 `gorm:"not null;default:0" json:"-"`
|
||||
Sequence int64 `gorm:"default:0" json:"-"`
|
||||
}
|
||||
|
||||
type ForwardingRule struct {
|
||||
BaseModel
|
||||
|
||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||
Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||
Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||
Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||
TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||
TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||
Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||
}
|
||||
|
||||
type FirewallRule struct {
|
||||
UUID string `gorm:"size:64;primaryKey" json:"uuid"`
|
||||
Family string `gorm:"size:16;not null" json:"family"`
|
||||
UUID string `gorm:"primaryKey" json:"uuid"`
|
||||
Family string `json:"family"`
|
||||
|
||||
Protocol string `gorm:"size:32;not null" json:"protocol"`
|
||||
SourceAddress string `gorm:"size:255" json:"sourceAddress"`
|
||||
SourcePort string `gorm:"size:64" json:"sourcePort"`
|
||||
DestinationAddress string `gorm:"size:255" json:"destinationAddress"`
|
||||
DestinationPort string `gorm:"size:64" json:"destinationPort"`
|
||||
Interface string `gorm:"size:128" json:"interface"`
|
||||
Protocol string `json:"protocol"`
|
||||
SourceAddress string `json:"sourceAddress"`
|
||||
SourcePort string `json:"sourcePort"`
|
||||
DestinationAddress string `json:"destinationAddress"`
|
||||
DestinationPort string `json:"destinationPort"`
|
||||
Interface string `json:"interface"`
|
||||
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
||||
Action string `gorm:"size:32;not null" json:"action"`
|
||||
Action string `json:"action"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
||||
|
||||
Origin string `gorm:"size:32;not null" json:"origin"`
|
||||
Owner string `gorm:"size:320;not null" json:"owner"`
|
||||
Revision uint `gorm:"not null;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
|
||||
Origin string `json:"origin"`
|
||||
Owner string `json:"owner"`
|
||||
Revision uint `gorm:"default:1" json:"revision"`
|
||||
}
|
||||
|
||||
@@ -11,10 +11,8 @@ import (
|
||||
|
||||
type IDockerPortGuardRepo interface {
|
||||
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||
DeleteBatch(context.Context, []string) error
|
||||
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
||||
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
|
||||
}
|
||||
|
||||
type DockerPortGuardRepo struct{}
|
||||
@@ -30,15 +28,6 @@ func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPo
|
||||
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 {
|
||||
return global.DB.WithContext(ctx).
|
||||
Where("read_only = ? AND uuid IN ?", false, uuids).
|
||||
@@ -59,19 +48,3 @@ func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.
|
||||
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/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,18 +24,14 @@ type IFirewallRuleRepo interface {
|
||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
||||
DeleteWithRevision(context.Context, string, uint) error
|
||||
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
|
||||
SaveResetOrder(context.Context, []model.FirewallRule) error
|
||||
}
|
||||
|
||||
type FirewallRuleRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func WithFirewallRuleSource(kind, id string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
|
||||
}
|
||||
}
|
||||
|
||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
||||
return &FirewallRuleRepo{}
|
||||
}
|
||||
@@ -93,6 +90,51 @@ func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error {
|
||||
failures := make(map[string]error)
|
||||
for start := 0; start < len(rules); start += 500 {
|
||||
batch := rules[start:min(start+500, len(rules))]
|
||||
ids := make([][]interface{}, 0, len(batch))
|
||||
for _, rule := range batch {
|
||||
ids = append(ids, []interface{}{rule.UUID, rule.Revision})
|
||||
failures[rule.UUID] = ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
var deleted []model.FirewallRule
|
||||
err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}).
|
||||
Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error
|
||||
if err != nil {
|
||||
for _, rule := range batch {
|
||||
failures[rule.UUID] = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, rule := range deleted {
|
||||
delete(failures, rule.UUID)
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, rule := range rules {
|
||||
result := tx.Model(&model.FirewallRule{}).
|
||||
Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision).
|
||||
Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
||||
return firewallDB(ctx, r.db)
|
||||
}
|
||||
@@ -133,18 +175,13 @@ func prepareFirewallRule(rule *model.FirewallRule) error {
|
||||
}
|
||||
|
||||
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, "uuid")
|
||||
delete(result, "revision")
|
||||
delete(result, "created_at")
|
||||
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/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IForwardingRuleRepo interface {
|
||||
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{}
|
||||
@@ -23,14 +23,16 @@ func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule,
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&rules).Error
|
||||
})
|
||||
func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
|
||||
}
|
||||
|
||||
func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
|
||||
}
|
||||
|
||||
@@ -936,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -967,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
|
||||
@@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
||||
} else if slices.Contains(options, "--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...)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -737,9 +738,11 @@ type modelProvider struct {
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ContextWindow int `json:"contextWindow,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
@@ -767,7 +770,7 @@ type browserConfig struct {
|
||||
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) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -852,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -906,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -920,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
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) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
@@ -1041,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
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
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
|
||||
@@ -30,10 +30,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
sslAutoRenewAlertSkipDays = 31
|
||||
)
|
||||
|
||||
type AlertTaskHelper struct {
|
||||
@@ -864,7 +865,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
daysDiff := int(math.Ceil(
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
currentDate := time.Now()
|
||||
daysDiffMap := make(map[int][]string)
|
||||
|
||||
@@ -40,14 +40,13 @@ const (
|
||||
appUpgradeDown
|
||||
appUpgradeMutated
|
||||
appUpgradeStarted
|
||||
appUpgradeReady
|
||||
appUpgradeCommitted
|
||||
)
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
var appUpgradeLocks sync.Map
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
type appUpgradeSnapshot interface {
|
||||
Restore() error
|
||||
Cleanup()
|
||||
@@ -439,15 +438,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
t.LogSuccess(logStr)
|
||||
u.phase = appUpgradeStarted
|
||||
|
||||
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||
return err
|
||||
containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
|
||||
if discoverErr != nil {
|
||||
t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
|
||||
} else if len(containerNames) > 0 {
|
||||
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.Message = ""
|
||||
|
||||
@@ -472,6 +470,11 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||
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.deleteOldImages(t)
|
||||
return nil
|
||||
@@ -591,9 +594,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) finishRollback() error {
|
||||
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||
return err
|
||||
}
|
||||
restored := u.original
|
||||
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||
return err
|
||||
@@ -881,28 +881,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
type appContainerReadinessClient interface {
|
||||
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
|
||||
}
|
||||
}
|
||||
func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
|
||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -916,36 +895,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
|
||||
if len(expectedServices) == 0 {
|
||||
return strings.Split(install.ContainerName, ","), nil
|
||||
}
|
||||
options := container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||
),
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
foundServices := make(map[string]bool, len(expectedServices))
|
||||
containerNames := make([]string, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
serviceName := item.Labels[composeServiceLabel]
|
||||
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 {
|
||||
if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(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)
|
||||
return containerNames, nil
|
||||
}
|
||||
|
||||
@@ -352,22 +352,34 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if dir != nil {
|
||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||
t.Log(logStr)
|
||||
cleanupFailed := false
|
||||
|
||||
if deleteReq.UseLifecycleScripts {
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
return err
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out, err := compose.Down(install.GetComposePath())
|
||||
if err != nil && !deleteReq.ForceDelete {
|
||||
return handleErr(install, err, out)
|
||||
out, downErr := compose.Down(install.GetComposePath())
|
||||
if downErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return handleErr(install, downErr, out)
|
||||
}
|
||||
}
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return err
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
if !cleanupFailed {
|
||||
t.LogSuccess(logStr)
|
||||
}
|
||||
if deleteReq.DeleteImage {
|
||||
content, err := op.GetContent(install.GetEnvPath())
|
||||
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)
|
||||
go func() {
|
||||
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete {
|
||||
if err := uninstallTask.Execute(); err != nil {
|
||||
install.Status = constant.StatusError
|
||||
install.Message = err.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -464,12 +464,14 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return datas
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
)
|
||||
|
||||
const dockerNftablesMinVersion = "29.0.0"
|
||||
@@ -82,6 +84,11 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
||||
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
||||
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)
|
||||
existed := readErr == nil
|
||||
@@ -276,7 +283,8 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
|
||||
delete(daemonMap, "ipv6")
|
||||
delete(daemonMap, "fixed-cidr-v6")
|
||||
delete(daemonMap, "ip6tables")
|
||||
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables {
|
||||
backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
delete(daemonMap, "experimental")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ var (
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
|
||||
websiteRepo = repo.NewIWebsiteRepo()
|
||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||
|
||||
@@ -439,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, c.Files)
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
@@ -516,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
||||
t.LogStart(c.Path)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, []string{c.Path})
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
dstExisted := fo.Stat(c.Dst)
|
||||
parentDir := filepath.Dir(c.Dst)
|
||||
if !fo.Stat(parentDir) {
|
||||
@@ -896,6 +900,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
key := "file-wget-" + common.GetUuid()
|
||||
options := files.DownloadOptions{
|
||||
IgnoreCertificate: w.IgnoreCertificate,
|
||||
UseServerFilename: w.UseServerFilename,
|
||||
}
|
||||
if w.UseProxy {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
fo := files.NewFileOp()
|
||||
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)) {
|
||||
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 {
|
||||
fileTransferLocks.Release(m.TaskID)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer fileTransferLocks.Release(m.TaskID)
|
||||
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
||||
t.LogStart(m.NewPath)
|
||||
taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
|
||||
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)
|
||||
if err != nil && t.TaskCtx.Err() != nil {
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/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/iptables_helper"
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||
}
|
||||
|
||||
type FirewallSettingService struct{}
|
||||
|
||||
var firewallWhitelistTaskMu sync.Mutex
|
||||
var firewallWhitelistMu sync.Mutex
|
||||
|
||||
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
|
||||
|
||||
func firewallBackendCleanupRequired(current, target string) error {
|
||||
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 {
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required)
|
||||
txCtx := context.WithValue(ctx, constant.DB, tx)
|
||||
if err := firewallService.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(ports)
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("key = ?", constant.FirewallPortWhiteList).
|
||||
Assign(map[string]interface{}{"value": string(value)}).
|
||||
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
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
|
||||
})
|
||||
return removed, err
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
|
||||
type state struct {
|
||||
ready bool
|
||||
err error
|
||||
}
|
||||
states := make(map[string]state)
|
||||
return func(port dto.FirewallSystemPort) (bool, error) {
|
||||
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)
|
||||
}
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
report("failed", label, err)
|
||||
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
||||
continue
|
||||
return err
|
||||
}
|
||||
report("applied", label, nil)
|
||||
}
|
||||
return errors.Join(failures...)
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func whitelistPortLabel(port dto.FirewallSystemPort) string {
|
||||
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol)
|
||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||
if request.Rule == nil {
|
||||
return fmt.Errorf("select one firewall port whitelist rule to delete")
|
||||
}
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
|
||||
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
|
||||
result.PortWhitelist = ports
|
||||
} else {
|
||||
result.PortWhitelist = constant.FirewallPortWhiteListValue
|
||||
}
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
|
||||
|
||||
installed := make(map[string]bool)
|
||||
for _, name := range lifecycle.InstalledProviders() {
|
||||
installed[name] = true
|
||||
}
|
||||
result.System.Selected = configuredSystemFirewallBackend()
|
||||
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
result.System.Selected = strings.TrimSpace(systemBackend)
|
||||
if result.System.Selected == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
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}
|
||||
if option.Installed && name == result.System.Selected {
|
||||
client, err := lifecycle.NewClientFor(name)
|
||||
client, err := lifecycle.NewClient(name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if supportsManagedFilterChains(name) {
|
||||
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
|
||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||
overview, err := loadSystemFirewallOverview(name, "base")
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
|
||||
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
|
||||
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
|
||||
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
|
||||
} else if option.Active, err = client.Status(); err != nil {
|
||||
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.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
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed && name == result.Forwarding.Selected {
|
||||
manager, err := newForwardingManagerFor(name)
|
||||
manager, err := newForwardingAdapterFor(name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if status, err := manager.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4)
|
||||
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6)
|
||||
option.IPv4 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound,
|
||||
status, statusErr := loadForwardingFirewallOverview(manager)
|
||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||
if statusErr != nil {
|
||||
option.Message = statusErr.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
}
|
||||
option.IPv6 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound,
|
||||
}
|
||||
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
|
||||
}
|
||||
if name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +211,11 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
if dockerInstalled {
|
||||
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
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{
|
||||
@@ -329,53 +228,42 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
option.Active = false
|
||||
}
|
||||
if option.Active {
|
||||
guard := docker_guard.NewRuntime(name)
|
||||
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
|
||||
guard := newDockerFirewallRuntime(name)
|
||||
ipv4, ipv6 := guard.Status(dockerfirewall.FamilyIPv4), guard.Status(dockerfirewall.FamilyIPv6)
|
||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
||||
option.Bound = ipv4.Bound || ipv6.Bound
|
||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
||||
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv4.Available = ipv4.Reason != dockerfirewall.ReasonCommandMissing
|
||||
option.IPv6.Available = ipv6.Reason != dockerfirewall.ReasonCommandMissing
|
||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
||||
}
|
||||
result.Docker.Options = append(result.Docker.Options, option)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
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)
|
||||
var err error
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus {
|
||||
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil || !commands.IPv6Available() {
|
||||
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
|
||||
}
|
||||
}
|
||||
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
|
||||
return dto.FirewallBackendFamilyStatus{
|
||||
Available: err == nil,
|
||||
Initialized: initialized,
|
||||
Bound: bound,
|
||||
result.PanelPort = LoadPanelPort()
|
||||
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
|
||||
if sshErr != nil {
|
||||
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
|
||||
} else {
|
||||
result.SSHPort = sshPort
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
switch request.Subsystem {
|
||||
@@ -385,10 +273,9 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
service := newFirewallService()
|
||||
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.syncConfiguredFirewallPorts(ctx)
|
||||
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
|
||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||
return errors.Join(rulesErr, whitelistErr)
|
||||
}
|
||||
return nil
|
||||
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 {
|
||||
guard := docker_guard.NewRuntime(request.Backend)
|
||||
if request.Operation == "cleanup" {
|
||||
if err := guard.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
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 NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
|
||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
@@ -465,7 +302,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if previous == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
previous = client.Name()
|
||||
}
|
||||
}
|
||||
@@ -475,7 +312,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
@@ -501,21 +338,14 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
}
|
||||
|
||||
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
|
||||
}
|
||||
|
||||
func systemFirewallBackendInitializedWithClientFactory(
|
||||
backend string,
|
||||
newClient func(string) (lifecycle.Client, error),
|
||||
) (bool, error) {
|
||||
client, err := newClient(backend)
|
||||
client, err := lifecycle.NewClient(backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if supportsManagedFilterChains(backend) {
|
||||
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
||||
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
@@ -533,30 +363,8 @@ func systemFirewallBackendInitializedWithClientFactory(
|
||||
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 {
|
||||
manager, err := newForwardingManagerFor(request.Backend)
|
||||
manager, err := newForwardingAdapterFor(request.Backend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -574,7 +382,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
detected, err := newForwardingManager()
|
||||
detected, err := newForwardingAdapter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -585,7 +393,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
@@ -599,7 +407,7 @@ func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOp
|
||||
}
|
||||
|
||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
manager, err := newForwardingManagerFor(backend)
|
||||
manager, err := newForwardingAdapterFor(backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
@@ -618,9 +426,59 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func alternateDirectBackend(backend string) string {
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
return constant.FirewallProviderIptables
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
guard := newDockerFirewallRuntime(request.Backend)
|
||||
if request.Operation == "cleanup" {
|
||||
if err := guard.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
}
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"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/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
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/ping"
|
||||
)
|
||||
|
||||
const (
|
||||
forwardingSyncConverged = "converged"
|
||||
forwardingSyncMissing = "missing"
|
||||
forwardingSyncRuntimeOnly = "runtime_only"
|
||||
)
|
||||
|
||||
type IForwardingService interface {
|
||||
@@ -32,7 +41,7 @@ type IForwardingService interface {
|
||||
}
|
||||
|
||||
type ForwardingService struct {
|
||||
managerFactory func() (*forwarding.Manager, error)
|
||||
clientFactory func() (forwarding.Adapter, error)
|
||||
rules repo.IForwardingRuleRepo
|
||||
enabled func() (bool, error)
|
||||
persistBackend func(string) error
|
||||
@@ -40,43 +49,27 @@ type ForwardingService struct {
|
||||
}
|
||||
|
||||
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
||||
var forwardingMutationMu sync.Mutex
|
||||
|
||||
const (
|
||||
forwardingSyncConverged = "converged"
|
||||
forwardingSyncMissing = "missing"
|
||||
forwardingSyncRuntimeOnly = "runtime_only"
|
||||
)
|
||||
var forwardingMutationMu sync.Mutex
|
||||
|
||||
var (
|
||||
forwardingSyncStateMu sync.RWMutex
|
||||
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) {
|
||||
selected := configuredForwardingBackend()
|
||||
baseInfo := dto.FirewallSubsystemStatus{
|
||||
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(),
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
selected = strings.TrimSpace(selected)
|
||||
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 errors.Is(err, errForwardingBackendUnavailable) {
|
||||
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
||||
@@ -84,39 +77,41 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
}
|
||||
return baseInfo, err
|
||||
}
|
||||
status, err := manager.Status()
|
||||
client, err := lifecycle.NewClient(manager.Name())
|
||||
if err != nil {
|
||||
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.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
|
||||
baseInfo.Version = status.Version
|
||||
baseInfo.PingStatus = ping.LoadStatus()
|
||||
baseInfo.Name, baseInfo.Backend = manager.Name(), manager.Name()
|
||||
if baseInfo.Backend == constant.FirewallProviderIptables || baseInfo.Backend == constant.FirewallProviderNftables {
|
||||
baseInfo.Name += "-forward"
|
||||
}
|
||||
baseInfo.Version = version
|
||||
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4)
|
||||
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6)
|
||||
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
||||
for _, family := range []struct {
|
||||
command string
|
||||
status *dto.FirewallBackendFamilyStatus
|
||||
}{
|
||||
{"iptables", &baseInfo.IPv4},
|
||||
{"ip6tables", &baseInfo.IPv6},
|
||||
} {
|
||||
policy, err := loadForwardPolicy(family.command)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
|
||||
continue
|
||||
}
|
||||
family.status.ForwardPolicy = policy
|
||||
}
|
||||
return baseInfo, nil
|
||||
}
|
||||
|
||||
func 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) {
|
||||
if request.Strategy != "" {
|
||||
return 0, nil, nil
|
||||
@@ -125,11 +120,11 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
manager, err := s.manager()
|
||||
manager, err := s.clientFactory()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
runtime, err := manager.List("", "")
|
||||
runtime, err := manager.List()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -179,24 +174,18 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
|
||||
return int64(total), items, nil
|
||||
}
|
||||
|
||||
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
|
||||
values := []string{
|
||||
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
|
||||
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), keyword) {
|
||||
return true
|
||||
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
||||
count := 0
|
||||
for _, rule := range request.Rules {
|
||||
if rule.Operation == "add" {
|
||||
count += strings.Count(rule.Protocol, "/") + 1
|
||||
}
|
||||
if count > filter.MaxAtomicExpansion {
|
||||
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
|
||||
for i, 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)
|
||||
for _, rule := range request.Rules {
|
||||
if rule.Operation != "add" {
|
||||
operation = task.TaskUpdate
|
||||
}
|
||||
@@ -204,48 +193,26 @@ func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.Fi
|
||||
if forwardingOperationsOnlyRemove(request.Rules) {
|
||||
operation = task.TaskDelete
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error {
|
||||
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)
|
||||
taskItem, err := task.NewTask(firewallTaskName(operation, firewallTaskForwarding, ""), operation, task.TaskScopeFirewall, "", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
desired, err := applyForwardingOperations(forwardingRulesFromModels(stored), request.Rules)
|
||||
if errors.Is(err, forwarding.ErrRuleExists) {
|
||||
return buserr.New("ErrRecordExist")
|
||||
} else if err != nil {
|
||||
return err
|
||||
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
||||
return s.operateRules(t.TaskCtx, request, t)
|
||||
}, nil, 0, 0)
|
||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||
closeUnstartedFirewallTask(taskItem)
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
go func() { _ = taskItem.Execute() }()
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Enable() error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := s.manager()
|
||||
manager, err := s.clientFactory()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
@@ -254,7 +221,7 @@ func (s *ForwardingService) Enable() error {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
if err := s.initializeForwarding(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
@@ -263,14 +230,12 @@ func (s *ForwardingService) Enable() error {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.Reconcile(forwardingRulesFromModels(rules))
|
||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ForwardingService) QueueInitialization(
|
||||
request dto.FirewallInitializationTask,
|
||||
) (dto.FilterChainOperationResponse, error) {
|
||||
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
@@ -278,13 +243,13 @@ func (s *ForwardingService) QueueInitialization(
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
|
||||
}
|
||||
var manager *forwarding.Manager
|
||||
var manager forwarding.Adapter
|
||||
var backend string
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
var err error
|
||||
manager, err = s.manager()
|
||||
manager, err = s.clientFactory()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
@@ -295,7 +260,7 @@ func (s *ForwardingService) QueueInitialization(
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
if err := s.initializeForwarding(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
@@ -309,7 +274,7 @@ func (s *ForwardingService) QueueInitialization(
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.Reconcile(forwardingRulesFromModels(rules))
|
||||
err = manager.ReplaceRules(forwardingRulesFromModels(rules))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}, nil)
|
||||
@@ -320,131 +285,43 @@ func (s *ForwardingService) QueueInitialization(
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
enabled, err := s.forwardingEnabled()
|
||||
if err != nil || !enabled {
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
func NewIForwardingService() IForwardingService {
|
||||
return newForwardingService()
|
||||
}
|
||||
|
||||
func loadForwardPolicy(command string) (string, error) {
|
||||
if !cmd.Which(command) {
|
||||
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 {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
stored, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
|
||||
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
|
||||
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
|
||||
}
|
||||
return fields[2], nil
|
||||
}
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.Reconcile(forwardingRulesFromModels(stored))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
return "", errors.New("FORWARD default policy was not found")
|
||||
}
|
||||
|
||||
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
|
||||
manager, err := s.manager()
|
||||
if err != nil {
|
||||
return err
|
||||
func lastForwardingSyncError() string {
|
||||
forwardingSyncStateMu.RLock()
|
||||
defer forwardingSyncStateMu.RUnlock()
|
||||
if forwardingLastSyncErr == nil {
|
||||
return ""
|
||||
}
|
||||
return s.reconcileWithManager(manager, rules)
|
||||
return forwardingLastSyncErr.Error()
|
||||
}
|
||||
|
||||
func (s *ForwardingService) reconcileWithManager(manager *forwarding.Manager, rules []forwarding.Rule) 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) {
|
||||
func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
|
||||
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
|
||||
byIdentity := make(map[string]int, len(stored)+len(runtime))
|
||||
for _, record := range stored {
|
||||
@@ -475,108 +352,215 @@ func mergeForwardingInventory(
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func recordForwardingSyncError(err error) {
|
||||
forwardingSyncStateMu.Lock()
|
||||
forwardingLastSyncErr = err
|
||||
forwardingSyncStateMu.Unlock()
|
||||
}
|
||||
|
||||
func lastForwardingSyncError() string {
|
||||
forwardingSyncStateMu.RLock()
|
||||
defer forwardingSyncStateMu.RUnlock()
|
||||
if forwardingLastSyncErr == nil {
|
||||
return ""
|
||||
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
|
||||
values := []string{
|
||||
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
|
||||
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
|
||||
}
|
||||
return forwardingLastSyncErr.Error()
|
||||
}
|
||||
|
||||
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)
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), keyword) {
|
||||
return true
|
||||
}
|
||||
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, "/") {
|
||||
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
||||
Family: operation.Family, Protocol: protocol, Port: operation.Port, TargetIP: operation.TargetIP,
|
||||
TargetPort: operation.TargetPort, Interface: operation.Interface,
|
||||
Family: operation.Family, Protocol: protocol, Port: operation.Port,
|
||||
TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
index := forwardingRuleIndex(desired, rule)
|
||||
switch forwarding.OperationType(operation.Operation) {
|
||||
case forwarding.OperationAdd:
|
||||
if index >= 0 {
|
||||
return nil, forwarding.ErrRuleExists
|
||||
}
|
||||
desired = append(desired, rule)
|
||||
case forwarding.OperationRemove:
|
||||
if index >= 0 {
|
||||
desired = append(desired[:index], desired[index+1:]...)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
|
||||
groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
|
||||
}
|
||||
}
|
||||
stored, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byIdentity := make(map[string]model.ForwardingRule, len(stored))
|
||||
for index, rule := range forwardingRulesFromModels(stored) {
|
||||
normalized, err := forwarding.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byIdentity[normalized.Identity()] = stored[index]
|
||||
}
|
||||
succeeded, failed, skipped := 0, 0, 0
|
||||
var nativeFailure error
|
||||
defer func() {
|
||||
recordForwardingSyncError(errors.Join(resultErr, nativeFailure))
|
||||
if t != nil {
|
||||
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed}))
|
||||
if skipped > 0 {
|
||||
t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped)
|
||||
}
|
||||
}
|
||||
}()
|
||||
record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) {
|
||||
label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
|
||||
switch status {
|
||||
case "skipped":
|
||||
skipped++
|
||||
if t != nil {
|
||||
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
|
||||
}
|
||||
case "failed":
|
||||
failed++
|
||||
if t != nil {
|
||||
t.LogFailedWithErr(label, cause)
|
||||
}
|
||||
default:
|
||||
succeeded++
|
||||
if t != nil {
|
||||
t.LogSuccess(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
return desired, nil
|
||||
}
|
||||
|
||||
func forwardingRuleIndex(rules []forwarding.Rule, wanted forwarding.Rule) int {
|
||||
wantedIdentity := wanted.Identity()
|
||||
for index, rule := range rules {
|
||||
if rule.Identity() == wantedIdentity {
|
||||
return index
|
||||
if len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd {
|
||||
old := make(map[string]bool, len(groups[0].rules))
|
||||
for _, rule := range groups[0].rules {
|
||||
old[rule.Identity()] = true
|
||||
}
|
||||
unchanged := len(old) == len(groups[1].rules)
|
||||
duplicate := false
|
||||
for _, rule := range groups[1].rules {
|
||||
key := rule.Identity()
|
||||
unchanged = unchanged && old[key]
|
||||
if _, exists := byIdentity[key]; exists && !old[key] {
|
||||
duplicate = true
|
||||
}
|
||||
}
|
||||
if unchanged || duplicate {
|
||||
for _, group := range groups {
|
||||
for _, rule := range group.rules {
|
||||
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, rule := range groups[1].rules {
|
||||
if rule.Family != forwarding.FamilyIPv6 {
|
||||
continue
|
||||
}
|
||||
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check IPv6 Router Advertisement: %w", err)
|
||||
}
|
||||
if len(interfaces) > 0 {
|
||||
return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool {
|
||||
if len(operations) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, operation := range operations {
|
||||
if operation.Operation != string(forwarding.OperationRemove) {
|
||||
return false
|
||||
var client forwarding.Adapter
|
||||
var failures []error
|
||||
for _, group := range groups {
|
||||
byFamily := make(map[string][]forwarding.Rule, 2)
|
||||
seen := make(map[string]bool, len(group.rules))
|
||||
for _, rule := range group.rules {
|
||||
key := rule.Identity()
|
||||
_, exists := byIdentity[key]
|
||||
if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
|
||||
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
byFamily[rule.Family] = append(byFamily[rule.Family], rule)
|
||||
}
|
||||
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
||||
rules := byFamily[family]
|
||||
if len(rules) == 0 {
|
||||
continue
|
||||
}
|
||||
err := ctx.Err()
|
||||
if err == nil && client == nil {
|
||||
var enabled bool
|
||||
enabled, err = s.forwardingEnabled()
|
||||
if err == nil && !enabled {
|
||||
err = fmt.Errorf("%w: forwarding is not initialized", filter.ErrProviderUnavailable)
|
||||
}
|
||||
if err == nil {
|
||||
client, err = s.clientFactory()
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
if group.operation == forwarding.OperationAdd {
|
||||
err = client.CreateRules(ctx, rules)
|
||||
} else {
|
||||
err = client.DeleteRules(ctx, rules)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
nativeFailure = errors.Join(nativeFailure, err)
|
||||
if !request.ForceDelete || !forwardingOperationsOnlyRemove(request.Rules) || ctx.Err() != nil {
|
||||
failures = append(failures, err)
|
||||
for _, rule := range rules {
|
||||
record(group.operation, rule, "failed", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if t != nil {
|
||||
t.Logf("force delete database records: %v", err)
|
||||
}
|
||||
}
|
||||
for start := 0; start < len(rules); start += 500 {
|
||||
batch := rules[start:min(start+500, len(rules))]
|
||||
records := make([]model.ForwardingRule, 0, len(batch))
|
||||
ids := make([]uint, 0, len(batch))
|
||||
for _, rule := range batch {
|
||||
if group.operation == forwarding.OperationAdd {
|
||||
records = append(records, model.ForwardingRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||
} else if stored, exists := byIdentity[rule.Identity()]; exists {
|
||||
ids = append(ids, stored.ID)
|
||||
}
|
||||
}
|
||||
if group.operation == forwarding.OperationAdd {
|
||||
err = s.rules.CreateBatch(context.WithoutCancel(ctx), records)
|
||||
} else {
|
||||
err = s.rules.DeleteBatch(context.WithoutCancel(ctx), ids)
|
||||
}
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
}
|
||||
for index, rule := range batch {
|
||||
if err != nil {
|
||||
record(group.operation, rule, "failed", err)
|
||||
continue
|
||||
}
|
||||
if group.operation == forwarding.OperationAdd {
|
||||
byIdentity[rule.Identity()] = records[index]
|
||||
} else {
|
||||
delete(byIdentity, rule.Identity())
|
||||
}
|
||||
record(group.operation, rule, "succeeded", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
if group.operation == forwarding.OperationRemove && len(failures) > 0 {
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
+31
-24
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
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/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -217,10 +219,21 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
ports := splitSSHPorts(value)
|
||||
if len(ports) == 0 {
|
||||
@@ -379,18 +398,6 @@ func loadPrimarySSHPort(value string) (int, error) {
|
||||
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) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
||||
if sudo != "" {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -32,7 +33,9 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -2470,10 +2470,19 @@
|
||||
},
|
||||
"/core/xpack/vms/del": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "virtual_machines",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
@@ -3770,12 +3779,31 @@
|
||||
},
|
||||
"/hosts/firewall/settings/whitelist": {
|
||||
"bodyKeys": [
|
||||
"value"
|
||||
"rule"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防火墙端口白名单 [value]",
|
||||
"formatEN": "update firewall port whitelist [value]"
|
||||
"formatZH": "创建防火墙端口白名单",
|
||||
"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": {
|
||||
"bodyKeys": [],
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
FirewallPingStatusKey = "BanPing"
|
||||
|
||||
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 (
|
||||
|
||||
+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'
|
||||
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
|
||||
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
|
||||
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed'
|
||||
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
|
||||
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
|
||||
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
|
||||
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
|
||||
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
|
||||
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
|
||||
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
|
||||
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"
|
||||
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'
|
||||
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
|
||||
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
|
||||
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
|
||||
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
|
||||
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
|
||||
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
|
||||
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
|
||||
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
|
||||
@@ -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 }}'
|
||||
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."
|
||||
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"
|
||||
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: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
|
||||
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
|
||||
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
|
||||
FirewallSyncOperationsResult: 'عملیات همگامسازی: {{ .removed }} حذفشده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرانشده'
|
||||
FirewallSyncOperationsResult: 'عملیات همگامسازی: {{ .removed }} حذفشده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرانشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
|
||||
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
|
||||
FirewallSyncStep: 'همگامسازی قوانین با {{ .name }}'
|
||||
FirewallSyncFailed: 'همگامسازی {{ .failed }} قانون فایروال ناموفق بود'
|
||||
FirewallResetSourceStep: 'بازنشانی و غیرفعالکردن فایروال مبدأ {{ .name }}'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker
|
||||
FirewallInitializeDockerGuardStep: 'راهاندازی و اتصال زنجیرههای محافظت پورت {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
|
||||
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمیکند. لطفاً یک قانون جدید ایجاد کنید."
|
||||
FirewallWhitelistTask: "بهروزرسانی فهرست پورتهای مجاز فایروال"
|
||||
FirewallWhitelistSaved: "پیکربندی فهرست مجاز ذخیره شد"
|
||||
FirewallWhitelistDeferred: "{{ .name }}: پیکربندی ذخیره شد؛ در انتظار فعالسازی فایروال"
|
||||
FirewallWhitelistReleased: "{{ .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: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
|
||||
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
|
||||
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
|
||||
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
|
||||
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
|
||||
FirewallSyncRuleUnchanged: '一致済み、変更不要'
|
||||
FirewallSyncStep: '{{ .name }} にルールを同期'
|
||||
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
|
||||
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエン
|
||||
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
|
||||
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
|
||||
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
|
||||
FirewallWhitelistTask: "ファイアウォールのポート許可リストを更新"
|
||||
FirewallWhitelistSaved: "許可リストの設定を保存しました"
|
||||
FirewallWhitelistDeferred: "{{ .name }}:設定を保存しました。ファイアウォールの有効化後に適用します"
|
||||
FirewallWhitelistReleased: "{{ .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: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
|
||||
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
|
||||
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
|
||||
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
|
||||
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
|
||||
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
|
||||
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
|
||||
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
|
||||
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책
|
||||
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
|
||||
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
|
||||
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
|
||||
FirewallWhitelistTask: "방화벽 포트 허용 목록 업데이트"
|
||||
FirewallWhitelistSaved: "허용 목록 설정이 저장되었습니다"
|
||||
FirewallWhitelistDeferred: "{{ .name }}: 설정이 저장되었으며 방화벽 활성화 후 적용됩니다"
|
||||
FirewallWhitelistReleased: "{{ .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: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
|
||||
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
|
||||
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
|
||||
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
|
||||
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
|
||||
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
|
||||
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
|
||||
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
|
||||
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
|
||||
@@ -707,8 +708,16 @@ FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແ
|
||||
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
|
||||
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
|
||||
FirewallWhitelistTask: "ອັບເດດລາຍຊື່ພອດທີ່ອະນຸຍາດຂອງໄຟວໍ"
|
||||
FirewallWhitelistSaved: "ບັນທຶກການຕັ້ງຄ່າລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ"
|
||||
FirewallWhitelistDeferred: "{{ .name }}: ບັນທຶກການຕັ້ງຄ່າແລ້ວ; ລໍຖ້າເປີດໃຊ້ໄຟວໍ"
|
||||
FirewallWhitelistReleased: "{{ .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'
|
||||
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
|
||||
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
|
||||
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
|
||||
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
|
||||
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
|
||||
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
|
||||
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
|
||||
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
|
||||
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
|
||||
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"
|
||||
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'
|
||||
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
|
||||
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
|
||||
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas'
|
||||
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
|
||||
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
|
||||
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
|
||||
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
|
||||
@@ -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 }}'
|
||||
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."
|
||||
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"
|
||||
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: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
|
||||
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
|
||||
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
|
||||
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}'
|
||||
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
|
||||
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
|
||||
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
|
||||
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
|
||||
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: 'Проверить бэкенд межсете
|
||||
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
|
||||
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
|
||||
FirewallWhitelistTask: "Обновить список разрешённых портов межсетевого экрана"
|
||||
FirewallWhitelistSaved: "Настройки списка разрешённых портов сохранены"
|
||||
FirewallWhitelistDeferred: "{{ .name }}: настройки сохранены; ожидается включение межсетевого экрана"
|
||||
FirewallWhitelistReleased: "{{ .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ı'
|
||||
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
|
||||
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
|
||||
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
|
||||
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
|
||||
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
|
||||
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
|
||||
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
|
||||
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
|
||||
@@ -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'
|
||||
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."
|
||||
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"
|
||||
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: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
|
||||
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
|
||||
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
|
||||
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
|
||||
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
|
||||
FirewallSyncRuleUnchanged: '已一致,無需變更'
|
||||
FirewallSyncStep: '同步規則到 {{ .name }}'
|
||||
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
|
||||
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
|
||||
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
|
||||
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
|
||||
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
|
||||
FirewallWhitelistTask: "更新防火牆連接埠白名單"
|
||||
FirewallWhitelistSaved: "白名單設定已儲存"
|
||||
FirewallWhitelistDeferred: "{{ .name }}:設定已儲存,待防火牆啟用後套用"
|
||||
FirewallWhitelistReleased: "{{ .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: '规则创建失败,未入库,已执行的命令不回滚'
|
||||
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
|
||||
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
|
||||
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
|
||||
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
|
||||
FirewallSyncRuleUnchanged: '已一致,无需变更'
|
||||
FirewallSyncStep: "同步规则到 {{ .name }}"
|
||||
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
|
||||
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
|
||||
@@ -716,8 +717,16 @@ FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
|
||||
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
|
||||
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
|
||||
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
|
||||
FirewallWhitelistTask: "更新防火墙端口白名单"
|
||||
FirewallWhitelistSaved: "白名单配置已保存"
|
||||
FirewallWhitelistDeferred: "{{ .name }}:配置已保存,待防火墙启用后下发"
|
||||
FirewallWhitelistReleased: "{{ .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/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
|
||||
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -26,6 +26,15 @@ func Init() {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
|
||||
return
|
||||
@@ -40,11 +49,11 @@ func Init() {
|
||||
if err := initForwardingRules(ctx); err != nil {
|
||||
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
|
||||
}
|
||||
if !needInit() {
|
||||
initialize = needInit()
|
||||
if !initialize {
|
||||
repairIptablesBaseChains(clientName)
|
||||
return
|
||||
}
|
||||
defer initDockerPortGuard(ctx)
|
||||
InitPingStatus()
|
||||
global.LOG.Info("initializing firewall settings...")
|
||||
if clientName == "nftables" {
|
||||
@@ -64,17 +73,12 @@ func Init() {
|
||||
return
|
||||
}
|
||||
settingRepo := repo.NewISettingRepo()
|
||||
panelPort := service.LoadPanelPort()
|
||||
if len(panelPort) == 0 {
|
||||
global.LOG.Errorf("find 1panel service port failed")
|
||||
return
|
||||
}
|
||||
requiredPorts, err := service.LoadRequiredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load required firewall ports failed, err: %v", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -99,11 +103,12 @@ func repairIptablesBaseChains(clientName string) {
|
||||
if status != constant.StatusEnable {
|
||||
return
|
||||
}
|
||||
manager := iptables_helper.Manager{
|
||||
PanelPort: service.LoadPanelPort,
|
||||
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList,
|
||||
ports, err := service.LoadRequiredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +155,7 @@ func needInit() bool {
|
||||
|
||||
func InitPingStatus() {
|
||||
global.LOG.Info("initializing ban ping status from settings...")
|
||||
status := ping.LoadStatus()
|
||||
status := firewall.LoadPingStatus()
|
||||
statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing")
|
||||
if statusInDB == status {
|
||||
return
|
||||
@@ -160,7 +165,7 @@ func InitPingStatus() {
|
||||
if statusInDB == constant.StatusDisable {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.NormalizeFirewallBackendSelections,
|
||||
migrations.SimplifyFirewallRulePolicy,
|
||||
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/copier"
|
||||
"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/xpack"
|
||||
|
||||
@@ -1131,7 +1131,7 @@ var AddisIPtoWebsiteSSL = &gormigrate.Migration{
|
||||
var InitPingStatus = &gormigrate.Migration{
|
||||
ID: "20251201-init-ping-status",
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
@@ -142,7 +145,7 @@ func convertLegacyHostFirewallRecords(records []legacyHostFirewallRecord, provid
|
||||
}
|
||||
continue
|
||||
}
|
||||
identity := item.PolicyKey()
|
||||
identity := hostFirewallPolicyKey(item)
|
||||
if index, exists := byIdentity[identity]; exists {
|
||||
if item.Description != "" {
|
||||
converted[index].Description = item.Description
|
||||
@@ -336,10 +339,30 @@ func legacyIPOrPrefix(value string) bool {
|
||||
}
|
||||
|
||||
func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error) {
|
||||
record, err := model.FirewallRuleFromDomain(rule)
|
||||
normalized, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return model.FirewallRule{}, err
|
||||
}
|
||||
switch normalized.NativeKind {
|
||||
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
|
||||
default:
|
||||
return model.FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
|
||||
}
|
||||
record := model.FirewallRule{
|
||||
Family: string(normalized.Scope.Family),
|
||||
Protocol: normalized.Protocol,
|
||||
SourceAddress: normalized.SourceAddress,
|
||||
SourcePort: normalized.SourcePort,
|
||||
DestinationAddress: normalized.DestinationAddress,
|
||||
DestinationPort: normalized.DestinationPort,
|
||||
Interface: normalized.Interface,
|
||||
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
|
||||
Action: string(normalized.Action),
|
||||
Description: normalized.Description,
|
||||
}
|
||||
if normalized.Scope.Provider == filter.ProviderFirewalld {
|
||||
record.Priority = normalized.Priority
|
||||
}
|
||||
record.UUID = uuid.NewString()
|
||||
record.Origin = constant.FirewallRuleOriginAdopted
|
||||
record.Owner = constant.FirewallRuleSourceUser
|
||||
@@ -347,6 +370,27 @@ func hostFirewallRuleModel(rule filter.FirewallRule) (model.FirewallRule, error)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func hostFirewallPolicyKey(rule model.FirewallRule) string {
|
||||
payload, _ := json.Marshal(struct {
|
||||
Family string `json:"family"`
|
||||
Protocol string `json:"protocol"`
|
||||
SourceAddress string `json:"sourceAddress,omitempty"`
|
||||
SourcePort string `json:"sourcePort,omitempty"`
|
||||
DestinationAddress string `json:"destinationAddress,omitempty"`
|
||||
DestinationPort string `json:"destinationPort,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
ConnectionStates string `json:"connectionStates,omitempty"`
|
||||
Action string `json:"action"`
|
||||
}{
|
||||
Family: rule.Family, Protocol: rule.Protocol,
|
||||
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
|
||||
})
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) error {
|
||||
var existing []model.FirewallRule
|
||||
if err := tx.Find(&existing).Error; err != nil {
|
||||
@@ -354,10 +398,10 @@ func importLegacyHostFirewallRules(tx *gorm.DB, rules []model.FirewallRule) erro
|
||||
}
|
||||
byIdentity := make(map[string]model.FirewallRule, len(existing))
|
||||
for _, item := range existing {
|
||||
byIdentity[item.PolicyKey()] = item
|
||||
byIdentity[hostFirewallPolicyKey(item)] = item
|
||||
}
|
||||
for _, item := range rules {
|
||||
identity := item.PolicyKey()
|
||||
identity := hostFirewallPolicyKey(item)
|
||||
if current, exists := byIdentity[identity]; exists {
|
||||
if current.Description == "" && item.Description != "" {
|
||||
if err := tx.Model(&model.FirewallRule{}).Where("uuid = ?", current.UUID).
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/conf"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -61,4 +62,10 @@ func initBaseInfo() {
|
||||
panic(err)
|
||||
}
|
||||
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")
|
||||
baseApi := v2.ApiGroupApp.BaseApi
|
||||
Router.POST("/internal/terminal/sessions/revoke", baseApi.RevokeTerminalSessions)
|
||||
Router.GET("/internal/terminal/capabilities", baseApi.TerminalCapabilities)
|
||||
{
|
||||
hostRouter.POST("", baseApi.CreateHost)
|
||||
hostRouter.POST("/info", baseApi.GetHostByID)
|
||||
@@ -27,7 +28,9 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
|
||||
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
|
||||
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
|
||||
hostRouter.POST("/firewall/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/search", baseApi.SearchForwardingRules)
|
||||
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
|
||||
|
||||
@@ -27,6 +27,7 @@ type CommandHelper struct {
|
||||
outputFile string
|
||||
scriptPath string
|
||||
stdin io.Reader
|
||||
stderr io.Writer
|
||||
env []string
|
||||
timeout time.Duration
|
||||
taskItem *task.Task
|
||||
@@ -360,6 +361,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
}
|
||||
if c.stderr != nil {
|
||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, c.stderr)
|
||||
}
|
||||
env := os.Environ()
|
||||
env = append(env, c.env...)
|
||||
cmd.Env = env
|
||||
@@ -481,6 +485,11 @@ func WithStdin(stdin io.Reader) Option {
|
||||
s.stdin = stdin
|
||||
}
|
||||
}
|
||||
func WithStderr(stderr io.Writer) Option {
|
||||
return func(s *CommandHelper) {
|
||||
s.stderr = stderr
|
||||
}
|
||||
}
|
||||
func WithEnv(env ...string) Option {
|
||||
return func(s *CommandHelper) {
|
||||
s.env = append(s.env, env...)
|
||||
|
||||
@@ -2,6 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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) {
|
||||
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":
|
||||
return handlerErr(run("rc-update", "del", serviceName, "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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -48,7 +49,7 @@ func (s *Snap) IsEnable(serviceName string) bool {
|
||||
|
||||
func (s *Snap) Operate(operate, serviceName string) error {
|
||||
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)
|
||||
}
|
||||
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 fallbackName := systemdAliasFallbackName(serviceName); fallbackName != "" && strings.Contains(out, "alias name or linked unit file") {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
return handlerErr(run(s.toolCmd, operate, serviceName))
|
||||
return handlerErr(out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (s *Sysvinit) Status(serviceName string) (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 {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -28,6 +29,8 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
|
||||
@@ -386,9 +389,23 @@ type DownloadProxyConfig struct {
|
||||
|
||||
type DownloadOptions struct {
|
||||
IgnoreCertificate bool
|
||||
UseServerFilename bool
|
||||
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) {
|
||||
proxyType := strings.TrimSpace(proxy.Type)
|
||||
proxyHost := strings.TrimSpace(proxy.URL)
|
||||
@@ -455,7 +472,7 @@ type downloadPolicy struct {
|
||||
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) {
|
||||
if process.Total > 0 {
|
||||
@@ -492,20 +509,27 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
|
||||
client.CloseIdleConnections()
|
||||
return err
|
||||
}
|
||||
original, err := os.Lstat(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
client.CloseIdleConnections()
|
||||
return err
|
||||
}
|
||||
if original != nil && !original.Mode().IsRegular() {
|
||||
client.CloseIdleConnections()
|
||||
return fmt.Errorf("download target must be a regular file")
|
||||
parent = filepath.Dir(dst)
|
||||
var original os.FileInfo
|
||||
if !options.UseServerFilename {
|
||||
original, err = os.Lstat(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
client.CloseIdleConnections()
|
||||
return err
|
||||
}
|
||||
if original != nil && !original.Mode().IsRegular() {
|
||||
client.CloseIdleConnections()
|
||||
return fmt.Errorf("download target must be a regular file")
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
task := &downloadTask{cancel: cancel, done: make(chan struct{}), dst: dst}
|
||||
if options.UseServerFilename {
|
||||
task.dst = ""
|
||||
}
|
||||
downloadMu.Lock()
|
||||
for _, active := range downloadTasks {
|
||||
if active.dst == dst {
|
||||
if task.dst != "" && active.dst == task.dst {
|
||||
downloadMu.Unlock()
|
||||
cancel()
|
||||
client.CloseIdleConnections()
|
||||
@@ -532,6 +556,34 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
|
||||
close(task.done)
|
||||
}()
|
||||
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) {
|
||||
process.Written = uint64(state.written)
|
||||
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)
|
||||
}
|
||||
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()
|
||||
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,
|
||||
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}
|
||||
for attempt := 0; ; attempt++ {
|
||||
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)
|
||||
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 {
|
||||
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,
|
||||
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)
|
||||
defer cancel()
|
||||
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 {
|
||||
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") {
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(resolveName) > 0 {
|
||||
if _, err := resolveName[0](resp); err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
}
|
||||
progress()
|
||||
timer := time.AfterFunc(idleTimeout, cancel)
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"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/lifecycle"
|
||||
"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 {
|
||||
Run(executable 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
|
||||
}
|
||||
|
||||
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()
|
||||
defer mutationMu.Unlock()
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !m.runner.Exists("iptables-restore") {
|
||||
@@ -160,7 +105,7 @@ func (m *Manager) Initialize(policies []Policy) error {
|
||||
return m.rebuildLocked(policies, inventory)
|
||||
}
|
||||
|
||||
func (m *Manager) Bind() error {
|
||||
func (m *Iptables) Bind() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.bindExistingFamily("iptables", true); err != nil {
|
||||
@@ -174,17 +119,13 @@ func (m *Manager) Bind() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Reconcile(policies []Policy) error {
|
||||
func (m *Iptables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)}
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
executable := executableForFamily(family)
|
||||
@@ -215,7 +156,7 @@ func (m *Manager) ListPolicies() (PolicyInventory, error) {
|
||||
return inventory, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Unbind() error {
|
||||
func (m *Iptables) Unbind() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.unbindFamily("iptables"); err != nil {
|
||||
@@ -229,7 +170,7 @@ func (m *Manager) Unbind() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Cleanup() error {
|
||||
func (m *Iptables) Cleanup() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
for _, executable := range []string{"iptables", "ip6tables"} {
|
||||
@@ -251,7 +192,7 @@ func (m *Manager) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Initialized(family string) (bool, error) {
|
||||
func (m *Iptables) Initialized(family string) (bool, error) {
|
||||
executable := executableForFamily(family)
|
||||
if executable == "" || !m.runner.Exists(executable) {
|
||||
return false, nil
|
||||
@@ -259,7 +200,7 @@ func (m *Manager) Initialized(family string) (bool, error) {
|
||||
return m.chainExists(executable, Chain)
|
||||
}
|
||||
|
||||
func (m *Manager) Status(family string) FamilyStatus {
|
||||
func (m *Iptables) Status(family string) FamilyStatus {
|
||||
executable := executableForFamily(family)
|
||||
if executable == "" || !m.runner.Exists(executable) {
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
|
||||
@@ -275,12 +216,7 @@ func (m *Manager) Status(family string) FamilyStatus {
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
|
||||
}
|
||||
status := FamilyStatus{State: StatusNotEffective, Initialized: true}
|
||||
rules, err := m.run(executable, "-S", DockerChain)
|
||||
if err != nil {
|
||||
status.Reason = ReasonInspectFailed
|
||||
return status
|
||||
}
|
||||
jumps := countJumps(rules)
|
||||
jumps := countJumps(chains)
|
||||
if jumps == 0 {
|
||||
status.Reason = ReasonJumpMissing
|
||||
return status
|
||||
@@ -289,7 +225,7 @@ func (m *Manager) Status(family string) FamilyStatus {
|
||||
status.Reason = ReasonJumpDuplicate
|
||||
return status
|
||||
}
|
||||
if !hasFirstUniqueJump(rules) {
|
||||
if !hasFirstUniqueJump(chains) {
|
||||
status.Reason = ReasonJumpNotFirst
|
||||
return status
|
||||
}
|
||||
@@ -299,7 +235,7 @@ func (m *Manager) Status(family string) FamilyStatus {
|
||||
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 required {
|
||||
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 required {
|
||||
return ErrDockerIptablesChainUnavailable
|
||||
return buserr.New("ErrDockerIptablesChainUnavailable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -325,7 +261,7 @@ func (m *Manager) bindExistingFamily(executable string, required bool) error {
|
||||
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 required {
|
||||
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 required {
|
||||
return ErrDockerIptablesChainUnavailable
|
||||
return buserr.New("ErrDockerIptablesChainUnavailable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -361,7 +297,7 @@ func dockerGuardLifecycleRules(output string, bind, createOwned bool) [][]string
|
||||
return rules
|
||||
}
|
||||
|
||||
func (m *Manager) restoreLifecycle(executable string, rules [][]string) error {
|
||||
func (m *Iptables) restoreLifecycle(executable string, rules [][]string) error {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -379,7 +315,7 @@ func (m *Manager) restoreLifecycle(executable string, rules [][]string) error {
|
||||
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} {
|
||||
executable := executableForFamily(family)
|
||||
if executable == "" || !m.runner.Exists(executable) {
|
||||
@@ -444,7 +380,7 @@ func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInve
|
||||
continue
|
||||
}
|
||||
compiled := compilePolicy(policy)
|
||||
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)]
|
||||
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
for ruleIndex, rule := range compiled {
|
||||
order := int64(0)
|
||||
if ruleIndex < len(orders) {
|
||||
@@ -521,7 +457,7 @@ func compilePolicy(policy Policy) [][]string {
|
||||
return rules
|
||||
}
|
||||
|
||||
func (m *Manager) unbindFamily(executable string) error {
|
||||
func (m *Iptables) unbindFamily(executable string) error {
|
||||
if !m.runner.Exists(executable) {
|
||||
return nil
|
||||
}
|
||||
@@ -535,7 +471,7 @@ func (m *Manager) unbindFamily(executable string) error {
|
||||
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")
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -553,7 +489,7 @@ func chainDeclared(output, chain string) bool {
|
||||
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...)
|
||||
return m.runner.Run(executable, commandArgs...)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package docker_guard
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,23 +19,22 @@ const (
|
||||
dockerNftTable = "docker-bridges"
|
||||
)
|
||||
|
||||
type NftablesManager struct {
|
||||
type Nftables struct {
|
||||
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()
|
||||
defer mutationMu.Unlock()
|
||||
if !m.runner.Exists("nft") {
|
||||
return errors.New("nft is not installed")
|
||||
}
|
||||
if err := m.checkForwardPolicy(); err != nil {
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
if err := m.checkForwardPolicy(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
|
||||
@@ -46,7 +46,7 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
|
||||
return m.rebuildLocked(policies, inventory)
|
||||
}
|
||||
|
||||
func (m *NftablesManager) Bind() error {
|
||||
func (m *Nftables) Bind() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
|
||||
@@ -58,17 +58,13 @@ func (m *NftablesManager) Bind() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NftablesManager) Reconcile(policies []Policy) error {
|
||||
func (m *Nftables) ReplacePolicies(policies []Policy, inventory PolicyInventory) error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.rebuildLocked(policies, inventory)
|
||||
}
|
||||
|
||||
func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
|
||||
func (m *Nftables) ListPolicies() (PolicyInventory, error) {
|
||||
if !m.runner.Exists("nft") {
|
||||
return PolicyInventory{}, nil
|
||||
}
|
||||
@@ -95,7 +91,7 @@ func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
|
||||
return inventory, nil
|
||||
}
|
||||
|
||||
func (m *NftablesManager) Unbind() error {
|
||||
func (m *Nftables) Unbind() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
@@ -106,7 +102,7 @@ func (m *NftablesManager) Unbind() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NftablesManager) Cleanup() error {
|
||||
func (m *Nftables) Cleanup() error {
|
||||
mutationMu.Lock()
|
||||
defer mutationMu.Unlock()
|
||||
if !m.runner.Exists("nft") {
|
||||
@@ -123,7 +119,7 @@ func (m *NftablesManager) Cleanup() error {
|
||||
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") {
|
||||
return false, nil
|
||||
}
|
||||
@@ -134,7 +130,7 @@ func (m *NftablesManager) Initialized(family string) (bool, error) {
|
||||
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)
|
||||
if tableFamily == "" || !m.runner.Exists("nft") {
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
|
||||
@@ -142,16 +138,32 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
|
||||
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
||||
return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing}
|
||||
}
|
||||
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) ||
|
||||
!m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
||||
output, err := m.run("-a", "list", "table", tableFamily, NftTable)
|
||||
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}
|
||||
}
|
||||
status := FamilyStatus{State: StatusNotEffective, Initialized: true}
|
||||
rules, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
||||
if err != nil {
|
||||
status.Reason = ReasonInspectFailed
|
||||
return status
|
||||
}
|
||||
rules := baseRules.String()
|
||||
jumps := nftJumpHandles(rules)
|
||||
if len(jumps) == 0 {
|
||||
status.Reason = ReasonJumpMissing
|
||||
@@ -171,14 +183,14 @@ func (m *NftablesManager) Status(family string) FamilyStatus {
|
||||
return status
|
||||
}
|
||||
|
||||
func (m *NftablesManager) ensureFamily(family string, required bool) error {
|
||||
func (m *Nftables) ensureFamily(family string, required bool) error {
|
||||
tableFamily := nftTableFamily(family)
|
||||
if tableFamily == "" {
|
||||
return fmt.Errorf("unsupported address family %q", family)
|
||||
}
|
||||
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
||||
if required {
|
||||
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family)
|
||||
return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -210,7 +222,7 @@ func (m *NftablesManager) ensureFamily(family string, required bool) error {
|
||||
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)
|
||||
if !m.runner.Exists("nft") {
|
||||
if required {
|
||||
@@ -220,7 +232,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
|
||||
}
|
||||
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
||||
if required {
|
||||
return fmt.Errorf("%w %s", ErrDockerNftablesChainUnavailable, family)
|
||||
return fmt.Errorf("%w %s", buserr.New("ErrDockerNftablesChainUnavailable"), family)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -234,7 +246,7 @@ func (m *NftablesManager) bindExistingFamily(family string, required bool) error
|
||||
return m.ensureJump(family)
|
||||
}
|
||||
|
||||
func (m *NftablesManager) ensureJump(family string) error {
|
||||
func (m *Nftables) ensureJump(family string) error {
|
||||
tableFamily := nftTableFamily(family)
|
||||
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
||||
if err != nil {
|
||||
@@ -248,7 +260,7 @@ func (m *NftablesManager) ensureJump(family string) error {
|
||||
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") {
|
||||
return nil
|
||||
}
|
||||
@@ -309,7 +321,7 @@ func orderedNftRules(family string, policies []Policy, inventory PolicyInventory
|
||||
continue
|
||||
}
|
||||
compiled := compileNftPolicy(policy)
|
||||
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)]
|
||||
orders := inventory.ManagedRuleOrders[policy.Family+"\x00"+policy.UUID]
|
||||
for ruleIndex, rule := range compiled {
|
||||
order := int64(0)
|
||||
if ruleIndex < len(orders) {
|
||||
@@ -405,7 +417,7 @@ func validNftToken(token string) bool {
|
||||
return !strings.ContainsAny(token, " \t\\\"'")
|
||||
}
|
||||
|
||||
func (m *NftablesManager) unbindFamily(family string) error {
|
||||
func (m *Nftables) unbindFamily(family string) error {
|
||||
if !m.runner.Exists("nft") {
|
||||
return nil
|
||||
}
|
||||
@@ -424,7 +436,7 @@ func (m *NftablesManager) unbindFamily(family string) error {
|
||||
return m.runBatch(commands)
|
||||
}
|
||||
|
||||
func (m *NftablesManager) runBatch(commands [][]string) error {
|
||||
func (m *Nftables) runBatch(commands [][]string) error {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -438,13 +450,13 @@ func (m *NftablesManager) runBatch(commands [][]string) error {
|
||||
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...)
|
||||
_, err := m.run(command...)
|
||||
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...)
|
||||
}
|
||||
|
||||
@@ -493,3 +505,40 @@ func nftHasFirstUniqueJump(output string) bool {
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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) {
|
||||
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family))
|
||||
policy.HostIP = strings.TrimSpace(policy.HostIP)
|
||||
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol))
|
||||
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode))
|
||||
if policy.HostPort == 0 ||
|
||||
(policy.Protocol != "tcp" && policy.Protocol != "udp") ||
|
||||
(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 == "" {
|
||||
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
|
||||
}
|
||||
prefix, err := netip.ParsePrefix(source)
|
||||
tokens, err := shellwords.Parse(line)
|
||||
if err != nil {
|
||||
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil {
|
||||
bits := 128
|
||||
if sourceAddress.Is4() {
|
||||
bits = 32
|
||||
}
|
||||
prefix = netip.PrefixFrom(sourceAddress, bits)
|
||||
} else {
|
||||
return Policy{}, fmt.Errorf("%w: invalid source address %q", ErrInvalidPolicy, source)
|
||||
}
|
||||
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
|
||||
}
|
||||
if (policy.Family == FamilyIPv4) != prefix.Addr().Is4() {
|
||||
return Policy{}, fmt.Errorf("%w: source %q does not match address family", ErrInvalidPolicy, source)
|
||||
}
|
||||
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 {
|
||||
managed := strings.Contains(line, "1panel-docker:")
|
||||
if !managed && !hasAcceptAction(tokens) {
|
||||
continue
|
||||
}
|
||||
seen[policyUUID] = struct{}{}
|
||||
uuids = append(uuids, policyUUID)
|
||||
}
|
||||
if len(uuids) == 0 {
|
||||
return nil, fmt.Errorf("%w: policy UUIDs cannot be empty", ErrInvalidPolicy)
|
||||
}
|
||||
return uuids, nil
|
||||
}
|
||||
|
||||
func PolicySyncKey(policy Policy) string {
|
||||
mode := policy.Mode
|
||||
if mode == ModeAllow && len(policy.Sources) == 0 {
|
||||
mode = ModeAll
|
||||
}
|
||||
sources := make([]string, 0, len(policy.Sources))
|
||||
for _, source := range policy.Sources {
|
||||
sources = append(sources, canonicalPolicySource(source))
|
||||
}
|
||||
sort.Strings(sources)
|
||||
return strings.Join([]string{
|
||||
policy.UUID, policy.Family, CanonicalHost(policy.HostIP), strconv.Itoa(int(policy.HostPort)),
|
||||
policy.Protocol, mode, strings.Join(sources, ","),
|
||||
}, "\x00")
|
||||
}
|
||||
|
||||
func canonicalPolicySource(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||
return prefix.Masked().String()
|
||||
}
|
||||
if address, err := netip.ParseAddr(value); err == nil {
|
||||
address = address.Unmap()
|
||||
return netip.PrefixFrom(address, address.BitLen()).String()
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
if address, err := netip.ParseAddr(value); err == nil {
|
||||
return address.String()
|
||||
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 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
|
||||
}
|
||||
|
||||
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 {
|
||||
result := []string{}
|
||||
_ = json.Unmarshal([]byte(value), &result)
|
||||
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,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
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
|
||||
"fmt"
|
||||
"slices"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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 {
|
||||
Family string `json:"family"`
|
||||
Order int64 `json:"order"`
|
||||
@@ -27,9 +92,9 @@ type PolicyInventory struct {
|
||||
}
|
||||
|
||||
type Runtime interface {
|
||||
Initialize([]Policy) error
|
||||
Initialize([]Policy, PolicyInventory) error
|
||||
Bind() error
|
||||
Reconcile([]Policy) error
|
||||
ReplacePolicies([]Policy, PolicyInventory) error
|
||||
Unbind() error
|
||||
Cleanup() error
|
||||
Initialized(string) (bool, error)
|
||||
@@ -37,110 +102,19 @@ type Runtime interface {
|
||||
ListPolicies() (PolicyInventory, error)
|
||||
}
|
||||
|
||||
func NewRuntime(provider string) Runtime {
|
||||
if provider == constant.FirewallProviderNftables {
|
||||
return NewNftablesManager()
|
||||
}
|
||||
return NewManager()
|
||||
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
|
||||
|
||||
func CheckIPv4Forwarding() error {
|
||||
return checkIPv4Forwarding(os.ReadFile)
|
||||
}
|
||||
|
||||
func Verify(runtime Runtime, desired []Policy, preserved []ReadOnlyPolicy) error {
|
||||
inventory, err := runtime.ListPolicies()
|
||||
func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error {
|
||||
value, err := readFile(ipv4ForwardingPath)
|
||||
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) {
|
||||
return fmt.Errorf("verify synchronized Docker firewall policies: target policies do not match the database")
|
||||
}
|
||||
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)
|
||||
}
|
||||
if strings.TrimSpace(string(value)) != "1" {
|
||||
return buserr.New("ErrDockerIPv4ForwardingDisabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
ChangeReorder ChangeOperation = "reorder"
|
||||
)
|
||||
|
||||
type DesiredChange struct {
|
||||
type RuleChange struct {
|
||||
CommandOnly bool `json:"-"`
|
||||
UnmarkedAdopted bool `json:"-"`
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
@@ -39,7 +39,7 @@ type NativeCommand struct {
|
||||
Stdin string `json:"stdin,omitempty"`
|
||||
}
|
||||
|
||||
type NativeRulePlan struct {
|
||||
type RuleCommands struct {
|
||||
RuleUUID string `json:"ruleUUID"`
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
Commands []NativeCommand `json:"commands"`
|
||||
@@ -48,15 +48,14 @@ type NativeRulePlan struct {
|
||||
Expected ObservedRule `json:"expected"`
|
||||
}
|
||||
|
||||
type BackendPlan struct {
|
||||
CommandOnly bool `json:"-"`
|
||||
Provider Provider `json:"provider"`
|
||||
Scope Scope `json:"scope"`
|
||||
SnapshotRevision string `json:"snapshotRevision"`
|
||||
Rules []NativeRulePlan `json:"rules"`
|
||||
type CommandBatch struct {
|
||||
CommandOnly bool `json:"-"`
|
||||
Provider Provider `json:"provider"`
|
||||
Scope Scope `json:"scope"`
|
||||
Rules []RuleCommands `json:"rules"`
|
||||
}
|
||||
|
||||
func (p BackendPlan) CreatesOnly() bool {
|
||||
func (p CommandBatch) CreatesOnly() bool {
|
||||
if len(p.Rules) == 0 {
|
||||
return false
|
||||
}
|
||||
@@ -68,27 +67,17 @@ func (p BackendPlan) CreatesOnly() bool {
|
||||
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 {
|
||||
Provider() Provider
|
||||
Capabilities(context.Context) (Capabilities, error)
|
||||
Observe(context.Context, Scope) (Snapshot, error)
|
||||
Compile(Snapshot, []DesiredChange) (BackendPlan, error)
|
||||
Apply(context.Context, BackendPlan) (ApplyResult, error)
|
||||
Verify(context.Context, BackendPlan) (VerifyResult, error)
|
||||
ListRules(context.Context, Scope) (RuleSet, error)
|
||||
BuildCommands(RuleSet, []RuleChange) (CommandBatch, error)
|
||||
RunCommands(context.Context, CommandBatch) error
|
||||
Rollback(context.Context, CommandBatch) error
|
||||
}
|
||||
|
||||
type MultiScopeObserver interface {
|
||||
ObserveScopes(context.Context, []Scope) ([]Snapshot, error)
|
||||
type MultiScopeReader interface {
|
||||
ListRuleScopes(context.Context, []Scope) ([]RuleSet, error)
|
||||
}
|
||||
|
||||
type RulePreparer interface {
|
||||
@@ -99,7 +88,8 @@ type RuleChecker interface {
|
||||
CheckRule(context.Context, FirewallRule) error
|
||||
}
|
||||
|
||||
type UnverifiedRuleAppender interface {
|
||||
type ExternalRuleAdapter interface {
|
||||
ListRulesByComment(context.Context, []Scope, string) ([]ObservedRule, error)
|
||||
AppendUnverified(context.Context, FirewallRule, string) error
|
||||
}
|
||||
|
||||
@@ -107,6 +97,6 @@ type NativeDetailReader interface {
|
||||
NativeDetail(context.Context, string, bool) (string, error)
|
||||
}
|
||||
|
||||
type PlanRollbacker interface {
|
||||
Rollback(context.Context, BackendPlan) error
|
||||
type RuleSaver interface {
|
||||
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/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -76,80 +75,6 @@ func SameRuleContent(before, after FirewallRule) (bool, error) {
|
||||
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) {
|
||||
identity := ruleIdentity{
|
||||
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})
|
||||
}
|
||||
|
||||
func SnapshotRevision(scope Scope, rules []ObservedRule) (string, error) {
|
||||
func NewRuleSet(scope Scope, rules []ObservedRule) (RuleSet, error) {
|
||||
scope = scope.Normalize()
|
||||
if err := scope.ValidateMVP(); err != nil {
|
||||
return "", err
|
||||
return RuleSet{}, err
|
||||
}
|
||||
|
||||
identities := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
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 {
|
||||
locator, err := validatedLocator(rule.Locator, scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if rule.ParseStatus == ParseStatusSupported {
|
||||
if _, err := NormalizeRule(rule.Rule); err != nil {
|
||||
return RuleSet{}, err
|
||||
}
|
||||
identity, err := opaqueInstanceKey(rule, locator)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
identities = append(identities, identity)
|
||||
continue
|
||||
}
|
||||
|
||||
identity, err := InstanceKey(rule)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if _, err := validatedLocator(rule.Locator, scope); err != nil {
|
||||
return RuleSet{}, err
|
||||
}
|
||||
identities = append(identities, identity)
|
||||
}
|
||||
sort.Strings(identities)
|
||||
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
|
||||
return RuleSet{Scope: scope, Rules: rules}, nil
|
||||
}
|
||||
|
||||
func normalizeLocator(locator Locator, scope Scope) Locator {
|
||||
@@ -306,3 +207,27 @@ func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, er
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
@@ -71,236 +68,3 @@ type InventoryMergeInput struct {
|
||||
Desired []DesiredRule
|
||||
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 {
|
||||
Family string `json:"family"`
|
||||
Port string `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Family string `json:"-"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
}
|
||||
|
||||
type Direction string
|
||||
@@ -108,13 +110,12 @@ type ScopeNotice struct {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidScope = errors.New("invalid 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")
|
||||
ErrProtectedRule = errors.New("protected firewall rule cannot be modified")
|
||||
ErrCompositeRule = errors.New("firewall rule must be atomic")
|
||||
ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
|
||||
ErrInvalidScope = errors.New("invalid firewall scope")
|
||||
ErrUnsupportedScope = errors.New("unsupported firewall scope")
|
||||
ErrInvalidRule = errors.New("invalid firewall rule")
|
||||
ErrProtectedRule = errors.New("protected firewall rule cannot be modified")
|
||||
ErrCompositeRule = errors.New("firewall rule must be atomic")
|
||||
ErrExpansionLimit = errors.New("firewall rule expansion limit exceeded")
|
||||
)
|
||||
|
||||
type Scope struct {
|
||||
@@ -296,11 +297,11 @@ type ObservedRule struct {
|
||||
Persistence PersistenceStatus `json:"persistence,omitempty"`
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Scope Scope `json:"scope"`
|
||||
Revision string `json:"revision"`
|
||||
Rules []ObservedRule `json:"rules"`
|
||||
Notices []ScopeNotice `json:"notices,omitempty"`
|
||||
type RuleSet struct {
|
||||
LastPosition int `json:"-"`
|
||||
Scope Scope `json:"scope"`
|
||||
Rules []ObservedRule `json:"rules"`
|
||||
Notices []ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type Capabilities struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MaxAtomicExpansion = 256
|
||||
const MaxAtomicExpansion = 500
|
||||
|
||||
func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
|
||||
rule.Scope = rule.Scope.Normalize()
|
||||
@@ -16,9 +16,9 @@ func NormalizeRule(rule FirewallRule) (FirewallRule, error) {
|
||||
return FirewallRule{}, err
|
||||
}
|
||||
|
||||
if hasCompositeValue(rule.SourceAddress) || hasCompositeValue(rule.DestinationAddress) ||
|
||||
hasCompositeValue(rule.SourcePort) ||
|
||||
(hasCompositeValue(rule.DestinationPort) && !supportsNativeDestinationPortSet(rule.Scope.Provider)) ||
|
||||
if strings.Contains(rule.SourceAddress, ",") || strings.Contains(rule.DestinationAddress, ",") ||
|
||||
strings.Contains(rule.SourcePort, ",") ||
|
||||
(strings.Contains(rule.DestinationPort, ",") && rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderUFW) ||
|
||||
isCompositeProtocol(rule.Protocol) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return FirewallRule{}, fmt.Errorf("%w: destination port: %v", ErrInvalidRule, err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func ExpandAtomicRules(input FirewallRule) ([]FirewallRule, error) {
|
||||
destinationAddresses := splitValues(input.DestinationAddress)
|
||||
sourcePorts := splitValues(input.SourcePort)
|
||||
destinationPorts := splitValues(input.DestinationPort)
|
||||
if supportsNativeDestinationPortSet(input.Scope.Provider) {
|
||||
if input.Scope.Provider == ProviderIptables || input.Scope.Provider == ProviderUFW {
|
||||
destinationPorts = []string{input.DestinationPort}
|
||||
}
|
||||
|
||||
@@ -272,10 +272,6 @@ func validateAddressFamily(address netip.Addr, family Family) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizePort(value string) (string, error) {
|
||||
return normalizePortValue(value, false)
|
||||
}
|
||||
|
||||
func normalizePortValue(value string, allowSet bool) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
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
|
||||
}
|
||||
|
||||
func supportsNativeDestinationPortSet(provider Provider) bool {
|
||||
return provider == ProviderIptables || provider == ProviderUFW
|
||||
}
|
||||
|
||||
func parsePort(value string) (int, error) {
|
||||
port, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
@@ -404,10 +396,6 @@ func normalizeConnectionStates(values []string, provider Provider) ([]string, er
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func hasCompositeValue(value string) bool {
|
||||
return strings.Contains(value, ",")
|
||||
}
|
||||
|
||||
func isCompositeProtocol(value string) bool {
|
||||
value = strings.ToLower(strings.TrimSpace(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"
|
||||
)
|
||||
|
||||
var ErrAlreadyEnabled = errors.New("firewalld rule already exists")
|
||||
|
||||
type CommandReader interface {
|
||||
Read(context.Context, ...string) (string, error)
|
||||
}
|
||||
@@ -114,17 +116,17 @@ func parseFirewalldVersion(output string) (int, int, error) {
|
||||
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()
|
||||
if err := scope.ValidateMVP(); err != nil {
|
||||
return filter.Snapshot{}, err
|
||||
return filter.RuleSet{}, err
|
||||
}
|
||||
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
|
||||
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
|
||||
@@ -141,15 +143,15 @@ func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snaps
|
||||
}()
|
||||
reads.Wait()
|
||||
if err := errors.Join(runtimeErr, permanentErr); err != nil {
|
||||
return filter.Snapshot{}, err
|
||||
return filter.RuleSet{}, err
|
||||
}
|
||||
rules, err := mergeZoneObjects(scope, runtime, permanent)
|
||||
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 {
|
||||
return filter.Snapshot{}, err
|
||||
return filter.RuleSet{}, err
|
||||
}
|
||||
snapshot.Notices = publicZoneNotices(runtime, permanent)
|
||||
return snapshot, nil
|
||||
@@ -186,135 +188,144 @@ func (a *Adapter) PrepareRule(rule filter.FirewallRule) (filter.FirewallRule, er
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) {
|
||||
if snapshot.Revision == "" {
|
||||
return filter.BackendPlan{}, filter.ErrRuleStale
|
||||
}
|
||||
func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
||||
if err := validateFirewalldScope(snapshot.Scope); err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
if len(changes) != 1 {
|
||||
return filter.BackendPlan{}, fmt.Errorf("%w: firewalld plans currently require exactly one change", filter.ErrInvalidRule)
|
||||
if len(changes) == 0 || len(changes) > filter.MaxAtomicExpansion {
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: invalid firewalld batch size", filter.ErrInvalidRule)
|
||||
}
|
||||
rulePlan, err := a.compileChange(snapshot, changes[0])
|
||||
if err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
plan := filter.CommandBatch{Provider: filter.ProviderFirewalld, Scope: snapshot.Scope}
|
||||
for _, change := range changes {
|
||||
rulePlan, err := a.compileChange(snapshot, change)
|
||||
if err != nil {
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
plan.Rules = append(plan.Rules, rulePlan)
|
||||
}
|
||||
return filter.BackendPlan{
|
||||
Provider: filter.ProviderFirewalld, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
|
||||
Rules: []filter.NativeRulePlan{rulePlan},
|
||||
}, nil
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.ApplyResult, error) {
|
||||
if plan.Provider != filter.ProviderFirewalld || len(plan.Rules) != 1 {
|
||||
return filter.ApplyResult{}, fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule)
|
||||
func (a *Adapter) RunCommands(ctx context.Context, plan filter.CommandBatch) error {
|
||||
commands, err := batchCommands(plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewalldScope(plan.Scope); err != nil {
|
||||
return filter.ApplyResult{}, err
|
||||
if len(commands.Commands) != 0 && a.writer == nil {
|
||||
return errors.New("firewalld writer is required")
|
||||
}
|
||||
rulePlan := plan.Rules[0]
|
||||
if len(rulePlan.Commands) != 0 && a.writer == nil {
|
||||
return filter.ApplyResult{}, errors.New("firewalld writer is required")
|
||||
}
|
||||
if len(rulePlan.Commands) != len(rulePlan.RollbackCommands) {
|
||||
return filter.ApplyResult{}, fmt.Errorf("%w: incomplete firewalld rollback plan", filter.ErrInvalidRule)
|
||||
}
|
||||
for _, command := range append(append([]filter.NativeCommand(nil), rulePlan.Commands...), rulePlan.RollbackCommands...) {
|
||||
if err := validateScopeCommand(plan.Scope, command); err != nil {
|
||||
return filter.ApplyResult{}, err
|
||||
executed, alreadyEnabled := 0, 0
|
||||
for index, command := range commands.Commands {
|
||||
err := a.writer.Run(ctx, command)
|
||||
if errors.Is(err, ErrAlreadyEnabled) {
|
||||
alreadyEnabled++
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
executed := 0
|
||||
for index, command := range rulePlan.Commands {
|
||||
if err := a.writer.Run(ctx, command); err != nil {
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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) {
|
||||
if plan.Provider != filter.ProviderFirewalld || len(plan.Rules) != 1 {
|
||||
return filter.VerifyResult{}, fmt.Errorf("%w: invalid firewalld backend plan", filter.ErrInvalidRule)
|
||||
}
|
||||
snapshot, err := a.Observe(ctx, plan.Scope)
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
commands, err := batchCommands(plan)
|
||||
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
|
||||
}
|
||||
if a.writer == nil {
|
||||
return errors.New("firewalld writer is required")
|
||||
}
|
||||
for ruleIndex := len(plan.Rules) - 1; ruleIndex >= 0; ruleIndex-- {
|
||||
rulePlan := plan.Rules[ruleIndex]
|
||||
if err := a.rollback(ctx, plan.Scope, rulePlan, len(rulePlan.RollbackCommands)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return a.rollback(ctx, plan.Scope, commands, len(commands.RollbackCommands))
|
||||
}
|
||||
|
||||
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
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
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 == "" {
|
||||
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)
|
||||
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 {
|
||||
case filter.ChangeCreate:
|
||||
plan.Commands, plan.RollbackCommands = missingRuleCommands(snapshot, normalized)
|
||||
plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
|
||||
case filter.ChangeAdopt:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, false)
|
||||
if targetErr != nil {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
plan.Previous = &target
|
||||
plan.Expected = target
|
||||
@@ -322,35 +333,31 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC
|
||||
case filter.ChangeUpdate:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
|
||||
if targetErr != nil {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
plan.Previous = &target
|
||||
if target.Locator.Canonical == expected.Locator.Canonical {
|
||||
break
|
||||
}
|
||||
removeCommands, restoreCommands := observedPairedCommands(target, "remove", "add")
|
||||
addCommands, removeNewCommands := missingRuleCommands(snapshot, normalized)
|
||||
removeCommands, restoreCommands := observedRuleCommands(target, "remove", "add")
|
||||
addCommands, removeNewCommands := ruleCommands(nativeOption(normalized, "add"), nativeOption(normalized, "remove"))
|
||||
plan.Commands = append(removeCommands, addCommands...)
|
||||
plan.RollbackCommands = append(restoreCommands, removeNewCommands...)
|
||||
case filter.ChangeDelete:
|
||||
if change.CommandOnly && change.Locator == nil {
|
||||
plan.Commands, plan.RollbackCommands = ruleCommands(nativeOption(normalized, "remove"), nativeOption(normalized, "add"))
|
||||
break
|
||||
}
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, true)
|
||||
if targetErr != nil {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
plan.Previous = &target
|
||||
plan.Expected = target
|
||||
plan.Expected.Rule.UUID = normalized.UUID
|
||||
plan.Commands, plan.RollbackCommands = observedPairedCommands(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:]
|
||||
}
|
||||
}
|
||||
plan.Commands, plan.RollbackCommands = observedRuleCommands(target, "remove", "add")
|
||||
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
|
||||
}
|
||||
@@ -407,48 +414,22 @@ func nativeCanonical(rule filter.FirewallRule) string {
|
||||
return "rich:" + canonicalRichRule(rule)
|
||||
}
|
||||
|
||||
func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]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) {
|
||||
func observedRuleCommands(observed filter.ObservedRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
|
||||
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,
|
||||
"--"+inverse+"-rich-rule="+observed.Raw)
|
||||
}
|
||||
|
||||
func pairedNativeCommands(scope filter.Scope, option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) {
|
||||
selector := scopeSelector(scope)
|
||||
func ruleCommands(option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) {
|
||||
selector := "--zone=" + filter.FirewalldInputZone
|
||||
commands := []filter.NativeCommand{
|
||||
{Executable: "firewall-cmd", Args: []string{selector, option}},
|
||||
{Executable: "firewall-cmd", Args: []string{"--permanent", selector, option}},
|
||||
}
|
||||
rollbackCommands := []filter.NativeCommand{
|
||||
{Executable: "firewall-cmd", Args: []string{selector, rollback}},
|
||||
{Executable: "firewall-cmd", Args: []string{"--permanent", selector, rollback}},
|
||||
}
|
||||
return commands, rollbackCommands
|
||||
}
|
||||
@@ -460,7 +441,7 @@ func nativeOption(rule filter.FirewallRule, operation string) string {
|
||||
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 == "" {
|
||||
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 {
|
||||
return filter.ObservedRule{}, filter.ErrProtectedRule
|
||||
}
|
||||
if target.ParseStatus != filter.ParseStatusSupported ||
|
||||
(target.Persistence != filter.PersistenceStatusConverged && !(change.CommandOnly && change.Operation == filter.ChangeDelete)) {
|
||||
if target.ParseStatus != filter.ParseStatusSupported {
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
want := normalized
|
||||
@@ -507,30 +487,37 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
|
||||
}
|
||||
|
||||
func validateScopeCommand(scope filter.Scope, command filter.NativeCommand) error {
|
||||
if command.Executable != "firewall-cmd" {
|
||||
return fmt.Errorf("%w: unexpected firewalld executable %q", filter.ErrInvalidRule, command.Executable)
|
||||
if err := validateFirewalldScope(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := scopeSelector(scope)
|
||||
foundSelector := false
|
||||
if command.Executable != "firewall-cmd" || command.Stdin != "" {
|
||||
return fmt.Errorf("%w: invalid firewalld command", filter.ErrInvalidRule)
|
||||
}
|
||||
expected := "--zone=" + filter.FirewalldInputZone
|
||||
foundSelector, options := false, 0
|
||||
for _, arg := range command.Args {
|
||||
if arg == expected {
|
||||
switch {
|
||||
case arg == expected:
|
||||
foundSelector = true
|
||||
}
|
||||
if (strings.HasPrefix(arg, "--zone=") || strings.HasPrefix(arg, "--policy=")) && arg != expected {
|
||||
return fmt.Errorf("%w: firewalld command targets another scope", filter.ErrUnsupportedScope)
|
||||
case arg == "--permanent":
|
||||
case strings.HasPrefix(arg, "--add-port="), strings.HasPrefix(arg, "--remove-port="),
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
func scopeSelector(scope filter.Scope) string {
|
||||
return "--zone=" + filter.FirewalldInputZone
|
||||
}
|
||||
|
||||
func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error {
|
||||
func (a *Adapter) compensate(ctx context.Context, plan filter.RuleCommands, executed int, cause error) error {
|
||||
if plan.Operation == filter.ChangeCreate {
|
||||
return cause
|
||||
}
|
||||
@@ -543,7 +530,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, ex
|
||||
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
|
||||
for index := executed - 1; index >= 0; index-- {
|
||||
if index >= len(plan.RollbackCommands) {
|
||||
@@ -559,26 +546,13 @@ func (a *Adapter) rollback(ctx context.Context, scope filter.Scope, plan filter.
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
ports string
|
||||
rich string
|
||||
@@ -590,7 +564,7 @@ func (a *Adapter) readScope(ctx context.Context, scope filter.Scope, permanent b
|
||||
if 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...)
|
||||
if err != nil {
|
||||
return zoneOutput{}, err
|
||||
@@ -1075,9 +1049,46 @@ func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) erro
|
||||
if err := validateSystemCommand(command); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.NewCommandMgr(
|
||||
cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LANGUAGE=en_US:en"),
|
||||
options, removals, alreadyEnabled := 0, 0, 0
|
||||
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...)
|
||||
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 {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
@@ -17,140 +16,187 @@ import (
|
||||
)
|
||||
|
||||
type RuleReader interface {
|
||||
filter.CommentRuleReader
|
||||
ListChain(context.Context, filter.Scope) (string, error)
|
||||
}
|
||||
|
||||
type TableReader interface {
|
||||
ListTable(context.Context, filter.Scope) (string, error)
|
||||
}
|
||||
|
||||
type RuleWriter interface {
|
||||
Run(context.Context, filter.NativeCommand) error
|
||||
Save(context.Context, filter.Scope) error
|
||||
}
|
||||
|
||||
type MultiportChecker interface {
|
||||
CheckMultiport(context.Context, filter.Family) error
|
||||
}
|
||||
|
||||
type Adapter struct {
|
||||
reader RuleReader
|
||||
writer RuleWriter
|
||||
checker MultiportChecker
|
||||
multiportMu sync.Mutex
|
||||
multiportOK map[filter.Family]bool
|
||||
reader RuleReader
|
||||
writer RuleWriter
|
||||
}
|
||||
|
||||
func NewAdapter() *Adapter {
|
||||
backend := systemBackend{}
|
||||
return &Adapter{reader: backend, writer: backend, checker: backend}
|
||||
return &Adapter{reader: backend, writer: backend}
|
||||
}
|
||||
|
||||
func NewAdapterWithReader(reader RuleReader) *Adapter {
|
||||
adapter := &Adapter{reader: reader}
|
||||
adapter.checker, _ = reader.(MultiportChecker)
|
||||
return adapter
|
||||
return &Adapter{reader: reader}
|
||||
}
|
||||
|
||||
func NewAdapterWithBackend(reader RuleReader, writer RuleWriter) *Adapter {
|
||||
adapter := &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
|
||||
return &Adapter{reader: reader, writer: writer}
|
||||
}
|
||||
|
||||
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) {
|
||||
return filter.Capabilities{
|
||||
Marker: true, OwnedChains: true, ExplicitPosition: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) {
|
||||
scope = scope.Normalize()
|
||||
if err := scope.ValidateMVP(); err != nil {
|
||||
return filter.Snapshot{}, err
|
||||
func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
|
||||
rule, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if scope.Provider != filter.ProviderIptables {
|
||||
return filter.Snapshot{}, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, scope.Key())
|
||||
if err := validateAdapterScope(rule.Scope); err != nil {
|
||||
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 {
|
||||
return filter.Snapshot{}, fmt.Errorf("iptables reader is required")
|
||||
return nil, fmt.Errorf("iptables reader is required")
|
||||
}
|
||||
output, err := a.reader.ListChain(ctx, scope)
|
||||
if err != nil {
|
||||
if errors.Is(err, filter.ErrProviderUnavailable) {
|
||||
snapshot, snapshotErr := filter.NewSnapshot(scope, nil)
|
||||
if snapshotErr != nil {
|
||||
return filter.Snapshot{}, snapshotErr
|
||||
}
|
||||
snapshot.Notices = []filter.ScopeNotice{{
|
||||
Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain},
|
||||
}}
|
||||
return snapshot, nil
|
||||
tableReader, readsTable := a.reader.(TableReader)
|
||||
snapshots := make([]filter.RuleSet, len(scopes))
|
||||
for index, scope := range normalized {
|
||||
if snapshots[index].Scope.Provider != "" {
|
||||
continue
|
||||
}
|
||||
var output string
|
||||
var err error
|
||||
if readsTable {
|
||||
output, err = tableReader.ListTable(ctx, scope)
|
||||
} 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 filter.NewSnapshot(scope, rules)
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) {
|
||||
if snapshot.Revision == "" {
|
||||
return filter.BackendPlan{}, filter.ErrRuleStale
|
||||
}
|
||||
func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
||||
snapshot.Scope = snapshot.Scope.Normalize()
|
||||
if err := validateAdapterScope(snapshot.Scope); err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
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 {
|
||||
return filter.ApplyResult{}, fmt.Errorf("iptables writer is required")
|
||||
return fmt.Errorf("iptables writer is required")
|
||||
}
|
||||
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 {
|
||||
return filter.ApplyResult{}, err
|
||||
return err
|
||||
}
|
||||
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 _, command := range rulePlan.Commands {
|
||||
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 {
|
||||
if err := a.writer.Run(ctx, command); err != nil {
|
||||
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++
|
||||
}
|
||||
}
|
||||
if err := a.writer.Save(ctx, plan.Scope); err != nil {
|
||||
if plan.CommandOnly {
|
||||
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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileBatch(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) {
|
||||
plan := filter.BackendPlan{
|
||||
Provider: filter.ProviderIptables, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
|
||||
Rules: make([]filter.NativeRulePlan, 0, len(changes)),
|
||||
}
|
||||
current := snapshot
|
||||
func compileDeleteBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
||||
plan := filter.CommandBatch{Provider: filter.ProviderIptables, Scope: snapshot.Scope, CommandOnly: true}
|
||||
var script strings.Builder
|
||||
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
|
||||
for _, change := range changes {
|
||||
rulePlan, err := compileChange(current, change)
|
||||
rulePlan, err := compileChange(snapshot, change)
|
||||
if err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
rulePlan.Commands = nil
|
||||
rulePlan.RollbackCommands = nil
|
||||
line, err := restoreRuleLine(snapshot.Scope, *rulePlan.Previous)
|
||||
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)
|
||||
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{{
|
||||
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: applyScript,
|
||||
}}
|
||||
plan.Rules[0].RollbackCommands = []filter.NativeCommand{{
|
||||
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: rollbackScript,
|
||||
Executable: restoreExecutableForFamily(snapshot.Scope.Family), Args: []string{"--noflush", "--wait"}, Stdin: script.String(),
|
||||
}}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func applyRestoreRulePlan(snapshot filter.Snapshot, plan filter.NativeRulePlan) (filter.Snapshot, error) {
|
||||
position := plan.Expected.Locator.Position
|
||||
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) {
|
||||
func compileCreateBatch(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
||||
plan := filter.CommandBatch{Provider: filter.ProviderIptables, Scope: snapshot.Scope, CommandOnly: true}
|
||||
var script strings.Builder
|
||||
script.WriteByte('*')
|
||||
script.WriteString(scope.Table)
|
||||
script.WriteByte('\n')
|
||||
script.WriteString("-F ")
|
||||
script.WriteString(scope.Chain)
|
||||
script.WriteByte('\n')
|
||||
for _, observed := range rules {
|
||||
line, err := restoreRuleLine(scope, observed)
|
||||
fmt.Fprintf(&script, "*%s\n", snapshot.Scope.Table)
|
||||
for _, change := range changes {
|
||||
rulePlan, err := compileChange(snapshot, change)
|
||||
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.WriteByte('\n')
|
||||
rulePlan.Commands, rulePlan.RollbackCommands = nil, nil
|
||||
plan.Rules = append(plan.Rules, rulePlan)
|
||||
}
|
||||
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) {
|
||||
@@ -309,50 +290,7 @@ func restoreRuleLine(scope filter.Scope, observed filter.ObservedRule) (string,
|
||||
return strings.Join(tokens, " "), nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, 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 {
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
if a.writer == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
return cause
|
||||
}
|
||||
@@ -381,7 +319,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, lastR
|
||||
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
|
||||
for index := lastRule; index >= 0; index-- {
|
||||
commands := plan.Rules[index].RollbackCommands
|
||||
@@ -418,53 +356,65 @@ func validateAdapterScope(scope filter.Scope) error {
|
||||
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
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
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 == "" {
|
||||
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") ||
|
||||
(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
|
||||
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
|
||||
previous := filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported}
|
||||
var commands []filter.NativeCommand
|
||||
if change.UnmarkedAdopted || change.PreviousMarker != "" {
|
||||
previous.Marker = change.PreviousMarker
|
||||
args := []string{"-w", "-t", snapshot.Scope.Table, "-D", snapshot.Scope.Chain}
|
||||
args = append(args, compileObservedRuleArgs(previous)...)
|
||||
commands = []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}}
|
||||
}
|
||||
return filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Previous: &previous, Expected: previous, Commands: commands}, nil
|
||||
}
|
||||
position := len(snapshot.Rules) + 1
|
||||
verb := "-I"
|
||||
var target filter.ObservedRule
|
||||
switch change.Operation {
|
||||
case filter.ChangeCreate:
|
||||
if normalized.OrderIndex != nil && (*normalized.OrderIndex < 1 || *normalized.OrderIndex > int64(len(snapshot.Rules)+1)) {
|
||||
return filter.NativeRulePlan{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
|
||||
hasSnapshot := !change.CommandOnly || snapshot.Rules != nil
|
||||
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)
|
||||
case filter.ChangeAdopt:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
verb = "-R"
|
||||
case filter.ChangeUpdate:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
targetPosition := position
|
||||
if normalized.OrderIndex != nil {
|
||||
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)
|
||||
}
|
||||
@@ -475,21 +425,21 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
case filter.ChangeDelete:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
verb = "-D"
|
||||
case filter.ChangeReorder:
|
||||
position, target, err = validateMutationTarget(snapshot, change, normalized, marker)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
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)
|
||||
return positionalMutationPlan(snapshot, normalized, target, marker, position, targetPosition, change.Operation), nil
|
||||
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)}
|
||||
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,
|
||||
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,
|
||||
Commands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: args}},
|
||||
RollbackCommands: []filter.NativeCommand{{Executable: executableForFamily(snapshot.Scope.Family), Args: rollbackArgs}},
|
||||
Previous: pointerToObserved(target, change.Operation != filter.ChangeCreate),
|
||||
Previous: previous,
|
||||
Expected: expected,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func positionalMutationPlan(
|
||||
snapshot filter.Snapshot,
|
||||
rule filter.FirewallRule,
|
||||
previous filter.ObservedRule,
|
||||
marker string,
|
||||
position int,
|
||||
targetPosition int,
|
||||
operation filter.ChangeOperation,
|
||||
) filter.NativeRulePlan {
|
||||
func positionalMutationPlan(snapshot filter.RuleSet, rule filter.FirewallRule, previous filter.ObservedRule, marker string, position int, targetPosition int, operation filter.ChangeOperation) filter.RuleCommands {
|
||||
expected := filter.ObservedRule{
|
||||
Rule: rule, Marker: marker, ParseStatus: filter.ParseStatusSupported,
|
||||
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,
|
||||
}
|
||||
if position == targetPosition {
|
||||
return plan
|
||||
}
|
||||
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 = append(insertArgs, compileRuleArgs(rule, marker)...)
|
||||
restoreArgs := []string{"-w", "-t", snapshot.Scope.Table, "-I", snapshot.Scope.Chain, strconv.Itoa(position)}
|
||||
@@ -558,13 +504,6 @@ func positionalMutationPlan(
|
||||
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 {
|
||||
args := make([]string, 0, 24)
|
||||
if rule.Protocol != "all" {
|
||||
@@ -585,16 +524,16 @@ func compileRuleArgs(rule filter.FirewallRule, marker string) []string {
|
||||
}
|
||||
if 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 {
|
||||
args = append(args, "--sport", nativePort(rule.SourcePort))
|
||||
args = append(args, "--sport", strings.ReplaceAll(rule.SourcePort, "-", ":"))
|
||||
}
|
||||
}
|
||||
if 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 {
|
||||
args = append(args, "--dport", nativePort(rule.DestinationPort))
|
||||
args = append(args, "--dport", strings.ReplaceAll(rule.DestinationPort, "-", ":"))
|
||||
}
|
||||
}
|
||||
if len(rule.ConnectionStates) != 0 {
|
||||
@@ -615,11 +554,7 @@ func compileObservedRuleArgs(observed filter.ObservedRule) []string {
|
||||
return compileRuleArgs(observed.Rule, comment)
|
||||
}
|
||||
|
||||
func nativePort(port string) string {
|
||||
return strings.ReplaceAll(port, "-", ":")
|
||||
}
|
||||
|
||||
func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChange, after filter.FirewallRule, marker string) (int, filter.ObservedRule, error) {
|
||||
func validateMutationTarget(snapshot filter.RuleSet, change filter.RuleChange, after filter.FirewallRule, marker string) (int, filter.ObservedRule, error) {
|
||||
if change.Locator == nil || change.Locator.Position == nil {
|
||||
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
|
||||
}
|
||||
|
||||
func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int {
|
||||
func insertionPosition(snapshot filter.RuleSet, rule filter.FirewallRule) int {
|
||||
if rule.OrderIndex != nil {
|
||||
return int(*rule.OrderIndex)
|
||||
}
|
||||
@@ -677,16 +612,20 @@ func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int {
|
||||
|
||||
type systemBackend struct{}
|
||||
|
||||
func (systemBackend) CheckMultiport(ctx context.Context, family filter.Family) error {
|
||||
executable, err := runtimeExecutableForFamily(family)
|
||||
func (systemBackend) ReadRulesByComment(ctx context.Context, scope filter.Scope, comment string) (string, error) {
|
||||
executable, err := runtimeExecutable(executableForFamily(scope.Family))
|
||||
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) {
|
||||
output, err := native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
|
||||
func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
@@ -752,10 +691,6 @@ func restoreExecutableForFamily(family filter.Family) string {
|
||||
return "iptables-restore"
|
||||
}
|
||||
|
||||
func runtimeExecutableForFamily(family filter.Family) (string, error) {
|
||||
return runtimeExecutable(executableForFamily(family))
|
||||
}
|
||||
|
||||
func runtimeExecutable(logical string) (string, error) {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
@@ -909,7 +844,7 @@ func parseRule(scope filter.Scope, raw string, position int) filter.ObservedRule
|
||||
}
|
||||
return filter.ObservedRule{
|
||||
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"
|
||||
}
|
||||
|
||||
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 {
|
||||
if *index+1 >= len(args) {
|
||||
return false
|
||||
@@ -951,3 +863,7 @@ func takeValue(args []string, index *int, target *string) bool {
|
||||
*target = args[*index]
|
||||
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 {
|
||||
filter.CommentRuleReader
|
||||
ListChain(context.Context, filter.Scope) (string, error)
|
||||
Run(context.Context, filter.NativeCommand) error
|
||||
Save(context.Context) error
|
||||
}
|
||||
|
||||
type TableReader interface {
|
||||
ListTable(context.Context, filter.Scope) (string, bool, error)
|
||||
}
|
||||
|
||||
type Adapter struct{ backend Backend }
|
||||
|
||||
func NewAdapter() *Adapter { return &Adapter{backend: systemBackend{}} }
|
||||
@@ -34,19 +39,47 @@ func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
|
||||
}, 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()
|
||||
if err := validateScope(scope); err != nil {
|
||||
return filter.Snapshot{}, err
|
||||
return filter.RuleSet{}, err
|
||||
}
|
||||
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)
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) {
|
||||
snapshot, snapshotErr := filter.NewSnapshot(scope, nil)
|
||||
snapshot, snapshotErr := filter.NewRuleSet(scope, nil)
|
||||
if snapshotErr != nil {
|
||||
return filter.Snapshot{}, snapshotErr
|
||||
return filter.RuleSet{}, snapshotErr
|
||||
}
|
||||
snapshot.Notices = []filter.ScopeNotice{{
|
||||
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
|
||||
}
|
||||
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) {
|
||||
if snapshot.Revision == "" {
|
||||
return filter.BackendPlan{}, filter.ErrRuleStale
|
||||
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 := 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 {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return filter.BackendPlan{}, 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)),
|
||||
return filter.CommandBatch{}, fmt.Errorf("%w: nftables plan requires at least one change", filter.ErrInvalidRule)
|
||||
}
|
||||
createOnly, deleteOnly := true, true
|
||||
for _, change := range changes {
|
||||
if len(changes) > 1 && change.Operation != operation {
|
||||
return filter.BackendPlan{}, fmt.Errorf("%w: nftables batch plan operations must be homogeneous", filter.ErrInvalidRule)
|
||||
createOnly = createOnly && change.Operation == filter.ChangeCreate && change.CommandOnly
|
||||
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 {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
plan.Rules = append(plan.Rules, filter.NativeRulePlan{
|
||||
RuleUUID: ruleUUID(change), Operation: change.Operation, Previous: previous, Expected: expected,
|
||||
handle := previous.Locator.NativeID
|
||||
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)
|
||||
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}
|
||||
plan.Rules[0].Commands = []filter.NativeCommand{{Executable: "nft", Stdin: script.String()}}
|
||||
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 {
|
||||
return filter.ApplyResult{}, err
|
||||
return err
|
||||
}
|
||||
for _, rulePlan := range plan.Rules {
|
||||
for _, command := range rulePlan.Commands {
|
||||
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 {
|
||||
if err := a.backend.Run(ctx, command); err != nil {
|
||||
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 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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, 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 {
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
if err := validatePlan(plan); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,7 +308,7 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
|
||||
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() {
|
||||
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 {
|
||||
return fmt.Errorf("%w: invalid nftables plan", filter.ErrInvalidRule)
|
||||
}
|
||||
@@ -244,66 +358,42 @@ func validateNativeCommand(command filter.NativeCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyChange(snapshot filter.Snapshot, change filter.DesiredChange) ([]filter.ObservedRule, filter.ObservedRule, *filter.ObservedRule, error) {
|
||||
rules := append([]filter.ObservedRule(nil), snapshot.Rules...)
|
||||
func compileChange(snapshot filter.RuleSet, change filter.RuleChange) (filter.ObservedRule, *filter.ObservedRule, error) {
|
||||
rule := change.After
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, filter.ObservedRule{}, nil, err
|
||||
return filter.ObservedRule{}, nil, err
|
||||
}
|
||||
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
|
||||
marker := "1panel-rule:" + normalized.UUID
|
||||
var previous *filter.ObservedRule
|
||||
|
||||
if change.Operation != filter.ChangeCreate {
|
||||
if change.Locator == nil || change.Locator.Position == nil {
|
||||
return nil, filter.ObservedRule{}, nil, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule)
|
||||
}
|
||||
position = *change.Locator.Position
|
||||
if position < 1 || position > len(rules) {
|
||||
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:]...)
|
||||
if change.Locator == nil || change.Locator.Position == nil {
|
||||
return filter.ObservedRule{}, nil, fmt.Errorf("%w: mutation requires a position locator", filter.ErrInvalidRule)
|
||||
}
|
||||
position := *change.Locator.Position
|
||||
if position < 1 || position > len(snapshot.Rules) {
|
||||
return filter.ObservedRule{}, nil, filter.ErrRuleStale
|
||||
}
|
||||
previous := snapshot.Rules[position-1]
|
||||
if previous.Protected {
|
||||
return filter.ObservedRule{}, nil, filter.ErrProtectedRule
|
||||
}
|
||||
|
||||
target := position
|
||||
if normalized.OrderIndex != nil {
|
||||
if normalized.OrderIndex != nil && (change.Operation == filter.ChangeUpdate || change.Operation == filter.ChangeReorder) {
|
||||
target = int(*normalized.OrderIndex)
|
||||
}
|
||||
if change.Operation == filter.ChangeCreate && normalized.OrderIndex == nil {
|
||||
target = len(rules) + 1
|
||||
}
|
||||
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)
|
||||
if target < 1 || target > len(snapshot.Rules) {
|
||||
return 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), " "))
|
||||
rules = append(rules, filter.ObservedRule{})
|
||||
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
|
||||
return expected, &previous, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return change.After.UUID
|
||||
}
|
||||
@@ -323,39 +413,6 @@ func ruleUUID(change filter.DesiredChange) string {
|
||||
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 {
|
||||
parts := make([]string, 0, 24)
|
||||
if rule.Protocol != "all" {
|
||||
@@ -416,7 +473,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
|
||||
return filter.ObservedRule{
|
||||
Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque},
|
||||
Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw,
|
||||
Protected: scope.Chain != filter.IptablesInputChain,
|
||||
Protected: scope.Chain == filter.BasicAfterChain,
|
||||
}
|
||||
}
|
||||
tokens, err := shellwords.Parse(raw)
|
||||
@@ -533,7 +590,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
|
||||
if err != nil {
|
||||
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 {
|
||||
@@ -582,6 +639,10 @@ func numericSymbol(value string) string {
|
||||
|
||||
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) {
|
||||
run := func(args ...string) (string, error) {
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
if command.Executable != "nft" {
|
||||
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 {
|
||||
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 {
|
||||
filter.CommentRuleReader
|
||||
Read(context.Context, ...string) (string, error)
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule
|
||||
}
|
||||
comment = strings.TrimSpace(comment)
|
||||
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)
|
||||
if err := validateCommand(command); err != nil {
|
||||
@@ -85,15 +86,35 @@ func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snapshot, error) {
|
||||
snapshots, err := a.ObserveScopes(ctx, []filter.Scope{scope})
|
||||
func (a *Adapter) ListRulesByComment(ctx context.Context, scopes []filter.Scope, comment string) ([]filter.ObservedRule, error) {
|
||||
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 {
|
||||
return filter.Snapshot{}, err
|
||||
return filter.RuleSet{}, err
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
snapshots := make([]filter.Snapshot, 0, len(normalizedScopes))
|
||||
snapshots := make([]filter.RuleSet, 0, len(normalizedScopes))
|
||||
for _, scope := range normalizedScopes {
|
||||
snapshot, err := filter.NewSnapshot(scope, parseNumberedRules(scope, numbered))
|
||||
snapshot, err := filter.NewRuleSet(scope, parseNumberedRules(scope, numbered))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot.Notices = append([]filter.ScopeNotice(nil), notices...)
|
||||
snapshot.LastPosition = lastPositions[scope.Family]
|
||||
snapshots = append(snapshots, snapshot)
|
||||
}
|
||||
return snapshots, nil
|
||||
@@ -145,46 +180,61 @@ func (a *Adapter) NativeDetail(ctx context.Context, profile string, _ bool) (str
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, error) {
|
||||
if snapshot.Revision == "" {
|
||||
return filter.BackendPlan{}, filter.ErrRuleStale
|
||||
}
|
||||
func (a *Adapter) BuildCommands(snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
||||
if err := validateScope(snapshot.Scope); err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
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 {
|
||||
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])
|
||||
if err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
return filter.CommandBatch{}, err
|
||||
}
|
||||
return filter.BackendPlan{
|
||||
Provider: filter.ProviderUFW, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
|
||||
Rules: []filter.NativeRulePlan{rulePlan},
|
||||
return filter.CommandBatch{
|
||||
Provider: filter.ProviderUFW, Scope: snapshot.Scope,
|
||||
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 err := validateBackendPlan(plan); err != nil {
|
||||
return filter.ApplyResult{}, err
|
||||
return err
|
||||
}
|
||||
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...) {
|
||||
if err := validateCommand(command); err != nil {
|
||||
return filter.ApplyResult{}, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
executed := 0
|
||||
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 {
|
||||
return filter.ApplyResult{}, fmt.Errorf("execute UFW rule: %w", err)
|
||||
return fmt.Errorf("execute UFW rule: %w", err)
|
||||
}
|
||||
if !plan.CreatesOnly() {
|
||||
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()
|
||||
}
|
||||
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
|
||||
}
|
||||
if plan.CommandOnly || plan.CreatesOnly() {
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
func ufwApplyError(plan filter.NativeRulePlan, cause error) error {
|
||||
func ufwApplyError(plan filter.RuleCommands, cause error) error {
|
||||
if plan.Operation == filter.ChangeAdopt {
|
||||
return buserr.WithDetail("ErrUFWRuleAdopt", cause.Error(), cause)
|
||||
}
|
||||
return cause
|
||||
}
|
||||
|
||||
func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.NativeRulePlan, commandIndex int) bool {
|
||||
snapshot, err := a.Observe(ctx, plan.Expected.Rule.Scope)
|
||||
func (a *Adapter) failedCommandApplied(ctx context.Context, plan filter.RuleCommands, commandIndex int) bool {
|
||||
snapshot, err := a.ListRules(ctx, plan.Expected.Rule.Scope)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
@@ -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) {
|
||||
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 {
|
||||
func (a *Adapter) Rollback(ctx context.Context, plan filter.CommandBatch) error {
|
||||
if err := validateBackendPlan(plan); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -291,8 +310,8 @@ func validateScope(scope filter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBackendPlan(plan filter.BackendPlan) error {
|
||||
if plan.Provider != filter.ProviderUFW || len(plan.Rules) != 1 || plan.SnapshotRevision == "" {
|
||||
func validateBackendPlan(plan filter.CommandBatch) error {
|
||||
if plan.Provider != filter.ProviderUFW || len(plan.Rules) != 1 {
|
||||
return fmt.Errorf("%w: invalid ufw backend plan", filter.ErrInvalidRule)
|
||||
}
|
||||
if err := validateScope(plan.Scope); err != nil {
|
||||
@@ -304,40 +323,56 @@ func validateBackendPlan(plan filter.BackendPlan) error {
|
||||
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
|
||||
if change.Operation == filter.ChangeDelete {
|
||||
rule = change.Before
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
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 {
|
||||
return filter.NativeRulePlan{}, err
|
||||
return filter.RuleCommands{}, err
|
||||
}
|
||||
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
|
||||
if change.Operation == filter.ChangeDelete && change.CommandOnly && change.Locator == nil {
|
||||
if change.UnmarkedAdopted || change.PreviousMarker != "" {
|
||||
marker = observedComment(filter.ObservedRule{Rule: normalized, Marker: change.PreviousMarker})
|
||||
}
|
||||
return filter.RuleCommands{
|
||||
RuleUUID: normalized.UUID, Operation: change.Operation,
|
||||
Expected: filter.ObservedRule{Rule: normalized, Marker: marker, ParseStatus: filter.ParseStatusSupported},
|
||||
Commands: []filter.NativeCommand{deleteRuleCommand(normalized, marker)},
|
||||
RollbackCommands: []filter.NativeCommand{commentCommand(normalized, marker)},
|
||||
}, nil
|
||||
}
|
||||
position := insertionPosition(snapshot, normalized)
|
||||
expected := observedForRule(normalized, marker, position)
|
||||
plan := filter.NativeRulePlan{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected}
|
||||
plan := filter.RuleCommands{RuleUUID: normalized.UUID, Operation: change.Operation, Expected: expected}
|
||||
|
||||
switch change.Operation {
|
||||
case filter.ChangeCreate:
|
||||
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)
|
||||
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)
|
||||
}
|
||||
if command.Args[0] != "insert" {
|
||||
plan.Expected.Locator.NativeID = ""
|
||||
plan.Expected.Locator.Position = nil
|
||||
}
|
||||
@@ -346,7 +381,7 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
case filter.ChangeAdopt:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, false)
|
||||
if targetErr != nil {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
position = *target.Locator.Position
|
||||
appendAtEnd := position == maximumObservedPosition(snapshot)
|
||||
@@ -366,21 +401,28 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
deleteRuleCommand(normalized, marker),
|
||||
}
|
||||
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 {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
position = *target.Locator.Position
|
||||
targetPosition := position
|
||||
if normalized.OrderIndex != nil {
|
||||
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)
|
||||
}
|
||||
maximumPosition := maximumObservedPosition(snapshot)
|
||||
if targetPosition > maximumPosition {
|
||||
return filter.NativeRulePlan{}, fmt.Errorf(
|
||||
return filter.RuleCommands{}, fmt.Errorf(
|
||||
"%w: update target position %d is out of range 1-%d",
|
||||
filter.ErrInvalidRule, targetPosition, maximumPosition,
|
||||
)
|
||||
@@ -404,18 +446,18 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
case filter.ChangeDelete:
|
||||
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted)
|
||||
if targetErr != nil {
|
||||
return filter.NativeRulePlan{}, targetErr
|
||||
return filter.RuleCommands{}, targetErr
|
||||
}
|
||||
position = *target.Locator.Position
|
||||
plan.Previous = &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)
|
||||
plan.RollbackCommands = []filter.NativeCommand{
|
||||
positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd),
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -434,7 +476,7 @@ func validateWritableRule(rule filter.FirewallRule) error {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return int(*rule.OrderIndex)
|
||||
}
|
||||
@@ -512,8 +554,8 @@ func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int {
|
||||
return position
|
||||
}
|
||||
|
||||
func maximumObservedPosition(snapshot filter.Snapshot) int {
|
||||
maximum := 0
|
||||
func maximumObservedPosition(snapshot filter.RuleSet) int {
|
||||
maximum := snapshot.LastPosition
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Locator.Position != nil && *observed.Locator.Position > maximum {
|
||||
maximum = *observed.Locator.Position
|
||||
@@ -609,47 +651,7 @@ func observedComment(observed filter.ObservedRule) string {
|
||||
return observed.Rule.Description
|
||||
}
|
||||
|
||||
func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, 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 {
|
||||
func (a *Adapter) compensate(ctx context.Context, plan filter.RuleCommands, executed int, cause error) error {
|
||||
if plan.Operation == filter.ChangeCreate {
|
||||
return cause
|
||||
}
|
||||
@@ -662,7 +664,7 @@ func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, ex
|
||||
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
|
||||
for index := executed - 1; index >= 0; index-- {
|
||||
if index >= len(plan.RollbackCommands) {
|
||||
@@ -678,18 +680,7 @@ func (a *Adapter) rollback(ctx context.Context, plan filter.NativeRulePlan, exec
|
||||
return rollbackErr
|
||||
}
|
||||
|
||||
func relatedScopes(scope filter.Scope) []filter.Scope {
|
||||
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 {
|
||||
func countMarker(snapshot filter.RuleSet, marker string) int {
|
||||
count := 0
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Marker == marker {
|
||||
@@ -699,7 +690,7 @@ func countMarker(snapshot filter.Snapshot, marker string) int {
|
||||
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 {
|
||||
if expected.Locator.Position != nil &&
|
||||
(observed.Locator.Position == nil || *observed.Locator.Position != *expected.Locator.Position) {
|
||||
@@ -733,7 +724,7 @@ func validateCommand(command filter.NativeCommand) error {
|
||||
return nil
|
||||
}
|
||||
switch first {
|
||||
case "insert", "allow", "deny", "reject":
|
||||
case "insert", "prepend", "allow", "deny", "reject":
|
||||
return nil
|
||||
default:
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
if !isInboundNumberedRule(matches[4], matches[5]) {
|
||||
if matches[4] == "OUT" || matches[4] == "FWD" || strings.Contains(matches[5], "(out)") {
|
||||
continue
|
||||
}
|
||||
family := familyForNumberedRule(matches[2], matches[5])
|
||||
@@ -774,7 +765,7 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di
|
||||
positionCopy := position
|
||||
locator := filter.Locator{
|
||||
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{
|
||||
"ALLOW": filter.ActionAccept,
|
||||
@@ -911,7 +902,7 @@ func looksLikeAddressedService(value string) bool {
|
||||
if len(tokens) < 2 {
|
||||
return false
|
||||
}
|
||||
if isAnywhere(tokens[0]) {
|
||||
if strings.EqualFold(strings.TrimSpace(tokens[0]), "Anywhere") {
|
||||
return true
|
||||
}
|
||||
_, ok := parseAddress(tokens[0])
|
||||
@@ -981,7 +972,7 @@ func parseUnrecognizedNumberedRule(scope filter.Scope, raw string) (filter.Obser
|
||||
},
|
||||
Locator: filter.Locator{
|
||||
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,
|
||||
}, family, inbound, true
|
||||
@@ -1025,7 +1016,7 @@ func parseDestination(value string) (address, port, protocol, iface, annotation
|
||||
tokens := strings.Fields(value)
|
||||
switch len(tokens) {
|
||||
case 1:
|
||||
if isAnywhere(tokens[0]) {
|
||||
if strings.EqualFold(strings.TrimSpace(tokens[0]), "Anywhere") {
|
||||
return "", "", "all", iface, annotation, true
|
||||
}
|
||||
if parsedAddress, parsedProtocol, endpointOK := parseAddressProtocol(tokens[0]); endpointOK {
|
||||
@@ -1057,7 +1048,7 @@ func parseAddressProtocol(value string) (string, string, bool) {
|
||||
return "", "", false
|
||||
}
|
||||
endpoint := strings.TrimSpace(value[:separator])
|
||||
if isAnywhere(endpoint) {
|
||||
if strings.EqualFold(strings.TrimSpace(endpoint), "Anywhere") {
|
||||
return "", protocol, true
|
||||
}
|
||||
address, ok := parseAddress(endpoint)
|
||||
@@ -1090,7 +1081,7 @@ func splitDestinationAnnotation(value string) (string, string) {
|
||||
}
|
||||
|
||||
func parseSource(value string) (string, bool) {
|
||||
if isAnywhere(value) {
|
||||
if strings.EqualFold(strings.TrimSpace(value), "Anywhere") {
|
||||
return "", true
|
||||
}
|
||||
if strings.Contains(value, " on ") || len(strings.Fields(value)) != 1 {
|
||||
@@ -1099,10 +1090,6 @@ func parseSource(value string) (string, bool) {
|
||||
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) {
|
||||
const delimiter = " on "
|
||||
index := strings.LastIndex(value, delimiter)
|
||||
@@ -1157,7 +1144,7 @@ func parsePortProtocol(value string) (string, string, bool) {
|
||||
}
|
||||
|
||||
func parseAddress(value string) (string, bool) {
|
||||
if isAnywhere(value) {
|
||||
if strings.EqualFold(strings.TrimSpace(value), "Anywhere") {
|
||||
return "", true
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||
@@ -1169,10 +1156,6 @@ func parseAddress(value string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isAnywhere(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "Anywhere")
|
||||
}
|
||||
|
||||
func canonicalRule(rule filter.FirewallRule) string {
|
||||
return strings.Join([]string{
|
||||
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 {
|
||||
notices := make([]filter.ScopeNotice, 0, 1)
|
||||
if !statusActive(numbered) {
|
||||
@@ -1229,6 +1208,10 @@ func IsIPv6Unavailable(err error) bool {
|
||||
|
||||
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) {
|
||||
return cmd.NewCommandMgr(
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return cmd.NewCommandMgr(
|
||||
output, err := cmd.NewCommandMgr(
|
||||
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"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -16,45 +17,107 @@ var (
|
||||
|
||||
var ErrVerificationFailed = errors.New("firewall rule verification failed")
|
||||
|
||||
func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) {
|
||||
rules := append([]ObservedRule(nil), snapshot.Rules...)
|
||||
func ProtectRuleSet(snapshot RuleSet, ports []PortWhitelist) (RuleSet, error) {
|
||||
rules := slices.Clone(snapshot.Rules)
|
||||
whitelist := NewPortWhitelistIndex(ports)
|
||||
for index := range rules {
|
||||
rule := 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 {
|
||||
if rules[index].ParseStatus == ParseStatusSupported && whitelist.Matches(rules[index].Rule) {
|
||||
rules[index].Protected = true
|
||||
}
|
||||
}
|
||||
protected := snapshot
|
||||
protected.Rules = rules
|
||||
if protected.Revision == "" {
|
||||
var err error
|
||||
protected, err = NewSnapshot(snapshot.Scope, rules)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
}
|
||||
|
||||
protected.LastPosition = snapshot.LastPosition
|
||||
protected.Notices = append([]ScopeNotice(nil), snapshot.Notices...)
|
||||
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 {
|
||||
if target.Protected {
|
||||
return ErrProtectedRule
|
||||
@@ -99,27 +162,7 @@ func MatchObservedByRuleKey(observed []ObservedRule, rule FirewallRule) ([]Obser
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func ManagedObserved(snapshot Snapshot, desired DesiredRule) (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) {
|
||||
func FindCommittedObserved(snapshot RuleSet, requested FirewallRule, plan CommandBatch) (ObservedRule, error) {
|
||||
if len(plan.Rules) == 1 && plan.Rules[0].Expected.Marker != "" {
|
||||
matches := make([]ObservedRule, 0, 1)
|
||||
for _, observed := range snapshot.Rules {
|
||||
@@ -147,8 +190,8 @@ func RulesOverlap(left, right FirewallRule) bool {
|
||||
if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() {
|
||||
return false
|
||||
}
|
||||
return familiesOverlap(left.Scope.Family, right.Scope.Family) &&
|
||||
protocolsOverlap(left.Protocol, right.Protocol) &&
|
||||
return (left.Scope.Family == FamilyInet || right.Scope.Family == FamilyInet || left.Scope.Family == right.Scope.Family) &&
|
||||
(left.Protocol == "all" || right.Protocol == "all" || left.Protocol == right.Protocol) &&
|
||||
addressesOverlap(left.SourceAddress, right.SourceAddress) &&
|
||||
addressesOverlap(left.DestinationAddress, right.DestinationAddress) &&
|
||||
portsOverlap(left.SourcePort, right.SourcePort) &&
|
||||
@@ -156,14 +199,6 @@ func RulesOverlap(left, right FirewallRule) bool {
|
||||
(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 {
|
||||
if left == "" || right == "" {
|
||||
return true
|
||||
@@ -176,36 +211,6 @@ func addressesOverlap(left, right string) bool {
|
||||
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 {
|
||||
if left == "" || right == "" {
|
||||
return true
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
package forwarding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
)
|
||||
|
||||
var ErrRuleExists = errors.New("forwarding rule already exists")
|
||||
|
||||
const (
|
||||
FamilyIPv4 = constant.FirewallFamilyIPv4
|
||||
FamilyIPv6 = constant.FirewallFamilyIPv6
|
||||
|
||||
FamilyIPv4 = constant.FirewallFamilyIPv4
|
||||
FamilyIPv6 = constant.FirewallFamilyIPv6
|
||||
ChainPreRouting = "1PANEL_PREROUTING"
|
||||
ChainPostRouting = "1PANEL_POSTROUTING"
|
||||
ChainForward = "1PANEL_FORWARD"
|
||||
|
||||
ForwardFile = "1panel_forward.rules"
|
||||
PreRoutingFile = "1panel_forward_pre.rules"
|
||||
PostRoutingFile = "1panel_forward_post.rules"
|
||||
ForwardFile = "1panel_forward.rules"
|
||||
PreRoutingFile = "1panel_forward_pre.rules"
|
||||
PostRoutingFile = "1panel_forward_post.rules"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
@@ -51,7 +46,9 @@ const (
|
||||
type Adapter interface {
|
||||
Name() string
|
||||
List() ([]Rule, error)
|
||||
Reconcile(rules []Rule) error
|
||||
CreateRules(context.Context, []Rule) error
|
||||
DeleteRules(context.Context, []Rule) error
|
||||
ReplaceRules(rules []Rule) error
|
||||
Enable() error
|
||||
Cleanup() error
|
||||
InitStatus() (bool, bool, error)
|
||||
@@ -59,80 +56,6 @@ type Adapter interface {
|
||||
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) {
|
||||
rule.Family = strings.ToLower(strings.TrimSpace(rule.Family))
|
||||
if rule.Family == "" {
|
||||
|
||||
+167
-97
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package forwarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/mattn/go-shellwords"
|
||||
@@ -25,7 +24,7 @@ type iptablesBackend interface {
|
||||
RunWithStd(table string, args ...string) (string, error)
|
||||
RunIPv6(table string, args ...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
|
||||
LoadIPv6RulesFromFile(table, chain, fileName string) error
|
||||
}
|
||||
@@ -59,20 +58,24 @@ func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (strin
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executable := commands.Restore4
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
executable = commands.Restore6
|
||||
if executable == "" {
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -87,7 +90,6 @@ func (systemIptablesBackend) LoadIPv6RulesFromFile(table, chain, fileName string
|
||||
type forwardingSystem interface {
|
||||
ReadFile(name string) ([]byte, error)
|
||||
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||
RunWithOptionalSudo(name string, args ...string) error
|
||||
}
|
||||
|
||||
type defaultForwardingSystem struct{}
|
||||
@@ -100,34 +102,30 @@ func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileM
|
||||
return cmd.WriteFileWithOptionalSudo(name, data, perm)
|
||||
}
|
||||
|
||||
func (defaultForwardingSystem) RunWithOptionalSudo(name string, args ...string) error {
|
||||
return cmd.NewCommandMgr().RunWithOptionalSudo(name, args...)
|
||||
}
|
||||
|
||||
type iptablesNATAdapter struct {
|
||||
type Iptables struct {
|
||||
provider string
|
||||
backend iptablesBackend
|
||||
system forwardingSystem
|
||||
}
|
||||
|
||||
func newIptablesNATAdapter(provider string) *iptablesNATAdapter {
|
||||
return &iptablesNATAdapter{
|
||||
func NewIptables(provider string) *Iptables {
|
||||
return &Iptables{
|
||||
provider: provider,
|
||||
backend: systemIptablesBackend{},
|
||||
system: defaultForwardingSystem{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Name() string {
|
||||
func (l *Iptables) Name() string {
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
|
||||
}
|
||||
rules := parseIptablesRules(stdout, forwarding.FamilyIPv4)
|
||||
rules := parseIptablesRules(stdout, FamilyIPv4)
|
||||
if !l.backend.IPv6Available() {
|
||||
return rules, nil
|
||||
}
|
||||
@@ -135,49 +133,127 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
byFamily := map[string][]forwarding.Rule{
|
||||
forwarding.FamilyIPv4: nil,
|
||||
forwarding.FamilyIPv6: nil,
|
||||
func (l *Iptables) ReplaceRules(rules []Rule) error {
|
||||
byFamily := map[string][]Rule{
|
||||
FamilyIPv4: nil,
|
||||
FamilyIPv6: nil,
|
||||
}
|
||||
for _, rule := range rules {
|
||||
normalized, err := forwarding.NormalizeRule(rule)
|
||||
normalized, err := NormalizeRule(rule)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
||||
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
var failures []error
|
||||
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
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
script, err := buildIptablesForwardRestoreScript(byFamily[family])
|
||||
script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true)
|
||||
if err != nil {
|
||||
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 nil
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) {
|
||||
natRules := [][]string{{"-F", forwarding.ChainPreRouting}, {"-F", forwarding.ChainPostRouting}}
|
||||
filterRules := [][]string{{"-F", forwarding.ChainForward}}
|
||||
func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error {
|
||||
if len(rules) == 0 {
|
||||
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 {
|
||||
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, "-", ":")
|
||||
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
|
||||
preRouting := []string{"-A", forwarding.ChainPreRouting}
|
||||
preRouting := []string{verb, ChainPreRouting}
|
||||
if rule.Interface != "" {
|
||||
preRouting = append(preRouting, "-i", rule.Interface)
|
||||
}
|
||||
@@ -188,11 +264,11 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
|
||||
}
|
||||
natRules = append(natRules,
|
||||
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,
|
||||
[]string{"-A", forwarding.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, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
|
||||
[]string{verb, ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"},
|
||||
)
|
||||
}
|
||||
var script strings.Builder
|
||||
@@ -220,39 +296,30 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
|
||||
return script.String(), nil
|
||||
}
|
||||
|
||||
func forwardingTarget(rule forwarding.Rule) string {
|
||||
if rule.Family == forwarding.FamilyIPv6 {
|
||||
func forwardingTarget(rule Rule) string {
|
||||
if rule.Family == FamilyIPv6 {
|
||||
return "[" + rule.TargetIP + "]:" + rule.TargetPort
|
||||
}
|
||||
return rule.TargetIP + ":" + rule.TargetPort
|
||||
}
|
||||
|
||||
func isRemoteTarget(family, target string) bool {
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
return target != "" && target != "::1" && target != "localhost"
|
||||
}
|
||||
return target != "" && target != "127.0.0.1" && target != "localhost"
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Enable() error {
|
||||
if err := ensureForwardingSysctls(l.system, l.backend.IPv6Available()); err != nil {
|
||||
func (l *Iptables) Enable() error {
|
||||
if err := ensureForwardingSysctls(l.system, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
return l.batchEnsureChains(FamilyIPv4)
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) batchEnsureChains(family string) error {
|
||||
func (l *Iptables) batchEnsureChains(family string) error {
|
||||
list := l.backend.RunWithStd
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
list = l.backend.RunIPv6WithStd
|
||||
}
|
||||
outputs := make(map[string]string, 2)
|
||||
@@ -267,19 +334,19 @@ func (l *iptablesNATAdapter) batchEnsureChains(family string) error {
|
||||
if script == "" {
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Cleanup() error {
|
||||
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
||||
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
func (l *Iptables) Cleanup() error {
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
continue
|
||||
}
|
||||
list := l.backend.RunWithStd
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
list = l.backend.RunIPv6WithStd
|
||||
}
|
||||
outputs := make(map[string]string, 2)
|
||||
@@ -292,13 +359,13 @@ func (l *iptablesNATAdapter) Cleanup() error {
|
||||
}
|
||||
script := buildIptablesForwardLifecycleScript(outputs, false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, file := range []string{forwarding.ForwardFile, forwarding.PreRoutingFile, forwarding.PostRoutingFile,
|
||||
iptables_helper.IPv6FileName(forwarding.ForwardFile), iptables_helper.IPv6FileName(forwarding.PreRoutingFile), iptables_helper.IPv6FileName(forwarding.PostRoutingFile)} {
|
||||
for _, file := range []string{ForwardFile, PreRoutingFile, 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) {
|
||||
return err
|
||||
}
|
||||
@@ -308,8 +375,8 @@ func (l *iptablesNATAdapter) Cleanup() error {
|
||||
|
||||
func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string {
|
||||
items := []struct{ table, parent, chain string }{
|
||||
{iptables_helper.NatTab, "PREROUTING", forwarding.ChainPreRouting},
|
||||
{iptables_helper.NatTab, "POSTROUTING", forwarding.ChainPostRouting},
|
||||
{iptables_helper.NatTab, "PREROUTING", ChainPreRouting},
|
||||
{iptables_helper.NatTab, "POSTROUTING", ChainPostRouting},
|
||||
}
|
||||
byTable := make(map[string][]string, 2)
|
||||
for _, item := range items {
|
||||
@@ -335,25 +402,25 @@ func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool)
|
||||
}
|
||||
|
||||
filterOutput := outputs[iptables_helper.FilterTab]
|
||||
filterChainExists := containsExactLine(filterOutput, "-N "+forwarding.ChainForward)
|
||||
filterBinding := "-A FORWARD -j " + forwarding.ChainForward
|
||||
filterChainExists := containsExactLine(filterOutput, "-N "+ChainForward)
|
||||
filterBinding := "-A FORWARD -j " + ChainForward
|
||||
filterBindingCount := countExactLines(filterOutput, filterBinding)
|
||||
if create {
|
||||
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) {
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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
|
||||
@@ -382,7 +449,7 @@ func countExactLines(output, want string) int {
|
||||
}
|
||||
|
||||
func forwardBindingEffective(output string) bool {
|
||||
binding := "-A FORWARD -j " + forwarding.ChainForward
|
||||
binding := "-A FORWARD -j " + ChainForward
|
||||
bindingPosition := 0
|
||||
terminalPosition := 0
|
||||
position := 0
|
||||
@@ -405,7 +472,7 @@ func forwardBindingEffective(output string) bool {
|
||||
}
|
||||
|
||||
func canonicalForwardBindingRule(output string) string {
|
||||
binding := "-A FORWARD -j " + forwarding.ChainForward
|
||||
binding := "-A FORWARD -j " + ChainForward
|
||||
position := 1
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
@@ -413,11 +480,11 @@ func canonicalForwardBindingRule(output string) string {
|
||||
continue
|
||||
}
|
||||
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++
|
||||
}
|
||||
return "-A FORWARD -j " + forwarding.ChainForward
|
||||
return "-A FORWARD -j " + ChainForward
|
||||
}
|
||||
|
||||
func isUnconditionalForwardTerminal(line string) bool {
|
||||
@@ -442,31 +509,34 @@ func containsExactLine(output, want string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) {
|
||||
ipv4Init, ipv4Bind, err := l.familyInitStatus(forwarding.FamilyIPv4)
|
||||
func (l *Iptables) InitStatus() (bool, bool, error) {
|
||||
ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
if !l.backend.IPv6Available() {
|
||||
return ipv4Init, ipv4Bind, nil
|
||||
}
|
||||
ipv6Init, ipv6Bind, err := l.familyInitStatus(forwarding.FamilyIPv6)
|
||||
ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
|
||||
if err != nil {
|
||||
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"
|
||||
label := "IPv4"
|
||||
list := l.backend.RunWithStd
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
|
||||
label = "IPv6"
|
||||
list = l.backend.RunIPv6WithStd
|
||||
}
|
||||
data, err := l.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", 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)
|
||||
}
|
||||
natInit, natBind := checkInitAndBind(
|
||||
[]string{"-N " + forwarding.ChainPreRouting, "-N " + forwarding.ChainPostRouting},
|
||||
[]string{"-A PREROUTING -j " + forwarding.ChainPreRouting, "-A POSTROUTING -j " + forwarding.ChainPostRouting},
|
||||
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
|
||||
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
|
||||
strings.Split(natRules, "\n"),
|
||||
)
|
||||
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)
|
||||
}
|
||||
filterInit, _ := checkInitAndBind(
|
||||
[]string{"-N " + forwarding.ChainForward},
|
||||
[]string{"-N " + ChainForward},
|
||||
nil,
|
||||
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
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) FamilyStatus(family string) (bool, bool, error) {
|
||||
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return false, false, nil
|
||||
}
|
||||
return l.familyInitStatus(family)
|
||||
@@ -526,9 +596,9 @@ func containsExactRule(lines []string, rule string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Replay() error {
|
||||
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
||||
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
func (l *Iptables) Replay() error {
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
continue
|
||||
}
|
||||
if err := l.batchEnsureChains(family); err != nil {
|
||||
@@ -540,9 +610,9 @@ func (l *iptablesNATAdapter) Replay() error {
|
||||
chain string
|
||||
file string
|
||||
}{
|
||||
{iptables_helper.FilterTab, forwarding.ChainForward, forwarding.ForwardFile},
|
||||
{iptables_helper.NatTab, forwarding.ChainPreRouting, forwarding.PreRoutingFile},
|
||||
{iptables_helper.NatTab, forwarding.ChainPostRouting, forwarding.PostRoutingFile},
|
||||
{iptables_helper.FilterTab, ChainForward, ForwardFile},
|
||||
{iptables_helper.NatTab, ChainPreRouting, PreRoutingFile},
|
||||
{iptables_helper.NatTab, ChainPostRouting, PostRoutingFile},
|
||||
} {
|
||||
if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil {
|
||||
return err
|
||||
@@ -556,17 +626,17 @@ func (l *iptablesNATAdapter) Replay() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseIptablesRules(stdout, family string) []forwarding.Rule {
|
||||
var rules []forwarding.Rule
|
||||
func parseIptablesRules(stdout, family string) []Rule {
|
||||
var rules []Rule
|
||||
num := 0
|
||||
lines:
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
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
|
||||
}
|
||||
num++
|
||||
rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family}
|
||||
rule := Rule{Num: strconv.Itoa(num), Family: family}
|
||||
target := ""
|
||||
for index := 2; index < len(fields); index++ {
|
||||
var value *string
|
||||
@@ -609,7 +679,7 @@ lines:
|
||||
switch target {
|
||||
case "REDIRECT":
|
||||
rule.TargetIP = "127.0.0.1"
|
||||
if family == forwarding.FamilyIPv6 {
|
||||
if family == FamilyIPv6 {
|
||||
rule.TargetIP = "::1"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
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) {
|
||||
return loadInitStatus(tab, RunWithStd, true)
|
||||
}
|
||||
@@ -172,47 +120,3 @@ func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool,
|
||||
}
|
||||
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"
|
||||
)
|
||||
|
||||
func (m *Manager) EnsureIPv6BaseChains() error {
|
||||
ports, err := m.loadRequiredPorts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return EnsureIPv6BaseChains(m.panelPort(), ports)
|
||||
}
|
||||
|
||||
func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) error {
|
||||
func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil || !commands.IPv6Available() {
|
||||
return fmt.Errorf("ip6tables and ip6tables-restore are required")
|
||||
}
|
||||
if panelPort == "" {
|
||||
return fmt.Errorf("panel port is required")
|
||||
}
|
||||
output, err := RunIPv6WithStd(FilterTab, "-S")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -33,7 +22,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
|
||||
if err := ensureBaseChainsFamily(true); err != nil {
|
||||
return err
|
||||
}
|
||||
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, panelPort, ports, output)
|
||||
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, ports, output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -43,16 +32,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
|
||||
if err := setBaseChainBindings(true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, chain := range []struct{ name, file string }{
|
||||
{BasicBeforeChain, IPv6FileName(BasicBeforeFileName)},
|
||||
{BasicChain, IPv6FileName(BasicFileName)},
|
||||
{BasicAfterChain, IPv6FileName(BasicAfterFileName)},
|
||||
} {
|
||||
if err := SaveIPv6RulesToFile(FilterTab, chain.name, chain.file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return saveBaseChainsFamily(true)
|
||||
}
|
||||
|
||||
func UnbindIPv6BaseChains() error {
|
||||
@@ -71,13 +51,13 @@ func BindIPv6BaseChains() error {
|
||||
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() {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
@@ -92,15 +72,15 @@ func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.P
|
||||
return script.String(), nil
|
||||
}
|
||||
|
||||
func baseDefaultRules(panelPort string, ports []firewall.PortWhitelist, family string) ([]string, error) {
|
||||
ports, err := firewall.NormalizeRequiredPorts(append([]firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}, ports...))
|
||||
func baseDefaultRules(ports []firewall.PortWhitelist, family string) ([]string, error) {
|
||||
ports, err := firewall.NormalizeRequiredPorts(ports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule}
|
||||
for _, port := range ports {
|
||||
if port.Family == "" || port.Family == family {
|
||||
rules = append(rules, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
|
||||
for _, port := range firewall.ExpandPortWhitelist(ports) {
|
||||
if port.Family == family {
|
||||
rules = append(rules, iptablesSystemPortRuleLine(port))
|
||||
}
|
||||
}
|
||||
return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil
|
||||
|
||||
@@ -3,9 +3,10 @@ package iptables_helper
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
UpdateSetting func(key, value string) error
|
||||
PanelPort func() string
|
||||
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
|
||||
}
|
||||
|
||||
func (m *Manager) Cleanup() error {
|
||||
if err := m.disableBase(); err != nil {
|
||||
func Cleanup() error {
|
||||
if err := disableBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupBaseChains(false); err != nil {
|
||||
@@ -42,59 +38,64 @@ func (m *Manager) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Operate(operation firewall.BaseOperation) error {
|
||||
func Operate(operation firewall.BaseOperation, requiredPorts []firewall.PortWhitelist) error {
|
||||
switch operation {
|
||||
case firewall.BaseOperationInit, firewall.BaseOperationBind:
|
||||
if _, err := lifecycle.ResolveIptablesCommands(); err != nil {
|
||||
return fmt.Errorf("failed to find iptables")
|
||||
}
|
||||
return m.enableBase(true)
|
||||
return enableBase(true, requiredPorts)
|
||||
case firewall.BaseOperationBindWithoutInit:
|
||||
return m.enableBase(false)
|
||||
return enableBase(false, requiredPorts)
|
||||
case firewall.BaseOperationUnbind:
|
||||
return m.disableBase()
|
||||
return disableBase()
|
||||
default:
|
||||
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 err := ensureBaseChains(); err != nil {
|
||||
if err := ensureBaseChainsFamily(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.initPreRules(); err != nil {
|
||||
if err := applyRequiredFirewallPortWhiteListRules(requiredPorts, false, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := saveBaseChains(); err != nil {
|
||||
if err := saveBaseChainsFamily(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := bindBaseChains(); err != nil {
|
||||
if err := setBaseChainBindings(false, true); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
} else if err := BindIPv6BaseChains(); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := UnbindIPv6BaseChains(); err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return err
|
||||
}
|
||||
return m.updateSetting("IptablesStatus", constant.StatusDisable)
|
||||
}
|
||||
|
||||
func ensureBaseChains() error {
|
||||
return ensureBaseChainsFamily(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseChainsFamily(ipv6 bool) error {
|
||||
@@ -165,10 +166,6 @@ func cleanupBaseChains(ipv6 bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindBaseChains() error {
|
||||
return setBaseChainBindings(false, true)
|
||||
}
|
||||
|
||||
func setBaseChainBindings(ipv6, bind bool) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
@@ -228,32 +225,43 @@ func baseChainBindingCommands(output string, bind bool) []string {
|
||||
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 }{
|
||||
{BasicBeforeChain, BasicBeforeFileName},
|
||||
{BasicChain, BasicFileName},
|
||||
{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 nil
|
||||
}
|
||||
|
||||
func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist) error {
|
||||
port, err := strconv.Atoi(panelPort)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("invalid panel port %q", panelPort)
|
||||
}
|
||||
func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureBaseChains(); err != nil {
|
||||
if err := ensureBaseChainsFamily(false); err != nil {
|
||||
return err
|
||||
}
|
||||
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, false)
|
||||
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, false, requiredPorts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,7 +274,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
|
||||
if err := ensureBaseChainsFamily(true); err != nil {
|
||||
return err
|
||||
}
|
||||
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, true, requiredPorts...)
|
||||
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, true, requiredPorts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -276,7 +284,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
|
||||
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
|
||||
script.WriteString("*filter\n")
|
||||
for _, chain := range BasicChains() {
|
||||
@@ -284,8 +292,6 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
|
||||
script.WriteString(chain)
|
||||
script.WriteByte('\n')
|
||||
}
|
||||
panelRule := "-A " + BasicBeforeChain + " -p tcp -m tcp --dport " + panelPort + " -j ACCEPT"
|
||||
panelRuleFound := false
|
||||
for _, item := range []struct{ chain, file string }{
|
||||
{BasicBeforeChain, BasicBeforeFileName},
|
||||
{BasicChain, BasicFileName},
|
||||
@@ -308,53 +314,31 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
|
||||
if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
|
||||
continue
|
||||
}
|
||||
if line == panelRule {
|
||||
panelRuleFound = true
|
||||
}
|
||||
script.WriteString(line)
|
||||
script.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
if !panelRuleFound {
|
||||
script.WriteString(panelRule)
|
||||
script.WriteByte('\n')
|
||||
}
|
||||
family := constant.FirewallFamilyIPv4
|
||||
if ipv6 {
|
||||
defaults, err := baseDefaultRules(panelPort, requiredPorts, constant.FirewallFamilyIPv6)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, rule := range defaults {
|
||||
if !containsIptablesRule(script.String(), rule) {
|
||||
script.WriteString(rule + "\n")
|
||||
family = constant.FirewallFamilyIPv6
|
||||
}
|
||||
defaults, err := baseDefaultRules(requiredPorts, family)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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")
|
||||
return script.String(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) initPreRules() 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
|
||||
}
|
||||
func syncRequiredPorts(requiredPorts []firewall.PortWhitelist, withSave bool) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -386,23 +370,22 @@ func (m *Manager) SyncRequiredPorts(withSave 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 {
|
||||
return err
|
||||
}
|
||||
run, save := RunWithStd, SaveRulesToFile
|
||||
rules := firewall.ExpandPortWhitelist(ports)
|
||||
run := RunWithStd
|
||||
save := SaveRulesToFile
|
||||
beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName
|
||||
if ipv6 {
|
||||
run, save = RunIPv6WithStd, SaveIPv6RulesToFile
|
||||
run = RunIPv6WithStd
|
||||
save = SaveIPv6RulesToFile
|
||||
beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile)
|
||||
}
|
||||
beforeRules, err := readFilterRulesByChain(BasicBeforeChain, run)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
afterRules, err := readFilterRulesByChain(BasicAfterChain, run)
|
||||
if err != nil {
|
||||
return err
|
||||
family := constant.FirewallFamilyIPv4
|
||||
if ipv6 {
|
||||
family = constant.FirewallFamilyIPv6
|
||||
}
|
||||
beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain)
|
||||
if err != nil {
|
||||
@@ -412,16 +395,16 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
|
||||
if err != nil {
|
||||
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 != "" {
|
||||
commands, resolveErr := lifecycle.ResolveIptablesCommands()
|
||||
if resolveErr != nil {
|
||||
return resolveErr
|
||||
}
|
||||
restore := commands.Restore4
|
||||
if ipv6 {
|
||||
restore = commands.Restore6
|
||||
}
|
||||
if err := restoreRules(restore, script); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
func buildRequiredPortsRestoreScript(
|
||||
desired []firewall.PortWhitelist,
|
||||
beforeRules, afterRules []FilterRules,
|
||||
beforeRaw, afterRaw string,
|
||||
includeDefaults bool,
|
||||
) 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))
|
||||
func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string {
|
||||
var commands []string
|
||||
for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
|
||||
if !containsIptablesRule(beforeRaw, line) {
|
||||
commands = append(commands, line)
|
||||
}
|
||||
}
|
||||
|
||||
if includeDefaults {
|
||||
for _, rule := range []string{
|
||||
"-A " + BasicBeforeChain + " " + IoRuleIn,
|
||||
"-A " + BasicBeforeChain + " " + EstablishedRule,
|
||||
} {
|
||||
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 _, rule := range desired {
|
||||
line := iptablesSystemPortRuleLine(rule)
|
||||
if rule.Family == family && !containsIptablesRule(beforeRaw, line) {
|
||||
commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1))
|
||||
beforeRaw += "\n" + line
|
||||
}
|
||||
}
|
||||
for _, port := range desired {
|
||||
if _, exists := kept[firewall.PortWhitelistKey(port)]; exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
|
||||
}
|
||||
if includeDefaults {
|
||||
for _, rule := range []string{DropAllTcp, DropAllUdp} {
|
||||
line := "-A " + BasicAfterChain + " " + rule
|
||||
@@ -497,13 +446,12 @@ func buildRequiredPortsRestoreScript(
|
||||
return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n"
|
||||
}
|
||||
|
||||
func simpleAcceptedPortRule(rule FilterRules) bool {
|
||||
return rule.Strategy == "accept" && (rule.Protocol == "tcp" || rule.Protocol == "udp") && rule.DstPort != "" &&
|
||||
rule.SrcIP == "" && rule.DstIP == "" && rule.SrcPort == ""
|
||||
}
|
||||
|
||||
func iptablesPortRuleLine(operation, chain, protocol, port string) string {
|
||||
return strings.Join([]string{operation, chain, "-p", protocol, "-m", protocol, "--dport", port, "-j", "ACCEPT"}, " ")
|
||||
func iptablesSystemPortRuleLine(rule firewall.SystemPort) string {
|
||||
parts := []string{"-A", BasicBeforeChain}
|
||||
if rule.SourceAddress != "" {
|
||||
parts = append(parts, "-s", rule.SourceAddress)
|
||||
}
|
||||
return strings.Join(append(parts, "-p", rule.Protocol, "-m", rule.Protocol, "--dport", rule.Port, "-j", "ACCEPT"), " ")
|
||||
}
|
||||
|
||||
func containsIptablesRule(output, rule string) bool {
|
||||
@@ -512,11 +460,27 @@ func containsIptablesRule(output, rule string) bool {
|
||||
|
||||
func countIptablesRule(output, rule string) int {
|
||||
canonical := func(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if strings.Contains(value, " -j ACCEPT") {
|
||||
return strings.Replace(value, " -j ACCEPT", "", 1) + " -j ACCEPT"
|
||||
fields, err := shellwords.Parse(value)
|
||||
if err != nil || len(fields)%2 != 0 {
|
||||
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)
|
||||
count := 0
|
||||
@@ -527,24 +491,3 @@ func countIptablesRule(output, rule string) int {
|
||||
}
|
||||
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 {
|
||||
rulesFile := path.Join(global.Dir.FirewallDir, fileName)
|
||||
|
||||
var stdout string
|
||||
var err error
|
||||
if strings.HasPrefix(path.Base(executable), "ip6tables") {
|
||||
@@ -65,11 +63,16 @@ func saveRulesToFile(ctx context.Context, executable, tab, chain, fileName strin
|
||||
if err != nil {
|
||||
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
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, fmt.Sprintf("-A %s", chain)) {
|
||||
if strings.HasPrefix(line, "-A "+chain+" ") {
|
||||
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/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"
|
||||
)
|
||||
|
||||
func (m *Manager) RepairBaseChains() error {
|
||||
func RepairBaseChains(ports []firewall.PortWhitelist) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -35,11 +36,7 @@ func (m *Manager) RepairBaseChains() error {
|
||||
return err
|
||||
}
|
||||
script, err := buildBaseChainsRepairScript(global.Dir.FirewallDir, output, ipv6, func() ([]string, error) {
|
||||
ports, err := m.loadRequiredPorts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return baseDefaultRules(m.panelPort(), ports, family)
|
||||
return baseDefaultRules(ports, family)
|
||||
})
|
||||
if err != nil {
|
||||
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"
|
||||
)
|
||||
|
||||
type Operation string
|
||||
|
||||
const (
|
||||
OperationStart Operation = "start"
|
||||
OperationStop Operation = "stop"
|
||||
OperationRestart Operation = "restart"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderFirewalld = constant.FirewallProviderFirewalld
|
||||
ProviderUFW = constant.FirewallProviderUFW
|
||||
@@ -109,15 +117,14 @@ type PreStopResetter interface {
|
||||
ResetBeforeStop() error
|
||||
}
|
||||
|
||||
func NewClient() (Client, error) {
|
||||
runtime, err := DetectRuntime()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewClient(provider string) (Client, error) {
|
||||
if provider == "" {
|
||||
runtime, err := DetectRuntime()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider = runtime.Provider
|
||||
}
|
||||
return NewClientFor(runtime.Provider)
|
||||
}
|
||||
|
||||
func NewClientFor(provider string) (Client, error) {
|
||||
switch provider {
|
||||
case "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/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
)
|
||||
|
||||
type Firewalld struct{}
|
||||
@@ -37,7 +38,8 @@ func (f *Firewalld) Name() string {
|
||||
func (f *Firewalld) Status() (bool, error) {
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--state")
|
||||
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, fmt.Errorf("load firewall status failed: %w", err)
|
||||
@@ -45,14 +47,6 @@ func (f *Firewalld) Status() (bool, error) {
|
||||
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) {
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--version")
|
||||
if err != nil {
|
||||
@@ -68,6 +62,19 @@ func (f *Firewalld) Start() error {
|
||||
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 {
|
||||
if err := controller.HandleStop("firewalld"); err != nil {
|
||||
return fmt.Errorf("stop the firewall failed, err: %v", err)
|
||||
@@ -105,12 +112,7 @@ func (f *Firewalld) ResetBeforeStop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceFirewalldConfig(
|
||||
configDir string,
|
||||
backupDir string,
|
||||
prepare func(string) error,
|
||||
validate func() error,
|
||||
) (func() error, error) {
|
||||
func replaceFirewalldConfig(configDir string, backupDir string, prepare func(string) error, validate func() error) (func() error, error) {
|
||||
info, err := os.Lstat(configDir)
|
||||
hadConfig := err == nil
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
|
||||
@@ -4,25 +4,20 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const requiredPortComment = "1Panel Port Whitelist"
|
||||
|
||||
type Manager struct {
|
||||
UpdateSetting func(key, value string) error
|
||||
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
|
||||
}
|
||||
|
||||
func (m *Manager) Cleanup() error {
|
||||
func Cleanup() error {
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
tableFamily := TableFamily(family)
|
||||
@@ -38,69 +33,58 @@ func (m *Manager) Cleanup() error {
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
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 {
|
||||
case firewall.BaseOperationInit, firewall.BaseOperationBind:
|
||||
return m.enableBase(true)
|
||||
return enableBase(true, requiredPorts)
|
||||
case firewall.BaseOperationBindWithoutInit:
|
||||
return m.enableBase(false)
|
||||
return enableBase(false, requiredPorts)
|
||||
case firewall.BaseOperationUnbind:
|
||||
return m.disableBase()
|
||||
return Unbind()
|
||||
default:
|
||||
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 err := m.ensureBaseChains(); err != nil {
|
||||
if err := ensureBaseChains(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.initPreRules(); err != nil {
|
||||
if err := initPreRules(requiredPorts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := Bind(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.updateSetting("IptablesStatus", constant.StatusEnable)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) disableBase() error {
|
||||
if err := Unbind(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.updateSetting("IptablesStatus", constant.StatusDisable)
|
||||
}
|
||||
|
||||
func (m *Manager) ensureBaseChains() error {
|
||||
func ensureBaseChains() error {
|
||||
commands := make([][]string, 0, 10)
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
tableFamily := TableFamily(family)
|
||||
tableExists := true
|
||||
if _, err := run("list", "table", tableFamily, TableName); err != nil {
|
||||
tableExists = false
|
||||
output, tableExists, err := ReadTable(run, tableFamily, TableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chains := ParseTableChains(output)
|
||||
if !tableExists {
|
||||
commands = append(commands, []string{"add", "table", tableFamily, TableName})
|
||||
}
|
||||
if !tableExists {
|
||||
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 {
|
||||
if _, exists := chains[InputChain]; !exists {
|
||||
commands = append(commands, []string{
|
||||
"add", "chain", tableFamily, TableName, InputChain,
|
||||
"{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}",
|
||||
})
|
||||
}
|
||||
for _, nativeChain := range BasicChains() {
|
||||
if tableExists {
|
||||
if _, err := run("list", "chain", tableFamily, TableName, nativeChain); err == nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := chains[nativeChain]; exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, []string{"add", "chain", tableFamily, TableName, nativeChain})
|
||||
}
|
||||
@@ -111,33 +95,45 @@ func (m *Manager) ensureBaseChains() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func requiredPortCommand(tableFamily string, port firewall.PortWhitelist) []string {
|
||||
return []string{
|
||||
"add", "rule", tableFamily, TableName, BasicBeforeChain,
|
||||
"meta", "l4proto", port.Protocol, port.Protocol, "dport", port.Port,
|
||||
"accept", "comment", `"` + requiredPortComment + `"`,
|
||||
func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string {
|
||||
command := []string{
|
||||
"insert", "rule", tableFamily, TableName, BasicBeforeChain,
|
||||
}
|
||||
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 {
|
||||
ports, err := m.loadRequiredPorts()
|
||||
func initPreRules(requiredPorts []firewall.PortWhitelist) error {
|
||||
ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err = firewall.NormalizeRequiredPorts(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands := make([][]string, 0, 12+len(ports)*2)
|
||||
rules := firewall.ExpandPortWhitelist(ports)
|
||||
var commands [][]string
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
tableFamily := TableFamily(family)
|
||||
commands = append(commands,
|
||||
[]string{"flush", "chain", tableFamily, TableName, BasicBeforeChain},
|
||||
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
|
||||
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
|
||||
)
|
||||
for _, port := range ports {
|
||||
commands = append(commands, requiredPortCommand(tableFamily, port))
|
||||
output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
candidates := [][]string{
|
||||
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
|
||||
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
|
||||
}
|
||||
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,
|
||||
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
|
||||
@@ -148,132 +144,56 @@ func (m *Manager) initPreRules() error {
|
||||
return runBatch(commands...)
|
||||
}
|
||||
|
||||
func (m *Manager) SyncRequiredPorts() error {
|
||||
ports, err := m.loadRequiredPorts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err = firewall.NormalizeRequiredPorts(ports)
|
||||
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
|
||||
func containsRequiredPortRule(output, expression string) bool {
|
||||
canonical := func(line string) string {
|
||||
line, _, _ = strings.Cut(line, " comment ")
|
||||
line, _, _ = strings.Cut(line, " # handle ")
|
||||
for _, protocol := range []string{"tcp", "udp"} {
|
||||
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
stdout, err := run("-n", "-a", "list", "chain", tableFamily, TableName, BasicBeforeChain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing := requiredPortRules(stdout)
|
||||
missing, staleHandles := requiredPortChanges(existing, ports)
|
||||
for _, port := range missing {
|
||||
commands = append(commands, requiredPortCommand(tableFamily, port))
|
||||
}
|
||||
for _, handle := range staleHandles {
|
||||
commands = append(commands, []string{"delete", "rule", tableFamily, TableName, BasicBeforeChain, "handle", handle})
|
||||
}
|
||||
}
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
type requiredPortRule struct {
|
||||
Key string
|
||||
Handle string
|
||||
}
|
||||
|
||||
func requiredPortChanges(existing []requiredPortRule, desiredPorts []firewall.PortWhitelist) ([]firewall.PortWhitelist, []string) {
|
||||
desired := firewall.PortWhitelistMap(desiredPorts)
|
||||
existingKeys := make(map[string]struct{}, len(existing))
|
||||
for _, rule := range existing {
|
||||
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
|
||||
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
|
||||
fields := strings.Fields(line)
|
||||
for index, field := range fields {
|
||||
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
|
||||
states := strings.Split(field, ",")
|
||||
for i, state := range states {
|
||||
value, err := strconv.ParseUint(state, 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch value {
|
||||
case 1:
|
||||
states[i] = "invalid"
|
||||
case 2:
|
||||
states[i] = "established"
|
||||
case 4:
|
||||
states[i] = "related"
|
||||
case 8:
|
||||
states[i] = "new"
|
||||
case 64:
|
||||
states[i] = "untracked"
|
||||
}
|
||||
}
|
||||
slices.Sort(states)
|
||||
fields[index] = strings.Join(states, ",")
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(field); err == nil {
|
||||
prefix = prefix.Masked()
|
||||
fields[index] = prefix.String()
|
||||
if prefix.Bits() == prefix.Addr().BitLen() {
|
||||
fields[index] = prefix.Addr().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
staleHandles = append(staleHandles, rule.Handle)
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
return missing, staleHandles
|
||||
}
|
||||
|
||||
func requiredPortRules(output string) []requiredPortRule {
|
||||
rules := make([]requiredPortRule, 0)
|
||||
marker := `comment "` + requiredPortComment + `"`
|
||||
wanted := canonical(expression)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if !strings.Contains(line, marker) {
|
||||
continue
|
||||
}
|
||||
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
|
||||
if canonical(line) == wanted {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
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")
|
||||
return false
|
||||
}
|
||||
|
||||
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