mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-21 00:24:12 +08:00
2552 lines
83 KiB
Go
2552 lines
83 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/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
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)
|
|
protectedPorts func() ([]firewall.PortWhitelist, error)
|
|
iptablesHelper *iptables_helper.Manager
|
|
cleanupBackend func(string) error
|
|
cleanupInactiveBackend func(string) error
|
|
resetBackend func(string) error
|
|
baseClient func() (lifecycle.Client, error)
|
|
installedProviders func() []string
|
|
}
|
|
|
|
type firewallRuleRuntimeResolver interface {
|
|
Resolve(filter.Provider) (*firewallRuleRuntime, error)
|
|
Providers() []filter.Provider
|
|
}
|
|
|
|
var firewallRuleMutationMu sync.Mutex
|
|
|
|
type IFirewallService interface {
|
|
LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error)
|
|
OperateFirewall(request dto.FirewallLifecycleOperation) error
|
|
OperateFilterChain(request dto.FilterChainOperation) 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: newFirewallRuleRuntimeRegistry(firewallRuleSnapshotPolicy),
|
|
forwardingSync: newForwardingService(),
|
|
dockerSync: newDockerPortGuardService(),
|
|
selectedProvider: firewallRuleSelectedProvider,
|
|
protectedPorts: loadFirewallPortWhiteList,
|
|
iptablesHelper: newIptablesHelperManager(),
|
|
cleanupBackend: cleanupSystemBackend,
|
|
cleanupInactiveBackend: cleanupInactiveSystemBackend,
|
|
resetBackend: resetServiceFirewallBackend,
|
|
baseClient: selectedSystemFirewallClient,
|
|
installedProviders: lifecycle.InstalledProviders,
|
|
}
|
|
}
|
|
|
|
func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) {
|
|
status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"}
|
|
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)
|
|
}
|
|
loadInstalled := s.installedProviders
|
|
if loadInstalled == nil {
|
|
loadInstalled = lifecycle.InstalledProviders
|
|
}
|
|
if len(loadInstalled()) > 0 {
|
|
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)
|
|
if chainGroup == "base" {
|
|
status.ConflictBackend = conflictingDirectFirewallBackend(runtimeStatus.Name)
|
|
}
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func conflictingDirectFirewallBackend(provider string) string {
|
|
other := ""
|
|
switch provider {
|
|
case constant.FirewallProviderIptables:
|
|
other = constant.FirewallProviderNftables
|
|
case constant.FirewallProviderNftables:
|
|
other = constant.FirewallProviderIptables
|
|
default:
|
|
return ""
|
|
}
|
|
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
|
var (
|
|
bound bool
|
|
err error
|
|
)
|
|
if other == constant.FirewallProviderIptables {
|
|
bound, err = iptables_helper.LoadFamilyBindStatus(family)
|
|
} else {
|
|
bound, err = nftables_helper.LoadFamilyBindStatus(filter.Family(family))
|
|
}
|
|
if err == nil && bound {
|
|
return other
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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)
|
|
}
|
|
client, err := selectedSystemFirewallClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := lifecycle.NewOperator(client).Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, s.addPortsBeforeStart); err != nil {
|
|
return err
|
|
}
|
|
if request.Operation == "start" || request.Operation == "restart" {
|
|
ReconcileDockerPortGuardBestEffort(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) error {
|
|
provider, err := selectedSystemFirewallProvider()
|
|
if 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
|
|
}
|
|
if request.Operate != string(firewall.BaseOperationInit) && request.Operate != string(firewall.BaseOperationBind) {
|
|
return nil
|
|
}
|
|
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
required, err := loadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ports := excludeFirewallPorts(configured, required)
|
|
return s.SyncSystemPorts(context.Background(), 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
|
|
}
|
|
if err := reset(string(provider)); 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) error {
|
|
client, err := lifecycle.NewClientFor(provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resetter, ok := client.(lifecycle.Resetter)
|
|
if !ok {
|
|
return fmt.Errorf("firewall provider %s does not support reset", provider)
|
|
}
|
|
if provider == constant.FirewallProviderFirewalld {
|
|
if err := lifecycle.NewOperator(client).Operate(lifecycle.OperationStop, false, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return resetter.Reset()
|
|
}
|
|
|
|
func (s *FirewallService) deleteFirewallRuleRecords(
|
|
ctx context.Context,
|
|
stored []model.FirewallRule,
|
|
) (int, error) {
|
|
deleted := 0
|
|
for _, record := range stored {
|
|
if err := s.rules.DeleteWithRevision(ctx, record.UUID, record.Revision); err != nil {
|
|
return deleted, fmt.Errorf("delete reset firewall rule %q: %w", record.UUID, err)
|
|
}
|
|
deleted++
|
|
}
|
|
return deleted, nil
|
|
}
|
|
|
|
func (s *FirewallService) loadProtectedPorts() ([]firewall.PortWhitelist, error) {
|
|
if s.protectedPorts == nil {
|
|
return nil, nil
|
|
}
|
|
return s.protectedPorts()
|
|
}
|
|
|
|
func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) {
|
|
scope := request.Scope.Normalize()
|
|
if isCombinedUFWInventoryScope(scope) {
|
|
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
|
|
}
|
|
return s.combinedUFWInventory(ctx, runtime, scope)
|
|
}
|
|
if err := scope.ValidateMVP(); err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
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
|
|
}
|
|
snapshot, err := runtime.Observe(ctx, scope)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
desired, err := s.desiredFirewallRulesForScope(ctx, stored, scope)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
return dto.FirewallRuleInventoryResponse{Items: items, Notices: snapshot.Notices}, nil
|
|
}
|
|
|
|
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 *firewallRuleRuntime,
|
|
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)
|
|
}
|
|
|
|
response := dto.FirewallRuleInventoryResponse{}
|
|
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())
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
desired, err := s.desiredFirewallRulesForScope(ctx, stored, snapshot.Scope)
|
|
if err != nil {
|
|
return dto.FirewallRuleInventoryResponse{}, err
|
|
}
|
|
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 protectedPorts []firewall.PortWhitelist
|
|
var selectedProvider filter.Provider
|
|
createStateLoaded := false
|
|
type checkState struct {
|
|
snapshot filter.Snapshot
|
|
desired []filter.DesiredRule
|
|
managedRevision string
|
|
}
|
|
states := make(map[string]checkState)
|
|
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
|
|
protectedPorts, err = s.loadProtectedPorts()
|
|
if err != nil {
|
|
return dto.FirewallRuleCheckResponse{}, err
|
|
}
|
|
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
|
|
}
|
|
stored, listErr := s.rules.List(ctx)
|
|
if listErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, listErr
|
|
}
|
|
desired, desiredErr := s.desiredFirewallRulesForScope(ctx, stored, rule.Scope)
|
|
if desiredErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, desiredErr
|
|
}
|
|
managedRevision, revisionErr := model.FirewallRulesRevision(stored)
|
|
if revisionErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, revisionErr
|
|
}
|
|
state = checkState{snapshot: snapshot, desired: desired, managedRevision: managedRevision}
|
|
states[scopeKey] = state
|
|
}
|
|
checked, checkErr := filter.CheckCreate(state.snapshot, rule, state.desired, clientIP, protectedPorts...)
|
|
if checkErr != nil {
|
|
return dto.FirewallRuleCheckResponse{}, checkErr
|
|
}
|
|
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 *firewallRuleRuntime
|
|
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()
|
|
|
|
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 *firewallRuleRuntime
|
|
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
|
|
}
|
|
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
|
|
}
|
|
created := make([]createdFirewallBatchRule, 0, len(prepared))
|
|
nextSequence, err := s.nextFirewallRuleSequence(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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 recordErr = s.ensureFirewallRuleIdentityAvailable(ctx, record, ""); recordErr != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, recordErr)
|
|
}
|
|
if recordErr = s.rules.Create(ctx, &record); recordErr != nil {
|
|
return s.cleanupFirewallBatchRecords(ctx, created, recordErr)
|
|
}
|
|
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 *firewallRuleRuntime
|
|
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()
|
|
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); 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 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 {
|
|
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)
|
|
}
|
|
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 {
|
|
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 *firewallRuleRuntime,
|
|
snapshot filter.Snapshot,
|
|
request dto.FirewallRuleCreateItem,
|
|
authorization firewallRuleCreateAuthorization,
|
|
) error {
|
|
domainRule := request.Rule
|
|
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) 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
|
|
}
|
|
type appliedDelete struct {
|
|
runtime *firewallRuleRuntime
|
|
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 {
|
|
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)
|
|
}
|
|
observed, managedErr := filter.ManagedObserved(snapshot, desired)
|
|
if managedErr != nil {
|
|
if errors.Is(managedErr, filter.ErrRuleStale) {
|
|
items, mergeErr := filter.MergeInventory(filter.InventoryMergeInput{
|
|
Observed: snapshot.Rules,
|
|
Desired: []filter.DesiredRule{desired},
|
|
})
|
|
if mergeErr != nil {
|
|
return rollback(mergeErr)
|
|
}
|
|
missing := false
|
|
for _, item := range items {
|
|
if item.Desired != nil && item.Desired.UUID == desired.UUID {
|
|
missing = item.Match == filter.InventoryMatchMissing && item.Observed == nil
|
|
break
|
|
}
|
|
}
|
|
if missing {
|
|
continue
|
|
}
|
|
}
|
|
return rollback(managedErr)
|
|
}
|
|
restoreAtEnd := false
|
|
if desired.Rule.Scope.Provider == filter.ProviderUFW && observed.Locator.Position != nil {
|
|
maxPosition, maxErr := runtime.MaxPosition(ctx, snapshot, desired.Rule)
|
|
if maxErr != nil {
|
|
return rollback(maxErr)
|
|
}
|
|
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 (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
|
|
}
|
|
protectedPorts, err := s.loadProtectedPorts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := filter.GuardMutation(snapshot, observed, after, clientIP, protectedPorts...); 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 *firewallRuleRuntime
|
|
}
|
|
|
|
type preparedManagedUpdate struct {
|
|
Stored model.FirewallRule
|
|
Before filter.DesiredRule
|
|
After filter.FirewallRule
|
|
Snapshot filter.Snapshot
|
|
Observed filter.ObservedRule
|
|
Runtime *firewallRuleRuntime
|
|
}
|
|
|
|
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 after.NativeKind != before.Rule.NativeKind {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
protectedPorts, err := s.loadProtectedPorts()
|
|
if err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
if err := filter.GuardMutation(snapshot, observed, after, clientIP, protectedPorts...); err != nil {
|
|
return preparedManagedUpdate{}, err
|
|
}
|
|
return preparedManagedUpdate{
|
|
Stored: stored, Before: before, After: after, Snapshot: snapshot, Observed: observed, Runtime: runtime,
|
|
}, nil
|
|
}
|
|
|
|
func (s *FirewallService) loadManagedMutation(
|
|
ctx context.Context,
|
|
ruleUUID string,
|
|
) (model.FirewallRule, filter.DesiredRule, filter.Snapshot, filter.ObservedRule, *firewallRuleRuntime, error) {
|
|
stored, err := s.rules.GetByUUID(ctx, ruleUUID)
|
|
if err != nil {
|
|
return model.FirewallRule{}, filter.DesiredRule{}, filter.Snapshot{}, filter.ObservedRule{}, nil, err
|
|
}
|
|
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
|
|
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, maxErr := request.Runtime.MaxPosition(ctx, request.Snapshot, after)
|
|
if maxErr != nil {
|
|
return maxErr
|
|
}
|
|
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 (s *FirewallService) ensureFirewallRuleIdentityAvailable(
|
|
ctx context.Context,
|
|
requested model.FirewallRule,
|
|
excludedUUID string,
|
|
) error {
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
requestedKey := requested.PolicyKey()
|
|
for _, candidate := range stored {
|
|
if candidate.UUID == excludedUUID {
|
|
continue
|
|
}
|
|
if candidate.PolicyKey() == requestedKey {
|
|
return fmt.Errorf("%w: equivalent managed rule already exists", filter.ErrRuleOperation)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (*firewallRuleRuntime, 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 *firewallRuleRuntime,
|
|
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 err
|
|
}
|
|
return s.createFirewallRuleItem(ctx, *create)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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.CheckClassificationCovered:
|
|
create.Action = filter.CheckActionCreateAnyway
|
|
case filter.CheckClassificationConflict:
|
|
if !containsFirewallCheckAction(check.AllowedActions, filter.CheckActionCreateAnyway) {
|
|
return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason)
|
|
}
|
|
create.Action = filter.CheckActionCreateAnyway
|
|
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 {
|
|
for {
|
|
stored, err := s.systemPortRecords(ctx, port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(stored) == 0 {
|
|
adopted, err := s.adoptExternalSystemPort(ctx, port)
|
|
if err != nil || !adopted {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
for _, rule := range stored {
|
|
result, err := s.Delete(ctx, dto.FirewallRuleDelete{UUIDs: []string{rule.UUID}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.Failed > 0 {
|
|
if len(result.Errors) > 0 {
|
|
return errors.New(result.Errors[0].Error)
|
|
}
|
|
return errors.New("delete firewall rule failed")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (s *FirewallService) adoptExternalSystemPort(ctx context.Context, port dto.FirewallSystemPort) (bool, error) {
|
|
provider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
check, err := s.checkRule(ctx, "", dto.FirewallRuleCheckItem{Rule: systemPortRule(provider, port)})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if check.Classification != filter.CheckClassificationExactExternal || len(check.Candidates) == 0 {
|
|
return false, nil
|
|
}
|
|
create := dto.FirewallRuleCreateItem{
|
|
Rule: check.RequestedRule,
|
|
CheckFlag: check.CheckFlag,
|
|
AdoptInstanceKey: check.Candidates[0].InstanceKey,
|
|
SourceKind: constant.FirewallRuleSourceSecurity,
|
|
SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port),
|
|
}
|
|
if len(check.Candidates) == 1 {
|
|
create.Action = filter.CheckActionAdopt
|
|
} else {
|
|
create.Action = filter.CheckActionSelectAdopt
|
|
}
|
|
if err := s.createFirewallRuleItem(ctx, create); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
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 mergeFirewallInventory(
|
|
observed []filter.ObservedRule,
|
|
stored []model.FirewallRule,
|
|
protectedObservedKeys map[string]struct{},
|
|
usage map[string]filter.RuntimeUsage,
|
|
) ([]filter.InventoryItem, error) {
|
|
desired := make([]filter.DesiredRule, 0, len(stored))
|
|
for _, storedRule := range stored {
|
|
rule, err := desiredFirewallRuleFromModel(storedRule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
desired = append(desired, rule)
|
|
}
|
|
items, err := filter.MergeInventory(filter.InventoryMergeInput{
|
|
Observed: observed,
|
|
Desired: desired,
|
|
ProtectedObservedKeys: protectedObservedKeys,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filter.AttachRuntimeUsage(items, usage), nil
|
|
}
|
|
|
|
func desiredFirewallRuleFromModel(stored model.FirewallRule) (filter.DesiredRule, error) {
|
|
rules, err := stored.RulesForProvider(filter.ProviderIptables)
|
|
if err != nil {
|
|
return filter.DesiredRule{}, err
|
|
}
|
|
if len(rules) != 1 {
|
|
return filter.DesiredRule{}, fmt.Errorf(
|
|
"%w: policy %q expands to %d canonical rules", filter.ErrUnsupportedScope, stored.UUID, len(rules),
|
|
)
|
|
}
|
|
rule := rules[0]
|
|
rule.UUID = stored.UUID
|
|
ruleKey, err := filter.RuleKey(rule)
|
|
if err != nil {
|
|
return filter.DesiredRule{}, err
|
|
}
|
|
return filter.DesiredRule{
|
|
UUID: stored.UUID, Rule: rule, RuleKey: ruleKey, Origin: filter.RuleOrigin(stored.Origin),
|
|
Marker: "1panel-rule:" + stored.UUID,
|
|
}, nil
|
|
}
|
|
|
|
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 (s *FirewallService) desiredFirewallRulesForScope(
|
|
ctx context.Context,
|
|
stored []model.FirewallRule,
|
|
scope filter.Scope,
|
|
) ([]filter.DesiredRule, error) {
|
|
model.SortFirewallRules(stored, scope.Provider)
|
|
desired := make([]filter.DesiredRule, 0, len(stored))
|
|
for _, record := range stored {
|
|
compiled, err := s.compileStoredFirewallRules(ctx, record, scope.Provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, rule := range compiled {
|
|
if rule.Rule.Scope.Key() == scope.Key() {
|
|
desired = append(desired, rule)
|
|
}
|
|
}
|
|
}
|
|
return desired, nil
|
|
}
|
|
|
|
func firewallRuleSnapshotPolicy(_ context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) {
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return filter.Snapshot{}, err
|
|
}
|
|
return filter.ProtectSnapshot(snapshot, ports)
|
|
}
|
|
|
|
func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
|
|
return selectedRuleProvider()
|
|
}
|
|
|
|
type firewallSnapshotPolicy = filterruntime.SnapshotPolicy
|
|
type firewallRuleRuntime = filterruntime.Engine
|
|
type firewallRuleRuntimeRegistry = filterruntime.Registry
|
|
|
|
func newFirewallRuleRuntimeRegistry(policy firewallSnapshotPolicy) firewallRuleRuntimeRegistry {
|
|
return filterruntime.NewRegistry(policy)
|
|
}
|
|
|
|
func newFirewallRuleRuntime(adapter filter.Adapter, policy firewallSnapshotPolicy) *firewallRuleRuntime {
|
|
return filterruntime.New(adapter, policy)
|
|
}
|
|
|
|
func rollbackFirewallPlan(ctx context.Context, runtime *firewallRuleRuntime, 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 containsFirewallCheckAction(actions []filter.CheckAction, expected filter.CheckAction) bool {
|
|
for _, action := range actions {
|
|
if action == expected {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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) {
|
|
for _, key := range sortedSystemPortKeys(currentSet) {
|
|
if _, exists := previousSet[key]; exists {
|
|
continue
|
|
}
|
|
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, key := range sortedSystemPortKeys(previousSet) {
|
|
if _, exists := currentSet[key]; exists {
|
|
continue
|
|
}
|
|
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
removed := make([]dto.FirewallSystemPort, 0)
|
|
deleteUUIDs := make([]string, 0)
|
|
for _, key := range sortedSystemPortKeys(previousSet) {
|
|
if _, exists := currentSet[key]; exists {
|
|
continue
|
|
}
|
|
port := previousSet[key]
|
|
removed = append(removed, port)
|
|
stored, listErr := s.systemPortRecords(ctx, port)
|
|
if listErr != nil {
|
|
return listErr
|
|
}
|
|
for _, rule := range stored {
|
|
deleteUUIDs = append(deleteUUIDs, rule.UUID)
|
|
}
|
|
}
|
|
if len(deleteUUIDs) > 0 {
|
|
result, batchErr := s.Delete(ctx, dto.FirewallRuleDelete{UUIDs: deleteUUIDs})
|
|
if batchErr != nil {
|
|
return batchErr
|
|
}
|
|
if result.Failed > 0 {
|
|
if len(result.Errors) > 0 {
|
|
return fmt.Errorf("batch delete accepted firewall ports: %s", result.Errors[0].Error)
|
|
}
|
|
return fmt.Errorf("batch delete accepted firewall ports failed")
|
|
}
|
|
}
|
|
for _, port := range removed {
|
|
if err := s.deleteSystemPort(ctx, port); 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) {
|
|
panelPort := LoadPanelPort()
|
|
if panelPort == "" {
|
|
return nil, fmt.Errorf("find 1panel service port failed")
|
|
}
|
|
return firewall.NormalizePortWhitelist([]firewall.PortWhitelist{
|
|
{Port: panelPort, Protocol: "tcp"},
|
|
{Port: loadSSHPort(), Protocol: "tcp"},
|
|
}), nil
|
|
}
|
|
|
|
func SyncFirewallPortWhitelistAfterUpdate(oldValue string) 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
|
|
}
|
|
} else if !state.IsActive {
|
|
return nil
|
|
}
|
|
ports, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldPorts, err := firewall.ParsePortWhitelist(oldValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
required, err := loadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables {
|
|
ports = excludeFirewallPorts(ports, required)
|
|
oldPorts = excludeFirewallPorts(oldPorts, required)
|
|
} else {
|
|
ports = firewall.NormalizePortWhitelist(append(ports, required...))
|
|
oldPorts = firewall.NormalizePortWhitelist(append(oldPorts, required...))
|
|
}
|
|
return syncManagedAcceptedPorts(oldPorts, ports)
|
|
}
|
|
|
|
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 {
|
|
if client.Name() == constant.FirewallProviderIptables || client.Name() == constant.FirewallProviderNftables {
|
|
isInit, _, err := loadDirectFirewallInitStatus(client.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isInit {
|
|
return nil
|
|
}
|
|
if client.Name() == constant.FirewallProviderIptables {
|
|
if err := newIptablesHelperManager().SyncRequiredPorts(true); err != nil {
|
|
return err
|
|
}
|
|
} else if err := newNftablesHelperManager().SyncRequiredPorts(); err != nil {
|
|
return err
|
|
}
|
|
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
required, err := loadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.SyncSystemPorts(context.Background(), nil, systemPorts(excludeFirewallPorts(configured, required)))
|
|
}
|
|
portWhitelist, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.SyncSystemPorts(context.Background(), nil, systemPorts(portWhitelist))
|
|
}
|
|
|
|
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 {
|
|
result = append(result, dto.FirewallSystemPort{Family: port.Family, Port: port.Port, Protocol: port.Protocol})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func excludeFirewallPorts(ports, excluded []firewall.PortWhitelist) []firewall.PortWhitelist {
|
|
return firewall.ExcludePorts(ports, excluded)
|
|
}
|
|
|
|
const (
|
|
sshConfigPath = "/etc/ssh/sshd_config"
|
|
defaultFirewallSSHPort = "22"
|
|
)
|
|
|
|
func loadSSHPort() string {
|
|
content, err := os.ReadFile(sshConfigPath)
|
|
if err != nil {
|
|
return defaultFirewallSSHPort
|
|
}
|
|
for _, line := range strings.Split(string(content), "\n") {
|
|
if !strings.HasPrefix(line, "Port ") {
|
|
continue
|
|
}
|
|
port := strings.TrimSpace(strings.TrimPrefix(line, "Port "))
|
|
value, _ := strconv.Atoi(port)
|
|
if value > 0 && value < 65535 {
|
|
return port
|
|
}
|
|
}
|
|
return defaultFirewallSSHPort
|
|
}
|