refactor(firewall): simplify whitelist configuration and rule protection (#13838)

This commit is contained in:
ssongliu
2026-09-16 22:09:49 +08:00
committed by GitHub
parent e864610015
commit 673ffac516
46 changed files with 564 additions and 1328 deletions

View File

@@ -368,8 +368,8 @@ func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
}
// @Tags Firewall
// @Summary Queue managed firewall rule deletion
// @Description Returns a taskID immediately; per-rule deletion results and failures are written to the task log.
// @Summary Queue firewall rule deletion
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
// @Accept json
// @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -494,10 +494,10 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
// @Tags Firewall
// @Summary Create firewall port whitelist rules
// @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post]
@@ -507,20 +507,19 @@ func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request)
if err != nil {
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Update firewall port whitelist rules
// @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/update [post]
@@ -530,20 +529,19 @@ func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request)
if err != nil {
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Delete firewall port whitelist rules
// @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/delete [post]
@@ -553,12 +551,11 @@ func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request)
if err != nil {
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall

View File

@@ -328,7 +328,13 @@ type FirewallRuleSyncFailure struct {
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
}
type FirewallRuleDeleteTarget struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
}
type FirewallRuleDeleteResponse struct {

View File

@@ -29,12 +29,6 @@ type FirewallRuleRepo struct {
db *gorm.DB
}
func WithFirewallRuleSource(kind, id string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
}
}
func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{}
}

View File

