package service import ( "context" "errors" "fmt" "os" "path/filepath" "slices" "strconv" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "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" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld" filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" lifecycleproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" "github.com/google/uuid" ) type FirewallService struct { adapters map[filter.Provider]filter.Adapter selectedProvider func(context.Context) (filter.Provider, error) requiredPorts func() ([]firewall.PortWhitelist, error) cleanupBackend func(string) error cleanupInactiveBackend func(string) error resetBackend func(string, bool) error dockerActive func() (bool, error) restoreForwarding func(context.Context) error restoreDockerGuard func(context.Context) error baseClient func() (lifecycle.Client, error) } var firewallRuleMutationMu sync.Mutex var ( firewallLifecycleTaskMu sync.Mutex firewallLifecycleTaskID string firewallLifecycleRequest dto.FirewallLifecycleOperation ) type IFirewallService interface { SyncPortWhitelist(context.Context) error UpdatePanelPort(context.Context, uint, uint) error LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) OperateFilterChain(request dto.FilterChainOperation) error QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error) ListRuleBackups(context.Context, string) (dto.FirewallRuleBackups, error) Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) ExportBackup(context.Context, filter.Provider) ([]dto.FirewallRuleExportItem, int, error) Inventory(context.Context, dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) LoadFirewallNativeDetail(context.Context, dto.FirewallNativeDetail) (string, error) Create(context.Context, dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) Delete(context.Context, dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) Update(context.Context, dto.FirewallRuleUpdate) error Reorder(context.Context, dto.FirewallRuleReorder) error } func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error { return s.syncPortWhitelist(ctx, nil) } func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error { if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 { return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port) } if LoadPanelPort() != strconv.Itoa(int(oldPort)) { return fmt.Errorf("panel port changed before firewall update") } if oldPort == port { return nil } return s.updateSystemAccessPortWhitelist(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))}) } func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { families, err := loadFirewallFamilies() if err != nil { return dto.FirewallSubsystemStatus{}, err } status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-", IPv6Enabled: len(families) > 1} status.LifecycleTaskID = currentFirewallLifecycleTaskID() selected, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) if selected = strings.TrimSpace(selected); selected != "" { status.Name, status.Backend = selected, selected } loadClient := s.baseClient if loadClient == nil { loadClient = NewSelectedSystemFirewallClient } client, err := loadClient() if err != nil { if global.LOG != nil { global.LOG.Errorf("load firewall failed, err: %v", err) } if errors.Is(err, lifecycle.ErrNotInstalled) { status.Reason = constant.FirewallBackendNotInstalled return status, nil } return status, err } status.IsExist = true runtimeStatus, err := lifecycle.LoadStatus(client) if err != nil { return status, err } status.Name, status.Backend = runtimeStatus.Name, runtimeStatus.Name status.Version, status.PingStatus = runtimeStatus.Version, firewall.LoadPingStatus() status.IsActive = runtimeStatus.IsActive if runtimeStatus.Name == constant.FirewallProviderIptables || runtimeStatus.Name == constant.FirewallProviderNftables { overview, err := loadSystemFirewallOverview(runtimeStatus.Name, chainGroup, families) if err != nil { return status, err } status.IsInit, status.IsBind = overview.IsInit, overview.IsBind status.IPv4, status.IPv6 = overview.IPv4, overview.IPv6 } return status, nil } func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) { response := dto.FirewallLifecycleOperationResponse{} if request.BackupFile != "" && request.Operation != string(lifecycle.OperationStart) { return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw") } if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" { return response, s.OperateFirewall(request) } if !firewallLifecycleTaskMu.TryLock() { return response, buserr.New("TaskIsExecuting") } defer firewallLifecycleTaskMu.Unlock() if firewallLifecycleTaskID != "" { if request == firewallLifecycleRequest { return dto.FirewallLifecycleOperationResponse{TaskID: firewallLifecycleTaskID, Queued: true}, nil } return response, buserr.New("TaskIsExecuting") } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return response, err } loadClient := s.baseClient if loadClient == nil { loadClient = NewSelectedSystemFirewallClient } client, err := loadClient() if err != nil { return response, err } var restoreRequest dto.FirewallRuleCreate if request.BackupFile != "" { if client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW { return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw") } restoreRequest, err = prepareFirewallRuleCreate(dto.FirewallRuleCreate{BackupFile: request.BackupFile, Initialize: true}) if err != nil { return response, err } if err := validateFirewallCreateBatch(restoreRequest, filter.Provider(client.Name())); err != nil { return response, err } } if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) || (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" switch request.Operation { case string(lifecycle.OperationStop): label = "Stop" case string(lifecycle.OperationRestart): operation, label = task.TaskRestart, task.TaskRestart } name := task.GetTaskName(client.Name(), label, task.TaskScopeFirewall) taskItem, err := task.NewTask(name, operation, task.TaskScopeFirewall, "", 0) if err != nil { return response, err } taskItem.AddSubTaskWithOps(name, func(t *task.Task) error { if request.BackupFile != "" { if err := s.checkSelectedProvider(t.TaskCtx, filter.Provider(client.Name())); err != nil { return err } } prepareStart := func(lifecycle.Client) error { if request.BackupFile != "" { return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallCreateRulesStep"), func() error { return s.runFirewallCreateRulesTask(t, restoreRequest, filter.Provider(client.Name())) }) } return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { err := s.SyncPortWhitelist(t.TaskCtx) if err == nil && client.Name() == lifecycle.ProviderFirewalld { err = lifecycleproviders.RemoveFirewalldSSHService() } return err }) } return s.runFirewallLifecycleTask(t, client, request, prepareStart) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { closeUnstartedFirewallTask(taskItem) return response, err } firewallLifecycleTaskID, firewallLifecycleRequest = taskItem.TaskID, request go func() { defer func() { closeUnstartedFirewallTask(taskItem) firewallLifecycleTaskMu.Lock() defer firewallLifecycleTaskMu.Unlock() if firewallLifecycleTaskID == taskItem.TaskID { firewallLifecycleTaskID = "" } }() if err := taskItem.Execute(); err != nil && taskItem.Task.Status == constant.StatusExecuting { taskItem.LogFailedWithErr(name, err) taskItem.Task.Status = constant.StatusFailed taskItem.Task.ErrorMsg = err.Error() taskItem.Task.EndAt = time.Now() _ = taskRepo.Update(context.Background(), taskItem.Task) } }() return dto.FirewallLifecycleOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) error { client, err := NewSelectedSystemFirewallClient() if err != nil { return err } provider := client.Name() if err := s.operateFilterChainBase(provider, request); err != nil { return err } if request.Operate != string(firewall.BaseOperationInit) && request.Operate != string(firewall.BaseOperationBind) { return nil } ctx := context.Background() return s.SyncPortWhitelist(ctx) } func (s *FirewallService) QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error) { if request.Operate != string(firewall.BaseOperationInit) { return dto.FilterChainOperationResponse{}, fmt.Errorf("only filter chain initialization can be queued") } client, err := NewSelectedSystemFirewallClient() if err != nil { return dto.FilterChainOperationResponse{}, err } provider := client.Name() if provider != constant.FirewallProviderIptables && provider != constant.FirewallProviderNftables { return dto.FilterChainOperationResponse{}, fmt.Errorf("filter chain operations are not supported for %s", provider) } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return dto.FilterChainOperationResponse{}, err } taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskHost, provider), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0) if err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("create firewall initialization task: %w", err) } taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeChainsStep", provider), func(t *task.Task) error { t.Logf("backend=%s", provider) families, err := loadFirewallFamilies() if err != nil { return err } if !slices.Contains(families, constant.FirewallFamilyIPv6) { t.Logf("IPv6 firewall support is disabled; skipping IPv6 host chain initialization") } return s.operateFilterChainBase(provider, request) }, nil) taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error { whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { return s.SyncPortWhitelist(t.TaskCtx) }) return whitelistErr }, nil) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall initialization task: %w", err) } go func() { _ = taskItem.Execute() }() return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *FirewallService) ListRuleBackups(ctx context.Context, subsystem string) (dto.FirewallRuleBackups, error) { if subsystem == "" { subsystem = "system" } directory, err := firewallBackupDirectory(subsystem) if err != nil { return dto.FirewallRuleBackups{}, err } result := dto.FirewallRuleBackups{Directory: directory, Files: make([]dto.FirewallRuleBackup, 0)} entries, err := os.ReadDir(directory) if errors.Is(err, os.ErrNotExist) { return result, nil } if err != nil { return result, err } for _, entry := range entries { if err := ctx.Err(); err != nil { return result, err } if !entry.Type().IsRegular() || filepath.Ext(entry.Name()) != ".json" { continue } var provider filter.Provider count := 0 if subsystem == "system" { items, err := readFirewallBackup(entry.Name()) if err != nil || len(items) == 0 { continue } provider, count = items[0].Scope.Provider, len(items) } else { backup, err := readFirewallSubsystemBackup(entry.Name(), subsystem) if err != nil { continue } provider, count = backup.Provider, len(backup.Forwarding) if backup.Docker != nil { count = len(backup.Docker.Policies) } } info, err := entry.Info() if err != nil { return result, err } result.Files = append(result.Files, dto.FirewallRuleBackup{Name: entry.Name(), Provider: provider, RuleCount: count, ModifiedAt: info.ModTime().UnixMilli()}) } slices.SortFunc(result.Files, func(a, b dto.FirewallRuleBackup) int { if a.ModifiedAt > b.ModifiedAt { return -1 } if a.ModifiedAt < b.ModifiedAt { return 1 } return strings.Compare(b.Name, a.Name) }) return result, nil } func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { if request.Subsystem != "" && request.Subsystem != "system" { return s.resetSubsystemRules(ctx, request) } if err := lockFirewallLifecycleIdle(); err != nil { return dto.FirewallRuleResetResponse{}, err } defer firewallLifecycleTaskMu.Unlock() firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() selected, err := s.selectedProvider(ctx) if err != nil { return dto.FirewallRuleResetResponse{}, err } provider := request.Provider if provider == "" { provider = selected } var backup string var count int if request.Backup == nil || *request.Backup { directory, directoryErr := firewallBackupDirectory("system") if directoryErr != nil { return dto.FirewallRuleResetResponse{}, directoryErr } var items []dto.FirewallRuleExportItem items, count, err = s.ExportBackup(ctx, provider) if err == nil { backup, err = writeFirewallJSON(directory, string(provider)+"-"+time.Now().Format("20060102-150405")+"-"+uuid.NewString()+".json", items) } } else { var inventory dto.FirewallRuleInventoryResponse inventory, err = s.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider)) count = len(inventory.Items) } if err != nil { return dto.FirewallRuleResetResponse{}, err } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { cleanup := s.cleanupBackend if (selected == filter.ProviderIptables || selected == filter.ProviderNftables) && selected != provider { cleanup = s.cleanupInactiveBackend if cleanup == nil { cleanup = cleanupInactiveSystemBackend } } else if cleanup == nil { cleanup = cleanupSystemBackend } if err := cleanup(string(provider)); err != nil { return dto.FirewallRuleResetResponse{}, err } return dto.FirewallRuleResetResponse{Removed: count, Disabled: true, BackupPath: backup}, nil } if provider != filter.ProviderUFW && provider != filter.ProviderFirewalld { return dto.FirewallRuleResetResponse{}, fmt.Errorf("%w: unsupported firewall provider %s", filter.ErrProviderUnavailable, provider) } reset := s.resetBackend if reset == nil { reset = resetServiceFirewallBackend } restartDocker := false if provider == filter.ProviderFirewalld && request.WithDockerRestart { dockerActive := s.dockerActive if dockerActive == nil { dockerActive = firewallDockerActive } active, err := dockerActive() if err != nil { return dto.FirewallRuleResetResponse{}, fmt.Errorf("check Docker status before resetting firewalld: %w", err) } restartDocker = active } resetErr := reset(string(provider), restartDocker) if resetErr != nil { var dockerRestartErr *firewallDockerRestartError if provider != filter.ProviderFirewalld || !errors.As(resetErr, &dockerRestartErr) { return dto.FirewallRuleResetResponse{}, resetErr } } if provider == filter.ProviderFirewalld { restoreForwarding := s.restoreForwarding if restoreForwarding == nil { restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) } } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { restoreDockerGuard = RestoreDockerPortGuard } restoreErr := restoreFirewalldDependents( ctx, "after resetting firewalld", restartDocker, restoreForwarding, restoreDockerGuard, ) if err := errors.Join(resetErr, restoreErr); err != nil { return dto.FirewallRuleResetResponse{}, err } } return dto.FirewallRuleResetResponse{Removed: count, Disabled: true, BackupPath: backup}, nil } func (s *FirewallService) ExportBackup(ctx context.Context, provider filter.Provider) ([]dto.FirewallRuleExportItem, int, error) { if provider == "" { var err error provider, err = s.selectedProvider(ctx) if err != nil { return nil, 0, err } } response, err := s.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider)) if err != nil { return nil, 0, err } descriptions, err := settingRepo.GetDescriptionList(repo.WithByType("firewall")) if err != nil { return nil, 0, err } byID := make(map[string]string, len(descriptions)) for _, description := range descriptions { byID[description.ID] = description.Description } items := make([]dto.FirewallRuleExportItem, 0, len(response.Items)) for _, item := range response.Items { if (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && item.Rule.Scope.Chain != filter.IptablesInputChain { continue } id, err := filter.DescriptionID(*item.Observed) if err != nil { return nil, 0, err } rule := item.Rule if description, ok := byID[id]; ok { rule.Description = description } rule.UUID, rule.OrderIndex = "", nil items = append(items, dto.FirewallRuleExportItem{FirewallRule: rule, Raw: item.Observed.Raw, ParseStatus: item.Observed.ParseStatus}) } return items, len(response.Items), nil } func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) { scopes := request.Scopes if len(scopes) == 0 && request.Scope.Provider != "" { scopes = []filter.Scope{request.Scope} } if len(scopes) == 0 { return dto.FirewallRuleInventoryResponse{}, filter.ErrInvalidScope } provider := scopes[0].Provider if err := s.checkSelectedProvider(ctx, provider); err != nil { return dto.FirewallRuleInventoryResponse{}, err } if len(scopes) == 1 && provider == filter.ProviderUFW && scopes[0].Family == filter.FamilyInet { scopes = filter.ManagedInputScopes(provider) } response, err := s.readFirewallInventory(ctx, provider, scopes) if err != nil { return response, err } searchDescription := strings.TrimSpace(request.Info) != "" if !searchDescription { response = finalizeFirewallInventory(response, request) } ids := make([]string, 0, len(response.Items)) for index := range response.Items { item := &response.Items[index] item.DescriptionID, err = filter.DescriptionID(*item.Observed) if err != nil { return response, err } ids = append(ids, item.DescriptionID) } byID := make(map[string]string, len(ids)) for start := 0; start < len(ids); start += 500 { descriptions, err := settingRepo.GetDescriptionList(repo.WithByType("firewall"), settingRepo.WithDescriptionIDs(ids[start:min(start+500, len(ids))])) if err != nil { return response, err } for _, description := range descriptions { byID[description.ID] = description.Description } } for index := range response.Items { item := &response.Items[index] if description, ok := byID[item.DescriptionID]; ok { item.Rule.Description = description } } if searchDescription { response = finalizeFirewallInventory(response, request) } ports, err := loadFirewallPortWhiteList() if err != nil { return response, err } whitelist := filter.NewPortWhitelistIndex(ports) for index := range response.Items { item := &response.Items[index] item.IsWhitelist = item.Observed.ParseStatus == filter.ParseStatusSupported && whitelist.Matches(item.Rule) item.Observed.Protected = item.Observed.Protected || item.IsWhitelist } return response, nil } func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request dto.FirewallNativeDetail) (string, error) { provider := filter.Provider(strings.ToLower(strings.TrimSpace(string(request.Provider)))) nativeKind := filter.NativeKind(strings.ToLower(strings.TrimSpace(string(request.NativeKind)))) switch provider { case filter.ProviderFirewalld: if nativeKind != filter.NativeKindZoneService { return "", fmt.Errorf("%w: firewalld detail kind %q", filter.ErrInvalidRule, nativeKind) } case filter.ProviderUFW: if nativeKind != filter.NativeKindUFWApplication { return "", fmt.Errorf("%w: UFW detail kind %q", filter.ErrInvalidRule, nativeKind) } default: return "", fmt.Errorf("%w: native details for %s", filter.ErrUnsupportedScope, provider) } if err := s.checkSelectedProvider(ctx, provider); err != nil { return "", err } runtime, err := s.firewallAdapter(provider) if err != nil { return "", err } reader, ok := runtime.(filter.NativeDetailReader) if !ok { return "", fmt.Errorf("%w: native details for %s", filter.ErrAdapterUnavailable, runtime.Provider()) } return reader.NativeDetail(ctx, request.Name, request.Permanent) } func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) { request, err := prepareFirewallRuleCreate(request) if err != nil { return dto.FirewallRuleCreateResponse{}, err } provider, err := s.selectedProvider(ctx) if err != nil { return dto.FirewallRuleCreateResponse{}, err } if err := validateFirewallCreateBatch(request, provider); err != nil { return dto.FirewallRuleCreateResponse{}, err } taskItem, err := task.NewTask(firewallTaskName(task.TaskCreate, firewallTaskHost, ""), task.TaskCreate, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FirewallRuleCreateResponse{}, err } if request.Initialize { taskItem.AddSubTask(i18n.GetWithName("FirewallInitializeChainsStep", string(provider)), func(t *task.Task) error { if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil { return err } families, err := loadFirewallFamilies() if err != nil { return err } if !slices.Contains(families, constant.FirewallFamilyIPv6) { t.Logf("IPv6 firewall support is disabled; skipping IPv6 host chain initialization") } if provider == filter.ProviderIptables || provider == filter.ProviderNftables { return s.operateFilterChainBase(string(provider), dto.FilterChainOperation{Name: "1PANEL_BASIC", Operate: string(firewall.BaseOperationInit)}) } client, err := s.baseClient() if err != nil { return err } return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, nil) }, nil) } taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error { return s.runFirewallCreateRulesTask(t, request, provider) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) closeUnstartedFirewallTask(taskItem) return dto.FirewallRuleCreateResponse{}, fmt.Errorf("save firewall creation task: %w", err) } go func() { if err := taskItem.Execute(); err != nil && global.LOG != nil { global.LOG.Errorf("firewall creation task %s failed: %v", taskItem.TaskID, err) } }() return dto.FirewallRuleCreateResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) { if err := ctx.Err(); err != nil { return dto.FirewallRuleDeleteResponse{}, err } if len(request.Targets) == 0 { return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("%w: rules are required", filter.ErrInvalidRule) } request.Targets = append([]dto.FirewallRuleDeleteItem(nil), request.Targets...) taskItem, err := task.NewTask(firewallTaskName(task.TaskDelete, firewallTaskHost, ""), task.TaskDelete, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FirewallRuleDeleteResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { t.Logf("rules=%d", len(request.Targets)) firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() if err := t.TaskCtx.Err(); err != nil { return err } _, err := s.deleteRules(t.TaskCtx, request, t) return err }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) closeUnstartedFirewallTask(taskItem) return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("save firewall deletion task: %w", err) } go func() { if err := taskItem.Execute(); err != nil && global.LOG != nil { global.LOG.Errorf("firewall deletion task %s failed: %v", taskItem.TaskID, err) } }() return dto.FirewallRuleDeleteResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *FirewallService) Update(ctx context.Context, request dto.FirewallRuleUpdate) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return s.updateObservedRule(ctx, request) } func (s *FirewallService) Reorder(ctx context.Context, request dto.FirewallRuleReorder) error { firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() return s.updateObservedRule(ctx, dto.FirewallRuleUpdate{FirewallRuleDeleteTarget: request.FirewallRuleDeleteTarget, OrderIndex: request.TargetPosition, Priority: request.Priority}) } func NewIFirewallService() IFirewallService { return newFirewallService() } func currentFirewallLifecycleTaskID() string { firewallLifecycleTaskMu.Lock() defer firewallLifecycleTaskMu.Unlock() return firewallLifecycleTaskID } func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error { switch request.Operation { case "disableBanPing": if err := firewall.UpdatePingStatus("0"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusDisable) case "enableBanPing": if err := firewall.UpdatePingStatus("1"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusEnable) } baseClient := s.baseClient if baseClient == nil { baseClient = NewSelectedSystemFirewallClient } client, err := baseClient() if err != nil { return err } operation := request.Operation operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, operation, request.WithDockerRestart, s.restoreFirewallAfterStart, nil) restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld && (operation == string(lifecycle.OperationStart) || operation == string(lifecycle.OperationRestart)) if operation != string(lifecycle.OperationStart) && operation != string(lifecycle.OperationRestart) { return operationErr } if operationErr != nil { var completedErr *firewallCompletedOperationError var dockerRestartErr *firewallDockerRestartError 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 } RestoreDockerPortGuardBestEffort(context.Background()) return nil } func (s *FirewallService) restoreFirewallAfterStart(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 := loadFirewallInitStatus(string(provider), "base") if err != nil { recordFailure("load managed chain status", err) return errors.Join(recoveryErrors...) } if !isInit { return nil } } whitelistErr := s.SyncPortWhitelist(ctx) if whitelistErr == nil && provider == filter.ProviderFirewalld { whitelistErr = lifecycleproviders.RemoveFirewalldSSHService() } recordFailure("restore whitelist allowances", whitelistErr) return errors.Join(recoveryErrors...) } func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation string) error { restoreForwarding := s.restoreForwarding if restoreForwarding == nil { restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) } } restoreDockerGuard := s.restoreDockerGuard if restoreDockerGuard == nil { restoreDockerGuard = RestoreDockerPortGuard } dockerActive := s.dockerActive if dockerActive == nil { dockerActive = firewallDockerActive } 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) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, prepareStart func(lifecycle.Client) error) error { ctx := t.TaskCtx provider := filter.Provider(client.Name()) operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t) if request.Operation == string(lifecycle.OperationStop) { return operationErr } var recoveryErr *firewallCompletedOperationError if operationErr != nil && !errors.As(operationErr, &recoveryErr) { return operationErr } var forwardingErr error if provider == filter.ProviderFirewalld { forwardingErr = runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func() error { if s.restoreForwarding != nil { return s.restoreForwarding(ctx) } return newForwardingService().Restore(ctx) }) } dockerErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func() error { if provider == filter.ProviderFirewalld { active := s.dockerActive if active == nil { active = firewallDockerActive } running, err := active() if err != nil || !running { return err } } if s.restoreDockerGuard != nil { return s.restoreDockerGuard(ctx) } return RestoreDockerPortGuard(ctx) }) return errors.Join(operationErr, forwardingErr, dockerErr) } func finalizeFirewallInventory(response dto.FirewallRuleInventoryResponse, request dto.FirewallRuleInventory) dto.FirewallRuleInventoryResponse { provider := request.Scope.Provider if len(request.Scopes) > 0 { provider = request.Scopes[0].Provider } chain := filter.IptablesInputChain if len(request.Scopes) == 1 { chain = request.Scopes[0].Normalize().Chain } else if len(request.Scopes) == 0 { chain = request.Scope.Normalize().Chain } response.IPv4Range, response.IPv6Range = firewallInventoryPositionRanges(provider, chain, response.Items) response.AllTotal = 0 filtered := make([]filter.InventoryItem, 0, len(response.Items)) for _, item := range response.Items { if item.Observed != nil && item.Observed.Protected { continue } response.AllTotal++ if matchesFirewallInventoryRequest(item, request) { filtered = append(filtered, item) } } response.Total = int64(len(filtered)) if request.All { response.Items = filtered return response } page, pageSize := max(1, request.Page), max(1, request.PageSize) start := (page - 1) * pageSize if start >= len(filtered) { response.Items = make([]filter.InventoryItem, 0) return response } end := min(start+pageSize, len(filtered)) response.Items = append([]filter.InventoryItem(nil), filtered[start:end]...) return response } func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.FirewallRuleInventory) bool { if slices.Contains(request.ExcludeChains, item.Rule.Scope.Chain) { return false } if len(request.Families) > 0 && !matchesFirewallInventoryFamily(item.Rule, request.Families) { return false } if len(request.Actions) > 0 && !matchesFirewallInventoryAction(item.Rule.Action, request.Actions) { return false } keyword := strings.ToLower(strings.TrimSpace(request.Info)) if keyword == "" { return true } rule := item.Rule values := []string{ firewallInventoryProtocol(rule), rule.SourceAddress, rule.SourcePort, rule.DestinationAddress, rule.DestinationPort, rule.Description, string(rule.Action), } if item.Observed != nil { values = append(values, item.Observed.Rule.Description) } for _, value := range values { if strings.Contains(strings.ToLower(value), keyword) { return true } } return false } func matchesFirewallInventoryFamily(rule filter.FirewallRule, families []filter.Family) bool { for _, family := range families { if rule.Scope.Family != filter.FamilyInet && rule.Scope.Family == family { return true } if rule.Scope.Family == filter.FamilyInet && (rule.SourceAddress == "" || (family == filter.FamilyIPv6) == strings.Contains(rule.SourceAddress, ":")) { return true } } return false } func matchesFirewallInventoryAction(action filter.Action, actions []string) bool { for _, requested := range actions { if requested == "accept" && action == filter.ActionAccept { return true } if requested == "deny" && (action == filter.ActionDrop || action == filter.ActionReject) { return true } } return false } func firewallInventoryProtocol(rule filter.FirewallRule) string { if rule.NativeKind == filter.NativeKindZoneService { return "service" } if rule.NativeKind == filter.NativeKindUFWApplication && rule.Protocol == "" { return "app" } if rule.Scope.Provider == filter.ProviderUFW && rule.Protocol == "all" && rule.DestinationPort != "" { return "tcp/udp" } return rule.Protocol } func (s *FirewallService) ensureWebsitePorts(ctx context.Context, ports map[string]firewall.SystemPort) error { if len(ports) == 0 { return nil } firewallRuleMutationMu.Lock() defer firewallRuleMutationMu.Unlock() provider, err := s.selectedProvider(ctx) if err != nil { return err } runtime, err := s.firewallAdapter(provider) if err != nil { return err } const comment = "1panel website" rules := make([]filter.FirewallRule, 0, len(ports)) var scopes []filter.Scope seenScopes := make(map[string]bool) for _, key := range firewall.SortedSystemPortKeys(ports) { prepared, err := prepareFirewallCreateRule(ctx, runtime, dto.FirewallRuleCreateItem{Rule: firewall.RuleForSystemPort(provider, ports[key])}) if err != nil { return err } rule := prepared.Rule rule.Description = comment rules = append(rules, rule) if !seenScopes[rule.Scope.Key()] { seenScopes[rule.Scope.Key()] = true scopes = append(scopes, rule.Scope) } } external, supportsExternal := runtime.(filter.ExternalRuleAdapter) existing := make(map[string]bool) if provider != filter.ProviderFirewalld { if !supportsExternal { return fmt.Errorf("%w: %s does not support external rules", filter.ErrAdapterUnavailable, provider) } observed, err := external.ListRulesByComment(ctx, scopes, comment) if err != nil { return err } for _, candidate := range observed { if candidate.ParseStatus != filter.ParseStatusSupported || candidate.Rule.Description != comment || candidate.Rule.Action != filter.ActionAccept { continue } key, err := filter.RuleMatchKey(candidate.Rule) if err != nil { return err } existing[key] = true } } var failures []error changedScopes := make(map[string]filter.Scope) for _, rule := range rules { key, err := filter.RuleMatchKey(rule) if err != nil { return err } if existing[key] { continue } if provider == filter.ProviderFirewalld { rule.UUID = uuid.NewString() plan, buildErr := runtime.BuildCommands(filter.RuleSet{Scope: rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &rule, CommandOnly: true, Append: true}}) if buildErr != nil { return buildErr } plan.CommandOnly = true err = runtime.RunCommands(ctx, plan) if errors.Is(err, filterfirewalld.ErrAlreadyEnabled) { err = nil } } else { err = external.AppendUnverified(ctx, rule, comment) } if rule.Scope.Family == filter.FamilyIPv6 && filterufw.IsIPv6Unavailable(err) { continue } if err != nil { failures = append(failures, fmt.Errorf("allow website port %s/%s (%s): %w", rule.DestinationPort, rule.Protocol, rule.Scope.Family, err)) continue } existing[key] = true saveKey := rule.Scope.Key() if provider == filter.ProviderNftables { saveKey = string(provider) } changedScopes[saveKey] = rule.Scope } if saver, ok := runtime.(filter.RuleSaver); ok { for _, scope := range changedScopes { saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) err := saver.SaveRules(saveCtx, scope) cancel() if err != nil { failures = append(failures, err) } } } return errors.Join(failures...) } func ensureFirewallPorts(ports []int) error { if len(ports) == 0 { return nil } client, err := NewSelectedSystemFirewallClient() if err != nil { return err } state, err := lifecycle.LoadState(client) if err != nil { return err } if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables { isInit, _, err := loadFirewallInitStatus(state.Name, "base") if err != nil { return err } if !isInit { return nil } } else if !state.IsActive { return nil } added := make([]firewall.PortWhitelist, 0, len(ports)) for _, port := range ports { added = append(added, firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"}) } normalized, err := firewall.NormalizeSystemPorts(firewall.ExpandPortWhitelist(added)) if err != nil { return err } return newFirewallService().ensureWebsitePorts(context.Background(), normalized) } func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallRuleDelete, t *task.Task) (dto.FirewallRuleDeleteResponse, error) { result := dto.FirewallRuleDeleteResponse{} provider, err := s.selectedProvider(ctx) if err != nil { return result, err } runtime, err := s.firewallAdapter(provider) if err != nil { return result, err } ports, err := loadFirewallPortWhiteList() if err != nil { return result, err } whitelist := filter.NewPortWhitelistIndex(ports) plans := make([]filter.CommandBatch, len(request.Targets)) groups := make([][]int, 0) byScope := make(map[string]int) seen := make(map[string]bool) var failures []error record := func(index int, err error) { label := fmt.Sprintf("[%d/%d] %s", index+1, len(request.Targets), request.Targets[index].Scope.Key()) if errors.Is(err, filter.ErrRuleNotFound) { if t != nil { t.Logf("%s: %v", label, err) } else if global.LOG != nil { global.LOG.Infof("%s: %v", label, err) } err = nil } if err == nil { result.Succeeded++ } else { result.Failed++ result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{Index: index, InstanceKey: request.Targets[index].InstanceKey, Error: err.Error()}) failures = append(failures, err) } if t != nil { t.LogWithStatus(label, err) } } for index, target := range request.Targets { if seen[target.InstanceKey] { continue } seen[target.InstanceKey] = true scope := target.Scope.Normalize() if scope.Provider != provider || target.Observed.Rule.Scope.Key() != scope.Key() { record(index, filter.ErrInvalidScope) continue } plan, err := runtime.BuildCommands(filter.RuleSet{Scope: scope}, []filter.RuleChange{{Operation: filter.ChangeDelete, Target: &target.Observed, CommandOnly: true}}) if err != nil { record(index, err) continue } if plan.Rules[0].Expected.Protected || whitelist.Matches(plan.Rules[0].Expected.Rule) { record(index, filter.ErrProtectedRule) continue } plan.CommandOnly = true plans[index] = plan key := scope.Key() if provider == filter.ProviderFirewalld { key += ":" + string(plan.Rules[0].Expected.Rule.NativeKind) } group, exists := byScope[key] if !exists { group = len(groups) byScope[key] = group groups = append(groups, nil) } groups[group] = append(groups[group], index) } var attempted []filter.CommandBatch var indexes []int var results []error for _, group := range groups { for start := 0; start < len(group); { end := start + 1 if provider != filter.ProviderUFW { end = min(start+filter.MaxAtomicExpansion, len(group)) } batch := group[start:end] start = end plan := plans[batch[0]] err := ctx.Err() if err == nil && len(batch) > 1 { changes := make([]filter.RuleChange, 0, len(batch)) for _, index := range batch { changes = append(changes, filter.RuleChange{Operation: filter.ChangeDelete, Target: &plans[index].Rules[0].Expected, CommandOnly: true}) } plan, err = runtime.BuildCommands(filter.RuleSet{Scope: plan.Scope}, changes) } if err == nil { plan.CommandOnly = true err = runtime.RunCommands(ctx, plan) } retry := errors.Is(err, filter.ErrRuleNotFound) if err != nil && provider == filter.ProviderIptables { retry = retry || strings.Contains(err.Error(), "iptables-restore: line ") && strings.Contains(err.Error(), " failed") } for _, index := range batch { itemErr := err if len(batch) > 1 && retry { itemErr = ctx.Err() if itemErr == nil { itemErr = runtime.RunCommands(ctx, plans[index]) } } if errors.Is(itemErr, filter.ErrRuleNotFound) { record(index, itemErr) continue } attempted = append(attempted, plans[index]) indexes = append(indexes, index) results = append(results, itemErr) } } } for index, saveErr := range persistFirewallRuleBatches(ctx, runtime, attempted) { record(indexes[index], errors.Join(results[index], saveErr)) } return result, errors.Join(failures...) } func (s *FirewallService) resetSubsystemRules(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) { result := dto.FirewallRuleResetResponse{} if err := lockFirewallLifecycleIdle(); err != nil { return result, err } defer firewallLifecycleTaskMu.Unlock() if request.Subsystem != "forwarding" && request.Subsystem != "docker" { return result, filter.ErrInvalidRule } if request.Subsystem == "forwarding" { forwardingMutationMu.Lock() defer forwardingMutationMu.Unlock() } else { dockerPortGuardServiceMu.Lock() defer dockerPortGuardServiceMu.Unlock() } backend := string(request.Provider) if backend == "" { if request.Subsystem == "forwarding" { manager, err := newForwardingAdapter(ctx) if err != nil { return result, err } backend = manager.Name() } else { backend = selectedDockerFirewallBackend("") } } if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables { return result, filter.ErrInvalidRule } backup := dto.FirewallSubsystemBackup{Subsystem: request.Subsystem, Provider: filter.Provider(backend)} var cleanup func() error if request.Subsystem == "forwarding" { manager, err := newForwardingAdapterFor(ctx, backend) if err != nil { return result, err } backup, err = newForwardingService().ExportBackup(ctx, filter.Provider(backend)) if err != nil { return result, err } result.Removed = len(backup.Forwarding) cleanup = manager.Cleanup } else { runtime := newDockerFirewallRuntime(ctx, backend) var err error backup, err = newDockerPortGuardService().ExportBackup(ctx, filter.Provider(backend)) if err != nil { return result, err } result.Removed = len(backup.Docker.Policies) cleanup = runtime.Cleanup } if request.Backup == nil || *request.Backup { directory, err := firewallBackupDirectory(request.Subsystem) if err != nil { return result, err } result.BackupPath, err = writeFirewallJSON(directory, request.Subsystem+"-"+backend+"-"+time.Now().Format("20060102-150405")+"-"+uuid.NewString()+".json", backup) if err != nil { return result, err } } if err := errors.Join(cleanup(), ctx.Err()); err != nil { return result, err } if err := os.Remove(filepath.Join(global.Dir.FirewallDir, request.Subsystem+"-"+backend+".rules")); err != nil && !errors.Is(err, os.ErrNotExist) { return result, err } key := constant.FirewallForwardingInitializedKey if request.Subsystem == "docker" { key = constant.FirewallDockerPortGuardStatusKey } if err := settingRepo.UpdateOrCreate(key, constant.StatusDisable); err != nil { return result, err } result.Disabled = true return result, nil }