fix(firewall): improve whitelist management and rule lifecycle (#13826)

This commit is contained in:
ssongliu
2026-09-15 23:55:01 +08:00
committed by GitHub
parent 9a5bd9bcba
commit fe742b9f41
45 changed files with 2111 additions and 1229 deletions

View File

@@ -492,21 +492,67 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Queue firewall port whitelist update // @Summary Create firewall port whitelist rules
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log. // @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post]
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Update firewall port whitelist rules
// @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Accept json // @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request" // @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post] // @Router /hosts/firewall/settings/whitelist/update [post]
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"} // @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) { func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistUpdate var request dto.FirewallPortWhitelistUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
result, err := firewallSettingService.QueuePortWhitelist(request.Value) result, err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Delete firewall port whitelist rules
// @Description Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/delete [post]
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return

View File

@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
) )
@@ -61,15 +62,24 @@ type FirewallBackendGroup struct {
} }
type FirewallSettings struct { type FirewallSettings struct {
System FirewallBackendGroup `json:"system"` System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"` Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"` Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"` PingStatus string `json:"pingStatus"`
PortWhitelist string `json:"portWhiteList"` PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
}
type FirewallPortWhitelistCreate struct {
Rule filter.PortWhitelist `json:"rule" validate:"required"`
} }
type FirewallPortWhitelistUpdate struct { type FirewallPortWhitelistUpdate struct {
Value string `json:"value" validate:"required"` OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistDelete struct {
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
} }
type FirewallBackendOperation struct { type FirewallBackendOperation struct {
@@ -93,11 +103,7 @@ type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
} }
type FirewallSystemPort struct { type FirewallSystemPort = firewall.SystemPort
Family string
Port string
Protocol string
}
type FirewallRuleInventoryResponse struct { type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"` IPv4Range filter.PositionRange `json:"ipv4Range"`

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -19,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
@@ -64,6 +64,7 @@ var (
) )
type IFirewallService interface { type IFirewallService interface {
SyncPortWhitelist(context.Context) error
UpdatePanelPort(context.Context, uint, uint) error UpdatePanelPort(context.Context, uint, uint) error
LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error)
QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error)
@@ -98,9 +99,7 @@ func newFirewallService() *FirewallService {
cleanupBackend: cleanupSystemBackend, cleanupBackend: cleanupSystemBackend,
cleanupInactiveBackend: cleanupInactiveSystemBackend, cleanupInactiveBackend: cleanupInactiveSystemBackend,
resetBackend: resetServiceFirewallBackend, resetBackend: resetServiceFirewallBackend,
dockerActive: func() (bool, error) { dockerActive: firewallDockerActive,
return controller.CheckActive("docker")
},
restoreForwarding: func(ctx context.Context) error { restoreForwarding: func(ctx context.Context) error {
return newForwardingService().Restore(ctx) return newForwardingService().Restore(ctx)
}, },
@@ -109,6 +108,13 @@ func newFirewallService() *FirewallService {
} }
} }
func firewallDockerActive() (bool, error) {
if !cmd.Which("docker") {
return false, nil
}
return controller.CheckActive("docker")
}
func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) {
status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"} status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"}
status.LifecycleTaskID = currentFirewallLifecycleTaskID() status.LifecycleTaskID = currentFirewallLifecycleTaskID()
@@ -279,17 +285,13 @@ func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycl
return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action) return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action)
} }
operationErr := operator.Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, func(lifecycle.Client) error { operationErr := operator.Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, func(lifecycle.Client) error {
rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error { runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error {
return s.restoreStoredFirewallRules(ctx, provider, t) return s.restoreStoredFirewallRules(ctx, provider, t)
}) })
whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
ports, err := loadFirewallPortWhiteList() return s.SyncPortWhitelist(ctx)
if err != nil {
return err
}
return s.SyncSystemPorts(ctx, nil, systemPorts(ports))
}) })
return errors.Join(rulesErr, whitelistErr) return nil
}) })
var recoveryErr *lifecycle.CompletedOperationError var recoveryErr *lifecycle.CompletedOperationError
if operationErr != nil && !errors.As(operationErr, &recoveryErr) { if operationErr != nil && !errors.As(operationErr, &recoveryErr) {
@@ -308,7 +310,7 @@ func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycl
if provider == filter.ProviderFirewalld { if provider == filter.ProviderFirewalld {
active := s.dockerActive active := s.dockerActive
if active == nil { if active == nil {
active = func() (bool, error) { return controller.CheckActive("docker") } active = firewallDockerActive
} }
running, err := active() running, err := active()
if err != nil || !running { if err != nil || !running {
@@ -345,7 +347,7 @@ func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation
return err return err
} }
operation := lifecycle.Operation(request.Operation) operation := lifecycle.Operation(request.Operation)
operationErr := lifecycle.NewOperator(firewallLifecycleClient{client}).Operate(operation, request.WithDockerRestart, s.addPortsBeforeStart) operationErr := lifecycle.NewOperator(firewallLifecycleClient{client}).Operate(operation, request.WithDockerRestart, s.restoreFirewallAfterStart)
restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld && restoreFirewalld := client.Name() == lifecycle.ProviderFirewalld &&
(operation == lifecycle.OperationStart || operation == lifecycle.OperationRestart) (operation == lifecycle.OperationStart || operation == lifecycle.OperationRestart)
if operation != lifecycle.OperationStart && operation != lifecycle.OperationRestart { if operation != lifecycle.OperationStart && operation != lifecycle.OperationRestart {
@@ -372,6 +374,19 @@ func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation
return nil return nil
} }
func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error {
if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 {
return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port)
}
if LoadPanelPort() != strconv.Itoa(int(oldPort)) {
return fmt.Errorf("panel port changed before firewall update")
}
if oldPort == port {
return nil
}
return s.syncSystemAccessPortTransition(ctx, firewall.PortWhitelistTypePanel, []string{strconv.Itoa(int(port))})
}
func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error { func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, operation lifecycle.Operation) error {
restoreForwarding := s.restoreForwarding restoreForwarding := s.restoreForwarding
if restoreForwarding == nil { if restoreForwarding == nil {
@@ -383,7 +398,7 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context,
} }
dockerActive := s.dockerActive dockerActive := s.dockerActive
if dockerActive == nil { if dockerActive == nil {
dockerActive = func() (bool, error) { return controller.CheckActive("docker") } dockerActive = firewallDockerActive
} }
active, err := dockerActive() active, err := dockerActive()
@@ -408,10 +423,9 @@ func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) e
return nil return nil
} }
ctx := context.Background() ctx := context.Background()
if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil); err != nil { rulesErr := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil)
return err whitelistErr := s.SyncPortWhitelist(ctx)
} return errors.Join(rulesErr, whitelistErr)
return s.syncConfiguredFirewallPorts(ctx)
} }
func (s *FirewallService) QueueFilterChainInitialization( func (s *FirewallService) QueueFilterChainInitialization(
@@ -439,11 +453,14 @@ func (s *FirewallService) QueueFilterChainInitialization(
t.Logf("backend=%s", provider) t.Logf("backend=%s", provider)
return s.operateFilterChainBase(provider, request) return s.operateFilterChainBase(provider, request)
}, nil) }, nil)
taskItem.AddSubTask(i18n.GetWithName("FirewallRestoreRulesStep", provider), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error {
return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), nil) rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", provider), func() error {
}, nil) return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), t)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func(t *task.Task) error { })
return s.syncConfiguredFirewallPorts(t.TaskCtx) whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
return s.SyncPortWhitelist(t.TaskCtx)
})
return errors.Join(rulesErr, whitelistErr)
}, nil) }, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil { if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall initialization task: %w", err) return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall initialization task: %w", err)
@@ -531,7 +548,7 @@ func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleRes
if provider == filter.ProviderFirewalld && request.WithDockerRestart { if provider == filter.ProviderFirewalld && request.WithDockerRestart {
dockerActive := s.dockerActive dockerActive := s.dockerActive
if dockerActive == nil { if dockerActive == nil {
dockerActive = func() (bool, error) { return controller.CheckActive("docker") } dockerActive = firewallDockerActive
} }
active, err := dockerActive() active, err := dockerActive()
if err != nil { if err != nil {
@@ -1725,9 +1742,13 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string, allow
if err != nil { if err != nil {
return err return err
} }
if ports, ok := ctx.Value(panelPortWhitelistKey{}).([]firewall.PortWhitelist); ok { if ctx.Value(firewallWhitelistOverrideKey{}) != nil {
ports, err := firewallWhitelistForProtection(ctx)
if err != nil {
return err
}
for _, desired := range desiredRules { for _, desired := range desiredRules {
if panelRuleStillRequired(desired.Rule, ports) { if filter.RuleMatchesPortWhitelist(desired.Rule, ports) {
return filter.ErrProtectedRule return filter.ErrProtectedRule
} }
} }
@@ -2299,7 +2320,7 @@ func (s *FirewallService) ensureSystemPortLocked(ctx context.Context, port dto.F
} }
source := dto.FirewallRuleCreateItem{ source := dto.FirewallRuleCreateItem{
Rule: systemPortRule(provider, port), SourceKind: constant.FirewallRuleSourceSecurity, Rule: systemPortRule(provider, port), SourceKind: constant.FirewallRuleSourceSecurity,
SourceID: constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port), SourceID: systemPortSourceID(port),
} }
prepared, err := s.prepareCreate(ctx, provider, source) prepared, err := s.prepareCreate(ctx, provider, source)
if err != nil { if err != nil {
@@ -2379,7 +2400,7 @@ func (s *FirewallService) deleteSystemPort(ctx context.Context, port dto.Firewal
} }
for _, rule := range stored { for _, rule := range stored {
if err := s.deleteProtectedSystemPortRule(ctx, rule.UUID); err != nil { if err := s.deleteProtectedSystemPortRule(ctx, rule.UUID); err != nil {
if errors.Is(err, filter.ErrProtectedRule) && ctx.Value(panelPortWhitelistKey{}) != nil { if errors.Is(err, filter.ErrProtectedRule) && ctx.Value(firewallWhitelistOverrideKey{}) != nil {
continue continue
} }
return err return err
@@ -2396,7 +2417,7 @@ func (s *FirewallService) deleteProtectedSystemPortRule(ctx context.Context, rul
func (s *FirewallService) systemPortRecords(ctx context.Context, port dto.FirewallSystemPort) ([]model.FirewallRule, error) { func (s *FirewallService) systemPortRecords(ctx context.Context, port dto.FirewallSystemPort) ([]model.FirewallRule, error) {
records := make([]model.FirewallRule, 0) records := make([]model.FirewallRule, 0)
sourceIDs := []string{constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port)} sourceIDs := []string{systemPortSourceID(port)}
if port.Family == constant.FirewallFamilyIPv4 { if port.Family == constant.FirewallFamilyIPv4 {
sourceIDs = append(sourceIDs, constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port)) sourceIDs = append(sourceIDs, constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port))
} }
@@ -2420,11 +2441,11 @@ func (s *FirewallService) systemPortRecords(ctx context.Context, port dto.Firewa
} }
func isProtectedSystemFirewallRule(rule model.FirewallRule) bool { func isProtectedSystemFirewallRule(rule model.FirewallRule) bool {
ownerPrefix := model.FirewallRuleOwner( acceptedPrefix := model.FirewallRuleOwner(
constant.FirewallRuleSourceSecurity, constant.FirewallRuleSourceSecurity,
constant.FirewallSystemAcceptedPortSourcePrefix, constant.FirewallSystemAcceptedPortSourcePrefix,
) )
return strings.HasPrefix(rule.Owner, ownerPrefix) return strings.HasPrefix(rule.Owner, acceptedPrefix)
} }
func systemPortRule(provider filter.Provider, port dto.FirewallSystemPort) filter.FirewallRule { func systemPortRule(provider filter.Provider, port dto.FirewallSystemPort) filter.FirewallRule {
@@ -2451,6 +2472,10 @@ func systemPortKey(port dto.FirewallSystemPort) string {
return firewall.SystemPortKey(firewall.SystemPort(port)) return firewall.SystemPortKey(firewall.SystemPort(port))
} }
func systemPortSourceID(port dto.FirewallSystemPort) string {
return constant.FirewallSystemAcceptedPortSourcePrefix + systemPortKey(port)
}
func legacySystemPortKey(port dto.FirewallSystemPort) string { func legacySystemPortKey(port dto.FirewallSystemPort) string {
return firewall.LegacySystemPortKey(firewall.SystemPort(port)) return firewall.LegacySystemPortKey(firewall.SystemPort(port))
} }
@@ -2663,7 +2688,7 @@ func (s *FirewallService) compileRestorableFirewallRules(
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
requiredPorts := systemPorts(required) requiredPorts := firewall.ExpandPortWhitelist(required)
for _, desired := range compiled { for _, desired := range compiled {
covered := false covered := false
for _, port := range requiredPorts { for _, port := range requiredPorts {
@@ -2722,16 +2747,29 @@ func (s *FirewallService) desiredFirewallRulesByScope(
} }
func firewallRuleSnapshotPolicy(ctx context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) { func firewallRuleSnapshotPolicy(ctx context.Context, snapshot filter.Snapshot) (filter.Snapshot, error) {
if ports, ok := ctx.Value(panelPortWhitelistKey{}).([]firewall.PortWhitelist); ok { ports, err := firewallWhitelistForProtection(ctx)
return filter.ProtectSnapshot(snapshot, ports)
}
ports, err := loadFirewallPortWhiteList()
if err != nil { if err != nil {
return filter.Snapshot{}, err return filter.Snapshot{}, err
} }
return filter.ProtectSnapshot(snapshot, ports) 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) { func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
return selectedRuleProvider() return selectedRuleProvider()
} }
@@ -2788,7 +2826,7 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
previous := make([]firewall.PortWhitelist, 0, len(oldPorts)) previous := make([]firewall.PortWhitelist, 0, len(oldPorts))
for _, port := range oldPorts { for _, port := range oldPorts {
item := firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"} item := firewall.PortWhitelist{Port: strconv.Itoa(port), Protocol: "tcp"}
if !containsFirewallPort(current, item) { if !firewall.ContainsPort(current, item) {
previous = append(previous, item) previous = append(previous, item)
} }
} }
@@ -2801,15 +2839,11 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
if err != nil { if err != nil {
return err return err
} }
added = excludeFirewallPorts(added, required) added = firewall.ExcludePorts(added, required)
} }
return syncManagedAcceptedPorts(previous, added) return syncManagedAcceptedPorts(previous, added)
} }
func containsFirewallPort(ports []firewall.PortWhitelist, target firewall.PortWhitelist) bool {
return firewall.ContainsPort(ports, target)
}
func LoadPanelPort() string { func LoadPanelPort() string {
if !global.IsMaster { if !global.IsMaster {
return global.CONF.Base.Port return global.CONF.Base.Port
@@ -2819,7 +2853,7 @@ func LoadPanelPort() string {
return portSetting.Value return portSetting.Value
} }
func loadConfiguredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList) value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
if err != nil { if err != nil {
value = constant.FirewallPortWhiteListValue value = constant.FirewallPortWhiteListValue
@@ -2830,77 +2864,17 @@ func loadConfiguredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
return firewall.ParsePortWhitelist(value) return firewall.ParsePortWhitelist(value)
} }
func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return nil, err
}
required, err := LoadRequiredFirewallPortWhiteList()
if err != nil {
return nil, err
}
return firewall.NormalizePortWhitelist(append(configured, required...)), nil
}
func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) { func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
return loadRequiredFirewallPorts(LoadPanelPort()) ports, err := loadFirewallPortWhiteList()
}
func loadRequiredFirewallPorts(panelPort string) ([]firewall.PortWhitelist, error) {
if panelPort == "" {
return nil, fmt.Errorf("find 1panel service port failed")
}
directives, _, err := parseSSHConfigTree(sshPath)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("load required SSH ports: %w", err)
}
ports := []firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}
for _, port := range loadSSHPortValues(directives) {
ports = append(ports, firewall.PortWhitelist{Port: port, Protocol: "tcp"})
}
return firewall.NormalizeRequiredPorts(ports)
}
func (s *FirewallService) releaseSystemPorts(ctx context.Context, ports []dto.FirewallSystemPort) error {
portSet, err := normalizeSystemPorts(ports)
if err != nil || len(portSet) == 0 {
return err
}
owners := make(map[string]struct{}, len(portSet)*2)
for _, port := range portSet {
owners[model.FirewallRuleOwner(
constant.FirewallRuleSourceSecurity,
constant.FirewallSystemAcceptedPortSourcePrefix+systemPortKey(port),
)] = struct{}{}
if port.Family == constant.FirewallFamilyIPv4 {
owners[model.FirewallRuleOwner(
constant.FirewallRuleSourceSecurity,
constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port),
)] = struct{}{}
}
}
records, err := s.rules.List(ctx)
if err != nil { if err != nil {
return err return nil, err
} }
for _, record := range records { return firewall.RequiredPortWhitelist(ports)
if _, exists := owners[record.Owner]; !exists {
continue
}
if err := s.rules.UpdateWithRevision(ctx, record.UUID, record.Revision, map[string]interface{}{
"owner": constant.FirewallRuleSourceUser,
}); err != nil {
return fmt.Errorf("release accepted firewall port rule %q: %w", record.UUID, err)
}
}
return nil
} }
func newIptablesHelperManager() *iptables_helper.Manager { func newIptablesHelperManager() *iptables_helper.Manager {
return &iptables_helper.Manager{ return &iptables_helper.Manager{
UpdateSetting: settingRepo.Update, UpdateSetting: settingRepo.Update,
PanelPort: LoadPanelPort,
LoadRequiredPorts: LoadRequiredFirewallPortWhiteList, LoadRequiredPorts: LoadRequiredFirewallPortWhiteList,
} }
} }
@@ -2931,7 +2905,7 @@ func supportsManagedFilterChains(provider string) bool {
return provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables return provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables
} }
func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error { func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) error {
ctx := context.Background() ctx := context.Background()
provider := filter.Provider(client.Name()) provider := filter.Provider(client.Name())
var recoveryErrors []error var recoveryErrors []error
@@ -2958,57 +2932,10 @@ func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error {
if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil { if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil {
recordFailure("restore stored firewall rules", err) recordFailure("restore stored firewall rules", err)
} }
if provider == filter.ProviderIptables || provider == filter.ProviderNftables { recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx))
if provider == filter.ProviderIptables {
if err := newIptablesHelperManager().SyncRequiredPorts(true); err != nil {
recordFailure("synchronize required ports", err)
}
} else if err := newNftablesHelperManager().SyncRequiredPorts(); err != nil {
recordFailure("synchronize required ports", err)
}
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
recordFailure("load configured accepted ports", err)
return errors.Join(recoveryErrors...)
}
required, err := LoadRequiredFirewallPortWhiteList()
if err != nil {
recordFailure("load required accepted ports", err)
return errors.Join(recoveryErrors...)
}
recordFailure(
"restore configured accepted ports",
s.SyncSystemPorts(ctx, nil, systemPorts(excludeFirewallPorts(configured, required))),
)
return errors.Join(recoveryErrors...)
}
portWhitelist, err := loadFirewallPortWhiteList()
if err != nil {
recordFailure("load accepted ports", err)
return errors.Join(recoveryErrors...)
}
recordFailure("restore accepted ports", s.SyncSystemPorts(ctx, nil, systemPorts(portWhitelist)))
return errors.Join(recoveryErrors...) return errors.Join(recoveryErrors...)
} }
func systemPorts(ports []firewall.PortWhitelist) []dto.FirewallSystemPort {
result := make([]dto.FirewallSystemPort, 0, len(ports))
for _, port := range ports {
families := []string{port.Family}
if port.Family == "" {
families = []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}
}
for _, family := range families {
result = append(result, dto.FirewallSystemPort{Family: family, Port: port.Port, Protocol: port.Protocol})
}
}
return result
}
func excludeFirewallPorts(ports, excluded []firewall.PortWhitelist) []firewall.PortWhitelist {
return firewall.ExcludePorts(ports, excluded)
}
func AdoptLegacyHostFirewallRuleOwnership(ctx context.Context) error { func AdoptLegacyHostFirewallRuleOwnership(ctx context.Context) error {
return newFirewallService().adoptLegacyHostFirewallRuleOwnership(ctx) return newFirewallService().adoptLegacyHostFirewallRuleOwnership(ctx)
} }

