fix: improve firewall lifecycle recovery (#13686)

This commit is contained in:
ssongliu
2026-09-02 14:48:59 +08:00
committed by GitHub
parent eab0bb4a94
commit 7be7368bb9
33 changed files with 883 additions and 151 deletions
+2
View File
@@ -170,6 +170,8 @@ type DockerPortGuardEndpoint struct {
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
+12 -8
View File
@@ -16,14 +16,18 @@ const FirewallRuleSequenceStep int64 = 1 << 32
type DockerPortGuardPolicy struct {
BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
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"`
Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"`
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"`
Sources string `gorm:"type:text" json:"-"`
Description string `gorm:"type:text" json:"description"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
NativeRules string `gorm:"type:text" json:"-"`
Sequence int64 `gorm:"not null;default:0" json:"-"`
}
type ForwardingRule struct {
+38 -5
View File
@@ -10,30 +10,47 @@ import (
)
type IDockerPortGuardRepo interface {
List(context.Context) ([]model.DockerPortGuardPolicy, error)
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{}
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
func (r *DockerPortGuardRepo) List(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).Order("family, host_ip, host_port, protocol").Find(&policies).Error
err := global.DB.WithContext(ctx).
Where("read_only = ?", false).
Order("family, host_ip, host_port, protocol").
Find(&policies).Error
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("uuid IN ?", uuids).Delete(&model.DockerPortGuardPolicy{}).Error
return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids).
Delete(&model.DockerPortGuardPolicy{}).Error
}
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for i := range policies {
policies[i].ReadOnly = false
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
}).Create(&policies[i]).Error; err != nil {
return err
@@ -42,3 +59,19 @@ 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
})
}
+151 -23
View File
@@ -156,15 +156,58 @@ func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation
if err != nil {
return err
}
if err := lifecycle.NewOperator(client).Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, s.addPortsBeforeStart); err != nil {
return err
operation := lifecycle.Operation(request.Operation)
operationErr := lifecycle.NewOperator(client).Operate(operation, request.WithDockerRestart, s.addPortsBeforeStart)
restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld &&
(operation == lifecycle.OperationStart || operation == lifecycle.OperationRestart)
if operation != lifecycle.OperationStart && operation != lifecycle.OperationRestart {
return operationErr
}
if request.Operation == "start" || request.Operation == "restart" {
ReconcileDockerPortGuardBestEffort(context.Background())
if operationErr != nil {
var completedErr *lifecycle.CompletedOperationError
var dockerRestartErr *lifecycle.DockerRestartError
if !errors.As(operationErr, &completedErr) && !errors.As(operationErr, &dockerRestartErr) {
return operationErr
}
if global.LOG != nil {
global.LOG.Warnf("firewall %s completed with post-start recovery errors: %v", operation, operationErr)
}
}
if restoreFirewalld {
restoreErr := s.restoreFirewalldRuntimeDependents(context.Background(), operation)
if restoreErr != nil && global.LOG != nil {
global.LOG.Errorf("restore firewalld runtime dependents after %s failed: %v", operation, restoreErr)
}
return nil
}
ReconcileDockerPortGuardBestEffort(context.Background())
return nil
}
func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error {
restoreForwarding := s.restoreForwarding
if restoreForwarding == nil {
restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) }
}
restoreDockerGuard := s.restoreDockerGuard
if restoreDockerGuard == nil {
restoreDockerGuard = ReconcileDockerPortGuard
}
dockerActive := s.dockerActive
if dockerActive == nil {
dockerActive = func() (bool, error) { return controller.CheckActive("docker") }
}
active, err := dockerActive()
restoreErr := restoreFirewalldDependents(
ctx, fmt.Sprintf("after firewalld %s", operation), err == nil && active, restoreForwarding, restoreDockerGuard,
)
if err != nil {
return errors.Join(fmt.Errorf("check Docker status after firewalld %s: %w", operation, err), restoreErr)
}
return restoreErr
}
func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) error {
provider, err := selectedSystemFirewallProvider()
if err != nil {
@@ -325,7 +368,9 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes
if restoreDockerGuard == nil {
restoreDockerGuard = ReconcileDockerPortGuard
}
restoreErr := restoreFirewalldDependents(ctx, restartDocker, restoreForwarding, restoreDockerGuard)
restoreErr := restoreFirewalldDependents(
ctx, "after resetting firewalld", restartDocker, restoreForwarding, restoreDockerGuard,
)
if err := errors.Join(resetErr, restoreErr); err != nil {
return dto.FirewallRuleResetResponse{}, err
}
@@ -371,17 +416,18 @@ func resetServiceFirewallClient(
func restoreFirewalldDependents(
ctx context.Context,
restartDocker bool,
reason string,
restoreDocker bool,
restoreForwarding func(context.Context) error,
restoreDockerGuard func(context.Context) error,
) error {
var errs []error
if err := restoreForwarding(ctx); err != nil {
errs = append(errs, fmt.Errorf("restore port forwarding after resetting firewalld: %w", err))
errs = append(errs, fmt.Errorf("restore port forwarding %s: %w", reason, err))
}
if restartDocker {
if restoreDocker {
if err := restoreDockerGuard(ctx); err != nil {
errs = append(errs, fmt.Errorf("restore Docker port guard after resetting firewalld: %w", err))
errs = append(errs, fmt.Errorf("restore Docker port guard %s: %w", reason, err))
}
}
return errors.Join(errs...)
@@ -1970,10 +2016,63 @@ func (s *FirewallService) cleanupAppliedCreate(
func (s *FirewallService) ensureSystemPort(ctx context.Context, port dto.FirewallSystemPort) error {
create, err := s.prepareSystemPortCreate(ctx, port)
if err != nil || create == nil {
if err == nil && create == nil {
return nil
}
if err == nil {
err = s.createFirewallRuleItem(ctx, *create)
}
if err == nil {
return nil
}
if !errors.Is(err, filter.ErrInventoryUnavailable) {
return err
}
return s.createFirewallRuleItem(ctx, *create)
return s.appendUFWSystemPortUnverified(ctx, port, err)
}
func (s *FirewallService) appendUFWSystemPortUnverified(
ctx context.Context,
port dto.FirewallSystemPort,
cause error,
) error {
if s.selectedProvider == nil || s.adapters == nil {
return cause
}
provider, providerErr := s.selectedProvider(ctx)
if providerErr != nil {
return errors.Join(cause, providerErr)
}
if provider != filter.ProviderUFW {
return cause
}
if global.LOG != nil {
global.LOG.Warnf(
"UFW inventory is unavailable while restoring accepted port %s/%s; attempting a restricted direct allow: %v",
port.Port, port.Protocol, cause,
)
}
runtime, resolveErr := s.adapters.Resolve(provider)
if resolveErr != nil {
return errors.Join(cause, resolveErr)
}
comment := "1panel-system-port:" + systemPortKey(port)
if appendErr := runtime.AppendUnverified(ctx, systemPortRule(provider, port), comment); appendErr != nil {
if global.LOG != nil {
global.LOG.Errorf(
"restore accepted UFW port %s/%s without rule inventory failed: %v; original error: %v",
port.Port, port.Protocol, appendErr, cause,
)
}
return errors.Join(cause, fmt.Errorf("append accepted UFW port without rule inventory: %w", appendErr))
}
if global.LOG != nil {
global.LOG.Warnf(
"restored accepted UFW port %s/%s without rule inventory; normal rule management failed: %v",
port.Port, port.Protocol, cause,
)
}
return nil
}
func (s *FirewallService) createFirewallRuleItem(ctx context.Context, item dto.FirewallRuleCreateItem) error {
@@ -2595,12 +2694,17 @@ func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current
return err
}
if !supportsNativeRuleBatch(provider) {
var syncErrors []error
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
return err
wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
}
for _, key := range sortedSystemPortKeys(previousSet) {
@@ -2608,10 +2712,14 @@ func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
return err
wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
}
return nil
return errors.Join(syncErrors...)
}
creates := make([]dto.FirewallRuleCreateItem, 0)
@@ -2806,41 +2914,61 @@ func supportsManagedFilterChains(provider string) bool {
func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error {
ctx := context.Background()
provider := filter.Provider(client.Name())
var recoveryErrors []error
recordFailure := func(stage string, err error) {
if err == nil {
return
}
wrapped := fmt.Errorf("%s for %s: %w", stage, provider, err)
recoveryErrors = append(recoveryErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("firewall post-start recovery failed: %v", wrapped)
}
}
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
isInit, _, err := loadDirectFirewallInitStatus(string(provider))
if err != nil {
return err
recordFailure("load managed chain status", err)
return errors.Join(recoveryErrors...)
}
if !isInit {
return nil
}
}
if err := s.restoreStoredFirewallRules(ctx, provider); err != nil {
return err
recordFailure("restore stored firewall rules", err)
}
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
if provider == filter.ProviderIptables {
if err := newIptablesHelperManager().SyncRequiredPorts(true); err != nil {
return err
recordFailure("synchronize required ports", err)
}
} else if err := newNftablesHelperManager().SyncRequiredPorts(); err != nil {
return err
recordFailure("synchronize required ports", err)
}
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return err
recordFailure("load configured accepted ports", err)
return errors.Join(recoveryErrors...)
}
required, err := loadRequiredFirewallPortWhiteList()
if err != nil {
return err
recordFailure("load required accepted ports", err)
return errors.Join(recoveryErrors...)
}
return s.SyncSystemPorts(ctx, nil, systemPorts(excludeFirewallPorts(configured, required)))
recordFailure(
"restore configured accepted ports",
s.SyncSystemPorts(ctx, nil, systemPorts(excludeFirewallPorts(configured, required))),
)
return errors.Join(recoveryErrors...)
}
portWhitelist, err := loadFirewallPortWhiteList()
if err != nil {
return err
recordFailure("load accepted ports", err)
return errors.Join(recoveryErrors...)
}
return s.SyncSystemPorts(ctx, nil, systemPorts(portWhitelist))
recordFailure("restore accepted ports", s.SyncSystemPorts(ctx, nil, systemPorts(portWhitelist)))
return errors.Join(recoveryErrors...)
}
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
+65 -4
View File
@@ -141,7 +141,7 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
selectedBackend := selectedDockerFirewallBackend("")
base := s.runtimeStatus(s.guardRuntime(selectedBackend), selectedBackend)
base.Version = s.loadFirewallVersion(selectedBackend)
policies, err := s.policies.List(ctx)
policies, err := s.policies.ListManaged(ctx)
if err != nil {
return dto.DockerPortGuardList{}, err
}
@@ -424,7 +424,7 @@ func (s *DockerPortGuardService) loadRuleSyncCandidates(
filter.ErrProviderUnavailable, selected, target,
)
}
policies, err := s.policies.List(ctx)
policies, err := s.policies.ListManaged(ctx)
if err != nil {
return "", nil, nil, err
}
@@ -487,6 +487,57 @@ func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGu
}
}
func dockerGuardReadOnlyRuleSyncDTO(policy docker_guard.ReadOnlyPolicy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...),
NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
}
}
func dockerGuardReadOnlyPolicyUUID(policy docker_guard.ReadOnlyPolicy) string {
nativeRules, _ := json.Marshal(policy.NativeRules)
fingerprint := strings.Join([]string{
policy.Policy.Family, policy.Policy.HostIP, strconv.Itoa(int(policy.Policy.HostPort)),
policy.Policy.Protocol, policy.Action, string(nativeRules),
}, "\x00")
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fingerprint)).String()
}
func dockerGuardRuntimeReadOnlyModels(policies []docker_guard.ReadOnlyPolicy) ([]model.DockerPortGuardPolicy, error) {
result := make([]model.DockerPortGuardPolicy, 0, len(policies))
for _, policy := range policies {
sources, err := json.Marshal(policy.Policy.Sources)
if err != nil {
return nil, err
}
nativeRules, err := json.Marshal(policy.NativeRules)
if err != nil {
return nil, err
}
result = append(result, model.DockerPortGuardPolicy{
UUID: dockerGuardReadOnlyPolicyUUID(policy),
ReadOnly: true,
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
Protocol: policy.Policy.Protocol, Sources: string(sources), NativeAction: policy.Action,
NativeRules: string(nativeRules), Sequence: policy.Sequence,
})
}
return result, nil
}
func (s *DockerPortGuardService) replaceRuntimeReadOnlyPolicies(
ctx context.Context,
policies []docker_guard.ReadOnlyPolicy,
) error {
stored, err := dockerGuardRuntimeReadOnlyModels(policies)
if err != nil {
return err
}
return s.policies.ReplaceRuntimeReadOnly(ctx, stored)
}
func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error) {
defer func() { recordDockerPortGuardReconcileError(err) }()
persistedEnabled, err := dockerPortGuardPersistedEnabled()
@@ -515,12 +566,22 @@ func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if err := s.replaceRuntimeReadOnlyPolicies(ctx, inventory.ReadOnly); err != nil {
return err
}
if !initialized {
err = runtime.Initialize(policies)
} else {
err = runtime.Reconcile(policies)
}
return err
if err != nil {
return err
}
return docker_guard.Verify(runtime, policies, inventory.ReadOnly)
}
func dockerPortGuardPersistedEnabled() (bool, error) {
@@ -588,7 +649,7 @@ func markDockerGuardFamilyNotEffective(base *dto.DockerPortGuardBase, family str
}
func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]docker_guard.Policy, error) {
stored, err := s.policies.List(ctx)
stored, err := s.policies.ListManaged(ctx)
if err != nil {
return nil, err
}
+40 -14
View File
@@ -778,7 +778,7 @@ func databaseSyncStatesEqual[T any](left, right []T, key func(T) string) bool {
func (p databaseSyncPlan) preview() dto.FirewallRuleSyncPreview {
result := dto.FirewallRuleSyncPreview{
Subsystem: p.subsystem, TargetProvider: p.target,
Items: append([]dto.FirewallRuleSyncItem(nil), p.items...),
Items: append(make([]dto.FirewallRuleSyncItem, 0, len(p.items)), p.items...),
}
for _, item := range p.items {
switch item.Status {
@@ -863,6 +863,22 @@ func (p databaseSyncPlan) reconcile(run func() error) (dto.FirewallRuleSyncResul
return p.completedResult(), nil
}
func (p databaseSyncPlan) reconcileReadOnlyPartial(run func() error) (dto.FirewallRuleSyncResult, error) {
preview := p.preview()
for _, item := range p.items {
if item.Status == firewallRuleSyncBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
return p.validationResult(), nil
}
}
if preview.Ready == 0 && preview.Removed == 0 {
return p.validationResult(), nil
}
if err := run(); err != nil {
return p.failedResult(err), err
}
return p.completedResult(), nil
}
func appendDatabaseSyncFailure(result *dto.FirewallRuleSyncResult, item dto.FirewallRuleSyncItem, err error) {
if err == nil {
err = errors.New("database synchronization failed")
@@ -988,11 +1004,11 @@ func (s *DockerPortGuardService) previewRuleSync(
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
targetPolicies, err := runtime.ListPolicies()
targetInventory, err := runtime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncPreview{}, err
}
return buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetPolicies).preview(), nil
return buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetInventory).preview(), nil
}
func (s *DockerPortGuardService) syncRules(
@@ -1007,16 +1023,19 @@ func (s *DockerPortGuardService) syncRules(
return dto.FirewallRuleSyncResult{}, err
}
runtimePolicies := dockerGuardPoliciesFromModels(policies)
targetPolicies, err := targetRuntime.ListPolicies()
targetInventory, err := targetRuntime.ListPolicies()
if err != nil {
return dto.FirewallRuleSyncResult{}, err
}
plan := buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetPolicies)
result, reconcileErr := plan.reconcile(func() error {
plan := buildDockerDatabaseSyncPlan(filter.Provider(target), policies, targetInventory)
result, reconcileErr := plan.reconcileReadOnlyPartial(func() error {
if err := s.replaceRuntimeReadOnlyPolicies(ctx, targetInventory.ReadOnly); err != nil {
return err
}
if err := docker_guard.ReconcileTarget(target, runtimePolicies, targetRuntime); err != nil {
return err
}
if err := docker_guard.Verify(targetRuntime, runtimePolicies); err != nil {
if err := docker_guard.Verify(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
return err
}
if len(policies) == 0 {
@@ -1029,10 +1048,7 @@ func (s *DockerPortGuardService) syncRules(
})
recordDockerPortGuardReconcileError(reconcileErr)
if reconcileErr != nil {
if len(policies) == 0 {
return result, reconcileErr
}
return result, nil
return result, reconcileErr
}
return result, nil
}
@@ -1040,7 +1056,7 @@ func (s *DockerPortGuardService) syncRules(
func buildDockerDatabaseSyncPlan(
target filter.Provider,
policies []model.DockerPortGuardPolicy,
actual []docker_guard.Policy,
inventory docker_guard.PolicyInventory,
) databaseSyncPlan {
desired := make([]databaseSyncDesired[docker_guard.Policy], 0, len(policies))
for _, policy := range policies {
@@ -1049,12 +1065,22 @@ func buildDockerDatabaseSyncPlan(
item: dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuleSyncDTO(policy)},
})
}
return buildDatabaseSyncPlan(
"docker", target, desired, actual, docker_guard.PolicySyncKey,
plan := buildDatabaseSyncPlan(
"docker", target, desired, inventory.Policies, docker_guard.PolicySyncKey,
func(policy docker_guard.Policy) dto.FirewallRuleSyncItem {
return dto.FirewallRuleSyncItem{SourceUUID: policy.UUID, DockerRule: dockerGuardRuntimeRuleSyncDTO(policy)}
},
)
for _, policy := range inventory.ReadOnly {
plan.items = append(plan.items, dto.FirewallRuleSyncItem{
SourceUUID: dockerGuardReadOnlyPolicyUUID(policy),
DockerRule: dockerGuardReadOnlyRuleSyncDTO(policy),
Status: firewallsync.StatusBlocked,
ReasonCode: firewallsync.ReasonReadOnlyRule,
Reason: firewallsync.ReasonMessage(firewallsync.ReasonReadOnlyRule),
})
}
return plan
}
type firewallScopeReconciler struct {
+1
View File
@@ -108,6 +108,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.InitDockerPortGuardStatus,
migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
}
}
+18
View File
@@ -1793,3 +1793,21 @@ var SimplifyFirewallRulePolicy = &gormigrate.Migration{
})
},
}
var AddDockerPortGuardReadOnly = &gormigrate.Migration{
ID: "20260902-add-docker-port-guard-read-only",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.DockerPortGuardPolicy{}) {
return nil
}
if err := tx.AutoMigrate(&model.DockerPortGuardPolicy{}); err != nil {
return err
}
if tx.Migrator().HasIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint") {
if err := tx.Migrator().DropIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint"); err != nil {
return err
}
}
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
},
}
+90 -18
View File
@@ -3,6 +3,7 @@ package docker_guard
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
@@ -134,6 +135,10 @@ func NewManagerWithRunner(runner Runner) *Manager { return &Manager{runner: runn
func (m *Manager) Initialize(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
if !m.runner.Exists("iptables-restore") {
return errors.New("iptables-restore is not installed")
}
@@ -154,7 +159,7 @@ func (m *Manager) Initialize(policies []Policy) error {
}
}
}
return m.rebuildLocked(policies)
return m.rebuildLocked(policies, inventory)
}
func (m *Manager) Bind() error {
@@ -174,11 +179,15 @@ func (m *Manager) Bind() error {
func (m *Manager) Reconcile(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
return m.rebuildLocked(policies)
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory)
}
func (m *Manager) ListPolicies() ([]Policy, error) {
policies := make([]Policy, 0)
func (m *Manager) ListPolicies() (PolicyInventory, error) {
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) {
@@ -186,22 +195,26 @@ func (m *Manager) ListPolicies() ([]Policy, error) {
}
exists, err := m.chainExists(executable, Chain)
if err != nil {
return nil, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("inspect %s chain: %w", Chain, err)}
}
if !exists {
continue
}
output, err := m.run(executable, "-S", Chain)
if err != nil {
return nil, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", Chain, err)}
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", Chain, err)}
}
parsed, err := parseDockerGuardPolicies(output, family)
if err != nil {
return nil, &FamilyError{Family: family, Err: err}
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
}
inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
for key, orders := range parsed.ManagedRuleOrders {
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
}
policies = append(policies, parsed...)
}
return policies, nil
return inventory, nil
}
func (m *Manager) Unbind() error {
@@ -368,7 +381,7 @@ func (m *Manager) restoreLifecycle(executable string, rules [][]string) error {
return nil
}
func (m *Manager) rebuildLocked(policies []Policy) error {
func (m *Manager) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
executable := executableForFamily(family)
if executable == "" || !m.runner.Exists(executable) {
@@ -382,12 +395,7 @@ func (m *Manager) rebuildLocked(policies []Policy) error {
continue
}
rules := [][]string{{"-F", Chain}, {"-A", Chain, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "RETURN"}}
for _, policy := range policies {
if policy.Family != family {
continue
}
rules = append(rules, compilePolicy(policy)...)
}
rules = append(rules, orderedIPTablesRules(family, policies, inventory)...)
rules = append(rules, []string{"-A", Chain, "-j", "RETURN"})
script, err := buildRestoreScript(rules)
if err != nil {
@@ -404,18 +412,82 @@ func (m *Manager) rebuildLocked(policies []Policy) error {
return nil
}
type orderedIPTablesRule struct {
order int64
index int
rules [][]string
}
func orderedIPTablesRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
segments := make([]orderedIPTablesRule, 0, len(policies)+len(inventory.ReadOnly))
maxOrder := int64(0)
index := 0
for _, orders := range inventory.ManagedRuleOrders {
for _, order := range orders {
if order > maxOrder {
maxOrder = order
}
}
}
for _, item := range inventory.ReadOnly {
for _, native := range item.NativeRules {
if native.Family != family || len(native.Tokens) < 2 || native.Tokens[0] != "-A" || native.Tokens[1] != Chain {
continue
}
segments = append(segments, orderedIPTablesRule{order: native.Order, index: index, rules: [][]string{append([]string(nil), native.Tokens...)}})
index++
if native.Order > maxOrder {
maxOrder = native.Order
}
}
}
for _, policy := range policies {
if policy.Family != family {
continue
}
compiled := compilePolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)]
for ruleIndex, rule := range compiled {
order := int64(0)
if ruleIndex < len(orders) {
order = orders[ruleIndex]
} else {
maxOrder++
order = maxOrder
}
segments = append(segments, orderedIPTablesRule{order: order, index: index, rules: [][]string{rule}})
index++
}
}
sort.SliceStable(segments, func(left, right int) bool {
if segments[left].order == segments[right].order {
return segments[left].index < segments[right].index
}
return segments[left].order < segments[right].order
})
rules := make([][]string, 0)
for _, segment := range segments {
rules = append(rules, segment.rules...)
}
return rules
}
func buildRestoreScript(rules [][]string) (string, error) {
var script strings.Builder
script.WriteString("*filter\n")
for _, rule := range rules {
for i, token := range rule {
if token == "" || strings.ContainsAny(token, " \t\r\n\\\"'") {
if token == "" || strings.ContainsAny(token, "\r\n\x00") {
return "", fmt.Errorf("invalid iptables-restore token %q", token)
}
if i > 0 {
script.WriteByte(' ')
}
script.WriteString(token)
if strings.ContainsAny(token, " \t\\\"'") {
script.WriteString(strconv.Quote(token))
} else {
script.WriteString(token)
}
}
script.WriteByte('\n')
}
+99 -15
View File
@@ -3,6 +3,7 @@ package docker_guard
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
@@ -28,6 +29,10 @@ func NewNftablesManagerWithRunner(runner Runner) *NftablesManager {
func (m *NftablesManager) Initialize(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
inventory, err := m.ListPolicies()
if err != nil {
return err
}
if !m.runner.Exists("nft") {
return errors.New("nft is not installed")
}
@@ -37,7 +42,7 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
return &FamilyError{Family: FamilyIPv6, Err: err}
}
return m.rebuildLocked(policies)
return m.rebuildLocked(policies, inventory)
}
func (m *NftablesManager) Bind() error {
@@ -55,14 +60,18 @@ func (m *NftablesManager) Bind() error {
func (m *NftablesManager) Reconcile(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
return m.rebuildLocked(policies)
inventory, err := m.ListPolicies()
if err != nil {
return err
}
return m.rebuildLocked(policies, inventory)
}
func (m *NftablesManager) ListPolicies() ([]Policy, error) {
func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
if !m.runner.Exists("nft") {
return nil, nil
return PolicyInventory{}, nil
}
policies := make([]Policy, 0)
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
@@ -70,15 +79,19 @@ func (m *NftablesManager) ListPolicies() ([]Policy, error) {
}
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftChain)
if err != nil {
return nil, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", NftChain, err)}
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", NftChain, err)}
}
parsed, err := parseDockerGuardPolicies(output, family)
if err != nil {
return nil, &FamilyError{Family: family, Err: err}
return PolicyInventory{}, &FamilyError{Family: family, Err: err}
}
inventory.Policies = append(inventory.Policies, parsed.Policies...)
inventory.ReadOnly = append(inventory.ReadOnly, parsed.ReadOnly...)
for key, orders := range parsed.ManagedRuleOrders {
inventory.ManagedRuleOrders[key] = append([]int64(nil), orders...)
}
policies = append(policies, parsed...)
}
return policies, nil
return inventory, nil
}
func (m *NftablesManager) Unbind() error {
@@ -234,7 +247,7 @@ func (m *NftablesManager) ensureJump(family string) error {
return m.runBatch(commands)
}
func (m *NftablesManager) rebuildLocked(policies []Policy) error {
func (m *NftablesManager) rebuildLocked(policies []Policy, inventory PolicyInventory) error {
if !m.runner.Exists("nft") {
return nil
}
@@ -245,11 +258,7 @@ func (m *NftablesManager) rebuildLocked(policies []Policy) error {
}
commands := [][]string{{"flush", "chain", tableFamily, NftTable, NftChain}}
commands = append(commands, []string{"add", "rule", tableFamily, NftTable, NftChain, "ct", "state", "{", "established,related", "}", "return"})
for _, policy := range policies {
if policy.Family == family {
commands = append(commands, compileNftPolicy(policy)...)
}
}
commands = append(commands, orderedNftRules(family, policies, inventory)...)
commands = append(commands, []string{"add", "rule", tableFamily, NftTable, NftChain, "return"})
script, err := buildNftScript(commands)
if err != nil {
@@ -262,6 +271,81 @@ func (m *NftablesManager) rebuildLocked(policies []Policy) error {
return nil
}
type orderedNftRule struct {
order int64
index int
rules [][]string
}
func orderedNftRules(family string, policies []Policy, inventory PolicyInventory) [][]string {
tableFamily := nftTableFamily(family)
segments := make([]orderedNftRule, 0, len(policies)+len(inventory.ReadOnly))
maxOrder := int64(0)
index := 0
for _, orders := range inventory.ManagedRuleOrders {
for _, order := range orders {
if order > maxOrder {
maxOrder = order
}
}
}
for _, item := range inventory.ReadOnly {
for _, native := range item.NativeRules {
if native.Family != family || len(native.Tokens) == 0 || native.Tokens[0] == "-A" {
continue
}
command := []string{"add", "rule", tableFamily, NftTable, NftChain}
command = append(command, quoteNftTokens(native.Tokens)...)
segments = append(segments, orderedNftRule{order: native.Order, index: index, rules: [][]string{command}})
index++
if native.Order > maxOrder {
maxOrder = native.Order
}
}
}
for _, policy := range policies {
if policy.Family != family {
continue
}
compiled := compileNftPolicy(policy)
orders := inventory.ManagedRuleOrders[managedOrderKey(policy.Family, policy.UUID)]
for ruleIndex, rule := range compiled {
order := int64(0)
if ruleIndex < len(orders) {
order = orders[ruleIndex]
} else {
maxOrder++
order = maxOrder
}
segments = append(segments, orderedNftRule{order: order, index: index, rules: [][]string{rule}})
index++
}
}
sort.SliceStable(segments, func(left, right int) bool {
if segments[left].order == segments[right].order {
return segments[left].index < segments[right].index
}
return segments[left].order < segments[right].order
})
rules := make([][]string, 0)
for _, segment := range segments {
rules = append(rules, segment.rules...)
}
return rules
}
func quoteNftTokens(tokens []string) []string {
quoted := make([]string, 0, len(tokens))
for _, token := range tokens {
if strings.ContainsAny(token, " \t\\\"'") && !strings.HasPrefix(token, `"`) {
quoted = append(quoted, strconv.Quote(token))
continue
}
quoted = append(quoted, token)
}
return quoted
}
func compileNftPolicy(policy Policy) [][]string {
tableFamily := nftTableFamily(policy.Family)
addressKeyword := tableFamily
@@ -11,35 +11,57 @@ import (
)
type observedPolicy struct {
policy Policy
dropAll bool
droppedSource []string
allowedSource []string
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool
droppedSource []string
allowedSource []string
acceptedSource []string
acceptAll bool
}
func parseDockerGuardPolicies(output, family string) ([]Policy, error) {
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
sequence := int64(0)
for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, "1panel-docker:") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens, err := shellwords.Parse(strings.TrimSpace(line))
tokens, err := shellwords.Parse(line)
if err != nil {
return nil, fmt.Errorf("parse Docker guard rule: %w", err)
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
}
managed := strings.Contains(line, "1panel-docker:")
if !managed && !hasAcceptAction(tokens) {
continue
}
sequence++
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil {
return nil, err
return PolicyInventory{}, err
}
key := strings.Join([]string{fragment.UUID, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
identity := fragment.UUID
if action == "accept" {
identity = action
}
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
group, exists := groups[key]
if !exists {
group = &observedPolicy{policy: fragment}
group = &observedPolicy{policy: fragment, sequence: sequence}
groups[key] = group
order = append(order, key)
}
switch {
case action == "accept" && source != "":
group.acceptedSource = append(group.acceptedSource, source)
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "accept":
group.acceptAll = true
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "":
@@ -47,12 +69,22 @@ func parseDockerGuardPolicies(output, family string) ([]Policy, error) {
case action == "drop":
group.dropAll = true
default:
return nil, fmt.Errorf("unsupported Docker guard rule action %q", action)
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
if action != "accept" {
group.managedOrders = append(group.managedOrders, sequence)
}
}
policies := make([]Policy, 0, len(order))
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
for _, key := range order {
group := groups[key]
if group.acceptAll || len(group.acceptedSource) > 0 {
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
})
continue
}
switch {
case len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
@@ -63,11 +95,36 @@ func parseDockerGuardPolicies(output, family string) ([]Policy, error) {
case group.dropAll:
group.policy.Mode = ModeAll
default:
return nil, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
policies = append(policies, group.policy)
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...)
}
return policies, nil
return inventory, nil
}
func nativeRuleTokens(tokens []string) []string {
result := make([]string, 0, len(tokens))
for index, token := range tokens {
if token == "#" {
tokens = tokens[:index]
break
}
}
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
tokens = tokens[:len(tokens)-2]
}
for index := 0; index < len(tokens); index++ {
result = append(result, tokens[index])
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
index += 4
}
}
return result
}
func managedOrderKey(family, policyUUID string) string {
return family + "\x00" + policyUUID
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
@@ -79,8 +136,12 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
policy.Protocol = nextPolicyToken(tokens, index)
case "--ctorigdst":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "-d":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "--ctorigdstport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "--dport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "-s":
source = nextPolicyToken(tokens, index)
case "--comment", "comment":
@@ -107,19 +168,52 @@ func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string,
}
}
case "ip", "ip6":
if nextPolicyToken(tokens, index) == "saddr" {
switch nextPolicyToken(tokens, index) {
case "saddr":
source = nextPolicyToken(tokens, index+1)
case "daddr":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
}
case "tcp", "udp":
if nextPolicyToken(tokens, index) == "dport" {
policy.Protocol = tokens[index]
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
}
case "accept", "drop", "return":
if isCommentValue(tokens, index) {
continue
}
case "drop", "return":
action = tokens[index]
}
}
if policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0 || action == "" {
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
policy.Protocol = "all"
}
return policy, source, action, nil
}
func hasAcceptAction(tokens []string) bool {
for index, token := range tokens {
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
return true
}
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) {
return true
}
}
return false
}
func isCommentValue(tokens []string, index int) bool {
if index == 0 {
return false
}
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
}
func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String()
+67 -4
View File
@@ -2,10 +2,30 @@ package docker_guard
import (
"fmt"
"slices"
"github.com/1Panel-dev/1Panel/agent/constant"
)
type NativeRule struct {
Family string `json:"family"`
Order int64 `json:"order"`
Tokens []string `json:"tokens"`
}
type ReadOnlyPolicy struct {
Policy Policy
Action string
Sequence int64
NativeRules []NativeRule
}
type PolicyInventory struct {
Policies []Policy
ReadOnly []ReadOnlyPolicy
ManagedRuleOrders map[string][]int64
}
type Runtime interface {
Initialize([]Policy) error
Bind() error
@@ -14,7 +34,7 @@ type Runtime interface {
Cleanup() error
Initialized(string) (bool, error)
Status(string) FamilyStatus
ListPolicies() ([]Policy, error)
ListPolicies() (PolicyInventory, error)
}
func NewRuntime(provider string) Runtime {
@@ -24,17 +44,60 @@ func NewRuntime(provider string) Runtime {
return NewManager()
}
func Verify(runtime Runtime, desired []Policy) error {
actual, err := runtime.ListPolicies()
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(actual, desired) {
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
+8 -1
View File
@@ -5,7 +5,10 @@ import (
"errors"
)
var ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable")
var (
ErrAdapterUnavailable = errors.New("firewall rule adapter is unavailable")
ErrInventoryUnavailable = errors.New("firewall rule inventory is unavailable")
)
type ChangeOperation string
@@ -80,6 +83,10 @@ type RuleChecker interface {
CheckRule(context.Context, FirewallRule) error
}
type UnverifiedRuleAppender interface {
AppendUnverified(context.Context, FirewallRule, string) error
}
type NativeDetailReader interface {
NativeDetail(context.Context, string, bool) (string, error)
}
+51 -6
View File
@@ -111,14 +111,18 @@ func CheckCreate(
exact := make([]ObservedRule, 0)
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
if observed.Rule.Scope.Provider == ProviderFirewalld && observed.Rule.NativeKind == NativeKindZoneService {
if hydrated, ok := hydrateOwnedPartialRule(observed, desired); ok {
observed = hydrated
} else {
if observed.Rule.Scope.Provider == ProviderFirewalld && observed.Rule.NativeKind == NativeKindZoneService {
continue
}
result.Decision = CheckDecisionBlocked
result.Classification = CheckClassificationUnsupported
result.Reason = "opaque_rule_in_target_scope"
result.Candidates = append(result.Candidates, observed)
continue
}
result.Decision = CheckDecisionBlocked
result.Classification = CheckClassificationUnsupported
result.Reason = "opaque_rule_in_target_scope"
result.Candidates = append(result.Candidates, observed)
continue
}
observedRule, err := NormalizeRule(observed.Rule)
if err != nil {
@@ -164,6 +168,47 @@ func CheckCreate(
return finishCheck(result)
}
func hydrateOwnedPartialRule(observed ObservedRule, desired []DesiredRule) (ObservedRule, bool) {
if observed.ParseStatus != ParseStatusPartial || strings.TrimSpace(observed.Marker) == "" ||
len(observed.UncertainFields) != 1 || observed.UncertainFields[0] != ObservedFieldProtocol {
return observed, false
}
var matched *DesiredRule
for index := range desired {
candidate := &desired[index]
if candidate.Origin != RuleOriginCreated && candidate.Origin != RuleOriginAdopted {
continue
}
if candidate.Rule.Scope.Normalize().Key() != observed.Rule.Scope.Normalize().Key() ||
!desiredMarkerMatchesObserved(*candidate, observed.Marker) ||
!ObservedRuleMatchesExpected(observed, candidate.Rule) {
continue
}
if matched != nil {
return observed, false
}
matched = candidate
}
if matched == nil {
return observed, false
}
orderIndex := observed.Rule.OrderIndex
observed.Rule = matched.Rule
observed.Rule.OrderIndex = orderIndex
observed.ParseStatus = ParseStatusSupported
observed.UncertainFields = nil
return observed, true
}
func desiredMarkerMatchesObserved(desired DesiredRule, observedMarker string) bool {
observedMarker = strings.TrimSpace(observedMarker)
if observedMarker == strings.TrimSpace(desired.Marker) {
return observedMarker != ""
}
return desired.Origin == RuleOriginAdopted && strings.TrimSpace(desired.UUID) != "" &&
observedMarker == "1panel-rule:"+strings.TrimSpace(desired.UUID)
}
func finishCheck(result RuleCheckResult) (RuleCheckResult, error) {
for index, candidate := range result.Candidates {
identity, err := InstanceKey(candidate)
@@ -54,6 +54,30 @@ func (a *Adapter) PrepareRule(rule filter.FirewallRule) (filter.FirewallRule, er
return normalized, nil
}
func (a *Adapter) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
normalized, err := a.PrepareRule(rule)
if err != nil {
return err
}
if normalized.Action != filter.ActionAccept || normalized.DestinationPort == "" ||
normalized.SourceAddress != "" || normalized.SourcePort != "" ||
normalized.DestinationAddress != "" || normalized.Interface != "" {
return fmt.Errorf("%w: unverified UFW appends only support accepted ports", filter.ErrInvalidRule)
}
if a.writer == nil {
return errors.New("ufw writer is required")
}
comment = strings.TrimSpace(comment)
if comment == "" || strings.ContainsAny(comment, "\r\n\x00") {
return fmt.Errorf("%w: invalid UFW fallback comment", filter.ErrInvalidRule)
}
command := commentCommand(normalized, comment)
if err := validateCommand(command); err != nil {
return err
}
return a.writer.Run(ctx, command)
}
func (a *Adapter) Capabilities(context.Context) (filter.Capabilities, error) {
return filter.Capabilities{
Scopes: []filter.ScopePattern{{
@@ -89,7 +113,7 @@ func (a *Adapter) ObserveScopes(ctx context.Context, scopes []filter.Scope) ([]f
}
numbered, err := a.reader.Read(ctx, "status", "numbered")
if err != nil {
return nil, err
return nil, fmt.Errorf("%w: read UFW numbered status: %w", filter.ErrInventoryUnavailable, err)
}
notices := statusNotices(numbered)
@@ -118,6 +118,14 @@ func (e *Engine) CheckRule(ctx context.Context, rule filter.FirewallRule) error
return checker.CheckRule(ctx, rule)
}
func (e *Engine) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
appender, ok := e.adapter.(filter.UnverifiedRuleAppender)
if !ok {
return fmt.Errorf("%w: %s does not support unverified rule appends", filter.ErrAdapterUnavailable, e.Provider())
}
return appender.AppendUnverified(ctx, rule, comment)
}
func (e *Engine) CompileDesired(
ctx context.Context,
policyUUID string,
+24 -4
View File
@@ -1,6 +1,7 @@
package lifecycle
import (
"errors"
"fmt"
"os"
@@ -36,11 +37,25 @@ func (e *DockerRestartError) Unwrap() error {
return e.Err
}
type CompletedOperationError struct {
Operation Operation
Err error
}
func (e *CompletedOperationError) Error() string {
return fmt.Sprintf("firewall %s completed with recovery errors: %v", e.Operation, e.Err)
}
func (e *CompletedOperationError) Unwrap() error {
return e.Err
}
func NewOperator(client Client) *Operator {
return &Operator{client: client}
}
func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error {
var recoveryErrors []error
switch operation {
case OperationStart:
if err := o.client.Start(); err != nil {
@@ -48,7 +63,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
return err
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after start: %w", err))
}
}
case OperationStop:
@@ -59,7 +74,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
return err
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after restart: %w", err))
}
}
default:
@@ -68,11 +83,16 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
return &DockerRestartError{Err: err}
recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err})
}
}
if o.client.Name() == ProviderFirewalld && operation == OperationStart {
return restoreFail2BanAfterFirewallStart()
if err := restoreFail2BanAfterFirewallStart(); err != nil {
recoveryErrors = append(recoveryErrors, err)
}
}
if err := errors.Join(recoveryErrors...); err != nil {
return &CompletedOperationError{Operation: operation, Err: err}
}
return nil
}
+3
View File
@@ -26,6 +26,7 @@ const (
ReasonOnlyExistsInTarget ReasonCode = "only_exists_in_target"
ReasonManagedOnlyInTarget ReasonCode = "managed_only_exists_in_target"
ReasonUnsafeRemoval ReasonCode = "unsafe_managed_rule_removal"
ReasonReadOnlyRule ReasonCode = "read_only_rule"
)
func ReasonMessage(code ReasonCode) string {
@@ -38,6 +39,8 @@ func ReasonMessage(code ReasonCode) string {
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 ""
}
+2
View File
@@ -400,6 +400,8 @@ export namespace Firewall {
application?: string;
policyUUID?: string;
mode?: 'deny_sources' | 'allow_sources' | 'deny_all';
nativeAction?: string;
readOnly?: boolean;
sources: string[];
effective: boolean;
description?: string;
+4 -2
View File
@@ -4153,6 +4153,8 @@ const message = {
stale: 'The firewall rule state is outdated. Refresh and try again.',
lockoutRisk: 'This firewall change may block management access.',
protectedRule: 'This protected firewall rule cannot be modified.',
dockerAcceptReadOnly:
'This ACCEPT rule is read-only and will be preserved while other rules are synchronized. To remove it, delete it manually on the host.',
cannotReconcile: 'The target rule cannot be reconciled: {0}',
},
ruleSyncPartial: 'Synchronization completed: {0} succeeded, {1} already existed, and {2} failed.',
@@ -4172,10 +4174,10 @@ const message = {
cleanupDockerBackendHelper:
'Reset the 1Panel Docker port protection runtime rules in {0}: delete all related rules and chains while retaining database data',
cleanupBeforeBackendSwitch:
'The current {0} backend still contains 1Panel runtime rules. Reset it before switching to {1}.',
'The current {0} backend still contains 1Panel runtime rules. Reset it before switching to {1}. Resetting only removes runtime rules; saved database policies are retained and can be initialized or synchronized after switching.',
cleanupAction: 'Reset',
backendSwitchNotice:
'Reset the current backend before switching the system firewall, port forwarding, or Docker protection. Saved database policies are retained and can be initialized or synchronized after switching.',
'Use only one firewall management method at a time. Running multiple firewalls simultaneously may cause rule conflicts, inconsistent status, or container port access failures.',
switchBackendHelper: 'Switch to {0}?',
switchDockerBackendHelper: 'Switch to {0}? This will update the Docker configuration and restart Docker.',
uninstalledStatus: 'Not installed',
+4 -2
View File
@@ -4200,6 +4200,8 @@ const message = {
stale: 'El estado de la regla está desactualizado. Actualiza e inténtalo de nuevo.',
lockoutRisk: 'Este cambio puede bloquear el acceso de administración.',
protectedRule: 'Esta regla protegida del firewall no se puede modificar.',
dockerAcceptReadOnly:
'Esta regla ACCEPT es de solo lectura y se conservará al sincronizar las demás reglas. Para eliminarla, bórrela manualmente en el host.',
cannotReconcile: 'No se puede sincronizar la regla de destino: {0}',
},
ruleSyncPartial: 'Sincronización completada: {0} correctas, {1} ya existían y {2} fallaron.',
@@ -4219,10 +4221,10 @@ const message = {
cleanupDockerBackendHelper:
'Restablece las reglas de ejecución de protección de puertos Docker de 1Panel en {0}: elimina todas las reglas y cadenas relacionadas y conserva los datos de la base de datos',
cleanupBeforeBackendSwitch:
'El backend actual {0} todavía contiene reglas de ejecución de 1Panel. Restablézcalo antes de cambiar a {1}.',
'El backend actual {0} todavía contiene reglas de ejecución de 1Panel. Restablézcalo antes de cambiar a {1}. El restablecimiento solo elimina las reglas de ejecución; las políticas de la base de datos se conservan y pueden inicializarse o sincronizarse después del cambio.',
cleanupAction: 'Restablecer',
backendSwitchNotice:
'Restablece el backend actual antes de cambiar el firewall del sistema, el reenvío de puertos o la protección Docker. Las políticas de la base de datos se conservan y pueden inicializarse o sincronizarse después.',
'Se recomienda utilizar un único método de gestión de firewall. Ejecutar varios firewalls al mismo tiempo puede causar conflictos de reglas, estados inconsistentes o problemas de acceso a los puertos de los contenedores.',
switchBackendHelper: '¿Cambiar a {0}?',
switchDockerBackendHelper: '¿Cambiar a {0}? Esto actualizará la configuración y reiniciará Docker.',
uninstalledStatus: 'No instalado',
+4 -2
View File
@@ -4109,6 +4109,8 @@ const message = {
stale: 'وضعیت قانون فایروال قدیمی است. صفحه را تازه کنید و دوباره تلاش کنید.',
lockoutRisk: 'این تغییر ممکن است دسترسی مدیریتی را مسدود کند.',
protectedRule: 'این قانون محافظت‌شدهٔ فایروال قابل تغییر نیست.',
dockerAcceptReadOnly:
'این قانون ACCEPT فقط خواندنی است و هنگام همگام‌سازی سایر قوانین حفظ می‌شود. برای حذف آن، قانون را به‌صورت دستی از میزبان پاک کنید.',
cannotReconcile: 'قانون مقصد قابل همگام‌سازی نیست: {0}',
},
ruleSyncPartial: 'همگام‌سازی پایان یافت: {0} موفق، {1} از قبل موجود و {2} ناموفق.',
@@ -4128,10 +4130,10 @@ const message = {
cleanupDockerBackendHelper:
'قوانین زمان اجرای محافظت پورت Docker مربوط به 1Panel در {0} بازنشانی می‌شوند: همه قوانین و زنجیره‌های مرتبط حذف و داده‌های پایگاه داده حفظ می‌شوند',
cleanupBeforeBackendSwitch:
'بک‌اند فعلی {0} هنوز شامل قوانین زمان اجرای 1Panel است. پیش از تغییر به {1} آن را بازنشانی کنید.',
'بک‌اند فعلی {0} هنوز شامل قوانین زمان اجرای 1Panel است. پیش از تغییر به {1} آن را بازنشانی کنید. بازنشانی فقط قوانین زمان اجرا را پاک می‌کند؛ سیاست‌های پایگاه داده حفظ می‌شوند و پس از تغییر می‌توان آن‌ها را دوباره راه‌اندازی یا همگام‌سازی کرد.',
cleanupAction: 'بازنشانی',
backendSwitchNotice:
'پیش از تغییر فایروال سیستم، انتقال پورت یا محافظت Docker، بک‌اند فعلی را بازنشانی کنید. سیاست‌های پایگاه داده حفظ می‌شوند و پس از تغییر قابل راه‌اندازی یا همگام‌سازی هستند.',
'توصیه می‌شود تنها یک روش مدیریت فایروال فعال باشد. اجرای هم‌زمان چند فایروال ممکن است باعث تداخل قوانین، ناهماهنگی وضعیت یا اختلال در دسترسی به پورت‌های کانتینر شود.',
switchBackendHelper: 'به {0} تغییر داده شود؟',
switchDockerBackendHelper:
'به {0} تغییر داده شود؟ پیکربندی Docker به‌روزرسانی و Docker راه‌اندازی مجدد می‌شود.',
+4 -2
View File
@@ -4138,6 +4138,8 @@ const message = {
stale: 'ファイアウォールルールの状態が古くなっています更新して再試行してください',
lockoutRisk: 'この変更により管理アクセスが遮断される可能性があります',
protectedRule: '保護されたファイアウォールルールは変更できません',
dockerAcceptReadOnly:
'この ACCEPT ルールは読み取り専用で他のルールを同期しても保持されます削除する場合はホスト上で手動で削除してください',
cannotReconcile: '対象ルールを同期できません{0}',
},
ruleSyncPartial: '同期完了成功 {0} 存在済み {1} 失敗 {2} ',
@@ -4157,10 +4159,10 @@ const message = {
cleanupDockerBackendHelper:
'{0} 1Panel Docker ポート保護ランタイムルールをリセットします関連するルールとチェーンをすべて削除しデータベースデータを保持します',
cleanupBeforeBackendSwitch:
'現在の {0} バックエンドには 1Panel の実行時ルールが残っています{1} に切り替える前にリセットしてください',
'現在の {0} バックエンドには 1Panel の実行時ルールが残っています{1} に切り替える前にリセットしてくださいリセットでは実行時ルールのみが削除されデータベースポリシーは保持されます切り替え後に再初期化または同期できます',
cleanupAction: 'リセット',
backendSwitchNotice:
'システムファイアウォールポート転送Docker 保護を切り替える前に現在のバックエンドをリセットしてくださいデータベースポリシーは保持され切り替え後に再初期化または同期できます',
'ファイアウォールの管理方式は 1 つだけ有効にすることを推奨します複数のファイアウォールを同時に実行するとルールの競合状態の不一致コンテナーポートへのアクセス異常が発生する可能性があります',
switchBackendHelper: '{0} に切り替えますか',
switchDockerBackendHelper: '{0} に切り替えますかDocker の設定を更新しDocker を再起動します',
uninstalledStatus: '未インストール',
+4 -2
View File
@@ -4061,6 +4061,8 @@ const message = {
stale: '방화벽 규칙 상태가 오래되었습니다. 새로 고친 다시 시도하세요.',
lockoutRisk: ' 방화벽 변경으로 관리 접근이 차단될 있습니다.',
protectedRule: '보호된 방화벽 규칙은 수정할 없습니다.',
dockerAcceptReadOnly:
' ACCEPT 규칙은 읽기 전용이며 다른 규칙을 동기화할 그대로 유지됩니다. 제거하려면 호스트에서 수동으로 삭제하세요.',
cannotReconcile: '대상 규칙을 동기화할 없습니다: {0}',
},
ruleSyncPartial: '동기화 완료: 성공 {0}, 이미 존재 {1}, 실패 {2}.',
@@ -4080,10 +4082,10 @@ const message = {
cleanupDockerBackendHelper:
'{0} 1Panel Docker 포트 보호 런타임 규칙을 재설정합니다. 관련 규칙과 체인을 모두 삭제하고 데이터베이스 데이터는 유지합니다',
cleanupBeforeBackendSwitch:
'현재 {0} 백엔드에 1Panel 런타임 규칙이 남아 있습니다. {1}() 전환하기 전에 먼저 재설정하세요.',
'현재 {0} 백엔드에 1Panel 런타임 규칙이 남아 있습니다. {1}() 전환하기 전에 먼저 재설정하세요. 재설정은 런타임 규칙만 정리하며 데이터베이스 정책은 유지됩니다. 전환 다시 초기화하거나 동기화할 있습니다.',
cleanupAction: '재설정',
backendSwitchNotice:
'시스템 방화벽, 포트 전달 Docker 보호를 전환하기 현재 백엔드를 재설정하세요. 데이터베이스 정책은 유지되며 전환 다시 초기화하거나 동기화 있습니다.',
'하나의 방화벽 관리 방식만 활성화하 것이 좋습니다. 여러 방화벽을 동시 실행하면 규칙 충돌, 상태 불일치 또는 컨테이너 포트 접근 오류가 발생 있습니다.',
switchBackendHelper: '{0}() 전환하시겠습니까?',
switchDockerBackendHelper: '{0}() 전환하시겠습니까? Docker 설정을 업데이트하고 Docker를 재시작합니다.',
uninstalledStatus: '설치되지 않음',
+5 -2
View File
@@ -4029,6 +4029,8 @@ const message = {
stale: 'ສະຖານະກົດໄຟວໍເກົ່າແລ້ວ. ກະລຸນາໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.',
lockoutRisk: 'ການປ່ຽນແປງນີ້ອາດຂັດຂວາງການເຂົ້າເຖິງຈັດການ.',
protectedRule: 'ກົດໄຟວໍທີ່ຖືກປົກປ້ອງນີ້ບໍ່ສາມາດແກ້ໄຂໄດ້.',
dockerAcceptReadOnly:
'ກົດ ACCEPT ນີ້ເປັນແບບອ່ານຢ່າງດຽວ ແລະຈະຖືກເກັບໄວ້ເມື່ອຊິງກົດອື່ນ. ຫາກຕ້ອງການລຶບ ໃຫ້ລຶບດ້ວຍຕົນເອງໃນໂຮສ.',
cannotReconcile: 'ບໍ່ສາມາດຊິງກົດເປົ້າໝາຍ: {0}',
},
ruleSyncPartial: 'ຊິງສຳເລັດ: ສຳເລັດ {0}, ມີແລ້ວ {1}, ລົ້ມເຫຼວ {2}.',
@@ -4047,10 +4049,11 @@ const message = {
'ຣີເຊັດກົດ runtime ສຳລັບການສົ່ງຕໍ່ພອດ 1Panel ໃນ {0}: ລຶບກົດ ແລະ chain ທີ່ກ່ຽວຂ້ອງທັງໝົດ ແລະເກັບຂໍ້ມູນໃນຖານຂໍ້ມູນ',
cleanupDockerBackendHelper:
'ຣີເຊັດກົດ runtime ປ້ອງກັນພອດ Docker ຂອງ 1Panel ໃນ {0}: ລຶບກົດ ແລະ chain ທີ່ກ່ຽວຂ້ອງທັງໝົດ ແລະເກັບຂໍ້ມູນໃນຖານຂໍ້ມູນ',
cleanupBeforeBackendSwitch: 'ແບັກເອັນປັດຈຸບັນ {0} ຍັງມີກົດ runtime ຂອງ 1Panel. ກະລຸນາຣີເຊັດກ່ອນສະຫຼັບໄປ {1}.',
cleanupBeforeBackendSwitch:
'ແບັກເອັນປັດຈຸບັນ {0} ຍັງມີກົດ runtime ຂອງ 1Panel. ກະລຸນາຣີເຊັດກ່ອນສະຫຼັບໄປ {1}. ການຣີເຊັດຈະລ້າງສະເພາະກົດ runtime; ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້ ແລະສາມາດເລີ່ມ ຫຼືຊິງຫຼັງສະຫຼັບ.',
cleanupAction: 'ຣີເຊັດ',
backendSwitchNotice:
'ຣີເຊັດແບັກເອັນປັດຈຸບັນກ່ອນສະຫຼັບໄຟວໍລະບົບ, ການສົ່ງຕໍ່ພອດ ຫຼື ການປ້ອງກັນ Docker. ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້ ແລະສາມາດເລີ່ມ ຫຼືຊິງຫຼັງສະຫຼັບ.',
'ແນະນຳໃຫ້ເປີດໃຊ້ວິທີຈັດການໄຟວໍພຽງແບບດຽວ. ການໃຊ້ຫຼາຍໄຟວໍພ້ອມກັນອາດເຮັດໃຫ້ກົດຂັດແຍ່ງ, ສະຖານະບໍ່ກົງກັນ ຫຼື ການເຂົ້າເຖິງພອດຄອນເທນເນີຜິດປົກກະຕິ.',
switchBackendHelper: 'ຢືນຢັນການສະຫຼັບເປັນ {0} ບໍ?',
switchDockerBackendHelper: 'ປ່ຽນເປັນ {0} ບໍ? ການຕັ້ງຄ່າ Docker ຈະຖືກອັບເດດ ແລະ Docker ຈະເລີ່ມໃໝ່.',
uninstalledStatus: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງ',
+4 -2
View File
@@ -4221,6 +4221,8 @@ const message = {
stale: 'Keadaan peraturan tembok api sudah lapuk. Muat semula dan cuba lagi.',
lockoutRisk: 'Perubahan ini mungkin menyekat akses pengurusan.',
protectedRule: 'Peraturan tembok api yang dilindungi ini tidak boleh diubah.',
dockerAcceptReadOnly:
'Peraturan ACCEPT ini adalah baca sahaja dan akan dikekalkan semasa peraturan lain disegerakkan. Untuk membuangnya, padam secara manual pada hos.',
cannotReconcile: 'Peraturan sasaran tidak dapat disegerakkan: {0}',
},
ruleSyncPartial: 'Penyegerakan selesai: {0} berjaya, {1} sudah wujud dan {2} gagal.',
@@ -4240,10 +4242,10 @@ const message = {
cleanupDockerBackendHelper:
'Tetapkan semula peraturan masa jalan perlindungan port Docker 1Panel dalam {0}: padam semua peraturan dan rantaian berkaitan sambil mengekalkan data pangkalan data',
cleanupBeforeBackendSwitch:
'Bahagian belakang semasa {0} masih mengandungi peraturan masa jalan 1Panel. Tetapkan semula sebelum bertukar kepada {1}.',
'Bahagian belakang semasa {0} masih mengandungi peraturan masa jalan 1Panel. Tetapkan semula sebelum bertukar kepada {1}. Penetapan semula hanya membersihkan peraturan masa jalan; dasar pangkalan data dikekalkan dan boleh dimulakan atau disegerakkan selepas penukaran.',
cleanupAction: 'Tetapkan semula',
backendSwitchNotice:
'Tetapkan semula bahagian belakang semasa sebelum menukar tembok api sistem, pemajuan port atau perlindungan Docker. Dasar pangkalan data dikekalkan dan boleh dimulakan atau disegerakkan selepas penukaran.',
'Disyorkan untuk mengaktifkan hanya satu kaedah pengurusan tembok api. Menjalankan berbilang tembok api serentak boleh menyebabkan konflik peraturan, status tidak konsisten atau masalah akses port kontena.',
switchBackendHelper: 'Tukar kepada {0}?',
switchDockerBackendHelper: 'Tukar kepada {0}? Ini akan mengemas kini konfigurasi dan memulakan semula Docker.',
uninstalledStatus: 'Belum dipasang',
+4 -2
View File
@@ -4240,6 +4240,8 @@ const message = {
stale: 'O estado da regra está desatualizado. Atualize e tente novamente.',
lockoutRisk: 'Esta alteração pode bloquear o acesso de gerenciamento.',
protectedRule: 'Esta regra protegida do firewall não pode ser modificada.',
dockerAcceptReadOnly:
'Esta regra ACCEPT é somente leitura e será preservada durante a sincronização das outras regras. Para removê-la, exclua-a manualmente no host.',
cannotReconcile: 'Não foi possível sincronizar a regra de destino: {0}',
},
ruleSyncPartial: 'Sincronização concluída: {0} com sucesso, {1} existiam e {2} falharam.',
@@ -4259,10 +4261,10 @@ const message = {
cleanupDockerBackendHelper:
'Redefina as regras de execução de proteção de portas Docker do 1Panel em {0}: exclua todas as regras e cadeias relacionadas e mantenha os dados do banco de dados',
cleanupBeforeBackendSwitch:
'O backend atual {0} ainda contém regras de execução do 1Panel. Redefina-o antes de mudar para {1}.',
'O backend atual {0} ainda contém regras de execução do 1Panel. Redefina-o antes de mudar para {1}. A redefinição remove apenas as regras de execução; as políticas do banco são mantidas e podem ser inicializadas ou sincronizadas após a troca.',
cleanupAction: 'Redefinir',
backendSwitchNotice:
'Redefina o backend atual antes de trocar o firewall do sistema, o encaminhamento de portas ou a proteção Docker. As políticas do banco são mantidas e podem ser inicializadas ou sincronizadas após a troca.',
'Recomenda-se habilitar apenas uma forma de gerenciamento de firewall. Executar vários firewalls ao mesmo tempo pode causar conflitos de regras, estados inconsistentes ou falhas de acesso às portas dos contêineres.',
switchBackendHelper: 'Mudar para {0}?',
switchDockerBackendHelper: 'Mudar para {0}? Isso atualizará a configuração e reiniciará o Docker.',
uninstalledStatus: 'Não instalado',
+4 -2
View File
@@ -4207,6 +4207,8 @@ const message = {
stale: 'Состояние правила устарело. Обновите данные и повторите попытку.',
lockoutRisk: 'Это изменение может заблокировать доступ к управлению.',
protectedRule: 'Это защищённое правило брандмауэра нельзя изменить.',
dockerAcceptReadOnly:
'Это правило ACCEPT доступно только для чтения и будет сохранено при синхронизации остальных правил. Чтобы удалить его, сделайте это вручную на хосте.',
cannotReconcile: 'Не удалось синхронизировать целевое правило: {0}',
},
ruleSyncPartial: 'Синхронизация завершена: успешно {0}, уже существовало {1}, ошибок {2}.',
@@ -4226,10 +4228,10 @@ const message = {
cleanupDockerBackendHelper:
'Сбросить правила времени выполнения защиты портов Docker 1Panel в {0}: удалить все связанные правила и цепочки, сохранив данные в базе данных',
cleanupBeforeBackendSwitch:
'Текущий бэкенд {0} всё ещё содержит правила времени выполнения 1Panel. Сбросьте его перед переключением на {1}.',
'Текущий бэкенд {0} всё ещё содержит правила времени выполнения 1Panel. Сбросьте его перед переключением на {1}. Сброс удаляет только правила времени выполнения; политики базы данных сохраняются и после переключения могут быть повторно инициализированы или синхронизированы.',
cleanupAction: 'Сбросить',
backendSwitchNotice:
'Перед переключением системного брандмауэра, переадресации портов или защиты Docker сбросьте текущий бэкенд. Политики базы данных сохраняются и могут быть инициализированы или синхронизированы после переключения.',
'Рекомендуется использовать только один способ управления брандмауэром. Одновременная работа нескольких брандмауэров может привести к конфликтам правил, несогласованному состоянию или проблемам доступа к портам контейнеров.',
switchBackendHelper: 'Переключиться на {0}?',
switchDockerBackendHelper: 'Переключиться на {0}? Конфигурация Docker будет обновлена, а Docker перезапущен.',
uninstalledStatus: 'Не установлено',
+4 -2
View File
@@ -4221,6 +4221,8 @@ const message = {
stale: 'Güvenlik duvarı kuralının durumu güncel değil. Yenileyip tekrar deneyin.',
lockoutRisk: 'Bu değişiklik yönetim erişimini engelleyebilir.',
protectedRule: 'Bu korumalı güvenlik duvarı kuralı değiştirilemez.',
dockerAcceptReadOnly:
'Bu ACCEPT kuralı salt okunurdur ve diğer kurallar eşitlenirken korunur. Kaldırmak için ana makinede manuel olarak silin.',
cannotReconcile: 'Hedef kural eşitlenemiyor: {0}',
},
ruleSyncPartial: 'Eşitleme tamamlandı: {0} başarılı, {1} zaten mevcut, {2} başarısız.',
@@ -4240,10 +4242,10 @@ const message = {
cleanupDockerBackendHelper:
'{0} içindeki 1Panel Docker bağlantı noktası koruma çalışma zamanı kurallarını sıfırlayın: ilgili tüm kuralları ve zincirleri silip veritabanı verilerini koruyun',
cleanupBeforeBackendSwitch:
'Mevcut {0} arka ucu hâlâ 1Panel çalışma zamanı kuralları içeriyor. {1} arka ucuna geçmeden önce sıfırlayın.',
'Mevcut {0} arka ucu hâlâ 1Panel çalışma zamanı kuralları içeriyor. {1} arka ucuna geçmeden önce sıfırlayın. Sıfırlama yalnızca çalışma zamanı kurallarını temizler; veritabanı ilkeleri korunur ve geçişten sonra yeniden başlatılabilir veya eşitlenebilir.',
cleanupAction: 'Sıfırla',
backendSwitchNotice:
'Sistem güvenlik duvarı, bağlantı noktası yönlendirme veya Docker korumasını değiştirmeden önce mevcut arka ucu sıfırlayın. Veritabanı ilkeleri korunur ve geçişten sonra yeniden başlatılabilir veya eşitlenebilir.',
'Yalnızca bir güvenlik duvarı yönetim yönteminin etkinleştirilmesi önerilir. Birden fazla güvenlik duvarının aynı anda çalışması kural çakışmalarına, tutarsız durumlara veya konteyner bağlantı noktası erişim sorunlarına yol açabilir.',
switchBackendHelper: '{0} arka ucuna geçilsin mi?',
switchDockerBackendHelper:
'{0} arka ucuna geçilsin mi? Docker yapılandırması güncellenecek ve Docker yeniden başlatılacak.',
+5 -2
View File
@@ -3880,6 +3880,8 @@ const message = {
stale: '防火牆規則狀態已過期請重新整理後再試',
lockoutRisk: '此次防火牆變更可能會阻斷管理存取',
protectedRule: '此防火牆規則受保護無法修改',
dockerAcceptReadOnly:
' ACCEPT 規則為唯讀不可同步同步其他規則時將保留此規則如需移除請在主機上手動刪除',
cannotReconcile: '無法同步目標規則{0}',
},
ruleSyncPartial: '同步完成成功 {0} 已存在 {1} 失敗 {2} ',
@@ -3897,10 +3899,11 @@ const message = {
'重設 {0} 中的 1Panel 連接埠轉送執行時規則刪除全部相關規則及規則鏈僅保留資料庫資料',
cleanupDockerBackendHelper:
'重設 {0} 中的 1Panel Docker 連接埠防護執行時規則刪除全部相關規則及規則鏈僅保留資料庫資料',
cleanupBeforeBackendSwitch: '目前後端 {0} 仍存在 1Panel 執行時規則請先重設該後端再切換到 {1}',
cleanupBeforeBackendSwitch:
'目前後端 {0} 仍存在 1Panel 執行時規則請先重設該後端再切換到 {1}重設僅清理執行時規則資料庫策略會保留切換後可重新初始化或同步',
cleanupAction: '重設',
backendSwitchNotice:
'主機防火牆連接埠轉送和 Docker 防護切換前都必須先重設目前後端資料庫策略會保留切換後可重新初始化或同步',
'建議僅啟用一種防火牆管理方式同時執行多個防火牆可能導致規則衝突狀態不一致或容器連接埠存取異常',
switchBackendHelper: '確認切換為 {0}',
switchDockerBackendHelper: '確認切換為 {0}此操作會更新 Docker 設定並重新啟動 Docker 服務',
uninstalledStatus: '未安裝',
+5 -2
View File
@@ -3929,6 +3929,8 @@ const message = {
stale: '检查后防火墙状态已发生变化请刷新后重试',
lockoutRisk: '执行后可能导致当前 1Panel 管理页面无法访问本次操作已停止',
protectedRule: '这是保证系统正常运行的必要规则不能修改',
dockerAcceptReadOnly:
' ACCEPT 规则为只读不可同步同步其他规则时会保留该规则如需移除请在主机上手动删除',
cannotReconcile: '该规则无法自动同步{0}',
},
ruleSyncPartial: '同步完成成功 {0} 已存在 {1} 失败 {2} ',
@@ -3946,10 +3948,11 @@ const message = {
'重置 {0} 中的 1Panel 端口转发运行时规则删除全部相关规则及规则链仅保留数据库数据',
cleanupDockerBackendHelper:
'重置 {0} 中的 1Panel Docker 端口防护运行时规则删除全部相关规则及规则链仅保留数据库数据',
cleanupBeforeBackendSwitch: '当前后端 {0} 仍存在 1Panel 运行时规则请先重置该后端再切换到 {1}',
cleanupBeforeBackendSwitch:
'当前后端 {0} 仍存在 1Panel 运行时规则请先重置该后端再切换到 {1}重置仅清理运行时规则数据库策略会保留切换后可以重新初始化或同步',
cleanupAction: '重置',
backendSwitchNotice:
'主机防火墙端口转发和 Docker 防护切换前都需要先重置当前后端数据库策略会保留切换后可以重新初始化或同步',
'建议仅启用一种防火墙管理方式多个防火墙同时运行可能导致规则冲突状态不一致或容器端口访问异常',
switchBackendHelper: '确认切换为 {0}',
switchDockerBackendHelper: '确认切换为 {0}该操作会更新 Docker 配置并重启 Docker 服务',
uninstalledStatus: '未安装',
@@ -3,7 +3,7 @@
<FireRouter />
<LayoutContent :title="$t('commons.button.set')">
<template #prompt>
<el-alert type="warning" show-icon :closable="false" :title="$t('firewall.backendSwitchNotice')" />
<el-alert type="info" show-icon :closable="false" :title="$t('firewall.backendSwitchNotice')" />
</template>
<template #main>
<el-form :label-position="isMobile ? 'top' : 'left'" label-width="150px">
@@ -136,7 +136,7 @@
<el-table-column :label="$t('commons.table.protocol')" prop="dockerRule.protocol" width="90" />
<el-table-column :label="$t('firewall.protectionMode')" min-width="145">
<template #default="{ row }">
{{ dockerMode(row.dockerRule?.mode) }}
{{ dockerMode(row.dockerRule) }}
</template>
</el-table-column>
<el-table-column :label="$t('firewall.sourceIP')" min-width="170" show-overflow-tooltip>
@@ -194,12 +194,19 @@ const syncHelper = computed(() => i18n.global.t('firewall.ruleSyncDatabaseHelper
const totalLabel = computed(() => i18n.global.t('firewall.ruleSyncDatabaseTotal'));
const syncDisabled = computed(() => {
if (!preview.value) return true;
if (subsystem.value === 'docker') {
const hasUnsupportedBlocker = (preview.value.items || []).some(
(item) => item.status === 'blocked' && item.reasonCode !== 'read_only_rule',
);
return hasUnsupportedBlocker || (preview.value.ready === 0 && preview.value.removed === 0);
}
return preview.value.blocked > 0;
});
const detailItems = computed(() => {
if (!preview.value || !detailFilter.value) return [];
if (detailFilter.value === 'total') return preview.value.items.filter((item) => item.status !== 'remove');
return preview.value.items.filter((item) => item.status === detailFilter.value);
const items = preview.value.items || [];
if (detailFilter.value === 'total') return items.filter((item) => item.status !== 'remove');
return items.filter((item) => item.status === detailFilter.value);
});
const toggleDetail = (filter: RuleDetailFilter, count: number) => {
@@ -302,6 +309,7 @@ const syncReasonCodeKeys: Record<string, string> = {
only_exists_in_target: 'onlyInTarget',
managed_only_exists_in_target: 'managedOnlyInTarget',
unsafe_managed_rule_removal: 'managedRuntimeCannotRemove',
read_only_rule: 'dockerAcceptReadOnly',
};
const reasonText = (reasonCode?: string, reason?: string) => {
@@ -338,7 +346,9 @@ const dockerAddress = (rule?: Firewall.DockerGuardEndpoint) => {
const dockerSources = (rule?: Firewall.DockerGuardEndpoint) =>
rule ? formatHostAddressList(rule.sources, rule.family) : '';
const dockerMode = (mode?: Firewall.DockerGuardPolicy['mode']) => {
const dockerMode = (rule?: Firewall.DockerGuardEndpoint) => {
if (rule?.readOnly && rule.nativeAction) return rule.nativeAction.toUpperCase();
const mode = rule?.mode;
if (!mode) return '-';
if (mode === 'deny_all') return i18n.global.t('firewall.denyAll');
return i18n.global.t(mode === 'allow_sources' ? 'firewall.allowSources' : 'firewall.denySources');