@@ -396,7 +396,7 @@ func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uin
if oldPort == port {
return nil
}
return s.syncSystemAccessPortTransition(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))})
return updateSystemAccessPortWhitelist(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))})
}
func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error {
@@ -1402,10 +1402,11 @@ func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDe
if err := ctx.Err(); err != nil {
return dto.FirewallRuleDeleteResponse{}, err
}
if len(request.UUIDs) == 0 {
return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("%w: rule UUIDs are required", filter.ErrInvalidRule)
if len(request.UUIDs) == 0 && len(request.BeforeRules) == 0 {
return dto.FirewallRuleDeleteResponse{}, fmt.Errorf("%w: rules are required", filter.ErrInvalidRule)
}
request.UUIDs = append([]string(nil), request.UUIDs...)
request.BeforeRules = append([]dto.FirewallRuleDeleteTarget(nil), request.BeforeRules...)
taskItem, err := task.NewTask(firewallTaskName(task.TaskDelete, firewallTaskHost, ""), task.TaskDelete, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FirewallRuleDeleteResponse{}, err
@@ -1428,7 +1429,7 @@ func (s *FirewallService) Delete(ctx context.Context, request dto.FirewallRuleDe
}
func (s *FirewallService) runDeleteTask(t *task.Task, request dto.FirewallRuleDelete) (dto.FirewallRuleDeleteResponse, error) {
t.Logf("rules=%d", len(request.UUIDs))
t.Logf("rules=%d", len(request.UUIDs)+len(request.BeforeRules))
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := t.TaskCtx.Err(); err != nil {
@@ -1461,7 +1462,7 @@ func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallR
result.Succeeded++
}
if t != nil {
label := fmt.Sprintf("[%d/%d] %s", result.Succeeded+result.Failed, len(request.UUIDs), ruleUUID)
label := fmt.Sprintf("[%d/%d] %s", result.Succeeded+result.Failed, len(request.UUIDs)+len(request.BeforeRules), ruleUUID)
t.LogWithStatus(label, err)
}
}
@@ -1469,6 +1470,26 @@ func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallR
if err != nil {
return result, err
}
type beforeGroup struct {
targets []dto.FirewallRuleDeleteTarget
indexes []int
}
beforeGroups := make(map[string]*beforeGroup)
for index, target := range request.BeforeRules {
key := target.Scope.Normalize().Key()
if beforeGroups[key] == nil {
beforeGroups[key] = &beforeGroup{}
}
group := beforeGroups[key]
group.targets = append(group.targets, target)
group.indexes = append(group.indexes, len(request.UUIDs)+index)
}
for _, group := range beforeGroups {
err := s.deleteBeforeRules(ctx, selectedProvider, group.targets)
for index, target := range group.targets {
record(group.indexes[index], target.InstanceKey, err)
}
}
type deleteGroup struct{ items []preparedFirewallRuleDelete }
groups := make([]deleteGroup, 0)
groupIndexes := make(map[string]int)
@@ -1521,7 +1542,7 @@ func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallR
if t != nil {
t.Logf("%s %s", i18n.GetMsgByKey(task.TaskDelete), item.stored.UUID)
}
err = s.deleteRule(ctx, item.stored.UUID, false)
err = s.deleteRule(ctx, item.stored.UUID)
}
record(item.index, item.stored.UUID, err)
}
@@ -1529,6 +1550,59 @@ func (s *FirewallService) deleteRules(ctx context.Context, request dto.FirewallR
return result, nil
}
func (s *FirewallService) deleteBeforeRules(ctx context.Context, provider filter.Provider, targets []dto.FirewallRuleDeleteTarget) error {
scope := targets[0].Scope.Normalize()
if scope.Provider != provider || !isDirectFirewallProvider(provider) || scope.Chain != filter.BasicBeforeChain {
return fmt.Errorf("%w: native deletion only supports the selected firewall before chain", filter.ErrUnsupportedScope)
}
runtime, err := s.adapters.Resolve(provider)
if err != nil {
return err
}
snapshot, err := runtime.ObserveMutation(ctx, scope)
if err != nil {
return err
}
changes := make([]filter.DesiredChange, 0, len(targets))
seen := make(map[string]bool, len(targets))
for _, target := range targets {
if target.Scope.Normalize().Key() != scope.Key() || seen[target.InstanceKey] {
return fmt.Errorf("%w: duplicate or mismatched before rule", filter.ErrInvalidRule)
}
seen[target.InstanceKey] = true
observed, err := filter.FindCandidate(snapshot.Rules, target.InstanceKey)
if err != nil {
return filter.ErrRuleStale
}
if err := filter.GuardMutation(observed); err != nil {
return err
}
if observed.ParseStatus != filter.ParseStatusSupported || observed.Locator.Position == nil {
return fmt.Errorf("%w: before rule cannot be deleted", filter.ErrUnsupportedScope)
}
rule := firewallsync.ObservedRule(observed)
if rule.UUID == "" {
rule.UUID = uuid.NewString()
}
locator := observed.Locator
changes = append(changes, filter.DesiredChange{
Operation: filter.ChangeDelete, Before: &rule, Locator: &locator, UnmarkedAdopted: observed.Marker == "",
})
}
sort.Slice(changes, func(i, j int) bool { return *changes[i].Locator.Position > *changes[j].Locator.Position })
plan, verification, err := runtime.Execute(ctx, snapshot, changes)
if err != nil {
return err
}
if !verification.Matched {
return filter.ErrVerificationFailed
}
if len(verification.Snapshot.Rules) != len(snapshot.Rules)-len(changes) {
return rollbackFirewallPlan(ctx, runtime, plan, filter.ErrVerificationFailed)
}
return nil
}
func (s *FirewallService) prepareDelete(
ctx context.Context,
index int,
@@ -1732,6 +1806,9 @@ func (s *FirewallService) Adopt(ctx context.Context, request dto.FirewallRuleAdo
}
func (s *FirewallService) adoptRule(ctx context.Context, runtime *filterruntime.Engine, snapshot filter.Snapshot, observed filter.ObservedRule, source dto.FirewallRuleCreateItem) error {
if isIptablesSystemPresetInventoryScope(observed.Rule.Scope) {
return fmt.Errorf("%w: system preset chains cannot be adopted", filter.ErrUnsupportedScope)
}
if observed.Protected {
return filter.ErrProtectedRule
}
@@ -1801,7 +1878,7 @@ func (s *FirewallService) adoptRule(ctx context.Context, runtime *filterruntime.
return s.saveFirewallRule(ctx, &record)
}
func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allowProtected bool) error {
func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string) error {
if ruleUUID == "" {
return fmt.Errorf("%w: rule UUID is required", repo.ErrFirewallPersistenceInvalid)
}
@@ -1823,16 +1900,8 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allow
if err != nil {
return err
}
if ctx.Value(firewallWhitelistOverrideKey{}) != nil {
ports, err := firewallWhitelistForProtection(ctx)
if err != nil {
return err
}
for _, desired := range desiredRules {
if filter.RuleMatchesPortWhitelist(desired.Rule, ports) {
return filter.ErrProtectedRule
}
}
if err := checkFirewallRuleWhitelistProtection(ctx, stored); err != nil {
return err
}
type appliedDelete struct {
runtime *filterruntime.Engine
@@ -1846,9 +1915,6 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allow
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)
@@ -1857,11 +1923,6 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allow
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) {
@@ -2473,53 +2534,6 @@ func (s *FirewallService) ensureSystemPortLocked(ctx context.Context, port dto.F
return err
}
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(firewallWhitelistOverrideKey{}) != 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{systemPortSourceID(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 hasSystemFirewallRuleOwner(rule model.FirewallRule) bool {
acceptedPrefix := model.FirewallRuleOwner(
constant.FirewallRuleSourceSecurity,
@@ -2532,22 +2546,6 @@ func systemPortRule(provider filter.Provider, port dto.FirewallSystemPort) filte
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))
}
@@ -2556,18 +2554,6 @@ func systemPortSourceID(port dto.FirewallSystemPort) string {
return constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(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 {
@@ -2797,7 +2783,7 @@ func (s *FirewallService) desiredFirewallRulesByScope(
model.SortFirewallRules(stored, provider)
desired := make(map[string][]filter.DesiredRule)
var failures []filter.InventoryItem
ports, protectionErr := firewallWhitelistForProtection(ctx)
ports, protectionErr := loadFirewallPortWhiteList()
for _, record := range stored {
compiled, _, err := s.compileRestorableFirewallRules(ctx, record, provider)
if err == nil {
@@ -2831,29 +2817,13 @@ func (s *FirewallService) desiredFirewallRulesByScope(
}
func firewallRuleSnapshotPolicy(ctx context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) {
ports, err := firewallWhitelistForProtection(ctx)
ports, err := loadFirewallPortWhiteList()
if err != nil {
return filter.Snapshot{}, err
}
return filter.ProtectSnapshot(snapshot, ports)
}
func firewallWhitelistForProtection(ctx context.Context) ([]firewall.PortWhitelist, error) {
ports, overridden := ctx.Value(firewallWhitelistOverrideKey{}).([]firewall.PortWhitelist)
if !overridden {
var err error
ports, err = loadFirewallPortWhiteList()
if err != nil {
return nil, err
}
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, err
}
return append(customWhitelist(ports), required...), nil
}
func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
return selectedRuleProvider()
}
@@ -2876,7 +2846,7 @@ func selectedRuleProvider() (filter.Provider, error) {
return filter.Provider(provider), nil
}
func OperateFirewallPort(oldPorts, newPorts []int) error {
func ensureFirewallPorts(ports []int) error {
filterruntime.InvalidateInventory()
defer filterruntime.InvalidateInventory()
client, err := selectedSystemFirewallClient()
@@ -2895,39 +2865,34 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
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()
added := make([]firewall.PortWhitelist, 0, len(ports))
for _, port := range ports {
added = append(added, firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"})
}
normalized, err := firewall.NormalizeSystemPorts(firewall.ExpandPortWhitelist(added))
if err != nil {
return err
}
previous := make([]firewall.PortWhitelist, 0, len(oldPorts))
for _, port := range oldPorts {
item := firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"}
if !firewall.ContainsPort(current, item) {
previous = append(previous, item)
service := newFirewallService()
var failures []error
for _, key := range firewall.SortedSystemPortKeys(normalized) {
if err := service.ensureSystemPort(context.Background(), normalized[key]); err != nil {
wrapped := fmt.Errorf("allow firewall port %s: %w", key, err)
if supportsNativeRuleBatch(filter.Provider(state.Name)) {
return wrapped
}
failures = append(failures, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
}
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 = firewall.ExcludePorts(added, required)
}
return syncManagedAcceptedPorts(previous, added)
return errors.Join(failures...)
}
func LoadPanelPort() string {
@@ -2940,14 +2905,11 @@ func LoadPanelPort() string {
}
func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
ports, err := loadPortWhitelistSetting(global.DB)
if err != nil {
value = constant.FirewallPortWhiteListValue
if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
return nil, err
}
return nil, err
}
return firewall.ParsePortWhitelist(value)
return firewall.ValidatePortWhitelist(ports)
}
func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {

View File

@@ -8,16 +8,12 @@ import (
"os"
"reflect"
"slices"
"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/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
@@ -26,21 +22,20 @@ import (
"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/google/uuid"
"gorm.io/gorm"
)
type IFirewallSettingService interface {
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) (dto.FilterChainOperationResponse, error)
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) (dto.FilterChainOperationResponse, error)
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) (dto.FilterChainOperationResponse, error)
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
}
type FirewallSettingService struct{}
var firewallWhitelistTaskMu sync.Mutex
var firewallWhitelistMu sync.Mutex
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
@@ -57,41 +52,33 @@ func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
type portWhitelistChange func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)
type portWhitelistPlan struct {
current []firewall.PortWhitelist
desired []firewall.PortWhitelist
}
type firewallWhitelistOverrideKey struct{}
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (dto.FilterChainOperationResponse, error) {
return s.queuePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
rule, err := initializeRequestedWhitelistRule(request.Rule)
if err != nil {
return nil, err
}
return append(slices.Clone(current), rule), nil
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
return append(current, request.Rule), nil
})
}
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (dto.FilterChainOperationResponse, error) {
return s.queuePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
rule, err := initializeRequestedWhitelistRule(request.Rule)
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return nil, err
}
return replacePortWhitelistRule(current, request.OldRule, rule)
current[index] = request.Rule
return current, nil
})
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) (dto.FilterChainOperationResponse, error) {
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
if request.Rule == nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("select one firewall port whitelist rule to delete")
return fmt.Errorf("select one firewall port whitelist rule to delete")
}
return s.queuePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
return removePortWhitelistRule(current, *request.Rule)
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return nil, err
}
return slices.Delete(current, index, index+1), nil
})
}
@@ -122,406 +109,36 @@ func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
func replacePortWhitelistRule(current []firewall.PortWhitelist, oldRule, rule firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, oldRule)
if err != nil {
return nil, err
}
rules := slices.Clone(current)
rules[index] = rule
return rules, nil
}
func removePortWhitelistRule(current []firewall.PortWhitelist, target firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, target)
if err != nil {
return nil, err
}
return slices.Delete(slices.Clone(current), index, index+1), nil
}
func (s *FirewallSettingService) queuePortWhitelist(ctx context.Context, change portWhitelistChange) (dto.FilterChainOperationResponse, error) {
firewallService := newFirewallService()
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
plan, err := s.preparePortWhitelist(ctx, change)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem, err := task.NewTask(i18n.GetMsgByKey("FirewallWhitelistTask"), task.TaskUpdate, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
succeeded, failed := 0, 0
err := s.applyPortWhitelist(t.TaskCtx, plan, firewallService, true, func(status, label string, err error) {
switch status {
case "applied":
succeeded++
t.LogSuccess(label)
case "failed":
failed++
t.LogFailedWithErr(label, err)
default:
t.Log(i18n.GetWithName(status, label))
}
})
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall whitelist task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
type whitelistReporter func(status, label string, err error)
func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, plan portWhitelistPlan, firewallService *FirewallService, requireActive bool, report whitelistReporter) error {
if err := ctx.Err(); err != nil {
return err
}
ports := plan.desired
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
report("failed", "SSH / 1Panel", err)
return err
}
ctx = context.WithValue(ctx, firewallWhitelistOverrideKey{}, plan.desired)
provider, providerErr := firewallService.selectedProvider(ctx)
if providerErr != nil && !requireActive && configuredSystemFirewallBackend() == "" && len(lifecycle.InstalledProviders()) == 0 {
return s.executePortWhitelist(ctx, plan, func() error { return nil }, report)
}
rulesFor := func(entries []firewall.PortWhitelist) ([]filter.FirewallRule, error) {
valid := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if normalized, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{entry}); err == nil {
valid = append(valid, normalized...)
}
}
system, err := firewall.RequiredPortWhitelist(valid)
if err != nil {
return nil, err
}
return whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(valid)), firewall.ExpandPortWhitelist(system)), nil
}
committed, err := rulesFor(plan.current)
if err != nil {
return err
}
desired := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
removals, additions := whitelistRuleChanges(committed, desired)
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
current, err := firewallService.prepareWhitelistRules(ctx, provider, additions, ready, report)
if err != nil {
return err
}
if err := checkPortWhitelist(ctx, firewallService, provider, current, report); err != nil {
return err
}
previous, err := firewallService.prepareWhitelistRules(ctx, provider, removals, ready, report)
if err != nil {
return err
}
return s.executePortWhitelist(ctx, plan, func() error {
if err := syncWhitelistRules(ctx, firewallService, previous, current, report); err != nil {
return err
}
return firewallService.reconcilePortWhitelist(ctx, provider, ready, report)
}, report)
}
func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule {
rules := make([]filter.FirewallRule, 0, len(ports)+len(required))
for _, port := range required {
rule := systemPortRule(provider, port)
if isDirectFirewallProvider(provider) {
rule.Scope.Chain = filter.BasicBeforeChain
}
rules = append(rules, rule)
}
for _, port := range ports {
rules = append(rules, systemPortRule(provider, port))
}
return rules
}
func whitelistRuleChanges(committed, desired []filter.FirewallRule) (removals, additions []filter.FirewallRule) {
for _, rule := range desired {
if !whitelistContainsRule(committed, rule) {
additions = append(additions, rule)
}
}
for _, rule := range committed {
if !whitelistContainsRule(desired, rule) && !whitelistContainsRule(removals, rule) {
removals = append(removals, rule)
}
}
return removals, additions
}
func (s *FirewallService) prepareWhitelistRules(
ctx context.Context, provider filter.Provider, candidates []filter.FirewallRule,
ready func(dto.FirewallSystemPort) (bool, error), report whitelistReporter,
) ([]preparedFirewallRuleCreate, error) {
rules := make([]preparedFirewallRuleCreate, 0, len(candidates))
seen := make(filter.RuleCollisionIndex)
var failures []error
for _, rule := range candidates {
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
prepare := func() error {
if err := ctx.Err(); err != nil {
return err
}
active, err := ready(port)
if err != nil {
return err
}
if !active {
report("FirewallWhitelistDeferred", whitelistPortLabel(port), nil)
return nil
}
prepared, err := s.prepareCreate(ctx, provider, dto.FirewallRuleCreateItem{
Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: systemPortSourceID(port),
})
if err != nil {
return err
}
if err := seen.CheckDuplicate(prepared.request.Rule); errors.Is(err, filter.ErrRuleOperation) {
return nil
} else if err != nil {
return err
}
if err := seen.Add(prepared.request.Rule); err != nil {
return err
}
rules = append(rules, prepared)
return nil
}
if err := prepare(); err != nil {
label := whitelistPortLabel(port)
report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
}
}
return rules, errors.Join(failures...)
}
func syncWhitelistRules(ctx context.Context, service *FirewallService,
previous, current []preparedFirewallRuleCreate, report whitelistReporter,
) error {
apply := func(rules []preparedFirewallRuleCreate, operation string, run func(context.Context, preparedFirewallRuleCreate) error) error {
var failures []error
for _, prepared := range rules {
if err := ctx.Err(); err != nil {
return err
}
rule := prepared.request.Rule
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
label := whitelistPortLabel(port)
if err := run(ctx, prepared); err != nil {
report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
} else {
report("applied", i18n.GetMsgByKey(operation)+": "+label, nil)
}
}
return errors.Join(failures...)
}
if err := apply(current, task.TaskCreate, service.addWhitelistRule); err != nil {
return err
}
removals := make([]preparedFirewallRuleCreate, 0, len(previous))
for _, prepared := range previous {
if !slices.ContainsFunc(current, func(candidate preparedFirewallRuleCreate) bool {
same, err := filter.SameRuleContent(candidate.request.Rule, prepared.request.Rule)
return err == nil && same
}) {
removals = append(removals, prepared)
}
}
return apply(removals, task.TaskDelete, service.deleteWhitelistRule)
}
func whitelistContainsRule(rules []filter.FirewallRule, target filter.FirewallRule) bool {
for _, rule := range rules {
if same, err := filter.SameRuleContent(rule, target); err == nil && same {
return true
}
}
return false
}
func (s *FirewallService) addWhitelistRule(ctx context.Context, prepared preparedFirewallRuleCreate) error {
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
rule, runtime := prepared.request.Rule, prepared.runtime
snapshot, err := runtime.ObserveMutation(ctx, rule.Scope)
if err != nil {
return err
}
for _, observed := range snapshot.Rules {
if observed.ParseStatus == filter.ParseStatusSupported {
if same, err := filter.SameRuleContent(rule, observed.Rule); err == nil && same {
return nil
}
}
}
if err := filter.CheckObservedRuleCollisions(snapshot, rule, nil); err != nil {
return err
}
if rule.Scope.Chain == filter.BasicBeforeChain {
rule.UUID = uuid.NewString()
return runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &rule}})
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
for _, record := range stored {
compiled, err := s.compileStoredFirewallRules(ctx, record, rule.Scope.Provider)
defer filterruntime.InvalidateInventory()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
continue
return err
}
for _, candidate := range compiled {
if err := filter.CheckRuleCollision(rule, candidate.Rule); errors.Is(err, filter.ErrRuleOperation) {
return runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &candidate.Rule}})
} else if err != nil {
return err
}
desired, err := change(current)
if err != nil {
return err
}
}
return s.applyCreateRules(ctx, runtime, snapshot, stored, []preparedFirewallRuleCreate{prepared})[0]
desired, err = firewall.ValidatePortWhitelist(desired)
if err != nil {
return err
}
value, err := json.Marshal(desired)
if err != nil {
return err
}
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
})
}
func (s *FirewallService) deleteWhitelistRule(ctx context.Context, prepared preparedFirewallRuleCreate) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
rule, runtime := prepared.request.Rule, prepared.runtime
provider := rule.Scope.Provider
ports, err := firewallWhitelistForProtection(ctx)
if err != nil {
return err
}
if rule.Scope.Chain != filter.BasicBeforeChain && filter.RuleMatchesPortWhitelist(rule, ports) {
return nil
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
cleanup, _ := ctx.Value(firewallWhitelistCleanupKey{}).(bool)
if !cleanup || rule.Scope.Chain == filter.BasicBeforeChain {
if err := deleteNativeWhitelistRule(ctx, runtime, rule); err != nil {
return err
}
}
for _, record := range stored {
if cleanup && !hasSystemFirewallRuleOwner(record) {
continue
}
compiled, err := s.compileStoredFirewallRules(ctx, record, provider)
if err != nil {
continue
}
remaining := make([]filter.FirewallRule, 0, len(compiled))
for _, candidate := range compiled {
if !matchesWhitelistRemoval(rule, candidate.Rule, record) || filter.RuleMatchesPortWhitelist(candidate.Rule, ports) {
remaining = append(remaining, candidate.Rule)
continue
}
if candidate.Rule.Scope.Key() != rule.Scope.Key() || cleanup && rule.Scope.Chain != filter.BasicBeforeChain {
if err := deleteNativeWhitelistRule(ctx, runtime, candidate.Rule, candidate.Marker); err != nil {
return err
}
}
}
if len(remaining) == len(compiled) {
continue
}
if err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
ctx := context.WithValue(ctx, constant.DB, tx)
if err := s.rules.DeleteWithRevision(ctx, record.UUID, record.Revision); err != nil {
return err
}
for _, rule := range remaining {
kept, err := model.FirewallRuleFromDomain(rule)
if err != nil {
return err
}
kept.UUID, kept.Origin, kept.Owner, kept.Sequence = rule.UUID, record.Origin, record.Owner, record.Sequence
if err := s.rules.Create(ctx, &kept); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
}
return nil
}
func deleteNativeWhitelistRule(ctx context.Context, runtime *filterruntime.Engine, rule filter.FirewallRule, markers ...string) error {
for {
if err := ctx.Err(); err != nil {
return err
}
snapshot, err := runtime.ObserveMutation(ctx, rule.Scope)
if err != nil {
return err
}
matched := -1
for index, observed := range snapshot.Rules {
if len(markers) > 0 && observed.Marker != markers[0] {
continue
}
if observed.ParseStatus == filter.ParseStatusSupported {
if same, err := filter.SameRuleContent(rule, observed.Rule); err == nil && same {
matched = index
break
}
}
}
if matched < 0 {
break
}
observed := snapshot.Rules[matched]
before := observed.Rule
before.UUID = strings.TrimPrefix(observed.Marker, "1panel-rule:")
if before.UUID == "" {
before.UUID = uuid.NewString()
}
snapshot.Rules[matched].Protected = false
_, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{
Operation: filter.ChangeDelete, Before: &before, Locator: &observed.Locator, UnmarkedAdopted: observed.Marker == "",
}})
if err != nil {
return err
}
if !verification.Matched {
return filter.ErrVerificationFailed
}
}
return nil
}
type firewallWhitelistCleanupKey struct{}
func checkFirewallRuleWhitelistProtection(ctx context.Context, record model.FirewallRule) error {
ports, err := firewallWhitelistForProtection(ctx)
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
@@ -537,100 +154,6 @@ func checkFirewallRuleWhitelistProtection(ctx context.Context, record model.Fire
return nil
}
func matchesWhitelistRemoval(requested, candidate filter.FirewallRule, record model.FirewallRule) bool {
if isDirectFirewallProvider(requested.Scope.Provider) && requested.Scope.Chain == filter.BasicBeforeChain && hasSystemFirewallRuleOwner(record) {
candidate.Scope.Chain = filter.BasicBeforeChain
}
return whitelistContainsRule([]filter.FirewallRule{requested}, candidate)
}
func isWhitelistPortAllowance(rule filter.FirewallRule) bool {
normalized, err := filter.NormalizeRule(rule)
return err == nil && normalized.Action == filter.ActionAccept &&
(normalized.Protocol == "tcp" || normalized.Protocol == "udp") && normalized.DestinationPort != "" &&
normalized.SourcePort == "" && normalized.DestinationAddress == "" && normalized.Interface == "" && len(normalized.ConnectionStates) == 0
}
func (s *FirewallService) reconcilePortWhitelist(ctx context.Context, provider filter.Provider,
ready func(dto.FirewallSystemPort) (bool, error), report whitelistReporter,
) error {
if !isDirectFirewallProvider(provider) {
return nil
}
ports, err := firewallWhitelistForProtection(ctx)
if err != nil {
return err
}
runtime, err := s.adapters.Resolve(provider)
if err != nil {
return err
}
var obsolete []filter.FirewallRule
for _, scope := range filter.ManagedInputScopes(provider) {
if scope.Chain != filter.BasicBeforeChain {
continue
}
active, err := ready(dto.FirewallSystemPort{Family: string(scope.Family)})
if err != nil {
return err
}
if !active {
continue
}
snapshot, err := runtime.ObserveMutation(ctx, scope)
if errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return err
}
for _, observed := range snapshot.Rules {
if observed.ParseStatus == filter.ParseStatusSupported && isWhitelistPortAllowance(observed.Rule) &&
!filter.RuleMatchesPortWhitelist(observed.Rule, ports) {
obsolete = append(obsolete, observed.Rule)
}
}
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
for _, record := range stored {
if !hasSystemFirewallRuleOwner(record) {
continue
}
compiled, err := s.compileStoredFirewallRules(ctx, record, provider)
if err != nil {
return err
}
for _, candidate := range compiled {
if isWhitelistPortAllowance(candidate.Rule) && !filter.RuleMatchesPortWhitelist(candidate.Rule, ports) {
obsolete = append(obsolete, candidate.Rule)
}
}
}
prepared, err := s.prepareWhitelistRules(ctx, provider, obsolete, ready, report)
if err != nil {
return err
}
return syncWhitelistRules(context.WithValue(ctx, firewallWhitelistCleanupKey{}, true), s, prepared, nil, report)
}
func (s *FirewallSettingService) preparePortWhitelist(ctx context.Context, change portWhitelistChange) (portWhitelistPlan, error) {
var plan portWhitelistPlan
var err error
plan.current, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
if err != nil {
return plan, err
}
plan.desired, err = change(plan.current)
if err != nil {
return plan, err
}
plan.desired, err = firewall.ValidatePortWhitelist(plan.desired)
return plan, err
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
@@ -643,151 +166,6 @@ func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
return rules, err
}
func writePortWhitelistSetting(db *gorm.DB, rules []firewall.PortWhitelist) error {
value, err := json.Marshal(rules)
if err != nil {
return err
}
return db.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
}
func checkPortWhitelistPlan(db *gorm.DB, plan portWhitelistPlan) error {
current, err := loadPortWhitelistSetting(db)
if err != nil {
return err
}
if !reflect.DeepEqual(current, plan.current) {
return fmt.Errorf("firewall port whitelist changed while the task was running; refresh and retry")
}
return nil
}
func (s *FirewallSettingService) executePortWhitelist(ctx context.Context, plan portWhitelistPlan, apply func() error, report whitelistReporter) error {
if err := checkPortWhitelistPlan(global.DB.WithContext(ctx), plan); err != nil {
return err
}
if err := apply(); err != nil {
return err
}
if err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := checkPortWhitelistPlan(tx, plan); err != nil {
return err
}
return writePortWhitelistSetting(tx, plan.desired)
}); err != nil {
return err
}
report("FirewallWhitelistSaved", "", nil)
return nil
}
func checkPortWhitelist(
ctx context.Context, service *FirewallService, provider filter.Provider,
rules []preparedFirewallRuleCreate, report whitelistReporter,
) error {
stored, err := service.rules.List(ctx)
if err != nil {
return err
}
var existing []filter.FirewallRule
for _, record := range stored {
if rules, err := record.RulesForProvider(provider); err == nil {
existing = append(existing, rules...)
}
}
snapshots := make(map[string]filter.Snapshot)
var failures []error
for _, prepared := range rules {
rule := prepared.request.Rule
check := func() error {
if err := ctx.Err(); err != nil {
return err
}
scope := rule.Scope.Key()
snapshot, found := snapshots[scope]
if !found {
var err error
snapshot, err = prepared.runtime.ObserveMutation(ctx, rule.Scope)
if err != nil {
return err
}
snapshots[scope] = snapshot
}
for _, candidate := range existing {
if err := whitelistRuleConflict(rule, candidate); err != nil {
return err
}
}
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported {
continue
}
if err := whitelistRuleConflict(rule, observed.Rule); err != nil {
return err
}
}
return nil
}
if err := check(); err != nil {
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
label := whitelistPortLabel(port)
report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
}
}
return errors.Join(failures...)
}
func whitelistRuleConflict(requested, existing filter.FirewallRule) error {
if filter.OppositeActions(requested.Action, existing.Action) && filter.RulesOverlap(requested, existing) {
return fmt.Errorf("%w: %s %s/%s [%s]", filter.ErrRuleConflict,
existing.Action, existing.DestinationPort, existing.Protocol, existing.SourceAddress)
}
err := filter.CheckRuleCollision(requested, existing)
if errors.Is(err, filter.ErrRuleOperation) {
return nil
}
return err
}
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
type state struct {
ready bool
err error
}
states := make(map[string]state)
return func(port dto.FirewallSystemPort) (bool, error) {
if providerErr != nil {
return false, providerErr
}
key := "service"
if isDirectFirewallProvider(provider) {
key = port.Family
}
if cached, ok := states[key]; ok {
return cached.ready, cached.err
}
var result state
if isDirectFirewallProvider(provider) {
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), port.Family)
result = state{ready: initialized && bound, err: err}
} else {
client, err := firewallService.baseClient()
result.err = err
if err == nil {
result.ready, result.err = client.Status()
}
}
states[key] = result
return result.ready, result.err
}
}
func whitelistPortLabel(port dto.FirewallSystemPort) string {
return fmt.Sprintf("%s %s/%s [%s]", port.Family, port.Port, port.Protocol, port.SourceAddress)
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
@@ -834,21 +212,8 @@ func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firew
return firewall.ValidatePortWhitelist(entries)
}
func initializeRequestedWhitelistRule(rule firewall.PortWhitelist) (firewall.PortWhitelist, error) {
rule.Type = strings.ToLower(strings.TrimSpace(rule.Type))
rules, err := InitializeFirewallWhitelistPorts([]firewall.PortWhitelist{rule})
if err != nil {
return rule, err
}
return rules[0], nil
}
func (service *FirewallService) syncSystemAccessPortTransition(ctx context.Context, serviceType string, ports []string) error {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
settings := &FirewallSettingService{}
plan, err := settings.preparePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
for i := range entries {
if entries[i].Type == serviceType {
if len(ports) == 0 {
@@ -859,17 +224,6 @@ func (service *FirewallService) syncSystemAccessPortTransition(ctx context.Conte
}
return entries, nil
})
if err != nil {
return err
}
if reflect.DeepEqual(plan.current, plan.desired) {
return nil
}
return settings.applyPortWhitelist(ctx, plan, service, false, func(status, label string, err error) {
if err != nil && global.LOG != nil {
global.LOG.Errorf("sync service firewall rule %s: %v", label, err)
}
})
}
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {

View File

@@ -37,10 +37,9 @@ type firewallDatabaseSyncAdapter interface {
type firewallSyncRule struct {
dto.FirewallRuleSyncItem
desired filter.DesiredRule
observed *filter.ObservedRule
done bool
whitelistCleanup bool
desired filter.DesiredRule
observed *filter.ObservedRule
done bool
}
func firewallSyncSubsystem(value string) string {
@@ -83,26 +82,16 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
return nil, nil, nil, err
}
model.SortFirewallRules(stored, request.TargetProvider)
var whitelist []firewall.PortWhitelist
var whitelistDesired []filter.FirewallRule
if isDirectFirewallProvider(request.TargetProvider) {
ports, overridden := ctx.Value(firewallWhitelistOverrideKey{}).([]firewall.PortWhitelist)
if !overridden {
ports, err = loadFirewallPortWhiteList()
if err != nil {
return nil, nil, nil, err
}
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, nil, nil, err
}
custom := customWhitelist(ports)
whitelist = append(custom, required...)
whitelistDesired = whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(custom), firewall.ExpandPortWhitelist(required))
ports, err := loadFirewallPortWhiteList()
if err != nil {
return nil, nil, nil, err
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, nil, nil, err
}
whitelistDesired := whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
rules := make([]*firewallSyncRule, 0, len(stored))
var whitelistCleanup []*firewallSyncRule
preservedMarkers := make(map[string]bool)
compileFailed := false
for _, record := range stored {
@@ -116,15 +105,6 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
preservedMarkers[rule.Marker] = true
}
for _, native := range desired {
if isDirectFirewallProvider(request.TargetProvider) && hasSystemFirewallRuleOwner(record) &&
isWhitelistPortAllowance(native.Rule) && !filter.RuleMatchesPortWhitelist(native.Rule, whitelist) {
stale := native.Rule
whitelistCleanup = append(whitelistCleanup, &firewallSyncRule{whitelistCleanup: true, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: record.UUID, Rule: &stale, Status: firewallsync.StatusRemove, Reason: "port allowance is no longer in the whitelist",
}})
preservedMarkers[native.Marker] = true
continue
}
rule := native.Rule
rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}})
}
@@ -137,9 +117,11 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
rule := prepared.request.Rule
duplicate := false
for _, existing := range rules {
if existing.Rule != nil && whitelistContainsRule([]filter.FirewallRule{*existing.Rule}, rule) {
duplicate = true
break
if existing.Rule != nil {
if same, err := filter.SameRuleContent(*existing.Rule, rule); err == nil && same {
duplicate = true
break
}
}
}
if duplicate {
@@ -181,16 +163,6 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
return nil, nil, nil, err
}
snapshots = append(snapshots, snapshot)
if isDirectFirewallProvider(request.TargetProvider) && scope.Chain == filter.BasicBeforeChain {
for _, observed := range snapshot.Rules {
if observed.ParseStatus == filter.ParseStatusSupported && isWhitelistPortAllowance(observed.Rule) && !filter.RuleMatchesPortWhitelist(observed.Rule, whitelist) {
stale := observed.Rule
whitelistCleanup = append(whitelistCleanup, &firewallSyncRule{whitelistCleanup: true, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &stale, Status: firewallsync.StatusRemove, Reason: "port allowance is no longer in the whitelist",
}})
}
}
}
inventory, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
if err != nil {
return nil, nil, nil, err
@@ -246,24 +218,22 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
}
}
}
return runtime, append(rules, whitelistCleanup...), snapshots, nil
return runtime, rules, snapshots, nil
}
func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) {
if isDirectFirewallProvider(request.TargetProvider) {
if request.SourceProvider != "" || request.ResetSource {
return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule)
}
if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil {
return result, err
}
whitelistErr := s.SyncPortWhitelist(ctx)
if t != nil {
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
}
if whitelistErr != nil {
return result, whitelistErr
}
if request.SourceProvider != "" || request.ResetSource {
return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule)
}
if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil {
return result, err
}
whitelistErr := s.SyncPortWhitelist(ctx)
if t != nil {
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
}
if whitelistErr != nil {
return result, whitelistErr
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
@@ -275,11 +245,7 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
if err != nil {
return result, err
}
for _, rule := range rules {
if rule.whitelistCleanup {
return result, fmt.Errorf("%w: whitelist cleanup is incomplete; check that the firewall family is initialized and active", filter.ErrRuleOperation)
}
}
created, removed, unexecuted := 0, 0, 0
stopped := make(map[string]error)
failedRemovals := make(map[string]error)
@@ -703,9 +669,24 @@ func runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID s
}
}
func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule {
rules := make([]filter.FirewallRule, 0, len(ports)+len(required))
for _, port := range required {
rule := systemPortRule(provider, port)
if isDirectFirewallProvider(provider) {
rule.Scope.Chain = filter.BasicBeforeChain
}
rules = append(rules, rule)
}
for _, port := range ports {
rules = append(rules, systemPortRule(provider, port))
}
return rules
}
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
filterruntime.InvalidateInventory()
defer filterruntime.InvalidateInventory()
ports, err := loadFirewallPortWhiteList()
@@ -720,96 +701,107 @@ func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
if err != nil {
return err
}
ready := (&FirewallSettingService{}).portWhitelistReadiness(provider, nil, s)
report := func(status, label string, err error) {
if global.LOG == nil {
return
}
if err != nil {
global.LOG.Warnf("synchronize firewall whitelist %s: %v", label, err)
} else if status == "FirewallWhitelistDeferred" {
global.LOG.Debugf("defer firewall whitelist %s: firewall is inactive or uninitialized", label)
}
}
rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
prepared, prepareErr := s.prepareWhitelistRules(ctx, provider, rules, ready, report)
if prepareErr != nil {
return prepareErr
}
if err := syncWhitelistRules(ctx, s, nil, prepared, report); err != nil {
return err
}
return s.reconcilePortWhitelist(ctx, provider, ready, report)
}
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 {
activeFamilies := make(map[filter.Family]bool)
if isDirectFirewallProvider(provider) {
for _, rule := range rules {
family := rule.Scope.Family
if _, checked := activeFamilies[family]; checked {
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)
}
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(family))
if err != nil {
return err
}
activeFamilies[family] = initialized && bound
}
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)
}
}
} else if len(rules) > 0 {
client, err := s.baseClient()
if err != nil {
return err
}
return errors.Join(syncErrors...)
}
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
active, err := client.Status()
if err != nil || !active {
return err
}
}
for _, key := range sortedSystemPortKeys(previousSet) {
if _, exists := currentSet[key]; exists {
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
prepared := make([]preparedFirewallRuleCreate, 0, len(rules))
var failures []error
for _, rule := range rules {
if err := ctx.Err(); err != nil {
return err
}
if isDirectFirewallProvider(provider) && !activeFamilies[rule.Scope.Family] {
continue
}
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
item, err := s.prepareCreate(ctx, provider, dto.FirewallRuleCreateItem{
Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: systemPortSourceID(port),
})
if err != nil {
failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err))
continue
}
prepared = append(prepared, item)
}
return nil
if len(failures) > 0 {
return errors.Join(failures...)
}
for _, item := range prepared {
if err := ctx.Err(); err != nil {
return err
}
if err := s.addWhitelistRule(ctx, item); err != nil {
rule := item.request.Rule
failures = append(failures, fmt.Errorf("add whitelist rule %s %s/%s [%s]: %w", rule.Scope.Family, rule.DestinationPort, rule.Protocol, rule.SourceAddress, err))
}
}
return errors.Join(failures...)
}
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
filterruntime.InvalidateInventory()
defer filterruntime.InvalidateInventory()
return newFirewallService().
SyncSystemPorts(context.Background(), firewall.ExpandPortWhitelist(previous), firewall.ExpandPortWhitelist(current))
func (s *FirewallService) addWhitelistRule(ctx context.Context, prepared preparedFirewallRuleCreate) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
rule, runtime := prepared.request.Rule, prepared.runtime
snapshot, err := runtime.ObserveMutation(ctx, rule.Scope)
if err != nil {
return err
}
for _, observed := range snapshot.Rules {
if observed.ParseStatus == filter.ParseStatusSupported {
if same, err := filter.SameRuleContent(rule, observed.Rule); err == nil && same {
return nil
}
}
}
if err := filter.CheckObservedRuleCollisions(snapshot, rule, nil); err != nil {
return err
}
if rule.Scope.Chain == filter.BasicBeforeChain {
rule.UUID = uuid.NewString()
return runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &rule}})
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
for _, record := range stored {
compiled, err := s.compileStoredFirewallRules(ctx, record, rule.Scope.Provider)
if err != nil {
continue
}
for _, candidate := range compiled {
if err := filter.CheckRuleCollision(rule, candidate.Rule); errors.Is(err, filter.ErrRuleOperation) {
return runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &candidate.Rule}})
} else if err != nil {
return err
}
}
}
return s.applyCreateRules(ctx, runtime, snapshot, stored, []preparedFirewallRuleCreate{prepared})[0]
}
type forwardingRuleSyncCandidate struct {

View File

@@ -228,9 +228,9 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err
}
if req.Key == "Port" {
if err := newFirewallService().syncSystemAccessPortTransition(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if err := updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
return fmt.Errorf("synchronize SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
}
return err
}

View File

@@ -32,7 +32,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err
}
go func() {
_ = OperateFirewallPort(nil, addPorts)
_ = ensureFirewallPorts(addPorts)
}()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
FirewallWhitelistTask: "Update firewall port whitelist"
FirewallWhitelistSaved: "Whitelist configuration saved"
FirewallWhitelistDeferred: "{{ .name }}: configuration saved; pending firewall activation"
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
FirewallWhitelistTask: "Actualizar la lista de puertos permitidos del cortafuegos"
FirewallWhitelistSaved: "Configuración de la lista de permitidos guardada"
FirewallWhitelistDeferred: "{{ .name }}: configuración guardada; pendiente de activar el cortafuegos"
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistTask: "به‌روزرسانی فهرست پورت‌های مجاز فایروال"
FirewallWhitelistSaved: "پیکربندی فهرست مجاز ذخیره شد"
FirewallWhitelistDeferred: "{{ .name }}: پیکربندی ذخیره شد؛ در انتظار فعال‌سازی فایروال"
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエン
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistTask: "ファイアウォールのポート許可リストを更新"
FirewallWhitelistSaved: "許可リストの設定を保存しました"
FirewallWhitelistDeferred: "{{ .name }}:設定を保存しました。ファイアウォールの有効化後に適用します"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistTask: "방화벽 포트 허용 목록 업데이트"
FirewallWhitelistSaved: "허용 목록 설정이 저장되었습니다"
FirewallWhitelistDeferred: "{{ .name }}: 설정이 저장되었으며 방화벽 활성화 후 적용됩니다"
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"

