mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
3070 lines
102 KiB
Go
3070 lines
102 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
|
|
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
|
|
"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"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type FirewallService struct {
|
|
rules repo.IFirewallRuleRepo
|
|
adapters firewallRuleRuntimeResolver
|
|
forwardingSync firewallDatabaseSyncAdapter
|
|
dockerSync firewallDatabaseSyncAdapter
|
|
selectedProvider func(context.Context) (filter.Provider, error)
|
|
requiredPorts func() ([]firewall.PortWhitelist, error)
|
|
iptablesHelper *iptables_helper.Manager
|
|
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)
|
|
}
|
|
|
|
type firewallRuleRuntimeResolver interface {
|
|
Resolve(filter.Provider) (*filterruntime.Engine, error)
|
|
Providers() []filter.Provider
|
|
}
|
|
|
|
var firewallRuleMutationMu sync.Mutex
|
|
|
|
type IFirewallService interface {
|
|
UpdatePanelPort(context.Context, uint, uint) error
|
|
LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error)
|
|
OperateFirewall(request dto.FirewallLifecycleOperation) error
|
|
OperateFilterChain(request dto.FilterChainOperation) error
|
|
QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error)
|
|
Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error)
|
|
Inventory(context.Context, dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error)
|
|
LoadFirewallNativeDetail(context.Context, dto.FirewallNativeDetail) (string, error)
|
|
Check(context.Context, string, dto.FirewallRuleCheck) (dto.FirewallRuleCheckResponse, error)
|
|
Create(context.Context, dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error)
|
|
Delete(context.Context, dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error)
|
|
Update(context.Context, string, dto.FirewallRuleUpdate) error
|
|
Reorder(context.Context, string, dto.FirewallRuleReorder) error
|
|
PreviewRuleSync(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
|
|
SyncRules(context.Context, string, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
|
|
CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error)
|
|
}
|
|
|
|
func NewIFirewallService() IFirewallService {
|
|
return newFirewallService()
|
|
}
|
|
|
|
func newFirewallService() *FirewallService {
|
|
return &FirewallService{
|
|
rules: repo.NewIFirewallRuleRepo(),
|
|
adapters: filterruntime.NewRegistry(firewallRuleSnapshotPolicy),
|
|
forwardingSync: newForwardingService(),
|
|
dockerSync: newDockerPortGuardService(),
|
|
selectedProvider: firewallRuleSelectedProvider,
|
|
requiredPorts: LoadRequiredFirewallPortWhiteList,
|
|
iptablesHelper: newIptablesHelperManager(),
|
|
cleanupBackend: cleanupSystemBackend,
|
|
cleanupInactiveBackend: cleanupInactiveSystemBackend,
|
|
resetBackend: resetServiceFirewallBackend,
|
|
dockerActive: func() (bool, error) {
|
|
return controller.CheckActive("docker")
|
|
},
|
|
restoreForwarding: func(ctx context.Context) error {
|
|
return newForwardingService().Restore(ctx)
|
|
},
|
|
restoreDockerGuard: ReconcileDockerPortGuard,
|
|
baseClient: selectedSystemFirewallClient,
|
|
}
|
|
}
|
|
|
|
func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) {
|
|
status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"}
|
|
if selected := configuredSystemFirewallBackend(); selected != "" {
|
|
status.Name, status.Backend = selected, selected
|
|
}
|
|
loadClient := s.baseClient
|
|
if loadClient == nil {
|
|
loadClient = selectedSystemFirewallClient
|
|
}
|
|
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
|
|
}
|
|
status.IsExist = true
|
|
status.Message = err.Error()
|
|
return status, nil
|
|
}
|
|
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, ping.LoadStatus()
|
|
status.IsActive = runtimeStatus.IsActive
|
|
if supportsManagedFilterChains(runtimeStatus.Name) {
|
|
initialized, bound, err := loadFirewallInitStatus(runtimeStatus.Name, chainGroup)
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
status.IsInit, status.IsBind = initialized, bound
|
|
status.IPv4 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv4)
|
|
status.IPv6 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv6)
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
type firewallLifecycleClient struct{ lifecycle.Client }
|
|
|
|
func (c firewallLifecycleClient) Start() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Start()
|
|
}
|
|
|
|
func (c firewallLifecycleClient) Stop() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Stop()
|
|
}
|
|
|
|
func (c firewallLifecycleClient) Restart() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Restart()
|
|
}
|
|
|
|
func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error {
|
|
switch request.Operation {
|
|
case "disableBanPing":
|
|
if err := ping.UpdateStatus("0"); err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusDisable)
|
|
case "enableBanPing":
|
|
if err := ping.UpdateStatus("1"); err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusEnable)
|
|
}
|
|
baseClient := s.baseClient
|
|
if baseClient == nil {
|
|
baseClient = selectedSystemFirewallClient
|
|
}
|
|
client, err := baseClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
operation := lifecycle.Operation(request.Operation)
|
|
operationErr := lifecycle.NewOperator(firewallLifecycleClient{client}).Operate(operation, request.WithDockerRestart, s.addPortsBeforeStart)
|
|
restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld &&
|
|
(operation == lifecycle.OperationStart || operation == lifecycle.OperationRestart)
|
|
if operation != lifecycle.OperationStart && operation != lifecycle.OperationRestart {
|
|
return operationErr
|
|
}
|
|
if operationErr != nil {
|
|
var completedErr *lifecycle.CompletedOperationError
|
|
var dockerRestartErr *lifecycle.DockerRestartError
|
|
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
|
|
}
|
|
ReconcileDockerPortGuardBestEffort(context.Background())
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error {
|
|
restoreForwarding := s.restoreForwarding
|
|
if restoreForwarding == nil {
|
|
restoreForwarding = func(ctx context.Context) error { return newForwardingService().Restore(ctx) }
|
|
}
|
|
restoreDockerGuard := s.restoreDockerGuard
|
|
if restoreDockerGuard == nil {
|
|
restoreDockerGuard = ReconcileDockerPortGuard
|
|
}
|
|
dockerActive := s.dockerActive
|
|
if dockerActive == nil {
|
|
dockerActive = func() (bool, error) { return controller.CheckActive("docker") }
|
|
}
|
|
|
|
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) OperateFilterChain(request dto.FilterChainOperation) error {
|
|
provider, err := selectedSystemFirewallProvider()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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()
|
|
if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider)); err != nil {
|
|
return err
|
|
}
|
|
return s.syncConfiguredFirewallPorts(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")
|
|
}
|
|
provider, err := selectedSystemFirewallProvider()
|
|
if err != nil {
|
|
return dto.FilterChainOperationResponse{}, err
|
|
}
|
|
if !supportsManagedFilterChains(provider) {
|
|
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
|
|
}
|
|
|
|
resourceName := fmt.Sprintf("%s filter", provider)
|
|
taskItem, err := task.NewTaskWithOps(resourceName, 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)
|
|
return s.operateFilterChainBase(provider, request)
|
|
}, nil)
|
|
taskItem.AddSubTask(i18n.GetWithName("FirewallRestoreRulesStep", provider), func(t *task.Task) error {
|
|
return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider))
|
|
}, nil)
|
|
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func(t *task.Task) error {
|
|
return s.syncConfiguredFirewallPorts(t.TaskCtx)
|
|
}, nil)
|
|
if err := repo.NewITaskRepo().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) operateFilterChainBase(provider string, request dto.FilterChainOperation) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return s.operateFilterChainBaseLocked(provider, request)
|
|
}
|
|
|
|
func (s *FirewallService) operateFilterChainBaseLocked(provider string, request dto.FilterChainOperation) error {
|
|
if err := s.checkSelectedProvider(context.Background(), filter.Provider(provider)); err != nil {
|
|
return err
|
|
}
|
|
if !supportsManagedFilterChains(provider) {
|
|
return fmt.Errorf("filter chain operations are not supported for %s", provider)
|
|
}
|
|
if provider == constant.FirewallProviderNftables {
|
|
if err := newNftablesHelperManager().Operate(firewall.BaseOperation(request.Operate)); err != nil {
|
|
return err
|
|
}
|
|
} else if err := s.iptablesHelper.Operate(firewall.BaseOperation(request.Operate)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error {
|
|
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
required, err := LoadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ports := excludeFirewallPorts(configured, required)
|
|
return s.SyncSystemPorts(ctx, nil, systemPorts(ports))
|
|
}
|
|
|
|
func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
|
|
provider := request.Provider
|
|
selected := provider
|
|
if provider == "" {
|
|
var err error
|
|
selected, err = s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleResetResponse{}, err
|
|
}
|
|
provider = selected
|
|
} else if isDirectFirewallProvider(provider) {
|
|
var err error
|
|
selected, err = s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleResetResponse{}, err
|
|
}
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleResetResponse{}, err
|
|
}
|
|
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
|
|
cleanup := s.cleanupBackend
|
|
if isDirectFirewallProvider(selected) && 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: len(stored), Disabled: true}, 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 = func() (bool, error) { return controller.CheckActive("docker") }
|
|
}
|
|
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 *lifecycle.DockerRestartError
|
|
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 = ReconcileDockerPortGuard
|
|
}
|
|
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: len(stored), Disabled: true}, nil
|
|
}
|
|
|
|
func isDirectFirewallProvider(provider filter.Provider) bool {
|
|
return provider == filter.ProviderIptables || provider == filter.ProviderNftables
|
|
}
|
|
|
|
func resetServiceFirewallBackend(provider string, withDockerRestart bool) error {
|
|
client, err := lifecycle.NewClientFor(provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return resetServiceFirewallClient(client, withDockerRestart, func(
|
|
client lifecycle.Client,
|
|
restartDocker bool,
|
|
prepareStop func() error,
|
|
) error {
|
|
return lifecycle.NewOperator(client).StopWithPrepare(restartDocker, prepareStop)
|
|
})
|
|
}
|
|
|
|
func resetServiceFirewallClient(
|
|
client lifecycle.Client,
|
|
withDockerRestart bool,
|
|
stop func(lifecycle.Client, bool, func() error) error,
|
|
) error {
|
|
resetter, ok := client.(lifecycle.Resetter)
|
|
if !ok {
|
|
return fmt.Errorf("firewall provider %s does not support reset", client.Name())
|
|
}
|
|
if resetBeforeStop, ok := client.(lifecycle.PreStopResetter); ok {
|
|
if err := stop(client, withDockerRestart, resetBeforeStop.ResetBeforeStop); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return resetter.Reset()
|
|
}
|
|
|
|
func restoreFirewalldDependents(
|
|
ctx context.Context,
|
|
reason string,
|
|
restoreDocker bool,
|
|
restoreForwarding func(context.Context) error,
|
|
restoreDockerGuard func(context.Context) error,
|
|
) error {
|
|
var errs []error
|
|
if err := restoreForwarding(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("restore port forwarding %s: %w", reason, err))
|
|
}
|
|
if restoreDocker {
|
|
if err := restoreDockerGuard(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("restore Docker port guard %s: %w", reason, err))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) {
|
|
requestedScopes := request.Scopes
|
|
if len(requestedScopes) == 0 && request.Scope.Provider != "" {
|
|
requestedScopes = []filter.Scope{request.Scope}
|
|
}
|
|
if len(requestedScopes) == 0 {
|
|
return dto.FirewallRuleInventoryResponse{}, filter.ErrInvalidScope
|
|
}
|
|
scopes := make([]filter.Scope, len(requestedScopes))
|
|
for index, requested := range requestedScopes {
|
|
scopes[index] = requested.Normalize()
|
|
}
|
|
if len(scopes) == 1 && isCombinedUFWInventoryScope(scopes[0]) {
|
|
scope := scopes[0]
|
|
if err := s.checkSelectedProvider(ctx, scope.Provider); err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
runtime, err := s.adapters.Resolve(scope.Provider)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
response, err := s.combinedUFWInventory(ctx, runtime, scope)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
return finalizeFirewallInventory(response, request), nil
|
|
}
|
|
provider := scopes[0].Provider
|
|
for _, scope := range scopes {
|
|
if err := scope.ValidateMVP(); err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
if scope.Provider != provider {
|
|
return dto.FirewallRuleInventoryResponse{}, fmt.Errorf(
|
|
"%w: inventory scopes must use the same provider", filter.ErrInvalidScope,
|
|
)
|
|
}
|
|
}
|
|
if err := s.checkSelectedProvider(ctx, provider); err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
runtime, err := s.adapters.Resolve(provider)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, provider)
|
|
response := dto.FirewallRuleInventoryResponse{Items: failures}
|
|
unavailable := make(map[filter.Family]error)
|
|
for _, scope := range scopes {
|
|
var snapshot filter.Snapshot
|
|
err := unavailable[scope.Family]
|
|
if err == nil {
|
|
snapshot, err = runtime.Observe(ctx, scope)
|
|
}
|
|
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
if unavailable[scope.Family] == nil {
|
|
response.Notices = append(response.Notices, filter.ScopeNotice{Code: filter.ScopeNoticeFamilyUnavailable, Values: []string{string(scope.Family), err.Error()}})
|
|
unavailable[scope.Family] = err
|
|
}
|
|
for _, desired := range desiredByScope[scope.Key()] {
|
|
response.Items = append(response.Items, filter.InventoryItem{
|
|
Rule: desired.Rule, Desired: &desired, State: filter.InventoryStateDrifted,
|
|
Match: filter.InventoryMatchNone, Error: err.Error(),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
desired := desiredByScope[scope.Key()]
|
|
items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
response.Items = append(response.Items, items...)
|
|
response.Notices = append(response.Notices, snapshot.Notices...)
|
|
}
|
|
return finalizeFirewallInventory(response, request), nil
|
|
}
|
|
|
|
func finalizeFirewallInventory(
|
|
response dto.FirewallRuleInventoryResponse,
|
|
request dto.FirewallRuleInventory,
|
|
) dto.FirewallRuleInventoryResponse {
|
|
provider := request.Scope.Provider
|
|
if len(request.Scopes) > 0 {
|
|
provider = request.Scopes[0].Provider
|
|
}
|
|
response.IPv4Range, response.IPv6Range = filter.InventoryPositionRanges(provider, response.Items)
|
|
response.AllTotal = int64(len(response.Items))
|
|
for _, item := range response.Items {
|
|
if isDeletableManagedInventoryItem(item) {
|
|
response.ManagedTotal++
|
|
}
|
|
}
|
|
filtered := make([]filter.InventoryItem, 0, len(response.Items))
|
|
for _, item := range response.Items {
|
|
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 = filtered[start:end]
|
|
return response
|
|
}
|
|
|
|
func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.FirewallRuleInventory) bool {
|
|
if slicesContains(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
|
|
}
|
|
if len(request.States) > 0 && !slicesContains(request.States, item.State) {
|
|
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), string(item.State),
|
|
}
|
|
if item.Observed != nil {
|
|
values = append(values, item.Observed.Rule.Description)
|
|
}
|
|
if item.Desired != nil {
|
|
values = append(values, item.Desired.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.ActionAccept {
|
|
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 slicesContains[T comparable](values []T, target T) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isDeletableManagedInventoryItem(item filter.InventoryItem) bool {
|
|
if item.Desired == nil || item.Desired.Protected || item.State == filter.InventoryStateProtected {
|
|
return false
|
|
}
|
|
if item.Desired.Origin != filter.RuleOriginCreated && item.Desired.Origin != filter.RuleOriginAdopted {
|
|
return false
|
|
}
|
|
if isIptablesSystemPresetInventoryScope(item.Rule.Scope) {
|
|
return false
|
|
}
|
|
return item.State != filter.InventoryStateDrifted ||
|
|
(item.Match == filter.InventoryMatchMissing && item.Observed == nil)
|
|
}
|
|
|
|
func isIptablesSystemPresetInventoryScope(scope filter.Scope) bool {
|
|
return (scope.Provider == filter.ProviderIptables || scope.Provider == filter.ProviderNftables) &&
|
|
(scope.Chain == filter.BasicBeforeChain || scope.Chain == filter.BasicAfterChain)
|
|
}
|
|
|
|
func isCombinedUFWInventoryScope(scope filter.Scope) bool {
|
|
scope = scope.Normalize()
|
|
return scope.Provider == filter.ProviderUFW && scope.Family == filter.FamilyInet && scope.Table == "" &&
|
|
scope.Zone == "" && scope.Chain == filter.UFWInputChain && scope.Direction == filter.DirectionInput
|
|
}
|
|
|
|
func (s *FirewallService) combinedUFWInventory(
|
|
ctx context.Context,
|
|
runtime *filterruntime.Engine,
|
|
scope filter.Scope,
|
|
) (dto.FirewallRuleInventoryResponse, error) {
|
|
scopes := []filter.Scope{scope, scope}
|
|
scopes[0].Family = filter.FamilyIPv4
|
|
scopes[1].Family = filter.FamilyIPv6
|
|
snapshots, err := runtime.ObserveScopes(ctx, scopes)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
if len(snapshots) != len(scopes) {
|
|
return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: incomplete UFW multi-family inventory", filter.ErrAdapterUnavailable)
|
|
}
|
|
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
desiredByScope, failures := s.desiredFirewallRulesByScope(ctx, stored, scope.Provider)
|
|
response := dto.FirewallRuleInventoryResponse{Items: failures}
|
|
seenNotices := make(map[string]struct{})
|
|
for index, snapshot := range snapshots {
|
|
if snapshot.Scope.Key() != scopes[index].Key() {
|
|
return dto.FirewallRuleInventoryResponse{}, fmt.Errorf("%w: unexpected UFW inventory scope %q", filter.ErrInvalidScope, snapshot.Scope.Key())
|
|
}
|
|
desired := desiredByScope[snapshot.Scope.Key()]
|
|
items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
response.Items = append(response.Items, items...)
|
|
for _, notice := range snapshot.Notices {
|
|
key := string(notice.Code) + "\x00" + strings.Join(notice.Values, "\x00")
|
|
if _, exists := seenNotices[key]; exists {
|
|
continue
|
|
}
|
|
seenNotices[key] = struct{}{}
|
|
response.Notices = append(response.Notices, notice)
|
|
}
|
|
}
|
|
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.adapters.Resolve(provider)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return runtime.NativeDetail(ctx, request.Name, request.Permanent)
|
|
}
|
|
|
|
func (s *FirewallService) checkUpdate(
|
|
ctx context.Context,
|
|
clientIP string,
|
|
ruleUUID string,
|
|
requestedRule filter.FirewallRule,
|
|
) (dto.FirewallRuleCheckResult, error) {
|
|
prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResult{}, err
|
|
}
|
|
semantic, err := model.FirewallRuleFromDomain(prepared.After)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResult{}, err
|
|
}
|
|
if err := s.ensureFirewallRuleIdentityAvailable(ctx, semantic, prepared.Stored.UUID); err != nil {
|
|
return dto.FirewallRuleCheckResult{}, err
|
|
}
|
|
return dto.FirewallRuleCheckResult{
|
|
Decision: filter.CheckDecisionReady,
|
|
Classification: filter.CheckClassificationNone,
|
|
Reason: "update_ready",
|
|
RequestedRule: prepared.After,
|
|
RequestedRuleKey: semantic.PolicyKey(),
|
|
}, nil
|
|
}
|
|
|
|
func (s *FirewallService) Check(
|
|
ctx context.Context,
|
|
clientIP string,
|
|
request dto.FirewallRuleCheck,
|
|
) (dto.FirewallRuleCheckResponse, error) {
|
|
response := dto.FirewallRuleCheckResponse{Items: make([]dto.FirewallRuleCheckResult, 0, len(request.Items))}
|
|
var selectedProvider filter.Provider
|
|
createStateLoaded := false
|
|
var desiredByScope map[string][]filter.DesiredRule
|
|
var incompatiblePolicies []filter.InventoryItem
|
|
var managedRevision string
|
|
type checkState struct {
|
|
snapshot filter.Snapshot
|
|
desired []filter.DesiredRule
|
|
managedRevision string
|
|
}
|
|
states := make(map[string]checkState)
|
|
pending := make([]filter.FirewallRule, 0, len(request.Items))
|
|
for _, item := range request.Items {
|
|
if ruleUUID := strings.TrimSpace(item.UUID); ruleUUID != "" {
|
|
result, updateErr := s.checkUpdate(ctx, clientIP, ruleUUID, item.Rule)
|
|
if updateErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, updateErr
|
|
}
|
|
response.Items = append(response.Items, result)
|
|
continue
|
|
}
|
|
if !createStateLoaded {
|
|
var err error
|
|
selectedProvider, err = s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
createStateLoaded = true
|
|
}
|
|
item.Rule = applySelectedProviderScopeDefaults(item.Rule, selectedProvider)
|
|
rule, err := filter.NormalizeRule(item.Rule)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
if rule.Scope.Provider != selectedProvider {
|
|
return dto.FirewallRuleCheckResponse{}, fmt.Errorf(
|
|
"%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selectedProvider, rule.Scope.Provider,
|
|
)
|
|
}
|
|
runtime, err := s.adapters.Resolve(rule.Scope.Provider)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
rule, err = runtime.Prepare(rule)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
if err = runtime.CheckRule(ctx, rule); err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
|
|
scopeKey := rule.Scope.Key()
|
|
state, exists := states[scopeKey]
|
|
if !exists {
|
|
snapshot, observeErr := runtime.ObserveMutation(ctx, rule.Scope)
|
|
if observeErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, observeErr
|
|
}
|
|
if desiredByScope == nil {
|
|
stored, listErr := s.rules.List(ctx)
|
|
if listErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, listErr
|
|
}
|
|
desiredByScope, incompatiblePolicies = s.desiredFirewallRulesByScope(ctx, stored, selectedProvider)
|
|
for _, failure := range incompatiblePolicies {
|
|
if !failure.Incompatible {
|
|
return dto.FirewallRuleCheckResponse{}, errors.New(failure.Error)
|
|
}
|
|
}
|
|
managedRevision, err = model.FirewallRulesRevision(stored)
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
}
|
|
for index := range snapshot.Rules {
|
|
for _, failure := range incompatiblePolicies {
|
|
marker := "1panel-rule:" + failure.Desired.UUID
|
|
if snapshot.Rules[index].Marker == marker || strings.HasPrefix(snapshot.Rules[index].Marker, marker+"-") {
|
|
snapshot.Rules[index].Protected = true
|
|
}
|
|
}
|
|
}
|
|
state = checkState{snapshot: snapshot, desired: desiredByScope[scopeKey], managedRevision: managedRevision}
|
|
states[scopeKey] = state
|
|
}
|
|
checked, checkErr := filter.CheckCreate(state.snapshot, rule, state.desired)
|
|
if checkErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, checkErr
|
|
}
|
|
if checked.Decision == filter.CheckDecisionReady {
|
|
for _, previous := range pending {
|
|
if collision := filter.CheckRuleCollision(rule, previous); collision != nil {
|
|
if errors.Is(collision, filter.ErrRuleConflict) {
|
|
checked.Decision, checked.Classification, checked.Reason = filter.CheckDecisionBlocked, filter.CheckClassificationConflict, "exact_rule_conflict"
|
|
} else if errors.Is(collision, filter.ErrRuleOperation) {
|
|
checked.Decision, checked.Classification, checked.Reason = filter.CheckDecisionNoChange, filter.CheckClassificationExactManaged, "equivalent_batch_rule"
|
|
} else {
|
|
return dto.FirewallRuleCheckResponse{}, collision
|
|
}
|
|
checked.AllowedActions = nil
|
|
break
|
|
}
|
|
}
|
|
if checked.Decision == filter.CheckDecisionReady {
|
|
pending = append(pending, rule)
|
|
}
|
|
}
|
|
checkFlag, signErr := signFirewallRuleCheck(checked, state.snapshot, state.managedRevision)
|
|
if signErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, signErr
|
|
}
|
|
result := dto.FirewallRuleCheckResult{
|
|
Decision: checked.Decision, Classification: checked.Classification, Reason: checked.Reason,
|
|
RequestedRule: checked.RequestedRule, RequestedRuleKey: checked.RequestedRuleKey,
|
|
ExistingRuleUUID: checked.ExistingRuleUUID, Candidates: checked.Candidates,
|
|
AllowedActions: checked.AllowedActions, CheckFlag: checkFlag,
|
|
}
|
|
response.Items = append(response.Items, result)
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filter.Provider) filter.FirewallRule {
|
|
scope := rule.Scope
|
|
if scope.Provider == "" {
|
|
scope.Provider = selected
|
|
}
|
|
if scope.Provider != selected {
|
|
return rule
|
|
}
|
|
if scope.Direction == "" {
|
|
scope.Direction = filter.DirectionInput
|
|
}
|
|
if scope.Family == "" {
|
|
scope.Family = defaultFirewallRuleFamily(rule, selected)
|
|
}
|
|
switch selected {
|
|
case filter.ProviderIptables, filter.ProviderNftables:
|
|
if scope.Table == "" {
|
|
scope.Table = "filter"
|
|
}
|
|
if scope.Chain == "" {
|
|
scope.Chain = filter.IptablesInputChain
|
|
}
|
|
case filter.ProviderFirewalld:
|
|
if scope.Zone == "" {
|
|
scope.Zone = filter.FirewalldInputZone
|
|
}
|
|
case filter.ProviderUFW:
|
|
if scope.Chain == "" {
|
|
scope.Chain = filter.UFWInputChain
|
|
}
|
|
}
|
|
rule.Scope = scope
|
|
return rule
|
|
}
|
|
|
|
func defaultFirewallRuleFamily(rule filter.FirewallRule, provider filter.Provider) filter.Family {
|
|
if strings.EqualFold(strings.TrimSpace(rule.Protocol), "icmpv6") ||
|
|
strings.Contains(rule.SourceAddress, ":") || strings.Contains(rule.DestinationAddress, ":") {
|
|
return filter.FamilyIPv6
|
|
}
|
|
if provider == filter.ProviderFirewalld {
|
|
return filter.FamilyInet
|
|
}
|
|
return filter.FamilyIPv4
|
|
}
|
|
|
|
func (s *FirewallService) checkRule(
|
|
ctx context.Context,
|
|
clientIP string,
|
|
item dto.FirewallRuleCheckItem,
|
|
) (dto.FirewallRuleCheckResult, error) {
|
|
response, err := s.Check(ctx, clientIP, dto.FirewallRuleCheck{Items: []dto.FirewallRuleCheckItem{item}})
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResult{}, err
|
|
}
|
|
if len(response.Items) != 1 {
|
|
return dto.FirewallRuleCheckResult{}, errors.New("firewall rule check returned no result")
|
|
}
|
|
return response.Items[0], nil
|
|
}
|
|
|
|
type preparedFirewallRuleCreate struct {
|
|
request dto.FirewallRuleCreateItem
|
|
runtime *filterruntime.Engine
|
|
snapshot filter.Snapshot
|
|
authorization firewallRuleCreateAuthorization
|
|
}
|
|
|
|
func (s *FirewallService) Create(
|
|
ctx context.Context,
|
|
request dto.FirewallRuleCreate,
|
|
) (dto.FirewallRuleCreateResponse, error) {
|
|
result, _ := s.create(ctx, request)
|
|
return result, nil
|
|
}
|
|
|
|
func (s *FirewallService) create(
|
|
ctx context.Context,
|
|
request dto.FirewallRuleCreate,
|
|
) (dto.FirewallRuleCreateResponse, error) {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
|
|
return s.createLocked(ctx, request)
|
|
}
|
|
|
|
func (s *FirewallService) createLocked(
|
|
ctx context.Context,
|
|
request dto.FirewallRuleCreate,
|
|
) (dto.FirewallRuleCreateResponse, error) {
|
|
prepared, failedIndex, err := s.prepareCreate(ctx, request.Items)
|
|
if err != nil {
|
|
return firewallCreatePrepareFailure(request.Items, failedIndex, err), err
|
|
}
|
|
|
|
result := dto.FirewallRuleCreateResponse{}
|
|
var createErr error
|
|
for index := 0; index < len(prepared); {
|
|
batchEnd := nativeCreateBatchEnd(prepared, index)
|
|
if batchEnd-index > 1 {
|
|
if err := s.createNativeRuleBatch(ctx, prepared[index:batchEnd]); err != nil {
|
|
failed := firewallCreateExecutionFailure(request.Items, index, err)
|
|
result.Failed += failed.Failed
|
|
result.Skipped += failed.Skipped
|
|
result.Errors = append(result.Errors, failed.Errors...)
|
|
if global.LOG != nil {
|
|
global.LOG.Errorf("batch create firewall rules %d-%d failed: %v", index+1, batchEnd, err)
|
|
}
|
|
createErr = err
|
|
break
|
|
}
|
|
result.Succeeded += batchEnd - index
|
|
index = batchEnd
|
|
continue
|
|
}
|
|
|
|
entry := prepared[index]
|
|
snapshot, err := entry.runtime.ObserveMutation(ctx, entry.request.Rule.Scope)
|
|
if err == nil {
|
|
entry.authorization, err = refreshCreateAuthorization(snapshot, entry)
|
|
}
|
|
if err == nil {
|
|
err = s.createRule(ctx, entry.runtime, snapshot, entry.request, entry.authorization)
|
|
}
|
|
if err != nil {
|
|
failed := firewallCreateExecutionFailure(request.Items, index, err)
|
|
result.Failed += failed.Failed
|
|
result.Skipped += failed.Skipped
|
|
result.Errors = append(result.Errors, failed.Errors...)
|
|
if global.LOG != nil {
|
|
global.LOG.Errorf("batch create firewall rule item %d failed: %v", index+1, err)
|
|
}
|
|
createErr = err
|
|
break
|
|
}
|
|
result.Succeeded++
|
|
index++
|
|
}
|
|
return result, createErr
|
|
}
|
|
|
|
func (s *FirewallService) prepareCreate(
|
|
ctx context.Context,
|
|
items []dto.FirewallRuleCreateItem,
|
|
) ([]preparedFirewallRuleCreate, int, error) {
|
|
type prepareState struct {
|
|
runtime *filterruntime.Engine
|
|
snapshot filter.Snapshot
|
|
managedRevision string
|
|
}
|
|
states := make(map[string]prepareState)
|
|
prepared := make([]preparedFirewallRuleCreate, 0, len(items))
|
|
selectedProvider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for index, request := range items {
|
|
if request.CheckFlag == "" {
|
|
return nil, index, filter.ErrRuleCheckRequired
|
|
}
|
|
rule, err := filter.NormalizeRule(request.Rule)
|
|
if err != nil {
|
|
return nil, index, err
|
|
}
|
|
if rule.Scope.Provider != selectedProvider {
|
|
return nil, index, fmt.Errorf(
|
|
"%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selectedProvider, rule.Scope.Provider,
|
|
)
|
|
}
|
|
runtime, err := s.adapters.Resolve(rule.Scope.Provider)
|
|
if err != nil {
|
|
return nil, index, err
|
|
}
|
|
rule, err = runtime.Prepare(rule)
|
|
if err != nil {
|
|
return nil, index, err
|
|
}
|
|
if err = runtime.CheckRule(ctx, rule); err != nil {
|
|
return nil, index, err
|
|
}
|
|
|
|
scopeKey := rule.Scope.Key()
|
|
state, exists := states[scopeKey]
|
|
if !exists {
|
|
snapshot, observeErr := runtime.ObserveMutation(ctx, rule.Scope)
|
|
if observeErr != nil {
|
|
return nil, index, observeErr
|
|
}
|
|
stored, listErr := s.rules.List(ctx)
|
|
if listErr != nil {
|
|
return nil, index, listErr
|
|
}
|
|
managedRevision, revisionErr := model.FirewallRulesRevision(stored)
|
|
if revisionErr != nil {
|
|
return nil, index, revisionErr
|
|
}
|
|
state = prepareState{runtime: runtime, snapshot: snapshot, managedRevision: managedRevision}
|
|
states[scopeKey] = state
|
|
}
|
|
authorization, authorizeErr := authorizeFirewallRuleCreate(
|
|
request.CheckFlag, request.Action, request.AdoptInstanceKey, rule, state.snapshot, state.managedRevision,
|
|
)
|
|
if authorizeErr != nil {
|
|
return nil, index, authorizeErr
|
|
}
|
|
sourceKind := request.SourceKind
|
|
if sourceKind == "" {
|
|
sourceKind = constant.FirewallRuleSourceUser
|
|
}
|
|
for _, previous := range prepared {
|
|
if err := filter.CheckRuleCollision(rule, previous.request.Rule); err != nil {
|
|
return nil, index, err
|
|
}
|
|
}
|
|
request.Rule = rule
|
|
request.SourceKind = sourceKind
|
|
prepared = append(prepared, preparedFirewallRuleCreate{
|
|
request: request, runtime: state.runtime, snapshot: state.snapshot, authorization: authorization,
|
|
})
|
|
}
|
|
return prepared, -1, nil
|
|
}
|
|
|
|
func nativeCreateBatchEnd(prepared []preparedFirewallRuleCreate, start int) int {
|
|
if start < 0 || start >= len(prepared) {
|
|
return start
|
|
}
|
|
first := prepared[start]
|
|
if first.runtime == nil || !supportsNativeRuleBatch(first.runtime.Provider()) ||
|
|
first.authorization.Operation != filter.ChangeCreate || first.request.Rule.OrderIndex != nil {
|
|
return start + 1
|
|
}
|
|
scopeKey := first.request.Rule.Scope.Key()
|
|
seen := make(map[string]struct{}, len(prepared)-start)
|
|
for index := start; index < len(prepared); index++ {
|
|
entry := prepared[index]
|
|
if entry.runtime != first.runtime || entry.authorization.Operation != filter.ChangeCreate ||
|
|
entry.request.Rule.Scope.Key() != scopeKey || entry.request.Rule.OrderIndex != nil {
|
|
return index
|
|
}
|
|
ruleKey, err := filter.RuleKey(entry.request.Rule)
|
|
if err != nil {
|
|
return index
|
|
}
|
|
if _, exists := seen[ruleKey]; exists {
|
|
return index
|
|
}
|
|
seen[ruleKey] = struct{}{}
|
|
}
|
|
return len(prepared)
|
|
}
|
|
|
|
type createdFirewallBatchRule struct {
|
|
record model.FirewallRule
|
|
rule filter.FirewallRule
|
|
}
|
|
|
|
func (s *FirewallService) createNativeRuleBatch(ctx context.Context, prepared []preparedFirewallRuleCreate) error {
|
|
if len(prepared) < 2 {
|
|
return fmt.Errorf("%w: native firewall batch requires at least two rules", filter.ErrInvalidRule)
|
|
}
|
|
runtime := prepared[0].runtime
|
|
snapshot, err := runtime.ObserveMutation(ctx, prepared[0].request.Rule.Scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
identities := make(firewallRuleIdentityIndex, len(stored)+len(prepared))
|
|
var maximumSequence int64
|
|
for _, record := range stored {
|
|
identities.add(record)
|
|
if record.Sequence != nil && *record.Sequence > maximumSequence {
|
|
maximumSequence = *record.Sequence
|
|
}
|
|
}
|
|
nextSequence := maximumSequence + model.FirewallRuleSequenceStep
|
|
created := make([]createdFirewallBatchRule, 0, len(prepared))
|
|
for _, entry := range prepared {
|
|
domainRule := entry.request.Rule
|
|
record, recordErr := firewallRuleModelForCreate(domainRule, entry.request, constant.FirewallRuleOriginCreated)
|
|
if recordErr != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, recordErr)
|
|
}
|
|
if domainRule.Scope.Provider != filter.ProviderFirewalld {
|
|
sequence := nextSequence
|
|
record.Sequence = &sequence
|
|
nextSequence += model.FirewallRuleSequenceStep
|
|
}
|
|
if err := identities.check(record); err != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, err)
|
|
}
|
|
if err := filter.CheckObservedRuleCollisions(snapshot, domainRule, nil); err != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, err)
|
|
}
|
|
if recordErr = s.rules.Create(ctx, &record); recordErr != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, recordErr)
|
|
}
|
|
identities.add(record)
|
|
domainRule.UUID = record.UUID
|
|
created = append(created, createdFirewallBatchRule{record: record, rule: domainRule})
|
|
}
|
|
|
|
changes := make([]filter.DesiredChange, 0, len(created))
|
|
for index := range created {
|
|
changes = append(changes, filter.DesiredChange{Operation: filter.ChangeCreate, After: &created[index].rule})
|
|
}
|
|
backendPlan, verification, err := runtime.Execute(ctx, snapshot, changes)
|
|
if err != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, err)
|
|
}
|
|
if !verification.Matched {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, filter.ErrVerificationFailed)
|
|
}
|
|
|
|
for index := range created {
|
|
_, commitErr := findBatchCommittedObserved(verification.Snapshot, created[index].rule.UUID)
|
|
if commitErr != nil {
|
|
commitErr = rollbackFirewallPlan(ctx, runtime, backendPlan, commitErr)
|
|
return s.cleanupFirewallBatchRecords(ctx, created, commitErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findBatchCommittedObserved(snapshot filter.Snapshot, ruleUUID string) (filter.ObservedRule, error) {
|
|
marker := "1panel-rule:" + ruleUUID
|
|
matches := make([]filter.ObservedRule, 0, 1)
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.Marker == marker {
|
|
matches = append(matches, observed)
|
|
}
|
|
}
|
|
if len(matches) != 1 {
|
|
return filter.ObservedRule{}, fmt.Errorf("%w: expected one committed batch rule, found %d", filter.ErrVerificationFailed, len(matches))
|
|
}
|
|
return matches[0], nil
|
|
}
|
|
|
|
func (s *FirewallService) cleanupFirewallBatchRecords(
|
|
ctx context.Context,
|
|
created []createdFirewallBatchRule,
|
|
cause error,
|
|
) error {
|
|
cleanupErrors := make([]error, 0)
|
|
for index := len(created) - 1; index >= 0; index-- {
|
|
if err := s.rules.DeleteWithRevision(ctx, created[index].record.UUID, created[index].record.Revision); err != nil {
|
|
cleanupErrors = append(cleanupErrors, fmt.Errorf("cleanup failed firewall rule %q: %w", created[index].record.UUID, err))
|
|
}
|
|
}
|
|
if len(cleanupErrors) == 0 {
|
|
return cause
|
|
}
|
|
return errors.Join(append([]error{cause}, cleanupErrors...)...)
|
|
}
|
|
|
|
func firewallCreatePrepareFailure(
|
|
items []dto.FirewallRuleCreateItem,
|
|
failedIndex int,
|
|
cause error,
|
|
) dto.FirewallRuleCreateResponse {
|
|
result := dto.FirewallRuleCreateResponse{
|
|
Failed: 1, Skipped: len(items) - 1,
|
|
Errors: make([]dto.FirewallRuleCreateFailure, 0, len(items)),
|
|
}
|
|
for index := range items {
|
|
failure := dto.FirewallRuleCreateFailure{
|
|
Index: index, Status: "skipped", Rule: items[index].Rule,
|
|
}
|
|
if index == failedIndex {
|
|
failure.Status = "failed"
|
|
failure.Error = cause.Error()
|
|
}
|
|
result.Errors = append(result.Errors, failure)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func firewallCreateExecutionFailure(
|
|
items []dto.FirewallRuleCreateItem,
|
|
failedIndex int,
|
|
cause error,
|
|
) dto.FirewallRuleCreateResponse {
|
|
result := dto.FirewallRuleCreateResponse{
|
|
Failed: 1, Skipped: len(items) - failedIndex - 1,
|
|
Errors: make([]dto.FirewallRuleCreateFailure, 0, len(items)-failedIndex),
|
|
}
|
|
for index := failedIndex; index < len(items); index++ {
|
|
failure := dto.FirewallRuleCreateFailure{
|
|
Index: index, Status: "skipped", Rule: items[index].Rule,
|
|
}
|
|
if index == failedIndex {
|
|
failure.Status = "failed"
|
|
failure.Error = cause.Error()
|
|
}
|
|
result.Errors = append(result.Errors, failure)
|
|
}
|
|
return result
|
|
}
|
|
|
|
type preparedFirewallRuleDelete struct {
|
|
index int
|
|
stored model.FirewallRule
|
|
desired filter.DesiredRule
|
|
runtime *filterruntime.Engine
|
|
compiled int
|
|
}
|
|
|
|
func (s *FirewallService) Delete(
|
|
ctx context.Context,
|
|
request dto.FirewallRuleDelete,
|
|
) (dto.FirewallRuleDeleteResponse, error) {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
|
|
result := dto.FirewallRuleDeleteResponse{}
|
|
selectedProvider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleDeleteResponse{}, err
|
|
}
|
|
type deleteGroup struct {
|
|
items []preparedFirewallRuleDelete
|
|
}
|
|
groups := make([]deleteGroup, 0)
|
|
groupIndexes := make(map[string]int)
|
|
seen := make(map[string]struct{}, len(request.UUIDs))
|
|
for index, value := range request.UUIDs {
|
|
ruleUUID := strings.TrimSpace(value)
|
|
if _, exists := seen[ruleUUID]; exists {
|
|
result.Failed++
|
|
result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{
|
|
Index: index, UUID: ruleUUID, Error: "duplicate firewall rule UUID",
|
|
})
|
|
continue
|
|
}
|
|
seen[ruleUUID] = struct{}{}
|
|
prepared, err := s.prepareDelete(ctx, index, ruleUUID, selectedProvider)
|
|
if err != nil {
|
|
result.Failed++
|
|
result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{Index: index, UUID: ruleUUID, Error: err.Error()})
|
|
continue
|
|
}
|
|
groupKey := string(prepared.desired.Rule.Scope.Provider) + ":" + prepared.desired.Rule.Scope.Key()
|
|
if prepared.compiled != 1 {
|
|
groupKey += ":" + prepared.stored.UUID
|
|
}
|
|
groupIndex, exists := groupIndexes[groupKey]
|
|
if !exists {
|
|
groupIndex = len(groups)
|
|
groupIndexes[groupKey] = groupIndex
|
|
groups = append(groups, deleteGroup{})
|
|
}
|
|
groups[groupIndex].items = append(groups[groupIndex].items, prepared)
|
|
}
|
|
|
|
for _, group := range groups {
|
|
if len(group.items) > 1 && group.items[0].compiled == 1 && supportsNativeRuleBatch(group.items[0].desired.Rule.Scope.Provider) {
|
|
if err := s.deleteNativeRuleBatch(ctx, group.items); err != nil {
|
|
result.Failed += len(group.items)
|
|
for _, item := range group.items {
|
|
result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{
|
|
Index: item.index, UUID: item.stored.UUID, Error: err.Error(),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
result.Succeeded += len(group.items)
|
|
continue
|
|
}
|
|
for _, item := range group.items {
|
|
if err := s.deleteRule(ctx, item.stored.UUID, false); err != nil {
|
|
result.Failed++
|
|
result.Errors = append(result.Errors, dto.FirewallRuleDeleteFailure{
|
|
Index: item.index, UUID: item.stored.UUID, Error: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
result.Succeeded++
|
|
}
|
|
}
|
|
sort.SliceStable(result.Errors, func(i, j int) bool { return result.Errors[i].Index < result.Errors[j].Index })
|
|
return result, nil
|
|
}
|
|
|
|
func (s *FirewallService) prepareDelete(
|
|
ctx context.Context,
|
|
index int,
|
|
ruleUUID string,
|
|
selectedProvider filter.Provider,
|
|
) (preparedFirewallRuleDelete, error) {
|
|
if ruleUUID == "" {
|
|
return preparedFirewallRuleDelete{}, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
|
|
}
|
|
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return preparedFirewallRuleDelete{}, fmt.Errorf("%w: managed rule %q was not found", filter.ErrInvalidRule, ruleUUID)
|
|
}
|
|
return preparedFirewallRuleDelete{}, err
|
|
}
|
|
if isProtectedSystemFirewallRule(stored) {
|
|
return preparedFirewallRuleDelete{}, filter.ErrProtectedRule
|
|
}
|
|
if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted {
|
|
return preparedFirewallRuleDelete{}, fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule)
|
|
}
|
|
desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selectedProvider)
|
|
if err != nil {
|
|
return preparedFirewallRuleDelete{}, err
|
|
}
|
|
if len(desiredRules) == 0 {
|
|
return preparedFirewallRuleDelete{}, fmt.Errorf("%w: policy %q has no compiled target rules", filter.ErrInvalidRule, ruleUUID)
|
|
}
|
|
desired := desiredRules[0]
|
|
runtime, err := s.adapters.Resolve(desired.Rule.Scope.Provider)
|
|
if err != nil {
|
|
return preparedFirewallRuleDelete{}, err
|
|
}
|
|
return preparedFirewallRuleDelete{
|
|
index: index, stored: stored, desired: desired, runtime: runtime, compiled: len(desiredRules),
|
|
}, nil
|
|
}
|
|
|
|
func (s *FirewallService) deleteNativeRuleBatch(ctx context.Context, prepared []preparedFirewallRuleDelete) error {
|
|
runtime := prepared[0].runtime
|
|
snapshot, err := runtime.ObserveMutation(ctx, prepared[0].desired.Rule.Scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
type positionedDelete struct {
|
|
position int
|
|
change filter.DesiredChange
|
|
}
|
|
positioned := make([]positionedDelete, 0, len(prepared))
|
|
for _, item := range prepared {
|
|
observed, observeErr := filter.ManagedObserved(snapshot, item.desired)
|
|
if observeErr != nil {
|
|
if errors.Is(observeErr, filter.ErrRuleStale) {
|
|
missing, mergeErr := managedFirewallRuleMissing(snapshot, item.desired)
|
|
if mergeErr != nil {
|
|
return mergeErr
|
|
}
|
|
if missing {
|
|
continue
|
|
}
|
|
}
|
|
return observeErr
|
|
}
|
|
if observed.Locator.Position == nil {
|
|
return fmt.Errorf("%w: managed native firewall rule has no position", filter.ErrRuleStale)
|
|
}
|
|
before := item.desired.Rule
|
|
locator := observed.Locator
|
|
positioned = append(positioned, positionedDelete{
|
|
position: *observed.Locator.Position,
|
|
change: filter.DesiredChange{
|
|
Operation: filter.ChangeDelete, Before: &before, Locator: &locator,
|
|
},
|
|
})
|
|
}
|
|
sort.Slice(positioned, func(i, j int) bool { return positioned[i].position > positioned[j].position })
|
|
changes := make([]filter.DesiredChange, 0, len(positioned))
|
|
for _, item := range positioned {
|
|
changes = append(changes, item.change)
|
|
}
|
|
var backendPlan filter.BackendPlan
|
|
if len(changes) > 0 {
|
|
var verification filter.VerifyResult
|
|
backendPlan, verification, err = runtime.Execute(ctx, snapshot, changes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !verification.Matched {
|
|
return filter.ErrVerificationFailed
|
|
}
|
|
}
|
|
|
|
deleted := make([]model.FirewallRule, 0, len(prepared))
|
|
for _, item := range prepared {
|
|
if err = s.rules.DeleteWithRevision(ctx, item.stored.UUID, item.stored.Revision); err != nil {
|
|
if len(changes) > 0 {
|
|
err = rollbackFirewallPlan(ctx, runtime, backendPlan, err)
|
|
}
|
|
return s.restoreDeletedFirewallRecords(ctx, deleted, err)
|
|
}
|
|
deleted = append(deleted, item.stored)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func supportsNativeRuleBatch(provider filter.Provider) bool {
|
|
return provider == filter.ProviderIptables || provider == filter.ProviderNftables
|
|
}
|
|
|
|
func (s *FirewallService) restoreDeletedFirewallRecords(
|
|
ctx context.Context,
|
|
deleted []model.FirewallRule,
|
|
cause error,
|
|
) error {
|
|
restoreErrors := make([]error, 0)
|
|
for index := range deleted {
|
|
record := deleted[index]
|
|
if err := s.rules.Create(ctx, &record); err != nil {
|
|
restoreErrors = append(restoreErrors, fmt.Errorf("restore deleted firewall rule %q: %w", record.UUID, err))
|
|
}
|
|
}
|
|
if len(restoreErrors) == 0 {
|
|
return cause
|
|
}
|
|
return errors.Join(append([]error{cause}, restoreErrors...)...)
|
|
}
|
|
|
|
func (s *FirewallService) Update(ctx context.Context, clientIP string, request dto.FirewallRuleUpdate) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
rule := request.Rule
|
|
rule.UUID = request.UUID
|
|
return s.updateRule(ctx, clientIP, request.UUID, rule)
|
|
}
|
|
|
|
func (s *FirewallService) Reorder(ctx context.Context, clientIP string, request dto.FirewallRuleReorder) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return s.reorderRule(ctx, clientIP, request.UUID, request.TargetPosition, request.Priority)
|
|
}
|
|
|
|
func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested filter.Provider) error {
|
|
selected, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if selected != requested {
|
|
return fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, requested)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) createRule(
|
|
ctx context.Context,
|
|
runtime *filterruntime.Engine,
|
|
snapshot filter.Snapshot,
|
|
request dto.FirewallRuleCreateItem,
|
|
authorization firewallRuleCreateAuthorization,
|
|
) error {
|
|
domainRule := request.Rule
|
|
if authorization.Operation == filter.ChangeCreate {
|
|
if err := filter.CheckObservedRuleCollisions(snapshot, domainRule, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
appendRule := false
|
|
if authorization.Operation == filter.ChangeCreate && domainRule.Scope.Provider == filter.ProviderUFW && domainRule.OrderIndex == nil {
|
|
appendPosition, err := runtime.AppendPosition(ctx, snapshot, domainRule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
domainRule.OrderIndex = &appendPosition
|
|
appendRule = true
|
|
} else if authorization.Operation == filter.ChangeCreate && domainRule.OrderIndex != nil {
|
|
maxPosition, err := runtime.MaxPosition(ctx, snapshot, domainRule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *domainRule.OrderIndex < 1 || *domainRule.OrderIndex > maxPosition+1 {
|
|
return fmt.Errorf("%w: create target position %d is out of range 1-%d", filter.ErrInvalidRule, *domainRule.OrderIndex, maxPosition+1)
|
|
}
|
|
appendRule = domainRule.Scope.Provider == filter.ProviderUFW && *domainRule.OrderIndex == maxPosition+1
|
|
}
|
|
origin := constant.FirewallRuleOriginCreated
|
|
change := filter.DesiredChange{
|
|
Operation: authorization.Operation,
|
|
After: &domainRule,
|
|
Locator: authorization.Locator,
|
|
Append: appendRule,
|
|
}
|
|
if authorization.Operation == filter.ChangeAdopt {
|
|
origin = constant.FirewallRuleOriginAdopted
|
|
}
|
|
ruleRecord, err := firewallRuleModelForCreate(domainRule, request, origin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.ensureFirewallRuleIdentityAvailable(ctx, ruleRecord, ""); err != nil {
|
|
return err
|
|
}
|
|
if domainRule.Scope.Provider != filter.ProviderFirewalld {
|
|
sequence, sequenceErr := s.sequenceForCreatedFirewallRule(ctx, snapshot, domainRule)
|
|
if sequenceErr != nil {
|
|
return sequenceErr
|
|
}
|
|
ruleRecord.Sequence = &sequence
|
|
}
|
|
if err := s.rules.Create(ctx, &ruleRecord); err != nil {
|
|
return err
|
|
}
|
|
domainRule.UUID = ruleRecord.UUID
|
|
change.After = &domainRule
|
|
backendPlan, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{change})
|
|
if err != nil {
|
|
return s.cleanupFailedCreate(ctx, ruleRecord, err)
|
|
}
|
|
if !verification.Matched {
|
|
return s.cleanupFailedCreate(ctx, ruleRecord, filter.ErrVerificationFailed)
|
|
}
|
|
_, err = filter.FindCommittedObserved(verification.Snapshot, domainRule, backendPlan)
|
|
if err != nil {
|
|
return s.cleanupAppliedCreate(ctx, runtime, backendPlan, ruleRecord, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allowProtected bool) error {
|
|
if ruleUUID == "" {
|
|
return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
|
|
}
|
|
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fmt.Errorf("%w: managed rule %q was not found", filter.ErrInvalidRule, ruleUUID)
|
|
}
|
|
return err
|
|
}
|
|
if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted {
|
|
return fmt.Errorf("%w: only created or adopted rules can be deleted", filter.ErrInvalidRule)
|
|
}
|
|
selected, err := s.selectedProviderForStoredRule(ctx, stored)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selected)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ports, ok := ctx.Value(panelPortWhitelistKey{}).([]firewall.PortWhitelist); ok {
|
|
for _, desired := range desiredRules {
|
|
if panelRuleStillRequired(desired.Rule, ports) {
|
|
return filter.ErrProtectedRule
|
|
}
|
|
}
|
|
}
|
|
type appliedDelete struct {
|
|
runtime *filterruntime.Engine
|
|
plan filter.BackendPlan
|
|
}
|
|
applied := make([]appliedDelete, 0, len(desiredRules))
|
|
rollback := func(cause error) error {
|
|
for index := len(applied) - 1; index >= 0; index-- {
|
|
cause = rollbackFirewallPlan(ctx, applied[index].runtime, applied[index].plan, cause)
|
|
}
|
|
return cause
|
|
}
|
|
for _, desired := range desiredRules {
|
|
if allowProtected {
|
|
desired.Protected = false
|
|
}
|
|
runtime, runtimeErr := s.resolveRuntime(ctx, desired.Rule.Scope.Provider)
|
|
if runtimeErr != nil {
|
|
return rollback(runtimeErr)
|
|
}
|
|
snapshot, observeErr := runtime.ObserveMutation(ctx, desired.Rule.Scope)
|
|
if observeErr != nil {
|
|
return rollback(observeErr)
|
|
}
|
|
if allowProtected {
|
|
for index := range snapshot.Rules {
|
|
snapshot.Rules[index].Protected = false
|
|
}
|
|
}
|
|
observed, managedErr := filter.ManagedObserved(snapshot, desired)
|
|
if managedErr != nil {
|
|
if errors.Is(managedErr, filter.ErrRuleStale) {
|
|
missing, mergeErr := managedFirewallRuleMissing(snapshot, desired)
|
|
if mergeErr != nil {
|
|
return rollback(mergeErr)
|
|
}
|
|
if missing {
|
|
continue
|
|
}
|
|
}
|
|
return rollback(managedErr)
|
|
}
|
|
restoreAtEnd := false
|
|
if desired.Rule.Scope.Provider == filter.ProviderUFW && observed.Locator.Position != nil {
|
|
maxPosition := maxObservedFirewallPosition(snapshot)
|
|
restoreAtEnd = int64(*observed.Locator.Position) == maxPosition
|
|
}
|
|
locator := observed.Locator
|
|
before := desired.Rule
|
|
backendPlan, verification, executeErr := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{
|
|
Operation: filter.ChangeDelete, Before: &before, Locator: &locator, RestoreAtEnd: restoreAtEnd,
|
|
}})
|
|
if executeErr != nil {
|
|
return rollback(executeErr)
|
|
}
|
|
if !verification.Matched {
|
|
return rollback(filter.ErrVerificationFailed)
|
|
}
|
|
applied = append(applied, appliedDelete{runtime: runtime, plan: backendPlan})
|
|
}
|
|
if err := s.rules.DeleteWithRevision(ctx, stored.UUID, stored.Revision); err != nil {
|
|
return rollback(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func managedFirewallRuleMissing(snapshot filter.Snapshot, desired filter.DesiredRule) (bool, error) {
|
|
items, err := filter.MergeInventory(filter.InventoryMergeInput{
|
|
Observed: snapshot.Rules,
|
|
Desired: []filter.DesiredRule{desired},
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, item := range items {
|
|
if item.Desired != nil && item.Desired.UUID == desired.UUID {
|
|
return item.Match == filter.InventoryMatchMissing && item.Observed == nil, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (s *FirewallService) updateRule(ctx context.Context, clientIP, ruleUUID string, requestedRule filter.FirewallRule) error {
|
|
prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
metadataOnly, err := isUFWMetadataOnlyUpdate(prepared.Before.Rule, prepared.After, prepared.Observed.Locator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if metadataOnly {
|
|
if prepared.After.Description == prepared.Before.Rule.Description {
|
|
return nil
|
|
}
|
|
return s.rules.UpdateWithRevision(ctx, prepared.Stored.UUID, prepared.Stored.Revision, map[string]interface{}{
|
|
"description": prepared.After.Description,
|
|
})
|
|
}
|
|
return s.executeManagedMutation(ctx, managedMutationRequest{
|
|
Stored: prepared.Stored, Before: prepared.Before.Rule, After: prepared.After,
|
|
Snapshot: prepared.Snapshot, Locator: prepared.Observed.Locator,
|
|
AdapterOperation: filter.ChangeUpdate, Runtime: prepared.Runtime,
|
|
})
|
|
}
|
|
|
|
func isUFWMetadataOnlyUpdate(before, after filter.FirewallRule, locator filter.Locator) (bool, error) {
|
|
if after.Scope.Provider != filter.ProviderUFW || locator.Position == nil || after.OrderIndex == nil {
|
|
return false, nil
|
|
}
|
|
beforeKey, err := filter.RuleKey(before)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
afterKey, err := filter.RuleKey(after)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return beforeKey == afterKey && *after.OrderIndex == int64(*locator.Position), nil
|
|
}
|
|
|
|
func (s *FirewallService) reorderRule(ctx context.Context, clientIP, ruleUUID string, targetPosition *int64, priority *int) error {
|
|
if ruleUUID == "" {
|
|
return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
|
|
}
|
|
if priority != nil {
|
|
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if stored.Priority == nil {
|
|
return fmt.Errorf("%w: rule has no explicit reorderable priority", filter.ErrUnsupportedScope)
|
|
}
|
|
}
|
|
stored, before, snapshot, observed, runtime, err := s.loadManagedMutation(ctx, ruleUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
capabilities, err := runtime.Capabilities(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
after := before.Rule
|
|
adapterOperation := filter.ChangeReorder
|
|
switch {
|
|
case capabilities.OwnedChains:
|
|
if targetPosition == nil || *targetPosition < 1 {
|
|
return fmt.Errorf("%w: target position is required", filter.ErrInvalidRule)
|
|
}
|
|
if err := runtime.ValidatePosition(ctx, snapshot, before.Rule, *targetPosition); err != nil {
|
|
return err
|
|
}
|
|
after.OrderIndex = targetPosition
|
|
case capabilities.ExplicitPriority:
|
|
if priority == nil {
|
|
return fmt.Errorf("%w: priority is required", filter.ErrInvalidRule)
|
|
}
|
|
after.Priority = priority
|
|
adapterOperation = filter.ChangeUpdate
|
|
default:
|
|
return fmt.Errorf("%w: provider does not support rule reordering", filter.ErrUnsupportedScope)
|
|
}
|
|
after, err = runtime.Prepare(after)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := filter.GuardMutation(observed); err != nil {
|
|
return err
|
|
}
|
|
return s.executeManagedMutation(ctx, managedMutationRequest{
|
|
Stored: stored, Before: before.Rule, After: after, Snapshot: snapshot, Locator: observed.Locator,
|
|
AdapterOperation: adapterOperation, Runtime: runtime,
|
|
})
|
|
}
|
|
|
|
type managedMutationRequest struct {
|
|
Stored model.FirewallRule
|
|
Before filter.FirewallRule
|
|
After filter.FirewallRule
|
|
Snapshot filter.Snapshot
|
|
Locator filter.Locator
|
|
AdapterOperation filter.ChangeOperation
|
|
Runtime *filterruntime.Engine
|
|
}
|
|
|
|
type preparedManagedUpdate struct {
|
|
Stored model.FirewallRule
|
|
Before filter.DesiredRule
|
|
After filter.FirewallRule
|
|
Snapshot filter.Snapshot
|
|
Observed filter.ObservedRule
|
|
Runtime *filterruntime.Engine
|
|
}
|
|
|
|
func (s *FirewallService) prepareManagedUpdate(
|
|
ctx context.Context,
|
|
clientIP string,
|
|
ruleUUID string,
|
|
requestedRule filter.FirewallRule,
|
|
) (preparedManagedUpdate, error) {
|
|
ruleUUID = strings.TrimSpace(ruleUUID)
|
|
if ruleUUID == "" {
|
|
return preparedManagedUpdate{}, fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
|
|
}
|
|
stored, before, snapshot, observed, runtime, err := s.loadManagedMutation(ctx, ruleUUID)
|
|
if err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
after, err := filter.NormalizeRule(requestedRule)
|
|
if err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
after.UUID = stored.UUID
|
|
after, err = runtime.Prepare(after)
|
|
if err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
if err := runtime.CheckRule(ctx, after); err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
if after.Scope.Key() != before.Rule.Scope.Key() {
|
|
return preparedManagedUpdate{}, fmt.Errorf("%w: managed rule scope cannot be changed", filter.ErrUnsupportedScope)
|
|
}
|
|
if !supportsManagedNativeKindTransition(before.Rule, after) {
|
|
return preparedManagedUpdate{}, fmt.Errorf("%w: native rule conversion requires an explicit workflow", filter.ErrUnsupportedScope)
|
|
}
|
|
capabilities, err := runtime.Capabilities(ctx)
|
|
if err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
if capabilities.ExplicitPosition || capabilities.OwnedChains {
|
|
if observed.Locator.Position == nil {
|
|
return preparedManagedUpdate{}, fmt.Errorf("%w: managed rule has no positional locator", filter.ErrInvalidRule)
|
|
}
|
|
currentPosition := int64(*observed.Locator.Position)
|
|
if after.OrderIndex == nil {
|
|
after.OrderIndex = ¤tPosition
|
|
} else if *after.OrderIndex != currentPosition {
|
|
if err := runtime.ValidatePosition(ctx, snapshot, before.Rule, *after.OrderIndex); err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
}
|
|
}
|
|
if err := filter.GuardMutation(observed); err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
if err := filter.CheckObservedRuleCollisions(snapshot, after, &observed.Locator); err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
return preparedManagedUpdate{
|
|
Stored: stored, Before: before, After: after, Snapshot: snapshot, Observed: observed, Runtime: runtime,
|
|
}, nil
|
|
}
|
|
|
|
func supportsManagedNativeKindTransition(before, after filter.FirewallRule) bool {
|
|
if before.NativeKind == after.NativeKind {
|
|
return true
|
|
}
|
|
if before.Scope.Key() != after.Scope.Key() || before.Scope.Provider != filter.ProviderFirewalld {
|
|
return false
|
|
}
|
|
return before.NativeKind == filter.NativeKindZonePort && after.NativeKind == filter.NativeKindRichRule ||
|
|
before.NativeKind == filter.NativeKindRichRule && after.NativeKind == filter.NativeKindZonePort
|
|
}
|
|
|
|
func (s *FirewallService) loadManagedMutation(
|
|
ctx context.Context,
|
|
ruleUUID string,
|
|
) (model.FirewallRule, filter.DesiredRule, filter.Snapshot, filter.ObservedRule, *filterruntime.Engine, error) {
|
|
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
if isProtectedSystemFirewallRule(stored) {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil,
|
|
filter.ErrProtectedRule
|
|
}
|
|
if stored.Origin != constant.FirewallRuleOriginCreated && stored.Origin != constant.FirewallRuleOriginAdopted {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil,
|
|
fmt.Errorf("%w: only created or adopted rules can be changed", filter.ErrInvalidRule)
|
|
}
|
|
selected, err := s.selectedProviderForStoredRule(ctx, stored)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
desiredRules, err := s.compileStoredFirewallRules(ctx, stored, selected)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
if len(desiredRules) != 1 {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil,
|
|
fmt.Errorf("%w: policy %q expands to %d target rules and cannot be edited atomically", filter.ErrUnsupportedScope, ruleUUID, len(desiredRules))
|
|
}
|
|
desired := desiredRules[0]
|
|
runtime, err := s.resolveRuntime(ctx, desired.Rule.Scope.Provider)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
snapshot, err := runtime.ObserveMutation(ctx, desired.Rule.Scope)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
observed, err := filter.ManagedObserved(snapshot, desired)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
return stored, desired, snapshot, observed, runtime, nil
|
|
}
|
|
|
|
func (s *FirewallService) selectedProviderForStoredRule(
|
|
ctx context.Context,
|
|
_ model.FirewallRule,
|
|
) (filter.Provider, error) {
|
|
if s.selectedProvider != nil {
|
|
return s.selectedProvider(ctx)
|
|
}
|
|
if s.adapters != nil {
|
|
providers := s.adapters.Providers()
|
|
if len(providers) == 1 {
|
|
return providers[0], nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%w: selected provider is unavailable", filter.ErrProviderUnavailable)
|
|
}
|
|
|
|
func (s *FirewallService) executeManagedMutation(ctx context.Context, request managedMutationRequest) error {
|
|
before, after := request.Before, request.After
|
|
if err := filter.CheckObservedRuleCollisions(request.Snapshot, after, &request.Locator); err != nil {
|
|
return err
|
|
}
|
|
semantic, err := model.FirewallRuleFromDomain(after)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.ensureFirewallRuleIdentityAvailable(ctx, semantic, request.Stored.UUID); err != nil {
|
|
return err
|
|
}
|
|
appendRule, restoreAtEnd := false, false
|
|
if after.Scope.Provider == filter.ProviderUFW && request.AdapterOperation == filter.ChangeUpdate {
|
|
maxPosition := maxObservedFirewallPosition(request.Snapshot)
|
|
appendRule = after.OrderIndex != nil && *after.OrderIndex == maxPosition
|
|
restoreAtEnd = request.Locator.Position != nil && int64(*request.Locator.Position) == maxPosition
|
|
}
|
|
backendPlan, verification, err := request.Runtime.Execute(ctx, request.Snapshot, []filter.DesiredChange{{
|
|
Operation: request.AdapterOperation,
|
|
Before: &before,
|
|
After: &after,
|
|
Locator: &request.Locator,
|
|
Append: appendRule,
|
|
RestoreAtEnd: restoreAtEnd,
|
|
}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !verification.Matched {
|
|
return filter.ErrVerificationFailed
|
|
}
|
|
_, err = filter.FindCommittedObserved(verification.Snapshot, request.After, backendPlan)
|
|
if err != nil {
|
|
return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err)
|
|
}
|
|
updates, err := firewallRuleSemanticUpdates(request.After)
|
|
if err != nil {
|
|
return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err)
|
|
}
|
|
if request.After.Scope.Provider == filter.ProviderFirewalld {
|
|
updates["sequence"] = nil
|
|
} else {
|
|
position, positionErr := firewallRuleMarkerPosition(verification.Snapshot, request.Stored.UUID)
|
|
if positionErr != nil {
|
|
return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, positionErr)
|
|
}
|
|
sequence, sequenceErr := s.sequenceForFirewallRulePosition(
|
|
ctx, verification.Snapshot, position, request.Stored.UUID, request.Stored.Sequence,
|
|
)
|
|
if sequenceErr != nil {
|
|
return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, sequenceErr)
|
|
}
|
|
updates["sequence"] = sequence
|
|
}
|
|
if err := s.rules.UpdateWithRevision(ctx, request.Stored.UUID, request.Stored.Revision, updates); err != nil {
|
|
return rollbackFirewallPlan(ctx, request.Runtime, backendPlan, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func maxObservedFirewallPosition(snapshot filter.Snapshot) int64 {
|
|
var maximum int64
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum {
|
|
maximum = int64(*observed.Locator.Position)
|
|
}
|
|
}
|
|
return maximum
|
|
}
|
|
|
|
func (s *FirewallService) ensureFirewallRuleIdentityAvailable(
|
|
ctx context.Context,
|
|
requested model.FirewallRule,
|
|
excludedUUID string,
|
|
) error {
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
identities := make(firewallRuleIdentityIndex, len(stored))
|
|
for _, candidate := range stored {
|
|
if candidate.UUID != excludedUUID {
|
|
identities.add(candidate)
|
|
}
|
|
}
|
|
return identities.check(requested)
|
|
}
|
|
|
|
type firewallRuleIdentityIndex map[string][]filter.Action
|
|
|
|
func firewallRuleComparisonKey(rule model.FirewallRule) string {
|
|
rule.Action = ""
|
|
priority := 0
|
|
if rule.Priority != nil {
|
|
priority = *rule.Priority
|
|
}
|
|
return rule.PolicyKey() + "/" + strconv.Itoa(priority)
|
|
}
|
|
|
|
func (index firewallRuleIdentityIndex) add(rule model.FirewallRule) {
|
|
key := firewallRuleComparisonKey(rule)
|
|
index[key] = append(index[key], filter.Action(rule.Action))
|
|
}
|
|
|
|
func (index firewallRuleIdentityIndex) check(rule model.FirewallRule) error {
|
|
for _, action := range index[firewallRuleComparisonKey(rule)] {
|
|
if action == filter.Action(rule.Action) {
|
|
return fmt.Errorf("%w: equivalent managed rule already exists", filter.ErrRuleOperation)
|
|
}
|
|
if filter.OppositeActions(action, filter.Action(rule.Action)) {
|
|
return filter.ErrRuleConflict
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (*filterruntime.Engine, error) {
|
|
if s.selectedProvider != nil {
|
|
selected, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if selected != provider {
|
|
return nil, fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, provider)
|
|
}
|
|
}
|
|
if s.adapters == nil {
|
|
return nil, filter.ErrAdapterUnavailable
|
|
}
|
|
return s.adapters.Resolve(provider)
|
|
}
|
|
|
|
func (s *FirewallService) cleanupFailedCreate(ctx context.Context, rule model.FirewallRule, cause error) error {
|
|
if err := s.rules.DeleteWithRevision(ctx, rule.UUID, rule.Revision); err != nil {
|
|
return errors.Join(cause, fmt.Errorf("cleanup failed firewall rule %q: %w", rule.UUID, err))
|
|
}
|
|
return cause
|
|
}
|
|
|
|
func (s *FirewallService) cleanupAppliedCreate(
|
|
ctx context.Context,
|
|
runtime *filterruntime.Engine,
|
|
plan filter.BackendPlan,
|
|
rule model.FirewallRule,
|
|
cause error,
|
|
) error {
|
|
cause = rollbackFirewallPlan(ctx, runtime, plan, cause)
|
|
return s.cleanupFailedCreate(ctx, rule, cause)
|
|
}
|
|
|
|
func (s *FirewallService) ensureSystemPort(ctx context.Context, port dto.FirewallSystemPort) error {
|
|
create, err := s.prepareSystemPortCreate(ctx, port)
|
|
if err == nil && create == nil {
|
|
return nil
|
|
}
|
|
if err == nil {
|
|
err = s.createFirewallRuleItem(ctx, *create)
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, filter.ErrInventoryUnavailable) {
|
|
err = s.appendUFWSystemPortUnverified(ctx, port, err)
|
|
}
|
|
if port.Family == constant.FirewallFamilyIPv6 && filterufw.IsIPv6Unavailable(err) {
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf("skip accepted UFW IPv6 port %s/%s: %v", port.Port, port.Protocol, err)
|
|
}
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *FirewallService) appendUFWSystemPortUnverified(
|
|
ctx context.Context,
|
|
port dto.FirewallSystemPort,
|
|
cause error,
|
|
) error {
|
|
if s.selectedProvider == nil || s.adapters == nil {
|
|
return cause
|
|
}
|
|
provider, providerErr := s.selectedProvider(ctx)
|
|
if providerErr != nil {
|
|
return errors.Join(cause, providerErr)
|
|
}
|
|
if provider != filter.ProviderUFW {
|
|
return cause
|
|
}
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf(
|
|
"UFW inventory is unavailable while restoring accepted port %s/%s; attempting a restricted direct allow: %v",
|
|
port.Port, port.Protocol, cause,
|
|
)
|
|
}
|
|
runtime, resolveErr := s.adapters.Resolve(provider)
|
|
if resolveErr != nil {
|
|
return errors.Join(cause, resolveErr)
|
|
}
|
|
comment := "1panel-system-port:" + systemPortKey(port)
|
|
if appendErr := runtime.AppendUnverified(ctx, systemPortRule(provider, port), comment); appendErr != nil {
|
|
if global.LOG != nil {
|
|
global.LOG.Errorf(
|
|
"restore accepted UFW port %s/%s without rule inventory failed: %v; original error: %v",
|
|
port.Port, port.Protocol, appendErr, cause,
|
|
)
|
|
}
|
|
return errors.Join(cause, fmt.Errorf("append accepted UFW port without rule inventory: %w", appendErr))
|
|
}
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf(
|
|
"restored accepted UFW port %s/%s without rule inventory; normal rule management failed: %v",
|
|
port.Port, port.Protocol, cause,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) createFirewallRuleItem(ctx context.Context, item dto.FirewallRuleCreateItem) error {
|
|
result, err := s.create(ctx, dto.FirewallRuleCreate{Items: []dto.FirewallRuleCreateItem{item}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.Failed == 0 && result.Skipped == 0 {
|
|
return nil
|
|
}
|
|
if len(result.Errors) > 0 && result.Errors[0].Error != "" {
|
|
return errors.New(result.Errors[0].Error)
|
|
}
|
|
return errors.New("create firewall rule failed")
|
|
}
|
|
|
|
func (s *FirewallService) prepareSystemPortCreate(ctx context.Context, port dto.FirewallSystemPort) (*dto.FirewallRuleCreateItem, error) {
|
|
provider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rule := systemPortRule(provider, port)
|
|
check, err := s.checkRule(ctx, "", dto.FirewallRuleCheckItem{Rule: rule})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if check.Decision == filter.CheckDecisionNoChange {
|
|
return nil, nil
|
|
}
|
|
create := dto.FirewallRuleCreateItem{
|
|
Rule: check.RequestedRule,
|
|
CheckFlag: check.CheckFlag,
|
|
SourceKind: constant.FirewallRuleSourceSecurity,
|
|
SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port),
|
|
}
|
|
switch check.Classification {
|
|
case filter.CheckClassificationNone:
|
|
create.Action = filter.CheckActionCreate
|
|
case filter.CheckClassificationExactExternal:
|
|
if len(check.Candidates) == 0 {
|
|
return nil, fmt.Errorf("%w: external port rule has no candidate", filter.ErrRuleStale)
|
|
}
|
|
if len(check.Candidates) == 1 {
|
|
create.Action = filter.CheckActionAdopt
|
|
} else {
|
|
create.Action = filter.CheckActionSelectAdopt
|
|
}
|
|
create.AdoptInstanceKey = check.Candidates[0].InstanceKey
|
|
case filter.CheckClassificationConflict:
|
|
return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason)
|
|
case filter.CheckClassificationExactManaged:
|
|
return nil, nil
|
|
case filter.CheckClassificationProtected:
|
|
if len(check.Candidates) > 0 {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("%w: protected accepted port %s", filter.ErrProtectedRule, port.Port)
|
|
default:
|
|
return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason)
|
|
}
|
|
return &create, nil
|
|
}
|
|
|
|
func (s *FirewallService) deleteSystemPort(ctx context.Context, port dto.FirewallSystemPort) error {
|
|
stored, err := s.systemPortRecords(ctx, port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, rule := range stored {
|
|
if err := s.deleteProtectedSystemPortRule(ctx, rule.UUID); err != nil {
|
|
if errors.Is(err, filter.ErrProtectedRule) && ctx.Value(panelPortWhitelistKey{}) != nil {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) deleteProtectedSystemPortRule(ctx context.Context, ruleUUID string) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return s.deleteRule(ctx, ruleUUID, true)
|
|
}
|
|
|
|
func (s *FirewallService) systemPortRecords(ctx context.Context, port dto.FirewallSystemPort) ([]model.FirewallRule, error) {
|
|
records := make([]model.FirewallRule, 0)
|
|
sourceIDs := []string{constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port)}
|
|
if port.Family == constant.FirewallFamilyIPv4 {
|
|
sourceIDs = append(sourceIDs, constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port))
|
|
}
|
|
seen := make(map[string]struct{})
|
|
for _, sourceID := range sourceIDs {
|
|
items, listErr := s.rules.List(ctx,
|
|
repo.WithFirewallRuleSource(constant.FirewallRuleSourceSecurity, sourceID),
|
|
)
|
|
if listErr != nil {
|
|
return nil, listErr
|
|
}
|
|
for _, item := range items {
|
|
if _, exists := seen[item.UUID]; exists {
|
|
continue
|
|
}
|
|
seen[item.UUID] = struct{}{}
|
|
records = append(records, item)
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func isProtectedSystemFirewallRule(rule model.FirewallRule) bool {
|
|
ownerPrefix := model.FirewallRuleOwner(
|
|
constant.FirewallRuleSourceSecurity,
|
|
constant.FirewallSystemAcceptedPortSourcePrefix,
|
|
)
|
|
return strings.HasPrefix(rule.Owner, ownerPrefix)
|
|
}
|
|
|
|
func systemPortRule(provider filter.Provider, port dto.FirewallSystemPort) filter.FirewallRule {
|
|
return firewall.RuleForSystemPort(provider, firewall.SystemPort(port))
|
|
}
|
|
|
|
func normalizeSystemPorts(ports []dto.FirewallSystemPort) (map[string]dto.FirewallSystemPort, error) {
|
|
domainPorts := make([]firewall.SystemPort, 0, len(ports))
|
|
for _, port := range ports {
|
|
domainPorts = append(domainPorts, firewall.SystemPort(port))
|
|
}
|
|
normalized, err := firewall.NormalizeSystemPorts(domainPorts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string]dto.FirewallSystemPort, len(normalized))
|
|
for key, port := range normalized {
|
|
result[key] = dto.FirewallSystemPort(port)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func systemPortKey(port dto.FirewallSystemPort) string {
|
|
return firewall.SystemPortKey(firewall.SystemPort(port))
|
|
}
|
|
|
|
func legacySystemPortKey(port dto.FirewallSystemPort) string {
|
|
return firewall.LegacySystemPortKey(firewall.SystemPort(port))
|
|
}
|
|
|
|
func sortedSystemPortKeys(ports map[string]dto.FirewallSystemPort) []string {
|
|
domainPorts := make(map[string]firewall.SystemPort, len(ports))
|
|
for key, port := range ports {
|
|
domainPorts[key] = firewall.SystemPort(port)
|
|
}
|
|
return firewall.SortedSystemPortKeys(domainPorts)
|
|
}
|
|
|
|
func firewallRuleModelForCreate(rule filter.FirewallRule, request dto.FirewallRuleCreateItem, origin string) (model.FirewallRule, error) {
|
|
record, err := model.FirewallRuleFromDomain(rule)
|
|
if err != nil {
|
|
return model.FirewallRule{}, err
|
|
}
|
|
record.Origin = origin
|
|
record.Owner = model.FirewallRuleOwner(request.SourceKind, request.SourceID)
|
|
return record, nil
|
|
}
|
|
|
|
func firewallRuleSemanticUpdates(rule filter.FirewallRule) (map[string]interface{}, error) {
|
|
record, err := model.FirewallRuleFromDomain(rule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]interface{}{
|
|
"family": record.Family, "protocol": record.Protocol,
|
|
"source_address": record.SourceAddress, "source_port": record.SourcePort,
|
|
"destination_address": record.DestinationAddress, "destination_port": record.DestinationPort,
|
|
"interface": record.Interface, "connection_states": record.ConnectionStates, "action": record.Action,
|
|
"description": record.Description, "compatibility_error": "", "priority": record.Priority,
|
|
}, nil
|
|
}
|
|
|
|
func (s *FirewallService) nextFirewallRuleSequence(ctx context.Context) (int64, error) {
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var maximum int64
|
|
for _, record := range stored {
|
|
if record.Sequence != nil && *record.Sequence > maximum {
|
|
maximum = *record.Sequence
|
|
}
|
|
}
|
|
return maximum + model.FirewallRuleSequenceStep, nil
|
|
}
|
|
|
|
func (s *FirewallService) sequenceForCreatedFirewallRule(
|
|
ctx context.Context,
|
|
snapshot filter.Snapshot,
|
|
rule filter.FirewallRule,
|
|
) (int64, error) {
|
|
if rule.OrderIndex == nil {
|
|
return s.nextFirewallRuleSequence(ctx)
|
|
}
|
|
return s.sequenceForFirewallRulePosition(ctx, snapshot, int(*rule.OrderIndex), "", nil)
|
|
}
|
|
|
|
func (s *FirewallService) sequenceForFirewallRulePosition(
|
|
ctx context.Context,
|
|
snapshot filter.Snapshot,
|
|
targetPosition int,
|
|
excludedUUID string,
|
|
current *int64,
|
|
) (int64, error) {
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
byUUID := make(map[string]model.FirewallRule, len(stored))
|
|
for _, record := range stored {
|
|
byUUID[record.UUID] = record
|
|
}
|
|
var previous, next *model.FirewallRule
|
|
needsRebalance := false
|
|
for _, observed := range snapshot.Rules {
|
|
uuid := strings.TrimPrefix(observed.Marker, "1panel-rule:")
|
|
if observed.Marker == uuid || uuid == excludedUUID || observed.Locator.Position == nil {
|
|
continue
|
|
}
|
|
record, exists := byUUID[uuid]
|
|
if !exists {
|
|
continue
|
|
}
|
|
position := *observed.Locator.Position
|
|
if position < targetPosition {
|
|
copy := record
|
|
previous = ©
|
|
} else if position > targetPosition || excludedUUID == "" {
|
|
copy := record
|
|
next = ©
|
|
break
|
|
}
|
|
}
|
|
if previous != nil && previous.Sequence == nil || next != nil && next.Sequence == nil {
|
|
needsRebalance = true
|
|
}
|
|
if !needsRebalance && current != nil &&
|
|
(previous == nil || *previous.Sequence < *current) && (next == nil || *current < *next.Sequence) {
|
|
return *current, nil
|
|
}
|
|
if !needsRebalance {
|
|
switch {
|
|
case previous == nil && next == nil:
|
|
return model.FirewallRuleSequenceStep, nil
|
|
case previous == nil:
|
|
return *next.Sequence - model.FirewallRuleSequenceStep, nil
|
|
case next == nil:
|
|
return *previous.Sequence + model.FirewallRuleSequenceStep, nil
|
|
case *next.Sequence-*previous.Sequence > 1:
|
|
return *previous.Sequence + (*next.Sequence-*previous.Sequence)/2, nil
|
|
default:
|
|
needsRebalance = true
|
|
}
|
|
}
|
|
if needsRebalance {
|
|
return s.rebalanceFirewallRuleSequences(ctx, snapshot, targetPosition, excludedUUID, byUUID)
|
|
}
|
|
return 0, fmt.Errorf("%w: cannot allocate firewall rule sequence", filter.ErrRuleOperation)
|
|
}
|
|
|
|
func (s *FirewallService) rebalanceFirewallRuleSequences(
|
|
ctx context.Context,
|
|
snapshot filter.Snapshot,
|
|
targetPosition int,
|
|
excludedUUID string,
|
|
byUUID map[string]model.FirewallRule,
|
|
) (int64, error) {
|
|
targetSequence := int64(targetPosition) * model.FirewallRuleSequenceStep
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.Locator.Position == nil {
|
|
continue
|
|
}
|
|
uuid := strings.TrimPrefix(observed.Marker, "1panel-rule:")
|
|
if observed.Marker == uuid || uuid == excludedUUID {
|
|
continue
|
|
}
|
|
record, exists := byUUID[uuid]
|
|
if !exists {
|
|
continue
|
|
}
|
|
position := *observed.Locator.Position
|
|
if excludedUUID == "" && position >= targetPosition {
|
|
position++
|
|
}
|
|
sequence := int64(position) * model.FirewallRuleSequenceStep
|
|
if record.Sequence != nil && *record.Sequence == sequence {
|
|
continue
|
|
}
|
|
if err := s.rules.UpdateWithRevision(ctx, record.UUID, record.Revision, map[string]interface{}{
|
|
"sequence": sequence,
|
|
}); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return targetSequence, nil
|
|
}
|
|
|
|
func firewallRuleMarkerPosition(snapshot filter.Snapshot, ruleUUID string) (int, error) {
|
|
marker := "1panel-rule:" + ruleUUID
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.Marker == marker && observed.Locator.Position != nil {
|
|
return *observed.Locator.Position, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("%w: committed firewall rule %q has no position", filter.ErrVerificationFailed, ruleUUID)
|
|
}
|
|
|
|
func (s *FirewallService) compileStoredFirewallRules(
|
|
ctx context.Context,
|
|
stored model.FirewallRule,
|
|
target filter.Provider,
|
|
) ([]filter.DesiredRule, error) {
|
|
rules, err := stored.RulesForProvider(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtime, err := s.adapters.Resolve(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return runtime.CompileDesired(ctx, stored.UUID, filter.RuleOrigin(stored.Origin), rules)
|
|
}
|
|
|
|
func isFirewallPolicyIncompatible(err error) bool {
|
|
return errors.Is(err, filter.ErrInvalidRule) || errors.Is(err, filter.ErrUnsupportedScope) ||
|
|
errors.Is(err, filter.ErrInvalidScope) || errors.Is(err, filter.ErrCompositeRule)
|
|
}
|
|
|
|
func (s *FirewallService) desiredFirewallRulesByScope(
|
|
ctx context.Context,
|
|
stored []model.FirewallRule,
|
|
provider filter.Provider,
|
|
) (map[string][]filter.DesiredRule, []filter.InventoryItem) {
|
|
model.SortFirewallRules(stored, provider)
|
|
desired := make(map[string][]filter.DesiredRule)
|
|
var failures []filter.InventoryItem
|
|
for _, record := range stored {
|
|
compiled, err := s.compileStoredFirewallRules(ctx, record, provider)
|
|
if err != nil {
|
|
rule := filter.FirewallRule{
|
|
UUID: record.UUID,
|
|
Scope: filter.Scope{Provider: provider, Family: filter.Family(record.Family), Direction: filter.DirectionInput}.Normalize(),
|
|
Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort,
|
|
DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort,
|
|
Interface: record.Interface, ConnectionStates: strings.FieldsFunc(record.ConnectionStates, func(r rune) bool { return r == ',' }),
|
|
Action: filter.Action(record.Action), Description: record.Description, Priority: record.Priority,
|
|
}
|
|
failures = append(failures, filter.InventoryItem{
|
|
Incompatible: isFirewallPolicyIncompatible(err),
|
|
Rule: rule, State: filter.InventoryStateDrifted, Match: filter.InventoryMatchNone,
|
|
Desired: &filter.DesiredRule{UUID: record.UUID, Rule: rule, Origin: filter.RuleOrigin(record.Origin), Protected: isProtectedSystemFirewallRule(record)},
|
|
Error: fmt.Sprintf("policy %s: %v", record.UUID, err),
|
|
})
|
|
continue
|
|
}
|
|
for _, rule := range compiled {
|
|
rule.Protected = isProtectedSystemFirewallRule(record)
|
|
rule.Expanded = len(compiled) > 1
|
|
key := rule.Rule.Scope.Key()
|
|
desired[key] = append(desired[key], rule)
|
|
}
|
|
}
|
|
return desired, failures
|
|
}
|
|
|
|
func firewallRuleSnapshotPolicy(ctx context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) {
|
|
if ports, ok := ctx.Value(panelPortWhitelistKey{}).([]firewall.PortWhitelist); ok {
|
|
return filter.ProtectSnapshot(snapshot, ports)
|
|
}
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return filter.Snapshot{}, err
|
|
}
|
|
return filter.ProtectSnapshot(snapshot, ports)
|
|
}
|
|
|
|
func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
|
|
return selectedRuleProvider()
|
|
}
|
|
|
|
func rollbackFirewallPlan(ctx context.Context, runtime *filterruntime.Engine, plan filter.BackendPlan, cause error) error {
|
|
if runtime == nil {
|
|
return cause
|
|
}
|
|
if err := runtime.Rollback(ctx, plan); err != nil {
|
|
return errors.Join(cause, fmt.Errorf("rollback applied firewall plan: %w", err))
|
|
}
|
|
return cause
|
|
}
|
|
|
|
func selectedRuleProvider() (filter.Provider, error) {
|
|
provider, err := selectedSystemFirewallProvider()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: %v", filter.ErrProviderUnavailable, err)
|
|
}
|
|
return filter.Provider(provider), nil
|
|
}
|
|
|
|
type firewallRuleCreateAuthorization = filter.CreateAuthorization
|
|
|
|
func refreshCreateAuthorization(
|
|
snapshot filter.Snapshot,
|
|
prepared preparedFirewallRuleCreate,
|
|
) (firewallRuleCreateAuthorization, error) {
|
|
authorization := prepared.authorization
|
|
if authorization.Operation != filter.ChangeAdopt {
|
|
return authorization, nil
|
|
}
|
|
if candidate, err := filter.FindCandidate(snapshot.Rules, prepared.request.AdoptInstanceKey); err == nil {
|
|
locator := candidate.Locator
|
|
authorization.Locator = &locator
|
|
return authorization, nil
|
|
}
|
|
|
|
ruleKey, err := filter.RuleKey(prepared.request.Rule)
|
|
if err != nil {
|
|
return firewallRuleCreateAuthorization{}, err
|
|
}
|
|
candidates := make([]filter.ObservedRule, 0)
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.Marker != "" || observed.Protected || observed.ParseStatus != filter.ParseStatusSupported {
|
|
continue
|
|
}
|
|
observedKey, keyErr := filter.RuleKey(observed.Rule)
|
|
if keyErr == nil && observedKey == ruleKey {
|
|
candidates = append(candidates, observed)
|
|
}
|
|
}
|
|
if len(candidates) == 1 {
|
|
locator := candidates[0].Locator
|
|
authorization.Locator = &locator
|
|
return authorization, nil
|
|
}
|
|
if authorization.Locator != nil {
|
|
for _, candidate := range candidates {
|
|
if authorization.Locator.Canonical != "" && candidate.Locator.Canonical == authorization.Locator.Canonical ||
|
|
authorization.Locator.NativeID != "" && candidate.Locator.NativeID == authorization.Locator.NativeID {
|
|
locator := candidate.Locator
|
|
authorization.Locator = &locator
|
|
return authorization, nil
|
|
}
|
|
}
|
|
}
|
|
return firewallRuleCreateAuthorization{}, filter.ErrRuleStale
|
|
}
|
|
|
|
func signFirewallRuleCheck(result filter.RuleCheckResult, snapshot filter.Snapshot, managedRevision string) (string, error) {
|
|
return firewallCheckFlagCodec().Sign(result, snapshot, managedRevision)
|
|
}
|
|
|
|
func authorizeFirewallRuleCreate(
|
|
checkFlag string,
|
|
action filter.CheckAction,
|
|
adoptInstanceKey string,
|
|
rule filter.FirewallRule,
|
|
snapshot filter.Snapshot,
|
|
managedRevision string,
|
|
) (firewallRuleCreateAuthorization, error) {
|
|
return firewallCheckFlagCodec().Authorize(checkFlag, action, adoptInstanceKey, rule, snapshot, managedRevision)
|
|
}
|
|
|
|
func firewallCheckFlagCodec() *filter.CheckFlagCodec {
|
|
secret := []byte(global.CONF.Base.EncryptKey + "\x00firewall-rule-check-v1")
|
|
return filter.NewCheckFlagCodec(secret, constant.FirewallRuleCheckVersion)
|
|
}
|
|
|
|
func OperateFirewallPort(oldPorts, newPorts []int) error {
|
|
client, err := selectedSystemFirewallClient()
|
|
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 := loadDirectFirewallInitStatus(state.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isInit {
|
|
return nil
|
|
}
|
|
if state.Name == constant.FirewallProviderIptables {
|
|
if err := newIptablesHelperManager().SyncRequiredPorts(true); err != nil {
|
|
return err
|
|
}
|
|
} else if err := newNftablesHelperManager().SyncRequiredPorts(); err != nil {
|
|
return err
|
|
}
|
|
} else if !state.IsActive {
|
|
return nil
|
|
}
|
|
current, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
previous := make([]firewall.PortWhitelist, 0, len(oldPorts))
|
|
for _, port := range oldPorts {
|
|
item := firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"}
|
|
if !containsFirewallPort(current, item) {
|
|
previous = append(previous, item)
|
|
}
|
|
}
|
|
added := make([]firewall.PortWhitelist, 0, len(newPorts))
|
|
for _, port := range newPorts {
|
|
added = append(added, firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"})
|
|
}
|
|
if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables {
|
|
required, err := LoadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
added = excludeFirewallPorts(added, required)
|
|
}
|
|
return syncManagedAcceptedPorts(previous, added)
|
|
}
|
|
|
|
func containsFirewallPort(ports []firewall.PortWhitelist, target firewall.PortWhitelist) bool {
|
|
return firewall.ContainsPort(ports, target)
|
|
}
|
|
|
|
func LoadPanelPort() string {
|
|
if !global.IsMaster {
|
|
return global.CONF.Base.Port
|
|
}
|
|
var portSetting model.Setting
|
|
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
|
|
return portSetting.Value
|
|
}
|
|
|
|
func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error {
|
|
previousSet, err := normalizeSystemPorts(previous)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSet, err := normalizeSystemPorts(current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
provider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !supportsNativeRuleBatch(provider) {
|
|
var syncErrors []error
|
|
for _, key := range sortedSystemPortKeys(currentSet) {
|
|
if _, exists := previousSet[key]; exists {
|
|
continue
|
|
}
|
|
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
|
|
wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err)
|
|
syncErrors = append(syncErrors, wrapped)
|
|
if global.LOG != nil {
|
|
global.LOG.Errorf("%v", wrapped)
|
|
}
|
|
}
|
|
}
|
|
for _, key := range sortedSystemPortKeys(previousSet) {
|
|
if _, exists := currentSet[key]; exists {
|
|
continue
|
|
}
|
|
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
|
|
wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err)
|
|
syncErrors = append(syncErrors, wrapped)
|
|
if global.LOG != nil {
|
|
global.LOG.Errorf("%v", wrapped)
|
|
}
|
|
}
|
|
}
|
|
return errors.Join(syncErrors...)
|
|
}
|
|
|
|
creates := make([]dto.FirewallRuleCreateItem, 0)
|
|
for _, key := range sortedSystemPortKeys(currentSet) {
|
|
if _, exists := previousSet[key]; exists {
|
|
continue
|
|
}
|
|
create, prepareErr := s.prepareSystemPortCreate(ctx, currentSet[key])
|
|
if prepareErr != nil {
|
|
return prepareErr
|
|
}
|
|
if create != nil {
|
|
creates = append(creates, *create)
|
|
}
|
|
}
|
|
if len(creates) > 0 {
|
|
result, batchErr := s.Create(ctx, dto.FirewallRuleCreate{Items: creates})
|
|
if batchErr != nil {
|
|
return batchErr
|
|
}
|
|
if result.Failed > 0 || result.Skipped > 0 {
|
|
if len(result.Errors) > 0 {
|
|
return fmt.Errorf("batch create accepted firewall ports: %s", result.Errors[0].Error)
|
|
}
|
|
return fmt.Errorf("batch create accepted firewall ports failed")
|
|
}
|
|
}
|
|
|
|
for _, key := range sortedSystemPortKeys(previousSet) {
|
|
if _, exists := currentSet[key]; exists {
|
|
continue
|
|
}
|
|
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadConfiguredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
|
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
|
|
if err != nil {
|
|
value = constant.FirewallPortWhiteListValue
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return firewall.ParsePortWhitelist(value)
|
|
}
|
|
|
|
func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
|
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
required, err := LoadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return firewall.NormalizePortWhitelist(append(configured, required...)), nil
|
|
}
|
|
|
|
func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
|
return loadRequiredFirewallPorts(LoadPanelPort())
|
|
}
|
|
|
|
func loadRequiredFirewallPorts(panelPort string) ([]firewall.PortWhitelist, error) {
|
|
if panelPort == "" {
|
|
return nil, fmt.Errorf("find 1panel service port failed")
|
|
}
|
|
directives, _, err := parseSSHConfigTree(sshPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("load required SSH ports: %w", err)
|
|
}
|
|
ports := []firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}
|
|
for _, port := range loadSSHPortValues(directives) {
|
|
ports = append(ports, firewall.PortWhitelist{Port: port, Protocol: "tcp"})
|
|
}
|
|
return firewall.NormalizeRequiredPorts(ports)
|
|
}
|
|
|
|
func (s *FirewallService) updatePortWhitelist(ctx context.Context, value string) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
|
|
ports, err := firewall.ParsePortWhitelist(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldValue, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
oldValue = constant.FirewallPortWhiteListValue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
oldPorts, err := firewall.ParsePortWhitelist(oldValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
required, err := s.requiredPorts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, firewall.NormalizePortWhitelist(append(oldPorts, required...)))
|
|
added := excludeFirewallPorts(excludeFirewallPorts(ports, oldPorts), required)
|
|
items := make([]dto.FirewallRuleCreateItem, 0, len(added))
|
|
for _, port := range systemPorts(added) {
|
|
item, err := s.prepareSystemPortCreate(ctx, port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if item != nil {
|
|
items = append(items, *item)
|
|
}
|
|
}
|
|
if len(items) > 0 {
|
|
if _, err := s.createLocked(ctx, dto.FirewallRuleCreate{Items: items}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
removed := excludeFirewallPorts(excludeFirewallPorts(oldPorts, ports), required)
|
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
txCtx := context.WithValue(ctx, constant.DB, tx)
|
|
if err := s.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil {
|
|
return err
|
|
}
|
|
return tx.Where("key = ?", constant.FirewallPortWhiteList).
|
|
Assign(map[string]interface{}{"value": value}).
|
|
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
|
})
|
|
}
|
|
|
|
func (s *FirewallService) releaseSystemPorts(ctx context.Context, ports []dto.FirewallSystemPort) error {
|
|
portSet, err := normalizeSystemPorts(ports)
|
|
if err != nil || len(portSet) == 0 {
|
|
return err
|
|
}
|
|
|
|
owners := make(map[string]struct{}, len(portSet)*2)
|
|
for _, port := range portSet {
|
|
owners[model.FirewallRuleOwner(
|
|
constant.FirewallRuleSourceSecurity,
|
|
constant.FirewallSystemAcceptedPortSourcePrefix+systemPortKey(port),
|
|
)] = struct{}{}
|
|
if port.Family == constant.FirewallFamilyIPv4 {
|
|
owners[model.FirewallRuleOwner(
|
|
constant.FirewallRuleSourceSecurity,
|
|
constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port),
|
|
)] = struct{}{}
|
|
}
|
|
}
|
|
records, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, record := range records {
|
|
if _, exists := owners[record.Owner]; !exists {
|
|
continue
|
|
}
|
|
if err := s.rules.UpdateWithRevision(ctx, record.UUID, record.Revision, map[string]interface{}{
|
|
"owner": constant.FirewallRuleSourceUser,
|
|
}); err != nil {
|
|
return fmt.Errorf("release accepted firewall port rule %q: %w", record.UUID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newIptablesHelperManager() *iptables_helper.Manager {
|
|
return &iptables_helper.Manager{
|
|
UpdateSetting: settingRepo.Update,
|
|
PanelPort: LoadPanelPort,
|
|
LoadRequiredPorts: LoadRequiredFirewallPortWhiteList,
|
|
}
|
|
}
|
|
|
|
func newNftablesHelperManager() *nftables_helper.Manager {
|
|
return &nftables_helper.Manager{
|
|
UpdateSetting: settingRepo.Update,
|
|
LoadRequiredPorts: LoadRequiredFirewallPortWhiteList,
|
|
}
|
|
}
|
|
|
|
func loadDirectFirewallInitStatus(provider string) (bool, bool, error) {
|
|
return loadFirewallInitStatus(provider, "base")
|
|
}
|
|
|
|
func loadFirewallInitStatus(provider, tab string) (bool, bool, error) {
|
|
switch provider {
|
|
case constant.FirewallProviderNftables:
|
|
return nftables_helper.LoadInitStatus(tab)
|
|
case constant.FirewallProviderIptables:
|
|
return iptables_helper.LoadInitStatus(tab)
|
|
default:
|
|
return false, false, fmt.Errorf("unsupported firewall provider: %s", provider)
|
|
}
|
|
}
|
|
|
|
func supportsManagedFilterChains(provider string) bool {
|
|
return provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables
|
|
}
|
|
|
|
func (s *FirewallService) addPortsBeforeStart(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 := loadDirectFirewallInitStatus(string(provider))
|
|
if err != nil {
|
|
recordFailure("load managed chain status", err)
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
if !isInit {
|
|
return nil
|
|
}
|
|
}
|
|
if err := s.restoreStoredFirewallRules(ctx, provider); err != nil {
|
|
recordFailure("restore stored firewall rules", err)
|
|
}
|
|
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
|
|
if provider == filter.ProviderIptables {
|
|
if err := newIptablesHelperManager().SyncRequiredPorts(true); err != nil {
|
|
recordFailure("synchronize required ports", err)
|
|
}
|
|
} else if err := newNftablesHelperManager().SyncRequiredPorts(); err != nil {
|
|
recordFailure("synchronize required ports", err)
|
|
}
|
|
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
recordFailure("load configured accepted ports", err)
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
required, err := LoadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
recordFailure("load required accepted ports", err)
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
recordFailure(
|
|
"restore configured accepted ports",
|
|
s.SyncSystemPorts(ctx, nil, systemPorts(excludeFirewallPorts(configured, required))),
|
|
)
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
portWhitelist, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
recordFailure("load accepted ports", err)
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
recordFailure("restore accepted ports", s.SyncSystemPorts(ctx, nil, systemPorts(portWhitelist)))
|
|
return errors.Join(recoveryErrors...)
|
|
}
|
|
|
|
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
|
|
return newFirewallService().
|
|
SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current))
|
|
}
|
|
|
|
func systemPorts(ports []firewall.PortWhitelist) []dto.FirewallSystemPort {
|
|
result := make([]dto.FirewallSystemPort, 0, len(ports))
|
|
for _, port := range ports {
|
|
families := []string{port.Family}
|
|
if port.Family == "" {
|
|
families = []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
|
|
}
|
|
for _, family := range families {
|
|
result = append(result, dto.FirewallSystemPort{Family: family, Port: port.Port, Protocol: port.Protocol})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func excludeFirewallPorts(ports, excluded []firewall.PortWhitelist) []firewall.PortWhitelist {
|
|
return firewall.ExcludePorts(ports, excluded)
|
|
}
|