View File

@@ -1,144 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
type panelPortWhitelistKey struct{}
func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error {
if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 {
return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port)
}
if LoadPanelPort() != strconv.Itoa(int(oldPort)) {
return fmt.Errorf("panel port changed before firewall update")
}
if oldPort == port {
return nil
}
client, err := s.baseClient()
if err != nil {
if configuredSystemFirewallBackend() == "" && len(lifecycle.InstalledProviders()) == 0 {
return nil
}
return err
}
provider := client.Name()
active, err := client.Status()
if err != nil {
return err
}
if !supportsManagedFilterChains(provider) && !active {
return nil
}
required, err := loadRequiredFirewallPorts(strconv.Itoa(int(port)))
if err != nil {
return err
}
managedChains := supportsManagedFilterChains(provider)
if managedChains {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
}
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return err
}
protected := firewall.NormalizePortWhitelist(append(configured, required...))
if managedChains {
prepared := append([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}}, required...)
if err := syncPanelRequiredPorts(provider, prepared); err != nil {
return err
}
if err := syncPanelRequiredPorts(provider, required); err != nil {
warnPanelPortCleanupFailure(oldPort, err)
return nil
}
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(port)), Protocol: "tcp"}})
for _, port := range ports {
if err := s.ensureSystemPort(ctx, port); err != nil {
return err
}
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
// cleanupPanelPortLocked removes the old system-owned policy as well as any
// remaining managed runtime rule. The caller must hold firewallRuleMutationMu.
func (s *FirewallService) cleanupPanelPortLocked(ctx context.Context, provider string, oldPort uint, protected []firewall.PortWhitelist) error {
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, protected)
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}})
if provider == constant.FirewallProviderUFW {
for _, port := range ports {
port.Protocol = "all"
ports = append(ports, port)
}
}
// Include family-neutral records left by firewalld or older versions, even
// when the selected backend has since changed.
ports = append(ports, dto.FirewallSystemPort{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"})
var cleanupErrors []error
for _, port := range ports {
if panelPortStillRequired(port, protected) {
continue
}
records, err := s.systemPortRecords(ctx, port)
if err != nil {
cleanupErrors = append(cleanupErrors, err)
continue
}
for _, record := range records {
if err := s.deleteRule(ctx, record.UUID, true); err != nil && !errors.Is(err, filter.ErrProtectedRule) {
cleanupErrors = append(cleanupErrors, err)
}
}
}
return errors.Join(cleanupErrors...)
}
func warnPanelPortCleanupFailure(port uint, err error) {
if err != nil && global.LOG != nil {
global.LOG.Warnf("clean up old panel firewall port %d failed: %v", port, err)
}
}
func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool {
rule := systemPortRule(filter.ProviderUFW, port)
for _, required := range protected {
if port.Family != "" && required.Family != "" && port.Family != required.Family {
continue
}
other := rule
other.Protocol, other.DestinationPort = required.Protocol, required.Port
if filter.RulesOverlap(rule, other) {
return true
}
}
return false
}
func panelRuleStillRequired(rule filter.FirewallRule, protected []firewall.PortWhitelist) bool {
family := string(rule.Scope.Family)
if rule.Scope.Family == filter.FamilyInet {
family = ""
}
return panelPortStillRequired(dto.FirewallSystemPort{
Family: family, Port: rule.DestinationPort, Protocol: rule.Protocol,
}, protected)
}

View File

@@ -5,6 +5,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"reflect"
"slices"
"strings"
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -22,11 +26,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
type IFirewallSettingService interface { type IFirewallSettingService interface {
QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) (dto.FilterChainOperationResponse, error)
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) (dto.FilterChainOperationResponse, error)
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) (dto.FilterChainOperationResponse, error)
Load(context.Context) (dto.FirewallSettings, error) Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error Operate(context.Context, dto.FirewallBackendOperation) error
} }
@@ -50,23 +57,109 @@ func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{} return &FirewallSettingService{}
} }
func (s *FirewallSettingService) QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) { type portWhitelistChange func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)
return s.queuePortWhitelist(value, newFirewallService())
type portWhitelistPlan struct {
current []firewall.PortWhitelist
desired []firewall.PortWhitelist
} }
func (s *FirewallSettingService) queuePortWhitelist(value string, firewallService *FirewallService) (dto.FilterChainOperationResponse, error) { 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(current, request.Rule)
if err != nil {
return nil, err
}
return append(slices.Clone(current), 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(current, request.Rule)
if err != nil {
return nil, err
}
return replacePortWhitelistRule(current, request.OldRule, rule)
})
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) (dto.FilterChainOperationResponse, error) {
if request.Rule == nil {
return dto.FilterChainOperationResponse{}, 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)
})
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil {
return false
}
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
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() firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock() defer firewallWhitelistTaskMu.Unlock()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err 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) taskItem, err := task.NewTask(i18n.GetMsgByKey("FirewallWhitelistTask"), task.TaskUpdate, task.TaskScopeFirewall, "", 0)
if err != nil { if err != nil {
return dto.FilterChainOperationResponse{}, err return dto.FilterChainOperationResponse{}, err
} }
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error { taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
succeeded, failed := 0, 0 succeeded, failed := 0, 0
err := s.applyPortWhitelist(t.TaskCtx, value, firewallService, func(status, label string, err error) { err := s.applyPortWhitelist(t.TaskCtx, plan, firewallService, true, func(status, label string, err error) {
switch status { switch status {
case "applied": case "applied":
succeeded++ succeeded++
@@ -93,63 +186,439 @@ func (s *FirewallSettingService) queuePortWhitelist(value string, firewallServic
type whitelistReporter func(status, label string, err error) type whitelistReporter func(status, label string, err error)
func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, value string, firewallService *FirewallService, report whitelistReporter) 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 {
return syncWhitelistRules(ctx, firewallService, previous, current, 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 {
firewallRuleMutationMu.Lock() firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock() defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
ports, err := firewall.ParsePortWhitelist(value) rule, runtime := prepared.request.Rule, prepared.runtime
snapshot, err := runtime.ObserveMutation(ctx, rule.Scope)
if err != nil { if err != nil {
return err return err
} }
required, err := firewallService.requiredPorts() 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 { if err != nil {
return err return err
} }
removed, err := s.savePortWhitelist(ctx, ports, required, firewallService) 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]
}
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
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 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
}
}
stored, err := s.rules.List(ctx)
if err != nil { if err != nil {
return err return err
} }
for _, record := range stored {
compiled, err := s.compileStoredFirewallRules(ctx, record, provider)
if err != nil {
continue
}
remaining := make([]filter.FirewallRule, 0, len(compiled))
for _, candidate := range compiled {
if !whitelistContainsRule([]filter.FirewallRule{rule}, candidate.Rule) {
remaining = append(remaining, candidate.Rule)
}
}
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 (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) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err
}
var rules []firewall.PortWhitelist
err := json.Unmarshal([]byte(setting.Value), &rules)
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) report("FirewallWhitelistSaved", "", nil)
for _, port := range systemPorts(removed) { return nil
report("FirewallWhitelistReleased", whitelistPortLabel(port), nil)
}
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, required)
provider, providerErr := firewallService.selectedProvider(ctx)
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
return syncPortWhitelist(ctx, ports, required, ready, firewallService.ensureSystemPortLocked, report)
} }
func (s *FirewallSettingService) savePortWhitelist(ctx context.Context, ports, required []firewall.PortWhitelist, firewallService *FirewallService) ([]firewall.PortWhitelist, error) { func checkPortWhitelist(
var removed []firewall.PortWhitelist ctx context.Context, service *FirewallService, provider filter.Provider,
err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { rules []preparedFirewallRuleCreate, report whitelistReporter,
var setting model.Setting ) error {
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error stored, err := service.rules.List(ctx)
if errors.Is(err, gorm.ErrRecordNotFound) { if err != nil {
setting.Value = constant.FirewallPortWhiteListValue return err
} else 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...)
} }
previous, err := firewall.ParsePortWhitelist(setting.Value) }
if err != nil { snapshots := make(map[string]filter.Snapshot)
return err 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
} }
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required) if err := check(); err != nil {
txCtx := context.WithValue(ctx, constant.DB, tx) port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
if err := firewallService.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil { label := whitelistPortLabel(port)
return err report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
} }
value, err := json.Marshal(ports) }
if err != nil { return errors.Join(failures...)
return err }
}
return tx.Where("key = ?", constant.FirewallPortWhiteList). func whitelistRuleConflict(requested, existing filter.FirewallRule) error {
Assign(map[string]interface{}{"value": string(value)}). if filter.OppositeActions(requested.Action, existing.Action) && filter.RulesOverlap(requested, existing) {
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error return fmt.Errorf("%w: %s %s/%s [%s]", filter.ErrRuleConflict,
}) existing.Action, existing.DestinationPort, existing.Protocol, existing.SourceAddress)
return removed, err }
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) { func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
@@ -185,52 +654,104 @@ func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider
} }
} }
func syncPortWhitelist( func whitelistPortLabel(port dto.FirewallSystemPort) string {
ctx context.Context, return fmt.Sprintf("%s %s/%s [%s]", port.Family, port.Port, port.Protocol, port.SourceAddress)
ports, required []firewall.PortWhitelist,
ready func(dto.FirewallSystemPort) (bool, error),
ensure func(context.Context, dto.FirewallSystemPort) error,
report whitelistReporter,
) error {
var failures []error
for _, port := range systemPorts(ports) {
if err := ctx.Err(); err != nil {
return errors.Join(append(failures, err)...)
}
label := whitelistPortLabel(port)
if containsFirewallPort(required, firewall.PortWhitelist{Family: port.Family, Port: port.Port, Protocol: port.Protocol}) {
report("FirewallWhitelistRequired", label, nil)
continue
}
active, err := ready(port)
if err == nil && !active {
report("FirewallWhitelistDeferred", label, nil)
continue
}
if err == nil {
err = ensure(ctx, port)
}
if err != nil {
report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
continue
}
report("applied", label, nil)
}
return errors.Join(failures...)
} }
func whitelistPortLabel(port dto.FirewallSystemPort) string { func loadSSHWhitelistPortFrom(path string) (string, error) {
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol) directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil {
return "", err
}
return loadSSHPortValues(directives)[0], nil
}
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
result := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
result = append(result, entry)
}
}
return result
}
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
var sshPort string
for i := range entries {
entry := &entries[i]
if entry.Type == "" || entry.Port != "" {
continue
}
switch entry.Type {
case firewall.PortWhitelistTypePanel:
entry.Port = LoadPanelPort()
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
}
}
return firewall.ValidatePortWhitelist(entries)
}
func initializeRequestedWhitelistRule(current []firewall.PortWhitelist, rule firewall.PortWhitelist) (firewall.PortWhitelist, error) {
rule.Type = strings.ToLower(strings.TrimSpace(rule.Type))
if rule.Port == "" {
for _, existing := range current {
if rule.Type != "" && existing.Type == rule.Type {
rule.Port = existing.Port
break
}
}
}
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)
for i := range entries {
if entries[i].Type == serviceType {
if len(ports) == 0 {
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
}
entries[i].Port = ports[0]
}
}
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) { func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()} result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
result.PortWhitelist = ports
} else {
result.PortWhitelist = constant.FirewallPortWhiteListValue
}
installed := make(map[string]bool) installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() { for _, name := range lifecycle.InstalledProviders() {
@@ -341,8 +862,9 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
} }
result.Docker.Options = append(result.Docker.Options, option) result.Docker.Options = append(result.Docker.Options, option)
} }
var err error
return result, nil result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
return result, err
} }
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) { func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
@@ -389,10 +911,9 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
} }
if request.Operation == "initialize" { if request.Operation == "initialize" {
service := newFirewallService() service := newFirewallService()
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil); err != nil { rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
return err whitelistErr := service.SyncPortWhitelist(ctx)
} return errors.Join(rulesErr, whitelistErr)
return service.syncConfiguredFirewallPorts(ctx)
} }
return nil return nil
case "forwarding": case "forwarding":

View File

