Compare commits

...
Author SHA1 Message Date
ssongliu 65243c68c4 fix: allow licensed community restore progress route (#13913) 2026-09-24 10:18:01 +08:00
ssongliu 256e79ca81 fix: remove minimum upgrade backup copies limit (#13907) 2026-09-23 18:17:32 +08:00
ssongliu 4861eb69cb fix: separate firewalld batches by native option (#13896) 2026-09-22 18:20:18 +08:00
蘭 fb8cf15537 fix: Fix the automatic renewal certificate alert issue (#13895) 2026-09-22 15:06:48 +08:00
ssongliu c4a6791271 fix: allow updated SSH ports through firewall before restart (#13894) 2026-09-22 14:51:54 +08:00
蘭 f58e147636 fix: enhance file rename functionality (#13891) 2026-09-22 12:49:05 +08:00
ssongliu 387e9fbeed fix: correct firewall port allowances and whitelist sync reporting (#13892) 2026-09-22 12:48:40 +08:00
ssongliu 4eb627bc79 fix(vm): display additional virtual machine states (#13890) 2026-09-22 10:27:49 +08:00
ssongliu 850c86229c fix: log dashboard disk command failures (#13889) 2026-09-22 10:16:54 +08:00
CityFun f984917a66 fix(upgrade): resolve application upgrade failures (#13888) 2026-09-22 10:11:13 +08:00
A_Words 8588217fbf fix: prioritize HTTP-to-HTTPS redirects over website redirects (#13847) 2026-09-22 10:05:53 +08:00
ssongliu a2307c5f64 fix(firewall): improve batch operations and task log completion (#13886) 2026-09-22 09:46:46 +08:00
蘭 5923290de8 ref: add file task messages for copy, move, compress and decompress (#13885) 2026-09-21 17:32:14 +08:00
蘭 1c994fba4a ref: update documentation URL for the panel (#13884) 2026-09-21 17:28:23 +08:00
蘭 415ab96aab ref:improve the panel API key (#13883) 2026-09-21 17:18:15 +08:00
ssongliu 19bb823b05 fix(firewall): guard IPv6 forwarding against RA disruption (#13882) 2026-09-21 17:12:17 +08:00
ssongliu 3a5371652e refactor(firewall): simplify state reads and batch verification (#13881) 2026-09-21 15:09:13 +08:00
蘭 a267b4148a ref:Optimize and improve the panel API key (#13880) 2026-09-21 12:49:45 +08:00
蘭 36a01eb60d feat: The panel API key supports creating multiple tokens and enhance terminal capabilities (#13872) 2026-09-20 18:32:54 +08:00
王贺 65f6fdd045 fix(ssh): display SSH login logs in server timezone (#13869)
The SSH log page formatted dates with the browser timezone, so logs looked shifted when the client timezone differed from the server. Format the timestamp with the offset returned by the API so the page matches auth.log, the terminal and the CSV export. Refs #13860.
2026-09-20 18:02:46 +08:00
168 changed files with 12443 additions and 9120 deletions
+32 -22
View File
@@ -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
}
+10
View File
@@ -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()
}
+10
View File
@@ -0,0 +1,10 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
}
+10 -6
View File
@@ -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
View File
@@ -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"`
}
-27
View File
@@ -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
})
}
+52 -9
View File
@@ -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
}
+14 -12
View File
@@ -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
}
+10 -5
View File
@@ -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)
+24 -57
View File
@@ -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
}
+23 -10
View File
@@ -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)
}
}()
+2
View File
@@ -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
}
}
+5 -3
View File
@@ -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")
}
}
+3 -2
View File
@@ -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()
+28 -9
View File
@@ -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()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
-65
View File
@@ -1,65 +0,0 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func selectedDockerFirewallBackend(fallback string) string {
selected := configuredDockerFirewallBackend()
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
fallback = strings.ToLower(strings.TrimSpace(fallback))
if fallback == constant.FirewallProviderNftables {
return fallback
}
return constant.FirewallProviderIptables
}
func configuredDockerFirewallBackend() string {
if global.DB == nil {
return ""
}
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected
}
return ""
}
func selectedSystemFirewallClient() (lifecycle.Client, error) {
if provider := configuredSystemFirewallBackend(); provider != "" {
return lifecycle.NewClientFor(provider)
}
client, err := lifecycle.NewClient()
if err != nil {
return nil, err
}
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
return client, nil
}
func configuredSystemFirewallBackend() string {
if global.DB == nil {
return ""
}
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
return strings.TrimSpace(provider)
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
return selectedSystemFirewallClient()
}
func selectedSystemFirewallProvider() (string, error) {
client, err := selectedSystemFirewallClient()
if err != nil {
return "", err
}
return client.Name(), nil
}
+155 -308
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+302 -313
View File
@@ -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...)
}
+1 -1
View File
@@ -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)
}
+4 -1
View File
@@ -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)
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+11
View File
@@ -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 }}"
+5 -3
View File
@@ -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).
+1
View File
@@ -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)
+9
View File
@@ -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"
}
+23 -90
View File
@@ -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...)
}
+51 -40
View File
@@ -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])}
+7 -159
View File
@@ -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))
+67 -115
View File
@@ -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
}
+18 -41
View File
@@ -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
}
+20
View File
@@ -0,0 +1,20 @@
package filter
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type CommentRuleReader interface {
ReadRulesByComment(context.Context, Scope, string) (string, error)
}
func ReadRulesByComment(ctx context.Context, executable string, args []string, comment string) (string, error) {
name, args := cmd.WrapWithOptionalSudo(executable, args...)
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en")).RunPipe(
cmd.PipeCommand{Name: name, Args: args},
cmd.PipeCommand{Name: "sh", Args: []string{"-c", `grep -F -- "$1"; result=$?; if [ "$result" -eq 1 ]; then exit 0; fi; exit "$result"`, "sh", comment}},
)
}
+9 -121
View File
@@ -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 {
-273
View File
@@ -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
}
+11 -12
View File
@@ -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 {
+7 -19
View File
@@ -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
}
+52 -93
View File
@@ -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
}
+9 -97
View File
@@ -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 == "" {
+119 -48
View File
@@ -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
+171 -60
View File
@@ -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) {
+96 -3
View File
@@ -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"}
+1 -18
View File
@@ -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 {
+39 -62
View File
@@ -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 {
+15 -8
View File
@@ -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") {
-178
View File
@@ -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) {
+25 -61
View File
@@ -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) {
-93
View File
@@ -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
}
-136
View File
@@ -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: &current.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
}
+56 -1
View File
@@ -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) {
+1 -1
View File
@@ -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")
+41
View File
@@ -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
}
+15 -7
View File
@@ -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) {
+16
View File
@@ -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, "")
+110
View File
@@ -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
View File
@@ -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)
}
+10 -2
View File
@@ -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" {
+24 -4
View File
@@ -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 {
+205
View File
@@ -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 = &copy
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)
}
}
}
+11
View File
@@ -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()
}
+45
View File
@@ -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})
}
+6 -1
View File
@@ -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 {
+62
View File
@@ -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
View File
@@ -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"`
+34
View File
@@ -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
View File
@@ -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"`
+28
View File
@@ -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
}
+509
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
}
+4
View File
@@ -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
+1
View File
@@ -134,6 +134,7 @@ var WebUrlMap = map[string]struct{}{
"/settings": {},
"/settings/panel": {},
"/settings/apikeys": {},
"/settings/backupaccount": {},
"/settings/license": {},
"/settings/about": {},
+8
View File
@@ -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."
+8
View File
@@ -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."
+8
View File
@@ -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