From b113824512c65abff6f4d2dd5fd13adb2ccad893 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Tue, 22 Sep 2026 12:46:40 +0800 Subject: [PATCH] fix: correct firewall port allowances and whitelist sync reporting --- agent/app/service/firewall.go | 41 ++- agent/app/service/firewall_sync.go | 236 +------------- agent/app/service/firewall_utils.go | 304 +++++++++++++++++- .../src/views/host/firewall/rule/index.vue | 23 +- .../firewall/setting/white-list/index.vue | 8 +- 5 files changed, 350 insertions(+), 262 deletions(-) diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 647829167092..8e75f0f240c3 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "errors" "fmt" "slices" @@ -98,7 +99,45 @@ func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uin if oldPort == port { return nil } - return updateSystemAccessPortWhitelist(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))}) + firewallWhitelistMu.Lock() + defer firewallWhitelistMu.Unlock() + entries, err := loadFirewallPortWhiteList() + if err != nil { + return err + } + panelPorts := make([]firewall.PortWhitelist, 0) + for index := range entries { + if entries[index].Type != firewall.PortWhitelistTypePanel { + continue + } + entries[index].Port = strconv.Itoa(int(port)) + panelPorts = append(panelPorts, entries[index]) + } + if len(panelPorts) == 0 { + panelPorts = append(panelPorts, firewall.PortWhitelist{ + Type: firewall.PortWhitelistTypePanel, Port: strconv.Itoa(int(port)), Protocol: "tcp", + Sources: []string{"0.0.0.0/0", "::/0"}, + }) + entries = append(entries, panelPorts...) + } + entries, err = firewall.ValidatePortWhitelist(entries) + if err != nil { + return err + } + value, err := json.Marshal(entries) + if err != nil { + return err + } + client, err := s.baseClient() + if err != nil && !errors.Is(err, lifecycle.ErrNotInstalled) { + return err + } + if err == nil { + if _, err := s.syncPortWhitelist(ctx, filter.Provider(client.Name()), panelPorts); err != nil { + return err + } + } + return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value)) } func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { diff --git a/agent/app/service/firewall_sync.go b/agent/app/service/firewall_sync.go index 837ec795f33e..bfeeffa9f001 100644 --- a/agent/app/service/firewall_sync.go +++ b/agent/app/service/firewall_sync.go @@ -7,21 +7,17 @@ import ( "fmt" "strings" "sync" - "time" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" - "github.com/1Panel-dev/1Panel/agent/utils/firewall" dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" - "github.com/google/uuid" ) var ( @@ -42,240 +38,12 @@ func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { if err != nil { return err } - required, err := firewall.RequiredPortWhitelist(ports) - if err != nil { - return err - } provider, err := s.selectedProvider(ctx) if err != nil { return err } - rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) - if provider != filter.ProviderIptables && provider != filter.ProviderNftables && len(rules) > 0 { - client, err := s.baseClient() - if err != nil { - return err - } - active, err := client.Status() - if err != nil || !active { - return err - } - } - client, err := s.firewallAdapter(provider) - if err != nil { - return err - } - prepared := make([]dto.FirewallRuleCreateItem, 0, len(rules)) - var failures []error - activeFamilies := make(map[filter.Family]bool) - for _, rule := range rules { - if err := ctx.Err(); err != nil { - return err - } - if provider == filter.ProviderIptables || provider == filter.ProviderNftables { - active, inspected := activeFamilies[rule.Scope.Family] - if !inspected { - initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(rule.Scope.Family)) - if err != nil { - return err - } - active = initialized && bound - activeFamilies[rule.Scope.Family] = active - } - if !active { - continue - } - } - port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress} - item, err := prepareFirewallCreateRule(ctx, client, dto.FirewallRuleCreateItem{ - Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + firewall.SystemPortKey(firewall.SystemPort(port)), - }) - if err != nil { - failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err)) - continue - } - prepared = append(prepared, item) - } - if len(failures) > 0 { - return errors.Join(failures...) - } - if len(prepared) == 0 { - return nil - } - firewallRuleMutationMu.Lock() - defer firewallRuleMutationMu.Unlock() - - scopes := make([]filter.Scope, 0, len(prepared)) - for _, item := range prepared { - scopes = append(scopes, item.Rule.Scope) - } - snapshots, err := readMutableFirewallRuleScopes(client, ctx, scopes) - if err != nil { - return err - } - byScope := make(map[string]filter.RuleSet, len(snapshots)) - byScopeIdentity := make(map[string]firewallRuleCollisionIndex, len(snapshots)) - for _, snapshot := range snapshots { - identities, err := observedFirewallRuleCollisionIndex(snapshot) - if err != nil { - return err - } - byScope[snapshot.Scope.Key()] = snapshot - byScopeIdentity[snapshot.Scope.Key()] = identities - } - var byMatch map[string][]filter.DesiredRule - type whitelistBatch struct { - snapshot filter.RuleSet - changes []filter.RuleChange - records []*model.FirewallRule - } - batches := make([]whitelistBatch, 0) - batchByScope := make(map[string]int) - for _, item := range prepared { - if err := ctx.Err(); err != nil { - return err - } - rule := item.Rule - scope := rule.Scope - identities := byScopeIdentity[scope.Key()] - if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) { - continue - } else if err != nil { - return err - } - if err := identities.Check(rule); err != nil { - return err - } - key, err := filter.RuleMatchKey(rule) - if err != nil { - return err - } - if byMatch == nil { - stored, err := s.rules.List(ctx) - if err != nil { - return err - } - byMatch = make(map[string][]filter.DesiredRule) - for _, record := range stored { - rules, err := compileStoredFirewallRules(ctx, record, client) - if isFirewallPolicyIncompatible(err) { - continue - } - if err != nil { - return err - } - for _, candidate := range rules { - key, err := filter.RuleMatchKey(candidate.Rule) - if err != nil { - return err - } - byMatch[key] = append(byMatch[key], candidate) - } - } - } - var existing *filter.DesiredRule - for _, candidate := range byMatch[key] { - if collision := checkCollisionActions(rule.Action, candidate.Rule.Action); errors.Is(collision, filter.ErrRuleOperation) { - existing = &candidate - } else if collision != nil { - return collision - } - } - var record *model.FirewallRule - if existing != nil && scope.Chain != filter.BasicBeforeChain { - rule = existing.Rule - } else { - rule.UUID = uuid.NewString() - if scope.Chain != filter.BasicBeforeChain { - created, err := firewallRuleModelForCreate(rule, item, constant.FirewallRuleOriginCreated) - if err != nil { - return err - } - created.UUID = rule.UUID - record = &created - } - } - if provider != filter.ProviderFirewalld { - position := int64(1) - rule.OrderIndex = &position - } - index, exists := batchByScope[scope.Key()] - if !exists || provider != filter.ProviderIptables && provider != filter.ProviderNftables { - index = len(batches) - batchByScope[scope.Key()] = index - batches = append(batches, whitelistBatch{snapshot: byScope[scope.Key()]}) - } - batches[index].changes = append(batches[index].changes, filter.RuleChange{Operation: filter.ChangeCreate, After: &rule, CommandOnly: true}) - batches[index].records = append(batches[index].records, record) - if err := identities.Add(rule); err != nil { - return err - } - } - saveRecords := func(batch whitelistBatch) { - saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - for _, record := range batch.records { - if record == nil { - continue - } - if err := s.saveFirewallRule(saveCtx, record); err != nil { - failures = append(failures, err) - if global.LOG != nil { - global.LOG.Errorf("save firewall whitelist rule %s failed: %v", record.UUID, err) - } - } - } - cancel() - } - _, savesRules := client.(filter.RuleSaver) - plans := make([]filter.CommandBatch, 0, len(batches)) - completed := make([]int, 0, len(batches)) - for batchIndex, batch := range batches { - if err := ctx.Err(); err != nil { - failures = append(failures, err) - break - } - commands, err := client.BuildCommands(batch.snapshot, batch.changes) - commands.CommandOnly = true - if err == nil { - err = client.RunCommands(ctx, commands) - } - if err != nil { - failures = append(failures, err) - if global.LOG != nil { - global.LOG.Errorf("create firewall whitelist rules for %s failed: %v", batch.snapshot.Scope.Key(), err) - } - continue - } - verification := filter.CommandBatch{Provider: commands.Provider, Scope: commands.Scope} - for _, command := range commands.Rules { - expected := command.Expected - expected.Locator.Position = nil - verification.Rules = append(verification.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected}) - } - plans = append(plans, verification) - completed = append(completed, batchIndex) - if !savesRules { - saveRecords(batch) - } - } - persistenceErrors := persistFirewallRuleBatches(ctx, client, plans) - for index, batchIndex := range completed { - if err := persistenceErrors[index]; err != nil { - failures = append(failures, err) - continue - } - if savesRules { - saveRecords(batches[batchIndex]) - } - } - - verification, err := verifyFirewallCommands(ctx, client, plans...) - if err != nil { - failures = append(failures, err) - } else if !verification.Matched { - failures = append(failures, filter.ErrVerificationFailed) - } - return errors.Join(failures...) + _, err = s.syncPortWhitelist(ctx, provider, ports) + return err } func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) { diff --git a/agent/app/service/firewall_utils.go b/agent/app/service/firewall_utils.go index ed58ff0fccf9..f27cbe83ee0d 100644 --- a/agent/app/service/firewall_utils.go +++ b/agent/app/service/firewall_utils.go @@ -128,6 +128,249 @@ func LoadPanelPort() string { return portSetting.Value } +func (s *FirewallService) syncPortWhitelist(ctx context.Context, provider filter.Provider, ports []firewall.PortWhitelist) ([]filter.FirewallRule, error) { + required, err := firewall.RequiredPortWhitelist(ports) + if err != nil { + return nil, err + } + rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required)) + if provider != filter.ProviderIptables && provider != filter.ProviderNftables && len(rules) > 0 { + client, err := s.baseClient() + if err != nil { + return nil, err + } + active, err := client.Status() + if err != nil || !active { + return nil, err + } + } + client, err := s.firewallAdapter(provider) + if err != nil { + return nil, err + } + prepared := make([]dto.FirewallRuleCreateItem, 0, len(rules)) + var failures []error + activeFamilies := make(map[filter.Family]bool) + for _, rule := range rules { + if err := ctx.Err(); err != nil { + return nil, err + } + if provider == filter.ProviderIptables || provider == filter.ProviderNftables { + active, inspected := activeFamilies[rule.Scope.Family] + if !inspected { + initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(rule.Scope.Family)) + if err != nil { + return nil, err + } + active = initialized && bound + activeFamilies[rule.Scope.Family] = active + } + if !active { + continue + } + } + port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress} + item, err := prepareFirewallCreateRule(ctx, client, dto.FirewallRuleCreateItem{ + Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + firewall.SystemPortKey(firewall.SystemPort(port)), + }) + if err != nil { + failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err)) + continue + } + prepared = append(prepared, item) + } + if len(failures) > 0 { + return nil, errors.Join(failures...) + } + if len(prepared) == 0 { + return nil, nil + } + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + + scopes := make([]filter.Scope, 0, len(prepared)) + for _, item := range prepared { + scopes = append(scopes, item.Rule.Scope) + } + snapshots, err := readMutableFirewallRuleScopes(client, ctx, scopes) + if err != nil { + return nil, err + } + byScope := make(map[string]filter.RuleSet, len(snapshots)) + byScopeIdentity := make(map[string]firewallRuleCollisionIndex, len(snapshots)) + for _, snapshot := range snapshots { + identities, err := observedFirewallRuleCollisionIndex(snapshot) + if err != nil { + return nil, err + } + byScope[snapshot.Scope.Key()] = snapshot + byScopeIdentity[snapshot.Scope.Key()] = identities + } + var byMatch map[string][]filter.DesiredRule + type whitelistBatch struct { + snapshot filter.RuleSet + changes []filter.RuleChange + records []*model.FirewallRule + } + batches := make([]whitelistBatch, 0) + batchByScope := make(map[string]int) + for _, item := range prepared { + if err := ctx.Err(); err != nil { + return nil, err + } + rule := item.Rule + scope := rule.Scope + identities := byScopeIdentity[scope.Key()] + if err := identities.CheckDuplicate(rule); errors.Is(err, filter.ErrRuleOperation) { + continue + } else if err != nil { + return nil, err + } + if err := identities.Check(rule); err != nil { + return nil, err + } + key, err := filter.RuleMatchKey(rule) + if err != nil { + return nil, err + } + if byMatch == nil { + stored, err := s.rules.List(ctx) + if err != nil { + return nil, err + } + byMatch = make(map[string][]filter.DesiredRule) + for _, record := range stored { + rules, err := compileStoredFirewallRules(ctx, record, client) + if isFirewallPolicyIncompatible(err) { + continue + } + if err != nil { + return nil, err + } + for _, candidate := range rules { + key, err := filter.RuleMatchKey(candidate.Rule) + if err != nil { + return nil, err + } + byMatch[key] = append(byMatch[key], candidate) + } + } + } + var existing *filter.DesiredRule + for _, candidate := range byMatch[key] { + if collision := checkCollisionActions(rule.Action, candidate.Rule.Action); errors.Is(collision, filter.ErrRuleOperation) { + existing = &candidate + } else if collision != nil { + return nil, collision + } + } + var record *model.FirewallRule + if existing != nil && scope.Chain != filter.BasicBeforeChain { + rule = existing.Rule + } else { + rule.UUID = uuid.NewString() + if scope.Chain != filter.BasicBeforeChain { + created, err := firewallRuleModelForCreate(rule, item, constant.FirewallRuleOriginCreated) + if err != nil { + return nil, err + } + created.UUID = rule.UUID + record = &created + } + } + if provider != filter.ProviderFirewalld { + position := int64(1) + rule.OrderIndex = &position + } + index, exists := batchByScope[scope.Key()] + if !exists || provider != filter.ProviderIptables && provider != filter.ProviderNftables { + index = len(batches) + batchByScope[scope.Key()] = index + batches = append(batches, whitelistBatch{snapshot: byScope[scope.Key()]}) + } + batches[index].changes = append(batches[index].changes, filter.RuleChange{Operation: filter.ChangeCreate, After: &rule, CommandOnly: true}) + batches[index].records = append(batches[index].records, record) + if err := identities.Add(rule); err != nil { + return nil, err + } + } + saveRecords := func(batch whitelistBatch) { + saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + for _, record := range batch.records { + if record == nil { + continue + } + if err := s.saveFirewallRule(saveCtx, record); err != nil { + failures = append(failures, err) + if global.LOG != nil { + global.LOG.Errorf("save firewall whitelist rule %s failed: %v", record.UUID, err) + } + } + } + cancel() + } + _, savesRules := client.(filter.RuleSaver) + plans := make([]filter.CommandBatch, 0, len(batches)) + completed := make([]int, 0, len(batches)) + for batchIndex, batch := range batches { + if err := ctx.Err(); err != nil { + failures = append(failures, err) + break + } + commands, err := client.BuildCommands(batch.snapshot, batch.changes) + commands.CommandOnly = true + if err == nil { + err = client.RunCommands(ctx, commands) + } + if err != nil { + failures = append(failures, err) + if global.LOG != nil { + global.LOG.Errorf("create firewall whitelist rules for %s failed: %v", batch.snapshot.Scope.Key(), err) + } + continue + } + verification := filter.CommandBatch{Provider: commands.Provider, Scope: commands.Scope} + for _, command := range commands.Rules { + expected := command.Expected + expected.Locator.Position = nil + verification.Rules = append(verification.Rules, filter.RuleCommands{Operation: filter.ChangeCreate, Expected: expected}) + } + plans = append(plans, verification) + completed = append(completed, batchIndex) + if !savesRules { + saveRecords(batch) + } + } + persistenceErrors := persistFirewallRuleBatches(ctx, client, plans) + for index, batchIndex := range completed { + if err := persistenceErrors[index]; err != nil { + failures = append(failures, err) + continue + } + if savesRules { + saveRecords(batches[batchIndex]) + } + } + + verification, err := verifyFirewallCommands(ctx, client, plans...) + if err != nil { + return nil, errors.Join(append(failures, err)...) + } + if !verification.Matched { + return nil, errors.Join(append(failures, filter.ErrVerificationFailed)...) + } + created := make([]filter.FirewallRule, 0) + for index, plan := range plans { + if persistenceErrors[index] != nil { + continue + } + for _, command := range plan.Rules { + created = append(created, command.Expected.Rule) + } + } + return created, errors.Join(failures...) +} + func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error { firewallWhitelistMu.Lock() defer firewallWhitelistMu.Unlock() @@ -1289,23 +1532,7 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F 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} - if err := ctx.Err(); err != nil { - return result, err - } - runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request) - if err != nil { - return result, err - } created, removed, unexecuted := 0, 0, 0 failedRemovals := make(map[string]error) @@ -1354,12 +1581,55 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F t.LogWithStatus(label, cause) } } + firewallWhitelistMu.Lock() + ports, whitelistErr := loadFirewallPortWhiteList() + var whitelistCreated []filter.FirewallRule + if whitelistErr == nil { + whitelistCreated, whitelistErr = s.syncPortWhitelist(ctx, request.TargetProvider, ports) + } + firewallWhitelistMu.Unlock() + createdWhitelistKeys := make(map[string]bool, len(whitelistCreated)) + for _, rule := range whitelistCreated { + key, err := filter.RuleMatchKey(rule) + if err != nil { + return result, err + } + createdWhitelistKeys[key] = true + result.Total++ + record("TaskCreate", &firewallSyncRule{FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule}}, nil, false) + } + if t != nil { + t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) + } + if whitelistErr != nil { + return result, whitelistErr + } + firewallRuleMutationMu.Lock() + defer firewallRuleMutationMu.Unlock() + if err := ctx.Err(); err != nil { + return result, err + } + runtime, rules, snapshots, err := s.loadFirewallSyncRules(ctx, request) + if err != nil { + return result, err + } + defer func() { if t != nil { t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted})) } }() for _, rule := range rules { + if rule.Status == firewallsync.StatusExisting && rule.Rule != nil { + key, err := filter.RuleMatchKey(*rule.Rule) + if err != nil { + return result, err + } + if createdWhitelistKeys[key] { + rule.done = true + continue + } + } if rule.Status != firewallsync.StatusRemove { result.Total++ } @@ -1368,7 +1638,7 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F } } for _, rule := range rules { - if rule.Status == firewallsync.StatusExisting { + if !rule.done && rule.Status == firewallsync.StatusExisting { record("TaskSync", rule, nil, false) } } diff --git a/frontend/src/views/host/firewall/rule/index.vue b/frontend/src/views/host/firewall/rule/index.vue index ac62ee7a4b7f..168ab6350af2 100644 --- a/frontend/src/views/host/firewall/rule/index.vue +++ b/frontend/src/views/host/firewall/rule/index.vue @@ -39,14 +39,16 @@