Files
1Panel/agent/app/service/firewall.go
T

1300 lines
45 KiB
Go

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
}