Files
1Panel/agent/app/service/firewall_setting.go
T
ssongliu e119239d3d refactor: manage firewall rules through runtime state and file backups (#13998)
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations.

Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
2026-10-08 18:19:30 +08:00

689 lines
23 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"gorm.io/gorm"
)
type IFirewallSettingService interface {
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
}
type FirewallSettingService struct{}
var firewallWhitelistMu sync.Mutex
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
firewallWhitelistMu.Lock()
firewallRuleMutationMu.Lock()
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
if result == nil {
result = newFirewallService().SyncPortWhitelist(ctx)
}
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
current = append(current, request.Rule)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
firewallWhitelistMu.Lock()
firewallRuleMutationMu.Lock()
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
if result == nil {
result = newFirewallService().SyncPortWhitelist(ctx)
}
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return err
}
current[index] = request.Rule
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
firewallWhitelistMu.Lock()
firewallRuleMutationMu.Lock()
defer func() {
firewallRuleMutationMu.Unlock()
firewallWhitelistMu.Unlock()
}()
current, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
if request.Rule == nil {
return filter.ErrInvalidRule
}
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return err
}
current = slices.Delete(current, index, index+1)
current, err = firewall.ValidatePortWhitelist(current)
if err != nil {
return err
}
if err := validateFirewallWhitelistFamilies(current); err != nil {
return err
}
value, err := json.Marshal(current)
if err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
}
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
families, err := loadFirewallFamilies()
if err != nil {
return dto.FirewallSettings{}, err
}
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() {
installed[name] = true
}
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
result.System.Selected = strings.TrimSpace(systemBackend)
if result.System.Selected == "" {
if client, err := lifecycle.NewClient(""); err == nil {
result.System.Selected = client.Name()
}
}
result.System.Current = result.System.Selected
for _, name := range []string{
constant.FirewallProviderFirewalld,
constant.FirewallProviderUFW,
constant.FirewallProviderIptables,
constant.FirewallProviderNftables,
} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.System.Selected {
client, err := lifecycle.NewClient(name)
if err != nil {
option.Message = err.Error()
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
overview, err := loadSystemFirewallOverview(name, "base", families)
if err != nil {
option.Message = err.Error()
}
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
} else if option.Active, err = client.Status(); err != nil {
option.Message = err.Error()
}
}
if name == result.System.Selected && name == constant.FirewallProviderIptables {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
option.Implementation = commands.IPv4
}
}
result.System.Options = append(result.System.Options, option)
}
forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
if result.Forwarding.Selected == "" {
result.Forwarding.Selected = constant.FirewallProviderIptables
}
result.Forwarding.Current = result.Forwarding.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.Forwarding.Selected {
manager, err := newForwardingAdapterFor(ctx, name)
if err != nil {
option.Message = err.Error()
} else {
status, statusErr := loadForwardingFirewallOverview(manager, families)
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
if statusErr != nil {
option.Message = statusErr.Error()
} else {
option.Initialized, option.Bound = status.IsInit, status.IsBind
}
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
}
}
}
}
if name == result.Forwarding.Selected && name == constant.FirewallProviderIptables {
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
option.Implementation = commands.IPv4
}
}
result.Forwarding.Options = append(result.Forwarding.Options, option)
}
dockerInstalled := cmd.Which("docker")
dockerVersion := ""
if dockerInstalled {
dockerVersion = loadDockerEngineVersion(ctx)
}
dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
result.Docker.Selected = dockerBackend
}
result.Docker.Current = result.Docker.Selected
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
option := dto.FirewallBackendOption{
Name: name, Installed: installed[name], Supported: dockerInstalled,
Active: dockerInstalled && installed[name] && result.Docker.Selected == name,
}
if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) {
option.Supported = false
option.SupportReason = "docker_version_unsupported"
option.Active = false
}
if option.Active {
guard := newDockerFirewallRuntime(ctx, name)
for _, family := range families {
status := guard.Status(family)
option.Initialized = option.Initialized || status.Initialized
option.Bound = option.Bound || status.Bound
info := dto.FirewallBackendFamilyStatus{
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
}
if family == constant.FirewallFamilyIPv4 {
option.IPv4 = info
} else {
option.IPv6 = info
}
}
}
result.Docker.Options = append(result.Docker.Options, option)
}
result.PortWhitelist, err = loadPortWhitelistSetting()
if err != nil {
return result, err
}
result.PanelPort = LoadPanelPort()
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
if sshErr != nil {
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
} else {
result.SSHPort = sshPort
}
return result, err
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if request.Operation == "cleanup" {
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
return err
}
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
}
if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
}
switch request.Subsystem {
case "system":
if err := s.operateSystem(request); err != nil {
return err
}
if request.Operation == "initialize" {
service := newFirewallService()
whitelistErr := service.SyncPortWhitelist(ctx)
return whitelistErr
}
return nil
case "forwarding":
return s.operateForwarding(ctx, request)
case "docker":
return s.operateDocker(ctx, request)
default:
return fmt.Errorf("unsupported firewall subsystem %q", request.Subsystem)
}
}
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
}
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
}
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
}
subsystem := ""
switch request.Subsystem {
case "system":
subsystem = firewallTaskHost
case "forwarding":
subsystem = firewallTaskForwarding
case "docker":
subsystem = firewallTaskDocker
default:
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if !slices.Contains(families, request.Family) {
return fmt.Errorf("IPv6 firewall support is disabled")
}
initialize := request.Operation != "bind"
switch request.Subsystem {
case "system":
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
return err
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return err
}
firewallRuleMutationMu.Lock()
if request.Backend == constant.FirewallProviderIptables {
err = iptables_helper.OperateFamily(request.Family, initialize, required)
} else {
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
}
firewallRuleMutationMu.Unlock()
if err != nil {
return err
}
if initialize {
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
return err
}
}
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
case "forwarding":
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := newForwardingAdapter(t.TaskCtx)
if err != nil {
return err
}
if manager.Name() != request.Backend {
return filter.ErrProviderUnavailable
}
if err := manager.OperateFamily(request.Family, initialize); err != nil {
return err
}
rules, err := manager.List()
if err != nil {
return err
}
if err := persistForwardingRules(manager, rules); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
default:
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
if err != nil {
return err
}
if backend != request.Backend {
return filter.ErrProviderUnavailable
}
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
if err := persistDockerRules(backend, inventory); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
}
})
}
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if request.Status == constant.StatusEnable {
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
}
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
value, err := json.Marshal(ipv4PortWhitelist(ports))
if err != nil {
return err
}
installed := lifecycle.InstalledProviders()
for _, selection := range []struct{ subsystem, key string }{
{"system", constant.FirewallSystemBackendKey},
{"forwarding", constant.FirewallForwardingBackendKey},
{"docker", constant.FirewallDockerBackendKey},
} {
backend, err := settingRepo.GetValueByKey(selection.key)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
backend = strings.ToLower(strings.TrimSpace(backend))
if backend == "" {
if selection.subsystem == "system" {
if len(installed) == 0 {
continue
}
client, err := NewSelectedSystemFirewallClient()
if err != nil {
return err
}
backend = client.Name()
} else {
backend = constant.FirewallProviderIptables
}
}
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
continue
}
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
switch selection.subsystem {
case "system":
if backend == constant.FirewallProviderIptables {
err = iptables_helper.UnbindIPv6BaseChains()
} else {
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
if err == nil {
err = nftables_helper.PersistRuleset(t.TaskCtx)
}
}
case "forwarding":
var manager forwarding.Adapter
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
if err == nil {
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
}
case "docker":
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
}
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
return err
}
}
if err := t.TaskCtx.Err(); err != nil {
return err
}
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
})
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if _, err := lifecycle.NewClient(request.Backend); err != nil {
return err
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
if previous == "" {
if client, err := lifecycle.NewClient(""); err == nil {
previous = client.Name()
}
}
if request.Operation == "select" && previous != "" && previous != request.Backend {
initialized, err := systemFirewallBackendInitialized(previous)
if err != nil {
return err
}
if initialized {
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
return err
}
rollback := func(err error) error {
if err == nil {
return nil
}
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, previous)
return err
}
if request.Operation == "select" {
return nil
}
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
})
if initErr != nil {
return rollback(initErr)
}
return settingRepo.UpdateOrCreate(constant.FirewallFilterInitializedKey, constant.StatusEnable)
}
func systemFirewallBackendInitialized(backend string) (bool, error) {
client, err := lifecycle.NewClient(backend)
if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil
}
return false, err
}
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
}
return client.Status()
}
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
return err
}
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
detected, err := newForwardingAdapter(ctx)
if err != nil {
return err
}
current = detected.Name()
}
initialized, err := forwardingBackendInitialized(ctx, current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "initialize" {
return newForwardingService().Enable(ctx)
}
return nil
}
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
manager, err := newForwardingAdapterFor(ctx, backend)
if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil
}
return false, err
}
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
initialized, _, err := manager.FamilyStatus(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
}
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
if request.Operation == "select" {
current := previous
if current == "" {
current = constant.FirewallProviderNftables
if request.Backend == constant.FirewallProviderNftables {
current = constant.FirewallProviderIptables
}
}
initialized, err := dockerGuardBackendInitialized(ctx, current)
if err != nil {
return err
}
if current != request.Backend && initialized {
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
return err
}
if request.Operation == "select" {
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
if request.Operation == "initialize" {
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
return err
}
}
return nil
}
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
guard := newDockerFirewallRuntime(ctx, backend)
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
initialized, err := guard.Initialized(family)
if err != nil {
return false, err
}
if initialized {
return true, nil
}
}
return false, nil
}