mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 |
@@ -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"
|
||||
)
|
||||
@@ -455,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)
|
||||
@@ -462,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):
|
||||
@@ -470,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:
|
||||
@@ -573,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)
|
||||
@@ -709,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
|
||||
}
|
||||
|
||||
@@ -243,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})
|
||||
}
|
||||
@@ -49,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 {
|
||||
@@ -240,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 {
|
||||
|
||||
+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,6 +24,8 @@ 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 {
|
||||
@@ -87,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)
|
||||
}
|
||||
@@ -127,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
|
||||
}
|
||||
|
||||
@@ -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,7 +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"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
)
|
||||
|
||||
const dockerNftablesMinVersion = "29.0.0"
|
||||
@@ -84,7 +85,7 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
if err := docker_guard.CheckIPv4Forwarding(); err != nil {
|
||||
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -282,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) {
|
||||
@@ -914,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 {
|
||||
@@ -925,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()
|
||||
|
||||
+884
-2245
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,23 +5,20 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"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/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/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"
|
||||
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -37,35 +34,61 @@ type FirewallSettingService struct{}
|
||||
|
||||
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) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
|
||||
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||
return append(current, request.Rule), nil
|
||||
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
|
||||
}
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
|
||||
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
return current, nil
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,156 +96,33 @@ func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, reques
|
||||
if request.Rule == nil {
|
||||
return fmt.Errorf("select one firewall port whitelist rule to delete")
|
||||
}
|
||||
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slices.Delete(current, index, index+1), nil
|
||||
})
|
||||
}
|
||||
|
||||
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
|
||||
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
|
||||
return samePortWhitelistRule(rule, target)
|
||||
})
|
||||
if index < 0 {
|
||||
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
|
||||
if reflect.DeepEqual(left, right) {
|
||||
return true
|
||||
}
|
||||
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
slices.Sort(normalizedLeft[0].Sources)
|
||||
slices.Sort(normalizedRight[0].Sources)
|
||||
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
|
||||
}
|
||||
|
||||
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
defer filterruntime.InvalidateInventory()
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
current, err := loadPortWhitelistSetting(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desired, err := change(current)
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desired, err = firewall.ValidatePortWhitelist(desired)
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(desired)
|
||||
value, err := json.Marshal(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
|
||||
})
|
||||
}
|
||||
|
||||
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := record.RulesForProvider(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if filter.RuleMatchesPortWhitelist(rule, ports) {
|
||||
return filter.ErrProtectedRule
|
||||
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 loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
|
||||
var setting model.Setting
|
||||
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
setting.Value = constant.FirewallPortWhiteListValue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rules []firewall.PortWhitelist
|
||||
err := json.Unmarshal([]byte(setting.Value), &rules)
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func loadSSHWhitelistPortFrom(path string) (string, error) {
|
||||
directives, _, err := parseSSHConfigTree(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return defaultSSHPort, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return loadSSHPortValues(directives)[0], nil
|
||||
}
|
||||
|
||||
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
|
||||
result := make([]firewall.PortWhitelist, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Type == "" {
|
||||
result = append(result, entry)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||
entries = slices.Clone(entries)
|
||||
var sshPort string
|
||||
for i := range entries {
|
||||
entry := &entries[i]
|
||||
if entry.Type == "" || entry.Port != "" {
|
||||
continue
|
||||
}
|
||||
switch entry.Type {
|
||||
case firewall.PortWhitelistTypePanel:
|
||||
entry.Port = LoadPanelPort()
|
||||
case firewall.PortWhitelistTypeSSH:
|
||||
if sshPort == "" {
|
||||
var err error
|
||||
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
entry.Port = sshPort
|
||||
}
|
||||
}
|
||||
return firewall.ValidatePortWhitelist(entries)
|
||||
}
|
||||
|
||||
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
|
||||
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||
for i := range entries {
|
||||
if entries[i].Type == serviceType {
|
||||
if len(ports) == 0 {
|
||||
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
|
||||
}
|
||||
entries[i].Port = ports[0]
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,9 +133,10 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -248,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()
|
||||
}
|
||||
@@ -270,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,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{
|
||||
@@ -326,14 +228,14 @@ 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)
|
||||
@@ -353,32 +255,6 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
return result, err
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
||||
switch provider {
|
||||
case constant.FirewallProviderIptables:
|
||||
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
||||
case constant.FirewallProviderNftables:
|
||||
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
|
||||
default:
|
||||
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
@@ -387,7 +263,7 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
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 {
|
||||
@@ -411,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" {
|
||||
@@ -476,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()
|
||||
}
|
||||
}
|
||||
@@ -486,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 {
|
||||
@@ -512,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) {
|
||||
@@ -544,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
|
||||
}
|
||||
@@ -585,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
|
||||
}
|
||||
@@ -596,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 {
|
||||
@@ -610,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
|
||||
@@ -629,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
|
||||
}
|
||||
|
||||
+262
-1201
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+302
-313
@@ -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,11 +18,19 @@ 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"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
)
|
||||
|
||||
const (
|
||||
forwardingSyncConverged = "converged"
|
||||
forwardingSyncMissing = "missing"
|
||||
forwardingSyncRuntimeOnly = "runtime_only"
|
||||
)
|
||||
|
||||
type IForwardingService interface {
|
||||
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||
@@ -31,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
|
||||
@@ -39,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.managerFactory()
|
||||
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
|
||||
@@ -83,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.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
|
||||
@@ -124,11 +120,11 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
manager, err := s.managerFactory()
|
||||
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
|
||||
}
|
||||
@@ -178,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
|
||||
}
|
||||
@@ -203,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.managerFactory()
|
||||
manager, err := s.clientFactory()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
@@ -253,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
|
||||
}
|
||||
@@ -262,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
|
||||
}
|
||||
@@ -277,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.managerFactory()
|
||||
manager, err = s.clientFactory()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
@@ -294,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
|
||||
}
|
||||
@@ -308,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)
|
||||
@@ -319,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.managerFactory()
|
||||
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.managerFactory()
|
||||
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 {
|
||||
@@ -474,104 +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 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 := forwarding.New(client.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return forwarding.NewManager(adapter, client), nil
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
|
||||
return err
|
||||
}
|
||||
if req.Key == "Port" {
|
||||
if err := updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
_ = ensureFirewallPorts(addPorts)
|
||||
if err := ensureFirewallPorts(addPorts); err != nil {
|
||||
global.LOG.Errorf("allow website firewall ports failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
|
||||
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
|
||||
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
|
||||
FileTaskCopy: 'Copy files to {{ .dst }}'
|
||||
FileTaskMove: 'Move files to {{ .dst }}'
|
||||
FileTaskCompress: 'Compress files to {{ .dst }}'
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de
|
||||
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
|
||||
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"
|
||||
FileTaskCopy: 'Copiar archivos a {{ .dst }}'
|
||||
FileTaskMove: 'Mover archivos a {{ .dst }}'
|
||||
FileTaskCompress: 'Comprimir archivos en {{ .dst }}'
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Doc
|
||||
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمیکند. لطفاً یک قانون جدید ایجاد کنید."
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
|
||||
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
|
||||
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
|
||||
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 }}"
|
||||
|
||||
@@ -710,3 +710,14 @@ FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອ
|
||||
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
|
||||
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
|
||||
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
|
||||
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
|
||||
FileTaskCopy: 'Salin fail ke {{ .dst }}'
|
||||
FileTaskMove: 'Pindahkan fail ke {{ .dst }}'
|
||||
FileTaskCompress: 'Mampatkan fail ke {{ .dst }}'
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Dock
|
||||
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
|
||||
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
|
||||
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"
|
||||
FileTaskCopy: 'Copiar arquivos para {{ .dst }}'
|
||||
FileTaskMove: 'Mover arquivos para {{ .dst }}'
|
||||
FileTaskCompress: 'Compactar arquivos em {{ .dst }}'
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Сохранить состояние защи
|
||||
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kayd
|
||||
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
|
||||
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
|
||||
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"
|
||||
FileTaskCopy: 'Dosyaları {{ .dst }} konumuna kopyala'
|
||||
FileTaskMove: 'Dosyaları {{ .dst }} konumuna taşı'
|
||||
FileTaskCompress: 'Dosyaları {{ .dst }} konumuna sıkıştır'
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
|
||||
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
|
||||
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 }}"
|
||||
|
||||
@@ -719,3 +719,14 @@ FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
|
||||
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
|
||||
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 }}"
|
||||
|
||||
@@ -103,10 +103,12 @@ func repairIptablesBaseChains(clientName string) {
|
||||
if status != constant.StatusEnable {
|
||||
return
|
||||
}
|
||||
manager := iptables_helper.Manager{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,9 @@ func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,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...)
|
||||
|
||||
@@ -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,24 +68,20 @@ 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()
|
||||
if err := CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !m.runner.Exists("iptables-restore") {
|
||||
return errors.New("iptables-restore is not installed")
|
||||
}
|
||||
@@ -163,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 {
|
||||
@@ -177,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)
|
||||
@@ -218,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 {
|
||||
@@ -232,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"} {
|
||||
@@ -254,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
|
||||
@@ -262,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}
|
||||
@@ -278,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
|
||||
@@ -292,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
|
||||
}
|
||||
@@ -302,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)
|
||||
@@ -315,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
|
||||
}
|
||||
@@ -328,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)
|
||||
@@ -341,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
|
||||
}
|
||||
@@ -364,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
|
||||
}
|
||||
@@ -382,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) {
|
||||
@@ -447,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) {
|
||||
@@ -524,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
|
||||
}
|
||||
@@ -538,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
|
||||
@@ -556,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,13 +19,13 @@ 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") {
|
||||
@@ -36,10 +37,6 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
|
||||
if err := m.checkForwardPolicy(); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := m.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -49,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 {
|
||||
@@ -61,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
|
||||
}
|
||||
@@ -98,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} {
|
||||
@@ -109,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") {
|
||||
@@ -126,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
|
||||
}
|
||||
@@ -137,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}
|
||||
@@ -145,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
|
||||
@@ -174,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
|
||||
}
|
||||
@@ -213,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 {
|
||||
@@ -223,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
|
||||
}
|
||||
@@ -237,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 {
|
||||
@@ -251,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
|
||||
}
|
||||
@@ -312,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) {
|
||||
@@ -408,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
|
||||
}
|
||||
@@ -427,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
|
||||
}
|
||||
@@ -441,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...)
|
||||
}
|
||||
|
||||
@@ -497,9 +506,7 @@ func nftHasFirstUniqueJump(output string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP")
|
||||
|
||||
func (m *NftablesManager) checkForwardPolicy() error {
|
||||
func (m *Nftables) checkForwardPolicy() error {
|
||||
for _, family := range []struct{ command, name string }{
|
||||
{"iptables", FamilyIPv4},
|
||||
{"ip6tables", FamilyIPv6},
|
||||
@@ -519,7 +526,11 @@ func (m *NftablesManager) checkForwardPolicy() error {
|
||||
}
|
||||
found = true
|
||||
if fields[2] == "DROP" {
|
||||
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop}
|
||||
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])}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sort"
|
||||
@@ -12,141 +10,6 @@ import (
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
var ErrInvalidPolicy = errors.New("invalid Docker port guard request")
|
||||
|
||||
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 == "" {
|
||||
continue
|
||||
}
|
||||
prefix, err := netip.ParsePrefix(source)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
counts[key]--
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CanonicalHost(value string) string {
|
||||
if address, err := netip.ParseAddr(value); err == nil {
|
||||
return address.String()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeSources(value string) []string {
|
||||
result := []string{}
|
||||
_ = json.Unmarshal([]byte(value), &result)
|
||||
return result
|
||||
}
|
||||
|
||||
type observedPolicy struct {
|
||||
policy Policy
|
||||
sequence int64
|
||||
@@ -235,7 +98,7 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
||||
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...)
|
||||
inventory.ManagedRuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.managedOrders...)
|
||||
}
|
||||
return inventory, nil
|
||||
}
|
||||
@@ -260,12 +123,11 @@ func nativeRuleTokens(tokens []string) []string {
|
||||
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)}
|
||||
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] {
|
||||
@@ -317,7 +179,7 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
|
||||
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
|
||||
}
|
||||
case "accept", "drop", "return":
|
||||
if isCommentValue(tokens, index) {
|
||||
if index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment") {
|
||||
continue
|
||||
}
|
||||
action = tokens[index]
|
||||
@@ -337,20 +199,13 @@ func hasAcceptAction(tokens []string) bool {
|
||||
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) {
|
||||
if strings.EqualFold(token, "accept") && !(index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment")) {
|
||||
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()
|
||||
@@ -373,13 +228,6 @@ func parsePolicyPort(value string) uint16 {
|
||||
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))
|
||||
|
||||
@@ -1,15 +1,77 @@
|
||||
package docker_guard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"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"`
|
||||
@@ -30,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)
|
||||
@@ -40,118 +102,8 @@ type Runtime interface {
|
||||
ListPolicies() (PolicyInventory, error)
|
||||
}
|
||||
|
||||
func NewRuntime(provider string) Runtime {
|
||||
if provider == constant.FirewallProviderNftables {
|
||||
return NewNftablesManager()
|
||||
}
|
||||
return NewManager()
|
||||
}
|
||||
|
||||
func Verify(runtime Runtime, desired []Policy, preserved []ReadOnlyPolicy) error {
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify synchronized Docker firewall policies: %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)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
|
||||
|
||||
var ErrIPv4ForwardingDisabled = errors.New("IPv4 forwarding is disabled; set net.ipv4.ip_forward=1 before using Docker's firewall backend")
|
||||
|
||||
func CheckIPv4Forwarding() error {
|
||||
return checkIPv4Forwarding(os.ReadFile)
|
||||
}
|
||||
@@ -162,7 +114,7 @@ func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error {
|
||||
return fmt.Errorf("inspect IPv4 forwarding: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(string(value)) != "1" {
|
||||
return ErrIPv4ForwardingDisabled
|
||||
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,40 +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 ObservationSessionFactory interface {
|
||||
NewObservationSession() Adapter
|
||||
}
|
||||
|
||||
type CreatePlanner interface {
|
||||
Compile(DesiredChange) (BackendPlan, error)
|
||||
Applied(ObservedRule)
|
||||
}
|
||||
|
||||
type CreatePlannerFactory interface {
|
||||
NewCreatePlanner(Snapshot) CreatePlanner
|
||||
type MultiScopeReader interface {
|
||||
ListRuleScopes(context.Context, []Scope) ([]RuleSet, error)
|
||||
}
|
||||
|
||||
type RulePreparer interface {
|
||||
@@ -112,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
|
||||
}
|
||||
|
||||
@@ -120,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
|
||||
}
|
||||
|
||||
@@ -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,93 +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 ObservedRuleCollisionIndex(snapshot Snapshot) (RuleCollisionIndex, error) {
|
||||
index := make(RuleCollisionIndex, len(snapshot.Rules))
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.ParseStatus != ParseStatusSupported {
|
||||
continue
|
||||
}
|
||||
if err := index.Add(observed.Rule); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func normalizedRuleKey(normalized FirewallRule) (string, error) {
|
||||
identity := ruleIdentity{
|
||||
Scope: normalized.Scope.Key(),
|
||||
@@ -222,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 {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
@@ -71,273 +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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -110,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 {
|
||||
@@ -298,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, ",")
|
||||
|
||||
@@ -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,52 +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")
|
||||
canonical := nativeCanonical(rule)
|
||||
var runtimeExists, permanentExists bool
|
||||
for _, observed := range snapshot.Rules {
|
||||
if observed.Locator.Canonical != canonical {
|
||||
continue
|
||||
}
|
||||
runtimeExists = runtimeExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusRuntimeOnly
|
||||
permanentExists = permanentExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusPermanentOnly
|
||||
if runtimeExists && permanentExists {
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -464,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)
|
||||
}
|
||||
@@ -487,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
|
||||
@@ -511,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
|
||||
}
|
||||
@@ -547,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) {
|
||||
@@ -563,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
|
||||
@@ -594,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
|
||||
@@ -1079,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 {
|
||||
@@ -1093,35 +1100,3 @@ func validateSystemCommand(command filter.NativeCommand) error {
|
||||
}
|
||||
return fmt.Errorf("%w: firewalld command must target the managed input zone", filter.ErrUnsupportedScope)
|
||||
}
|
||||
|
||||
type createPlanner struct {
|
||||
adapter *Adapter
|
||||
snapshot filter.Snapshot
|
||||
byCanonical map[string][]filter.ObservedRule
|
||||
}
|
||||
|
||||
func (a *Adapter) NewCreatePlanner(snapshot filter.Snapshot) filter.CreatePlanner {
|
||||
byCanonical := make(map[string][]filter.ObservedRule, len(snapshot.Rules))
|
||||
for _, observed := range snapshot.Rules {
|
||||
byCanonical[observed.Locator.Canonical] = append(byCanonical[observed.Locator.Canonical], observed)
|
||||
}
|
||||
snapshot.Rules = nil
|
||||
return &createPlanner{adapter: a, snapshot: snapshot, byCanonical: byCanonical}
|
||||
}
|
||||
|
||||
func (p *createPlanner) Compile(change filter.DesiredChange) (filter.BackendPlan, error) {
|
||||
if change.Operation != filter.ChangeCreate || change.After == nil {
|
||||
return filter.BackendPlan{}, filter.ErrInvalidRule
|
||||
}
|
||||
rule, err := p.adapter.PrepareRule(*change.After)
|
||||
if err != nil {
|
||||
return filter.BackendPlan{}, err
|
||||
}
|
||||
snapshot := p.snapshot
|
||||
snapshot.Rules = p.byCanonical[nativeCanonical(rule)]
|
||||
return p.adapter.Compile(snapshot, []filter.DesiredChange{change})
|
||||
}
|
||||
|
||||
func (p *createPlanner) Applied(rule filter.ObservedRule) {
|
||||
p.byCanonical[rule.Locator.Canonical] = []filter.ObservedRule{rule}
|
||||
}
|
||||
|
||||
@@ -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,142 +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
|
||||
current.Rules = make([]filter.ObservedRule, len(snapshot.Rules), len(snapshot.Rules)+len(changes))
|
||||
copy(current.Rules, snapshot.Rules)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := filter.NewSnapshot(snapshot.Scope, current.Rules); 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 := snapshot.Rules
|
||||
firstChanged := nativePosition - 1
|
||||
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
|
||||
firstChanged = min(firstChanged, previousPosition-1)
|
||||
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 := firstChanged; index < len(rules); index++ {
|
||||
position := index + 1
|
||||
rules[index].Locator.Position = &position
|
||||
}
|
||||
snapshot.Rules = rules
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -317,56 +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
|
||||
}
|
||||
byMarker := make(map[string][]int, len(snapshot.Rules))
|
||||
for index, observed := range snapshot.Rules {
|
||||
byMarker[observed.Marker] = append(byMarker[observed.Marker], index)
|
||||
}
|
||||
for _, expected := range plan.Rules {
|
||||
markerMatches := 0
|
||||
semanticMatches := 0
|
||||
for _, index := range byMarker[expected.Expected.Marker] {
|
||||
observed := snapshot.Rules[index]
|
||||
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 _, index := range byMarker[expected.Expected.Marker] {
|
||||
observed := snapshot.Rules[index]
|
||||
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")
|
||||
}
|
||||
@@ -382,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
|
||||
}
|
||||
@@ -395,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
|
||||
@@ -432,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)
|
||||
}
|
||||
@@ -489,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 {
|
||||
@@ -525,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)}
|
||||
@@ -572,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" {
|
||||
@@ -599,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 {
|
||||
@@ -629,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)
|
||||
}
|
||||
@@ -673,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)
|
||||
}
|
||||
@@ -691,17 +612,27 @@ 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 := (systemBackend{}).ListTable(ctx, scope)
|
||||
return chainOutput(scope, output, err)
|
||||
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
|
||||
}
|
||||
if !containsChainDeclaration(output, scope.Chain) {
|
||||
return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func containsChainDeclaration(output, chain string) bool {
|
||||
@@ -760,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 {
|
||||
@@ -937,54 +864,6 @@ func takeValue(args []string, index *int, target *string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type tableReader interface {
|
||||
ListTable(context.Context, filter.Scope) (string, error)
|
||||
}
|
||||
|
||||
type tableRead struct {
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
type tableObservationReader struct {
|
||||
reader tableReader
|
||||
tables map[string]tableRead
|
||||
}
|
||||
|
||||
func (r *tableObservationReader) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
key := string(scope.Family) + ":" + scope.Table
|
||||
read, exists := r.tables[key]
|
||||
if !exists {
|
||||
read.output, read.err = r.reader.ListTable(ctx, scope)
|
||||
r.tables[key] = read
|
||||
}
|
||||
return chainOutput(scope, read.output, read.err)
|
||||
}
|
||||
|
||||
func (a *Adapter) NewObservationSession() filter.Adapter {
|
||||
reader, ok := a.reader.(tableReader)
|
||||
if !ok {
|
||||
return a
|
||||
}
|
||||
return &Adapter{
|
||||
reader: &tableObservationReader{reader: reader, tables: make(map[string]tableRead)},
|
||||
writer: a.writer, checker: a.checker,
|
||||
}
|
||||
}
|
||||
|
||||
func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) {
|
||||
return native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
|
||||
}
|
||||
|
||||
func chainOutput(scope filter.Scope, output string, err error) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !containsChainDeclaration(output, scope.Chain) {
|
||||
return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain)
|
||||
}
|
||||
return output, nil
|
||||
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,70 +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
|
||||
current.Rules = make([]filter.ObservedRule, len(snapshot.Rules), len(snapshot.Rules)+len(changes))
|
||||
copy(current.Rules, snapshot.Rules)
|
||||
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.Rules = rules
|
||||
}
|
||||
if _, err := filter.NewSnapshot(snapshot.Scope, current.Rules); 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,62 +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
|
||||
}
|
||||
byMarker := make(map[string][]int, len(snapshot.Rules))
|
||||
for index, observed := range snapshot.Rules {
|
||||
byMarker[observed.Marker] = append(byMarker[observed.Marker], index)
|
||||
}
|
||||
for _, expected := range plan.Rules {
|
||||
matches := 0
|
||||
for _, index := range byMarker[expected.Expected.Marker] {
|
||||
observed := snapshot.Rules[index]
|
||||
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
|
||||
}
|
||||
@@ -201,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
|
||||
}
|
||||
@@ -237,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)
|
||||
}
|
||||
@@ -251,70 +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 := 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, "")
|
||||
for index := position - 1; index < len(rules); index++ {
|
||||
position := index + 1
|
||||
rules[index].Locator.Position = &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 := min(position, target) - 1; index < len(rules); index++ {
|
||||
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 {
|
||||
@@ -324,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
|
||||
}
|
||||
@@ -334,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" {
|
||||
@@ -593,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(
|
||||
@@ -602,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)
|
||||
@@ -616,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,41 +323,53 @@ 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 && normalized.OrderIndex != nil && position == 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 || position == maximumObservedPosition(snapshot)+1 {
|
||||
} else if change.Append || normalized.OrderIndex == nil || hasSnapshot && position == maximumObservedPosition(snapshot)+1 {
|
||||
command = commentCommand(normalized, marker)
|
||||
}
|
||||
if command.Args[0] != "insert" {
|
||||
@@ -350,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)
|
||||
@@ -371,27 +402,27 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
}
|
||||
case filter.ChangeUpdate, filter.ChangeReorder:
|
||||
if change.Before == nil {
|
||||
return filter.NativeRulePlan{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
|
||||
return filter.RuleCommands{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
|
||||
}
|
||||
before, err := filter.NormalizeRule(*change.Before)
|
||||
if err != nil {
|
||||
return filter.NativeRulePlan{}, err
|
||||
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,
|
||||
)
|
||||
@@ -415,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
|
||||
}
|
||||
@@ -445,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)
|
||||
}
|
||||
@@ -515,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)
|
||||
}
|
||||
@@ -523,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
|
||||
@@ -620,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
|
||||
}
|
||||
@@ -673,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) {
|
||||
@@ -689,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 {
|
||||
@@ -710,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) {
|
||||
@@ -767,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])
|
||||
@@ -785,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,
|
||||
@@ -922,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])
|
||||
@@ -992,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
|
||||
@@ -1036,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 {
|
||||
@@ -1068,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)
|
||||
@@ -1101,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 {
|
||||
@@ -1110,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)
|
||||
@@ -1168,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 {
|
||||
@@ -1180,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,
|
||||
@@ -1191,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) {
|
||||
@@ -1240,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"),
|
||||
@@ -1250,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,123 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
)
|
||||
|
||||
const inventoryTTL = 2 * time.Second
|
||||
|
||||
var inventoryGeneration atomic.Uint64
|
||||
|
||||
func InvalidateInventory() { inventoryGeneration.Add(1) }
|
||||
|
||||
type inventoryEntry struct {
|
||||
snapshots []filter.Snapshot
|
||||
expires time.Time
|
||||
generation uint64
|
||||
}
|
||||
|
||||
type inventoryCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]inventoryEntry
|
||||
reading map[string]chan struct{}
|
||||
}
|
||||
|
||||
func (e *Engine) ObserveInventory(ctx context.Context, scope filter.Scope, refresh bool) (filter.Snapshot, error) {
|
||||
snapshots, err := e.ObserveInventoryScopes(ctx, []filter.Scope{scope}, refresh)
|
||||
if err != nil {
|
||||
return filter.Snapshot{}, err
|
||||
}
|
||||
return snapshots[0], nil
|
||||
}
|
||||
|
||||
func (e *Engine) ObserveInventoryScopes(ctx context.Context, scopes []filter.Scope, refresh bool) ([]filter.Snapshot, error) {
|
||||
read := func() ([]filter.Snapshot, error) {
|
||||
if len(scopes) == 1 {
|
||||
snapshot, err := e.adapter.Observe(ctx, scopes[0])
|
||||
return []filter.Snapshot{snapshot}, err
|
||||
}
|
||||
observer, ok := e.adapter.(filter.MultiScopeObserver)
|
||||
if !ok {
|
||||
return nil, filter.ErrAdapterUnavailable
|
||||
}
|
||||
return observer.ObserveScopes(ctx, scopes)
|
||||
}
|
||||
var snapshots []filter.Snapshot
|
||||
var err error
|
||||
if e.Provider() == filter.ProviderFirewalld || e.Provider() == filter.ProviderUFW {
|
||||
keys := make([]string, len(scopes))
|
||||
for i, scope := range scopes {
|
||||
keys[i] = scope.Normalize().Key()
|
||||
}
|
||||
snapshots, err = e.inventory.load(ctx, strings.Join(keys, "\n"), refresh, read)
|
||||
} else {
|
||||
snapshots, err = read()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := append([]filter.Snapshot(nil), snapshots...)
|
||||
for i := range result {
|
||||
result[i].Rules = append([]filter.ObservedRule(nil), snapshots[i].Rules...)
|
||||
result[i].Notices = append([]filter.ScopeNotice(nil), snapshots[i].Notices...)
|
||||
if e.policy != nil {
|
||||
result[i], err = e.policy(ctx, result[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *inventoryCache) load(ctx context.Context, key string, refresh bool, read func() ([]filter.Snapshot, error)) ([]filter.Snapshot, error) {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.mu.Lock()
|
||||
generation := inventoryGeneration.Load()
|
||||
if entry, ok := c.entries[key]; !refresh && ok && entry.generation == generation && time.Now().Before(entry.expires) {
|
||||
c.mu.Unlock()
|
||||
return entry.snapshots, nil
|
||||
}
|
||||
if done := c.reading[key]; done != nil {
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-done:
|
||||
refresh = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if c.reading == nil {
|
||||
c.reading = make(map[string]chan struct{})
|
||||
}
|
||||
done := make(chan struct{})
|
||||
c.reading[key] = done
|
||||
delete(c.entries, key)
|
||||
c.mu.Unlock()
|
||||
snapshots, err := read()
|
||||
c.mu.Lock()
|
||||
if err == nil && generation == inventoryGeneration.Load() {
|
||||
if len(c.entries) >= 32 {
|
||||
clear(c.entries)
|
||||
}
|
||||
if c.entries == nil {
|
||||
c.entries = make(map[string]inventoryEntry)
|
||||
}
|
||||
c.entries[key] = inventoryEntry{snapshots, time.Now().Add(inventoryTTL), generation}
|
||||
}
|
||||
delete(c.reading, key)
|
||||
close(done)
|
||||
c.mu.Unlock()
|
||||
return snapshots, err
|
||||
}
|
||||
}
|
||||
@@ -1,404 +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
|
||||
inventory inventoryCache
|
||||
}
|
||||
|
||||
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) NewObservationSession() *Engine {
|
||||
if factory, ok := e.adapter.(filter.ObservationSessionFactory); ok {
|
||||
return New(factory.NewObservationSession(), e.policy)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
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 {
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
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 {
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
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) (filter.ApplyResult, error) {
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return filter.ApplyResult{}, 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 filter.ApplyResult{}, err
|
||||
}
|
||||
plan.CommandOnly = true
|
||||
return e.adapter.Apply(ctx, plan)
|
||||
}
|
||||
|
||||
func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) {
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
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 {
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
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)
|
||||
}
|
||||
|
||||
func (e *Engine) NewCreatePlanner(snapshot filter.Snapshot) (filter.CreatePlanner, error) {
|
||||
factory, ok := e.adapter.(filter.CreatePlannerFactory)
|
||||
if !ok {
|
||||
return nil, filter.ErrAdapterUnavailable
|
||||
}
|
||||
return factory.NewCreatePlanner(snapshot), nil
|
||||
}
|
||||
|
||||
func (e *Engine) ExecutePlannedCreate(ctx context.Context, planner filter.CreatePlanner, change filter.DesiredChange) (filter.ObservedRule, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
plan, err := planner.Compile(change)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
if !plan.CreatesOnly() {
|
||||
return filter.ObservedRule{}, filter.ErrInvalidRule
|
||||
}
|
||||
InvalidateInventory()
|
||||
defer InvalidateInventory()
|
||||
plan.CommandOnly = change.CommandOnly
|
||||
result, err := e.adapter.Apply(ctx, plan)
|
||||
if err != nil {
|
||||
return filter.ObservedRule{}, err
|
||||
}
|
||||
if len(result.Applied) != 1 {
|
||||
return filter.ObservedRule{}, filter.ErrVerificationFailed
|
||||
}
|
||||
planner.Applied(result.Applied[0])
|
||||
return result.Applied[0], nil
|
||||
}
|
||||
@@ -17,27 +17,65 @@ 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 {
|
||||
if rules[index].ParseStatus == ParseStatusSupported && RuleMatchesPortWhitelist(rules[index].Rule, ports) {
|
||||
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
|
||||
@@ -51,36 +89,7 @@ func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
|
||||
families = []Family{FamilyIPv4, FamilyIPv6}
|
||||
}
|
||||
for _, family := range families {
|
||||
matched := false
|
||||
for _, port := range ports {
|
||||
portFamily := Family(strings.ToLower(strings.TrimSpace(port.Family)))
|
||||
if portFamily != "" && !familiesOverlap(family, portFamily) {
|
||||
continue
|
||||
}
|
||||
protocol, err := normalizeProtocol(port.Protocol)
|
||||
if err != nil || rule.Protocol != protocol {
|
||||
continue
|
||||
}
|
||||
portRange, err := normalizePort(port.Port)
|
||||
if err != nil || rule.DestinationPort != portRange {
|
||||
continue
|
||||
}
|
||||
sources := port.Sources
|
||||
if len(sources) == 0 {
|
||||
sources = []string{""}
|
||||
}
|
||||
for _, source := range sources {
|
||||
normalized, err := normalizeAddress(source, family)
|
||||
if err == nil && normalized == rule.SourceAddress {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
if !index[portWhitelistKey{family, rule.Protocol, rule.DestinationPort, rule.SourceAddress}] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -153,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 {
|
||||
@@ -201,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) &&
|
||||
@@ -210,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
|
||||
@@ -278,25 +259,3 @@ func portInterval(value string) (int, int, error) {
|
||||
end, err := strconv.Atoi(parts[1])
|
||||
return start, end, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,91 +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 New(provider string) (Adapter, error) {
|
||||
switch provider {
|
||||
case "iptables":
|
||||
return newIptablesNATAdapter(provider), nil
|
||||
case "nftables":
|
||||
return newNftablesAdapter(), nil
|
||||
default:
|
||||
return nil, errors.New("unsupported forwarding provider: " + provider)
|
||||
}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
|
||||
@@ -24,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
|
||||
}
|
||||
@@ -58,7 +58,7 @@ 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
|
||||
@@ -70,8 +70,12 @@ func (systemIptablesBackend) Restore(family, input string) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -86,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{}
|
||||
@@ -99,29 +102,25 @@ 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() ([]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)
|
||||
@@ -137,7 +136,7 @@ func (l *iptablesNATAdapter) List() ([]Rule, error) {
|
||||
return append(rules, parseIptablesRules(stdout, FamilyIPv6)...), nil
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Reconcile(rules []Rule) error {
|
||||
func (l *Iptables) ReplaceRules(rules []Rule) error {
|
||||
byFamily := map[string][]Rule{
|
||||
FamilyIPv4: nil,
|
||||
FamilyIPv6: nil,
|
||||
@@ -147,36 +146,114 @@ func (l *iptablesNATAdapter) Reconcile(rules []Rule) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalized.Family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return fmt.Errorf("ip6tables command family is unavailable")
|
||||
}
|
||||
byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized)
|
||||
}
|
||||
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 []Rule) (string, error) {
|
||||
natRules := [][]string{{"-F", ChainPreRouting}, {"-F", ChainPostRouting}}
|
||||
filterRules := [][]string{{"-F", 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", ChainPreRouting}
|
||||
preRouting := []string{verb, ChainPreRouting}
|
||||
if rule.Interface != "" {
|
||||
preRouting = append(preRouting, "-i", rule.Interface)
|
||||
}
|
||||
@@ -187,11 +264,11 @@ func buildIptablesForwardRestoreScript(rules []Rule) (string, error) {
|
||||
}
|
||||
natRules = append(natRules,
|
||||
append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)),
|
||||
[]string{"-A", 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", ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
|
||||
[]string{"-A", 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
|
||||
@@ -233,23 +310,14 @@ func isRemoteTarget(family, target string) bool {
|
||||
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{FamilyIPv4, FamilyIPv6} {
|
||||
if family == 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 == FamilyIPv6 {
|
||||
list = l.backend.RunIPv6WithStd
|
||||
@@ -266,13 +334,13 @@ 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 {
|
||||
func (l *Iptables) Cleanup() error {
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
continue
|
||||
@@ -291,7 +359,7 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -441,7 +509,7 @@ func containsExactLine(output, want string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) {
|
||||
func (l *Iptables) InitStatus() (bool, bool, error) {
|
||||
ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
@@ -453,10 +521,10 @@ func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) {
|
||||
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
|
||||
@@ -466,6 +534,9 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
|
||||
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)
|
||||
}
|
||||
@@ -495,7 +566,7 @@ 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) {
|
||||
func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
return false, false, nil
|
||||
}
|
||||
@@ -525,7 +596,7 @@ func containsExactRule(lines []string, rule string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *iptablesNATAdapter) Replay() error {
|
||||
func (l *Iptables) Replay() error {
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
||||
continue
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forwarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
)
|
||||
|
||||
@@ -22,18 +24,18 @@ const (
|
||||
nftForwardMarker = "1panel-forward:"
|
||||
)
|
||||
|
||||
type nftablesAdapter struct{ system forwardingSystem }
|
||||
type Nftables struct{ system forwardingSystem }
|
||||
|
||||
func newNftablesAdapter() *nftablesAdapter {
|
||||
return &nftablesAdapter{system: defaultForwardingSystem{}}
|
||||
func NewNftables() *Nftables {
|
||||
return &Nftables{system: defaultForwardingSystem{}}
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) Name() string { return "nftables" }
|
||||
func (n *Nftables) Name() string { return "nftables" }
|
||||
|
||||
func (n *nftablesAdapter) List() ([]Rule, error) {
|
||||
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, nftForwardChain(ChainPreRouting))
|
||||
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, "NFT_"+ChainPreRouting)
|
||||
if errors.Is(err, nftables_helper.ErrChainNotFound) {
|
||||
continue
|
||||
}
|
||||
@@ -45,28 +47,124 @@ func (n *nftablesAdapter) List() ([]Rule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) Reconcile(rules []Rule) error {
|
||||
if err := ensureNftForwardTables(); err != nil {
|
||||
return fmt.Errorf("initialize nftables forwarding table: %w", err)
|
||||
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)
|
||||
}
|
||||
commands, err := rebuildNftForwardCommands(rules)
|
||||
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
|
||||
}
|
||||
return nftRunCommands(commands)
|
||||
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 *nftablesAdapter) Enable() error {
|
||||
if err := ensureForwardingSysctls(n.system, true); err != nil {
|
||||
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(); err != nil {
|
||||
if err := ensureNftForwardTables(FamilyIPv4); err != nil {
|
||||
return fmt.Errorf("initialize nftables forwarding table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) Cleanup() error {
|
||||
func (n *Nftables) Cleanup() error {
|
||||
commands := make([][]string, 0, 2)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
tableFamily := nftTableFamily(family)
|
||||
@@ -76,7 +174,7 @@ func (n *nftablesAdapter) Cleanup() error {
|
||||
commands = append(commands, []string{"delete", "table", tableFamily, nftForwardTable})
|
||||
}
|
||||
if len(commands) > 0 {
|
||||
if err := nftRunCommands(commands); err != nil {
|
||||
if err := nftRunCommands(context.Background(), commands); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -87,34 +185,48 @@ func (n *nftablesAdapter) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) InitStatus() (bool, bool, error) {
|
||||
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 || !initialized || !bound {
|
||||
return initialized, bound, err
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
anyInitialized = anyInitialized || initialized
|
||||
anyBound = anyBound || bound
|
||||
}
|
||||
return true, true, nil
|
||||
return anyInitialized, anyBound, nil
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) FamilyStatus(family string) (bool, bool, error) {
|
||||
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 _, err := nftRun("list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)); err != nil {
|
||||
if _, exists := chains["NFT_"+chain]; !exists {
|
||||
return false, false, nil
|
||||
}
|
||||
}
|
||||
return true, strings.TrimSpace(string(data)) != "0", nil
|
||||
}
|
||||
|
||||
func (n *nftablesAdapter) Replay() error {
|
||||
func (n *Nftables) Replay() error {
|
||||
file := filepath.Join(global.Dir.FirewallDir, nftForwardFile)
|
||||
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
@@ -133,27 +245,28 @@ func (n *nftablesAdapter) Replay() error {
|
||||
return nftRunCommand("-f", file)
|
||||
}
|
||||
|
||||
func ensureNftForwardTables() error {
|
||||
func ensureNftForwardTables(families ...string) error {
|
||||
commands := make([][]string, 0, 8)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
for _, family := range families {
|
||||
tableFamily := nftTableFamily(family)
|
||||
tableExists := true
|
||||
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
|
||||
tableExists = false
|
||||
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
|
||||
}{
|
||||
{nftForwardChain(ChainPreRouting), "nat", "prerouting", "-100"},
|
||||
{nftForwardChain(ChainPostRouting), "nat", "postrouting", "100"},
|
||||
{nftForwardChain(ChainForward), "filter", "forward", "0"},
|
||||
{"NFT_" + ChainPreRouting, "nat", "prerouting", "-100"},
|
||||
{"NFT_" + ChainPostRouting, "nat", "postrouting", "100"},
|
||||
{"NFT_" + ChainForward, "filter", "forward", "0"},
|
||||
}
|
||||
for _, chain := range chains {
|
||||
if tableExists {
|
||||
if _, err := nftRun("list", "chain", tableFamily, nftForwardTable, chain.name); err == nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := existingChains[chain.name]; exists {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, []string{
|
||||
"add", "chain", tableFamily, nftForwardTable, chain.name,
|
||||
@@ -164,16 +277,20 @@ func ensureNftForwardTables() error {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
return nftRunCommands(commands)
|
||||
return nftRunCommands(context.Background(), commands)
|
||||
}
|
||||
|
||||
func rebuildNftForwardCommands(rules []Rule) ([][]string, error) {
|
||||
func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) {
|
||||
commands := make([][]string, 0, 6+len(rules)*4)
|
||||
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
||||
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
|
||||
commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)})
|
||||
}
|
||||
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 {
|
||||
@@ -181,25 +298,25 @@ func rebuildNftForwardCommands(rules []Rule) ([][]string, error) {
|
||||
}
|
||||
rule = normalized
|
||||
tableFamily := nftTableFamily(rule.Family)
|
||||
addressKeyword := nftAddressKeyword(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, nftForwardChain(ChainPreRouting)}
|
||||
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, nftForwardChain(ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment},
|
||||
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment},
|
||||
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment},
|
||||
[]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, nftForwardChain(ChainPreRouting)}
|
||||
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)
|
||||
@@ -214,13 +331,6 @@ func nftTableFamily(family string) string {
|
||||
return nftForwardFamily
|
||||
}
|
||||
|
||||
func nftAddressKeyword(family string) string {
|
||||
if family == FamilyIPv6 {
|
||||
return "ip6"
|
||||
}
|
||||
return "ip"
|
||||
}
|
||||
|
||||
func encodeNftForwardRule(rule Rule) string {
|
||||
family, protocol := "4", "t"
|
||||
if rule.Family == FamilyIPv6 {
|
||||
@@ -312,10 +422,6 @@ func parseNftForwardRules(stdout string) []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 {
|
||||
@@ -332,12 +438,17 @@ func nftRunCommand(args ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func nftRunCommands(commands [][]string) error {
|
||||
func nftRunCommands(ctx context.Context, commands [][]string) error {
|
||||
script, err := nftCommandsScript(commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nftables_helper.RunScript(script)
|
||||
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) {
|
||||
|
||||
@@ -4,12 +4,24 @@ 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")
|
||||
@@ -27,12 +39,93 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error {
|
||||
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 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"}
|
||||
|
||||
@@ -10,14 +10,6 @@ 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(ports)
|
||||
}
|
||||
|
||||
func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil || !commands.IPv6Available() {
|
||||
@@ -40,16 +32,7 @@ func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
|
||||
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 {
|
||||
|
||||
@@ -17,13 +17,8 @@ import (
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
UpdateSetting func(key, value string) error
|
||||
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 {
|
||||
@@ -43,31 +38,31 @@ 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 := 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
|
||||
}
|
||||
}
|
||||
@@ -75,26 +70,32 @@ func (m *Manager) enableBase(prepare bool) error {
|
||||
return err
|
||||
}
|
||||
if prepare {
|
||||
if err := m.ensureIPv6BaseChains(); err != nil {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.SyncRequiredPorts(true); err != nil {
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseChainsFamily(ipv6 bool) error {
|
||||
@@ -224,13 +225,28 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -322,27 +338,7 @@ func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts .
|
||||
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
|
||||
@@ -422,12 +418,7 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
|
||||
return save(FilterTab, BasicAfterChain, afterFile)
|
||||
}
|
||||
|
||||
func buildRequiredPortsRestoreScript(
|
||||
desired []firewall.SystemPort,
|
||||
family string,
|
||||
beforeRaw, afterRaw string,
|
||||
includeDefaults bool,
|
||||
) string {
|
||||
func buildRequiredPortsRestoreScript(desired []firewall.SystemPort, family string, beforeRaw, afterRaw string, includeDefaults bool) string {
|
||||
var commands []string
|
||||
for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
|
||||
if !containsIptablesRule(beforeRaw, line) {
|
||||
@@ -500,17 +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) 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +36,6 @@ 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(ports, family)
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -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,178 +0,0 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
|
||||
)
|
||||
|
||||
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
|
||||
RunAction func(operation, name string, action func() error) error
|
||||
}
|
||||
|
||||
// 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) runAction(operation, name string, action func() error) error {
|
||||
if o.RunAction != nil {
|
||||
return o.RunAction(operation, name, action)
|
||||
}
|
||||
return action()
|
||||
}
|
||||
|
||||
func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error {
|
||||
var recoveryErrors []error
|
||||
switch operation {
|
||||
case OperationStart:
|
||||
if err := o.runAction("Start", o.client.Name(), o.client.Start); err != nil {
|
||||
return err
|
||||
}
|
||||
if prepareStart != nil {
|
||||
if err := o.prepareAfterStart(prepareStart); 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.runAction("TaskRestart", o.client.Name(), o.client.Restart); err != nil {
|
||||
return err
|
||||
}
|
||||
if prepareStart != nil {
|
||||
if err := o.prepareAfterStart(prepareStart); 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 := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
|
||||
recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err})
|
||||
}
|
||||
}
|
||||
if o.client.Name() == ProviderFirewalld && operation == OperationStart {
|
||||
if err := o.runAction("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil {
|
||||
recoveryErrors = append(recoveryErrors, err)
|
||||
}
|
||||
}
|
||||
if err := errors.Join(recoveryErrors...); err != nil {
|
||||
return &CompletedOperationError{Operation: operation, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Operator) prepareAfterStart(prepare func(Client) error) error {
|
||||
if err := prepare(o.client); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.client.Name() == ProviderFirewalld {
|
||||
return providers.RemoveFirewalldSSHService()
|
||||
}
|
||||
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.runAction("Stop", o.client.Name(), o.client.Stop); err != nil {
|
||||
return err
|
||||
}
|
||||
if withDockerRestart {
|
||||
if err := o.runAction("TaskRestart", "Docker", func() error { return 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
|
||||
}
|
||||
@@ -38,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)
|
||||
@@ -46,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 {
|
||||
@@ -70,15 +63,13 @@ func (f *Firewalld) Start() error {
|
||||
}
|
||||
|
||||
func RemoveFirewalldSSHService() error {
|
||||
for _, permanent := range []bool{true, false} {
|
||||
args := []string{"--zone=" + filter.FirewalldInputZone, "--remove-service=ssh"}
|
||||
configuration := "runtime"
|
||||
if permanent {
|
||||
args = append(args, "--permanent")
|
||||
configuration = "permanent"
|
||||
}
|
||||
if _, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", args...); err != nil {
|
||||
return fmt.Errorf("remove firewalld SSH service from %s configuration: %w", configuration, err)
|
||||
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
|
||||
@@ -121,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,27 +4,20 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -40,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})
|
||||
}
|
||||
@@ -124,12 +106,8 @@ func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string
|
||||
"accept", "comment", `"`+requiredPortComment+`"`)
|
||||
}
|
||||
|
||||
func (m *Manager) initPreRules() error {
|
||||
ports, err := m.loadRequiredPorts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err = firewall.NormalizeRequiredPorts(ports)
|
||||
func initPreRules(requiredPorts []firewall.PortWhitelist) error {
|
||||
ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -218,20 +196,6 @@ func containsRequiredPortRule(output, expression string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) updateSetting(key, value string) error {
|
||||
if m != nil && m.UpdateSetting != nil {
|
||||
return m.UpdateSetting(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
|
||||
if m != nil && m.LoadRequiredPorts != nil {
|
||||
return m.LoadRequiredPorts()
|
||||
}
|
||||
return nil, fmt.Errorf("load required firewall ports is not configured")
|
||||
}
|
||||
|
||||
func Bind() error {
|
||||
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
||||
tableFamily := TableFamily(family)
|
||||
|
||||
@@ -169,23 +169,53 @@ func hasBaseChainBinding(output string) bool {
|
||||
}
|
||||
|
||||
func loadFamilyInitStatus(family filter.Family) (bool, bool, error) {
|
||||
for _, chain := range BasicChains() {
|
||||
if _, exists, err := readNftObject(run, "list", "chain", TableFamily(family), TableName, chain); err != nil || !exists {
|
||||
return false, false, err
|
||||
}
|
||||
}
|
||||
stdout, exists, err := readNftObject(run, "list", "chain", TableFamily(family), TableName, InputChain)
|
||||
output, exists, err := ReadTable(run, TableFamily(family), TableName)
|
||||
if err != nil || !exists {
|
||||
return false, false, err
|
||||
}
|
||||
chains := ParseTableChains(output)
|
||||
for _, chain := range BasicChains() {
|
||||
if !strings.Contains(stdout, "jump "+chain) {
|
||||
if _, exists := chains[chain]; !exists {
|
||||
return false, false, nil
|
||||
}
|
||||
}
|
||||
input, exists := chains[InputChain]
|
||||
if !exists {
|
||||
return false, false, nil
|
||||
}
|
||||
for _, chain := range BasicChains() {
|
||||
if !strings.Contains(input, "jump "+chain) {
|
||||
return true, false, nil
|
||||
}
|
||||
}
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
func ReadTable(run func(...string) (string, error), family, table string) (string, bool, error) {
|
||||
return readNftObject(run, "-a", "list", "table", family, table)
|
||||
}
|
||||
|
||||
func ParseTableChains(output string) map[string]string {
|
||||
chains := make(map[string]string)
|
||||
lines := strings.Split(output, "\n")
|
||||
name, indent, start := "", "", 0
|
||||
for index, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if name == "" {
|
||||
fields := strings.Fields(trimmed)
|
||||
if len(fields) >= 3 && fields[0] == "chain" && fields[2] == "{" {
|
||||
name, indent, start = fields[1], line[:len(line)-len(strings.TrimLeft(line, " \t"))], index
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, indent+"}") {
|
||||
chains[name] = strings.Join(lines[start:index+1], "\n")
|
||||
name = ""
|
||||
}
|
||||
}
|
||||
return chains
|
||||
}
|
||||
|
||||
var ErrChainNotFound = errors.New("nftables chain is not initialized")
|
||||
|
||||
func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) {
|
||||
|
||||
@@ -19,96 +19,3 @@ const (
|
||||
ReasonUnsafeRemoval ReasonCode = "unsafe_managed_rule_removal"
|
||||
ReasonReadOnlyRule ReasonCode = "read_only_rule"
|
||||
)
|
||||
|
||||
func ReasonMessage(code ReasonCode) string {
|
||||
switch code {
|
||||
case ReasonAlreadyExists:
|
||||
return "rule already exists in target backend"
|
||||
case ReasonOnlyExistsInTarget:
|
||||
return "rule exists only in target backend"
|
||||
case ReasonManagedOnlyInTarget:
|
||||
return "managed rule exists only in target backend"
|
||||
case ReasonUnsafeRemoval:
|
||||
return "managed runtime rule cannot be safely removed"
|
||||
case ReasonReadOnlyRule:
|
||||
return "read-only runtime rule is preserved but cannot be synchronized"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type Desired[T any, P any] struct {
|
||||
Value T
|
||||
Payload P
|
||||
Err error
|
||||
}
|
||||
|
||||
type Item[P any] struct {
|
||||
Payload P
|
||||
Status Status
|
||||
ReasonCode ReasonCode
|
||||
Reason string
|
||||
}
|
||||
|
||||
func Diff[T any, P any](desired []Desired[T, P], actual []T, key func(T) string, actualPayload func(T) P) []Item[P] {
|
||||
items := make([]Item[P], 0, len(desired)+len(actual))
|
||||
actualByKey := make(map[string][]int, len(actual))
|
||||
for index, value := range actual {
|
||||
actualByKey[key(value)] = append(actualByKey[key(value)], index)
|
||||
}
|
||||
matched := make([]bool, len(actual))
|
||||
for _, candidate := range desired {
|
||||
item := Item[P]{Payload: candidate.Payload}
|
||||
switch {
|
||||
case candidate.Err != nil:
|
||||
item.Status, item.ReasonCode, item.Reason = StatusBlocked, ReasonInvalidPolicy, candidate.Err.Error()
|
||||
default:
|
||||
match := unmatchedIndex(actualByKey[key(candidate.Value)], matched)
|
||||
if match >= 0 {
|
||||
matched[match] = true
|
||||
item.Status, item.ReasonCode = StatusExisting, ReasonAlreadyExists
|
||||
item.Reason = ReasonMessage(item.ReasonCode)
|
||||
} else {
|
||||
item.Status = StatusReady
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
for index, value := range actual {
|
||||
if matched[index] {
|
||||
continue
|
||||
}
|
||||
items = append(items, Item[P]{
|
||||
Payload: actualPayload(value), Status: StatusRemove, ReasonCode: ReasonOnlyExistsInTarget,
|
||||
Reason: ReasonMessage(ReasonOnlyExistsInTarget),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func StatesEqual[T any](left, right []T, key func(T) string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
counts := make(map[string]int, len(left))
|
||||
for _, value := range left {
|
||||
counts[key(value)]++
|
||||
}
|
||||
for _, value := range right {
|
||||
valueKey := key(value)
|
||||
if counts[valueKey] == 0 {
|
||||
return false
|
||||
}
|
||||
counts[valueKey]--
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func unmatchedIndex(indices []int, matched []bool) int {
|
||||
for _, index := range indices {
|
||||
if !matched[index] {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
)
|
||||
|
||||
func RuleOrder(snapshot filter.Snapshot, ordered []filter.InventoryItem) map[string]bool {
|
||||
drifted := make(map[string]bool)
|
||||
if snapshot.Scope.Provider == filter.ProviderFirewalld {
|
||||
return drifted
|
||||
}
|
||||
markers := make([]string, 0, len(ordered))
|
||||
projected := append([]filter.ObservedRule(nil), snapshot.Rules...)
|
||||
markerSet := make(map[string]bool, len(ordered))
|
||||
locations := make(map[string][]int, len(projected))
|
||||
for index, rule := range projected {
|
||||
key := locatorBucket(rule.Locator)
|
||||
locations[key] = append(locations[key], index)
|
||||
}
|
||||
for _, item := range ordered {
|
||||
if item.Desired == nil || item.Desired.Marker == "" {
|
||||
continue
|
||||
}
|
||||
markers = append(markers, item.Desired.Marker)
|
||||
markerSet[item.Desired.Marker] = true
|
||||
if item.Observed != nil {
|
||||
for _, index := range locations[locatorBucket(item.Observed.Locator)] {
|
||||
if filter.SameLocator(projected[index].Locator, item.Observed.Locator) {
|
||||
projected[index].Marker = item.Desired.Marker
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actual := make([]string, 0, len(markers))
|
||||
presentMarkers := make(map[string]bool, len(markers))
|
||||
for _, rule := range projected {
|
||||
if markerSet[rule.Marker] {
|
||||
actual = append(actual, rule.Marker)
|
||||
presentMarkers[rule.Marker] = true
|
||||
}
|
||||
}
|
||||
present := make([]string, 0, len(actual))
|
||||
for _, marker := range markers {
|
||||
if presentMarkers[marker] {
|
||||
present = append(present, marker)
|
||||
}
|
||||
}
|
||||
for index, marker := range present {
|
||||
if actual[index] != marker {
|
||||
drifted[marker] = true
|
||||
drifted[actual[index]] = true
|
||||
}
|
||||
}
|
||||
return drifted
|
||||
}
|
||||
|
||||
func InsertionPosition(snapshot filter.Snapshot, markers []string, target string) *int64 {
|
||||
if snapshot.Scope.Provider == filter.ProviderFirewalld {
|
||||
return nil
|
||||
}
|
||||
positions := make(map[string]int, len(snapshot.Rules))
|
||||
for _, rule := range snapshot.Rules {
|
||||
if _, exists := positions[rule.Marker]; !exists && rule.Locator.Position != nil {
|
||||
positions[rule.Marker] = *rule.Locator.Position
|
||||
}
|
||||
}
|
||||
targetIndex := slices.Index(markers, target)
|
||||
for index := targetIndex - 1; index >= 0; index-- {
|
||||
if previous, ok := positions[markers[index]]; ok {
|
||||
position := int64(previous + 1)
|
||||
return &position
|
||||
}
|
||||
}
|
||||
if targetIndex >= 0 {
|
||||
for _, marker := range markers[targetIndex+1:] {
|
||||
if next, ok := positions[marker]; ok {
|
||||
position := int64(next)
|
||||
return &position
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteChange(snapshot filter.Snapshot, previous filter.ObservedRule, desired filter.DesiredRule) (filter.DesiredChange, error) {
|
||||
beforeKey, err := filter.RuleKey(previous.Rule)
|
||||
if err != nil {
|
||||
return filter.DesiredChange{}, err
|
||||
}
|
||||
matches := make([]filter.ObservedRule, 0, 1)
|
||||
for _, current := range snapshot.Rules {
|
||||
if previous.Marker != "" {
|
||||
if current.Marker != previous.Marker {
|
||||
continue
|
||||
}
|
||||
} else if current.Marker != "" || !filter.SameLocator(current.Locator, previous.Locator) {
|
||||
continue
|
||||
}
|
||||
key, err := filter.RuleKey(current.Rule)
|
||||
if err == nil && key == beforeKey {
|
||||
matches = append(matches, current)
|
||||
}
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
return filter.DesiredChange{}, filter.ErrRuleStale
|
||||
}
|
||||
current := matches[0]
|
||||
if err := filter.GuardMutation(current); err != nil {
|
||||
return filter.DesiredChange{}, err
|
||||
}
|
||||
before := ObservedRule(current)
|
||||
if before.UUID == "" {
|
||||
before.UUID = desired.Rule.UUID
|
||||
}
|
||||
return filter.DesiredChange{Operation: filter.ChangeDelete, Before: &before, Locator: ¤t.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil
|
||||
}
|
||||
|
||||
func ObservedRule(observed filter.ObservedRule) filter.FirewallRule {
|
||||
rule := observed.Rule
|
||||
if rule.UUID == "" && strings.HasPrefix(observed.Marker, "1panel-rule:") {
|
||||
rule.UUID = strings.TrimSpace(strings.TrimPrefix(observed.Marker, "1panel-rule:"))
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func locatorBucket(locator filter.Locator) string {
|
||||
if locator.Position != nil {
|
||||
return locator.ScopeKey + "\x00position:" + strconv.Itoa(*locator.Position)
|
||||
}
|
||||
return locator.ScopeKey + "\x00canonical:" + locator.Canonical
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package components
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -526,7 +527,61 @@ func (s *Server) AddHTTP2HTTPS(httpsPort int) {
|
||||
}
|
||||
|
||||
newDir.Block = block
|
||||
s.UpdateDirectiveBySecondKey("if", "($scheme", newDir)
|
||||
directives := make([]IDirective, 0, len(s.Directives)+1)
|
||||
insertAt := -1
|
||||
for _, dir := range s.Directives {
|
||||
params := dir.GetParameters()
|
||||
isHTTPRedirect := isHTTP2HTTPSRedirect(dir)
|
||||
isRedirectInclude := dir.GetName() == "include" && len(params) > 0 &&
|
||||
strings.HasSuffix(strings.Trim(params[0], "\"'"), "/redirect/*.conf")
|
||||
if insertAt == -1 && (isHTTPRedirect || isRedirectInclude) {
|
||||
insertAt = len(directives)
|
||||
}
|
||||
if !isHTTPRedirect {
|
||||
directives = append(directives, dir)
|
||||
}
|
||||
}
|
||||
if insertAt == -1 {
|
||||
insertAt = len(directives)
|
||||
}
|
||||
directives = append(directives, nil)
|
||||
copy(directives[insertAt+1:], directives[insertAt:])
|
||||
directives[insertAt] = &newDir
|
||||
s.Directives = directives
|
||||
}
|
||||
|
||||
func isHTTP2HTTPSRedirect(dir IDirective) bool {
|
||||
condition := strings.Join(dir.GetParameters(), " ")
|
||||
normalizedCondition := strings.Join(strings.Fields(condition), "")
|
||||
if dir.GetName() != "if" || normalizedCondition != "($scheme=http)" || dir.GetBlock() == nil {
|
||||
return false
|
||||
}
|
||||
var returnDirective IDirective
|
||||
for _, child := range dir.GetBlock().GetDirectives() {
|
||||
if _, ok := child.(*Comment); ok {
|
||||
continue
|
||||
}
|
||||
if returnDirective != nil || child.GetName() != "return" || child.GetBlock() != nil {
|
||||
return false
|
||||
}
|
||||
returnDirective = child
|
||||
}
|
||||
if returnDirective == nil {
|
||||
return false
|
||||
}
|
||||
params := returnDirective.GetParameters()
|
||||
if len(params) != 2 || params[0] != "301" {
|
||||
return false
|
||||
}
|
||||
if params[1] == "https://$host$request_uri" {
|
||||
return true
|
||||
}
|
||||
port, ok := strings.CutPrefix(params[1], "https://$host:")
|
||||
if !ok || !strings.HasSuffix(port, "$request_uri") {
|
||||
return false
|
||||
}
|
||||
value, err := strconv.ParseUint(strings.TrimSuffix(port, "$request_uri"), 10, 16)
|
||||
return err == nil && value > 0
|
||||
}
|
||||
|
||||
func (s *Server) UpdateAllowIPs(ips []string) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
const (
|
||||
pingInterval = 30 * time.Second
|
||||
pongWait = 75 * time.Second
|
||||
writeWait = 5 * time.Second
|
||||
writeWait = 30 * time.Second
|
||||
)
|
||||
|
||||
var errAttachmentClosed = errors.New("terminal attachment is closed")
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package terminal
|
||||
|
||||
import "time"
|
||||
|
||||
func (s *Session) renewAuthLease(until time.Time) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
select {
|
||||
case <-s.done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
now := time.Now()
|
||||
if s.authLeaseExpired || (!s.authLeaseUntil.IsZero() && !now.Before(s.authLeaseUntil)) {
|
||||
return false
|
||||
}
|
||||
if until.IsZero() {
|
||||
return s.authLeaseUntil.IsZero()
|
||||
}
|
||||
if !now.Before(until) {
|
||||
return false
|
||||
}
|
||||
s.authLeaseUntil = until
|
||||
s.authLeaseVersion++
|
||||
version := s.authLeaseVersion
|
||||
if s.authLeaseTimer != nil {
|
||||
s.authLeaseTimer.Stop()
|
||||
}
|
||||
s.authLeaseTimer = time.AfterFunc(time.Until(until), func() {
|
||||
s.mu.Lock()
|
||||
expired := s.authLeaseVersion == version && !time.Now().Before(s.authLeaseUntil)
|
||||
if expired {
|
||||
s.authLeaseExpired = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if expired {
|
||||
s.Close()
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -3,27 +3,35 @@ package terminal
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// sessions is the process wide registry of live sessions, keyed by id.
|
||||
// Open stores, Close deletes.
|
||||
var sessions sync.Map
|
||||
|
||||
var errSessionNotFound = errors.New("terminal session not found")
|
||||
|
||||
const (
|
||||
HeaderUserID = "X-Panel-User-ID"
|
||||
HeaderAuthSessionID = "X-Panel-Auth-Session-ID"
|
||||
HeaderUserID = "X-Panel-User-ID"
|
||||
HeaderAuthSessionID = "X-Panel-Auth-Session-ID"
|
||||
HeaderAuthLeaseUntil = "X-Panel-Auth-Lease-Until"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
UserID string
|
||||
AuthSessionID string
|
||||
UserID string
|
||||
AuthSessionID string
|
||||
AuthLeaseUntil time.Time
|
||||
}
|
||||
|
||||
func (i Identity) Valid() bool {
|
||||
return i.UserID != "" && i.AuthSessionID != ""
|
||||
if i.UserID == "" || i.AuthSessionID == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(i.AuthSessionID, "api-key:") && i.AuthLeaseUntil.IsZero() {
|
||||
return false
|
||||
}
|
||||
return i.AuthLeaseUntil.IsZero() || time.Now().Before(i.AuthLeaseUntil)
|
||||
}
|
||||
|
||||
func registerSession(s *Session) {
|
||||
|
||||
@@ -73,6 +73,10 @@ type Session struct {
|
||||
grace *time.Timer
|
||||
revalidateCursor uint64
|
||||
revalidatePending bool
|
||||
authLeaseUntil time.Time
|
||||
authLeaseTimer *time.Timer
|
||||
authLeaseVersion uint64
|
||||
authLeaseExpired bool
|
||||
cols int
|
||||
rows int
|
||||
|
||||
@@ -127,6 +131,11 @@ func serve(ws *websocket.Conn, sessionID string, opts SessionOptions, open func(
|
||||
if sessionID != "" {
|
||||
sess, ok := Lookup(sessionID, opts.Identity)
|
||||
if ok && sess.Kind == opts.Kind && sess.Target == opts.Target && sess.Persistent == opts.Persistent && sess.HostID == opts.HostID {
|
||||
if !sess.renewAuthLease(opts.Identity.AuthLeaseUntil) {
|
||||
sess.Close()
|
||||
sendClose(ws, CloseCodeRevalidate, "terminal authorization expired")
|
||||
return nil
|
||||
}
|
||||
att, err := sess.Attach(ws, opts.Cols, opts.Rows)
|
||||
if err == nil {
|
||||
att.Run()
|
||||
@@ -197,6 +206,10 @@ func openBackend(backend sessionBackend, ring *ringBuffer, opts SessionOptions)
|
||||
}
|
||||
s.closeFn = sync.OnceFunc(s.doClose)
|
||||
registerSession(s)
|
||||
if !s.renewAuthLease(opts.Identity.AuthLeaseUntil) {
|
||||
s.Close()
|
||||
return s
|
||||
}
|
||||
go s.pump()
|
||||
go s.keepaliveLoop()
|
||||
go s.waitBackend()
|
||||
@@ -331,6 +344,9 @@ func (s *Session) doClose() {
|
||||
if s.grace != nil {
|
||||
s.grace.Stop()
|
||||
}
|
||||
if s.authLeaseTimer != nil {
|
||||
s.authLeaseTimer.Stop()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if att != nil {
|
||||
att.close(websocket.CloseNormalClosure, "")
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/core/app/auth"
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/service"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func bindAPIKeyRequest(c *gin.Context, value interface{}) bool {
|
||||
if _, err := auth.RequireAPIKeySession(c); err != nil {
|
||||
helper.BadAuth(c, "ErrNotLogin", nil)
|
||||
return false
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(c.Writer, c.Request.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(value); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return false
|
||||
}
|
||||
if err := decoder.Decode(new(interface{})); err != io.EOF {
|
||||
helper.BadRequest(c, errors.New("unexpected JSON data"))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func apiKeyError(c *gin.Context, err error) {
|
||||
var business buserr.BusinessError
|
||||
if errors.As(err, &business) {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, business.Msg, business.Err)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
}
|
||||
|
||||
func (b *BaseApi) SearchAPIKeys(c *gin.Context) {
|
||||
var req dto.APIKeySearch
|
||||
if !bindAPIKeyRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
result, err := service.NewAPIKeyService().Search(c, req)
|
||||
if err != nil {
|
||||
apiKeyError(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "no-store")
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CreateAPIKey(c *gin.Context) {
|
||||
req := dto.APIKeyCreate{APIKeyFields: dto.APIKeyFields{APIKeyValidityTime: 120}}
|
||||
if !bindAPIKeyRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
result, err := service.NewAPIKeyService().Create(c, req)
|
||||
if err != nil {
|
||||
apiKeyError(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "no-store")
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
func (b *BaseApi) UpdateAPIKey(c *gin.Context) {
|
||||
var req dto.APIKeyUpdate
|
||||
if !bindAPIKeyRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := service.NewAPIKeyService().Update(c, req); err != nil {
|
||||
apiKeyError(c, err)
|
||||
return
|
||||
}
|
||||
apiKeyMutationSuccess(c)
|
||||
}
|
||||
|
||||
func (b *BaseApi) SetAPIKeyStatus(c *gin.Context) {
|
||||
var req dto.APIKeyStatus
|
||||
if !bindAPIKeyRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := service.NewAPIKeyService().Status(c, req); err != nil {
|
||||
apiKeyError(c, err)
|
||||
return
|
||||
}
|
||||
apiKeyMutationSuccess(c)
|
||||
}
|
||||
|
||||
func (b *BaseApi) RevokeAPIKey(c *gin.Context) {
|
||||
var req dto.APIKeyMutation
|
||||
if !bindAPIKeyRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := service.NewAPIKeyService().Revoke(c, req); err != nil {
|
||||
apiKeyError(c, err)
|
||||
return
|
||||
}
|
||||
apiKeyMutationSuccess(c)
|
||||
}
|
||||
|
||||
func apiKeyMutationSuccess(c *gin.Context) {
|
||||
helper.SuccessWithData(c, gin.H{"terminalClosePending": c.GetBool("API_KEY_TERMINAL_CLOSE_PENDING")})
|
||||
}
|
||||
+10
-4
@@ -10,6 +10,7 @@ import (
|
||||
appauth "github.com/1Panel-dev/1Panel/core/app/auth"
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/app/service"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
@@ -423,8 +424,7 @@ func (b *BaseApi) MFAClose(c *gin.Context) {
|
||||
// @Router /core/auth/api/generate [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"}
|
||||
func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
if panelToken != "" {
|
||||
if _, err := appauth.RequireAPIKeySession(c); err != nil {
|
||||
helper.BadAuth(c, "ErrApiConfigDisable", nil)
|
||||
return
|
||||
}
|
||||
@@ -433,6 +433,8 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
service.NewAPIKeyService().AuditLegacyChange(c)
|
||||
c.Header("Cache-Control", "no-store")
|
||||
helper.SuccessWithData(c, apiKey)
|
||||
}
|
||||
|
||||
@@ -446,8 +448,7 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
||||
// @Router /core/auth/api/update [post]
|
||||
// @x-panel-log {"bodyKeys":["ipWhiteList","apiTrustedProxies"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]","formatEN":"update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"}
|
||||
func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
if panelToken != "" {
|
||||
if _, err := appauth.RequireAPIKeySession(c); err != nil {
|
||||
helper.BadAuth(c, "ErrApiConfigDisable", nil)
|
||||
return
|
||||
}
|
||||
@@ -466,6 +467,7 @@ func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
service.NewAPIKeyService().AuditLegacyChange(c)
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
@@ -481,6 +483,10 @@ func (b *BaseApi) GetCurrentUser(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if c.GetBool("API_AUTH") || appauth.HasAPICredentials(c) {
|
||||
userInfo.ApiKey = ""
|
||||
}
|
||||
c.Header("Cache-Control", "no-store")
|
||||
helper.SuccessWithData(c, userInfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ func (b *BaseApi) UpdateFileDownloadPreference(c *gin.Context) {
|
||||
}
|
||||
|
||||
func fileDownloadPreferenceUser(c *gin.Context) (psession.SessionUser, bool) {
|
||||
// Preferences always belong to the authenticated session, never a request-supplied user ID.
|
||||
user, err := global.SESSION.Get(c)
|
||||
if err != nil || user.ID == "" {
|
||||
helper.BadAuth(c, "ErrNotLogin", buserr.New("ErrNotLogin"))
|
||||
@@ -137,7 +136,16 @@ func (b *BaseApi) GetSystemAvailable(c *gin.Context) {
|
||||
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"}
|
||||
func (b *BaseApi) UpdateSetting(c *gin.Context) {
|
||||
var req dto.SettingUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if appauth.IsAPICredentialSetting(req.Key) {
|
||||
helper.BadRequest(c, buserr.New("ErrInvalidParams"))
|
||||
return
|
||||
}
|
||||
if err := global.VALID.Struct(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.Key == "SecurityEntrance" {
|
||||
|
||||
@@ -26,12 +26,21 @@ type APIAuthConfig struct {
|
||||
IpWhiteList string
|
||||
ApiTrustedProxies string
|
||||
ApiKeyValidityTime int
|
||||
KeyID string
|
||||
KeyName string
|
||||
KeyRevision uint64
|
||||
KeyExpiresAt *time.Time
|
||||
Owner APIKeyOwner
|
||||
}
|
||||
|
||||
type APIAuthConfigLoader func(c *gin.Context) (APIAuthConfig, error)
|
||||
type APIAuthSuccessHandler func(c *gin.Context, config APIAuthConfig)
|
||||
|
||||
func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessHandler) gin.HandlerFunc {
|
||||
func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessHandler, ownerProviders ...APIKeyOwnerProvider) gin.HandlerFunc {
|
||||
var ownerProvider APIKeyOwnerProvider
|
||||
if len(ownerProviders) > 0 {
|
||||
ownerProvider = ownerProviders[0]
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") {
|
||||
c.Next()
|
||||
@@ -40,12 +49,18 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH
|
||||
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
panelTimestamp := c.GetHeader("1Panel-Timestamp")
|
||||
if panelToken == "" && panelTimestamp == "" {
|
||||
if !HasAPICredentials(c) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
config, err := loadConfig(c)
|
||||
loader := loadConfig
|
||||
if ownerProvider != nil {
|
||||
loader = func(c *gin.Context) (APIAuthConfig, error) {
|
||||
return loadMultiAPIKeyConfig(c, loadConfig, ownerProvider)
|
||||
}
|
||||
}
|
||||
config, err := loader(c)
|
||||
if err != nil {
|
||||
var bizErr buserr.BusinessError
|
||||
if errors.As(err, &bizErr) && strings.HasPrefix(bizErr.Msg, "ErrApiConfig") {
|
||||
@@ -63,7 +78,7 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH
|
||||
helper.BadAuth(c, "ErrApiConfigKeyTimeInvalid", nil)
|
||||
return
|
||||
}
|
||||
if !isValid1PanelToken(panelToken, panelTimestamp, config.ApiKey) {
|
||||
if !IsValid1PanelTokenWithVersion(panelToken, panelTimestamp, config.ApiKey, c.GetHeader("1Panel-Signature-Version")) {
|
||||
helper.BadAuth(c, "ErrApiConfigKeyInvalid", nil)
|
||||
return
|
||||
}
|
||||
@@ -73,9 +88,11 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH
|
||||
}
|
||||
|
||||
c.Set("API_AUTH", true)
|
||||
c.Set("API_AUTH_CLIENT_IP", GetAPIClientIP(c, config.ApiTrustedProxies))
|
||||
if onSuccess != nil {
|
||||
onSuccess(c, config)
|
||||
}
|
||||
SetAPIKeyContext(c, config, ownerProvider)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -141,6 +158,9 @@ func IsValid1PanelToken(panelToken string, panelTimestamp string, apiKey string)
|
||||
}
|
||||
|
||||
func IsValid1PanelTokenWithVersion(panelToken string, panelTimestamp string, apiKey string, signatureVersion string) bool {
|
||||
if apiKey == "" || panelToken == "" {
|
||||
return false
|
||||
}
|
||||
panelToken = strings.ToLower(strings.TrimSpace(panelToken))
|
||||
version := strings.ToLower(strings.TrimSpace(signatureVersion))
|
||||
switch version {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/1Panel-dev/1Panel/core/init/session/psession"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const APIKeyOwnerPanelAdmin = "panel_admin"
|
||||
const APIKeyOwnerEnterpriseUser = "enterprise_user"
|
||||
|
||||
func IsAPICredentialSetting(key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "apikey", "apiinterfacestatus", "ipwhitelist", "apitrustedproxies", "apikeyvaliditytime", "encryptkey":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type APIKeyOwner struct {
|
||||
Type string
|
||||
ID string
|
||||
Name string
|
||||
IsSuperAdmin bool
|
||||
}
|
||||
|
||||
type APIKeyOwnerProvider interface {
|
||||
CurrentAPIKeyOwner(c *gin.Context) (APIKeyOwner, error)
|
||||
ResolveAPIKeyOwner(c *gin.Context, ownerType, ownerID string) (APIKeyOwner, error)
|
||||
LoadLegacyAPIKey(owner APIKeyOwner) (APIAuthConfig, error)
|
||||
SaveLegacyAPIKey(owner APIKeyOwner, config APIAuthConfig) error
|
||||
}
|
||||
|
||||
type APIKeyBinding struct {
|
||||
KeyID string
|
||||
KeyName string
|
||||
Owner APIKeyOwner
|
||||
Revision uint64
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func RequireAPIKeySession(c *gin.Context) (psession.SessionUser, error) {
|
||||
if c == nil || c.GetBool("API_AUTH") || c.GetBool("LOCAL_REQUEST") || HasAPICredentials(c) || global.SESSION == nil {
|
||||
return psession.SessionUser{}, buserr.New("ErrNotLogin")
|
||||
}
|
||||
u, err := global.SESSION.Get(c)
|
||||
if err != nil || u.ID == "" || u.Name == "" {
|
||||
return psession.SessionUser{}, buserr.New("ErrNotLogin")
|
||||
}
|
||||
c.Set(psession.GinContextSessionUserKey, u)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func HasAPICredentials(c *gin.Context) bool {
|
||||
return c.GetHeader("1Panel-Token") != "" || c.GetHeader("1Panel-Timestamp") != "" || c.GetHeader("1Panel-Key-ID") != "" || c.GetHeader("1Panel-Signature-Version") != ""
|
||||
}
|
||||
|
||||
func LoadLegacyAPIKeyPolicy(owner APIKeyOwner) (model.LegacyAPIKeyPolicy, error) {
|
||||
policy := model.LegacyAPIKeyPolicy{OwnerType: owner.Type, OwnerID: owner.ID, AllowAppBinding: true, Revision: 1}
|
||||
var saved model.LegacyAPIKeyPolicy
|
||||
err := repo.APIKeyOwnerQuery(global.DB, owner.Type, owner.ID).First(&saved).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return policy, nil
|
||||
}
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func LegacyAPIKeyFingerprint(owner APIKeyOwner, config APIAuthConfig, policy model.LegacyAPIKeyPolicy) (string, uint64) {
|
||||
data, _ := json.Marshal([]interface{}{owner.Type, owner.ID, config.ApiInterfaceStatus, config.ApiKey, config.IpWhiteList, config.ApiTrustedProxies, config.ApiKeyValidityTime, policy.AllowAppBinding, policy.Revision})
|
||||
hash := sha256.Sum256(data)
|
||||
return hex.EncodeToString(hash[:]), (binary.BigEndian.Uint64(hash[:8]) & ((1 << 53) - 1)) + 1
|
||||
}
|
||||
|
||||
func APIKeySecret(key model.APIKey) (string, error) {
|
||||
if key.SecretVersion != 1 || key.SecretCiphertext == "" {
|
||||
return "", buserr.New("ErrApiConfigKeyInvalid")
|
||||
}
|
||||
master, err := encrypt.APIKeyEncryptionKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encrypt.DecryptAPIKey(key.SecretCiphertext, key.ID, master)
|
||||
}
|
||||
|
||||
func ValidateAPIKeyState(key model.APIKey, now time.Time) error {
|
||||
if key.Status != constant.StatusEnable {
|
||||
return buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
if key.ExpiresAt != nil && !now.Before(*key.ExpiresAt) {
|
||||
return buserr.New("ErrApiConfigKeyExpired")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configForAPIKey(key model.APIKey, secret string, owner APIKeyOwner) APIAuthConfig {
|
||||
return APIAuthConfig{ApiInterfaceStatus: key.Status, ApiKey: secret, IpWhiteList: key.IPWhiteList, ApiTrustedProxies: key.APITrustedProxies, ApiKeyValidityTime: key.APIKeyValidityTime, KeyID: key.ID, KeyName: key.Name, KeyRevision: key.Revision, KeyExpiresAt: key.ExpiresAt, Owner: owner}
|
||||
}
|
||||
|
||||
func loadMultiAPIKeyConfig(c *gin.Context, legacy APIAuthConfigLoader, provider APIKeyOwnerProvider) (APIAuthConfig, error) {
|
||||
keyID := c.GetHeader("1Panel-Key-ID")
|
||||
q := global.DB.Model(&model.APIKey{}).Select("id", "secret_ciphertext", "secret_version", "revision")
|
||||
if keyID != "" {
|
||||
q = q.Where("id = ?", keyID)
|
||||
} else {
|
||||
q = q.Where("status <> ?", "Revoked")
|
||||
}
|
||||
master, err := encrypt.APIKeyEncryptionKey()
|
||||
if err != nil {
|
||||
if keyID == "" {
|
||||
return legacy(c)
|
||||
}
|
||||
return APIAuthConfig{}, err
|
||||
}
|
||||
var candidates []model.APIKey
|
||||
var matched *model.APIKey
|
||||
err = q.Order("id").FindInBatches(&candidates, 128, func(_ *gorm.DB, _ int) error {
|
||||
for _, key := range candidates {
|
||||
if key.SecretVersion != 1 {
|
||||
continue
|
||||
}
|
||||
secret, err := encrypt.DecryptAPIKey(key.SecretCiphertext, key.ID, master)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if IsValid1PanelTokenWithVersion(c.GetHeader("1Panel-Token"), c.GetHeader("1Panel-Timestamp"), secret, c.GetHeader("1Panel-Signature-Version")) {
|
||||
copy := key
|
||||
matched = ©
|
||||
return errAPIKeyMatched
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).Error
|
||||
if err != nil && !errors.Is(err, errAPIKeyMatched) {
|
||||
return APIAuthConfig{}, err
|
||||
}
|
||||
if matched == nil {
|
||||
if keyID != "" {
|
||||
return APIAuthConfig{}, buserr.New("ErrApiConfigKeyInvalid")
|
||||
}
|
||||
return legacy(c)
|
||||
}
|
||||
key, err := repo.NewAPIKeyRepo().Get(matched.ID)
|
||||
if err != nil || key.Revision != matched.Revision {
|
||||
return APIAuthConfig{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
if err = ValidateAPIKeyState(key, time.Now()); err != nil {
|
||||
return APIAuthConfig{}, err
|
||||
}
|
||||
owner, err := provider.ResolveAPIKeyOwner(c, key.OwnerType, key.OwnerID)
|
||||
if err != nil {
|
||||
return APIAuthConfig{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
secret, err := APIKeySecret(key)
|
||||
if err != nil {
|
||||
return APIAuthConfig{}, buserr.New("ErrApiConfigKeyInvalid")
|
||||
}
|
||||
return configForAPIKey(key, secret, owner), nil
|
||||
}
|
||||
|
||||
var errAPIKeyMatched = errors.New("API credential matched")
|
||||
|
||||
func SetAPIKeyContext(c *gin.Context, config APIAuthConfig, provider APIKeyOwnerProvider) {
|
||||
if config.KeyID != "" {
|
||||
c.Set("API_AUTH_KEY_KIND", "apiKey")
|
||||
c.Set("API_AUTH_KEY_ID", config.KeyID)
|
||||
c.Set("API_AUTH_KEY_NAME", config.KeyName)
|
||||
c.Set("API_AUTH_KEY_REVISION", config.KeyRevision)
|
||||
c.Set("API_AUTH_OWNER_TYPE", config.Owner.Type)
|
||||
c.Set("API_AUTH_OWNER_ID", config.Owner.ID)
|
||||
if config.KeyExpiresAt != nil {
|
||||
c.Set("API_AUTH_KEY_EXPIRES_AT", *config.KeyExpiresAt)
|
||||
}
|
||||
role := "COMMON_USER"
|
||||
if config.Owner.IsSuperAdmin {
|
||||
role = "ADMIN"
|
||||
}
|
||||
c.Set(psession.GinContextSessionUserKey, psession.SessionUser{ID: config.Owner.ID, Name: config.Owner.Name, Role: role})
|
||||
c.Set("API_AUTH_USERNAME", config.Owner.Name)
|
||||
return
|
||||
}
|
||||
c.Set("API_AUTH_KEY_KIND", "legacy")
|
||||
c.Set("API_AUTH_KEY_ID", "legacy")
|
||||
c.Set("API_AUTH_KEY_NAME", "Legacy API Key")
|
||||
if provider != nil {
|
||||
if owner, err := provider.CurrentAPIKeyOwner(c); err == nil {
|
||||
c.Set("API_AUTH_OWNER_TYPE", owner.Type)
|
||||
c.Set("API_AUTH_OWNER_ID", owner.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package auth
|
||||
|
||||
import "sync"
|
||||
|
||||
var legacyAPIKeyMutationMu sync.Mutex
|
||||
|
||||
func WithLegacyAPIKeyMutation(change func() error) error {
|
||||
legacyAPIKeyMutationMu.Lock()
|
||||
defer legacyAPIKeyMutationMu.Unlock()
|
||||
return change()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/1Panel-dev/1Panel/core/init/session/psession"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PanelAPIKeyOwnerProvider struct{}
|
||||
|
||||
func (p PanelAPIKeyOwnerProvider) CurrentAPIKeyOwner(c *gin.Context) (APIKeyOwner, error) {
|
||||
value, ok := c.Get(psession.GinContextSessionUserKey)
|
||||
u, valid := value.(psession.SessionUser)
|
||||
if !ok || !valid || u.ID != psession.SuperAdminSessionUserID {
|
||||
return APIKeyOwner{}, buserr.New("ErrNotLogin")
|
||||
}
|
||||
return p.ResolveAPIKeyOwner(c, APIKeyOwnerPanelAdmin, u.ID)
|
||||
}
|
||||
|
||||
func (PanelAPIKeyOwnerProvider) ResolveAPIKeyOwner(_ *gin.Context, ownerType, ownerID string) (APIKeyOwner, error) {
|
||||
if ownerType != APIKeyOwnerPanelAdmin || ownerID != psession.SuperAdminSessionUserID {
|
||||
return APIKeyOwner{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
name, err := repo.NewISettingRepo().GetValueByKey("UserName")
|
||||
if err != nil || name == "" {
|
||||
return APIKeyOwner{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
return APIKeyOwner{Type: ownerType, ID: ownerID, Name: name, IsSuperAdmin: true}, nil
|
||||
}
|
||||
|
||||
func (p PanelAPIKeyOwnerProvider) LoadLegacyAPIKey(owner APIKeyOwner) (APIAuthConfig, error) {
|
||||
if _, err := p.ResolveAPIKeyOwner(nil, owner.Type, owner.ID); err != nil {
|
||||
return APIAuthConfig{}, err
|
||||
}
|
||||
return LoadAPIAuthConfig(nil)
|
||||
}
|
||||
|
||||
func (p PanelAPIKeyOwnerProvider) SaveLegacyAPIKey(owner APIKeyOwner, config APIAuthConfig) error {
|
||||
if _, err := p.ResolveAPIKeyOwner(nil, owner.Type, owner.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return StoreLegacyAPIConfig(dto.ApiInterfaceConfig{ApiInterfaceStatus: config.ApiInterfaceStatus, ApiKey: config.ApiKey, IpWhiteList: config.IpWhiteList, ApiTrustedProxies: config.ApiTrustedProxies, ApiKeyValidityTime: config.ApiKeyValidityTime})
|
||||
}
|
||||
@@ -341,12 +341,17 @@ func UpdateCurrentUserInfo(c *gin.Context, req dto.CurrentUserUpdate) error {
|
||||
|
||||
func GenerateApiKey() (string, error) {
|
||||
apiKey := common.RandStr(32)
|
||||
if err := repo.NewISettingRepo().Update("ApiKey", apiKey); err != nil {
|
||||
if err := WithLegacyAPIKeyMutation(func() error { return repo.NewISettingRepo().Update("ApiKey", apiKey) }); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
func UpdateApiConfig(req dto.ApiInterfaceConfig) error {
|
||||
return WithLegacyAPIKeyMutation(func() error { return StoreLegacyAPIConfig(req) })
|
||||
}
|
||||
|
||||
func StoreLegacyAPIConfig(req dto.ApiInterfaceConfig) error {
|
||||
settingRepo := repo.NewISettingRepo()
|
||||
trustedProxies, err := NormalizeAPITrustedProxies(req.ApiTrustedProxies)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type APIKeyItem struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
KeyHint string `json:"keyHint"`
|
||||
Status string `json:"status"`
|
||||
IPWhiteList string `json:"ipWhiteList"`
|
||||
APITrustedProxies string `json:"apiTrustedProxies"`
|
||||
APIKeyValidityTime int `json:"apiKeyValidityTime"`
|
||||
ExpiresAt *time.Time `json:"expiresAt"`
|
||||
AllowAppBinding bool `json:"allowAppBinding"`
|
||||
Revision uint64 `json:"revision"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type APIKeySearch struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
ExcludeRevoked bool `json:"excludeRevoked"`
|
||||
}
|
||||
type APIKeyPage struct {
|
||||
Items []APIKeyItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
type APIKeyFields struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IPWhiteList string `json:"ipWhiteList"`
|
||||
APITrustedProxies string `json:"apiTrustedProxies"`
|
||||
APIKeyValidityTime int `json:"apiKeyValidityTime"`
|
||||
ExpiresAt json.RawMessage `json:"expiresAt"`
|
||||
AllowAppBinding bool `json:"allowAppBinding"`
|
||||
}
|
||||
type APIKeyCreate struct {
|
||||
RequestID string `json:"requestID"`
|
||||
APIKeyFields
|
||||
}
|
||||
type APIKeyCreated struct {
|
||||
Item APIKeyItem `json:"item"`
|
||||
APIKey string `json:"apiKey"`
|
||||
AlreadyCreated bool `json:"alreadyCreated,omitempty"`
|
||||
}
|
||||
type APIKeyMutation struct {
|
||||
ID string `json:"id"`
|
||||
Revision uint64 `json:"revision"`
|
||||
}
|
||||
type APIKeyUpdate struct {
|
||||
APIKeyMutation
|
||||
APIKeyFields
|
||||
}
|
||||
type APIKeyStatus struct {
|
||||
APIKeyMutation
|
||||
Status string `json:"status"`
|
||||
}
|
||||
+11
-8
@@ -5,14 +5,17 @@ import (
|
||||
)
|
||||
|
||||
type OperationLog struct {
|
||||
ID uint `json:"id"`
|
||||
Source string `json:"source"`
|
||||
User string `json:"user"`
|
||||
Node string `json:"node"`
|
||||
IP string `json:"ip"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
ID uint `json:"id"`
|
||||
Source string `json:"source"`
|
||||
User string `json:"user"`
|
||||
APIKeyID string `json:"apiKeyID"`
|
||||
APIKeyName string `json:"apiKeyName"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
Node string `json:"node"`
|
||||
IP string `json:"ip"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
|
||||
Latency time.Duration `json:"latency"`
|
||||
Status string `json:"status"`
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type APIKey struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey" json:"-"`
|
||||
OwnerType string `gorm:"not null;uniqueIndex:idx_api_key_request;uniqueIndex:idx_api_key_active_name;index:idx_api_key_owner" json:"-"`
|
||||
OwnerID string `gorm:"not null;uniqueIndex:idx_api_key_request;uniqueIndex:idx_api_key_active_name;index:idx_api_key_owner" json:"-"`
|
||||
Name string `gorm:"not null" json:"-"`
|
||||
ActiveName *string `gorm:"uniqueIndex:idx_api_key_active_name" json:"-"`
|
||||
Description string `json:"-"`
|
||||
RequestID string `gorm:"not null;uniqueIndex:idx_api_key_request" json:"-"`
|
||||
RequestHash string `json:"-"`
|
||||
SecretCiphertext string `gorm:"type:text;not null" json:"-"`
|
||||
SecretVersion int `gorm:"not null" json:"-"`
|
||||
KeyHint string `json:"-"`
|
||||
Status string `gorm:"not null;index" json:"-"`
|
||||
IPWhiteList string `json:"-"`
|
||||
APITrustedProxies string `json:"-"`
|
||||
APIKeyValidityTime int `json:"-"`
|
||||
ExpiresAt *time.Time `gorm:"index" json:"-"`
|
||||
AllowAppBinding bool `json:"-"`
|
||||
Revision uint64 `gorm:"not null" json:"-"`
|
||||
CreatedAt time.Time `json:"-"`
|
||||
UpdatedAt time.Time `json:"-"`
|
||||
RevokedAt *time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type LegacyAPIKeyPolicy struct {
|
||||
OwnerType string `gorm:"primaryKey"`
|
||||
OwnerID string `gorm:"primaryKey"`
|
||||
AllowAppBinding bool
|
||||
Revision uint64
|
||||
}
|
||||
+10
-7
@@ -6,13 +6,16 @@ import (
|
||||
|
||||
type OperationLog struct {
|
||||
BaseModel
|
||||
Source string `json:"source"`
|
||||
User string `json:"user"`
|
||||
IP string `json:"ip"`
|
||||
Node string `json:"node"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Source string `json:"source"`
|
||||
User string `json:"user"`
|
||||
APIKeyID string `json:"apiKeyID"`
|
||||
APIKeyName string `json:"apiKeyName"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
IP string `json:"ip"`
|
||||
Node string `json:"node"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
|
||||
Latency time.Duration `json:"latency"`
|
||||
Status string `json:"status"`
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type APIKeyRepo struct{}
|
||||
|
||||
func NewAPIKeyRepo() *APIKeyRepo { return &APIKeyRepo{} }
|
||||
func APIKeyOwnerQuery(db *gorm.DB, ownerType, ownerID string) *gorm.DB {
|
||||
return db.Where("owner_type = ? AND owner_id = ?", ownerType, ownerID)
|
||||
}
|
||||
func (r *APIKeyRepo) Get(id string) (model.APIKey, error) {
|
||||
var key model.APIKey
|
||||
err := global.DB.Where("id = ?", id).First(&key).Error
|
||||
return key, err
|
||||
}
|
||||
func (r *APIKeyRepo) List(ownerType, ownerID string, excludeRevoked bool) ([]model.APIKey, error) {
|
||||
items := make([]model.APIKey, 0)
|
||||
q := APIKeyOwnerQuery(global.DB, ownerType, ownerID)
|
||||
if excludeRevoked {
|
||||
q = q.Where("status <> ?", "Revoked")
|
||||
}
|
||||
err := q.Order("created_at DESC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/auth"
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/apikey_audit"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
|
||||
terminalsession "github.com/1Panel-dev/1Panel/core/utils/terminal_session"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/xpack"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var apiKeyMutationMu sync.Mutex
|
||||
|
||||
type APIKeyService struct {
|
||||
Provider auth.APIKeyOwnerProvider
|
||||
RevokeTerminals terminalsession.RevokeFunc
|
||||
}
|
||||
|
||||
func NewAPIKeyService() *APIKeyService {
|
||||
provider, _ := xpack.AuthProvider.(auth.APIKeyOwnerProvider)
|
||||
return &APIKeyService{Provider: provider, RevokeTerminals: xpack.AuthProvider.RevokeTerminalSessions}
|
||||
}
|
||||
|
||||
func (s *APIKeyService) owner(c *gin.Context) (auth.APIKeyOwner, error) {
|
||||
if _, err := auth.RequireAPIKeySession(c); err != nil {
|
||||
return auth.APIKeyOwner{}, err
|
||||
}
|
||||
if s.Provider == nil {
|
||||
return auth.APIKeyOwner{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
return s.Provider.CurrentAPIKeyOwner(c)
|
||||
}
|
||||
|
||||
func apiKeyItem(key model.APIKey) dto.APIKeyItem {
|
||||
status := key.Status
|
||||
if status != "Revoked" && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
|
||||
status = "Expired"
|
||||
}
|
||||
return dto.APIKeyItem{ID: key.ID, Kind: "apiKey", Name: key.Name, Description: key.Description, KeyHint: key.KeyHint, Status: status, IPWhiteList: key.IPWhiteList, APITrustedProxies: key.APITrustedProxies, APIKeyValidityTime: key.APIKeyValidityTime, ExpiresAt: key.ExpiresAt, AllowAppBinding: key.AllowAppBinding, Revision: key.Revision, CreatedAt: &key.CreatedAt}
|
||||
}
|
||||
|
||||
func (s *APIKeyService) legacy(owner auth.APIKeyOwner) (dto.APIKeyItem, auth.APIAuthConfig, model.LegacyAPIKeyPolicy, string, error) {
|
||||
config, err := s.Provider.LoadLegacyAPIKey(owner)
|
||||
if err != nil {
|
||||
return dto.APIKeyItem{}, config, model.LegacyAPIKeyPolicy{}, "", err
|
||||
}
|
||||
policy, err := auth.LoadLegacyAPIKeyPolicy(owner)
|
||||
if err != nil {
|
||||
return dto.APIKeyItem{}, config, policy, "", err
|
||||
}
|
||||
fingerprint, revision := auth.LegacyAPIKeyFingerprint(owner, config, policy)
|
||||
item := dto.APIKeyItem{ID: "legacy", Kind: "legacy", Name: "Legacy API Key", KeyHint: keyHint(config.ApiKey), Status: config.ApiInterfaceStatus, IPWhiteList: config.IpWhiteList, APITrustedProxies: config.ApiTrustedProxies, APIKeyValidityTime: config.ApiKeyValidityTime, AllowAppBinding: policy.AllowAppBinding, Revision: revision}
|
||||
return item, config, policy, fingerprint, nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) Search(c *gin.Context, req dto.APIKeySearch) (*dto.APIKeyPage, error) {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Page < 1 || req.PageSize < 1 || req.PageSize > 100 {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
keys, err := repo.NewAPIKeyRepo().List(owner.Type, owner.ID, req.ExcludeRevoked)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.APIKeyItem, 0, len(keys)+1)
|
||||
legacy, config, _, _, err := s.legacy(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.ApiKey != "" {
|
||||
items = append(items, legacy)
|
||||
}
|
||||
for _, key := range keys {
|
||||
items = append(items, apiKeyItem(key))
|
||||
}
|
||||
result := &dto.APIKeyPage{Items: []dto.APIKeyItem{}, Total: len(items)}
|
||||
start := (req.Page - 1) * req.PageSize
|
||||
if start >= 0 && start < len(items) {
|
||||
result.Items = items[start:min(start+req.PageSize, len(items))]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validateAPIKeyFields(fields *dto.APIKeyFields, create bool) (*time.Time, error) {
|
||||
fields.Name = strings.TrimSpace(fields.Name)
|
||||
fields.Description = strings.TrimSpace(fields.Description)
|
||||
if fields.Name == "" || utf8.RuneCountInString(fields.Name) > 64 || utf8.RuneCountInString(fields.Description) > 256 || fields.APIKeyValidityTime < 0 || fields.APIKeyValidityTime > 1440 || len(fields.IPWhiteList) > 4096 || len(fields.APITrustedProxies) > 4096 {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
ips, err := common.HandleIPList(fields.IPWhiteList)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
|
||||
fields.IPWhiteList = strings.Join(ips, "\n")
|
||||
fields.APITrustedProxies, err = auth.NormalizeAPITrustedProxies(fields.APITrustedProxies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fields.ExpiresAt) == 0 {
|
||||
if !create {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
expires := time.Now().UTC().Add(90 * 24 * time.Hour)
|
||||
return &expires, nil
|
||||
}
|
||||
if bytes.Equal(bytes.TrimSpace(fields.ExpiresAt), []byte("null")) {
|
||||
return nil, nil
|
||||
}
|
||||
var expires time.Time
|
||||
if err = json.Unmarshal(fields.ExpiresAt, &expires); err != nil || !expires.After(time.Now()) {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
return &expires, nil
|
||||
}
|
||||
|
||||
func keyHint(secret string) string {
|
||||
if len(secret) < 8 {
|
||||
return ""
|
||||
}
|
||||
return secret[:4] + "••••" + secret[len(secret)-4:]
|
||||
}
|
||||
|
||||
func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKeyCreated, error) {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = uuid.Parse(req.RequestID); err != nil {
|
||||
return nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
expires, err := validateAPIKeyFields(&req.APIKeyFields, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, _ := json.Marshal(req.APIKeyFields)
|
||||
digest := sha256.Sum256(encoded)
|
||||
requestHash := hex.EncodeToString(digest[:])
|
||||
master, err := encrypt.APIKeyEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiKeyMutationMu.Lock()
|
||||
var key model.APIKey
|
||||
var secret string
|
||||
repeated := false
|
||||
err = global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
q := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID)
|
||||
err := q.Where("request_id = ?", req.RequestID).First(&key).Error
|
||||
if err == nil {
|
||||
if key.RequestHash != requestHash {
|
||||
return buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
repeated = true
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if err = ensureAPIKeyName(tx, owner, req.Name, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
bytes := make([]byte, 16)
|
||||
if _, err = rand.Read(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
secret = hex.EncodeToString(bytes)
|
||||
key = model.APIKey{ID: uuid.NewString(), OwnerType: owner.Type, OwnerID: owner.ID, Name: req.Name, ActiveName: &req.Name, Description: req.Description, RequestID: req.RequestID, RequestHash: requestHash, SecretVersion: 1, KeyHint: keyHint(secret), Status: constant.StatusEnable, IPWhiteList: req.IPWhiteList, APITrustedProxies: req.APITrustedProxies, APIKeyValidityTime: req.APIKeyValidityTime, ExpiresAt: expires, AllowAppBinding: req.AllowAppBinding, Revision: 1}
|
||||
key.SecretCiphertext, err = encrypt.EncryptAPIKey(secret, key.ID, master)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&key).Error
|
||||
})
|
||||
apiKeyMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repeated {
|
||||
recordAPIKeyEvent(c, owner, key.ID, key.Name, "create")
|
||||
}
|
||||
return &dto.APIKeyCreated{Item: apiKeyItem(key), APIKey: secret, AlreadyCreated: repeated}, nil
|
||||
}
|
||||
|
||||
func ensureAPIKeyName(tx *gorm.DB, owner auth.APIKeyOwner, name, id string) error {
|
||||
var count int64
|
||||
err := repo.APIKeyOwnerQuery(tx.Model(&model.APIKey{}), owner.Type, owner.ID).Where("active_name = ? AND id <> ?", name, id).Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return buserr.New("ErrAPIKeyNameExists")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ID == "legacy" {
|
||||
return s.updateLegacy(c, owner, req.APIKeyMutation, &req.APIKeyFields, "update", "")
|
||||
}
|
||||
expires, err := validateAPIKeyFields(&req.APIKeyFields, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.mutate(c, owner, req.APIKeyMutation, "update", func(tx *gorm.DB, key *model.APIKey) error {
|
||||
if err := ensureAPIKeyName(tx, owner, req.Name, key.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
key.Name = req.Name
|
||||
key.ActiveName = &req.Name
|
||||
key.Description = req.Description
|
||||
key.IPWhiteList = req.IPWhiteList
|
||||
key.APITrustedProxies = req.APITrustedProxies
|
||||
key.APIKeyValidityTime = req.APIKeyValidityTime
|
||||
key.ExpiresAt = expires
|
||||
key.AllowAppBinding = req.AllowAppBinding
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status != constant.StatusEnable && req.Status != constant.StatusDisable {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
action := "disable"
|
||||
if req.Status == constant.StatusEnable {
|
||||
action = "enable"
|
||||
}
|
||||
if req.ID == "legacy" {
|
||||
return s.updateLegacy(c, owner, req.APIKeyMutation, nil, action, req.Status)
|
||||
}
|
||||
return s.mutate(c, owner, req.APIKeyMutation, action, func(_ *gorm.DB, key *model.APIKey) error {
|
||||
if req.Status == constant.StatusEnable && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
|
||||
return buserr.New("ErrApiConfigKeyExpired")
|
||||
}
|
||||
key.Status = req.Status
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIKeyService) Revoke(c *gin.Context, req dto.APIKeyMutation) error {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ID == "legacy" {
|
||||
return s.updateLegacy(c, owner, req, nil, "revoke", constant.StatusDisable)
|
||||
}
|
||||
return s.mutate(c, owner, req, "revoke", func(_ *gorm.DB, key *model.APIKey) error {
|
||||
now := time.Now()
|
||||
key.Status = "Revoked"
|
||||
key.RevokedAt = &now
|
||||
key.ActiveName = nil
|
||||
key.SecretCiphertext = ""
|
||||
key.AllowAppBinding = false
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIKeyService) mutate(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, action string, change func(*gorm.DB, *model.APIKey) error) error {
|
||||
if req.ID == "" || req.Revision == 0 {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
apiKeyMutationMu.Lock()
|
||||
var key model.APIKey
|
||||
closeTerminals := action == "disable" || action == "revoke"
|
||||
err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID).Where("id = ?", req.ID).First(&key).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return buserr.New("ErrAPIKeyNotFound")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if key.Revision != req.Revision || key.Status == "Revoked" {
|
||||
return buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
before := key
|
||||
if err := change(tx, &key); err != nil {
|
||||
return err
|
||||
}
|
||||
closeTerminals = closeTerminals || before.IPWhiteList != key.IPWhiteList || before.APITrustedProxies != key.APITrustedProxies || before.APIKeyValidityTime != key.APIKeyValidityTime || (key.ExpiresAt != nil && (before.ExpiresAt == nil || key.ExpiresAt.Before(*before.ExpiresAt)))
|
||||
key.Revision++
|
||||
result := tx.Model(&model.APIKey{}).Where("id = ? AND revision = ?", key.ID, req.Revision).Select("*").Updates(&key)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
apiKeyMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordAPIKeyEvent(c, owner, key.ID, key.Name, action)
|
||||
if closeTerminals {
|
||||
s.revokeAPIKeyTerminals(c, owner, key.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) updateLegacy(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, fields *dto.APIKeyFields, action, status string) error {
|
||||
return auth.WithLegacyAPIKeyMutation(func() error {
|
||||
return s.updateLegacyLocked(c, owner, req, fields, action, status)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIKeyService) updateLegacyLocked(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, fields *dto.APIKeyFields, action, status string) error {
|
||||
item, config, policy, _, err := s.legacy(owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.ApiKey == "" {
|
||||
return buserr.New("ErrAPIKeyNotFound")
|
||||
}
|
||||
if req.Revision == 0 || req.Revision != item.Revision {
|
||||
return buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
previousConfig := config
|
||||
if fields != nil {
|
||||
fields.Name = "Legacy API Key"
|
||||
legacyWindow := fields.APIKeyValidityTime
|
||||
if legacyWindow > 1440 && legacyWindow == config.ApiKeyValidityTime {
|
||||
fields.APIKeyValidityTime = 1440
|
||||
}
|
||||
expires, err := validateAPIKeyFields(fields, false)
|
||||
fields.APIKeyValidityTime = legacyWindow
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expires != nil {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
config.IpWhiteList = fields.IPWhiteList
|
||||
config.ApiTrustedProxies = fields.APITrustedProxies
|
||||
config.ApiKeyValidityTime = fields.APIKeyValidityTime
|
||||
policy.AllowAppBinding = fields.AllowAppBinding
|
||||
}
|
||||
if status != "" {
|
||||
config.ApiInterfaceStatus = status
|
||||
}
|
||||
if action == "revoke" {
|
||||
config.ApiKey = ""
|
||||
policy.AllowAppBinding = false
|
||||
}
|
||||
allowAppBinding := policy.AllowAppBinding
|
||||
policy.AllowAppBinding = false
|
||||
policy.Revision++
|
||||
if err = global.DB.Clauses(clause.OnConflict{UpdateAll: true}).Create(&policy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.Provider.SaveLegacyAPIKey(owner, config); err != nil {
|
||||
return err
|
||||
}
|
||||
if allowAppBinding {
|
||||
policy.AllowAppBinding = true
|
||||
policy.Revision++
|
||||
if err = global.DB.Clauses(clause.OnConflict{UpdateAll: true}).Create(&policy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
recordAPIKeyEvent(c, owner, "legacy", item.Name, action)
|
||||
if action == "disable" || action == "revoke" || previousConfig.IpWhiteList != config.IpWhiteList || previousConfig.ApiTrustedProxies != config.ApiTrustedProxies || previousConfig.ApiKeyValidityTime != config.ApiKeyValidityTime {
|
||||
s.revokeAPIKeyTerminals(c, owner, "legacy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordAPIKeyEvent(c *gin.Context, owner auth.APIKeyOwner, id, name, action string) {
|
||||
if err := apikey_audit.Record(c, apikey_audit.Event{OwnerType: owner.Type, OwnerID: owner.ID, KeyID: id, KeyName: name, Action: action, Status: constant.StatusSuccess}); err != nil && global.LOG != nil {
|
||||
global.LOG.Errorf("API key audit failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIKeyService) AuditLegacyChange(c *gin.Context) {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Errorf("legacy API key audit owner unavailable: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
recordAPIKeyEvent(c, owner, "legacy", "Legacy API Key", "update")
|
||||
}
|
||||
|
||||
func (s *APIKeyService) revokeAPIKeyTerminals(c *gin.Context, owner auth.APIKeyOwner, id string) {
|
||||
sessionID := "api-key:" + id
|
||||
if id == "legacy" {
|
||||
sessionID = terminalsession.APIAuthSessionID(owner.ID)
|
||||
}
|
||||
if err := terminalsession.RevokeWithRetry("auth_session", owner.ID, sessionID, s.RevokeTerminals); err != nil {
|
||||
c.Set("API_KEY_TERMINAL_CLOSE_PENDING", true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIKeyService) PrepareAppBinding(c *gin.Context, id string) (*auth.APIKeyBinding, error) {
|
||||
owner, err := s.owner(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id == "" {
|
||||
id = "legacy"
|
||||
}
|
||||
if id == "legacy" {
|
||||
item, config, _, fingerprint, err := s.legacy(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.ApiKey == "" || config.ApiInterfaceStatus != constant.StatusEnable {
|
||||
return nil, buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
if !item.AllowAppBinding {
|
||||
return nil, buserr.New("ErrAPIKeyAppBindingDisabled")
|
||||
}
|
||||
return &auth.APIKeyBinding{KeyID: id, KeyName: item.Name, Owner: owner, Revision: item.Revision, Fingerprint: fingerprint}, nil
|
||||
}
|
||||
key, err := repo.NewAPIKeyRepo().Get(id)
|
||||
if err != nil || key.OwnerID != owner.ID || key.OwnerType != owner.Type {
|
||||
return nil, buserr.New("ErrAPIKeyNotFound")
|
||||
}
|
||||
if err = auth.ValidateAPIKeyState(key, time.Now()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !key.AllowAppBinding {
|
||||
return nil, buserr.New("ErrAPIKeyAppBindingDisabled")
|
||||
}
|
||||
return &auth.APIKeyBinding{KeyID: key.ID, KeyName: key.Name, Owner: owner, Revision: key.Revision}, nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) ResolveAppBinding(c *gin.Context, binding auth.APIKeyBinding) (string, error) {
|
||||
if s.Provider == nil {
|
||||
return "", buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
owner, err := s.Provider.ResolveAPIKeyOwner(c, binding.Owner.Type, binding.Owner.ID)
|
||||
if err != nil {
|
||||
return "", buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
if binding.KeyID == "legacy" {
|
||||
item, config, _, fingerprint, err := s.legacy(owner)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if binding.Fingerprint == "" || fingerprint != binding.Fingerprint || binding.Revision != item.Revision {
|
||||
return "", buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
if config.ApiKey == "" || config.ApiInterfaceStatus != constant.StatusEnable {
|
||||
return "", buserr.New("ErrApiConfigStatusInvalid")
|
||||
}
|
||||
if !item.AllowAppBinding {
|
||||
return "", buserr.New("ErrAPIKeyAppBindingDisabled")
|
||||
}
|
||||
if !auth.IsIPInWhiteList(auth.GetAPIClientIP(c, config.ApiTrustedProxies), config.IpWhiteList) {
|
||||
return "", buserr.New("ErrApiConfigIPInvalid")
|
||||
}
|
||||
c.Set("API_AUTH_CLIENT_IP", auth.GetAPIClientIP(c, config.ApiTrustedProxies))
|
||||
return config.ApiKey, nil
|
||||
}
|
||||
key, err := repo.NewAPIKeyRepo().Get(binding.KeyID)
|
||||
if err != nil || key.OwnerID != owner.ID || key.OwnerType != owner.Type {
|
||||
return "", buserr.New("ErrAPIKeyNotFound")
|
||||
}
|
||||
if key.Revision != binding.Revision {
|
||||
return "", buserr.New("ErrAPIKeyConflict")
|
||||
}
|
||||
if err = auth.ValidateAPIKeyState(key, time.Now()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !key.AllowAppBinding {
|
||||
return "", buserr.New("ErrAPIKeyAppBindingDisabled")
|
||||
}
|
||||
if !auth.IsIPInWhiteList(auth.GetAPIClientIP(c, key.APITrustedProxies), key.IPWhiteList) {
|
||||
return "", buserr.New("ErrApiConfigIPInvalid")
|
||||
}
|
||||
c.Set("API_AUTH_CLIENT_IP", auth.GetAPIClientIP(c, key.APITrustedProxies))
|
||||
return auth.APIKeySecret(key)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (u *AuthService) LogOut(c *gin.Context) error {
|
||||
}
|
||||
|
||||
func CloseTerminalSessions(scope, userID, authSessionID string) {
|
||||
if err := xpack.AuthProvider.RevokeTerminalSessions(scope, userID, authSessionID); err != nil {
|
||||
if err := terminalsession.RevokeWithRetry(scope, userID, authSessionID, xpack.AuthProvider.RevokeTerminalSessions); err != nil {
|
||||
global.LOG.Warnf("revoke terminal sessions failed, scope=%s, err: %v", scope, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/auth"
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
@@ -220,6 +221,9 @@ func sortShowMenus(menus []dto.ShowMenu) {
|
||||
}
|
||||
|
||||
func (u *SettingService) Update(c *gin.Context, key, value string) error {
|
||||
if auth.IsAPICredentialSetting(key) {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
oldVal, err := settingRepo.Get(repo.WithByKey(key))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -134,6 +134,7 @@ var WebUrlMap = map[string]struct{}{
|
||||
|
||||
"/settings": {},
|
||||
"/settings/panel": {},
|
||||
"/settings/apikeys": {},
|
||||
"/settings/backupaccount": {},
|
||||
"/settings/license": {},
|
||||
"/settings/about": {},
|
||||
|
||||
@@ -36,6 +36,7 @@ ErrApiConfigKeyInvalid: "Invalid API key: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "The API request IP is not on the whitelist: {{ .detail }}"
|
||||
ErrApiConfigDisable: "This interface prohibits API calls: {{ .detail }}"
|
||||
ErrApiConfigKeyTimeInvalid: "Invalid API timestamp: {{ .detail }}"
|
||||
ErrApiConfigKeyExpired: "The API Key has expired. Adjust its expiration date or replace the key in the panel’s API Key management."
|
||||
ErrPasskeyDisabled: "Passkey requires HTTPS to be enabled"
|
||||
ErrPasskeyNotConfigured: "No passkey configured"
|
||||
ErrPasskeyLimit: "Passkey limit reached (max 5)"
|
||||
@@ -528,3 +529,10 @@ ErrSAML2LogoutExpired: "The SAML2 logout request has expired."
|
||||
ErrSAML2LogoutInvalid: "The SAML2 logout message is invalid."
|
||||
ErrSAML2LocalCredentialsReadonly: "SAML2 usernames and passwords are managed by the identity provider and cannot be changed locally."
|
||||
ErrSAML2PasswordExpirationUnsupported: "SAML2 users do not use the local password expiration policy."
|
||||
|
||||
# API key management
|
||||
ErrAPIKeyConflict: "The API key changed. Refresh and retry; repeated creation must use the original request parameters"
|
||||
ErrAPIKeyNameExists: "An unrevoked API key with this name already exists"
|
||||
ErrAPIKeyNotFound: "The API key does not exist or does not belong to the current user"
|
||||
ErrAPIKeyAppBindingDisabled: "APP QR binding is not allowed for this API key"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "Unable to verify API key terminal authorization support. Check the node connection and upgrade its Agent before retrying."
|
||||
|
||||
@@ -36,6 +36,7 @@ ErrApiConfigKeyInvalid: 'Clave API inválida: {{ .detail }}'
|
||||
ErrApiConfigIPInvalid: 'IP no permitida: {{ .detail }}'
|
||||
ErrApiConfigDisable: 'API no permitida: {{ .detail }}'
|
||||
ErrApiConfigKeyTimeInvalid: 'Timestamp inválido: {{ .detail }}'
|
||||
ErrApiConfigKeyExpired: "La clave API ha caducado. Ajusta su fecha de caducidad o reemplázala en la gestión de claves API del panel."
|
||||
ErrPasskeyDisabled: "Passkey requiere que HTTPS esté habilitado"
|
||||
ErrPasskeyNotConfigured: "No hay Passkey configurado"
|
||||
ErrPasskeyLimit: "Límite de Passkey alcanzado (máximo 5)"
|
||||
@@ -528,3 +529,10 @@ ErrSAML2LogoutExpired: "La solicitud de cierre de sesión SAML2 ha caducado."
|
||||
ErrSAML2LogoutInvalid: "El mensaje de cierre de sesión SAML2 no es válido."
|
||||
ErrSAML2LocalCredentialsReadonly: "Los nombres de usuario y las contraseñas SAML2 los gestiona el proveedor de identidad y no pueden modificarse localmente."
|
||||
ErrSAML2PasswordExpirationUnsupported: "Los usuarios SAML2 no utilizan la política local de caducidad de contraseñas."
|
||||
|
||||
# API key management
|
||||
ErrAPIKeyConflict: "La clave API cambió. Actualice y reintente; al repetir la creación, use los parámetros originales"
|
||||
ErrAPIKeyNameExists: "Ya existe una clave API no revocada con este nombre"
|
||||
ErrAPIKeyNotFound: "La clave API no existe o no pertenece al usuario actual"
|
||||
ErrAPIKeyAppBindingDisabled: "Esta clave API no permite vincular la APP mediante código QR"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "No se pudo verificar la autorización de terminal con clave API. Compruebe la conexión del nodo y actualice su Agent antes de reintentar."
|
||||
|
||||
@@ -36,6 +36,7 @@ ErrApiConfigKeyInvalid: "کلید API نامعتبر است: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "IP درخواست API در لیست سفید نیست: {{ .detail }}"
|
||||
ErrApiConfigDisable: "این رابط فراخوانی API را ممنوع میکند: {{ .detail }}"
|
||||
ErrApiConfigKeyTimeInvalid: "مهر زمانی API نامعتبر است: {{ .detail }}"
|
||||
ErrApiConfigKeyExpired: "کلید API منقضی شده است. تاریخ انقضا را تغییر دهید یا کلید را در بخش مدیریت کلیدهای API پنل جایگزین کنید."
|
||||
ErrPasskeyDisabled: "کلید عبور نیاز به فعال بودن HTTPS دارد"
|
||||
ErrPasskeyNotConfigured: "هیچ کلید عبوری پیکربندی نشده است"
|
||||
ErrPasskeyLimit: "محدودیت کلید عبور رسیده است (حداکثر ۵)"
|
||||
@@ -528,3 +529,10 @@ ErrSAML2LogoutExpired: "درخواست خروج SAML2 منقضی شده است."
|
||||
ErrSAML2LogoutInvalid: "پیام خروج SAML2 نامعتبر است."
|
||||
ErrSAML2LocalCredentialsReadonly: "نامهای کاربری و گذرواژههای SAML2 توسط ارائهدهنده هویت مدیریت میشوند و در محل قابل تغییر نیستند."
|
||||
ErrSAML2PasswordExpirationUnsupported: "کاربران SAML2 از سیاست محلی انقضای گذرواژه استفاده نمیکنند."
|
||||
|
||||
# API key management
|
||||
ErrAPIKeyConflict: "کلید API تغییر کرده است. صفحه را تازهسازی و دوباره تلاش کنید؛ برای تکرار ایجاد، از پارامترهای اصلی استفاده کنید"
|
||||
ErrAPIKeyNameExists: "یک کلید API لغونشده با این نام از قبل وجود دارد"
|
||||
ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست"
|
||||
ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user