@@ -20,7 +20,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime" filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -137,6 +136,9 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
continue continue
} }
observed := item.Observed observed := item.Observed
if observed.Rule.Scope.Chain == filter.BasicBeforeChain {
continue
}
rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{ rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove, SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove,
ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallsync.ReasonMessage(firewallsync.ReasonManagedOnlyInTarget), ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallsync.ReasonMessage(firewallsync.ReasonManagedOnlyInTarget),
@@ -627,17 +629,34 @@ func runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID s
} }
} }
func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error { func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
configured, err := loadConfiguredFirewallPortWhiteList() ports, err := loadFirewallPortWhiteList()
if err != nil { if err != nil {
return err return err
} }
required, err := LoadRequiredFirewallPortWhiteList() required, err := firewall.RequiredPortWhitelist(ports)
if err != nil { if err != nil {
return err return err
} }
ports := excludeFirewallPorts(configured, required) provider, err := s.selectedProvider(ctx)
return s.SyncSystemPorts(ctx, nil, systemPorts(ports)) 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)
syncErr := syncWhitelistRules(ctx, s, nil, prepared, report)
return errors.Join(prepareErr, syncErr)
} }
func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error { func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error {
@@ -705,33 +724,7 @@ func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error { func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
return newFirewallService(). return newFirewallService().
SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current)) SyncSystemPorts(context.Background(), firewall.ExpandPortWhitelist(previous), firewall.ExpandPortWhitelist(current))
}
func syncPanelRequiredPorts(provider string, ports []firewall.PortWhitelist) error {
loadPorts := func() ([]firewall.PortWhitelist, error) { return ports, nil }
if provider == constant.FirewallProviderIptables {
manager := newIptablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts(true)
}
initialized := false
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
familyInitialized, _, err := nftables_helper.LoadFamilyInitStatus(family, "base")
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
}
if err != nil {
return err
}
initialized = initialized || familyInitialized
}
if !initialized {
return nil
}
manager := newNftablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts()
} }
type forwardingRuleSyncCandidate struct { type forwardingRuleSyncCandidate struct {

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -22,6 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/copier"
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export" csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/geo" "github.com/1Panel-dev/1Panel/agent/utils/geo"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -217,10 +219,21 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err return err
} }
oldPortValue := strings.Join(loadSSHPortValues(directives), ",") oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
if req.Key == "Port" {
if err := checkSSHPortAvailability(splitSSHPorts(oldPortValue), splitSSHPorts(req.NewValue)); err != nil {
return err
}
}
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil { if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
return err return err
} }
if req.Key == "Port" { if req.Key == "Port" {
if err := newFirewallService().syncSystemAccessPortTransition(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 err
}
handleSSHPortUpdate(oldPortValue, req.NewValue) handleSSHPortUpdate(oldPortValue, req.NewValue)
} }
@@ -320,18 +333,6 @@ func handleSSHPortUpdate(oldValue, newValue string) {
} }
} }
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
if err != nil {
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
} else {
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
if err != nil {
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
}
}
primaryPort, err := loadPrimarySSHPort(newValue) primaryPort, err := loadPrimarySSHPort(newValue)
if err != nil { if err != nil {
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err) global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
@@ -371,6 +372,24 @@ func diffSSHPorts(left, right []string) []string {
return diff return diff
} }
func checkSSHPortAvailability(oldPorts, newPorts []string) error {
for _, port := range diffSSHPorts(newPorts, oldPorts) {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("invalid SSH port %q", port)
}
if common.ScanPort(value) {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
listener, err := net.Listen("tcp4", ":"+strconv.Itoa(value))
if err != nil {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
_ = listener.Close()
}
return nil
}
func loadPrimarySSHPort(value string) (int, error) { func loadPrimarySSHPort(value string) (int, error) {
ports := splitSSHPorts(value) ports := splitSSHPorts(value)
if len(ports) == 0 { if len(ports) == 0 {
@@ -379,18 +398,6 @@ func loadPrimarySSHPort(value string) (int, error) {
return strconv.Atoi(ports[0]) return strconv.Atoi(ports[0])
} }
func parseSSHPortsToInts(ports []string) ([]int, error) {
var values []int
for _, port := range ports {
value, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
values = append(values, value)
}
return values, nil
}
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) { func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)) cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if sudo != "" { if sudo != "" {

View File

@@ -3770,12 +3770,31 @@
}, },
"/hosts/firewall/settings/whitelist": { "/hosts/firewall/settings/whitelist": {
"bodyKeys": [ "bodyKeys": [
"value" "rule"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙端口白名单 [value]", "formatZH": "创建防火墙端口白名单",
"formatEN": "update firewall port whitelist [value]" "formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
}, },
"/hosts/monitor/clean": { "/hosts/monitor/clean": {
"bodyKeys": [], "bodyKeys": [],

View File

@@ -27,7 +27,7 @@ const (
FirewallPingStatusKey = "BanPing" FirewallPingStatusKey = "BanPing"
FirewallPortWhiteList = "FirewallPortWhiteList" FirewallPortWhiteList = "FirewallPortWhiteList"
FirewallPortWhiteListValue = "80/tcp,443/tcp,443/udp" FirewallPortWhiteListValue = `[{"port":"80","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"udp","sources":["0.0.0.0/0","::/0"]}]`
) )
const ( const (

View File

@@ -26,6 +26,15 @@ func Init() {
return return
} }
clientName := client.Name() clientName := client.Name()
initialize := false
defer func() {
if err := service.NewIFirewallService().SyncPortWhitelist(ctx); err != nil {
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
}
if initialize {
initDockerPortGuard(ctx)
}
}()
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil { if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err) global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
return return
@@ -40,11 +49,11 @@ func Init() {
if err := initForwardingRules(ctx); err != nil { if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err) global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
} }
if !needInit() { initialize = needInit()
if !initialize {
repairIptablesBaseChains(clientName) repairIptablesBaseChains(clientName)
return return
} }
defer initDockerPortGuard(ctx)
InitPingStatus() InitPingStatus()
global.LOG.Info("initializing firewall settings...") global.LOG.Info("initializing firewall settings...")
if clientName == "nftables" { if clientName == "nftables" {
@@ -64,17 +73,12 @@ func Init() {
return return
} }
settingRepo := repo.NewISettingRepo() settingRepo := repo.NewISettingRepo()
panelPort := service.LoadPanelPort()
if len(panelPort) == 0 {
global.LOG.Errorf("find 1panel service port failed")
return
}
requiredPorts, err := service.LoadRequiredFirewallPortWhiteList() requiredPorts, err := service.LoadRequiredFirewallPortWhiteList()
if err != nil { if err != nil {
global.LOG.Errorf("load required firewall ports failed, err: %v", err) global.LOG.Errorf("load required firewall ports failed, err: %v", err)
return return
} }
if err := iptables_helper.RestoreBaseChains(panelPort, requiredPorts); err != nil { if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
global.LOG.Errorf("restore iptables base chains failed, err: %v", err) global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return return
} }
@@ -100,7 +104,6 @@ func repairIptablesBaseChains(clientName string) {
return return
} }
manager := iptables_helper.Manager{ manager := iptables_helper.Manager{
PanelPort: service.LoadPanelPort,
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList, LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList,
} }
if err := manager.RepairBaseChains(); err != nil { if err := manager.RepairBaseChains(); err != nil {

View File

@@ -110,6 +110,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.NormalizeFirewallBackendSelections, migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy, migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly, migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
} }
} }

View File

@@ -0,0 +1,180 @@
package migrations
import (
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{
ID: "20260915-migrate-firewall-port-whitelist-sources",
Migrate: func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
rules, err := migrateFirewallPortWhitelist(setting.Value)
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
}
if setting.ID == 0 {
err = tx.Create(&model.Setting{Key: constant.FirewallPortWhiteList, Value: string(value)}).Error
} else {
err = tx.Model(&setting).Update("value", string(value)).Error
}
if err != nil {
return err
}
return tx.Where("key = ?", "FirewallPortWhiteListPending").Delete(&model.Setting{}).Error
},
}
type legacyPortWhitelist struct {
Ports []string `json:"ports"`
Family string `json:"family"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Sources []string `json:"sources"`
}
func (entry legacyPortWhitelist) singlePortRule() firewall.PortWhitelist {
rule := firewall.PortWhitelist{Port: entry.Port, Protocol: entry.Protocol, Type: entry.Type, Sources: entry.Sources}
if strings.TrimSpace(rule.Type) != "" && rule.Port == "" && len(entry.Ports) > 0 {
rule.Port = entry.Ports[0]
}
return rule
}
func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error) {
legacy, err := parseLegacyPortWhitelist(value)
if err != nil {
return nil, err
}
rules := make([]firewall.PortWhitelist, 0, len(legacy)+5)
indexes := make(map[string]int)
key := func(rule firewall.PortWhitelist) string {
if rule.Type != "" {
return rule.Type + "/" + rule.Protocol
}
return rule.Type + "/" + rule.Protocol + "/" + rule.Port
}
for index, entry := range legacy {
family := strings.ToLower(strings.TrimSpace(entry.Family))
if family != "" && family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("entry #%d: invalid address family %q", index+1, entry.Family)
}
rule := entry.singlePortRule()
if strings.TrimSpace(rule.Protocol) == "" {
rule.Protocol = "tcp"
}
if len(rule.Sources) == 0 {
rule.Sources = []string{"0.0.0.0/0"}
if family == constant.FirewallFamilyIPv6 {
rule.Sources = []string{"::/0"}
} else if family == "" && strings.TrimSpace(rule.Type) != "" {
rule.Sources = append(rule.Sources, "::/0")
}
}
rule.Sources, err = firewall.NormalizeWhitelistSources(family, rule.Sources)
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
normalized, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{rule})
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
rule = normalized[0]
if existing, found := indexes[key(rule)]; found {
rules[existing].Sources, err = firewall.NormalizeWhitelistSources("", append(rules[existing].Sources, rule.Sources...))
if err != nil {
return nil, err
}
continue
}
indexes[key(rule)] = len(rules)
rules = append(rules, rule)
}
defaults := []firewall.PortWhitelist{
{Type: firewall.PortWhitelistTypePanel, Protocol: "tcp"},
{Type: firewall.PortWhitelistTypeSSH, Protocol: "tcp"},
{Port: "443", Protocol: "tcp"},
{Port: "443", Protocol: "udp"},
{Port: "80", Protocol: "tcp"},
}
for _, rule := range defaults {
index, found := indexes[key(rule)]
if !found {
index = len(rules)
indexes[key(rule)] = index
rules = append(rules, rule)
}
var ipv4, ipv6 bool
for _, source := range rules[index].Sources {
if strings.Contains(source, ":") {
ipv6 = true
} else {
ipv4 = true
}
}
if !ipv4 {
rules[index].Sources = append(rules[index].Sources, "0.0.0.0/0")
}
if !ipv6 {
rules[index].Sources = append(rules[index].Sources, "::/0")
}
}
return firewall.ValidatePortWhitelist(rules)
}
func parseLegacyPortWhitelist(value string) ([]legacyPortWhitelist, error) {
value = strings.TrimSpace(value)
if value == "" || value == "null" {
return nil, nil
}
if strings.HasPrefix(value, "[") {
var rules []legacyPortWhitelist
err := json.Unmarshal([]byte(value), &rules)
return rules, err
}
items := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || unicode.IsSpace(r) })
rules := make([]legacyPortWhitelist, 0, len(items))
for _, item := range items {
parts := strings.Split(item, "/")
rule := legacyPortWhitelist{}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid legacy whitelist entry %q", item)
}
rules = append(rules, rule)
}
return rules, nil
}

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"path" "path"
"strconv"
"github.com/1Panel-dev/1Panel/agent/cmd/server/conf" "github.com/1Panel-dev/1Panel/agent/cmd/server/conf"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -61,4 +62,10 @@ func initBaseInfo() {
panic(err) panic(err)
} }
global.CONF.Base.InstallDir = nodeInfo.BaseDir global.CONF.Base.InstallDir = nodeInfo.BaseDir
if !global.IsMaster {
global.CONF.Base.Port = strconv.FormatUint(uint64(nodeInfo.NodePort), 10)
if nodeInfo.NodePort == 0 {
global.CONF.Base.Port = "9999"
}
}
} }

View File

@@ -27,7 +27,9 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort) hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings) hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend) hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
hostRouter.POST("/firewall/settings/whitelist", baseApi.UpdateFirewallPortWhitelist) hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo) hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo)
hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules) hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules)
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules) hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)

View File

@@ -1,6 +1,7 @@
package manager package manager
import ( import (
"fmt"
"strings" "strings"
) )
@@ -50,5 +51,5 @@ func (s *Snap) Operate(operate, serviceName string) error {
if s.IsExist(serviceName) { if s.IsExist(serviceName) {
return handlerErr(run(s.toolCmd, operate, serviceName)) return handlerErr(run(s.toolCmd, operate, serviceName))
} }
return nil return fmt.Errorf("snap service %q does not exist", serviceName)
} }

View File

@@ -65,7 +65,7 @@ func (s *Systemd) Operate(operate, serviceName string) error {
if err := NewSnap().Operate(operate, serviceName); err == nil { if err := NewSnap().Operate(operate, serviceName); err == nil {
return nil return nil
} }
return handlerErr(run(s.toolCmd, operate, serviceName)) return handlerErr(out, err)
} }
return nil return nil
} }

View File

@@ -28,9 +28,11 @@ const (
) )
type PortWhitelist struct { type PortWhitelist struct {
Family string `json:"family"` Family string `json:"-"`
Port string `json:"port"` Port string `json:"port,omitempty"`
Protocol string `json:"protocol"` Protocol string `json:"protocol,omitempty"`
Type string `json:"type,omitempty"`
Sources []string `json:"sources"`
} }
type Direction string type Direction string

View File

@@ -366,7 +366,14 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
deleteRuleCommand(normalized, marker), deleteRuleCommand(normalized, marker),
} }
case filter.ChangeUpdate, filter.ChangeReorder: case filter.ChangeUpdate, filter.ChangeReorder:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true) if change.Before == nil {
return filter.NativeRulePlan{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
}
before, err := filter.NormalizeRule(*change.Before)
if err != nil {
return filter.NativeRulePlan{}, err
}
target, targetErr := validateMutationTarget(snapshot, change, before, marker, true)
if targetErr != nil { if targetErr != nil {
return filter.NativeRulePlan{}, targetErr return filter.NativeRulePlan{}, targetErr
} }

View File

@@ -19,26 +19,7 @@ var ErrVerificationFailed = errors.New("firewall rule verification failed")
func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) { func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) {
rules := append([]ObservedRule(nil), snapshot.Rules...) rules := append([]ObservedRule(nil), snapshot.Rules...)
for index := range rules { for index := range rules {
rule := rules[index].Rule if rules[index].ParseStatus == ParseStatusSupported && RuleMatchesPortWhitelist(rules[index].Rule, ports) {
if rules[index].ParseStatus != ParseStatusSupported || rule.Action != ActionAccept ||
rule.SourceAddress != "" || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" {
continue
}
protected := false
for _, protectedPort := range ports {
family := strings.ToLower(strings.TrimSpace(protectedPort.Family))
if family != "" && rule.Scope.Family != FamilyInet && string(rule.Scope.Family) != family {
continue
}
if rule.Protocol != "all" && rule.Protocol != protectedPort.Protocol {
continue
}
if portCovers(rule.DestinationPort, protectedPort.Port) {
protected = true
break
}
}
if protected {
rules[index].Protected = true rules[index].Protected = true
} }
} }
@@ -55,6 +36,52 @@ func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error)
return protected, nil return protected, nil
} }
func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
rule, err := NormalizeRule(rule)
if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 {
return false
}
families := []Family{rule.Scope.Family}
if rule.Scope.Family == FamilyInet {
families = []Family{FamilyIPv4, FamilyIPv6}
}
for _, family := range families {
matched := false
for _, port := range ports {
portFamily := Family(strings.ToLower(strings.TrimSpace(port.Family)))
if portFamily != "" && !familiesOverlap(family, portFamily) {
continue
}
protocol, err := normalizeProtocol(port.Protocol)
if err != nil || rule.Protocol != protocol {
continue
}
portRange, err := normalizePort(port.Port)
if err != nil || rule.DestinationPort != portRange {
continue
}
sources := port.Sources
if len(sources) == 0 {
sources = []string{""}
}
for _, source := range sources {
normalized, err := normalizeAddress(source, family)
if err == nil && normalized == rule.SourceAddress {
matched = true
break
}
}
if matched {
break
}
}
if !matched {
return false
}
}
return true
}
func GuardMutation(target ObservedRule) error { func GuardMutation(target ObservedRule) error {
if target.Protected { if target.Protected {
return ErrProtectedRule return ErrProtectedRule
@@ -176,36 +203,6 @@ func addressesOverlap(left, right string) bool {
return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr()) return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr())
} }
func portCovers(existing, requested string) bool {
if existing == "" {
return true
}
if requested == "" {
return false
}
existingIntervals, err := portIntervals(existing)
if err != nil {
return false
}
requestedIntervals, err := portIntervals(requested)
if err != nil {
return false
}
for _, requestedInterval := range requestedIntervals {
covered := false
for _, existingInterval := range existingIntervals {
if existingInterval[0] <= requestedInterval[0] && existingInterval[1] >= requestedInterval[1] {
covered = true
break
}
}
if !covered {
return false
}
}
return true
}
func portsOverlap(left, right string) bool { func portsOverlap(left, right string) bool {
if left == "" || right == "" { if left == "" || right == "" {
return true return true

View File

@@ -4,62 +4,10 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
) )
type FilterRules struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Protocol string `json:"protocol"`
SrcPort string `json:"srcPort"`
DstPort string `json:"dstPort"`
SrcIP string `json:"srcIP"`
DstIP string `json:"dstIP"`
Strategy string `json:"strategy"`
Description string `json:"description"`
}
func readFilterRulesByChain(chain string, run func(string, ...string) (string, error)) ([]FilterRules, error) {
var rules []FilterRules
if cmd.CheckIllegal(chain) {
return rules, buserr.New("ErrCmdIllegal")
}
stdout, err := run(FilterTab, "-nL", chain)
if err != nil {
return rules, fmt.Errorf("load filter fules by chain %s failed, %v", chain, err)
}
lines := strings.Split(stdout, "\n")
for i := 0; i < len(lines); i++ {
fields := strings.Fields(lines[i])
if len(fields) > 2 && strings.Contains(fields[2], ":") {
fields = append(fields, "")
copy(fields[3:], fields[2:])
fields[2] = "--"
}
if len(fields) < 5 {
continue
}
strategy := strings.ToLower(fields[0])
if strategy != "accept" && strategy != "drop" && strategy != "reject" {
continue
}
itemRule := FilterRules{
Chain: chain,
Protocol: loadProtocol(fields[1]),
SrcPort: loadPort("src", fields),
DstPort: loadPort("dst", fields),
SrcIP: loadIP(fields[3]),
DstIP: loadIP(fields[4]),
Strategy: strategy,
}
rules = append(rules, itemRule)
}
return rules, nil
}
func LoadInitStatus(tab string) (bool, bool, error) { func LoadInitStatus(tab string) (bool, bool, error) {
return loadInitStatus(tab, RunWithStd, true) return loadInitStatus(tab, RunWithStd, true)
} }
@@ -172,47 +120,3 @@ func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool,
} }
return true, true return true, true
} }
func loadPort(position string, portStr []string) string {
if len(portStr) < 7 {
return ""
}
var portItem string
if strings.Contains(portStr[6], "spt:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spt:", "")
}
if strings.Contains(portStr[6], "dpt:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpt:", "")
}
if strings.Contains(portStr[6], "spts:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spts:", "")
}
if strings.Contains(portStr[6], "dpts:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpts:", "")
}
portItem = strings.ReplaceAll(portItem, ":", "-")
return portItem
}
func loadIP(ipStr string) string {
if ipStr == ANYWHERE || ipStr == "0.0.0.0/0" || ipStr == "::/0" {
return ""
}
return ipStr
}
func loadProtocol(protocol string) string {
switch protocol {
case "0":
return "all"
case "1":
return "icmp"
case "6":
return "tcp"
case "17":
return "udp"
default:
return protocol
}
}

View File

@@ -15,17 +15,14 @@ func (m *Manager) EnsureIPv6BaseChains() error {
if err != nil { if err != nil {
return err return err
} }
return EnsureIPv6BaseChains(m.panelPort(), ports) return EnsureIPv6BaseChains(ports)
} }
func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) error { func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() { if err != nil || !commands.IPv6Available() {
return fmt.Errorf("ip6tables and ip6tables-restore are required") return fmt.Errorf("ip6tables and ip6tables-restore are required")
} }
if panelPort == "" {
return fmt.Errorf("panel port is required")
}
output, err := RunIPv6WithStd(FilterTab, "-S") output, err := RunIPv6WithStd(FilterTab, "-S")
if err != nil { if err != nil {
return err return err
@@ -33,7 +30,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
if err := ensureBaseChainsFamily(true); err != nil { if err := ensureBaseChainsFamily(true); err != nil {
return err return err
} }
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, panelPort, ports, output) script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, ports, output)
if err != nil { if err != nil {
return err return err
} }
@@ -71,13 +68,13 @@ func BindIPv6BaseChains() error {
return setBaseChainBindings(true, true) return setBaseChainBindings(true, true)
} }
func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.PortWhitelist, output string) (string, error) { func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) {
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
if !containsIptablesRule(output, "-N "+chain) { if !containsIptablesRule(output, "-N "+chain) {
return buildBaseChainsRestoreScript(dir, panelPort, true, ports...) return buildBaseChainsRestoreScript(dir, true, ports...)
} }
} }
defaults, err := baseDefaultRules(panelPort, ports, constant.FirewallFamilyIPv6) defaults, err := baseDefaultRules(ports, constant.FirewallFamilyIPv6)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -92,15 +89,15 @@ func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.P
return script.String(), nil return script.String(), nil
} }
func baseDefaultRules(panelPort string, ports []firewall.PortWhitelist, family string) ([]string, error) { func baseDefaultRules(ports []firewall.PortWhitelist, family string) ([]string, error) {
ports, err := firewall.NormalizeRequiredPorts(append([]firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}, ports...)) ports, err := firewall.NormalizeRequiredPorts(ports)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule}
for _, port := range ports { for _, port := range firewall.ExpandPortWhitelist(ports) {
if port.Family == "" || port.Family == family { if port.Family == family {
rules = append(rules, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port)) rules = append(rules, iptablesSystemPortRuleLine(port))
} }
} }
return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil

View File

@@ -3,9 +3,10 @@ package iptables_helper
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "sort"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -13,11 +14,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
) )
type Manager struct { type Manager struct {
UpdateSetting func(key, value string) error UpdateSetting func(key, value string) error
PanelPort func() string
LoadRequiredPorts func() ([]firewall.PortWhitelist, error) LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
} }
@@ -77,6 +78,9 @@ func (m *Manager) enableBase(prepare bool) error {
if err := m.ensureIPv6BaseChains(); err != nil { if err := m.ensureIPv6BaseChains(); err != nil {
return err return err
} }
if err := m.SyncRequiredPorts(true); err != nil {
return err
}
} else if err := BindIPv6BaseChains(); err != nil { } else if err := BindIPv6BaseChains(); err != nil {
return err return err
} }
@@ -241,11 +245,7 @@ func saveBaseChains() error {
return nil return nil
} }
func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist) error { func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
port, err := strconv.Atoi(panelPort)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("invalid panel port %q", panelPort)
}
commands, err := lifecycle.ResolveIptablesCommands() commands, err := lifecycle.ResolveIptablesCommands()
if err != nil { if err != nil {
return err return err
@@ -253,7 +253,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
if err := ensureBaseChains(); err != nil { if err := ensureBaseChains(); err != nil {
return err return err
} }
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, false) input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, false, requiredPorts...)
if err != nil { if err != nil {
return err return err
} }
@@ -266,7 +266,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
if err := ensureBaseChainsFamily(true); err != nil { if err := ensureBaseChainsFamily(true); err != nil {
return err return err
} }
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, true, requiredPorts...) input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, true, requiredPorts...)
if err != nil { if err != nil {
return err return err
} }
@@ -276,7 +276,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
return nil return nil
} }
func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) { func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
var script strings.Builder var script strings.Builder
script.WriteString("*filter\n") script.WriteString("*filter\n")
for _, chain := range BasicChains() { for _, chain := range BasicChains() {
@@ -284,8 +284,6 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
script.WriteString(chain) script.WriteString(chain)
script.WriteByte('\n') script.WriteByte('\n')
} }
panelRule := "-A " + BasicBeforeChain + " -p tcp -m tcp --dport " + panelPort + " -j ACCEPT"
panelRuleFound := false
for _, item := range []struct{ chain, file string }{ for _, item := range []struct{ chain, file string }{
{BasicBeforeChain, BasicBeforeFileName}, {BasicBeforeChain, BasicBeforeFileName},
{BasicChain, BasicFileName}, {BasicChain, BasicFileName},
@@ -308,26 +306,21 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") { if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
continue continue
} }
if line == panelRule {
panelRuleFound = true
}
script.WriteString(line) script.WriteString(line)
script.WriteByte('\n') script.WriteByte('\n')
} }
} }
if !panelRuleFound { family := constant.FirewallFamilyIPv4
script.WriteString(panelRule)
script.WriteByte('\n')
}
if ipv6 { if ipv6 {
defaults, err := baseDefaultRules(panelPort, requiredPorts, constant.FirewallFamilyIPv6) family = constant.FirewallFamilyIPv6
if err != nil { }
return "", err defaults, err := baseDefaultRules(requiredPorts, family)
} if err != nil {
for _, rule := range defaults { return "", err
if !containsIptablesRule(script.String(), rule) { }
script.WriteString(rule + "\n") for _, rule := range defaults {
} if !containsIptablesRule(script.String(), rule) {
script.WriteString(rule + "\n")
} }
} }
script.WriteString("COMMIT\n") script.WriteString("COMMIT\n")
@@ -386,23 +379,22 @@ func (m *Manager) SyncRequiredPorts(withSave bool) error {
} }
func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitelist, withSave, includeDefaults, ipv6 bool) error { func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitelist, withSave, includeDefaults, ipv6 bool) error {
portWhiteList, err := firewall.NormalizeRequiredPorts(portWhiteList) ports, err := firewall.NormalizeRequiredPorts(portWhiteList)
if err != nil { if err != nil {
return err return err
} }
run, save := RunWithStd, SaveRulesToFile rules := firewall.ExpandPortWhitelist(ports)
run := RunWithStd
save := SaveRulesToFile
beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName
if ipv6 { if ipv6 {
run, save = RunIPv6WithStd, SaveIPv6RulesToFile run = RunIPv6WithStd
save = SaveIPv6RulesToFile
beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile) beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile)
} }
beforeRules, err := readFilterRulesByChain(BasicBeforeChain, run) family := constant.FirewallFamilyIPv4
if err != nil { if ipv6 {
return err family = constant.FirewallFamilyIPv6
}
afterRules, err := readFilterRulesByChain(BasicAfterChain, run)
if err != nil {
return err
} }
beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain) beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain)
if err != nil { if err != nil {
@@ -412,16 +404,16 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
if err != nil { if err != nil {
return err return err
} }
script := buildRequiredPortsRestoreScript(portWhiteList, beforeRules, afterRules, beforeRaw, afterRaw, includeDefaults) commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
script := buildRequiredPortsRestoreScript(rules, family, beforeRaw, afterRaw, includeDefaults)
if script != "" { if script != "" {
commands, resolveErr := lifecycle.ResolveIptablesCommands()
if resolveErr != nil {
return resolveErr
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
if err := restoreRules(restore, script); err != nil { if err := restoreRules(restore, script); err != nil {
return fmt.Errorf("batch sync required firewall ports with %s: %w", restore, err) return fmt.Errorf("batch sync required firewall ports with %s: %w", restore, err)
} }
@@ -436,53 +428,24 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
} }
func buildRequiredPortsRestoreScript( func buildRequiredPortsRestoreScript(
desired []firewall.PortWhitelist, desired []firewall.SystemPort,
beforeRules, afterRules []FilterRules, family string,
beforeRaw, afterRaw string, beforeRaw, afterRaw string,
includeDefaults bool, includeDefaults bool,
) string { ) string {
desiredKeys := firewall.PortWhitelistMap(desired) var commands []string
kept := make(map[string]struct{}, len(desired)) for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
commands := make([]string, 0) if !containsIptablesRule(beforeRaw, line) {
for _, rule := range beforeRules { commands = append(commands, line)
if !simpleAcceptedPortRule(rule) {
continue
}
key := firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: rule.Protocol, Port: rule.DstPort})
if _, wanted := desiredKeys[key]; wanted {
if _, alreadyKept := kept[key]; !alreadyKept {
kept[key] = struct{}{}
continue
}
}
commands = append(commands, iptablesPortRuleLine("-D", BasicBeforeChain, rule.Protocol, rule.DstPort))
}
for _, rule := range afterRules {
if simpleAcceptedPortRule(rule) && rule.Protocol == "udp" {
commands = append(commands, iptablesPortRuleLine("-D", BasicAfterChain, rule.Protocol, rule.DstPort))
} }
} }
for _, rule := range desired {
if includeDefaults { line := iptablesSystemPortRuleLine(rule)
for _, rule := range []string{ if rule.Family == family && !containsIptablesRule(beforeRaw, line) {
"-A " + BasicBeforeChain + " " + IoRuleIn, commands = append(commands, line)
"-A " + BasicBeforeChain + " " + EstablishedRule, beforeRaw += "\n" + line
} {
count := countIptablesRule(beforeRaw, rule)
for duplicate := 1; duplicate < count; duplicate++ {
commands = append(commands, strings.Replace(rule, "-A ", "-D ", 1))
}
if count == 0 {
commands = append(commands, rule)
}
} }
} }
for _, port := range desired {
if _, exists := kept[firewall.PortWhitelistKey(port)]; exists {
continue
}
commands = append(commands, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
}
if includeDefaults { if includeDefaults {
for _, rule := range []string{DropAllTcp, DropAllUdp} { for _, rule := range []string{DropAllTcp, DropAllUdp} {
line := "-A " + BasicAfterChain + " " + rule line := "-A " + BasicAfterChain + " " + rule
@@ -497,13 +460,12 @@ func buildRequiredPortsRestoreScript(
return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n" return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n"
} }
func simpleAcceptedPortRule(rule FilterRules) bool { func iptablesSystemPortRuleLine(rule firewall.SystemPort) string {
return rule.Strategy == "accept" && (rule.Protocol == "tcp" || rule.Protocol == "udp") && rule.DstPort != "" && parts := []string{"-A", BasicBeforeChain}
rule.SrcIP == "" && rule.DstIP == "" && rule.SrcPort == "" if rule.SourceAddress != "" {
} parts = append(parts, "-s", rule.SourceAddress)
}
func iptablesPortRuleLine(operation, chain, protocol, port string) string { return strings.Join(append(parts, "-p", rule.Protocol, "-m", rule.Protocol, "--dport", rule.Port, "-j", "ACCEPT"), " ")
return strings.Join([]string{operation, chain, "-p", protocol, "-m", protocol, "--dport", port, "-j", "ACCEPT"}, " ")
} }
func containsIptablesRule(output, rule string) bool { func containsIptablesRule(output, rule string) bool {
@@ -512,11 +474,27 @@ func containsIptablesRule(output, rule string) bool {
func countIptablesRule(output, rule string) int { func countIptablesRule(output, rule string) int {
canonical := func(value string) string { canonical := func(value string) string {
value = strings.TrimSpace(value) fields, err := shellwords.Parse(value)
if strings.Contains(value, " -j ACCEPT") { if err != nil || len(fields)%2 != 0 {
return strings.Replace(value, " -j ACCEPT", "", 1) + " -j ACCEPT" return strings.TrimSpace(value)
} }
return value var options []string
for index := 0; index < len(fields); index += 2 {
key, value := fields[index], fields[index+1]
if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") {
continue
}
if prefix, err := netip.ParsePrefix(value); err == nil {
prefix = prefix.Masked()
value = prefix.String()
if prefix.Bits() == prefix.Addr().BitLen() {
value = prefix.Addr().String()
}
}
options = append(options, key+" "+value)
}
sort.Strings(options)
return strings.Join(options, " ")
} }
rule = canonical(rule) rule = canonical(rule)
count := 0 count := 0
@@ -535,13 +513,6 @@ func (m *Manager) updateSetting(key, value string) error {
return nil return nil
} }
func (m *Manager) panelPort() string {
if m != nil && m.PanelPort != nil {
return m.PanelPort()
}
return ""
}
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) { func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
if m != nil && m.LoadRequiredPorts != nil { if m != nil && m.LoadRequiredPorts != nil {
return m.LoadRequiredPorts() return m.LoadRequiredPorts()

View File

@@ -39,7 +39,7 @@ func (m *Manager) RepairBaseChains() error {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return baseDefaultRules(m.panelPort(), ports, family) return baseDefaultRules(ports, family)
}) })
if err != nil { if err != nil {
return fmt.Errorf("prepare %s base chain repair: %w", family, err) return fmt.Errorf("prepare %s base chain repair: %w", family, err)

View File

@@ -4,9 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -111,12 +111,15 @@ func (m *Manager) ensureBaseChains() error {
return nil return nil
} }
func requiredPortCommand(tableFamily string, port firewall.PortWhitelist) []string { func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string {
return []string{ command := []string{
"add", "rule", tableFamily, TableName, BasicBeforeChain, "add", "rule", tableFamily, TableName, BasicBeforeChain,
"meta", "l4proto", port.Protocol, port.Protocol, "dport", port.Port,
"accept", "comment", `"` + requiredPortComment + `"`,
} }
if rule.SourceAddress != "" {
command = append(command, tableFamily, "saddr", rule.SourceAddress)
}
return append(command, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port,
"accept", "comment", `"`+requiredPortComment+`"`)
} }
func (m *Manager) initPreRules() error { func (m *Manager) initPreRules() error {
@@ -128,17 +131,10 @@ func (m *Manager) initPreRules() error {
if err != nil { if err != nil {
return err return err
} }
commands := make([][]string, 0, 12+len(ports)*2) rules := firewall.ExpandPortWhitelist(ports)
commands := requiredPortCommands(rules)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) 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 _, port := range ports {
commands = append(commands, requiredPortCommand(tableFamily, port))
}
commands = append(commands, commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain}, []string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"}, []string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
@@ -157,9 +153,31 @@ func (m *Manager) SyncRequiredPorts() error {
if err != nil { if err != nil {
return err return err
} }
commands, err := requiredPortSyncCommands(run, ports) rules := firewall.ExpandPortWhitelist(ports)
if err != nil { var commands [][]string
return err 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 { if err := runBatch(commands...); err != nil {
return err return err
@@ -167,99 +185,50 @@ func (m *Manager) SyncRequiredPorts() error {
return PersistRuleset(context.Background()) return PersistRuleset(context.Background())
} }
func requiredPortSyncCommands(run func(...string) (string, error), ports []firewall.PortWhitelist) ([][]string, error) { func containsRequiredPortRule(output, expression string) bool {
commands := make([][]string, 0) canonical := func(line string) string {
line, _, _ = strings.Cut(line, " comment ")
line, _, _ = strings.Cut(line, " # handle ")
for _, protocol := range []string{"tcp", "udp"} {
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
}
fields := strings.Fields(line)
for index, field := range fields {
if prefix, err := netip.ParsePrefix(field); err == nil {
prefix = prefix.Masked()
fields[index] = prefix.String()
if prefix.Bits() == prefix.Addr().BitLen() {
fields[index] = prefix.Addr().String()
}
}
}
return strings.Join(fields, " ")
}
wanted := canonical(expression)
for _, line := range strings.Split(output, "\n") {
if canonical(line) == wanted {
return true
}
}
return false
}
func requiredPortCommands(rules []firewall.SystemPort) [][]string {
commands := make([][]string, 0, len(rules)+6)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} { for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family) tableFamily := TableFamily(family)
_, exists, err := readNftObject(run, "list", "table", tableFamily, TableName) commands = append(commands,
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) { []string{"flush", "chain", tableFamily, TableName, BasicBeforeChain},
continue []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"`},
if err != nil { )
return nil, err for _, rule := range rules {
} if rule.Family == string(family) {
if !exists { commands = append(commands, requiredPortCommand(tableFamily, rule))
continue
}
stdout, err := run("-n", "-a", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if err != nil {
return nil, err
}
existing := requiredPortRules(stdout)
missing, staleHandles := requiredPortChanges(existing, ports)
for _, port := range missing {
commands = append(commands, requiredPortCommand(tableFamily, port))
}
for _, handle := range staleHandles {
commands = append(commands, []string{"delete", "rule", tableFamily, TableName, BasicBeforeChain, "handle", handle})
}
}
return commands, nil
}
type requiredPortRule struct {
Key string
Handle string
}
func requiredPortChanges(existing []requiredPortRule, desiredPorts []firewall.PortWhitelist) ([]firewall.PortWhitelist, []string) {
desired := firewall.PortWhitelistMap(desiredPorts)
existingKeys := make(map[string]struct{}, len(existing))
for _, rule := range existing {
existingKeys[rule.Key] = struct{}{}
}
missing := make([]firewall.PortWhitelist, 0)
for _, port := range desiredPorts {
if _, exists := existingKeys[firewall.PortWhitelistKey(port)]; !exists {
missing = append(missing, port)
}
}
kept := make(map[string]struct{}, len(existing))
staleHandles := make([]string, 0)
for _, rule := range existing {
if _, wanted := desired[rule.Key]; wanted {
if _, alreadyKept := kept[rule.Key]; !alreadyKept {
kept[rule.Key] = struct{}{}
continue
} }
} }
staleHandles = append(staleHandles, rule.Handle)
} }
return missing, staleHandles return commands
}
func requiredPortRules(output string) []requiredPortRule {
rules := make([]requiredPortRule, 0)
marker := `comment "` + requiredPortComment + `"`
for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, marker) {
continue
}
handleIndex := strings.LastIndex(line, "# handle ")
if handleIndex < 0 {
continue
}
handle := strings.TrimSpace(line[handleIndex+len("# handle "):])
if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
continue
}
fields := strings.Fields(line[:handleIndex])
for index := 0; index+2 < len(fields); index++ {
protocol := fields[index]
if (protocol != "tcp" && protocol != "udp") || fields[index+1] != "dport" {
continue
}
port, err := strconv.Atoi(fields[index+2])
if err != nil || port < 1 || port > 65535 {
break
}
rules = append(rules, requiredPortRule{
Key: firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: protocol, Port: strconv.Itoa(port)}), Handle: handle,
})
break
}
}
return rules
} }
func (m *Manager) updateSetting(key, value string) error { func (m *Manager) updateSetting(key, value string) error {

View File

@@ -3,6 +3,7 @@ package firewall
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/netip"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -13,73 +14,69 @@ import (
type PortWhitelist = filter.PortWhitelist type PortWhitelist = filter.PortWhitelist
func ParsePortWhitelist(value string) ([]PortWhitelist, error) { const (
value = strings.TrimSpace(value) PortWhitelistTypePanel = "panel"
if value == "" { PortWhitelistTypeSSH = "ssh"
return []PortWhitelist{}, nil )
}
if strings.HasPrefix(value, "[") {
var rules []PortWhitelist
if err := json.Unmarshal([]byte(value), &rules); err != nil {
return nil, fmt.Errorf("invalid firewall port whitelist JSON: %w", err)
}
return validatePortWhitelist(rules)
}
items := strings.FieldsFunc(value, func(r rune) bool { func ParsePortWhitelist(value string) ([]PortWhitelist, error) {
return r == ',' || r == '\n' || r == ';' || r == ' ' var rules []PortWhitelist
}) if err := json.Unmarshal([]byte(value), &rules); err != nil {
rules := make([]PortWhitelist, 0, len(items)) return nil, err
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
parts := strings.Split(item, "/")
rule := PortWhitelist{Family: constant.FirewallFamilyIPv4, Protocol: "tcp"}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid firewall port whitelist: %s", item)
}
rules = append(rules, rule)
} }
return validatePortWhitelist(rules) return ValidatePortWhitelist(rules)
} }
func validatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) { func ValidatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
if rules == nil {
return nil, fmt.Errorf("firewall port whitelist must be an array")
}
result := make([]PortWhitelist, 0, len(rules)) result := make([]PortWhitelist, 0, len(rules))
exists := make(map[string]struct{}, len(rules)) seen := make(map[string]bool, len(rules))
for _, rule := range rules { for _, rule := range rules {
rule.Family = strings.ToLower(strings.TrimSpace(rule.Family)) rule.Type = strings.ToLower(strings.TrimSpace(rule.Type))
if rule.Family == "" {
rule.Family = constant.FirewallFamilyIPv4
}
if rule.Family != constant.FirewallFamilyIPv4 && rule.Family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("invalid firewall port whitelist family: %s", rule.Family)
}
rule.Protocol = strings.ToLower(strings.TrimSpace(rule.Protocol)) rule.Protocol = strings.ToLower(strings.TrimSpace(rule.Protocol))
if rule.Protocol == "" { if rule.Type != "" && rule.Protocol == "" {
rule.Protocol = "tcp" rule.Protocol = "tcp"
} }
if rule.Protocol != "tcp" && rule.Protocol != "udp" { if rule.Protocol != "tcp" && rule.Protocol != "udp" {
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", rule.Protocol) return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", rule.Protocol)
} }
port, err := normalizeWhitelistPort(rule.Port) if rule.Type != "" {
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)
}
} else {
var err error
rule.Port, err = normalizeWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
}
if len(rule.Sources) == 0 {
return nil, fmt.Errorf("firewall port whitelist requires at least one source")
}
var err error
rule.Sources, err = NormalizeWhitelistSources("", rule.Sources)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rule.Port = port rule.Family = ""
key := PortWhitelistKey(rule) key := rule.Type + "/" + rule.Protocol + "/" + rule.Port
if _, ok := exists[key]; ok { if rule.Type != "" {
continue key = rule.Type + "/" + rule.Protocol
} }
exists[key] = struct{}{} if seen[key] {
return nil, fmt.Errorf("duplicate firewall port whitelist: %s", key)
}
seen[key] = true
result = append(result, rule) result = append(result, rule)
} }
return result, nil return result, nil
@@ -132,10 +129,10 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
if item.Port == "" { if item.Port == "" {
continue continue
} }
baseKey := item.Port + "/" + strings.ToLower(strings.TrimSpace(item.Protocol)) baseKey := strings.Join([]string{item.Port, item.Protocol, strings.Join(item.Sources, ",")}, "/")
duplicate := false duplicate := false
for _, current := range ports { for _, current := range ports {
currentBaseKey := current.Port + "/" + strings.ToLower(strings.TrimSpace(current.Protocol)) currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey == baseKey && (current.Family == "" || current.Family == item.Family) { if currentBaseKey == baseKey && (current.Family == "" || current.Family == item.Family) {
duplicate = true duplicate = true
break break
@@ -147,7 +144,7 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
if item.Family == "" { if item.Family == "" {
filtered := ports[:0] filtered := ports[:0]
for _, current := range ports { for _, current := range ports {
currentBaseKey := current.Port + "/" + strings.ToLower(strings.TrimSpace(current.Protocol)) currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey != baseKey { if currentBaseKey != baseKey {
filtered = append(filtered, current) filtered = append(filtered, current)
} }
@@ -159,26 +156,75 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
return ports return ports
} }
func PortWhitelistMap(items []PortWhitelist) map[string]struct{} { func NormalizeWhitelistSources(family string, sources []string) ([]string, error) {
ports := make(map[string]struct{}, len(items)) result := make([]string, 0, len(sources))
for _, item := range items { seen := make(map[string]struct{}, len(sources))
ports[PortWhitelistKey(item)] = struct{}{} for _, source := range sources {
source = strings.TrimSpace(source)
prefix, err := netip.ParsePrefix(source)
if err != nil {
address, err := netip.ParseAddr(source)
if err != nil {
return nil, err
}
prefix = netip.PrefixFrom(address, address.BitLen())
}
sourceFamily := family
if sourceFamily == "" {
sourceFamily = constant.FirewallFamilyIPv6
if prefix.Addr().Unmap().Is4() {
sourceFamily = constant.FirewallFamilyIPv4
}
}
rule, err := filter.NormalizeRule(RuleForSystemPort(filter.ProviderIptables, SystemPort{
Family: sourceFamily, Port: "1", Protocol: "tcp", SourceAddress: source,
}))
if err != nil {
return nil, err
}
if rule.SourceAddress == "" {
rule.SourceAddress = "0.0.0.0/0"
if sourceFamily == constant.FirewallFamilyIPv6 {
rule.SourceAddress = "::/0"
}
}
if _, exists := seen[rule.SourceAddress]; !exists {
seen[rule.SourceAddress] = struct{}{}
result = append(result, rule.SourceAddress)
}
} }
return ports return result, nil
}
func PortWhitelistKey(item PortWhitelist) string {
key := item.Port + "/" + strings.ToLower(strings.TrimSpace(item.Protocol))
if family := strings.ToLower(strings.TrimSpace(item.Family)); family != "" {
return family + "/" + key
}
return key
} }
type SystemPort struct { type SystemPort struct {
Family string Family string
Port string Port string
Protocol string Protocol string
SourceAddress string
}
func ExpandPortWhitelist(ports []PortWhitelist) []SystemPort {
result := make([]SystemPort, 0, len(ports))
for _, port := range ports {
sources := port.Sources
if len(sources) == 0 {
sources = []string{"0.0.0.0/0", "::/0"}
}
for _, source := range sources {
family := constant.FirewallFamilyIPv4
if strings.Contains(source, ":") {
family = constant.FirewallFamilyIPv6
}
if port.Family != "" && port.Family != family {
continue
}
if source == "0.0.0.0/0" || source == "::/0" {
source = ""
}
result = append(result, SystemPort{Family: family, Port: port.Port, Protocol: port.Protocol, SourceAddress: source})
}
}
return result
} }
func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.FirewallRule { func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.FirewallRule {
@@ -203,7 +249,7 @@ func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.Firewal
} }
return filter.FirewallRule{ return filter.FirewallRule{
Scope: scope, Protocol: port.Protocol, DestinationPort: port.Port, Scope: scope, Protocol: port.Protocol, DestinationPort: port.Port,
Action: filter.ActionAccept, SourceAddress: port.SourceAddress, Action: filter.ActionAccept,
} }
} }
@@ -218,7 +264,10 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
if family != "" { if family != "" {
family = string(normalized.Scope.Family) family = string(normalized.Scope.Family)
} }
item := SystemPort{Family: family, Port: normalized.DestinationPort, Protocol: normalized.Protocol} item := SystemPort{
Family: family, Port: normalized.DestinationPort,
Protocol: normalized.Protocol, SourceAddress: normalized.SourceAddress,
}
result[SystemPortKey(item)] = item result[SystemPortKey(item)] = item
} }
return result, nil return result, nil
@@ -227,7 +276,10 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
func SystemPortKey(port SystemPort) string { func SystemPortKey(port SystemPort) string {
key := LegacySystemPortKey(port) key := LegacySystemPortKey(port)
if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" { if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" {
return family + "/" + key key = family + "/" + key
}
if source := strings.TrimSpace(port.SourceAddress); source != "" {
key += "/" + source
} }
return key return key
} }
@@ -277,7 +329,31 @@ func NormalizeRequiredPorts(ports []PortWhitelist) ([]PortWhitelist, error) {
return nil, fmt.Errorf("invalid required firewall port %q", port.Port) return nil, fmt.Errorf("invalid required firewall port %q", port.Port)
} }
port.Port = strconv.Itoa(portNumber) port.Port = strconv.Itoa(portNumber)
if len(port.Sources) > 0 {
port.Sources, err = NormalizeWhitelistSources(port.Family, port.Sources)
if err != nil {
return nil, err
}
}
result = append(result, port) result = append(result, port)
} }
return NormalizePortWhitelist(result), nil return NormalizePortWhitelist(result), nil
} }
func RequiredPortWhitelist(entries []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
continue
}
if entry.Port == "" {
return nil, fmt.Errorf("firewall whitelist %s has no stored port", entry.Type)
}
protocol := strings.ToLower(strings.TrimSpace(entry.Protocol))
if protocol == "" {
protocol = "tcp"
}
result = append(result, PortWhitelist{Family: entry.Family, Port: entry.Port, Protocol: protocol, Sources: entry.Sources})
}
return NormalizeRequiredPorts(result)
}

