diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 71b2b9499..39ed1b2ab 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -232,11 +232,15 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp return response, err } if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) || - (request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationRestart)) { + (request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) && + request.Operation != string(lifecycle.OperationRestart)) { return response, s.OperateFirewall(request) } operation, label := task.TaskExec, "Start" - if request.Operation == string(lifecycle.OperationRestart) { + switch lifecycle.Operation(request.Operation) { + case lifecycle.OperationStop: + label = "Stop" + case lifecycle.OperationRestart: operation, label = task.TaskRestart, task.TaskRestart } name := task.GetTaskName(client.Name(), label, task.TaskScopeFirewall) @@ -291,14 +295,17 @@ func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycl return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action) } operationErr := operator.Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, func(lifecycle.Client) error { - runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error { + rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error { return s.restoreStoredFirewallRules(ctx, provider, t) }) - runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { + whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { return s.SyncPortWhitelist(ctx) }) - return nil + return errors.Join(rulesErr, whitelistErr) }) + if request.Operation == string(lifecycle.OperationStop) { + return operationErr + } var recoveryErr *lifecycle.CompletedOperationError if operationErr != nil && !errors.As(operationErr, &recoveryErr) { return operationErr @@ -1539,8 +1546,8 @@ func (s *FirewallService) prepareDelete( } return preparedFirewallRuleDelete{}, err } - if isProtectedSystemFirewallRule(stored) { - return preparedFirewallRuleDelete{}, filter.ErrProtectedRule + if err := checkFirewallRuleWhitelistProtection(ctx, stored); err != nil { + return preparedFirewallRuleDelete{}, err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return preparedFirewallRuleDelete{}, fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule) @@ -1674,8 +1681,8 @@ func (s *FirewallService) updateRuleDescription(ctx context.Context, ruleUUID, d if err != nil { return err } - if isProtectedSystemFirewallRule(stored) { - return filter.ErrProtectedRule + if err := checkFirewallRuleWhitelistProtection(ctx, stored); err != nil { + return err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule) @@ -2145,9 +2152,8 @@ func (s *FirewallService) loadManagedMutation( if err != nil { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } - if isProtectedSystemFirewallRule(stored) { - return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, - filter.ErrProtectedRule + if err := checkFirewallRuleWhitelistProtection(ctx, stored); err != nil { + return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err } if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted { return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, @@ -2515,7 +2521,7 @@ func (s *FirewallService) systemPortRecords(ctx context.Context, port dto.Firewa return records, nil } -func isProtectedSystemFirewallRule(rule model.FirewallRule) bool { +func hasSystemFirewallRuleOwner(rule model.FirewallRule) bool { acceptedPrefix := model.FirewallRuleOwner( constant.FirewallRuleSourceSecurity, constant.FirewallSystemAcceptedPortSourcePrefix, @@ -2752,7 +2758,7 @@ func (s *FirewallService) compileRestorableFirewallRules( if err != nil { return nil, nil, err } - if !supportsManagedFilterChains(string(provider)) || !isProtectedSystemFirewallRule(stored) { + if !supportsManagedFilterChains(string(provider)) || !hasSystemFirewallRuleOwner(stored) { return compiled, nil, nil } loadRequired := s.requiredPorts @@ -2792,8 +2798,12 @@ func (s *FirewallService) desiredFirewallRulesByScope( model.SortFirewallRules(stored, provider) desired := make(map[string][]filter.DesiredRule) var failures []filter.InventoryItem + ports, protectionErr := firewallWhitelistForProtection(ctx) for _, record := range stored { compiled, _, err := s.compileRestorableFirewallRules(ctx, record, provider) + if err == nil { + err = protectionErr + } if err != nil { rule := filter.FirewallRule{ UUID: record.UUID, @@ -2806,13 +2816,13 @@ func (s *FirewallService) desiredFirewallRulesByScope( failures = append(failures, filter.InventoryItem{ Incompatible: isFirewallPolicyIncompatible(err), Rule: rule, State: filter.InventoryStateDrifted, Match: filter.InventoryMatchNone, - Desired: &filter.DesiredRule{UUID: record.UUID, Rule: rule, Origin: filter.RuleOrigin(record.Origin), Protected: isProtectedSystemFirewallRule(record)}, + Desired: &filter.DesiredRule{UUID: record.UUID, Rule: rule, Origin: filter.RuleOrigin(record.Origin), Protected: protectionErr != nil || filter.RuleMatchesPortWhitelist(rule, ports)}, Error: fmt.Sprintf("policy %s: %v", record.UUID, err), }) continue } for _, rule := range compiled { - rule.Protected = isProtectedSystemFirewallRule(record) + rule.Protected = filter.RuleMatchesPortWhitelist(rule.Rule, ports) rule.Expanded = len(compiled) > 1 key := rule.Rule.Scope.Key() desired[key] = append(desired[key], rule) diff --git a/agent/app/service/firewall_setting.go b/agent/app/service/firewall_setting.go index cac280892..251b3f45f 100644 --- a/agent/app/service/firewall_setting.go +++ b/agent/app/service/firewall_setting.go @@ -22,6 +22,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall" "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" @@ -233,7 +234,10 @@ func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, plan po return err } return s.executePortWhitelist(ctx, plan, func() error { - return syncWhitelistRules(ctx, firewallService, previous, current, report) + if err := syncWhitelistRules(ctx, firewallService, previous, current, report); err != nil { + return err + } + return firewallService.reconcilePortWhitelist(ctx, provider, ready, report) }, report) } @@ -408,6 +412,70 @@ func (s *FirewallService) deleteWhitelistRule(ctx context.Context, prepared prep defer firewallRuleMutationMu.Unlock() rule, runtime := prepared.request.Rule, prepared.runtime provider := rule.Scope.Provider + ports, err := firewallWhitelistForProtection(ctx) + if err != nil { + return err + } + if rule.Scope.Chain != filter.BasicBeforeChain && filter.RuleMatchesPortWhitelist(rule, ports) { + return nil + } + stored, err := s.rules.List(ctx) + if err != nil { + return err + } + cleanup, _ := ctx.Value(firewallWhitelistCleanupKey{}).(bool) + if !cleanup || rule.Scope.Chain == filter.BasicBeforeChain { + if err := deleteNativeWhitelistRule(ctx, runtime, rule); err != nil { + return err + } + } + for _, record := range stored { + if cleanup && !hasSystemFirewallRuleOwner(record) { + continue + } + compiled, err := s.compileStoredFirewallRules(ctx, record, provider) + if err != nil { + continue + } + remaining := make([]filter.FirewallRule, 0, len(compiled)) + for _, candidate := range compiled { + if !matchesWhitelistRemoval(rule, candidate.Rule, record) || filter.RuleMatchesPortWhitelist(candidate.Rule, ports) { + remaining = append(remaining, candidate.Rule) + continue + } + if candidate.Rule.Scope.Key() != rule.Scope.Key() || cleanup && rule.Scope.Chain != filter.BasicBeforeChain { + if err := deleteNativeWhitelistRule(ctx, runtime, candidate.Rule, candidate.Marker); err != nil { + return err + } + } + } + if len(remaining) == len(compiled) { + continue + } + if err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + ctx := context.WithValue(ctx, constant.DB, tx) + if err := s.rules.DeleteWithRevision(ctx, record.UUID, record.Revision); err != nil { + return err + } + for _, rule := range remaining { + kept, err := model.FirewallRuleFromDomain(rule) + if err != nil { + return err + } + kept.UUID, kept.Origin, kept.Owner, kept.Sequence = rule.UUID, record.Origin, record.Owner, record.Sequence + if err := s.rules.Create(ctx, &kept); err != nil { + return err + } + } + return nil + }); err != nil { + return err + } + } + return nil +} + +func deleteNativeWhitelistRule(ctx context.Context, runtime *filterruntime.Engine, rule filter.FirewallRule, markers ...string) error { for { if err := ctx.Err(); err != nil { return err @@ -418,6 +486,9 @@ func (s *FirewallService) deleteWhitelistRule(ctx context.Context, prepared prep } matched := -1 for index, observed := range snapshot.Rules { + if len(markers) > 0 && observed.Marker != markers[0] { + continue + } if observed.ParseStatus == filter.ParseStatusSupported { if same, err := filter.SameRuleContent(rule, observed.Rule); err == nil && same { matched = index @@ -445,45 +516,105 @@ func (s *FirewallService) deleteWhitelistRule(ctx context.Context, prepared prep return filter.ErrVerificationFailed } } + return nil +} + +type firewallWhitelistCleanupKey struct{} + +func checkFirewallRuleWhitelistProtection(ctx context.Context, record model.FirewallRule) error { + ports, err := firewallWhitelistForProtection(ctx) + if err != nil { + return err + } + rules, err := record.RulesForProvider(filter.ProviderIptables) + if err != nil { + return err + } + for _, rule := range rules { + if filter.RuleMatchesPortWhitelist(rule, ports) { + return filter.ErrProtectedRule + } + } + return nil +} + +func matchesWhitelistRemoval(requested, candidate filter.FirewallRule, record model.FirewallRule) bool { + if isDirectFirewallProvider(requested.Scope.Provider) && requested.Scope.Chain == filter.BasicBeforeChain && hasSystemFirewallRuleOwner(record) { + candidate.Scope.Chain = filter.BasicBeforeChain + } + return whitelistContainsRule([]filter.FirewallRule{requested}, candidate) +} + +func isWhitelistPortAllowance(rule filter.FirewallRule) bool { + normalized, err := filter.NormalizeRule(rule) + return err == nil && normalized.Action == filter.ActionAccept && + (normalized.Protocol == "tcp" || normalized.Protocol == "udp") && normalized.DestinationPort != "" && + normalized.SourcePort == "" && normalized.DestinationAddress == "" && normalized.Interface == "" && len(normalized.ConnectionStates) == 0 +} + +func (s *FirewallService) reconcilePortWhitelist(ctx context.Context, provider filter.Provider, + ready func(dto.FirewallSystemPort) (bool, error), report whitelistReporter, +) error { + if !isDirectFirewallProvider(provider) { + return nil + } + ports, err := firewallWhitelistForProtection(ctx) + if err != nil { + return err + } + runtime, err := s.adapters.Resolve(provider) + if err != nil { + return err + } + var obsolete []filter.FirewallRule + for _, scope := range filter.ManagedInputScopes(provider) { + if scope.Chain != filter.BasicBeforeChain { + continue + } + active, err := ready(dto.FirewallSystemPort{Family: string(scope.Family)}) + if err != nil { + return err + } + if !active { + continue + } + snapshot, err := runtime.ObserveMutation(ctx, scope) + if errors.Is(err, filter.ErrFamilyUnavailable) { + continue + } + if err != nil { + return err + } + for _, observed := range snapshot.Rules { + if observed.ParseStatus == filter.ParseStatusSupported && isWhitelistPortAllowance(observed.Rule) && + !filter.RuleMatchesPortWhitelist(observed.Rule, ports) { + obsolete = append(obsolete, observed.Rule) + } + } + } stored, err := s.rules.List(ctx) if err != nil { return err } for _, record := range stored { + if !hasSystemFirewallRuleOwner(record) { + continue + } compiled, err := s.compileStoredFirewallRules(ctx, record, provider) if err != nil { - continue - } - remaining := make([]filter.FirewallRule, 0, len(compiled)) - for _, candidate := range compiled { - if !whitelistContainsRule([]filter.FirewallRule{rule}, candidate.Rule) { - remaining = append(remaining, candidate.Rule) - } - } - if len(remaining) == len(compiled) { - continue - } - if err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - ctx := context.WithValue(ctx, constant.DB, tx) - if err := s.rules.DeleteWithRevision(ctx, record.UUID, record.Revision); err != nil { - return err - } - for _, rule := range remaining { - kept, err := model.FirewallRuleFromDomain(rule) - if err != nil { - return err - } - kept.UUID, kept.Origin, kept.Owner, kept.Sequence = rule.UUID, record.Origin, record.Owner, record.Sequence - if err := s.rules.Create(ctx, &kept); err != nil { - return err - } - } - return nil - }); err != nil { return err } + for _, candidate := range compiled { + if isWhitelistPortAllowance(candidate.Rule) && !filter.RuleMatchesPortWhitelist(candidate.Rule, ports) { + obsolete = append(obsolete, candidate.Rule) + } + } } - return nil + prepared, err := s.prepareWhitelistRules(ctx, provider, obsolete, ready, report) + if err != nil { + return err + } + return syncWhitelistRules(context.WithValue(ctx, firewallWhitelistCleanupKey{}, true), s, prepared, nil, report) } func (s *FirewallSettingService) preparePortWhitelist(ctx context.Context, change portWhitelistChange) (portWhitelistPlan, error) { diff --git a/agent/app/service/firewall_sync.go b/agent/app/service/firewall_sync.go index c099fded0..d8dc21899 100644 --- a/agent/app/service/firewall_sync.go +++ b/agent/app/service/firewall_sync.go @@ -21,6 +21,7 @@ import ( filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -36,9 +37,10 @@ type firewallDatabaseSyncAdapter interface { type firewallSyncRule struct { dto.FirewallRuleSyncItem - desired filter.DesiredRule - observed *filter.ObservedRule - done bool + desired filter.DesiredRule + observed *filter.ObservedRule + done bool + whitelistCleanup bool } func firewallSyncSubsystem(value string) string { @@ -81,7 +83,26 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto return nil, nil, nil, err } model.SortFirewallRules(stored, request.TargetProvider) + var whitelist []firewall.PortWhitelist + var whitelistDesired []filter.FirewallRule + if isDirectFirewallProvider(request.TargetProvider) { + ports, overridden := ctx.Value(firewallWhitelistOverrideKey{}).([]firewall.PortWhitelist) + if !overridden { + ports, err = loadFirewallPortWhiteList() + if err != nil { + return nil, nil, nil, err + } + } + required, err := firewall.RequiredPortWhitelist(ports) + if err != nil { + return nil, nil, nil, err + } + custom := customWhitelist(ports) + whitelist = append(custom, required...) + whitelistDesired = whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(custom), firewall.ExpandPortWhitelist(required)) + } rules := make([]*firewallSyncRule, 0, len(stored)) + var whitelistCleanup []*firewallSyncRule preservedMarkers := make(map[string]bool) compileFailed := false for _, record := range stored { @@ -95,10 +116,45 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto preservedMarkers[rule.Marker] = true } for _, native := range desired { + if isDirectFirewallProvider(request.TargetProvider) && hasSystemFirewallRuleOwner(record) && + isWhitelistPortAllowance(native.Rule) && !filter.RuleMatchesPortWhitelist(native.Rule, whitelist) { + stale := native.Rule + whitelistCleanup = append(whitelistCleanup, &firewallSyncRule{whitelistCleanup: true, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{ + SourceUUID: record.UUID, Rule: &stale, Status: firewallsync.StatusRemove, Reason: "port allowance is no longer in the whitelist", + }}) + preservedMarkers[native.Marker] = true + continue + } rule := native.Rule rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}}) } } + for _, candidate := range whitelistDesired { + prepared, err := s.prepareCreate(ctx, request.TargetProvider, dto.FirewallRuleCreateItem{Rule: candidate}) + if err != nil { + return nil, nil, nil, err + } + rule := prepared.request.Rule + duplicate := false + for _, existing := range rules { + if existing.Rule != nil && whitelistContainsRule([]filter.FirewallRule{*existing.Rule}, rule) { + duplicate = true + break + } + } + if duplicate { + continue + } + key, err := filter.RuleKey(rule) + if err != nil { + return nil, nil, nil, err + } + rule.UUID = uuid.NewSHA1(uuid.NameSpaceOID, []byte(key)).String() + rules = append(rules, &firewallSyncRule{ + desired: filter.DesiredRule{UUID: rule.UUID, Rule: rule, RuleKey: key, Origin: filter.RuleOriginCreated, Protected: true}, + FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule}, + }) + } snapshots := make([]filter.Snapshot, 0) for _, scope := range filter.ManagedInputScopes(request.TargetProvider) { desired := make([]filter.DesiredRule, 0) @@ -125,6 +181,16 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto return nil, nil, nil, err } snapshots = append(snapshots, snapshot) + if isDirectFirewallProvider(request.TargetProvider) && scope.Chain == filter.BasicBeforeChain { + for _, observed := range snapshot.Rules { + if observed.ParseStatus == filter.ParseStatusSupported && isWhitelistPortAllowance(observed.Rule) && !filter.RuleMatchesPortWhitelist(observed.Rule, whitelist) { + stale := observed.Rule + whitelistCleanup = append(whitelistCleanup, &firewallSyncRule{whitelistCleanup: true, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{ + SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &stale, Status: firewallsync.StatusRemove, Reason: "port allowance is no longer in the whitelist", + }}) + } + } + } inventory, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired}) if err != nil { return nil, nil, nil, err @@ -180,10 +246,25 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto } } } - return runtime, rules, snapshots, nil + return runtime, append(rules, whitelistCleanup...), snapshots, nil } func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) { + if isDirectFirewallProvider(request.TargetProvider) { + if request.SourceProvider != "" || request.ResetSource { + return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule) + } + if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil { + return result, err + } + whitelistErr := s.SyncPortWhitelist(ctx) + if t != nil { + t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) + } + if whitelistErr != nil { + return result, whitelistErr + } + } firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider} @@ -194,6 +275,11 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F if err != nil { return result, err } + for _, rule := range rules { + if rule.whitelistCleanup { + return result, fmt.Errorf("%w: whitelist cleanup is incomplete; check that the firewall family is initialized and active", filter.ErrRuleOperation) + } + } created, removed, unexecuted := 0, 0, 0 stopped := make(map[string]error) failedRemovals := make(map[string]error) @@ -618,6 +704,8 @@ func runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID s } func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { + firewallWhitelistTaskMu.Lock() + defer firewallWhitelistTaskMu.Unlock() filterruntime.InvalidateInventory() defer filterruntime.InvalidateInventory() ports, err := loadFirewallPortWhiteList() @@ -645,8 +733,13 @@ func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { } rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) prepared, prepareErr := s.prepareWhitelistRules(ctx, provider, rules, ready, report) - syncErr := syncWhitelistRules(ctx, s, nil, prepared, report) - return errors.Join(prepareErr, syncErr) + if prepareErr != nil { + return prepareErr + } + if err := syncWhitelistRules(ctx, s, nil, prepared, report); err != nil { + return err + } + return s.reconcilePortWhitelist(ctx, provider, ready, report) } func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error { diff --git a/agent/init/firewall/firewall.go b/agent/init/firewall/firewall.go index a411fbead..010e9f1ce 100644 --- a/agent/init/firewall/firewall.go +++ b/agent/init/firewall/firewall.go @@ -11,6 +11,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/init/migration/migrations" migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils" "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" @@ -28,7 +29,7 @@ func Init() { clientName := client.Name() initialize := false defer func() { - if err := service.NewIFirewallService().SyncPortWhitelist(ctx); err != nil { + if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil { global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err) } if initialize { diff --git a/agent/init/migration/migrations/firewall_whitelist.go b/agent/init/migration/migrations/firewall_whitelist.go index 6d169cccf..fa7131a73 100644 --- a/agent/init/migration/migrations/firewall_whitelist.go +++ b/agent/init/migration/migrations/firewall_whitelist.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "encoding/json" "errors" "fmt" @@ -10,11 +11,52 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/firewall" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" + "gorm.io/gorm/clause" ) +const firewalldSSHServiceMigrationID = "20260916-remove-firewalld-ssh-service" + +func TransferFirewalldSSHService(ctx context.Context, client lifecycle.Client, syncWhitelist func(context.Context) error) error { + return transferFirewalldSSHService(ctx, global.DB, client, syncWhitelist) +} + +func transferFirewalldSSHService(ctx context.Context, db *gorm.DB, client lifecycle.Client, syncWhitelist func(context.Context) error) error { + if err := syncWhitelist(ctx); err != nil { + return err + } + if client.Name() != lifecycle.ProviderFirewalld { + return nil + } + var count int64 + if err := db.WithContext(ctx).Table("migrations").Where("id = ?", firewalldSSHServiceMigrationID).Count(&count).Error; err != nil { + return fmt.Errorf("check firewalld SSH service migration: %w", err) + } + if count > 0 { + return nil + } + active, err := client.Status() + if err != nil || !active { + return err + } + if err := ctx.Err(); err != nil { + return err + } + if err := providers.RemoveFirewalldSSHService(); err != nil { + return fmt.Errorf("transfer firewalld SSH access to whitelist: %w", err) + } + if err := db.WithContext(ctx).Table("migrations").Clauses(clause.OnConflict{DoNothing: true}). + Create(map[string]interface{}{"id": firewalldSSHServiceMigrationID}).Error; err != nil { + return fmt.Errorf("record firewalld SSH service migration: %w", err) + } + return nil +} + var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{ ID: "20260915-migrate-firewall-port-whitelist-sources", Migrate: func(tx *gorm.DB) error { diff --git a/agent/utils/controller/manager/common.go b/agent/utils/controller/manager/common.go index fe5598f2d..e82c39095 100644 --- a/agent/utils/controller/manager/common.go +++ b/agent/utils/controller/manager/common.go @@ -2,6 +2,7 @@ package manager import ( "errors" + "strings" "time" "github.com/1Panel-dev/1Panel/agent/utils/cmd" @@ -18,5 +19,19 @@ func handlerErr(out string, err error) error { } func run(name string, args ...string) (string, error) { - return cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...) + return runWithTimeout(10*time.Second, name, args...) +} + +func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) { + return cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...) +} + +func serviceOperationTimeout(operation, serviceName string) time.Duration { + if operation == "restart" { + switch strings.TrimSuffix(serviceName, ".service") { + case "docker", "dockerd", "docker.dockerd", "snap.docker.dockerd": + return 2 * time.Minute + } + } + return 10 * time.Second } diff --git a/agent/utils/controller/manager/openrc.go b/agent/utils/controller/manager/openrc.go index 92dd73d51..f9f7c0f4d 100644 --- a/agent/utils/controller/manager/openrc.go +++ b/agent/utils/controller/manager/openrc.go @@ -46,7 +46,7 @@ func (s *Openrc) Operate(operate, serviceName string) error { case "disable": return handlerErr(run("rc-update", "del", serviceName, "default")) default: - return handlerErr(run(s.toolCmd, serviceName, operate)) + return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate)) } } diff --git a/agent/utils/controller/manager/snap.go b/agent/utils/controller/manager/snap.go index 51f22af91..dacba37d0 100644 --- a/agent/utils/controller/manager/snap.go +++ b/agent/utils/controller/manager/snap.go @@ -49,7 +49,7 @@ func (s *Snap) IsEnable(serviceName string) bool { func (s *Snap) Operate(operate, serviceName string) error { if s.IsExist(serviceName) { - return handlerErr(run(s.toolCmd, operate, serviceName)) + return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName)) } return fmt.Errorf("snap service %q does not exist", serviceName) } diff --git a/agent/utils/controller/manager/systemd.go b/agent/utils/controller/manager/systemd.go index 69da10feb..12f17766a 100644 --- a/agent/utils/controller/manager/systemd.go +++ b/agent/utils/controller/manager/systemd.go @@ -57,7 +57,7 @@ func (s *Systemd) Status(serviceName string) (string, error) { return run(s.toolCmd, "status", serviceName) } func (s *Systemd) Operate(operate, serviceName string) error { - out, err := run(s.toolCmd, operate, serviceName) + out, err := runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName) if err != nil { if fallbackName := systemdAliasFallbackName(serviceName); fallbackName != "" && strings.Contains(out, "alias name or linked unit file") { return s.Operate(operate, fallbackName) diff --git a/agent/utils/controller/manager/sysvinit.go b/agent/utils/controller/manager/sysvinit.go index aeaa309f0..1dcf62b74 100644 --- a/agent/utils/controller/manager/sysvinit.go +++ b/agent/utils/controller/manager/sysvinit.go @@ -39,7 +39,7 @@ func (s *Sysvinit) Status(serviceName string) (string, error) { } func (s *Sysvinit) Operate(operate, serviceName string) error { - return handlerErr(run(s.toolCmd, serviceName, operate)) + return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate)) } func (s *Sysvinit) Reload() error { diff --git a/agent/utils/firewall/lifecycle/operator.go b/agent/utils/firewall/lifecycle/operator.go index 185ec012c..d1cc45540 100644 --- a/agent/utils/firewall/lifecycle/operator.go +++ b/agent/utils/firewall/lifecycle/operator.go @@ -7,6 +7,7 @@ import ( "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" @@ -70,7 +71,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS return err } if prepareStart != nil { - if err := prepareStart(o.client); err != nil { + if err := o.prepareAfterStart(prepareStart); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after start: %w", err)) } } @@ -81,7 +82,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS return err } if prepareStart != nil { - if err := prepareStart(o.client); err != nil { + if err := o.prepareAfterStart(prepareStart); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after restart: %w", err)) } } @@ -105,6 +106,16 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS 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 { @@ -118,11 +129,11 @@ func (o *Operator) StopWithPrepare(withDockerRestart bool, prepareStop func() er return err } } - if err := o.client.Stop(); err != nil { + if err := o.runAction("Stop", o.client.Name(), o.client.Stop); err != nil { return err } if withDockerRestart { - if err := controller.HandleRestart("docker"); err != nil { + if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil { return &DockerRestartError{Err: err} } } diff --git a/agent/utils/firewall/lifecycle/providers/firewalld.go b/agent/utils/firewall/lifecycle/providers/firewalld.go index ce6722986..baa9bbf54 100644 --- a/agent/utils/firewall/lifecycle/providers/firewalld.go +++ b/agent/utils/firewall/lifecycle/providers/firewalld.go @@ -11,6 +11,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/controller" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" ) type Firewalld struct{} @@ -68,6 +69,21 @@ func (f *Firewalld) Start() error { return nil } +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) + } + } + return nil +} + func (f *Firewalld) Stop() error { if err := controller.HandleStop("firewalld"); err != nil { return fmt.Errorf("stop the firewall failed, err: %v", err)