package service import ( "context" "errors" "fmt" "os" "slices" "strconv" "strings" "sync" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" ) type IForwardingService interface { LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) Enable(ctx context.Context) error QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) Restore(context.Context) error } type ForwardingService struct { clientFactory func(context.Context) (forwarding.Adapter, error) } var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var forwardingMutationMu sync.Mutex func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) { families, err := loadFirewallFamilies() if err != nil { return dto.FirewallSubsystemStatus{}, err } selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) selected = strings.TrimSpace(selected) if selected == "" { selected = constant.FirewallProviderIptables } baseInfo := dto.FirewallSubsystemStatus{ Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1, } if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables { baseInfo.Name += "-forward" } manager, err := s.clientFactory(ctx) if err != nil { if errors.Is(err, errForwardingBackendUnavailable) { baseInfo.Reason = constant.FirewallBackendNotInstalled return baseInfo, nil } return baseInfo, err } client, err := lifecycle.NewClient(manager.Name()) if err != nil { return baseInfo, err } version, versionErr := client.Version() status, statusErr := loadForwardingFirewallOverview(manager, families) if err := errors.Join(versionErr, statusErr); err != nil { return baseInfo, err } baseInfo.IsExist = true baseInfo.Name, baseInfo.Backend = manager.Name(), manager.Name() if baseInfo.Backend == constant.FirewallProviderIptables || baseInfo.Backend == constant.FirewallProviderNftables { baseInfo.Name += "-forward" } baseInfo.Version = version baseInfo.PingStatus = firewall.LoadPingStatus() baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6 return baseInfo, nil } func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) { var manager forwarding.Adapter var err error if provider == "" { manager, err = s.clientFactory(ctx) } else { manager, err = newForwardingAdapterFor(ctx, string(provider)) } if err != nil { return dto.FirewallSubsystemBackup{}, err } rules, err := manager.List() if err != nil { return dto.FirewallSubsystemBackup{}, err } return forwardingBackup(manager, rules) } func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) { manager, err := s.clientFactory(ctx) if err != nil { return 0, nil, err } rules, err := manager.List() if err != nil { return 0, nil, err } items := make([]dto.ForwardRule, 0, len(rules)) keyword := strings.ToLower(strings.TrimSpace(request.Info)) for _, rule := range rules { if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) { continue } items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}) } total := len(items) if request.All { return int64(total), items, nil } start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total) end := min(start+max(request.PageSize, 1), total) return int64(total), items[start:end], nil } func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) { count := 0 for _, rule := range request.Rules { if rule.Operation == "add" { count += strings.Count(rule.Protocol, "/") + 1 } if count > filter.MaxAtomicExpansion { return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion) } } operation := task.TaskCreate for _, rule := range request.Rules { if rule.Operation != "add" { operation = task.TaskUpdate } } if forwardingOperationsOnlyRemove(request.Rules) { operation = task.TaskDelete } taskItem, err := task.NewTask(firewallTaskName(operation, firewallTaskForwarding, ""), operation, task.TaskScopeFirewall, "", 0) if err != nil { return dto.FilterChainOperationResponse{}, err } taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { return s.operateRules(t.TaskCtx, request, t, false) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) closeUnstartedFirewallTask(taskItem) return dto.FilterChainOperationResponse{}, err } go func() { _ = taskItem.Execute() }() return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil } func (s *ForwardingService) Enable(ctx context.Context) error { forwardingMutationMu.Lock() defer forwardingMutationMu.Unlock() manager, err := s.clientFactory(ctx) if err != nil { return err } rules, err := manager.List() if err != nil { return err } families, err := loadFirewallFamilies() if err != nil { return err } if err := initializeForwarding(manager, families); err != nil { return err } return persistForwardingRules(manager, rules) } func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { return dto.FilterChainOperationResponse{}, err } var backup dto.FirewallSubsystemBackup if request.BackupFile != "" { var err error backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding") if err != nil { return dto.FilterChainOperationResponse{}, err } } operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))} for _, rule := range backup.Forwarding { operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface}) } return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error { return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...) }) } func (s *ForwardingService) Restore(ctx context.Context) error { forwardingMutationMu.Lock() defer forwardingMutationMu.Unlock() status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey) if err != nil || status != constant.StatusEnable { return err } manager, err := s.clientFactory(ctx) if err != nil { return err } backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding") if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } families, err := loadFirewallFamilies() if err != nil { return err } if !slices.Contains(families, constant.FirewallFamilyIPv6) { before := len(backup.Forwarding) backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 }) logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding)) } missing := make(map[string]bool) needsBind := false for _, family := range families { initialized, bound, err := manager.FamilyStatus(family) if err != nil { return err } missing[family] = !initialized needsBind = needsBind || (initialized && !bound) } restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6) for _, rule := range backup.Forwarding { restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6 } if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind { return nil } current, err := manager.List() if err != nil { return err } seen := make(map[string]int, len(current)) for _, rule := range current { seen[rule.Identity()]++ } for _, family := range families { if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 { continue } if err := manager.OperateFamily(family, missing[family]); err != nil { return err } if !missing[family] { continue } rules := make([]forwarding.Rule, 0) for _, rule := range backup.Forwarding { if rule.Family != family { continue } if seen[rule.Identity()] > 0 { seen[rule.Identity()]-- continue } rules = append(rules, rule) } if err := manager.CreateRules(ctx, rules); err != nil { return err } } return nil } func NewIForwardingService() IForwardingService { return newForwardingService() }