View File

@@ -19418,7 +19418,103 @@ const docTemplate = `{
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"description": "Returns a taskID; configuration save and per-rule results are recorded in the task log.", "description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistCreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Create firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rule"
],
"formatEN": "create firewall port whitelist",
"formatZH": "创建防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/delete": {
"post": {
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistDelete"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Delete firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rules"
],
"formatEN": "delete firewall port whitelist",
"formatZH": "删除防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/update": {
"post": {
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@@ -19446,17 +19542,18 @@ const docTemplate = `{
"Timestamp": [] "Timestamp": []
} }
], ],
"summary": "Queue firewall port whitelist update", "summary": "Update firewall port whitelist rules",
"tags": [ "tags": [
"Firewall" "Firewall"
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [
"value" "oldRule",
"rule"
], ],
"formatEN": "update firewall port whitelist [value]", "formatEN": "update firewall port whitelist",
"formatZH": "更新防火墙端口白名单 [value]", "formatZH": "编辑防火墙端口白名单",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -36182,6 +36279,43 @@ const docTemplate = `{
], ],
"type": "object" "type": "object"
}, },
"dto.FirewallPortWhitelistCreate": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistDelete": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"oldRule": {
"$ref": "#/definitions/filter.PortWhitelist"
},
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"oldRule",
"rule"
],
"type": "object"
},
"dto.FirewallRuleAdopt": { "dto.FirewallRuleAdopt": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -36452,17 +36586,6 @@ const docTemplate = `{
], ],
"type": "object" "type": "object"
}, },
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"value": {
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"dto.FirewallSettings": { "dto.FirewallSettings": {
"properties": { "properties": {
"docker": { "docker": {
@@ -36475,7 +36598,10 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"portWhiteList": { "portWhiteList": {
"type": "string" "type": "array",
"items": {
"$ref": "#/definitions/filter.PortWhitelist"
}
}, },
"system": { "system": {
"$ref": "#/definitions/dto.FirewallBackendGroup" "$ref": "#/definitions/dto.FirewallBackendGroup"
@@ -41473,6 +41599,26 @@ const docTemplate = `{
"PersistenceStatusPermanentOnly" "PersistenceStatusPermanentOnly"
] ]
}, },
"filter.PortWhitelist": {
"properties": {
"port": {
"type": "string"
},
"protocol": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"type": "string"
}
},
"type": "object"
},
"filter.RuntimeUsage": { "filter.RuntimeUsage": {
"properties": { "properties": {
"reason": { "reason": {

View File

@@ -19414,7 +19414,103 @@
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"description": "Returns a taskID; configuration save and per-rule results are recorded in the task log.", "description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistCreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Create firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rule"
],
"formatEN": "create firewall port whitelist",
"formatZH": "创建防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/delete": {
"post": {
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistDelete"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Delete firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rules"
],
"formatEN": "delete firewall port whitelist",
"formatZH": "删除防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/update": {
"post": {
"consumes": [
"application/json"
],
"description": "Returns a synchronization taskID. The whitelist configuration is saved only after synchronization succeeds.",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@@ -19442,17 +19538,18 @@
"Timestamp": [] "Timestamp": []
} }
], ],
"summary": "Queue firewall port whitelist update", "summary": "Update firewall port whitelist rules",
"tags": [ "tags": [
"Firewall" "Firewall"
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [
"value" "oldRule",
"rule"
], ],
"formatEN": "update firewall port whitelist [value]", "formatEN": "update firewall port whitelist",
"formatZH": "更新防火墙端口白名单 [value]", "formatZH": "编辑防火墙端口白名单",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -36178,6 +36275,43 @@
], ],
"type": "object" "type": "object"
}, },
"dto.FirewallPortWhitelistCreate": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistDelete": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"oldRule": {
"$ref": "#/definitions/filter.PortWhitelist"
},
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"oldRule",
"rule"
],
"type": "object"
},
"dto.FirewallRuleAdopt": { "dto.FirewallRuleAdopt": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -36448,17 +36582,6 @@
], ],
"type": "object" "type": "object"
}, },
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"value": {
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"dto.FirewallSettings": { "dto.FirewallSettings": {
"properties": { "properties": {
"docker": { "docker": {
@@ -36471,7 +36594,10 @@
"type": "string" "type": "string"
}, },
"portWhiteList": { "portWhiteList": {
"type": "string" "type": "array",
"items": {
"$ref": "#/definitions/filter.PortWhitelist"
}
}, },
"system": { "system": {
"$ref": "#/definitions/dto.FirewallBackendGroup" "$ref": "#/definitions/dto.FirewallBackendGroup"
@@ -41469,6 +41595,26 @@
"PersistenceStatusPermanentOnly" "PersistenceStatusPermanentOnly"
] ]
}, },
"filter.PortWhitelist": {
"properties": {
"port": {
"type": "string"
},
"protocol": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"type": "string"
}
},
"type": "object"
},
"filter.RuntimeUsage": { "filter.RuntimeUsage": {
"properties": { "properties": {
"reason": { "reason": {
@@ -49043,4 +49189,4 @@
"type": "object" "type": "object"
} }
} }
} }

View File

@@ -3770,12 +3770,31 @@
}, },
"/hosts/firewall/settings/whitelist": { "/hosts/firewall/settings/whitelist": {
"bodyKeys": [ "bodyKeys": [
"value" "rule"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙端口白名单 [value]", "formatZH": "创建防火墙端口白名单",
"formatEN": "update firewall port whitelist [value]" "formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
}, },
"/hosts/monitor/clean": { "/hosts/monitor/clean": {
"bodyKeys": [], "bodyKeys": [],

View File

@@ -33,8 +33,19 @@ export namespace Firewall {
forwarding: BackendGroup; forwarding: BackendGroup;
docker: BackendGroup; docker: BackendGroup;
pingStatus: string; pingStatus: string;
portWhiteList: string; portWhiteList: PortWhitelist[];
} }
export interface PortWhitelist {
port?: string;
protocol?: 'tcp' | 'udp';
type?: 'panel' | 'ssh';
sources: string[];
}
export interface PortWhitelistUpdate {
oldRule: PortWhitelist;
rule: PortWhitelist;
}
export interface BackendOperateRequest { export interface BackendOperateRequest {
subsystem: BackendSubsystem; subsystem: BackendSubsystem;
backend: Provider; backend: Provider;

View File

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

View File

@@ -4300,9 +4300,8 @@ const message = {
unbindHelper: unbindHelper:
'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?', 'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?',
portWhiteList: 'Port allowlist', portWhiteList: 'Port allowlist',
portWhiteListAlter: portWhiteListHelper:
'Whitelist changes take effect immediately when saved. Added ports are allowed automatically; removing a port only removes whitelist protection. To close it, delete its allow rule from the rule list.', 'Enter a single port (e.g. 80) or a port range (e.g. 8000-8100). Port numbers must be between 1 and 65535.',
portWhiteListHelper: 'Supports IPv4/IPv6, TCP/UDP, single ports, and port ranges such as 8000-8100.',
chain: 'Chain', chain: 'Chain',
sourceIP: 'Source IP', sourceIP: 'Source IP',
destIP: 'Destination IP', destIP: 'Destination IP',
@@ -4313,6 +4312,13 @@ const message = {
allPorts: 'All Ports', allPorts: 'All Ports',
allProtocolHelper: 'All protocols and 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', 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.',
whitelistSourcesHelper:
'Enter IP addresses or CIDR ranges, separated by commas or new lines. Leave blank to allow all IPv4 and IPv6 sources.',
systemAccessSourceError: 'Enter at least one valid {0} address or CIDR range.',
destinationPortPlaceholder: 'e.g. 80, 80,443, or 8080-8089', destinationPortPlaceholder: 'e.g. 80, 80,443, or 8080-8089',
deleteRuleConfirm: 'Will delete {0} rules. Continue?', deleteRuleConfirm: 'Will delete {0} rules. Continue?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4354,9 +4354,8 @@ const message = {
unbindHelper: unbindHelper:
'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?', 'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?',
portWhiteList: 'Lista blanca de puertos', portWhiteList: 'Lista blanca de puertos',
portWhiteListAlter: portWhiteListHelper:
'Los cambios se aplican al guardar. Los puertos añadidos se permiten automáticamente; quitar un puerto solo elimina su protección. Para cerrarlo, elimine su regla de permiso en la lista de reglas.', 'Introduzca un puerto (p. ej., 80) o un rango (p. ej., 8000-8100). Los puertos deben estar entre 1 y 65535.',
portWhiteListHelper: 'Admite IPv4/IPv6, TCP/UDP, puertos individuales y rangos como 8000-8100.',
chain: 'Cadena', chain: 'Cadena',
sourceIP: 'IP de Origen', sourceIP: 'IP de Origen',
destIP: 'IP de Destino', destIP: 'IP de Destino',
@@ -4367,6 +4366,13 @@ const message = {
allPorts: 'Todos los Puertos', allPorts: 'Todos los Puertos',
allProtocolHelper: 'Todos los protocolos y 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', 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.',
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.',
systemAccessSourceError: 'Introduzca al menos una dirección o red CIDR {0} válida.',
destinationPortPlaceholder: 'p. ej. 80, 80,443 o 8080-8089', destinationPortPlaceholder: 'p. ej. 80, 80,443 o 8080-8089',
deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?', deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4257,9 +4257,8 @@ const message = {
unbindHelper: unbindHelper:
'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بی‌اثر می‌شوند. با احتیاط ادامه دهید. تأیید می‌کنید؟', 'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بی‌اثر می‌شوند. با احتیاط ادامه دهید. تأیید می‌کنید؟',
portWhiteList: 'لیست سفید پورت', portWhiteList: 'لیست سفید پورت',
portWhiteListAlter: portWhiteListHelper:
'تغییرات فهرست مجاز بلافاصله پس از ذخیره اعمال می‌شوند. پورت‌های جدید خودکار مجاز می‌شوند؛ حذف پورت فقط حفاظت آن را برمی‌دارد. برای بستن پورت، قانون اجازه آن را از فهرست قوانین حذف کنید.', 'یک پورت (مانند 80) یا بازهٔ پورت (مانند 8000-8100) وارد کنید. شمارهٔ پورت باید بین 1 و 65535 باشد.',
portWhiteListHelper: 'از IPv4/IPv6، TCP/UDP، پورت تکی و بازه‌هایی مانند 8000-8100 پشتیبانی می‌کند.',
chain: 'زنجیره', chain: 'زنجیره',
sourceIP: 'IP مبدأ', sourceIP: 'IP مبدأ',
destIP: 'IP مقصد', destIP: 'IP مقصد',
@@ -4270,6 +4269,13 @@ const message = {
allPorts: 'همه پورت‌ها', allPorts: 'همه پورت‌ها',
allProtocolHelper: 'همه پروتکل‌ها و پورت‌ها', allProtocolHelper: 'همه پروتکل‌ها و پورت‌ها',
sourceAddressPlaceholder: 'مثلاً: 172.16.10.11، 172.16.0.0/24، 2001:db8::1 یا 2001:db8::/64', sourceAddressPlaceholder: 'مثلاً: 172.16.10.11، 172.16.0.0/24، 2001:db8::1 یا 2001:db8::/64',
systemAccessChangeConfirm:
'این عملیات روی قوانین فهرست مجاز {0} ممکن است دسترسی راه دور از طریق این سرویس‌ها را قطع کند. ادامه می‌دهید؟',
whitelistServicePortsHelper:
'یک درگاه مجاز برای دیواره آتش تنظیم کنید. درگاه شنود را در تنظیمات سرویس تغییر دهید.',
whitelistSourcesHelper:
'آدرس‌های IP یا محدوده‌های CIDR را با ویرگول یا خط جدید جدا کنید. برای مجاز کردن همه مبدأهای IPv4 و IPv6، این قسمت را خالی بگذارید.',
systemAccessSourceError: 'حداقل یک نشانی {0} یا بازه CIDR معتبر وارد کنید.',
destinationPortPlaceholder: 'مثلاً: 80، 80,443 یا 8080-8089', destinationPortPlaceholder: 'مثلاً: 80، 80,443 یا 8080-8089',
deleteRuleConfirm: '{0} قانون حذف خواهند شد. ادامه می‌دهید؟', deleteRuleConfirm: '{0} قانون حذف خواهند شد. ادامه می‌دهید؟',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4284,9 +4284,8 @@ const message = {
unbindHelper: unbindHelper:
'アンバインド - アンバインドすると追加されたすべてのファイアウォールルールが無効になります注意して操作してください確認しますか', 'アンバインド - アンバインドすると追加されたすべてのファイアウォールルールが無効になります注意して操作してください確認しますか',
portWhiteList: 'ポートホワイトリスト', portWhiteList: 'ポートホワイトリスト',
portWhiteListAlter: portWhiteListHelper:
'変更は保存時に即座に反映されます追加したポートは自動的に許可されます削除したポートは保護のみ解除されるため閉じるにはルール一覧から許可ルールを削除してください', '単一ポート80またはポート範囲8000-8100を入力してくださいポート番号は 165535 です',
portWhiteListHelper: 'IPv4/IPv6TCP/UDP単一ポート8000-8100 のようなポート範囲に対応します',
chain: 'チェーン', chain: 'チェーン',
sourceIP: '送信元 IP', sourceIP: '送信元 IP',
destIP: '宛先 IP', destIP: '宛先 IP',
@@ -4297,6 +4296,13 @@ const message = {
allPorts: 'すべてのポート', allPorts: 'すべてのポート',
allProtocolHelper: 'すべてのプロトコルとポート', allProtocolHelper: 'すべてのプロトコルとポート',
sourceAddressPlaceholder: ': 172.16.10.11172.16.0.0/242001:db8::12001:db8::/64', sourceAddressPlaceholder: ': 172.16.10.11172.16.0.0/242001:db8::12001:db8::/64',
systemAccessChangeConfirm:
'この操作を {0} の許可リストルールに適用するとこれらのサービス経由のリモートアクセスができなくなる可能性があります続行しますか',
whitelistServicePortsHelper:
'ファイアウォールで許可するポートを1つ設定します待ち受けポートは各サービスの設定で変更してください',
whitelistSourcesHelper:
'IP アドレスまたは CIDR をカンマか改行で区切って入力してください 空欄の場合すべての IPv4 および IPv6 の接続元を許可します',
systemAccessSourceError: '有効な {0} アドレスまたは CIDR を1つ以上入力してください',
destinationPortPlaceholder: ': 8080,4438080-8089', destinationPortPlaceholder: ': 8080,4438080-8089',
deleteRuleConfirm: '{0} 個のルールを削除します続行しますか', deleteRuleConfirm: '{0} 個のルールを削除します続行しますか',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4204,9 +4204,8 @@ const message = {
unbindHelper: unbindHelper:
'바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?', '바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?',
portWhiteList: '포트 화이트리스트', portWhiteList: '포트 화이트리스트',
portWhiteListAlter: portWhiteListHelper:
'변경 사항은 저장 즉시 적용됩니다. 추가한 포트는 자동으로 허용됩니다. 포트를 제거하면 보호만 해제됩니다. 포트를 닫으려면 규칙 목록에서 허용 규칙을 삭제하세요.', '단일 포트(예: 80) 또는 포트 범위(예: 8000-8100)를 입력하세요. 포트 번호는 1~65535여야 합니다.',
portWhiteListHelper: 'IPv4/IPv6, TCP/UDP, 단일 포트 및 8000-8100과 같은 포트 범위를 지원합니다.',
chain: '체인', chain: '체인',
sourceIP: '소스 IP', sourceIP: '소스 IP',
destIP: '대상 IP', destIP: '대상 IP',
@@ -4217,6 +4216,13 @@ const message = {
allPorts: '모든 포트', allPorts: '모든 포트',
allProtocolHelper: '모든 프로토콜 및 포트', allProtocolHelper: '모든 프로토콜 및 포트',
sourceAddressPlaceholder: '예: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 또는 2001:db8::/64', sourceAddressPlaceholder: '예: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 또는 2001:db8::/64',
systemAccessChangeConfirm:
'이 작업을 {0} 허용 목록 규칙에 적용하면 해당 서비스를 통한 원격 접속이 불가능해질 수 있습니다. 계속하시겠습니까?',
whitelistServicePortsHelper:
'방화벽에서 허용할 포트 하나를 설정합니다. 수신 포트는 해당 서비스 설정에서 변경하세요.',
whitelistSourcesHelper:
'IP 주소 또는 CIDR 대역을 쉼표나 줄바꿈으로 구분하세요. 비워 두면 모든 IPv4 및 IPv6 출발지를 허용합니다.',
systemAccessSourceError: '유효한 {0} 주소 또는 CIDR 대역을 하나 이상 입력하세요.',
destinationPortPlaceholder: '예: 80, 80,443 또는 8080-8089', destinationPortPlaceholder: '예: 80, 80,443 또는 8080-8089',
deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?', deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4169,9 +4169,7 @@ const message = {
unbindHelper: unbindHelper:
'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?', 'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?',
portWhiteList: 'ພອດທີ່ອະນຸຍາດ', portWhiteList: 'ພອດທີ່ອະນຸຍາດ',
portWhiteListAlter: portWhiteListHelper: 'ປ້ອນພອດດຽວ (ເຊັ່ນ 80) ຫຼື ຊ່ວງພອດ (ເຊັ່ນ 8000-8100). ໝາຍເລກພອດຕ້ອງຢູ່ລະຫວ່າງ 1 ຫາ 65535.',
'ການປ່ຽນແປງມີຜົນທັນທີເມື່ອບັນທຶກ. ພອດທີ່ເພີ່ມຈະຖືກອະນຸຍາດອັດຕະໂນມັດ; ການລຶບພອດອອກຈະຍົກເລີກພຽງການປ້ອງກັນ. ເພື່ອປິດພອດ ໃຫ້ລຶບກົດອະນຸຍາດໃນລາຍການກົດ.',
portWhiteListHelper: 'ຮອງຮັບ IPv4/IPv6, TCP/UDP, ພອດດຽວ ແລະ ຊ່ວງພອດເຊັ່ນ 8000-8100.',
chain: 'Chain', chain: 'Chain',
sourceIP: 'IP ຕົ້ນທາງ', sourceIP: 'IP ຕົ້ນທາງ',
destIP: 'IP ປາຍທາງ', destIP: 'IP ປາຍທາງ',
@@ -4182,6 +4180,12 @@ const message = {
allPorts: 'ທຸກໆພອດ', allPorts: 'ທຸກໆພອດ',
allProtocolHelper: 'ໂປຣໂຕຄໍ ແລະ ພອດທັງໝົດ', allProtocolHelper: 'ໂປຣໂຕຄໍ ແລະ ພອດທັງໝົດ',
sourceAddressPlaceholder: 'ຕົວຢ່າງ: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ຫຼື 2001:db8::/64', sourceAddressPlaceholder: 'ຕົວຢ່າງ: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ຫຼື 2001:db8::/64',
systemAccessChangeConfirm:
'ການດຳເນີນການນີ້ກັບກົດລາຍຊື່ອະນຸຍາດ {0} ອາດເຮັດໃຫ້ບໍ່ສາມາດເຂົ້າເຖິງທາງໄກຜ່ານບໍລິການເຫຼົ່ານີ້. ສືບຕໍ່ບໍ?',
whitelistServicePortsHelper: 'ຕັ້ງຄ່າໜຶ່ງພອດທີ່ໄຟວໍອະນຸຍາດ. ປ່ຽນພອດຮັບຟັງໃນການຕັ້ງຄ່າບໍລິການ.',
whitelistSourcesHelper:
'ປ້ອນທີ່ຢູ່ IP ຫຼື CIDR ໂດຍແຍກດ້ວຍຈຸດຈ້ຳ ຫຼື ຂຶ້ນແຖວໃໝ່. ຖ້າປະໄວ້ຫວ່າງ ຈະອະນຸຍາດທຸກແຫຼ່ງທີ່ມາ IPv4 ແລະ IPv6.',
systemAccessSourceError: 'ປ້ອນທີ່ຢູ່ {0} ຫຼື CIDR ທີ່ຖືກຕ້ອງຢ່າງໜ້ອຍໜຶ່ງລາຍການ.',
destinationPortPlaceholder: 'ຕົວຢ່າງ: 80, 80,443 ຫຼື 8080-8089', destinationPortPlaceholder: 'ຕົວຢ່າງ: 80, 80,443 ຫຼື 8080-8089',
deleteRuleConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. ຕ້ອງການຕໍ່ຫຼືບໍ່?', deleteRuleConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4367,9 +4367,8 @@ const message = {
unbindHelper: unbindHelper:
'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?', 'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?',
portWhiteList: 'Senarai putih port', portWhiteList: 'Senarai putih port',
portWhiteListAlter: portWhiteListHelper:
'Perubahan berkuat kuasa serta-merta apabila disimpan. Port baharu dibenarkan secara automatik; mengalih keluar port hanya membuang perlindungannya. Untuk menutupnya, padam peraturan kebenaran dalam senarai peraturan.', 'Masukkan satu port (cth. 80) atau julat port (cth. 8000-8100). Nombor port mestilah antara 1 hingga 65535.',
portWhiteListHelper: 'Menyokong IPv4/IPv6, TCP/UDP, port tunggal dan julat seperti 8000-8100.',
chain: 'Rantai', chain: 'Rantai',
sourceIP: 'IP Sumber', sourceIP: 'IP Sumber',
destIP: 'IP Destinasi', destIP: 'IP Destinasi',
@@ -4380,6 +4379,13 @@ const message = {
allPorts: 'Semua Port', allPorts: 'Semua Port',
allProtocolHelper: 'Semua protokol dan port', allProtocolHelper: 'Semua protokol dan port',
sourceAddressPlaceholder: 'contoh: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 atau 2001:db8::/64', 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.',
whitelistSourcesHelper:
'Masukkan alamat IP atau julat CIDR, dipisahkan dengan koma atau baris baharu. Biarkan kosong untuk membenarkan semua sumber IPv4 dan IPv6.',
systemAccessSourceError: 'Masukkan sekurang-kurangnya satu alamat {0} atau julat CIDR yang sah.',
destinationPortPlaceholder: 'contoh: 80, 80,443 atau 8080-8089', destinationPortPlaceholder: 'contoh: 80, 80,443 atau 8080-8089',
deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?', deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4389,9 +4389,8 @@ const message = {
unbindHelper: unbindHelper:
'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?', 'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?',
portWhiteList: 'Lista branca de portas', portWhiteList: 'Lista branca de portas',
portWhiteListAlter: portWhiteListHelper:
'As alterações entram em vigor ao salvar. As portas adicionadas são liberadas automaticamente; remover uma porta apenas retira sua proteção. Para fechá-la, exclua a regra de permissão na lista de regras.', 'Insira uma porta (ex.: 80) ou um intervalo (ex.: 8000-8100). Os números das portas devem estar entre 1 e 65535.',
portWhiteListHelper: 'Suporta IPv4/IPv6, TCP/UDP, portas únicas e intervalos como 8000-8100.',
chain: 'Cadeia', chain: 'Cadeia',
sourceIP: 'IP de Origem', sourceIP: 'IP de Origem',
destIP: 'IP de Destino', destIP: 'IP de Destino',
@@ -4402,6 +4401,13 @@ const message = {
allPorts: 'Todas as Portas', allPorts: 'Todas as Portas',
allProtocolHelper: 'Todos os protocolos e 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', 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.',
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.',
systemAccessSourceError: 'Informe pelo menos um endereço {0} ou intervalo CIDR válido.',
destinationPortPlaceholder: 'por exemplo: 80, 80,443 ou 8080-8089', destinationPortPlaceholder: 'por exemplo: 80, 80,443 ou 8080-8089',
deleteRuleConfirm: 'Excluirá {0} regras. Continuar?', deleteRuleConfirm: 'Excluirá {0} regras. Continuar?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4355,9 +4355,8 @@ const message = {
unbindHelper: unbindHelper:
'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?', 'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?',
portWhiteList: 'Белый список портов', portWhiteList: 'Белый список портов',
portWhiteListAlter: portWhiteListHelper:
'Изменения применяются сразу при сохранении. Добавленные порты разрешаются автоматически. Удаление порта из списка только снимает защиту. Чтобы закрыть порт, удалите разрешающее правило из списка правил.', 'Введите один порт (например, 80) или диапазон портов (например, 8000-8100). Номера портов должны быть от 1 до 65535.',
portWhiteListHelper: 'Поддерживает IPv4/IPv6, TCP/UDP, отдельные порты и диапазоны, например 8000-8100.',
chain: 'Цепочка', chain: 'Цепочка',
sourceIP: 'Исходный IP', sourceIP: 'Исходный IP',
destIP: 'Целевой IP', destIP: 'Целевой IP',
@@ -4368,6 +4367,13 @@ const message = {
allPorts: 'Все Порта', allPorts: 'Все Порта',
allProtocolHelper: 'Все протоколы и порты', allProtocolHelper: 'Все протоколы и порты',
sourceAddressPlaceholder: 'например: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 или 2001:db8::/64', sourceAddressPlaceholder: 'например: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 или 2001:db8::/64',
systemAccessChangeConfirm:
'Эта операция с правилами белого списка {0} может привести к потере удалённого доступа через эти службы. Продолжить?',
whitelistServicePortsHelper:
'Укажите один порт, разрешённый брандмауэром. Порт прослушивания меняется в настройках службы.',
whitelistSourcesHelper:
'Введите IP-адреса или подсети CIDR через запятую или с новой строки. Оставьте поле пустым, чтобы разрешить все источники IPv4 и IPv6.',
systemAccessSourceError: 'Укажите хотя бы один допустимый адрес {0} или диапазон CIDR.',
destinationPortPlaceholder: 'например: 80, 80,443 или 8080-8089', destinationPortPlaceholder: 'например: 80, 80,443 или 8080-8089',
deleteRuleConfirm: 'Удалит {0} правил. Продолжить?', deleteRuleConfirm: 'Удалит {0} правил. Продолжить?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4372,9 +4372,8 @@ const message = {
unbindHelper: 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?', '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', portWhiteList: 'Port beyaz listesi',
portWhiteListAlter: portWhiteListHelper:
'Değişiklikler kaydedildiğinde hemen uygulanır. Eklenen portlara otomatik olarak izin verilir. Portu listeden çıkarmak yalnızca korumasını kaldırır. Portu kapatmak için kural listesinden izin kuralını silin.', 'Tek bir port (ör. 80) veya port aralığı (ör. 8000-8100) girin. Port numaraları 1 ile 65535 arasında olmalıdır.',
portWhiteListHelper: 'IPv4/IPv6, TCP/UDP, tek portlar ve 8000-8100 gibi port aralıklarını destekler.',
chain: 'Zincir', chain: 'Zincir',
sourceIP: 'Kaynak IP', sourceIP: 'Kaynak IP',
destIP: 'Hedef IP', destIP: 'Hedef IP',
@@ -4385,6 +4384,13 @@ const message = {
allPorts: 'Tüm Portlar', allPorts: 'Tüm Portlar',
allProtocolHelper: 'Tüm protokoller ve 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', 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.',
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.',
systemAccessSourceError: 'En az bir geçerli {0} adresi veya CIDR aralığı girin.',
destinationPortPlaceholder: 'örn. 80, 80,443 veya 8080-8089', destinationPortPlaceholder: 'örn. 80, 80,443 veya 8080-8089',
deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?', deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?',
deleteUsedRuleConfirm: deleteUsedRuleConfirm:

View File

@@ -4008,9 +4008,7 @@ const message = {
bindHelper: '綁定 僅當狀態為綁定時,防火牆規則才能生效,是否確認?', bindHelper: '綁定 僅當狀態為綁定時,防火牆規則才能生效,是否確認?',
unbindHelper: '解除綁定 解除綁定時,已新增的所有防火牆規則將失效,請謹慎操作,是否確認?', unbindHelper: '解除綁定 解除綁定時,已新增的所有防火牆規則將失效,請謹慎操作,是否確認?',
portWhiteList: '埠白名單', portWhiteList: '埠白名單',
portWhiteListAlter: portWhiteListHelper: '支援單一連接埠(如 80或連接埠範圍如 8000-8100連接埠取值為 1-65535。',
'白名單修改在儲存後立即生效。新增連接埠自動放行;移出連接埠僅解除白名單保護,如需關閉,請前往規則清單手動刪除放行規則。',
portWhiteListHelper: '支援 IPv4/IPv6、TCP/UDP、單一連接埠以及 8000-8100 格式的連接埠範圍。',
chain: '鏈', chain: '鏈',
sourceIP: '來源 IP', sourceIP: '來源 IP',
destIP: '目標 IP', destIP: '目標 IP',
@@ -4021,6 +4019,10 @@ const message = {
allPorts: '所有埠', allPorts: '所有埠',
allProtocolHelper: '所有協定和連接埠', allProtocolHelper: '所有協定和連接埠',
sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64',
systemAccessChangeConfirm: '對 {0} 白名單規則執行此操作後,可能無法透過該服務遠端存取。是否繼續?',
whitelistServicePortsHelper: '設定一個防火牆放行連接埠,服務監聽連接埠請在對應服務設定中修改。',
whitelistSourcesHelper: '支援 IP 或 CIDR多個以逗號或換行分隔。留空預設允許所有 IPv4 和 IPv6 來源。',
systemAccessSourceError: '請至少填寫一個有效的 {0} 位址或 CIDR 網段。',
destinationPortPlaceholder: '例如80、80,443 或 8080-8089', destinationPortPlaceholder: '例如80、80,443 或 8080-8089',
deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?', deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?',
deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?', deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?',

View File

@@ -4056,9 +4056,7 @@ const message = {
bindHelper: '绑定 仅当状态为绑定时,防火墙规则才能生效,是否确认?', bindHelper: '绑定 仅当状态为绑定时,防火墙规则才能生效,是否确认?',
unbindHelper: '解绑 解除绑定时,已添加的所有防火墙规则将失效,请谨慎操作,是否确认?', unbindHelper: '解绑 解除绑定时,已添加的所有防火墙规则将失效,请谨慎操作,是否确认?',
portWhiteList: '端口白名单', portWhiteList: '端口白名单',
portWhiteListAlter: portWhiteListHelper: '支持单个端口(如 80或端口范围如 8000-8100端口取值为 1-65535。',
'白名单修改在保存后立即生效。新增端口自动放行;移出端口仅解除白名单保护,如需关闭,请前往规则列表手动删除放行规则。',
portWhiteListHelper: '支持 IPv4/IPv6、TCP/UDP、单端口以及 8000-8100 格式的端口范围。',
chain: '链', chain: '链',
sourceIP: '源 IP', sourceIP: '源 IP',
destIP: '目标 IP', destIP: '目标 IP',
@@ -4069,6 +4067,10 @@ const message = {
allPorts: '所有端口', allPorts: '所有端口',
allProtocolHelper: '所有协议和端口', allProtocolHelper: '所有协议和端口',
sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', sourceAddressPlaceholder: '例如172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64',
systemAccessChangeConfirm: '对 {0} 白名单规则执行此操作后,可能无法通过该服务远程访问。是否继续?',
whitelistServicePortsHelper: '设置一个防火墙放行端口,服务监听端口请在对应服务设置中修改。',
whitelistSourcesHelper: '支持 IP 或 CIDR多个以逗号或换行分隔。留空默认允许所有 IPv4 和 IPv6 来源。',
systemAccessSourceError: '请至少填写一个有效的 {0} 地址或 CIDR 网段。',
destinationPortPlaceholder: '例如80、80,443 或 8080-8089', destinationPortPlaceholder: '例如80、80,443 或 8080-8089',
deleteRuleConfirm: '将删除 {0} 条规则,是否继续?', deleteRuleConfirm: '将删除 {0} 条规则,是否继续?',
deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?', deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?',

View File

@@ -86,12 +86,7 @@
</el-form-item> </el-form-item>
<el-form-item :label="$t('firewall.portWhiteList')"> <el-form-item :label="$t('firewall.portWhiteList')">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<el-button <el-button v-permission v-node-admin icon="Setting" @click="openWhitelist">
v-permission
v-node-admin
icon="Setting"
@click="whiteListRef.acceptParams()"
>
{{ $t('commons.button.set') }} {{ $t('commons.button.set') }}
</el-button> </el-button>
<span class="input-help !mt-0"> <span class="input-help !mt-0">
@@ -104,7 +99,12 @@
</el-form> </el-form>
</template> </template>
</LayoutContent> </LayoutContent>
<WhiteList ref="whiteListRef" @created="openWhitelistTask" /> <WhiteList
ref="whiteListRef"
:rules="settings?.portWhiteList"
:loading="loading"
@created="openWhitelistTask"
/>
<TaskLog ref="whitelistTaskRef" @close="load" /> <TaskLog ref="whitelistTaskRef" @close="load" />
</div> </div>
</template> </template>
@@ -116,7 +116,6 @@ import { loadFirewallSettings, operateFire, operateFirewallBackend } from '@/api
import FireRouter from '@/views/host/firewall/index.vue'; import FireRouter from '@/views/host/firewall/index.vue';
import WhiteList from '@/views/host/firewall/setting/white-list/index.vue'; import WhiteList from '@/views/host/firewall/setting/white-list/index.vue';
import TaskLog from '@/components/log/task/index.vue'; import TaskLog from '@/components/log/task/index.vue';
import { whiteListRuleCount } from '@/views/host/firewall/setting/white-list/model';
import { useGlobalStore } from '@/composables/useGlobalStore'; import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
@@ -133,9 +132,13 @@ const savedBackends = ref<Record<Firewall.BackendSubsystem, string>>({
}); });
const pingStatus = ref('Disable'); const pingStatus = ref('Disable');
const oldPingStatus = ref('Disable'); const oldPingStatus = ref('Disable');
const whiteListRef = ref(); const whiteListRef = ref<InstanceType<typeof WhiteList>>();
const whitelistTaskRef = ref<InstanceType<typeof TaskLog>>(); const whitelistTaskRef = ref<InstanceType<typeof TaskLog>>();
const openWhitelistTask = (taskID: string) => whitelistTaskRef.value?.openWithTaskID(taskID, true); const openWhitelistTask = (taskID: string) => whitelistTaskRef.value?.openWithTaskID(taskID, true);
const openWhitelist = () => {
whiteListRef.value?.acceptParams();
load();
};
const providerOrder: Record<Firewall.Provider, number> = { const providerOrder: Record<Firewall.Provider, number> = {
iptables: 0, iptables: 0,
@@ -206,7 +209,7 @@ const groups = computed(() => {
]; ];
}); });
const whiteListCount = computed(() => whiteListRuleCount(settings.value?.portWhiteList || '')); const whiteListCount = computed(() => settings.value?.portWhiteList?.length || 0);
const sortedOptions = (group: Firewall.BackendGroup) => { const sortedOptions = (group: Firewall.BackendGroup) => {
return [...group.options].sort((left, right) => providerOrder[left.name] - providerOrder[right.name]); return [...group.options].sort((left, right) => providerOrder[left.name] - providerOrder[right.name]);
@@ -224,6 +227,9 @@ const load = async () => {
}; };
pingStatus.value = res.data.pingStatus; pingStatus.value = res.data.pingStatus;
oldPingStatus.value = pingStatus.value; oldPingStatus.value = pingStatus.value;
} catch (error) {
settings.value = undefined;
throw error;
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@@ -1,207 +1,240 @@
<template> <template>
<DrawerPro v-model="drawerVisible" :header="$t('firewall.portWhiteList')" @close="handleClose" size="large"> <DrawerPro v-model="drawerVisible" :header="$t('firewall.portWhiteList')" size="large">
<template #content> <template #content>
<el-button type="primary" @click="openCreate"> <div class="mb-3">
{{ $t('commons.button.add') }} <el-button type="primary" :disabled="disabled" @click="openEditor()">
</el-button> {{ $t('commons.button.create') }}
<ComplexTable :data="data" v-loading="loading"> </el-button>
<el-table-column :label="$t('firewall.addressFamily')" width="120"> </div>
<ComplexTable :data="data" v-loading="busy">
<el-table-column :label="$t('commons.table.protocol')" width="100">
<template #default="{ row }">{{ (row.protocol || 'tcp').toUpperCase() }}</template>
</el-table-column>
<el-table-column :label="$t('firewall.portOrRange')" width="150">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="!row.edit">{{ row.family.toUpperCase() }}</span> <div v-if="row.type" class="flex items-center gap-1 whitespace-nowrap">
<el-select v-else v-model="row.family"> <span class="truncate" :title="row.port">
<el-option value="ipv4" label="IPv4" /> {{ row.port || $t('commons.status.unknown') }}
<el-option value="ipv6" label="IPv6" /> </span>
</el-select> <span class="shrink-0 text-xs text-[var(--el-text-color-secondary)]">
{{ serviceLabel(row.type) }}
</span>
</div>
<span v-else>{{ row.port }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.protocol')" width="120"> <el-table-column :label="$t('firewall.allowedSources')" min-width="260">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="!row.edit">{{ row.protocol.toUpperCase() }}</span> <span class="whitelist-sources">{{ formatHostAddressList(row.sources || []) }}</span>
<el-select v-else v-model="row.protocol">
<el-option value="tcp" label="TCP" />
<el-option value="udp" label="UDP" />
</el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('firewall.portOrRange')" prop="port"> <el-table-column :label="$t('commons.table.operate')" width="140" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="!row.edit">{{ row.port }}</span> <el-button link type="primary" :disabled="disabled" @click="openEditor(row)">
<el-input v-else v-model.trim="row.port" placeholder="80 / 8000-8100" clearable />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" width="160">
<template #default="{ row, $index }">
<el-button v-if="row.edit" link type="primary" @click="saveRow(row)">
{{ $t('commons.button.save') }}
</el-button>
<el-button v-if="!row.edit" link type="primary" @click="editRow(row)">
{{ $t('commons.button.edit') }} {{ $t('commons.button.edit') }}
</el-button> </el-button>
<el-button v-if="!row.edit" link type="primary" @click="removeRow($index)"> <el-button link type="primary" :disabled="disabled" @click="removeRule(row)">
{{ $t('commons.button.delete') }} {{ $t('commons.button.delete') }}
</el-button> </el-button>
<el-button v-if="row.edit" link type="primary" @click="cancelEdit(row, $index)">
{{ $t('commons.button.cancel') }}
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</ComplexTable> </ComplexTable>
<span class="input-help">{{ $t('firewall.portWhiteListHelper') }}</span>
</template> </template>
</DrawerPro>
<DialogPro
v-model="dialogVisible"
:title="$t(editingRule ? 'commons.button.edit' : 'commons.button.create')"
:show-close="!saving"
>
<el-form label-position="top" :disabled="saving" @submit.prevent="saveRule">
<el-form-item :label="$t('commons.table.type')">
<el-select v-model="form.type" :disabled="!!editingRule">
<el-option value="custom" :label="$t('website.other')" />
<el-option
v-for="type in serviceTypes"
:key="type"
:value="type"
:label="`${serviceLabel(type)} (${servicePortLabel(type)})`"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.protocol')" required>
<el-select v-model="form.protocol">
<el-option value="tcp" label="TCP" />
<el-option value="udp" label="UDP" />
</el-select>
</el-form-item>
<template v-if="form.type === 'custom'">
<el-form-item :label="$t('firewall.portOrRange')" required>
<el-input v-model.trim="form.port" placeholder="80 / 8000-8100" clearable />
<span class="input-help">{{ $t('firewall.portWhiteListHelper') }}</span>
</el-form-item>
</template>
<el-form-item v-else-if="editingRule" :label="$t('commons.table.port')" required>
<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>
<el-form-item :label="$t('firewall.allowedSources')">
<el-input
v-model.trim="form.sourceInput"
type="textarea"
:autosize="{ minRows: 3, maxRows: 8 }"
:placeholder="$t('firewall.sourceAddressPlaceholder')"
/>
<span class="input-help">{{ $t('firewall.whitelistSourcesHelper') }}</span>
</el-form-item>
</el-form>
<template #footer> <template #footer>
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button> <el-button :disabled="saving" @click="dialogVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :loading="loading" type="primary" @click="onSubmit"> <el-button :loading="saving" :disabled="disabled" type="primary" @click="saveRule">
{{ $t('commons.button.confirm') }} {{ $t('commons.button.confirm') }}
</el-button> </el-button>
</template> </template>
</DrawerPro> </DialogPro>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { getAgentSettingInfo } from '@/api/modules/setting'; import {
import { updateFirewallPortWhitelist } from '@/api/modules/firewall'; createFirewallPortWhitelist,
deleteFirewallPortWhitelist,
updateFirewallPortWhitelist,
} from '@/api/modules/firewall';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgError } from '@/utils/message'; import { MsgError } from '@/utils/message';
import { import { formatHostAddressList, isValidIPOrCIDR, splitTagValues } from '@/views/host/firewall/utils/validation';
normalizeWhiteListRule, import { ElMessageBox } from 'element-plus';
parseWhiteList, import { normalizeWhiteListRule, WhiteListProtocol, WhiteListRule, WhiteListType, whiteListRuleKey } from './model';
serializeWhiteList,
WhiteListFamily,
WhiteListProtocol,
WhiteListRule,
whiteListRuleKey,
} from './model';
interface WhiteListItem extends WhiteListRule { const props = defineProps<{ rules?: WhiteListRule[]; loading: boolean }>();
oldRule: WhiteListRule;
edit: boolean;
isNew: boolean;
}
const emit = defineEmits<{ (e: 'created', taskID: string): void }>(); const emit = defineEmits<{ (e: 'created', taskID: string): void }>();
const drawerVisible = ref(false); const drawerVisible = ref(false);
const loading = ref(false); const dialogVisible = ref(false);
const data = ref<WhiteListItem[]>([]); const saving = ref(false);
const defaultWhiteList = '80/tcp,443/tcp,443/udp'; const busy = computed(() => props.loading || saving.value);
const disabled = computed(() => busy.value || !props.rules);
const data = computed(() => props.rules || []);
const editingRule = ref<WhiteListRule>();
const form = ref({
type: 'custom' as WhiteListType | 'custom',
protocol: 'tcp' as WhiteListProtocol,
port: '',
sourceInput: '',
});
const serviceTypes: WhiteListType[] = ['ssh', 'panel'];
const servicePorts = computed<Record<WhiteListType, string>>(() => ({
panel: data.value.find((rule) => rule.type === 'panel')?.port || '',
ssh: data.value.find((rule) => rule.type === 'ssh')?.port || '',
}));
const serviceLabel = (type: WhiteListType) => (type === 'panel' ? '1Panel' : 'SSH');
const servicePortLabel = (type: WhiteListType) => servicePorts.value[type] || i18n.global.t('commons.status.unknown');
const acceptParams = async (): Promise<void> => { const acceptParams = () => {
drawerVisible.value = true; drawerVisible.value = true;
loading.value = true; dialogVisible.value = false;
await getAgentSettingInfo()
.then((res) => {
data.value = parseWhiteList(res.data.firewallPortWhiteList ?? defaultWhiteList).map((rule) => ({
...rule,
oldRule: { ...rule },
edit: false,
isNew: false,
}));
})
.finally(() => {
loading.value = false;
});
}; };
const openCreate = () => { const openEditor = (rule?: WhiteListRule) => {
for (const item of data.value) { if (disabled.value) return;
if (item.isNew && item.port === '') { editingRule.value = rule;
return; form.value = {
} type: rule?.type || 'custom',
protocol: rule?.protocol || 'tcp',
port: rule?.port || '',
sourceInput: formatHostAddressList(rule ? rule.sources || [] : ['0.0.0.0/0', '::/0']),
};
dialogVisible.value = true;
};
const saveRule = async () => {
if (disabled.value) return;
const sources = splitTagValues([form.value.sourceInput]);
if (!sources.length) {
sources.push('0.0.0.0/0', '::/0');
} }
data.value.unshift({ if (sources.some((source) => !isValidIPOrCIDR(source))) {
family: 'ipv4', MsgError(i18n.global.t('firewall.systemAccessSourceError', ['IPv4 / IPv6']));
protocol: 'tcp',
port: '',
oldRule: { family: 'ipv4', protocol: 'tcp', port: '' },
edit: true,
isNew: true,
});
};
const editRow = (row: WhiteListItem) => {
row.oldRule = { family: row.family, protocol: row.protocol, port: row.port };
row.edit = true;
};
const saveRow = (row: WhiteListItem) => {
const rule = validateRule(row);
if (!rule) return;
if (hasDuplicate(rule, row)) {
MsgError(i18n.global.t('commons.rule.duplicate'));
return; return;
} }
Object.assign(row, rule); let rule: WhiteListRule;
row.oldRule = { ...rule };
row.edit = false;
row.isNew = false;
};
const cancelEdit = (row: WhiteListItem, index: number) => {
if (row.isNew) {
data.value.splice(index, 1);
return;
}
Object.assign(row, row.oldRule);
row.edit = false;
};
const removeRow = (index: number) => {
data.value.splice(index, 1);
};
const validateRule = (row: Pick<WhiteListItem, 'family' | 'protocol' | 'port'>): WhiteListRule | undefined => {
try { try {
return normalizeWhiteListRule({ rule = normalizeWhiteListRule({
family: row.family as WhiteListFamily, ...form.value,
protocol: row.protocol as WhiteListProtocol, type: form.value.type === 'custom' ? undefined : form.value.type,
port: row.port, port: form.value.type === 'custom' || editingRule.value ? form.value.port : undefined,
sources,
}); });
} catch { } catch {
MsgError(i18n.global.t('firewall.portFormatError')); MsgError(i18n.global.t('firewall.portFormatError'));
return undefined; return;
} }
}; const key = whiteListRuleKey(rule);
if (data.value.some((item) => item !== editingRule.value && whiteListRuleKey(item) === key)) {
const hasDuplicate = (rule: WhiteListRule, row?: WhiteListItem): boolean => { MsgError(i18n.global.t('commons.rule.duplicate'));
return data.value.some( return;
(item) => item !== row && item.port !== '' && whiteListRuleKey(rule) === whiteListRuleKey(item), }
const oldRule = editingRule.value;
await submit(
() => (oldRule ? updateFirewallPortWhitelist({ oldRule, rule }) : createFirewallPortWhitelist(rule)),
oldRule ? { operation: 'edit', rule: oldRule } : undefined,
); );
}; };
const validateRules = (): WhiteListRule[] | undefined => { const removeRule = async (rule: WhiteListRule) => {
const rules: WhiteListRule[] = []; await submit(() => deleteFirewallPortWhitelist(rule), { operation: 'delete', rule });
for (const item of data.value) {
if (!item.port) continue;
const rule = validateRule(item);
if (!rule) return undefined;
if (rules.some((existing) => whiteListRuleKey(existing) === whiteListRuleKey(rule))) {
MsgError(i18n.global.t('commons.rule.duplicate'));
return undefined;
}
rules.push(rule);
}
return rules;
}; };
const onSubmit = async () => { const submit = async (
const rules = validateRules(); request: () => ReturnType<typeof createFirewallPortWhitelist>,
if (!rules || loading.value) return; change?: { operation: 'edit' | 'delete'; rule: WhiteListRule },
loading.value = true; ) => {
if (disabled.value) return;
saving.value = true;
try { try {
const { data: result } = await updateFirewallPortWhitelist(serializeWhiteList(rules)); if (change && (change.operation === 'delete' || change.rule.type)) {
if (!result.taskID || !result.queued) return; const messages: string[] = [];
drawerVisible.value = false; 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;
}
dialogVisible.value = false;
emit('created', result.taskID); emit('created', result.taskID);
} catch {
} finally { } finally {
loading.value = false; saving.value = false;
} }
}; };
const handleClose = () => { defineExpose({ acceptParams });
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script> </script>
<style scoped>
.whitelist-sources {
overflow-wrap: anywhere;
}
</style>

View File

@@ -1,72 +1,33 @@
import { normalizePortRange } from '@/views/host/firewall/utils/validation'; import { normalizePortRange } from '@/views/host/firewall/utils/validation';
export type WhiteListFamily = 'ipv4' | 'ipv6'; import type { Firewall } from '@/api/interface/firewall';
export type WhiteListProtocol = 'tcp' | 'udp';
export interface WhiteListRule { export type WhiteListProtocol = 'tcp' | 'udp';
family: WhiteListFamily; export type WhiteListType = 'panel' | 'ssh';
protocol: WhiteListProtocol; export type WhiteListRule = Firewall.PortWhitelist;
port: string;
}
export const normalizeWhiteListRule = (rule: WhiteListRule): WhiteListRule => { export const normalizeWhiteListRule = (rule: WhiteListRule): WhiteListRule => {
const family = rule.family?.toLowerCase() as WhiteListFamily; const { sources } = rule;
const protocol = rule.protocol?.toLowerCase() as WhiteListProtocol; const protocol = (rule.protocol?.trim().toLowerCase() || (rule.type ? 'tcp' : '')) as WhiteListProtocol;
if (!['ipv4', 'ipv6'].includes(family) || !['tcp', 'udp'].includes(protocol)) { if (!['tcp', 'udp'].includes(protocol)) throw new Error('invalid whitelist rule');
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())) {
throw new Error('invalid service port');
}
return {
type: rule.type,
protocol,
port: rule.port === undefined ? undefined : normalizePortRange(rule.port),
sources,
};
} }
return { family, protocol, port: normalizePortRange(rule.port) }; return {
protocol,
port: normalizePortRange(rule.port || ''),
sources,
};
}; };
export const whiteListRuleKey = (rule: WhiteListRule): string => { export const whiteListRuleKey = (rule: WhiteListRule): string =>
const normalized = normalizeWhiteListRule(rule); rule.type ? `${rule.type}/${rule.protocol || 'tcp'}` : `custom/${rule.protocol}/${rule.port}`;
return `${normalized.family}/${normalized.protocol}/${normalized.port}`;
};
export const parseWhiteList = (value: string): WhiteListRule[] => {
const input = value?.trim();
if (!input) return [];
let rules: WhiteListRule[];
if (input.startsWith('[')) {
rules = JSON.parse(input) as WhiteListRule[];
} else {
rules = input
.split(/[\s,;]+/)
.filter(Boolean)
.map((item) => {
const parts = item.split('/');
if (parts.length === 3) {
return { family: parts[0], port: parts[1], protocol: parts[2] } as WhiteListRule;
}
return {
family: 'ipv4',
port: parts[0],
protocol: parts[1] || 'tcp',
} as WhiteListRule;
});
}
const result: WhiteListRule[] = [];
const seen = new Set<string>();
for (const item of rules) {
const rule = normalizeWhiteListRule(item);
const key = whiteListRuleKey(rule);
if (seen.has(key)) continue;
seen.add(key);
result.push(rule);
}
return result;
};
export const serializeWhiteList = (rules: WhiteListRule[]): string => {
return JSON.stringify(rules.map((rule) => normalizeWhiteListRule(rule)));
};
export const whiteListRuleCount = (value: string): number => {
try {
return parseWhiteList(value).length;
} catch {
return 0;
}
};