View File

@@ -708,8 +708,5 @@ FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແ
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistTask: "ອັບເດດລາຍຊື່ພອດທີ່ອະນຸຍາດຂອງໄຟວໍ"
FirewallWhitelistSaved: "ບັນທຶກການຕັ້ງຄ່າລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ"
FirewallWhitelistDeferred: "{{ .name }}: ບັນທຶກການຕັ້ງຄ່າແລ້ວ; ລໍຖ້າເປີດໃຊ້ໄຟວໍ"
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
FirewallWhitelistTask: "Kemas kini senarai port dibenarkan tembok api"
FirewallWhitelistSaved: "Konfigurasi senarai dibenarkan telah disimpan"
FirewallWhitelistDeferred: "{{ .name }}: konfigurasi disimpan; menunggu pengaktifan tembok api"
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as p
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
FirewallWhitelistTask: "Atualizar a lista de portas permitidas do firewall"
FirewallWhitelistSaved: "Configuração da lista de permissões salva"
FirewallWhitelistDeferred: "{{ .name }}: configuração salva; aguardando a ativação do firewall"
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Проверить бэкенд межсете
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistTask: "Обновить список разрешённых портов межсетевого экрана"
FirewallWhitelistSaved: "Настройки списка разрешённых портов сохранены"
FirewallWhitelistDeferred: "{{ .name }}: настройки сохранены; ожидается включение межсетевого экрана"
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkeleri
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
FirewallWhitelistTask: "Güvenlik duvarı izin verilen port listesini güncelle"
FirewallWhitelistSaved: "İzin verilenler listesi yapılandırması kaydedildi"
FirewallWhitelistDeferred: "{{ .name }}: yapılandırma kaydedildi; güvenlik duvarının etkinleştirilmesi bekleniyor"
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistTask: "更新防火牆連接埠白名單"
FirewallWhitelistSaved: "白名單設定已儲存"
FirewallWhitelistDeferred: "{{ .name }}:設定已儲存,待防火牆啟用後套用"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"

View File

@@ -717,8 +717,5 @@ FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistTask: "更新防火墙端口白名单"
FirewallWhitelistSaved: "白名单配置已保存"
FirewallWhitelistDeferred: "{{ .name }}:配置已保存,待防火墙启用后下发"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"

View File

@@ -69,13 +69,6 @@ var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{
if err != nil {
return fmt.Errorf("migrate firewall port whitelist: %w", err)
}
rules, err = service.InitializeFirewallWhitelistPorts(rules)
if err != nil {
return fmt.Errorf("initialize firewall whitelist ports: %w", err)
}
if _, err := firewall.RequiredPortWhitelist(rules); err != nil {
return err
}
value, err := json.Marshal(rules)
if err != nil {
return err
@@ -143,7 +136,7 @@ func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
normalized, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{rule})
normalized, err := service.InitializeFirewallWhitelistPorts([]firewall.PortWhitelist{rule})
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
@@ -188,7 +181,7 @@ func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error
rules[index].Sources = append(rules[index].Sources, "::/0")
}
}
return firewall.ValidatePortWhitelist(rules)
return service.InitializeFirewallWhitelistPorts(rules)
}
func parseLegacyPortWhitelist(value string) ([]legacyPortWhitelist, error) {

View File

@@ -917,7 +917,7 @@ func parseRule(scope filter.Scope, raw string, position int) filter.ObservedRule
}
return filter.ObservedRule{
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw,
Protected: isProtectedRule(scope, normalized, comment),
Protected: filter.IsBuiltinProtectedRule(normalized),
}
}
@@ -928,29 +928,6 @@ func isDefaultRejectWith(family filter.Family, value string) bool {
return value == "icmp-port-unreachable"
}
func isProtectedRule(scope filter.Scope, rule filter.FirewallRule, comment string) bool {
if scope.Chain == native.BasicBeforeChain || scope.Chain == native.BasicAfterChain {
return true
}
if rule.Action == filter.ActionAccept && rule.Interface == "lo" {
return true
}
if rule.Action == filter.ActionAccept {
states := make(map[string]struct{}, len(rule.ConnectionStates))
for _, state := range rule.ConnectionStates {
states[state] = struct{}{}
}
if _, established := states["established"]; established {
return true
}
}
if scope.Chain == native.BasicAfterChain && rule.Action == filter.ActionDrop &&
rule.SourceAddress == "" && rule.DestinationAddress == "" && rule.SourcePort == "" && rule.DestinationPort == "" {
return true
}
return strings.Contains(strings.ToLower(comment), "whitelist")
}
func takeValue(args []string, index *int, target *string) bool {
if *index+1 >= len(args) {
return false

View File

@@ -427,7 +427,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return filter.ObservedRule{
Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque},
Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw,
Protected: scope.Chain != filter.IptablesInputChain,
Protected: scope.Chain == filter.BasicAfterChain,
}
}
tokens, err := shellwords.Parse(raw)
@@ -544,7 +544,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
if err != nil {
return opaque()
}
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: scope.Chain != filter.IptablesInputChain}
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: filter.IsBuiltinProtectedRule(normalized)}
}
func parseProtocol(value string) string {

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"strconv"
"strings"
)
@@ -82,6 +83,28 @@ func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
return true
}
func IsBuiltinProtectedRule(rule FirewallRule) bool {
if rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderNftables {
return false
}
rule, err := NormalizeRule(rule)
if err != nil || rule.SourceAddress != "" || rule.DestinationAddress != "" || rule.SourcePort != "" || rule.DestinationPort != "" {
return false
}
switch rule.Scope.Chain {
case BasicBeforeChain:
if rule.Action != ActionAccept || rule.Protocol != "all" {
return false
}
return rule.Interface == "lo" && len(rule.ConnectionStates) == 0 ||
rule.Interface == "" && slices.Equal(rule.ConnectionStates, []string{"established", "related"})
case BasicAfterChain:
return rule.Action == ActionDrop && (rule.Protocol == "tcp" || rule.Protocol == "udp") &&
rule.Interface == "" && len(rule.ConnectionStates) == 0
}
return false
}
func GuardMutation(target ObservedRule) error {
if target.Protected {
return ErrProtectedRule

View File

@@ -132,9 +132,29 @@ func (m *Manager) initPreRules() error {
return err
}
rules := firewall.ExpandPortWhitelist(ports)
commands := requiredPortCommands(rules)
var commands [][]string
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if err != nil {
return err
}
candidates := [][]string{
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
}
for _, rule := range rules {
if rule.Family == string(family) {
candidates = append(candidates, requiredPortCommand(tableFamily, rule))
}
}
for _, command := range candidates {
expression := strings.Join(command[5:], " ")
if !containsRequiredPortRule(output, expression) {
commands = append(commands, command)
output += "\n" + expression
}
}
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
@@ -144,47 +164,6 @@ func (m *Manager) initPreRules() error {
return runBatch(commands...)
}
func (m *Manager) SyncRequiredPorts() error {
ports, err := m.loadRequiredPorts()
if err != nil {
return err
}
ports, err = firewall.NormalizeRequiredPorts(ports)
if err != nil {
return err
}
rules := firewall.ExpandPortWhitelist(ports)
var commands [][]string
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
output, exists, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return err
}
if !exists {
continue
}
for _, rule := range rules {
if rule.Family != string(family) {
continue
}
command := requiredPortCommand(tableFamily, rule)
expression := strings.Join(command[5:], " ")
if !containsRequiredPortRule(output, expression) {
commands = append(commands, command)
output += "\n" + expression
}
}
}
if err := runBatch(commands...); err != nil {
return err
}
return PersistRuleset(context.Background())
}
func containsRequiredPortRule(output, expression string) bool {
canonical := func(line string) string {
line, _, _ = strings.Cut(line, " comment ")
@@ -192,6 +171,7 @@ func containsRequiredPortRule(output, expression string) bool {
for _, protocol := range []string{"tcp", "udp"} {
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
}
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
fields := strings.Fields(line)
for index, field := range fields {
if prefix, err := netip.ParsePrefix(field); err == nil {
@@ -213,24 +193,6 @@ func containsRequiredPortRule(output, expression string) bool {
return false
}
func requiredPortCommands(rules []firewall.SystemPort) [][]string {
commands := make([][]string, 0, len(rules)+6)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicBeforeChain},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
)
for _, rule := range rules {
if rule.Family == string(family) {
commands = append(commands, requiredPortCommand(tableFamily, rule))
}
}
}
return commands
}
func (m *Manager) updateSetting(key, value string) error {
if m != nil && m.UpdateSetting != nil {
return m.UpdateSetting(key, value)

View File

@@ -1,7 +1,6 @@
package firewall
import (
"encoding/json"
"fmt"
"net/netip"
"sort"
@@ -19,14 +18,6 @@ const (
PortWhitelistTypeSSH = "ssh"
)
func ParsePortWhitelist(value string) ([]PortWhitelist, error) {
var rules []PortWhitelist
if err := json.Unmarshal([]byte(value), &rules); err != nil {
return nil, err
}
return ValidatePortWhitelist(rules)
}
func ValidatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
if rules == nil {
return nil, fmt.Errorf("firewall port whitelist must be an array")
@@ -46,13 +37,11 @@ func ValidatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
if rule.Type != PortWhitelistTypePanel && rule.Type != PortWhitelistTypeSSH {
return nil, fmt.Errorf("invalid firewall port whitelist type: %s", rule.Type)
}
if rule.Port != "" {
port, err := parseWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
rule.Port = strconv.Itoa(port)
port, err := parseWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
rule.Port = strconv.Itoa(port)
} else {
var err error
rule.Port, err = normalizeWhitelistPort(rule.Port)
@@ -297,26 +286,6 @@ func SortedSystemPortKeys(ports map[string]SystemPort) []string {
return keys
}
func ContainsPort(ports []PortWhitelist, target PortWhitelist) bool {
for _, port := range ports {
familyMatches := port.Family == "" || target.Family == "" || port.Family == target.Family
if familyMatches && port.Port == target.Port && port.Protocol == target.Protocol {
return true
}
}
return false
}
func ExcludePorts(ports, excluded []PortWhitelist) []PortWhitelist {
result := make([]PortWhitelist, 0, len(ports))
for _, port := range ports {
if !ContainsPort(excluded, port) {
result = append(result, port)
}
}
return result
}
func NormalizeRequiredPorts(ports []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(ports))
for _, port := range ports {

View File

@@ -19106,7 +19106,7 @@ const docTemplate = `{
},
"/hosts/firewall/rules/delete": {
"post": {
"description": "Returns a taskID immediately; per-rule deletion results and failures are written to the task log.",
"description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.",
"consumes": [
"application/json"
],
@@ -19143,7 +19143,7 @@ const docTemplate = `{
"Timestamp": []
}
],
"summary": "Queue managed firewall rule deletion",
"summary": "Queue firewall rule deletion",
"tags": [
"Firewall"
],
@@ -19419,7 +19419,7 @@ const docTemplate = `{
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19433,10 +19433,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19467,7 +19464,7 @@ const docTemplate = `{
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19481,10 +19478,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19515,7 +19509,7 @@ const docTemplate = `{
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19529,10 +19523,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -36413,17 +36404,36 @@ const docTemplate = `{
"type": "object"
},
"dto.FirewallRuleDelete": {
"description": "Provide managed rule UUIDs, before-chain rule targets, or both.",
"properties": {
"beforeRules": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleDeleteTarget"
},
"type": "array"
},
"uuids": {
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleDeleteTarget": {
"properties": {
"instanceKey": {
"maxLength": 128,
"type": "string"
},
"scope": {
"$ref": "#/definitions/filter.Scope"
}
},
"required": [
"uuids"
"scope",
"instanceKey"
],
"type": "object"
},

View File

@@ -19102,7 +19102,7 @@
},
"/hosts/firewall/rules/delete": {
"post": {
"description": "Returns a taskID immediately; per-rule deletion results and failures are written to the task log.",
"description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.",
"consumes": [
"application/json"
],
@@ -19139,7 +19139,7 @@
"Timestamp": []
}
],
"summary": "Queue managed firewall rule deletion",
"summary": "Queue firewall rule deletion",
"tags": [
"Firewall"
],
@@ -19415,7 +19415,7 @@
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19429,10 +19429,7 @@
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19463,7 +19460,7 @@
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19477,10 +19474,7 @@
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19511,7 +19505,7 @@
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19525,10 +19519,7 @@
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -36409,17 +36400,36 @@
"type": "object"
},
"dto.FirewallRuleDelete": {
"description": "Provide managed rule UUIDs, before-chain rule targets, or both.",
"properties": {
"beforeRules": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleDeleteTarget"
},
"type": "array"
},
"uuids": {
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleDeleteTarget": {
"properties": {
"instanceKey": {
"maxLength": 128,
"type": "string"
},
"scope": {
"$ref": "#/definitions/filter.Scope"
}
},
"required": [
"uuids"
"scope",
"instanceKey"
],
"type": "object"
},

View File

@@ -333,6 +333,7 @@ export namespace Firewall {
export interface DeleteRequest {
uuids: string[];
beforeRules?: { scope: Scope; instanceKey: string }[];
}
export interface DeleteResponse {

View File

@@ -4,13 +4,13 @@ import { Firewall } from '@/api/interface/firewall';
import { TimeoutEnum } from '@/enums/http-enum';
export const createFirewallPortWhitelist = (rule: Firewall.PortWhitelist) =>
http.post<Firewall.FilterChainOperationResult>('/hosts/firewall/settings/whitelist', { rule });
http.post('/hosts/firewall/settings/whitelist', { rule });
export const updateFirewallPortWhitelist = (request: Firewall.PortWhitelistUpdate) =>
http.post<Firewall.FilterChainOperationResult>('/hosts/firewall/settings/whitelist/update', request);
http.post('/hosts/firewall/settings/whitelist/update', request);
export const deleteFirewallPortWhitelist = (rule: Firewall.PortWhitelist) =>
http.post<Firewall.FilterChainOperationResult>('/hosts/firewall/settings/whitelist/delete', { rule });
http.post('/hosts/firewall/settings/whitelist/delete', { rule });
export const loadFireBaseInfo = (tab: string) =>
http.post<Firewall.FirewallBase>('/hosts/firewall/base', { name: tab }, TimeoutEnum.T_40S);

View File

@@ -4102,7 +4102,7 @@ const message = {
external: 'External rule',
externalHelper: 'Not managed by 1Panel.',
protected: 'System-protected',
protectedHelper: 'A critical system rule that cannot be edited or deleted.',
protectedHelper: 'Built-in rules and rules that exactly match the whitelist cannot be edited or deleted.',
stateShort: {
managed: 'Panel-created',
adopted: 'Externally managed',
@@ -4300,6 +4300,10 @@ const message = {
unbindHelper:
'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?',
portWhiteList: 'Port allowlist',
whitelistConfigHelper:
'The allowlist only saves configuration and protects matching allow rules. Missing rules are added on start, restart, initialization, or synchronization. Editing or deleting allowlist entries does not remove existing rules.',
whitelistDeleteConfirm:
'Only this whitelist entry will be deleted. Existing firewall rules will remain. Continue?',
portWhiteListHelper:
'Enter a single port (e.g. 80) or a port range (e.g. 8000-8100). Port numbers must be between 1 and 65535.',
chain: 'Chain',
@@ -4312,10 +4316,8 @@ const message = {
allPorts: 'All Ports',
allProtocolHelper: 'All protocols and ports',
sourceAddressPlaceholder: 'e.g. 172.16.10.11, 172.16.0.0/24, 2001:db8::1, or 2001:db8::/64',
systemAccessChangeConfirm:
'This operation on the {0} allowlist rules may prevent remote access through these services. Continue?',
whitelistServicePortsHelper:
'Set one port allowed by the firewall. Manage the listening port in the service settings.',
'Enter a single port (1-65535). This does not change the service listening port. Changing the port in the service settings also updates this whitelist entry.',
whitelistSourcesHelper:
'Enter IP addresses or CIDR ranges, separated by commas or new lines. Leave blank to allow all IPv4 and IPv6 sources.',
destinationPortPlaceholder: 'e.g. 80, 80,443, or 8080-8089',

View File

@@ -4149,7 +4149,7 @@ const message = {
'Regla existente del sistema que aún no gestiona 1Panel. Se puede adoptar cuando sea necesario.',
protected: 'Protegida por el sistema',
protectedHelper:
'Regla crítica del sistema que no se puede editar ni eliminar para evitar interrumpir el acceso.',
'Las reglas integradas y las que coinciden exactamente con la lista blanca no se pueden editar ni eliminar.',
stateShort: {
managed: 'Panel',
adopted: 'Gestionada',
@@ -4353,6 +4353,10 @@ const message = {
unbindHelper:
'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?',
portWhiteList: 'Lista blanca de puertos',
whitelistConfigHelper:
'La lista de permitidos solo guarda la configuración y protege las reglas de acceso coincidentes. Las reglas que falten se añaden al iniciar, reiniciar, inicializar o sincronizar. Editar o eliminar entradas no elimina las reglas existentes.',
whitelistDeleteConfirm:
'Solo se eliminará esta entrada de la lista blanca. Se conservarán las reglas existentes del firewall. ¿Continuar?',
portWhiteListHelper:
'Introduzca un puerto (p. ej., 80) o un rango (p. ej., 8000-8100). Los puertos deben estar entre 1 y 65535.',
chain: 'Cadena',
@@ -4365,10 +4369,8 @@ const message = {
allPorts: 'Todos los Puertos',
allProtocolHelper: 'Todos los protocolos y puertos',
sourceAddressPlaceholder: 'p. ej. 172.16.10.11, 172.16.0.0/24, 2001:db8::1 o 2001:db8::/64',
systemAccessChangeConfirm:
'Esta operación en las reglas de la lista de permitidos de {0} puede impedir el acceso remoto mediante estos servicios. ¿Continuar?',
whitelistServicePortsHelper:
'Configure un único puerto permitido por el cortafuegos. El puerto de escucha se gestiona en la configuración del servicio.',
'Introduzca un único puerto (1-65535). Esto no cambia el puerto de escucha del servicio. Al cambiar el puerto en los ajustes del servicio, también se actualiza esta entrada de la lista blanca.',
whitelistSourcesHelper:
'Introduzca direcciones IP o rangos CIDR separados por comas o saltos de línea. Déjelo en blanco para permitir todos los orígenes IPv4 e IPv6.',
destinationPortPlaceholder: 'p. ej. 80, 80,443 o 8080-8089',

View File

@@ -4067,7 +4067,7 @@ const message = {
externalHelper:
'قانونی موجود در سیستم که هنوز توسط 1Panel مدیریت نمی‌شود و در صورت نیاز می‌توان آن را تحت مدیریت قرار داد.',
protected: 'محافظت‌شده توسط سیستم',
protectedHelper: 'قانون حیاتی سیستم که برای جلوگیری از اختلال در دسترسی قابل ویرایش یا حذف نیست.',
protectedHelper: 'قوانین داخلی و قوانینی که دقیقاً با فهرست سفید مطابقت دارند قابل ویرایش یا حذف نیستند.',
stateShort: { managed: 'پنل', adopted: 'مدیریت', external: 'خارجی', protected: 'محافظت', drifted: 'ناهمگام' },
ruleTargetRequired: 'حداقل یک نشانی IP یا درگاه وارد کنید',
resolution_adopt: 'واگذاری مدیریت',
@@ -4257,6 +4257,10 @@ const message = {
unbindHelper:
'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بی‌اثر می‌شوند. با احتیاط ادامه دهید. تأیید می‌کنید؟',
portWhiteList: 'لیست سفید پورت',
whitelistConfigHelper:
'فهرست مجاز فقط تنظیمات را ذخیره می‌کند و از قوانین مجاز منطبق محافظت می‌کند. قوانین مفقود هنگام شروع، راه‌اندازی مجدد، مقداردهی اولیه یا همگام‌سازی اضافه می‌شوند. ویرایش یا حذف موارد فهرست مجاز، قوانین موجود را حذف نمی‌کند.',
whitelistDeleteConfirm:
'فقط این ورودی فهرست سفید حذف می‌شود. قوانین موجود فایروال باقی می‌مانند. ادامه می‌دهید؟',
portWhiteListHelper:
'یک پورت (مانند 80) یا بازهٔ پورت (مانند 8000-8100) وارد کنید. شمارهٔ پورت باید بین 1 و 65535 باشد.',
chain: 'زنجیره',
@@ -4269,10 +4273,8 @@ const message = {
allPorts: 'همه پورت‌ها',
allProtocolHelper: 'همه پروتکل‌ها و پورت‌ها',
sourceAddressPlaceholder: 'مثلاً: 172.16.10.11، 172.16.0.0/24، 2001:db8::1 یا 2001:db8::/64',
systemAccessChangeConfirm:
'این عملیات روی قوانین فهرست مجاز {0} ممکن است دسترسی راه دور از طریق این سرویس‌ها را قطع کند. ادامه می‌دهید؟',
whitelistServicePortsHelper:
'یک درگاه مجاز برای دیواره آتش تنظیم کنید. درگاه شنود را در تنظیمات سرویس تغییر دهید.',
'یک پورت (1-65535) وارد کنید. این کار پورت شنود سرویس را تغییر نمی‌دهد. تغییر پورت در تنظیمات سرویس، این ورودی فهرست سفید را نیز به‌روزرسانی می‌کند.',
whitelistSourcesHelper:
'آدرس‌های IP یا محدوده‌های CIDR را با ویرگول یا خط جدید جدا کنید. برای مجاز کردن همه مبدأهای IPv4 و IPv6، این قسمت را خالی بگذارید.',
destinationPortPlaceholder: 'مثلاً: 80، 80,443 یا 8080-8089',

View File

@@ -4092,7 +4092,7 @@ const message = {
externalHelper:
'システムに存在しますがまだ 1Panel の管理対象ではありません必要に応じて管理対象にできます',
protected: 'システム保護',
protectedHelper: 'アクセスへの影響を防ぐため編集や削除できない重要なシステムルールです',
protectedHelper: '組み込みルールとホワイトリストに完全一致するルールは編集削除できません',
stateShort: { managed: 'パネル', adopted: '管理済', external: '外部', protected: '保護', drifted: '差異' },
ruleTargetRequired: 'IP アドレスまたはポートを1つ以上入力してください',
resolution_adopt: '管理対象にする',
@@ -4283,6 +4283,10 @@ const message = {
unbindHelper:
'アンバインド - アンバインドすると追加されたすべてのファイアウォールルールが無効になります注意して操作してください確認しますか',
portWhiteList: 'ポートホワイトリスト',
whitelistConfigHelper:
'許可リストは設定の保存と一致する許可ルールの保護のみを行います不足するルールは起動再起動初期化同期時に追加されます許可リストを編集削除しても既存のルールは削除されません',
whitelistDeleteConfirm:
'このホワイトリスト項目のみを削除します既存のファイアウォールルールは保持されます続行しますか',
portWhiteListHelper:
'単一ポート80またはポート範囲8000-8100を入力してくださいポート番号は 165535 です',
chain: 'チェーン',
@@ -4295,10 +4299,8 @@ const message = {
allPorts: 'すべてのポート',
allProtocolHelper: 'すべてのプロトコルとポート',
sourceAddressPlaceholder: ': 172.16.10.11172.16.0.0/242001:db8::12001:db8::/64',
systemAccessChangeConfirm:
'この操作を {0} の許可リストルールに適用するとこれらのサービス経由のリモートアクセスができなくなる可能性があります続行しますか',
whitelistServicePortsHelper:
'ファイアウォールで許可するポートを1つ設定します待ち受けポートは各サービス設定で変更してください',
'ポートを1つ入力してください1-65535サービスの待受ポートは変更されませんサービス設定でポートを変更するとこのホワイトリスト項目も更新されます',
whitelistSourcesHelper:
'IP アドレスまたは CIDR をカンマか改行で区切って入力してください 空欄の場合すべての IPv4 および IPv6 の接続元を許可します',
destinationPortPlaceholder: ': 8080,4438080-8089',

View File

@@ -4017,7 +4017,7 @@ const message = {
externalHelper:
'시스템에 존재하지만 아직 1Panel에서 관리하지 않는 규칙이며 필요할 때 관리 대상으로 전환할 수 있습니다.',
protected: '시스템 보호',
protectedHelper: '접속 장애를 방지하기 위해 편집하거나 삭제할 수 없는 중요한 시스템 규칙입니다.',
protectedHelper: '기본 제공 규칙과 화이트리스트에 정확히 일치하는 규칙은 편집하거나 삭제할 수 없니다.',
stateShort: { managed: '패널', adopted: '관리', external: '외부', protected: '보호', drifted: '불일치' },
ruleTargetRequired: 'IP 주소 또는 포트를 하나 이상 입력하세요',
resolution_adopt: '관리 대상으로 전환',
@@ -4204,6 +4204,9 @@ const message = {
unbindHelper:
'바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?',
portWhiteList: '포트 화이트리스트',
whitelistConfigHelper:
'허용 목록은 설정만 저장하고 일치하는 허용 규칙을 보호합니다. 누락된 규칙은 시작, 재시작, 초기화 또는 동기화 시 추가됩니다. 허용 목록을 편집하거나 삭제해도 기존 규칙은 삭제되지 않습니다.',
whitelistDeleteConfirm: '이 화이트리스트 항목만 삭제됩니다. 기존 방화벽 규칙은 유지됩니다. 계속하시겠습니까?',
portWhiteListHelper:
'단일 포트(예: 80) 또는 포트 범위(예: 8000-8100)를 입력하세요. 포트 번호는 1~65535여야 합니다.',
chain: '체인',
@@ -4216,10 +4219,8 @@ const message = {
allPorts: '모든 포트',
allProtocolHelper: '모든 프로토콜 및 포트',
sourceAddressPlaceholder: '예: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 또는 2001:db8::/64',
systemAccessChangeConfirm:
'이 작업을 {0} 허용 목록 규칙에 적용하면 해당 서비스를 통한 원격 접속이 불가능해질 수 있습니다. 계속하시겠습니까?',
whitelistServicePortsHelper:
'방화벽에서 허용할 포트 하나를 설정합니다. 수신 포트는 해당 서비스 설정에서 변경하세요.',
'포트 하나를 입력하세요(1-65535). 서비스의 수신 포트는 변경되지 않습니다. 서비스 설정에서 포트를 변경하면 이 화이트리스트 항목도 업데이트됩니다.',
whitelistSourcesHelper:
'IP 주소 또는 CIDR 대역을 쉼표나 줄바꿈으로 구분하세요. 비워 두면 모든 IPv4 및 IPv6 출발지를 허용합니다.',
destinationPortPlaceholder: '예: 80, 80,443 또는 8080-8089',

View File

@@ -3988,7 +3988,7 @@ const message = {
external: 'ກົດພາຍນອກ',
externalHelper: 'ກົດທີ່ມີຢູ່ໃນລະບົບແຕ່ 1Panel ຍັງບໍ່ໄດ້ຈັດການ ແລະ ສາມາດຮັບເຂົ້າຈັດການໄດ້ເມື່ອຈຳເປັນ.',
protected: 'ລະບົບປົກປ້ອງ',
protectedHelper: 'ກົດສຳຄັນຂອງລະບົບທີ່ບໍ່ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້ ເພື່ອຫຼີກລ້ຽງການຂັດຂວາງການເຂົ້າເຖິງ.',
protectedHelper: 'ກົດທີ່ມີໃນລະບົບ ແລະ ກົດທີ່ກົງກັບບັນຊີຂາວທຸກປະການ ບໍ່ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້.',
stateShort: { managed: 'ແຜງ', adopted: 'ຈັດການ', external: 'ພາຍນອກ', protected: 'ປົກປ້ອງ', drifted: 'ບໍ່ກົງ' },
ruleTargetRequired: 'ກະລຸນາປ້ອນ IP ຫຼື ພອດຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
resolution_adopt: 'ຮັບເຂົ້າຈັດການ',
@@ -4169,6 +4169,9 @@ const message = {
unbindHelper:
'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?',
portWhiteList: 'ພອດທີ່ອະນຸຍາດ',
whitelistConfigHelper:
'ລາຍການອະນຸຍາດບັນທຶກສະເພາະການຕັ້ງຄ່າ ແລະ ປົກປ້ອງກົດອະນຸຍາດທີ່ກົງກັນ. ກົດທີ່ຂາດຈະຖືກເພີ່ມເມື່ອເລີ່ມ, ເລີ່ມໃໝ່, ຕັ້ງຄ່າເບື້ອງຕົ້ນ ຫຼື ຊິງຂໍ້ມູນ. ການແກ້ໄຂ ຫຼື ລຶບລາຍການອະນຸຍາດຈະບໍ່ລຶບກົດທີ່ມີຢູ່.',
whitelistDeleteConfirm: 'ຈະລຶບສະເພາະລາຍການບັນຊີຂາວນີ້. ກົດໄຟວໍທີ່ມີຢູ່ຈະຖືກເກັບໄວ້. ສືບຕໍ່ບໍ?',
portWhiteListHelper: 'ປ້ອນພອດດຽວ (ເຊັ່ນ 80) ຫຼື ຊ່ວງພອດ (ເຊັ່ນ 8000-8100). ໝາຍເລກພອດຕ້ອງຢູ່ລະຫວ່າງ 1 ຫາ 65535.',
chain: 'Chain',
sourceIP: 'IP ຕົ້ນທາງ',
@@ -4180,9 +4183,8 @@ const message = {
allPorts: 'ທຸກໆພອດ',
allProtocolHelper: 'ໂປຣໂຕຄໍ ແລະ ພອດທັງໝົດ',
sourceAddressPlaceholder: 'ຕົວຢ່າງ: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ຫຼື 2001:db8::/64',
systemAccessChangeConfirm:
'ການດຳເນີນການນີ້ກັບກົດລາຍຊື່ອະນຸຍາດ {0} ອາດເຮັດໃຫ້ບໍ່ສາມາດເຂົ້າເຖິງທາງໄກຜ່າບໍລິການເຫຼົ່ານີ້. ສືບຕໍ່ບໍ?',
whitelistServicePortsHelper: 'ຕັ້ງຄ່າໜຶ່ງພອດທີ່ໄຟວໍອະນຸຍາດ. ປ່ຽນພອດຮັບຟັງໃນການຕັ້ງຄ່າບໍລິການ.',
whitelistServicePortsHelper:
'ປ້ອນພອດດຽວ (1-65535). ການຕັ້ງຄ່ານີ້ຈະບໍ່ປ່ຽນພອດຮັບຟັງຂອງບໍລິການ. ເມື່ອປ່ຽນພອດໃນການຕັ້ງຄ່າບໍລິການ, ລາຍການບັນຊີຂາວນີ້ຈະຖືກອັບເດດດ້ວຍ.',
whitelistSourcesHelper:
'ປ້ອນທີ່ຢູ່ IP ຫຼື CIDR ໂດຍແຍກດ້ວຍຈຸດຈ້ຳ ຫຼື ຂຶ້ນແຖວໃໝ່. ຖ້າປະໄວ້ຫວ່າງ ຈະອະນຸຍາດທຸກແຫຼ່ງທີ່ມາ IPv4 ແລະ IPv6.',
destinationPortPlaceholder: 'ຕົວຢ່າງ: 80, 80,443 ຫຼື 8080-8089',

View File

@@ -4168,7 +4168,7 @@ const message = {
'Peraturan sistem sedia ada yang belum diurus oleh 1Panel. Ia boleh diambil alih apabila diperlukan.',
protected: 'Dilindungi sistem',
protectedHelper:
'Peraturan sistem kritikal yang tidak boleh diedit atau dipadam untuk mengelakkan gangguan akses.',
'Peraturan terbina dalam dan peraturan yang sepadan tepat dengan senarai putih tidak boleh diedit atau dipadam.',
stateShort: {
managed: 'Panel',
adopted: 'Terurus',
@@ -4367,6 +4367,10 @@ const message = {
unbindHelper:
'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?',
portWhiteList: 'Senarai putih port',
whitelistConfigHelper:
'Senarai dibenarkan hanya menyimpan konfigurasi dan melindungi peraturan kebenaran yang sepadan. Peraturan yang tiada ditambah semasa mula, mula semula, pemulaan atau penyegerakan. Mengedit atau memadam entri tidak membuang peraturan sedia ada.',
whitelistDeleteConfirm:
'Hanya entri senarai putih ini akan dipadam. Peraturan tembok api sedia ada akan dikekalkan. Teruskan?',
portWhiteListHelper:
'Masukkan satu port (cth. 80) atau julat port (cth. 8000-8100). Nombor port mestilah antara 1 hingga 65535.',
chain: 'Rantai',
@@ -4379,10 +4383,8 @@ const message = {
allPorts: 'Semua Port',
allProtocolHelper: 'Semua protokol dan port',
sourceAddressPlaceholder: 'contoh: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 atau 2001:db8::/64',
systemAccessChangeConfirm:
'Operasi ini pada peraturan senarai dibenarkan {0} mungkin menghalang akses jauh melalui perkhidmatan tersebut. Teruskan?',
whitelistServicePortsHelper:
'Tetapkan satu port yang dibenarkan oleh tembok api. Urus port pendengar dalam tetapan perkhidmatan.',
'Masukkan satu port (1-65535). Ini tidak mengubah port pendengaran perkhidmatan. Menukar port dalam tetapan perkhidmatan turut mengemas kini entri senarai putih ini.',
whitelistSourcesHelper:
'Masukkan alamat IP atau julat CIDR, dipisahkan dengan koma atau baris baharu. Biarkan kosong untuk membenarkan semua sumber IPv4 dan IPv6.',
destinationPortPlaceholder: 'contoh: 80, 80,443 atau 8080-8089',

View File

@@ -4187,7 +4187,7 @@ const message = {
'Regra existente do sistema que ainda não é gerenciada pelo 1Panel. Pode ser adotada quando necessário.',
protected: 'Protegida pelo sistema',
protectedHelper:
'Regra crítica do sistema que não pode ser editada ou excluída para evitar interrupções de acesso.',
'As regras integradas e as que correspondem exatamente à lista de permissões não podem ser editadas nem excluídas.',
stateShort: {
managed: 'Painel',
adopted: 'Gerenciada',
@@ -4389,6 +4389,10 @@ const message = {
unbindHelper:
'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?',
portWhiteList: 'Lista branca de portas',
whitelistConfigHelper:
'A lista de permissões apenas salva a configuração e protege as regras de permissão correspondentes. As regras ausentes são adicionadas ao iniciar, reiniciar, inicializar ou sincronizar. Editar ou excluir entradas não remove as regras existentes.',
whitelistDeleteConfirm:
'Apenas esta entrada da lista de permissões será excluída. As regras existentes do firewall serão mantidas. Continuar?',
portWhiteListHelper:
'Insira uma porta (ex.: 80) ou um intervalo (ex.: 8000-8100). Os números das portas devem estar entre 1 e 65535.',
chain: 'Cadeia',
@@ -4401,10 +4405,8 @@ const message = {
allPorts: 'Todas as Portas',
allProtocolHelper: 'Todos os protocolos e portas',
sourceAddressPlaceholder: 'por exemplo: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ou 2001:db8::/64',
systemAccessChangeConfirm:
'Esta operação nas regras da lista de permissões de {0} pode impedir o acesso remoto por esses serviços. Continuar?',
whitelistServicePortsHelper:
'Defina uma única porta permitida pelo firewall. Gerencie a porta de escuta nas configurações do serviço.',
'Informe uma única porta (1-65535). Isso não altera a porta de escuta do serviço. Alterar a porta nas configurações do serviço também atualiza esta entrada da lista de permissões.',
whitelistSourcesHelper:
'Insira endereços IP ou intervalos CIDR separados por vírgulas ou quebras de linha. Deixe em branco para permitir todas as origens IPv4 e IPv6.',
destinationPortPlaceholder: 'por exemplo: 80, 80,443 ou 8080-8089',

View File

@@ -4154,7 +4154,7 @@ const message = {
'Существующее системное правило, которое пока не управляется 1Panel. При необходимости его можно принять под управление.',
protected: 'Защищено системой',
protectedHelper:
'Критически важное системное правило, которое нельзя изменить или удалить во избежание потери доступа.',
'Встроенные правила и правила, точно совпадающие с белым списком, нельзя изменять или удалять.',
stateShort: {
managed: 'Панель',
adopted: 'Принято',
@@ -4355,6 +4355,10 @@ const message = {
unbindHelper:
'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?',
portWhiteList: 'Белый список портов',
whitelistConfigHelper:
'Список разрешений только сохраняет настройки и защищает совпадающие разрешающие правила. Недостающие правила добавляются при запуске, перезапуске, инициализации или синхронизации. Изменение или удаление записей списка не удаляет существующие правила.',
whitelistDeleteConfirm:
'Будет удалена только эта запись белого списка. Существующие правила брандмауэра сохранятся. Продолжить?',
portWhiteListHelper:
'Введите один порт (например, 80) или диапазон портов (например, 8000-8100). Номера портов должны быть от 1 до 65535.',
chain: 'Цепочка',
@@ -4367,10 +4371,8 @@ const message = {
allPorts: 'Все Порта',
allProtocolHelper: 'Все протоколы и порты',
sourceAddressPlaceholder: 'например: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 или 2001:db8::/64',
systemAccessChangeConfirm:
'Эта операция с правилами белого списка {0} может привести к потере удалённого доступа через эти службы. Продолжить?',
whitelistServicePortsHelper:
'Укажите один порт, разрешённый брандмауэром. Порт прослушивания меняется в настройках службы.',
'Введите один порт (1-65535). Порт прослушивания службы не изменится. Изменение порта в настройках службы также обновит эту запись белого списка.',
whitelistSourcesHelper:
'Введите IP-адреса или подсети CIDR через запятую или с новой строки. Оставьте поле пустым, чтобы разрешить все источники IPv4 и IPv6.',
destinationPortPlaceholder: 'например: 80, 80,443 или 8080-8089',

View File

@@ -4172,8 +4172,7 @@ const message = {
externalHelper:
'Sistemde bulunan ancak henüz 1Panel tarafından yönetilmeyen bir kuraldır. Gerektiğinde yönetime alınabilir.',
protected: 'Sistem korumalı',
protectedHelper:
'Erişimin kesilmesini önlemek için düzenlenemeyen veya silinemeyen kritik bir sistem kuralıdır.',
protectedHelper: 'Yerleşik kurallar ve beyaz listeyle tam olarak eşleşen kurallar düzenlenemez veya silinemez.',
stateShort: {
managed: 'Panel',
adopted: 'Yönetilen',
@@ -4371,6 +4370,10 @@ const message = {
unbindHelper:
'Bağlantıyı Kaldır - Bağlantı kaldırıldığında, eklenen tüm güvenlik duvarı kuralları geçersiz olacaktır. Dikkatli ilerleyin. Onaylıyor musunuz?',
portWhiteList: 'Port beyaz listesi',
whitelistConfigHelper:
'İzin listesi yalnızca yapılandırmayı kaydeder ve eşleşen izin kurallarını korur. Eksik kurallar başlatma, yeniden başlatma, ilk yapılandırma veya eşitleme sırasında eklenir. İzin listesi girdilerini düzenlemek veya silmek mevcut kuralları kaldırmaz.',
whitelistDeleteConfirm:
'Yalnızca bu beyaz liste kaydı silinecek. Mevcut güvenlik duvarı kuralları korunacak. Devam edilsin mi?',
portWhiteListHelper:
'Tek bir port (ör. 80) veya port aralığı (ör. 8000-8100) girin. Port numaraları 1 ile 65535 arasında olmalıdır.',
chain: 'Zincir',
@@ -4383,10 +4386,8 @@ const message = {
allPorts: 'Tüm Portlar',
allProtocolHelper: 'Tüm protokoller ve portlar',
sourceAddressPlaceholder: 'örn. 172.16.10.11, 172.16.0.0/24, 2001:db8::1 veya 2001:db8::/64',
systemAccessChangeConfirm:
'Bu işlemi {0} izin listesi kurallarına uygulamak, bu hizmetler üzerinden uzaktan erişimi engelleyebilir. Devam edilsin mi?',
whitelistServicePortsHelper:
'Güvenlik duvarının izin verdiği tek bir port ayarlayın. Dinleme portunu hizmet ayarlarından yönetin.',
'Tek bir port girin (1-65535). Bu işlem hizmetin dinleme portunu değiştirmez. Hizmet ayarlarında port değiştirildiğinde bu beyaz liste kaydı da güncellenir.',
whitelistSourcesHelper:
'IP adreslerini veya CIDR aralıklarını virgül ya da yeni satırla ayırın. Tüm IPv4 ve IPv6 kaynaklarına izin vermek için boş bırakın.',
destinationPortPlaceholder: 'örn. 80, 80,443 veya 8080-8089',

View File

@@ -3830,7 +3830,7 @@ const message = {
external: '外部規則',
externalHelper: '尚未納入 1Panel 管理。',
protected: '系統保護',
protectedHelper: '系統關鍵規則,不可編輯或刪除。',
protectedHelper: '系統內建或與白名單完全相符的規則,不可編輯或刪除。',
stateShort: {
managed: '面板建立',
adopted: '外部納管',
@@ -4008,6 +4008,9 @@ const message = {
bindHelper: '綁定 僅當狀態為綁定時,防火牆規則才能生效,是否確認?',
unbindHelper: '解除綁定 解除綁定時,已新增的所有防火牆規則將失效,請謹慎操作,是否確認?',
portWhiteList: '埠白名單',
whitelistConfigHelper:
'白名單僅儲存設定並保護相符的放行規則。缺少的規則將於啟動、重新啟動、初始化或同步時補齊;編輯或刪除白名單不會刪除現有規則。',
whitelistDeleteConfirm: '僅刪除此白名單設定,現有防火牆規則會保留。是否繼續?',
portWhiteListHelper: '支援單一連接埠(如 80或連接埠範圍如 8000-8100連接埠取值為 1-65535。',
chain: '鏈',
sourceIP: '來源 IP',
@@ -4019,8 +4022,8 @@ const message = {
allPorts: '所有埠',
allProtocolHelper: '所有協定和連接埠',
sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64',
systemAccessChangeConfirm: '對 {0} 白名單規則執行此操作後,可能無法透過該服務遠端存取。是否繼續?',
whitelistServicePortsHelper: '設定一個防火牆放行連接埠,服務監聽連接埠請在對應服務設定中修改。',
whitelistServicePortsHelper:
'手動填寫一個連接埠1-65535不會修改服務監聽連接埠。在服務設定中修改連接埠時,會同步更新此白名單設定。',
whitelistSourcesHelper: '支援 IP 或 CIDR多個以逗號或換行分隔。留空預設允許所有 IPv4 和 IPv6 來源。',
destinationPortPlaceholder: '例如80、80,443 或 8080-8089',
deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?',

View File

@@ -3876,7 +3876,7 @@ const message = {
external: '外部规则',
externalHelper: '尚未纳入 1Panel 管理。',
protected: '系统保护',
protectedHelper: '系统关键规则,不可编辑或删除。',
protectedHelper: '系统内置或与白名单完全匹配的规则,不可编辑或删除。',
stateShort: {
managed: '面板创建',
adopted: '外部纳管',
@@ -3955,7 +3955,7 @@ const message = {
alreadyExistsInTarget: '防火墙中已存在相同规则,无需重复添加。',
onlyInTarget: '该规则只存在于当前防火墙中。',
stale: '检查后防火墙状态已发生变化,请刷新后重试。',
protectedRule: '这是保证系统正常运行的必要规则,不能修改。',
protectedRule: '该防火墙规则受到保护,不能修改。',
dockerAcceptReadOnly:
'该 ACCEPT 规则为只读、不可同步;同步其他规则时会保留该规则。如需移除,请在主机上手动删除。',
cannotReconcile: '该规则无法自动同步:{0}',
@@ -4056,6 +4056,9 @@ const message = {
bindHelper: '绑定 仅当状态为绑定时,防火墙规则才能生效,是否确认?',
unbindHelper: '解绑 解除绑定时,已添加的所有防火墙规则将失效,请谨慎操作,是否确认?',
portWhiteList: '端口白名单',
whitelistConfigHelper:
'白名单仅保存配置并保护匹配的放行规则。缺失规则将在启动、重启、初始化或同步时补齐;编辑或删除白名单不会删除已有规则。',
whitelistDeleteConfirm: '仅删除这条白名单配置,已有防火墙规则会保留。是否继续?',
portWhiteListHelper: '支持单个端口(如 80或端口范围如 8000-8100端口取值为 1-65535。',
chain: '链',
sourceIP: '源 IP',
@@ -4067,8 +4070,8 @@ const message = {
allPorts: '所有端口',
allProtocolHelper: '所有协议和端口',
sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64',
systemAccessChangeConfirm: '对 {0} 白名单规则执行此操作后,可能无法通过该服务远程访问。是否继续?',
whitelistServicePortsHelper: '设置一个防火墙放行端口,服务监听端口请在对应服务设置中修改。',
whitelistServicePortsHelper:
'手动填写一个端口1-65535不会修改服务监听端口。在服务设置中修改端口时,会同步更新此白名单配置。',
whitelistSourcesHelper: '支持 IP 或 CIDR多个以逗号或换行分隔。留空默认允许所有 IPv4 和 IPv6 来源。',
destinationPortPlaceholder: '例如80、80,443 或 8080-8089',
deleteRuleConfirm: '将删除 {0} 条规则,是否继续?',

View File

@@ -75,7 +75,10 @@
</el-button>
<el-button
v-permission
:disabled="loading || managedTotal === 0"
:disabled="
loading ||
(selects.length > 0 ? !selects.some(isDeletableManagedRule) : managedTotal === 0)
"
@click="exportRulesBySelection"
>
{{ $t('commons.button.export') }}
@@ -187,7 +190,7 @@
row-key="rowKey"
@search="searchPage"
>
<el-table-column type="selection" :selectable="isDeletableManagedRule" width="48" fix />
<el-table-column type="selection" :selectable="isDeletableRule" width="48" fix />
<el-table-column :label="$t('firewall.action')" width="76">
<template #default="{ row }">
<span
@@ -1047,7 +1050,7 @@ const exportRules = async (rows: RuleRow[]) => {
const exportRulesBySelection = async () => {
const selected = selects.value.filter((row) => isDeletableManagedRule(row));
if (selected.length > 0) return exportRules(selected);
if (selects.value.length > 0) return exportRules(selected);
const allManagedRules = toRuleRows(await loadAllInventoryItems()).filter((row) => isDeletableManagedRule(row));
return exportRules(allManagedRules);
};
@@ -1067,13 +1070,13 @@ const usageOwnersSummary = (owners: string[]) => {
};
const deleteRulesConfirmMessage = (selected: RuleRow[]) => {
const count = new Set(selected.map((row) => row.desired?.uuid)).size;
const count = new Set(selected.map((row) => row.desired?.uuid || row.observed?.instanceKey)).size;
const accepted = selected.filter((row) => row.rule.action === 'accept' && Boolean(row.observed));
const risky = accepted.filter((row) => isWildcardDestinationPort(row.rule) || ruleUsageEntries(row).length > 0);
if (selected.length > 1 && risky.length > 0) {
return i18n.global.t('firewall.deleteRiskRulesConfirm', [
count,
new Set(risky.map((row) => row.desired?.uuid)).size,
new Set(risky.map((row) => row.desired?.uuid || row.observed?.instanceKey)).size,
]);
}
if (selected.length === 1 && accepted.length === 1) {
@@ -1108,19 +1111,24 @@ const removeRules = async (selected: RuleRow[]) => {
}
loading.value = true;
const uuids = [...new Set(selected.flatMap((row) => (row.desired?.uuid ? [row.desired.uuid] : [])))];
const beforeRules = selected.filter(isDeletableBeforeRule).map((row) => ({
scope: row.rule.scope,
instanceKey: row.observed.instanceKey,
}));
const count = uuids.length + beforeRules.length;
try {
if (uuids.length === 0) return;
const { taskID, queued, succeeded, failed } = (await deleteFirewallRules({ uuids })).data;
if (count === 0) return;
const { taskID, queued, succeeded, failed } = (await deleteFirewallRules({ uuids, beforeRules })).data;
if (queued && taskID) {
selects.value = [];
openRuleTask(taskID);
return;
}
if (succeeded > 0) {
MsgSuccess(`${i18n.global.t('commons.msg.operationSuccess')} (${succeeded}/${uuids.length})`);
MsgSuccess(`${i18n.global.t('commons.msg.operationSuccess')} (${succeeded}/${count})`);
}
if (failed > 0) {
MsgError(`${i18n.global.t('commons.msg.operationFailed')} (${failed}/${uuids.length})`);
MsgError(`${i18n.global.t('commons.msg.operationFailed')} (${failed}/${count})`);
}
await search();
} finally {
@@ -1128,7 +1136,7 @@ const removeRules = async (selected: RuleRow[]) => {
}
};
const removeSelectedRules = () => removeRules(selects.value.filter((row) => isDeletableManagedRule(row)));
const removeSelectedRules = () => removeRules(selects.value.filter((row) => isDeletableRule(row)));
const resetRules = () => {
withDockerRestart.value = false;
@@ -1242,6 +1250,19 @@ const isDeletableManagedRule = (row: Firewall.InventoryItem) =>
row.state !== 'protected' &&
(row.state !== 'drifted' || isMissingManagedRule(row));
const isDeletableBeforeRule = (
row: Firewall.InventoryItem,
): row is Firewall.InventoryItem & { observed: Firewall.ObservedRule & { instanceKey: string } } =>
(row.rule.scope.provider === 'iptables' || row.rule.scope.provider === 'nftables') &&
row.rule.scope.chain === '1PANEL_BASIC_BEFORE' &&
!row.desired &&
row.state !== 'protected' &&
row.observed?.parseStatus === 'supported' &&
!row.observed.protected &&
Boolean(row.observed.instanceKey);
const isDeletableRule = (row: Firewall.InventoryItem) => isDeletableManagedRule(row) || isDeletableBeforeRule(row);
const displayRulePriority = (row: Firewall.InventoryItem) => {
if (row.rule.scope.provider === 'firewalld') {
if (!supportsFirewalldPriority.value || row.rule.nativeKind !== 'rich_rule') return '-';
@@ -1280,7 +1301,10 @@ const operationButtons = [
label: i18n.global.t('firewall.resolution_adopt'),
permission: true,
nodeAdmin: true,
show: (row: RuleRow) => row.state === 'external' && row.observed?.parseStatus === 'supported',
show: (row: RuleRow) =>
row.state === 'external' &&
row.observed?.parseStatus === 'supported' &&
!isIptablesSystemPresetScope(row.rule.scope),
click: adoptRule,
},
{
@@ -1294,7 +1318,7 @@ const operationButtons = [
label: i18n.global.t('commons.button.delete'),
permission: true,
nodeAdmin: true,
show: (row: RuleRow) => isDeletableManagedRule(row),
show: (row: RuleRow) => isDeletableRule(row),
click: removeRule,
},
];

View File

@@ -105,9 +105,8 @@
:panel-port="settings?.panelPort"
:ssh-port="settings?.sshPort"
:loading="loading"
@created="openWhitelistTask"
@saved="load"
/>
<TaskLog ref="whitelistTaskRef" @close="load" />
</div>
</template>
@@ -117,7 +116,6 @@ import { Firewall } from '@/api/interface/firewall';
import { loadFirewallSettings, operateFire, operateFirewallBackend } from '@/api/modules/firewall';
import FireRouter from '@/views/host/firewall/index.vue';
import WhiteList from '@/views/host/firewall/setting/white-list/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
@@ -135,8 +133,6 @@ const savedBackends = ref<Record<Firewall.BackendSubsystem, string>>({
const pingStatus = ref('Disable');
const oldPingStatus = ref('Disable');
const whiteListRef = ref<InstanceType<typeof WhiteList>>();
const whitelistTaskRef = ref<InstanceType<typeof TaskLog>>();
const openWhitelistTask = (taskID: string) => whitelistTaskRef.value?.openWithTaskID(taskID, true);
const openWhitelist = () => {
whiteListRef.value?.acceptParams();
load();

View File

@@ -1,6 +1,7 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('firewall.portWhiteList')" size="large">
<template #content>
<p class="input-help mb-3">{{ $t('firewall.whitelistConfigHelper') }}</p>
<div class="mb-3">
<el-button type="primary" :disabled="disabled" @click="openEditor()">
{{ $t('commons.button.create') }}
@@ -77,7 +78,7 @@
<span class="input-help">{{ $t('firewall.portWhiteListHelper') }}</span>
</el-form-item>
</template>
<el-form-item v-else-if="editingRule" :label="$t('commons.table.port')" prop="port">
<el-form-item v-else :label="$t('commons.table.port')" prop="port">
<el-input v-model.trim="form.port" :placeholder="form.type === 'ssh' ? '2222' : '18443'" clearable />
<span class="input-help">{{ $t('firewall.whitelistServicePortsHelper') }}</span>
</el-form-item>
@@ -108,7 +109,7 @@ import {
updateFirewallPortWhitelist,
} from '@/api/modules/firewall';
import i18n from '@/lang';
import { MsgError } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import {
formatHostAddressList,
isValidIPOrCIDR,
@@ -124,7 +125,7 @@ const props = defineProps<{
sshPort?: string;
loading: boolean;
}>();
const emit = defineEmits<{ (e: 'created', taskID: string): void }>();
const emit = defineEmits<{ (e: 'saved'): void }>();
const drawerVisible = ref(false);
const dialogVisible = ref(false);
const saving = ref(false);
@@ -207,7 +208,7 @@ const saveRule = async () => {
rule = normalizeWhiteListRule({
...form.value,
type: form.value.type === 'custom' ? undefined : form.value.type,
port: form.value.type === 'custom' || editingRule.value ? form.value.port : undefined,
port: form.value.port,
sources,
});
} catch {
@@ -220,56 +221,33 @@ const saveRule = async () => {
return;
}
const oldRule = editingRule.value;
await submit(
() => (oldRule ? updateFirewallPortWhitelist({ oldRule, rule }) : createFirewallPortWhitelist(rule)),
oldRule ? { operation: 'edit', rule: oldRule } : undefined,
);
await submit(() => (oldRule ? updateFirewallPortWhitelist({ oldRule, rule }) : createFirewallPortWhitelist(rule)));
};
const removeRule = async (rule: WhiteListRule) => {
await submit(() => deleteFirewallPortWhitelist(rule), { operation: 'delete', rule });
if (disabled.value) return;
const confirmed = await ElMessageBox.confirm(
i18n.global.t('firewall.whitelistDeleteConfirm'),
i18n.global.t('commons.button.delete'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
},
)
.then(() => true)
.catch(() => false);
if (confirmed) await submit(() => deleteFirewallPortWhitelist(rule));
};
const submit = async (
request: () => ReturnType<typeof createFirewallPortWhitelist>,
change?: { operation: 'edit' | 'delete'; rule: WhiteListRule },
) => {
const submit = async (request: () => ReturnType<typeof createFirewallPortWhitelist>) => {
if (disabled.value) return;
saving.value = true;
try {
if (change && (change.operation === 'delete' || change.rule.type)) {
const messages: string[] = [];
if (change.operation !== 'delete' || !change.rule.type) {
messages.push(
change.operation === 'delete'
? i18n.global.t('commons.msg.delete')
: i18n.global.t('firewall.editRuleConfirm'),
);
}
if (change.rule.type) {
const service = `${serviceLabel(change.rule.type)} (${change.rule.port || servicePortLabel(change.rule.type)} / ${(change.rule.protocol || 'tcp').toUpperCase()})`;
messages.push(i18n.global.t('firewall.systemAccessChangeConfirm', [service]));
}
const confirmed = await ElMessageBox.confirm(
messages.join('\n'),
i18n.global.t(`commons.button.${change.operation}`),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
},
)
.then(() => true)
.catch(() => false);
if (!confirmed) return;
}
const { data: result } = await request();
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
await request();
dialogVisible.value = false;
emit('created', result.taskID);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('saved');
} catch {
} finally {
saving.value = false;

View File

@@ -12,13 +12,13 @@ export const normalizeWhiteListRule = (rule: WhiteListRule): WhiteListRule => {
if (!['tcp', 'udp'].includes(protocol)) throw new Error('invalid whitelist rule');
if (rule.type) {
if (!['panel', 'ssh'].includes(rule.type)) throw new Error('invalid whitelist rule');
if (rule.port !== undefined && !/^\d+$/.test(rule.port.trim())) {
if (!/^\d+$/.test(rule.port?.trim() || '')) {
throw new Error('invalid service port');
}
return {
type: rule.type,
protocol,
port: rule.port === undefined ? undefined : normalizePortRange(rule.port),
port: normalizePortRange(rule.port || ''),
sources,
};
}