refactor(firewall): queue rule operations and simplify synchronization (#13784)

This commit is contained in:
ssongliu
2026-09-10 23:54:46 +08:00
committed by GitHub
parent 63b2d4e4d5
commit 2fcfe56a30
71 changed files with 2667 additions and 4516 deletions

View File

@@ -121,7 +121,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
// @Summary Operate forwarding rules
// @Accept json
// @Param request body dto.ForwardRuleOperate true "request"
// @Success 200
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/forward/operate [post]
@@ -132,11 +132,12 @@ func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
return
}
if err := forwardingService.OperateRules(request); err != nil {
result, err := forwardingService.OperateRules(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
// @Tags Firewall
@@ -259,29 +260,30 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
}
// @Tags Firewall
// @Summary Check unified firewall v2 rules for duplicates and conflicts
// @Summary Adopt an external firewall rule
// @Accept json
// @Param request body dto.FirewallRuleCheck true "request"
// @Success 200 {object} dto.FirewallRuleCheckResponse
// @Param request body dto.FirewallRuleAdopt true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/check [post]
func (b *BaseApi) CheckFirewallRules(c *gin.Context) {
var request dto.FirewallRuleCheck
// @Router /hosts/firewall/rules/adopt [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
var request dto.FirewallRuleAdopt
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Check(c.Request.Context(), c.ClientIP(), request)
if err != nil {
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Create unified firewall v2 rules
// @Summary Queue firewall rule creation
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
// @Accept json
// @Param request body dto.FirewallRuleCreate true "request"
// @Success 200 {object} dto.FirewallRuleCreateResponse
@@ -458,8 +460,6 @@ func handleFirewallRuleError(c *gin.Context, err error) {
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrRuleCheckRequired):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_CHECK_REQUIRED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
@@ -598,7 +598,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
// @Summary Delete Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
// @Success 200
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/delete/batch [post]
@@ -608,32 +608,34 @@ func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := dockerPortGuardService.DeletePolicies(c.Request.Context(), request); err != nil {
result, err := dockerPortGuardService.DeletePolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Batch upsert Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
// @Success 200
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/batch [post]
// @x-panel-log {"bodyKeys":["mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略 [mode]","formatEN":"batch update Docker port guard policies [mode]"}
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatch
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := dockerPortGuardService.UpsertPolicies(c.Request.Context(), request); err != nil {
result, err := dockerPortGuardService.UpsertPolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
func handleDockerPortGuardError(c *gin.Context, err error) {

View File

@@ -109,18 +109,6 @@ type FirewallRuleReset struct {
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallRuleCheckResult struct {
Decision filter.CheckDecision `json:"decision"`
Classification filter.CheckClassification `json:"classification"`
Reason string `json:"reason"`
RequestedRule filter.FirewallRule `json:"requestedRule"`
RequestedRuleKey string `json:"requestedRuleKey"`
ExistingRuleUUID string `json:"existingRuleUUID,omitempty"`
Candidates []filter.ObservedRule `json:"candidates,omitempty"`
AllowedActions []filter.CheckAction `json:"allowedActions,omitempty"`
CheckFlag string `json:"checkFlag"`
}
type FirewallRuleInventory struct {
PageInfo
Scope filter.Scope `json:"scope,omitempty"`
@@ -213,14 +201,18 @@ type DockerPortGuardEndpointIdentity struct {
}
type DockerPortGuardPolicyBatch struct {
Endpoints []DockerPortGuardEndpointIdentity `json:"endpoints" validate:"required,min=1,max=256,dive"`
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"max=256,dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
}
type DockerPortGuardPolicyBatchDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
}
type DockerPortGuardPolicy struct {
DockerPortGuardEndpointIdentity
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
}
type DockerPortGuardOperation struct {
@@ -228,34 +220,24 @@ type DockerPortGuardOperation struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleCheckItem struct {
AdoptLocator *filter.Locator `json:"adoptLocator,omitempty" validate:"excluded_with=UUID"`
UUID string `json:"uuid" validate:"omitempty,max=64"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
}
type FirewallRuleCheck struct {
Items []FirewallRuleCheckItem `json:"items" validate:"required,min=1,max=256,dive"`
}
type FirewallRuleCheckResponse struct {
Items []FirewallRuleCheckResult `json:"items"`
type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
}
type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"`
CheckFlag string `json:"checkFlag"`
Action filter.CheckAction `json:"action"`
AdoptInstanceKey string `json:"adoptInstanceKey"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
}
type FirewallRuleCreate struct {
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,max=256,dive"`
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
}
type FirewallRuleCreateResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
@@ -327,7 +309,7 @@ type FirewallRuleSyncFailure struct {
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
}
type FirewallRuleDeleteResponse struct {
@@ -355,3 +337,22 @@ type FirewallRuleReorder struct {
TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"`
}
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
p.Items = append(p.Items, item)
switch item.Status {
case firewallsync.StatusReady:
p.Ready++
p.Total++
case firewallsync.StatusExisting:
p.Existing++
p.Total++
case firewallsync.StatusRemove:
p.Removed++
case firewallsync.StatusBlocked:
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
}

View File

@@ -8,8 +8,6 @@ type ForwardRuleSearch struct {
Strategy string `json:"strategy"`
}
// ForwardRule preserves the existing firewall search response shape while
// keeping forwarding data separate from the filter client model.
type ForwardRule struct {
ID uint `json:"id"`
Chain string `json:"chain"`
@@ -34,7 +32,7 @@ type ForwardRule struct {
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
}
type ForwardRuleOperation struct {

View File

@@ -72,19 +72,6 @@ func FirewallRuleOwner(sourceKind, sourceID string) string {
return sourceKind + ":" + sourceID
}
func FirewallRulesRevision(rules []FirewallRule) (string, error) {
ordered := append([]FirewallRule(nil), rules...)
sort.Slice(ordered, func(i, j int) bool {
return ordered[i].UUID < ordered[j].UUID
})
payload, err := json.Marshal(ordered)
if err != nil {
return "", err
}
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:]), nil
}
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"sort"
"strconv"
@@ -17,13 +18,13 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
agenti18n "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
@@ -88,8 +89,8 @@ type IDockerPortGuardService interface {
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
Operate(context.Context, dto.DockerPortGuardOperation) error
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
DeletePolicies(context.Context, dto.DockerPortGuardPolicyBatchDelete) error
UpsertPolicies(context.Context, dto.DockerPortGuardPolicyBatch) error
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
Reconcile(context.Context) error
}
@@ -125,6 +126,12 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
}
defer cli.Close()
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
return []dto.DockerPortGuardContainer{}, nil
}
}
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
if err != nil {
return nil, err
@@ -213,6 +220,19 @@ func matchDockerGuardPolicies(
return endpoints, orphanPolicies
}
func initializeDockerGuardRuntime(runtime dockerGuardRuntime, policies []docker_guard.Policy) error {
err := runtime.Initialize(policies)
if errors.Is(err, docker_guard.ErrDockerForwardPolicyDrop) {
family := "IPv4"
var familyErr *docker_guard.FamilyError
if errors.As(err, &familyErr) && familyErr.Family == docker_guard.FamilyIPv6 {
family = "IPv6"
}
return buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": family}, err)
}
return err
}
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
@@ -226,7 +246,7 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
if err != nil {
return err
}
if err := runtime.Initialize(policies); err != nil {
if err := initializeDockerGuardRuntime(runtime, policies); err != nil {
recordDockerPortGuardReconcileError(err)
return err
}
@@ -296,7 +316,7 @@ func (s *DockerPortGuardService) QueueInitialization(
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
t.Logf("backend=%s", backend)
err := runtime.Initialize(policies)
err := initializeDockerGuardRuntime(runtime, policies)
recordDockerPortGuardReconcileError(err)
return err
}, nil)
@@ -317,51 +337,68 @@ func (s *DockerPortGuardService) QueueInitialization(
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func (s *DockerPortGuardService) DeletePolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatchDelete) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
uuids, err := docker_guard.NormalizePolicyUUIDs(request.UUIDs)
if err != nil {
return err
return dto.FilterChainOperationResponse{}, err
}
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
return err
labels := make([]string, len(uuids))
for i, id := range uuids {
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
}
return s.reconcileLocked(ctx)
}
func (s *DockerPortGuardService) UpsertPolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatch) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := s.rejectHostInputDockerGuardEndpoints(ctx, request.Endpoints); err != nil {
return err
}
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Endpoints))
seen := make(map[string]struct{}, len(request.Endpoints))
for _, endpoint := range request.Endpoints {
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
Family: endpoint.Family, HostIP: endpoint.HostIP, HostPort: endpoint.HostPort,
Protocol: endpoint.Protocol, Mode: request.Mode, Sources: request.Sources,
})
if err != nil {
return queueFirewallRuleTask("Docker", task.TaskDelete, labels, func(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
key := guardEndpointKey(normalized.Family, normalized.HostIP, normalized.HostPort, normalized.Protocol)
if _, exists := seen[key]; exists {
continue
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
return err
}
seen[key] = struct{}{}
encoded, _ := json.Marshal(normalized.Sources)
policies = append(policies, model.DockerPortGuardPolicy{
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
Sources: string(encoded), Description: strings.TrimSpace(request.Description),
})
return s.reconcileLocked(ctx)
})
}
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Policies))
for i, policy := range request.Policies {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
}
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
return err
}
return s.reconcileLocked(ctx)
return queueFirewallRuleTask("Docker", task.TaskUpdate, labels, func(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
for i, policy := range request.Policies {
if err := ctx.Err(); err != nil {
return err
}
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
})
if err != nil {
return fmt.Errorf("%s: %w", labels[i], err)
}
encoded, err := json.Marshal(normalized.Sources)
if err != nil {
return fmt.Errorf("%s: %w", labels[i], err)
}
policies = append(policies, model.DockerPortGuardPolicy{
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
})
endpoints = append(endpoints, policy.DockerPortGuardEndpointIdentity)
}
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
return err
}
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
return err
}
return s.reconcileLocked(ctx)
})
}
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(
@@ -407,63 +444,6 @@ func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
return s.reconcileLocked(ctx)
}
func (s *DockerPortGuardService) loadRuleSyncCandidates(
ctx context.Context,
request dto.FirewallRuleSyncRequest,
) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, error) {
targetProvider, err := databaseRuleSyncTarget(request, "Docker")
if err != nil {
return "", nil, nil, err
}
target := string(targetProvider)
selected, err := s.selectedRuleSyncBackend(ctx)
if err != nil {
return "", nil, nil, err
}
if target != selected {
return "", nil, nil, fmt.Errorf(
"%w: selected Docker firewall backend is %s, requested target is %s",
filter.ErrProviderUnavailable, selected, target,
)
}
policies, err := s.policies.ListManaged(ctx)
if err != nil {
return "", nil, nil, err
}
return target, policies, s.guardRuntime(target), nil
}
func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) {
if global.DB != nil {
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
selected = strings.ToLower(strings.TrimSpace(selected))
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
return selected, nil
}
}
if s.client == nil {
return "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable)
}
cli, err := s.client()
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
}
return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil
}
func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy {
result := make([]docker_guard.Policy, 0, len(policies))
for _, policy := range policies {
result = append(result, dockerGuardPolicyFromModel(policy))
}
return result
}
func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard.Policy {
return docker_guard.Policy{
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
@@ -471,33 +451,6 @@ func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard
}
}
func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}
}
func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...),
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}
}
func dockerGuardReadOnlyRuleSyncDTO(policy docker_guard.ReadOnlyPolicy) *dto.DockerPortGuardEndpoint {
return &dto.DockerPortGuardEndpoint{
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
Protocol: policy.Policy.Protocol, PolicyUUID: dockerGuardReadOnlyPolicyUUID(policy), Sources: append([]string(nil), policy.Policy.Sources...),
NativeAction: policy.Action, ReadOnly: true, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
}
}
func dockerGuardReadOnlyPolicyUUID(policy docker_guard.ReadOnlyPolicy) string {
nativeRules, _ := json.Marshal(policy.NativeRules)
fingerprint := strings.Join([]string{
@@ -576,7 +529,7 @@ func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error
return err
}
if !initialized {
err = runtime.Initialize(policies)
err = initializeDockerGuardRuntime(runtime, policies)
} else {
err = runtime.Reconcile(policies)
}

View File

@@ -2,7 +2,6 @@ package service
import (
"context"
"errors"
"fmt"
"strconv"
@@ -12,7 +11,6 @@ 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/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
type panelPortWhitelistKey struct{}
@@ -97,32 +95,6 @@ func warnPanelPortCleanupFailure(port uint, err error) {
}
}
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()
}
func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool {
rule := systemPortRule(filter.ProviderUFW, port)
for _, required := range protected {

View File

@@ -0,0 +1,58 @@
package service
import (
"context"
"fmt"
"io"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
func queueFirewallRuleTask(name, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTaskWithOps(name, operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
@@ -26,7 +25,7 @@ import (
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
OperateRules(request dto.ForwardRuleOperate) error
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
Enable() error
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
Restore(context.Context) error
@@ -72,11 +71,6 @@ func newForwardingService() *ForwardingService {
}
}
type forwardingRuleSyncCandidate struct {
rule forwarding.Rule
err error
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected := configuredForwardingBackend()
baseInfo := dto.FirewallSubsystemStatus{
@@ -198,10 +192,29 @@ func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string)
return false
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) error {
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Rules))
operation := task.TaskCreate
for i, rule := range request.Rules {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
if rule.Operation != "add" {
operation = task.TaskUpdate
}
}
if forwardingOperationsOnlyRemove(request.Rules) {
operation = task.TaskDelete
}
return queueFirewallRuleTask("Forwarding", operation, labels, func(ctx context.Context) error {
return s.operateRules(ctx, request)
})
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
ctx := context.Background()
if err := ctx.Err(); err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
@@ -336,85 +349,6 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
return err
}
func (s *ForwardingService) loadRuleSyncCandidates(
ctx context.Context,
targetProvider filter.Provider,
) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
target, err := s.manager()
if err != nil {
return nil, nil, nil, false, err
}
if target.Name() != string(targetProvider) {
return nil, nil, nil, false, fmt.Errorf(
"%w: selected forwarding backend is %s, requested target is %s",
filter.ErrProviderUnavailable, target.Name(), targetProvider,
)
}
stored, err := s.rules.List(ctx)
if err != nil {
return nil, nil, nil, false, err
}
candidates := make([]forwardingRuleSyncCandidate, 0, len(stored))
for _, record := range stored {
rule := forwarding.Rule{
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
TargetPort: record.TargetPort, Interface: record.Interface,
}
normalized, normalizeErr := forwarding.NormalizeRule(rule)
candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr})
}
targetStatus, err := target.Status()
if err != nil {
return nil, nil, nil, false, err
}
targetRules := make([]forwarding.Rule, 0)
if targetStatus.IsInit {
targetRules, err = target.List("", "")
if err != nil {
return nil, nil, nil, false, err
}
targetRules, err = normalizeForwardingRuntimeRules(targetRules)
if err != nil {
return nil, nil, nil, false, err
}
}
return target, candidates, targetRules, targetStatus.IsInit, nil
}
func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error {
actual, err := target.List("", "")
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
actual, err = normalizeForwardingRuntimeRules(actual)
if err != nil {
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
}
if !databaseSyncStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
}
return nil
}
func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) {
normalized := make([]forwarding.Rule, 0, len(rules))
for _, rule := range rules {
item, err := forwarding.NormalizeRule(rule)
if err != nil {
return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err)
}
normalized = append(normalized, item)
}
return normalized, nil
}
func forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule {
return &dto.ForwardRule{
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort, Interface: rule.Interface,
}
}
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
manager, err := s.manager()
if err != nil {

View File

@@ -32,7 +32,6 @@ const (
const (
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
FirewallRuleCheckVersion = 1
FirewallRuleOriginCreated = "created"
FirewallRuleOriginAdopted = "adopted"

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "The {{ .name }} application is not compatible with the
ErrDockerFailed: 'Docker is abnormal; check service status'
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD defaults to DROP. Adjust the policy and retry'
ErrUFWRuleAdopt: 'Failed to adopt the UFW rule: {{ .detail }}. If the installed UFW version is earlier than 0.35, upgrade UFW and try again'
ErrDockerComposeCmdNotFound: 'Docker Compose not found on host'
UseExistImage: 'Image exists; using existing image'
@@ -510,13 +511,20 @@ AppLink: 'Linked Application'
EnableSSL: 'Enable HTTPS'
AppStore: 'App Store'
Firewall: 'Firewall'
FirewallCreateRulesStep: 'Create firewall rules'
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
FirewallCreateRuleSkipped: 'Not executed'
FirewallCreateBatchStep: 'Submit {{ .count }} rules as a batch to {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules'
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncResult: 'Synchronization result: {{ .succeeded }} succeeded, {{ .existing }} existing, {{ .failed }} failed'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallSyncFailedResetSkipped: '{{ .failed }} firewall rules failed to synchronize; the source firewall was not reset'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
FirewallVerifyTargetStep: 'Verify target firewall rules'
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
FirewallRestoreRulesStep: 'Restore database rules to {{ .name }}'
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
FirewallTargetRuleIneffective: 'The target rule is no longer effective'
FirewallVerifyRuleFailed: 'Failed to verify synchronized rule {{ .name }}: {{ .detail }}'
TaskSync: 'Sync'
LocalApp: 'Local Application'
SubTask: 'Subtask'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'La aplicación {{ .name }} no es compatible con la ver
ErrDockerFailed: 'El estado de Docker es anómalo, revise el servicio'
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo'
ErrDockerForwardPolicyDrop: 'La política predeterminada de FORWARD para {{ .family }} es DROP. Ajústela y vuelva a intentarlo'
ErrUFWRuleAdopt: 'No se pudo administrar la regla UFW: {{ .detail }}. Si la versión actual de UFW es anterior a 0.35, actualice UFW y vuelva a intentarlo'
ErrDockerComposeCmdNotFound: 'El comando Docker Compose no existe instálelo primero en el host'
UseExistImage: 'La imagen ya existe, usando imagen existente'
@@ -510,13 +511,20 @@ AppLink: 'Aplicación enlazada'
EnableSSL: 'Habilitar HTTPS'
AppStore: 'Tienda de aplicaciones'
Firewall: 'Firewall'
FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
FirewallCreateRuleSkipped: 'Sin ejecutar'
FirewallCreateBatchStep: 'Enviar {{ .count }} reglas en un lote a {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas'
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncResult: 'Resultado de sincronización: {{ .succeeded }} correctas, {{ .existing }} existentes, {{ .failed }} fallidas'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallSyncFailedResetSkipped: 'No se pudieron sincronizar {{ .failed }} reglas; el firewall de origen no se restableció'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
FirewallVerifyTargetStep: 'Verificar las reglas del firewall de destino'
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar las reglas de la base de datos en {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos
FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas'
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
FirewallTargetRuleIneffective: 'La regla de destino ya no es efectiva'
FirewallVerifyRuleFailed: 'No se pudo verificar la regla sincronizada {{ .name }}: {{ .detail }}'
TaskSync: 'Sincronizar'
LocalApp: 'Aplicación local'
SubTask: 'Subtarea'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "برنامه {{ .name }} با نسخه فعلی 1Panel
ErrDockerFailed: 'Docker غیرعادی است؛ وضعیت سرویس را بررسی کنید'
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید'
ErrDockerNftablesChainUnavailable: 'زنجیره فایروال IPv4 مربوط به nftables داکر یافت نشد. بررسی کنید نسخه فعلی داکر از بک‌اند فایروال nftables پشتیبانی کند، سپس داکر را راه‌اندازی مجدد کرده و دوباره تلاش کنید'
ErrDockerForwardPolicyDrop: 'سیاست پیش‌فرض FORWARD برای {{ .family }} برابر DROP است. آن را تغییر دهید و دوباره تلاش کنید'
ErrUFWRuleAdopt: 'مدیریت قانون UFW ناموفق بود: {{ .detail }}. اگر نسخه فعلی UFW قدیمی‌تر از 0.35 است، ابتدا UFW را ارتقا دهید و دوباره تلاش کنید'
ErrDockerComposeCmdNotFound: 'Docker Compose روی میزبان یافت نشد'
UseExistImage: 'تصویر وجود دارد؛ استفاده از تصویر موجود'
@@ -510,13 +511,20 @@ AppLink: 'برنامه متصل'
EnableSSL: 'فعال‌سازی HTTPS'
AppStore: 'فروشگاه برنامه'
Firewall: 'فایروال'
FirewallCreateRulesStep: 'ایجاد قوانین دیوار آتش'
FirewallCreateRulesResult: 'نتیجه ایجاد: {{ .succeeded }} موفق، {{ .failed }} ناموفق، {{ .skipped }} اجرا نشده'
FirewallRuleOperationResult: 'نتیجه عملیات: {{ .succeeded }} موفق، {{ .failed }} ناموفق'
FirewallCreateRuleSkipped: 'اجرا نشده'
FirewallCreateBatchStep: 'ارسال {{ .count }} قانون به صورت دسته‌ای به {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}؛ به {{ .count }} قانون تبدیل شد'
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncResult: 'نتیجه همگام‌سازی: {{ .succeeded }} موفق، {{ .existing }} موجود، {{ .failed }} ناموفق'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallSyncFailedResetSkipped: 'همگام‌سازی {{ .failed }} قانون ناموفق بود؛ فایروال مبدأ بازنشانی نشد'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
FirewallResetSourceResult: 'فایروال مبدأ بازنشانی شد و {{ .removed }} قانون پایگاه داده حذف شد'
FirewallVerifyTargetStep: 'بررسی قوانین فایروال مقصد'
FirewallInitializeChainsStep: 'راه‌اندازی و اتصال زنجیره‌های پایه {{ .name }}'
FirewallRestoreRulesStep: 'بازیابی قوانین پایگاه داده در {{ .name }}'
FirewallSyncWhitelistStep: 'همگام‌سازی فهرست مجاز پورت‌های فایروال'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'بازیابی قوانین هدایت پو
FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker و سیاست‌ها'
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
FirewallTargetRuleIneffective: 'قانون مقصد دیگر مؤثر نیست'
FirewallVerifyRuleFailed: 'بررسی قانون همگام‌شده {{ .name }} ناموفق بود: {{ .detail }}'
TaskSync: 'همگام‌سازی'
LocalApp: 'برنامه محلی'
SubTask: 'وظیفه فرعی'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} アプリケーションは現在の 1Pane
ErrDockerFailed: 'Docker の状態が異常です。サービス状態を確認してください'
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
ErrDockerNftablesChainUnavailable: 'Docker の nftables IPv4 ファイアウォールチェーンが見つかりません。現在の Docker バージョンが nftables ファイアウォールバックエンドをサポートしていることを確認し、Docker を再起動してから再試行してください'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD の既定ポリシーが DROP です。変更してから再試行してください'
ErrUFWRuleAdopt: 'UFW ルールの管理に失敗しました:{{ .detail }}。現在の UFW バージョンが 0.35 未満の場合は、UFW をアップグレードしてから再試行してください'
ErrDockerComposeCmdNotFound: 'Docker Compose コマンドは存在しません。ホストマシンにこのコマンドを先にインストールしてください'
UseExistImage: 'イメージは既に存在します。既存のイメージを使用します'
@@ -510,13 +511,20 @@ AppLink: 'リンクされたアプリケーション'
EnableSSL: 'HTTPS を有効にする'
AppStore: 'Appストア'
Firewall: 'ファイアウォール'
FirewallCreateRulesStep: 'ファイアウォールルールを作成'
FirewallCreateRulesResult: '作成結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 件、失敗 {{ .failed }} 件'
FirewallCreateRuleSkipped: '未実行'
FirewallCreateBatchStep: '{{ .backend }} に {{ .count }} 件のルールを一括送信'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}{{ .count }} 件のルールに変換'
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncResult: '同期結果:成功 {{ .succeeded }} 件、既存 {{ .existing }} 件、失敗 {{ .failed }} 件'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallSyncFailedResetSkipped: '{{ .failed }} 件のルールを同期できなかったため、移行元ファイアウォールはリセットされませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
FirewallResetSourceResult: '移行元ファイアウォールをリセットし、データベースルール {{ .removed }} 件を削除しました'
FirewallVerifyTargetStep: '移行先ファイアウォールルールを検証'
FirewallInitializeChainsStep: '{{ .name }} のベースチェーンを初期化してバインド'
FirewallRestoreRulesStep: 'データベースルールを {{ .name }} に復元'
FirewallSyncWhitelistStep: 'ファイアウォールポート許可リストを同期'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'データベースのポート転送ルー
FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエンドと保護ポリシーを確認'
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
FirewallTargetRuleIneffective: '移行先ルールは有効ではありません'
FirewallVerifyRuleFailed: '同期ルール {{ .name }} の検証に失敗しました:{{ .detail }}'
TaskSync: '同期'
LocalApp: 'ローカルアプリケーション'
SubTask: 'サブタスク'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} 응용 프로그램은 현재 1Panel 버
ErrDockerFailed: 'Docker의 상태가 비정상입니다. 서비스 상태를 확인하세요'
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 방화벽 체인을 찾을 수 없습니다. 현재 Docker 버전이 nftables 방화벽 백엔드를 지원하는지 확인하고 Docker를 다시 시작한 후 재시도하세요'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD 기본 정책이 DROP입니다. 정책을 조정한 후 다시 시도하세요'
ErrUFWRuleAdopt: 'UFW 규칙 관리에 실패했습니다: {{ .detail }}. 현재 UFW 버전이 0.35 미만이면 UFW를 업그레이드한 후 다시 시도하세요'
ErrDockerComposeCmdNotFound: 'Docker Compose 명령이 없습니다. 호스트 머신에 먼저 이 명령을 설치하세요'
UseExistImage: '이미지가 이미 존재하여 기존 이미지를 사용합니다'
@@ -510,13 +511,20 @@ AppLink: '연결된 애플리케이션'
EnableSSL: 'HTTPS 활성화'
AppStore: '앱스토어'
Firewall: '방화벽'
FirewallCreateRulesStep: '방화벽 규칙 생성'
FirewallCreateRulesResult: '생성 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallRuleOperationResult: '작업 결과: 성공 {{ .succeeded }}개, 실패 {{ .failed }}개'
FirewallCreateRuleSkipped: '미실행'
FirewallCreateBatchStep: '{{ .backend }}에 규칙 {{ .count }}개 일괄 제출'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; 규칙 {{ .count }}개로 변환'
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncResult: '동기화 결과: 성공 {{ .succeeded }}개, 기존 {{ .existing }}개, 실패 {{ .failed }}개'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallSyncFailedResetSkipped: '규칙 {{ .failed }}개를 동기화하지 못해 원본 방화벽을 초기화하지 않았습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
FirewallResetSourceResult: '원본 방화벽을 초기화하고 데이터베이스 규칙 {{ .removed }}개를 삭제했습니다'
FirewallVerifyTargetStep: '대상 방화벽 규칙 확인'
FirewallInitializeChainsStep: '{{ .name }} 기본 체인 초기화 및 바인딩'
FirewallRestoreRulesStep: '데이터베이스 규칙을 {{ .name }}에 복원'
FirewallSyncWhitelistStep: '방화벽 포트 허용 목록 동기화'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: '데이터베이스 포트 전달 규칙 복
FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책 확인'
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
FirewallTargetRuleIneffective: '대상 규칙이 더 이상 적용되지 않습니다'
FirewallVerifyRuleFailed: '동기화된 규칙 {{ .name }} 확인 실패: {{ .detail }}'
TaskSync: '동기화'
LocalApp: '로컬 애플리케이션'
SubTask: '하위 작업'

View File

@@ -151,6 +151,7 @@ ErrAppVersionDeprecated: "ແອັບພລິເຄຊັນ {{ .name }} ບ
ErrDockerFailed: 'Docker ຜິດປົກກະຕິ; ກະລຸນາກວດສອບສະຖານະການບໍລິການ'
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerNftablesChainUnavailable: 'ບໍ່ພົບ chain ໄຟວໍ nftables IPv4 ຂອງ Docker. ກະລຸນາກວດສອບວ່າ Docker ລຸ້ນປັດຈຸບັນຮອງຮັບ backend ໄຟວໍ nftables, ເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
ErrDockerForwardPolicyDrop: 'ນະໂຍບາຍເລີ່ມຕົ້ນ FORWARD ຂອງ {{ .family }} ແມ່ນ DROP. ກະລຸນາປັບແລ້ວລອງອີກຄັ້ງ'
ErrUFWRuleAdopt: 'ຈັດການກົດ UFW ບໍ່ສຳເລັດ: {{ .detail }}. ຖ້າ UFW ລຸ້ນປັດຈຸບັນຕ່ຳກວ່າ 0.35, ກະລຸນາອັບເກຣດ UFW ແລ້ວລອງໃໝ່'
ErrDockerComposeCmdNotFound: 'ບໍ່ພົບຄຳສັ່ງ Docker Compose ໃນເຄື່ອງ'
UseExistImage: 'ມີຮູບພາບຢູ່ແລ້ວ; ກຳລັງໃຊ້ຮູບພາບທີ່ມີຢູ່'
@@ -500,13 +501,20 @@ AppLink: 'ແອັບພລິເຄຊັນທີ່ເຊື່ອມໂຍ
EnableSSL: 'ເປີດໃຊ້ HTTPS'
AppStore: 'ຮ້ານແອັບ'
Firewall: 'ໄຟວອລ'
FirewallCreateRulesStep: 'ສ້າງກົດໄຟວໍ'
FirewallCreateRulesResult: 'ຜົນການສ້າງ: ສຳເລັດ {{ .succeeded }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallRuleOperationResult: 'ຜົນການດຳເນີນການ: ສຳເລັດ {{ .succeeded }} ລາຍການ, ລົ້ມເຫຼວ {{ .failed }} ລາຍການ'
FirewallCreateRuleSkipped: 'ບໍ່ໄດ້ດຳເນີນການ'
FirewallCreateBatchStep: 'ສົ່ງ {{ .count }} ກົດເປັນຊຸດໄປຫາ {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ແປງເປັນ {{ .count }} ກົດ'
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncResult: 'ຜົນການຊິງຄ໌: ສຳເລັດ {{ .succeeded }}, ມີແລ້ວ {{ .existing }}, ລົ້ມເຫຼວ {{ .failed }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallSyncFailedResetSkipped: 'ຊິງຄ໌ກົດ {{ .failed }} ລາຍການບໍ່ສຳເລັດ; ບໍ່ໄດ້ຣີເຊັດໄຟວອລຕົ້ນທາງ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
FirewallResetSourceResult: 'ຣີເຊັດໄຟວອລຕົ້ນທາງແລ້ວ ແລະ ລຶບກົດຖານຂໍ້ມູນ {{ .removed }} ລາຍການ'
FirewallVerifyTargetStep: 'ກວດສອບກົດໄຟວອລປາຍທາງ'
FirewallInitializeChainsStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ພື້ນຖານ {{ .name }}'
FirewallRestoreRulesStep: 'ກູ້ຄືນກົດຖານຂໍ້ມູນໄປຫາ {{ .name }}'
FirewallSyncWhitelistStep: 'ຊິງຄ໌ລາຍການພອດໄຟວໍທີ່ອະນຸຍາດ'
@@ -515,8 +523,6 @@ FirewallRestoreForwardingRulesStep: 'ກູ້ຄືນກົດສົ່ງຕ
FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແລະ ນະໂຍບາຍ'
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
FirewallTargetRuleIneffective: 'ກົດປາຍທາງບໍ່ມີຜົນແລ້ວ'
FirewallVerifyRuleFailed: 'ກວດສອບກົດທີ່ຊິງຄ໌ {{ .name }} ບໍ່ສຳເລັດ: {{ .detail }}'
TaskSync: 'ຊິງຄ໌'
LocalApp: 'ແອັບພລິເຄຊັນພາຍໃນເຄື່ອງ'
SubTask: 'ວຽກຍ່ອຍ'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'Aplikasi {{ .name }} tidak sesuai dengan versi 1Panel
ErrDockerFailed: 'Keadaan Docker tidak normal, sila periksa status perkhidmatan'
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
ErrDockerNftablesChainUnavailable: 'Rantaian firewall IPv4 nftables Docker tidak ditemui. Pastikan versi Docker semasa menyokong bahagian belakang firewall nftables, mulakan semula Docker dan cuba lagi'
ErrDockerForwardPolicyDrop: 'Dasar lalai FORWARD {{ .family }} ialah DROP. Laraskan dasar dan cuba lagi'
ErrUFWRuleAdopt: 'Gagal mengurus peraturan UFW: {{ .detail }}. Jika versi UFW semasa lebih lama daripada 0.35, tingkatkan UFW dan cuba lagi'
ErrDockerComposeCmdNotFound: 'Perintah Docker Compose tidak wujud, sila pasang perintah ini di mesin tuan terlebih dahulu'
UseExistImage: 'Imej sudah wujud, menggunakan imej sedia ada'
@@ -510,13 +511,20 @@ AppLink: 'Aplikasi Terpaut'
EnableSSL: 'Dayakan HTTPS'
AppStore: 'Kedai aplikasi'
Firewall: 'Firewall'
FirewallCreateRulesStep: 'Cipta peraturan tembok api'
FirewallCreateRulesResult: 'Hasil penciptaan: {{ .succeeded }} berjaya, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallRuleOperationResult: 'Hasil operasi: {{ .succeeded }} berjaya, {{ .failed }} gagal'
FirewallCreateRuleSkipped: 'Tidak dilaksanakan'
FirewallCreateBatchStep: 'Hantar {{ .count }} peraturan secara kelompok ke {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; ditukar kepada {{ .count }} peraturan'
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncResult: 'Hasil penyegerakan: {{ .succeeded }} berjaya, {{ .existing }} sedia ada, {{ .failed }} gagal'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallSyncFailedResetSkipped: '{{ .failed }} peraturan gagal disegerakkan; firewall sumber tidak ditetapkan semula'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
FirewallResetSourceResult: 'Firewall sumber ditetapkan semula; {{ .removed }} peraturan pangkalan data dipadamkan'
FirewallVerifyTargetStep: 'Sahkan peraturan firewall sasaran'
FirewallInitializeChainsStep: 'Mulakan dan ikat rantai asas {{ .name }}'
FirewallRestoreRulesStep: 'Pulihkan peraturan pangkalan data ke {{ .name }}'
FirewallSyncWhitelistStep: 'Segerakkan senarai putih port firewall'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Pulihkan peraturan pemajuan port pangkalan
FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
FirewallTargetRuleIneffective: 'Peraturan sasaran tidak lagi berkesan'
FirewallVerifyRuleFailed: 'Gagal mengesahkan peraturan disegerakkan {{ .name }}: {{ .detail }}'
TaskSync: 'Segerakkan'
LocalApp: 'Aplikasi Tempatan'
SubTask: 'Subtugas'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'O aplicativo {{ .name }} não é compatível com a ver
ErrDockerFailed: 'O estado do Docker está anormal, verifique o status do serviço'
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
ErrDockerNftablesChainUnavailable: 'A cadeia de firewall IPv4 do nftables do Docker não foi encontrada. Confirme se a versão atual do Docker oferece suporte ao backend de firewall nftables, reinicie o Docker e tente novamente'
ErrDockerForwardPolicyDrop: 'A política padrão de FORWARD para {{ .family }} é DROP. Ajuste-a e tente novamente'
ErrUFWRuleAdopt: 'Falha ao gerenciar a regra UFW: {{ .detail }}. Se a versão atual do UFW for anterior à 0.35, atualize o UFW e tente novamente'
ErrDockerComposeCmdNotFound: 'O comando Docker Compose não existe, instale este comando na máquina host primeiro'
UseExistImage: 'A imagem já existe, usando imagem existente'
@@ -510,13 +511,20 @@ AppLink: 'Aplicativo vinculado'
EnableSSL: 'Habilitar HTTPS'
AppStore: 'Loja de aplicativos'
Firewall: 'Firewall'
FirewallCreateRulesStep: 'Criar regras de firewall'
FirewallCreateRulesResult: 'Resultado da criação: {{ .succeeded }} com sucesso, {{ .failed }} com falha, {{ .skipped }} não executadas'
FirewallRuleOperationResult: 'Resultado da operação: {{ .succeeded }} com sucesso, {{ .failed }} com falha'
FirewallCreateRuleSkipped: 'Não executada'
FirewallCreateBatchStep: 'Enviar {{ .count }} regras em lote para {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido em {{ .count }} regras'
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncResult: 'Resultado da sincronização: {{ .succeeded }} com sucesso, {{ .existing }} existentes, {{ .failed }} com falha'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallSyncFailedResetSkipped: '{{ .failed }} regras falharam na sincronização; o firewall de origem não foi redefinido'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
FirewallResetSourceResult: 'Firewall de origem redefinido; {{ .removed }} regras do banco de dados foram excluídas'
FirewallVerifyTargetStep: 'Verificar regras do firewall de destino'
FirewallInitializeChainsStep: 'Inicializar e vincular as cadeias base do {{ .name }}'
FirewallRestoreRulesStep: 'Restaurar regras do banco de dados para {{ .name }}'
FirewallSyncWhitelistStep: 'Sincronizar a lista de portas permitidas do firewall'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Restaurar regras de encaminhamento de porta
FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as políticas'
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
FirewallTargetRuleIneffective: 'A regra de destino não está mais efetiva'
FirewallVerifyRuleFailed: 'Falha ao verificar a regra sincronizada {{ .name }}: {{ .detail }}'
TaskSync: 'Sincronizar'
LocalApp: 'Aplicativo local'
SubTask: 'Subtarefa'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: 'Приложение {{ .name }} несовмести
ErrDockerFailed: 'Состояние Docker аномально, проверьте состояние сервиса'
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
ErrDockerNftablesChainUnavailable: 'Цепочка IPv4 брандмауэра nftables Docker не найдена. Убедитесь, что текущая версия Docker поддерживает бэкенд брандмауэра nftables, перезапустите Docker и повторите попытку'
ErrDockerForwardPolicyDrop: 'Политика FORWARD по умолчанию для {{ .family }} — DROP. Измените её и повторите попытку'
ErrUFWRuleAdopt: 'Не удалось принять правило UFW под управление: {{ .detail }}. Если текущая версия UFW ниже 0.35, обновите UFW и повторите попытку'
ErrDockerComposeCmdNotFound: 'Команда Docker Compose отсутствует, пожалуйста, установите эту команду на хост-машине сначала'
UseExistImage: 'Образ уже существует, используется существующий образ'
@@ -510,13 +511,20 @@ AppLink: 'Связанное приложение'
EnableSSL: 'Включить HTTPS'
AppStore: 'Магазин приложений'
Firewall: 'Межсетевой экран'
FirewallCreateRulesStep: 'Создание правил межсетевого экрана'
FirewallCreateRulesResult: 'Результат создания: успешно — {{ .succeeded }}, ошибок — {{ .failed }}, не выполнено — {{ .skipped }}'
FirewallRuleOperationResult: 'Результат операции: успешно — {{ .succeeded }}, ошибок — {{ .failed }}'
FirewallCreateRuleSkipped: 'Не выполнено'
FirewallCreateBatchStep: 'Отправка {{ .count }} правил одним пакетом в {{ .backend }}'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; преобразовано в {{ .count }} правил'
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncResult: 'Результат синхронизации: успешно — {{ .succeeded }}, уже существуют — {{ .existing }}, ошибок — {{ .failed }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallSyncFailedResetSkipped: 'Не удалось синхронизировать правил: {{ .failed }}; исходный межсетевой экран не был сброшен'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
FirewallResetSourceResult: 'Исходный межсетевой экран сброшен; удалено правил из базы данных: {{ .removed }}'
FirewallVerifyTargetStep: 'Проверить правила целевого межсетевого экрана'
FirewallInitializeChainsStep: 'Инициализировать и привязать базовые цепочки {{ .name }}'
FirewallRestoreRulesStep: 'Восстановить правила базы данных в {{ .name }}'
FirewallSyncWhitelistStep: 'Синхронизировать белый список портов межсетевого экрана'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Восстановить правила п
FirewallInspectDockerGuardStep: 'Проверить бэкенд межсетевого экрана Docker и политики'
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
FirewallTargetRuleIneffective: 'Целевое правило больше не действует'
FirewallVerifyRuleFailed: 'Не удалось проверить синхронизированное правило {{ .name }}: {{ .detail }}'
TaskSync: 'Синхронизировать'
LocalApp: 'Локальное приложение'
SubTask: 'Подзадача'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} uygulaması mevcut 1Panel sürümü ile uy
ErrDockerFailed: 'Docker durumu anormal, lütfen servis durumunu kontrol edin'
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Dockerı yeniden başlatıp tekrar deneyin'
ErrDockerNftablesChainUnavailable: 'Docker nftables IPv4 güvenlik duvarı zinciri bulunamadı. Geçerli Docker sürümünün nftables güvenlik duvarı arka ucunu desteklediğini doğrulayın, Dockerı yeniden başlatıp tekrar deneyin'
ErrDockerForwardPolicyDrop: '{{ .family }} FORWARD varsayılan ilkesi DROP. İlkeyi düzenleyip tekrar deneyin'
ErrUFWRuleAdopt: 'UFW kuralı yönetilemedi: {{ .detail }}. Geçerli UFW sürümü 0.35ten eskiyse UFWyi yükseltip tekrar deneyin'
ErrDockerComposeCmdNotFound: 'Docker Compose komutu mevcut değil, lütfen önce bu komutu host makinesine yükleyin'
UseExistImage: 'Görüntü zaten mevcut, mevcut görüntü kullanılıyor'
@@ -510,13 +511,20 @@ AppLink: 'Bağlantılı Uygulama'
EnableSSL: 'HTTPS Etkinleştir'
AppStore: 'Uygulama Mağazası'
Firewall: 'Güvenlik duvarı'
FirewallCreateRulesStep: 'Güvenlik duvarı kuralları oluştur'
FirewallCreateRulesResult: 'Oluşturma sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallRuleOperationResult: 'İşlem sonucu: {{ .succeeded }} başarılı, {{ .failed }} başarısız'
FirewallCreateRuleSkipped: 'Yürütülmedi'
FirewallCreateBatchStep: '{{ .backend }} için {{ .count }} kuralı toplu gönder'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; {{ .count }} kurala dönüştürüldü'
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncResult: 'Eşitleme sonucu: {{ .succeeded }} başarılı, {{ .existing }} mevcut, {{ .failed }} başarısız'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallSyncFailedResetSkipped: '{{ .failed }} kural eşitlenemedi; kaynak güvenlik duvarı sıfırlanmadı'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
FirewallResetSourceResult: 'Kaynak güvenlik duvarı sıfırlandı; {{ .removed }} veritabanı kuralı silindi'
FirewallVerifyTargetStep: 'Hedef güvenlik duvarı kurallarını doğrula'
FirewallInitializeChainsStep: '{{ .name }} temel zincirlerini başlat ve bağla'
FirewallRestoreRulesStep: 'Veritabanı kurallarını {{ .name }} üzerine geri yükle'
FirewallSyncWhitelistStep: 'Güvenlik duvarı bağlantı noktası izin listesini eşitle'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: 'Veritabanı bağlantı noktası yönlendirm
FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkelerini denetle'
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
FirewallTargetRuleIneffective: 'Hedef kural artık etkili değil'
FirewallVerifyRuleFailed: 'Eşitlenen {{ .name }} kuralı doğrulanamadı: {{ .detail }}'
TaskSync: 'Senkronize Et'
LocalApp: 'Yerel Uygulama'
SubTask: 'Alt görev'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: '{{ .name }} 應用不適配目前 1Panel 版本,跳
ErrDockerFailed: 'Docker 狀態異常,請檢查服務狀態'
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
ErrDockerNftablesChainUnavailable: '未偵測到 Docker 的 nftables IPv4 防火牆鏈,請確認目前 Docker 版本支援 nftables 防火牆後端,重新啟動 Docker 後再試'
ErrDockerForwardPolicyDrop: '偵測到 {{ .family }} FORWARD 預設策略為 DROP請調整後重試'
ErrUFWRuleAdopt: 'UFW 規則納管失敗:{{ .detail }}。如果目前 UFW 版本低於 0.35,請先升級 UFW 後再試'
ErrDockerComposeCmdNotFound: 'Docker Compose 指令不存在,請先在宿主機安裝此指令'
UseExistImage: '映像已存在,使用現有映像'
@@ -510,13 +511,20 @@ AppLink: '關聯應用程式'
EnableSSL: '開啟HTTPS'
AppStore: '應用程式商店'
Firewall: '防火牆'
FirewallCreateRulesStep: '建立防火牆規則'
FirewallCreateRulesResult: '建立結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallRuleOperationResult: '操作結果:成功 {{ .succeeded }} 條,失敗 {{ .failed }} 條'
FirewallCreateRuleSkipped: '未執行'
FirewallCreateBatchStep: '向 {{ .backend }} 批次提交 {{ .count }} 條規則'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}{{ .rule }};轉換為 {{ .count }} 條規則'
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncResult: '同步結果:成功 {{ .succeeded }} 條,已存在 {{ .existing }} 條,失敗 {{ .failed }} 條'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallSyncFailedResetSkipped: '{{ .failed }} 條防火牆規則同步失敗,未重設來源防火牆'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
FirewallResetSourceResult: '來源防火牆重設完成,已刪除 {{ .removed }} 條資料庫規則'
FirewallVerifyTargetStep: '驗證目標防火牆規則'
FirewallInitializeChainsStep: '初始化並綁定 {{ .name }} 基礎鏈'
FirewallRestoreRulesStep: '恢復資料庫規則至 {{ .name }}'
FirewallSyncWhitelistStep: '同步防火牆連接埠白名單'
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: '恢復資料庫連接埠轉發規則'
FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
FirewallTargetRuleIneffective: '目標規則已失效'
FirewallVerifyRuleFailed: '驗證同步規則 {{ .name }} 失敗:{{ .detail }}'
TaskSync: '同步'
LocalApp: '本機應用'
SubTask: '子任務'

View File

@@ -161,6 +161,7 @@ ErrAppVersionDeprecated: "{{ .name }} 应用不适配当前 1Panel 版本,跳
ErrDockerFailed: "Docker 状态异常,请检查服务状态"
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
ErrDockerNftablesChainUnavailable: "未检测到 Docker 的 nftables IPv4 防火墙链,请确认当前 Docker 版本支持 nftables 防火墙后端,重启 Docker 后重试"
ErrDockerForwardPolicyDrop: '检测到 {{ .family }} FORWARD 默认策略为 DROP请调整后重试'
ErrUFWRuleAdopt: "UFW 规则纳管失败:{{ .detail }}。如果当前 UFW 版本低于 0.35,请先升级 UFW 后重试"
ErrDockerComposeCmdNotFound: "Docker Compose 命令不存在,请先在宿主机安装"
UseExistImage: "镜像已存在,使用存量镜像"
@@ -510,13 +511,20 @@ AppLink: "关联应用"
EnableSSL: "开启 HTTPS"
AppStore: "应用商店"
Firewall: "防火墙"
FirewallCreateRulesStep: '创建防火墙规则'
FirewallCreateRulesResult: '创建结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallRuleOperationResult: '操作结果:成功 {{ .succeeded }} 条,失败 {{ .failed }} 条'
FirewallCreateRuleSkipped: '未执行'
FirewallCreateBatchStep: '向 {{ .backend }} 批量提交 {{ .count }} 条规则'
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}{{ .rule }};转换为 {{ .count }} 条规则'
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncResult: "同步结果:成功 {{ .succeeded }} 条,已存在 {{ .existing }} 条,失败 {{ .failed }} 条"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallSyncFailedResetSkipped: "{{ .failed }} 条防火墙规则同步失败,未重置源防火墙"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
FirewallResetSourceResult: "源防火墙重置完成,已删除 {{ .removed }} 条数据库规则"
FirewallVerifyTargetStep: "验证目标防火墙规则"
FirewallInitializeChainsStep: "初始化并绑定 {{ .name }} 基础链"
FirewallRestoreRulesStep: "恢复数据库规则到 {{ .name }}"
FirewallSyncWhitelistStep: "同步防火墙端口白名单"
@@ -525,8 +533,6 @@ FirewallRestoreForwardingRulesStep: "恢复数据库端口转发规则"
FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
FirewallTargetRuleIneffective: "目标规则已失效"
FirewallVerifyRuleFailed: "验证同步规则 {{ .name }} 失败:{{ .detail }}"
TaskSync: "同步"
LocalApp: "本地应用"
SubTask: "子任务"

View File

@@ -34,7 +34,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/rules/search", baseApi.SearchFirewallRules)
hostRouter.POST("/firewall/rules/reset", baseApi.ResetFirewallRules)
hostRouter.POST("/firewall/rules/native/detail", baseApi.LoadFirewallNativeDetail)
hostRouter.POST("/firewall/rules/check", baseApi.CheckFirewallRules)
hostRouter.POST("/firewall/rules/adopt", baseApi.AdoptFirewallRule)
hostRouter.POST("/firewall/rules", baseApi.CreateFirewallRules)
hostRouter.POST("/firewall/rules/sync/preview", baseApi.PreviewFirewallRuleSync)
hostRouter.GET("/firewall/rules/sync/task", baseApi.LoadFirewallRuleSyncTask)

View File

@@ -0,0 +1,42 @@
package docker_guard
import (
"errors"
"fmt"
"strings"
)
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP")
func (m *NftablesManager) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if !m.runner.Exists(family.command) {
continue
}
output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD")
if err != nil {
return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)}
}
found := false
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" {
continue
}
found = true
if fields[2] == "DROP" {
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
}
}
if !found {
return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")}
}
}
return nil
}

View File

@@ -6,6 +6,8 @@ import (
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
const (
@@ -25,13 +27,16 @@ func NewNftablesManager() *NftablesManager { return &NftablesManager{runner: com
func (m *NftablesManager) Initialize(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
if !m.runner.Exists("nft") {
return errors.New("nft is not installed")
}
if err := m.checkForwardPolicy(); err != nil {
return err
}
inventory, err := m.ListPolicies()
if err != nil {
return err
}
if !m.runner.Exists("nft") {
return errors.New("nft is not installed")
}
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
return err
}
@@ -70,10 +75,10 @@ func (m *NftablesManager) ListPolicies() (PolicyInventory, error) {
inventory := PolicyInventory{Policies: make([]Policy, 0), ManagedRuleOrders: make(map[string][]int64)}
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
output, err := nftables_helper.ReadChain(m.run, tableFamily, NftTable, NftChain)
if errors.Is(err, nftables_helper.ErrChainNotFound) {
continue
}
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftChain)
if err != nil {
return PolicyInventory{}, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", NftChain, err)}
}

View File

@@ -22,13 +22,15 @@ const (
)
type DesiredChange struct {
Operation ChangeOperation `json:"operation"`
Before *FirewallRule `json:"before,omitempty"`
After *FirewallRule `json:"after,omitempty"`
Locator *Locator `json:"locator,omitempty"`
PreviousMarker string `json:"previousMarker,omitempty"`
Append bool `json:"append,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
CommandOnly bool `json:"-"`
UnmarkedAdopted bool `json:"-"`
Operation ChangeOperation `json:"operation"`
Before *FirewallRule `json:"before,omitempty"`
After *FirewallRule `json:"after,omitempty"`
Locator *Locator `json:"locator,omitempty"`
PreviousMarker string `json:"previousMarker,omitempty"`
Append bool `json:"append,omitempty"`
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
}
type NativeCommand struct {
@@ -47,12 +49,25 @@ type NativeRulePlan struct {
}
type BackendPlan struct {
CommandOnly bool `json:"-"`
Provider Provider `json:"provider"`
Scope Scope `json:"scope"`
SnapshotRevision string `json:"snapshotRevision"`
Rules []NativeRulePlan `json:"rules"`
}
func (p BackendPlan) CreatesOnly() bool {
if len(p.Rules) == 0 {
return false
}
for _, rule := range p.Rules {
if rule.Operation != ChangeCreate {
return false
}
}
return true
}
type ApplyResult struct {
Applied []ObservedRule `json:"applied"`
Verification *VerifyResult `json:"verification,omitempty"`

View File

@@ -23,75 +23,3 @@ func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error {
}
return nil
}
func CheckAdopt(snapshot Snapshot, requested FirewallRule, desired []DesiredRule, locator Locator) (RuleCheckResult, error) {
normalized, err := NormalizeRule(requested)
if err != nil {
return RuleCheckResult{}, err
}
if normalized.Scope.Key() != snapshot.Scope.Key() {
return RuleCheckResult{}, ErrInvalidScope
}
key, err := RuleKey(normalized)
if err != nil {
return RuleCheckResult{}, err
}
result := RuleCheckResult{RequestedRule: normalized, RequestedRuleKey: key}
var selected *ObservedRule
for index := range snapshot.Rules {
candidate := &snapshot.Rules[index]
if SameLocator(candidate.Locator, locator) {
if selected != nil {
return RuleCheckResult{}, ErrRuleStale
}
selected = candidate
}
}
if selected == nil {
return RuleCheckResult{}, ErrRuleStale
}
selectedKey, err := RuleKey(selected.Rule)
if err != nil || selected.ParseStatus != ParseStatusSupported || selectedKey != key {
return RuleCheckResult{}, fmt.Errorf("%w: selected rule changed or cannot be adopted", ErrRuleStale)
}
if err := CheckAdoptDuplicates(snapshot, normalized); err != nil {
if err != ErrDuplicateAdoption {
return RuleCheckResult{}, err
}
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactExternal, "duplicate_rules"
return finishCheck(result)
}
items, err := MergeInventory(InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
if err != nil {
return RuleCheckResult{}, err
}
for _, item := range items {
if item.Desired != nil && item.Observed != nil && SameLocator(item.Observed.Locator, locator) {
result.Decision, result.Classification, result.Reason = CheckDecisionNoChange, CheckClassificationExactManaged, "equivalent_managed_rule"
result.ExistingRuleUUID = item.Desired.UUID
return result, nil
}
}
for _, owned := range desired {
same, err := SameRuleContent(owned.Rule, normalized)
if err != nil {
return RuleCheckResult{}, err
}
if same {
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactManaged, "duplicate_rules"
result.ExistingRuleUUID = owned.UUID
return result, nil
}
}
result.Candidates = []ObservedRule{*selected}
switch {
case selected.Protected:
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationProtected, "protected_rule"
case containsPersistenceDrift(result.Candidates):
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "runtime_permanent_mismatch"
default:
result.Decision, result.Classification, result.Reason = CheckDecisionConfirmationRequired, CheckClassificationExactExternal, "equivalent_external_rule"
result.AllowedActions = []CheckAction{CheckActionAdopt, CheckActionCancel}
}
return finishCheck(result)
}

View File

@@ -1,420 +0,0 @@
package filter
import (
"errors"
"fmt"
"net/netip"
"strconv"
"strings"
)
var (
ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action")
ErrRuleStale = errors.New("firewall rule state is stale")
ErrRuleOperation = errors.New("firewall rule operation is not allowed")
ErrRuleCheckRequired = errors.New("firewall rule must be checked again")
)
type CheckDecision string
const (
CheckDecisionReady CheckDecision = "ready"
CheckDecisionConfirmationRequired CheckDecision = "confirmation_required"
CheckDecisionBlocked CheckDecision = "blocked"
CheckDecisionNoChange CheckDecision = "no_change"
)
type CheckClassification string
const (
CheckClassificationNone CheckClassification = "none"
CheckClassificationExactManaged CheckClassification = "exact_managed"
CheckClassificationExactExternal CheckClassification = "exact_external"
CheckClassificationConflict CheckClassification = "conflict"
CheckClassificationUnsupported CheckClassification = "unsupported"
CheckClassificationProtected CheckClassification = "protected"
)
type CheckAction string
const (
CheckActionCreate CheckAction = "create"
CheckActionAdopt CheckAction = "adopt"
CheckActionSelectAdopt CheckAction = "select_adopt"
CheckActionCancel CheckAction = "cancel"
)
type RuleCheckResult struct {
Decision CheckDecision `json:"decision"`
Classification CheckClassification `json:"classification"`
Reason string `json:"reason"`
RequestedRule FirewallRule `json:"requestedRule"`
RequestedRuleKey string `json:"requestedRuleKey"`
ExistingRuleUUID string `json:"existingRuleUUID,omitempty"`
Candidates []ObservedRule `json:"candidates,omitempty"`
AllowedActions []CheckAction `json:"allowedActions,omitempty"`
}
func CheckCreate(
snapshot Snapshot,
requested FirewallRule,
desired []DesiredRule,
) (RuleCheckResult, error) {
normalized, err := NormalizeRule(requested)
if err != nil {
return RuleCheckResult{}, err
}
if normalized.Scope.Key() != snapshot.Scope.Normalize().Key() {
return RuleCheckResult{}, fmt.Errorf("%w: requested scope %q does not match snapshot %q", ErrInvalidRule, normalized.Scope.Key(), snapshot.Scope.Key())
}
ruleKey, err := RuleKey(normalized)
if err != nil {
return RuleCheckResult{}, err
}
result := RuleCheckResult{
Decision: CheckDecisionReady,
Classification: CheckClassificationNone,
Reason: "new_rule",
RequestedRule: normalized,
RequestedRuleKey: ruleKey,
}
matchKey, err := RuleMatchKey(normalized)
if err != nil {
return RuleCheckResult{}, err
}
for _, owned := range desired {
ownedKey, err := desiredRuleKey(owned)
if err != nil {
return RuleCheckResult{}, err
}
ownedMatchKey, err := RuleMatchKey(owned.Rule)
if err != nil {
return RuleCheckResult{}, err
}
if ownedMatchKey != matchKey || (owned.Origin != RuleOriginCreated && owned.Origin != RuleOriginAdopted) {
continue
}
if OppositeActions(normalized.Action, owned.Rule.Action) {
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "exact_rule_conflict"
return finishCheck(result)
}
if normalized.Action != owned.Rule.Action {
continue
}
owned.RuleKey = ownedKey
result.ExistingRuleUUID = owned.UUID
if snapshotHasOwnedRule(snapshot, owned) {
result.Decision = CheckDecisionNoChange
result.Classification = CheckClassificationExactManaged
result.Reason = "equivalent_managed_rule"
} else {
result.Decision = CheckDecisionBlocked
result.Classification = CheckClassificationConflict
result.Reason = "managed_rule_drifted"
}
return finishCheck(result)
}
exact := make([]ObservedRule, 0)
equivalentExternal := false
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
if hydrated, ok := hydrateOwnedPartialRule(observed, desired); ok {
observed = hydrated
} else {
continue
}
}
observedRule, err := NormalizeRule(observed.Rule)
if err != nil {
return RuleCheckResult{}, err
}
observed.Rule = observedRule
observedKey, err := RuleMatchKey(observedRule)
if err != nil {
return RuleCheckResult{}, err
}
if observedKey != matchKey {
continue
}
if OppositeActions(normalized.Action, observedRule.Action) {
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationConflict, "exact_rule_conflict"
return finishCheck(result)
}
if normalized.Action == observedRule.Action {
equivalentExternal = true
key, err := RuleKey(observedRule)
if err != nil {
return RuleCheckResult{}, err
}
if key == ruleKey {
exact = append(exact, observed)
}
}
}
if err := CheckAdoptDuplicates(snapshot, normalized); err != nil {
if err != ErrDuplicateAdoption {
return RuleCheckResult{}, err
}
result.Decision, result.Classification, result.Reason = CheckDecisionBlocked, CheckClassificationExactExternal, "duplicate_rules"
return finishCheck(result)
}
switch {
case containsPersistenceDrift(exact):
result.Decision = CheckDecisionBlocked
result.Classification = CheckClassificationConflict
result.Reason = "runtime_permanent_mismatch"
result.Candidates = exact
case containsProtectedRule(exact):
result.Decision = CheckDecisionBlocked
result.Classification = CheckClassificationProtected
result.Reason = "protected_rule"
result.Candidates = exact
case len(exact) == 1:
result.Decision = CheckDecisionConfirmationRequired
result.Classification = CheckClassificationExactExternal
result.Reason = "equivalent_external_rule"
result.Candidates = exact
result.AllowedActions = []CheckAction{CheckActionAdopt, CheckActionCancel}
case equivalentExternal:
result.Decision = CheckDecisionNoChange
result.Classification = CheckClassificationExactExternal
result.Reason = "equivalent_external_rule"
default:
result.AllowedActions = []CheckAction{CheckActionCreate}
}
return finishCheck(result)
}
func hydrateOwnedPartialRule(observed ObservedRule, desired []DesiredRule) (ObservedRule, bool) {
if observed.ParseStatus != ParseStatusPartial || strings.TrimSpace(observed.Marker) == "" ||
len(observed.UncertainFields) != 1 || observed.UncertainFields[0] != ObservedFieldProtocol {
return observed, false
}
var matched *DesiredRule
for index := range desired {
candidate := &desired[index]
if candidate.Origin != RuleOriginCreated && candidate.Origin != RuleOriginAdopted {
continue
}
if candidate.Rule.Scope.Normalize().Key() != observed.Rule.Scope.Normalize().Key() ||
!desiredMarkerMatchesObserved(*candidate, observed.Marker) ||
!ObservedRuleMatchesExpected(observed, candidate.Rule) {
continue
}
if matched != nil {
return observed, false
}
matched = candidate
}
if matched == nil {
return observed, false
}
orderIndex := observed.Rule.OrderIndex
observed.Rule = matched.Rule
observed.Rule.OrderIndex = orderIndex
observed.ParseStatus = ParseStatusSupported
observed.UncertainFields = nil
return observed, true
}
func desiredMarkerMatchesObserved(desired DesiredRule, observedMarker string) bool {
observedMarker = strings.TrimSpace(observedMarker)
if observedMarker == strings.TrimSpace(desired.Marker) {
return observedMarker != ""
}
return desired.Origin == RuleOriginAdopted && strings.TrimSpace(desired.UUID) != "" &&
observedMarker == "1panel-rule:"+strings.TrimSpace(desired.UUID)
}
func finishCheck(result RuleCheckResult) (RuleCheckResult, error) {
for index, candidate := range result.Candidates {
identity, err := InstanceKey(candidate)
if err != nil {
identity, err = RuleKey(candidate.Rule)
if err != nil {
return RuleCheckResult{}, err
}
}
result.Candidates[index].InstanceKey = identity
}
return result, nil
}
func containsPersistenceDrift(rules []ObservedRule) bool {
for _, rule := range rules {
if rule.Persistence != "" && rule.Persistence != PersistenceStatusConverged {
return true
}
}
return false
}
func containsProtectedRule(rules []ObservedRule) bool {
for _, rule := range rules {
if rule.Protected {
return true
}
}
return false
}
func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, error) {
matched := make([]ObservedRule, 0, 1)
for _, candidate := range candidates {
identity, err := InstanceKey(candidate)
if err != nil {
identity, err = RuleKey(candidate.Rule)
if err != nil {
continue
}
}
if selected != "" && identity == selected {
matched = append(matched, candidate)
}
}
if len(matched) != 1 {
return ObservedRule{}, ErrRuleOperation
}
return matched[0], nil
}
func desiredRuleKey(desired DesiredRule) (string, error) {
key, err := RuleKey(desired.Rule)
if err != nil {
return "", err
}
if desired.RuleKey != "" && desired.RuleKey != key {
return "", fmt.Errorf("%w: desired rule %q key mismatch", ErrInvalidRule, desired.UUID)
}
return key, nil
}
func snapshotHasOwnedRule(snapshot Snapshot, desired DesiredRule) bool {
items, err := MergeInventory(InventoryMergeInput{Observed: snapshot.Rules, Desired: []DesiredRule{desired}})
if err != nil {
return false
}
for _, item := range items {
if item.Desired != nil {
return item.Match == InventoryMatchExact && item.State != InventoryStateDrifted
}
}
return false
}
func RulesOverlap(left, right FirewallRule) bool {
left, leftErr := NormalizeRule(left)
right, rightErr := NormalizeRule(right)
if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() {
return false
}
return familiesOverlap(left.Scope.Family, right.Scope.Family) &&
protocolsOverlap(left.Protocol, right.Protocol) &&
addressesOverlap(left.SourceAddress, right.SourceAddress) &&
addressesOverlap(left.DestinationAddress, right.DestinationAddress) &&
portsOverlap(left.SourcePort, right.SourcePort) &&
portsOverlap(left.DestinationPort, right.DestinationPort) &&
(left.Interface == "" || right.Interface == "" || left.Interface == right.Interface)
}
func familiesOverlap(left, right Family) bool {
return left == FamilyInet || right == FamilyInet || left == right
}
func protocolsOverlap(left, right string) bool {
return left == "all" || right == "all" || left == right
}
func addressesOverlap(left, right string) bool {
if left == "" || right == "" {
return true
}
leftPrefix, leftErr := netip.ParsePrefix(left)
rightPrefix, rightErr := netip.ParsePrefix(right)
if leftErr != nil || rightErr != nil {
return false
}
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 {
if left == "" || right == "" {
return true
}
leftIntervals, leftErr := portIntervals(left)
rightIntervals, rightErr := portIntervals(right)
if leftErr != nil || rightErr != nil {
return false
}
for _, leftInterval := range leftIntervals {
for _, rightInterval := range rightIntervals {
if leftInterval[0] <= rightInterval[1] && rightInterval[0] <= leftInterval[1] {
return true
}
}
}
return false
}
func portIntervals(value string) ([][2]int, error) {
parts := strings.Split(value, ",")
intervals := make([][2]int, 0, len(parts))
for _, part := range parts {
start, end, err := portInterval(strings.TrimSpace(part))
if err != nil {
return nil, err
}
intervals = append(intervals, [2]int{start, end})
}
return intervals, nil
}
func portInterval(value string) (int, int, error) {
parts := strings.Split(value, "-")
if len(parts) == 1 {
port, err := strconv.Atoi(parts[0])
return port, port, err
}
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid port interval %q", value)
}
start, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
}
end, err := strconv.Atoi(parts[1])
return start, end, err
}

View File

@@ -1,170 +0,0 @@
package filter
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
)
type CheckFlagCodec struct {
secret []byte
version int
}
type checkFlagClaims struct {
Version int `json:"version"`
Provider Provider `json:"provider"`
ScopeKey string `json:"scopeKey"`
RuleDigest string `json:"ruleDigest"`
SnapshotRevision string `json:"snapshotRevision"`
ManagedRevision string `json:"managedRevision"`
Decision CheckDecision `json:"decision"`
Classification CheckClassification `json:"classification"`
AllowedActions []CheckAction `json:"allowedActions"`
AdoptionCandidates []checkFlagAdoptionCandidate `json:"adoptionCandidates,omitempty"`
}
type checkFlagAdoptionCandidate struct {
InstanceKey string `json:"instanceKey"`
Locator Locator `json:"locator"`
}
type CreateAuthorization struct {
Operation ChangeOperation
Locator *Locator
}
func NewCheckFlagCodec(secret []byte, version int) *CheckFlagCodec {
return &CheckFlagCodec{secret: append([]byte(nil), secret...), version: version}
}
func (c *CheckFlagCodec) Sign(result RuleCheckResult, snapshot Snapshot, managedRevision string) (string, error) {
ruleDigest, err := ruleDigest(result.RequestedRule)
if err != nil {
return "", err
}
claims := checkFlagClaims{
Version: c.version,
Provider: result.RequestedRule.Scope.Provider,
ScopeKey: result.RequestedRule.Scope.Key(),
RuleDigest: ruleDigest,
SnapshotRevision: snapshot.Revision,
ManagedRevision: managedRevision,
Decision: result.Decision,
Classification: result.Classification,
AllowedActions: result.AllowedActions,
}
if result.Classification == CheckClassificationExactExternal {
claims.AdoptionCandidates = make([]checkFlagAdoptionCandidate, 0, len(result.Candidates))
for _, candidate := range result.Candidates {
claims.AdoptionCandidates = append(claims.AdoptionCandidates, checkFlagAdoptionCandidate{
InstanceKey: candidate.InstanceKey,
Locator: candidate.Locator,
})
}
}
payload, err := json.Marshal(claims)
if err != nil {
return "", err
}
signature := c.signature(payload)
return base64.RawURLEncoding.EncodeToString(payload) + "." + base64.RawURLEncoding.EncodeToString(signature), nil
}
func (c *CheckFlagCodec) Authorize(
checkFlag string,
action CheckAction,
adoptInstanceKey string,
rule FirewallRule,
snapshot Snapshot,
managedRevision string,
) (CreateAuthorization, error) {
claims, err := c.parse(checkFlag)
if err != nil {
return CreateAuthorization{}, err
}
ruleDigest, err := ruleDigest(rule)
if err != nil {
return CreateAuthorization{}, err
}
if claims.Version != c.version ||
claims.Provider != rule.Scope.Provider ||
claims.ScopeKey != rule.Scope.Key() ||
claims.RuleDigest != ruleDigest ||
claims.SnapshotRevision != snapshot.Revision ||
claims.ManagedRevision != managedRevision {
return CreateAuthorization{}, fmt.Errorf("%w: firewall or managed rules changed", ErrRuleCheckRequired)
}
if claims.Decision != CheckDecisionReady && claims.Decision != CheckDecisionConfirmationRequired {
return CreateAuthorization{}, ErrRuleOperation
}
if !containsCheckAction(claims.AllowedActions, action) {
return CreateAuthorization{}, ErrRuleOperation
}
switch action {
case CheckActionCreate:
if strings.TrimSpace(adoptInstanceKey) != "" {
return CreateAuthorization{}, ErrRuleOperation
}
return CreateAuthorization{Operation: ChangeCreate}, nil
case CheckActionAdopt, CheckActionSelectAdopt:
for _, candidate := range claims.AdoptionCandidates {
if candidate.InstanceKey == adoptInstanceKey && adoptInstanceKey != "" {
locator := candidate.Locator
return CreateAuthorization{Operation: ChangeAdopt, Locator: &locator}, nil
}
}
return CreateAuthorization{}, ErrRuleOperation
default:
return CreateAuthorization{}, ErrRuleOperation
}
}
func (c *CheckFlagCodec) parse(checkFlag string) (checkFlagClaims, error) {
parts := strings.Split(strings.TrimSpace(checkFlag), ".")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return checkFlagClaims{}, ErrRuleCheckRequired
}
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return checkFlagClaims{}, ErrRuleCheckRequired
}
signature, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil || !hmac.Equal(signature, c.signature(payload)) {
return checkFlagClaims{}, ErrRuleCheckRequired
}
var claims checkFlagClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return checkFlagClaims{}, ErrRuleCheckRequired
}
return claims, nil
}
func (c *CheckFlagCodec) signature(payload []byte) []byte {
mac := hmac.New(sha256.New, c.secret)
_, _ = mac.Write(payload)
return mac.Sum(nil)
}
func ruleDigest(rule FirewallRule) (string, error) {
payload, err := json.Marshal(rule)
if err != nil {
return "", err
}
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:]), nil
}
func containsCheckAction(actions []CheckAction, expected CheckAction) bool {
for _, action := range actions {
if action == expected {
return true
}
}
return false
}

View File

@@ -289,3 +289,20 @@ func hashJSON(value any) (string, error) {
sum := sha256.Sum256(payload)
return "sha256:" + hex.EncodeToString(sum[:]), nil
}
func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, error) {
matched := make([]ObservedRule, 0, 1)
for _, candidate := range candidates {
identity, err := InstanceKey(candidate)
if err != nil {
continue
}
if selected != "" && identity == selected {
matched = append(matched, candidate)
}
}
if len(matched) != 1 {
return ObservedRule{}, ErrRuleOperation
}
return matched[0], nil
}

View File

@@ -103,6 +103,7 @@ func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) {
byRuleKey[candidate.ruleKey] = append(byRuleKey[candidate.ruleKey], index)
if instanceKey, err := instanceKeyWithRuleKey(candidate.rule, candidate.ruleKey); err == nil {
candidate.instanceKey = instanceKey
candidate.rule.InstanceKey = instanceKey
byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index)
}
}

View File

@@ -174,9 +174,6 @@ func (a *Adapter) PrepareRule(rule filter.FirewallRule) (filter.FirewallRule, er
return filter.FirewallRule{}, err
}
} else if normalized.NativeKind == filter.NativeKindZonePort && !isNativeZonePort(normalized) {
// Edit requests inherit the current native kind. Once a native zone port
// gains an address, loses its port, or changes to a deny action, it must
// be represented as a rich rule instead.
normalized.NativeKind = filter.NativeKindRichRule
normalized, err = filter.NormalizeRule(normalized)
if err != nil {
@@ -231,6 +228,9 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
executed := 0
for index, command := range rulePlan.Commands {
if err := a.writer.Run(ctx, command); err != nil {
if plan.CommandOnly {
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, rulePlan, executed, err)
}
executed = index + 1
@@ -328,7 +328,7 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC
if target.Locator.Canonical == expected.Locator.Canonical {
break
}
removeCommands, restoreCommands := pairedCommands(target.Rule, "remove", "add")
removeCommands, restoreCommands := observedPairedCommands(target, "remove", "add")
addCommands, removeNewCommands := missingRuleCommands(snapshot, normalized)
plan.Commands = append(removeCommands, addCommands...)
plan.RollbackCommands = append(restoreCommands, removeNewCommands...)
@@ -340,7 +340,15 @@ func (a *Adapter) compileChange(snapshot filter.Snapshot, change filter.DesiredC
plan.Previous = &target
plan.Expected = target
plan.Expected.Rule.UUID = normalized.UUID
plan.Commands, plan.RollbackCommands = pairedCommands(target.Rule, "remove", "add")
plan.Commands, plan.RollbackCommands = observedPairedCommands(target, "remove", "add")
if change.CommandOnly {
switch target.Persistence {
case filter.PersistenceStatusRuntimeOnly:
plan.Commands, plan.RollbackCommands = plan.Commands[:1], plan.RollbackCommands[:1]
case filter.PersistenceStatusPermanentOnly:
plan.Commands, plan.RollbackCommands = plan.Commands[1:], plan.RollbackCommands[1:]
}
}
default:
return filter.NativeRulePlan{}, fmt.Errorf("%w: unsupported operation %s", filter.ErrInvalidRule, change.Operation)
}
@@ -420,9 +428,20 @@ func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]
}
func pairedCommands(rule filter.FirewallRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
option := nativeOption(rule, operation)
rollback := nativeOption(rule, inverse)
selector := scopeSelector(rule.Scope)
return pairedNativeCommands(rule.Scope, nativeOption(rule, operation), nativeOption(rule, inverse))
}
func observedPairedCommands(observed filter.ObservedRule, operation, inverse string) ([]filter.NativeCommand, []filter.NativeCommand) {
if observed.Rule.NativeKind != filter.NativeKindRichRule || observed.Raw == "" {
return pairedCommands(observed.Rule, operation, inverse)
}
return pairedNativeCommands(observed.Rule.Scope,
"--"+operation+"-rich-rule="+observed.Raw,
"--"+inverse+"-rich-rule="+observed.Raw)
}
func pairedNativeCommands(scope filter.Scope, option, rollback string) ([]filter.NativeCommand, []filter.NativeCommand) {
selector := scopeSelector(scope)
commands := []filter.NativeCommand{
{Executable: "firewall-cmd", Args: []string{selector, option}},
{Executable: "firewall-cmd", Args: []string{"--permanent", selector, option}},
@@ -464,7 +483,8 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
if target.Protected {
return filter.ObservedRule{}, filter.ErrProtectedRule
}
if target.ParseStatus != filter.ParseStatusSupported || target.Persistence != filter.PersistenceStatusConverged {
if target.ParseStatus != filter.ParseStatusSupported ||
(target.Persistence != filter.PersistenceStatusConverged && !(change.CommandOnly && change.Operation == filter.ChangeDelete)) {
return filter.ObservedRule{}, filter.ErrRuleStale
}
want := normalized
@@ -511,6 +531,11 @@ func scopeSelector(scope filter.Scope) string {
}
func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error {
if plan.Operation == filter.ChangeCreate {
return cause
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
rollbackErr := a.rollback(ctx, plan.Expected.Rule.Scope, plan, executed)
if rollbackErr != nil {
return fmt.Errorf("firewalld apply failed: %w; compensation failed: %v", cause, rollbackErr)

View File

@@ -158,12 +158,18 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
executed := 0
for _, command := range rulePlan.Commands {
if err := a.writer.Run(ctx, command); err != nil {
if plan.CommandOnly {
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, ruleIndex, executed, err)
}
executed++
}
}
if err := a.writer.Save(ctx, plan.Scope); err != nil {
if plan.CommandOnly {
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, len(plan.Rules)-1, -1, err)
}
applied := make([]filter.ObservedRule, 0, len(plan.Rules))
@@ -363,6 +369,11 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
}
func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, lastRule, lastCommandCount int, cause error) error {
if plan.CreatesOnly() {
return cause
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
rollbackErr := a.rollback(ctx, plan, lastRule, lastCommandCount)
if rollbackErr != nil {
return fmt.Errorf("iptables apply failed: %w; compensation failed: %v", cause, rollbackErr)
@@ -641,7 +652,8 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
if wantErr != nil || observedErr != nil || wantKey != observedKey {
return 0, filter.ObservedRule{}, filter.ErrRuleStale
}
if change.Operation != filter.ChangeAdopt && observed.Marker != marker {
if change.Operation != filter.ChangeAdopt && observed.Marker != marker &&
!(change.Operation == filter.ChangeDelete && change.UnmarkedAdopted && observed.Marker == "") {
return 0, filter.ObservedRule{}, filter.ErrRuleStale
}
return position, observed, nil
@@ -674,18 +686,7 @@ func (systemBackend) CheckMultiport(ctx context.Context, family filter.Family) e
}
func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
var output string
var err error
if scope.Family == filter.FamilyIPv6 {
output, err = native.RunIPv6WithStdContext(ctx, scope.Table, "-S", scope.Chain)
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
if table, readErr := native.RunIPv6WithStdContext(ctx, scope.Table, "-S"); readErr == nil && !containsChainDeclaration(table, scope.Chain) {
return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain)
}
}
} else {
output, err = native.RunWithStdContext(ctx, scope.Table, "-S", scope.Chain)
}
output, err := native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
if err != nil {
return "", err
}

View File

@@ -2,6 +2,7 @@ package nftables
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
@@ -42,6 +43,16 @@ func (a *Adapter) Observe(ctx context.Context, scope filter.Scope) (filter.Snaps
return filter.Snapshot{}, fmt.Errorf("nftables backend is required")
}
output, err := a.backend.ListChain(ctx, scope)
if errors.Is(err, nftables_helper.ErrChainNotFound) {
snapshot, snapshotErr := filter.NewSnapshot(scope, nil)
if snapshotErr != nil {
return filter.Snapshot{}, snapshotErr
}
snapshot.Notices = []filter.ScopeNotice{{
Code: filter.ScopeNoticeManagedScopeMissing, Values: []string{string(scope.Family), scope.Chain},
}}
return snapshot, nil
}
if err != nil {
return filter.Snapshot{}, err
}
@@ -111,11 +122,17 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
for _, rulePlan := range plan.Rules {
for _, command := range rulePlan.Commands {
if err := a.backend.Run(ctx, command); err != nil {
if plan.CommandOnly {
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, err)
}
}
}
if err := a.backend.Save(ctx); err != nil {
if plan.CommandOnly {
return filter.ApplyResult{}, err
}
return filter.ApplyResult{}, a.compensate(ctx, plan, err)
}
applied := make([]filter.ObservedRule, 0, len(plan.Rules))
@@ -178,6 +195,11 @@ func (a *Adapter) Rollback(ctx context.Context, plan filter.BackendPlan) error {
}
func (a *Adapter) compensate(ctx context.Context, plan filter.BackendPlan, cause error) error {
if plan.CreatesOnly() {
return cause
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if err := a.Rollback(ctx, plan); err != nil {
return fmt.Errorf("nftables apply failed: %w; compensation failed: %v", cause, err)
}
@@ -378,8 +400,6 @@ func parseChain(scope filter.Scope, output string) []filter.ObservedRule {
continue
}
raw := strings.TrimSpace(line[:handleIndex])
// `nft -a list chain` includes a handle on the chain declaration itself.
// It is metadata for the chain, not a rule in the chain.
if strings.HasPrefix(raw, "chain ") {
continue
}
@@ -393,7 +413,11 @@ func parseChain(scope filter.Scope, output string) []filter.ObservedRule {
func parseRule(scope filter.Scope, raw, handle string, position int) filter.ObservedRule {
locator := filter.Locator{Provider: filter.ProviderNftables, ScopeKey: scope.Key(), NativeID: handle, Canonical: raw, Position: &position}
opaque := func() filter.ObservedRule {
return filter.ObservedRule{Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque}, Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw}
return filter.ObservedRule{
Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque},
Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw,
Protected: scope.Chain != filter.IptablesInputChain,
}
}
tokens, err := shellwords.Parse(raw)
if err != nil {
@@ -416,10 +440,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return opaque()
}
} else if tokens[index+1] == "l4proto" {
rule.Protocol = tokens[index+2]
if rule.Protocol == "ipv6-icmp" {
rule.Protocol = "icmpv6"
}
rule.Protocol = parseProtocol(tokens[index+2])
} else {
return opaque()
}
@@ -463,26 +484,26 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return opaque()
}
index += 2
if tokens[index] != "{" {
for _, state := range strings.Split(tokens[index], ",") {
if state = strings.TrimSpace(state); state != "" {
rule.ConnectionStates = append(rule.ConnectionStates, state)
}
}
values := tokens[index]
if values == "{" {
index++
continue
start := index
for index < len(tokens) && tokens[index] != "}" {
index++
}
if index == len(tokens) {
return opaque()
}
values = strings.Join(tokens[start:index], " ")
}
for _, state := range strings.Split(values, ",") {
state = strings.TrimSpace(state)
if state == "" {
return opaque()
}
rule.ConnectionStates = append(rule.ConnectionStates, parseConnectionState(state))
}
index++
for index < len(tokens) && tokens[index] != "}" {
state := strings.Trim(tokens[index], ",")
if state != "" {
rule.ConnectionStates = append(rule.ConnectionStates, state)
}
index++
}
if index < len(tokens) && tokens[index] == "}" {
index++
}
case "accept", "drop", "reject":
rule.Action = filter.Action(tokens[index])
index++
@@ -515,17 +536,59 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: scope.Chain != filter.IptablesInputChain}
}
func parseProtocol(value string) string {
switch numericSymbol(value) {
case "1":
return "icmp"
case "6":
return "tcp"
case "17":
return "udp"
case "58", "ipv6-icmp":
return "icmpv6"
default:
return value
}
}
func parseConnectionState(value string) string {
switch numericSymbol(value) {
case "1":
return "invalid"
case "2":
return "established"
case "4":
return "related"
case "8":
return "new"
case "64":
return "untracked"
default:
return value
}
}
func numericSymbol(value string) string {
base := 10
digits := value
if strings.HasPrefix(strings.ToLower(value), "0x") {
base, digits = 16, value[2:]
}
if number, err := strconv.ParseUint(digits, base, 32); err == nil {
return strconv.FormatUint(number, 10)
}
return value
}
type systemBackend struct{}
func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout(
"nft", "-n", "-n", "-a", "list", "chain", nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope),
)
if err != nil && scope.Family == filter.FamilyIPv6 &&
(strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
run := func(args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second)).RunWithOptionalSudoAndStdout(
"nft", append([]string{"-n", "-n"}, args...)...,
)
}
return output, err
return nftables_helper.ReadChain(run, nftables_helper.TableFamily(scope.Family), nftables_helper.TableName, nativeChainName(scope))
}
func (systemBackend) Run(ctx context.Context, command filter.NativeCommand) error {

View File

@@ -183,14 +183,24 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
executed := 0
for index, command := range plan.Rules[0].Commands {
if err := a.writer.Run(ctx, command); err != nil {
if a.failedCommandApplied(ctx, plan.Rules[0], index) {
executed = index + 1
if plan.CommandOnly {
return filter.ApplyResult{}, fmt.Errorf("execute UFW rule: %w", err)
}
if !plan.CreatesOnly() {
probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
if a.failedCommandApplied(probeCtx, plan.Rules[0], index) {
executed = index + 1
}
cancel()
}
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("execute UFW rule: %w", err))
return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, cause)
}
executed = index + 1
}
if plan.CommandOnly || plan.CreatesOnly() {
return filter.ApplyResult{Applied: []filter.ObservedRule{plan.Rules[0].Expected}}, nil
}
verification, err := a.verify(ctx, plan)
if err != nil {
cause := ufwApplyError(plan.Rules[0], fmt.Errorf("verify UFW rule: %w", err))
@@ -392,7 +402,7 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
deleteRuleCommand(normalized, marker),
}
case filter.ChangeDelete:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true)
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, !change.UnmarkedAdopted)
if targetErr != nil {
return filter.NativeRulePlan{}, targetErr
}
@@ -628,10 +638,6 @@ func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.V
positionMatches := rulePlan.Expected.Locator.Position == nil ||
(observed.Locator.Position != nil && *observed.Locator.Position == *rulePlan.Expected.Locator.Position)
semanticMatches := filter.ObservedRuleMatchesExpected(observed, rulePlan.Expected.Rule)
// A unique generated marker proves that UFW committed this command. Some
// UFW versions render otherwise valid rules in a form the numbered-status
// parser cannot fully recover. Do not turn that read-side limitation into
// a failed write, but keep rejecting fully parsed semantic mismatches.
if observed.ParseStatus == filter.ParseStatusOpaque {
semanticMatches = true
}
@@ -644,6 +650,11 @@ func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.V
}
func (a *Adapter) compensate(ctx context.Context, plan filter.NativeRulePlan, executed int, cause error) error {
if plan.Operation == filter.ChangeCreate {
return cause
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
rollbackErr := a.rollback(ctx, plan, executed)
if rollbackErr != nil {
return fmt.Errorf("ufw apply failed: %w; compensation failed: %v", cause, rollbackErr)

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
@@ -236,6 +237,35 @@ func (e *Engine) Capabilities(ctx context.Context) (filter.Capabilities, error)
return e.adapter.Capabilities(ctx)
}
func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
}
if !plan.CreatesOnly() {
return fmt.Errorf("%w: expected a create-only plan", filter.ErrInvalidRule)
}
_, err = e.adapter.Apply(ctx, plan)
return err
}
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
if err := ctx.Err(); err != nil {
return err
}
changes = append([]filter.DesiredChange(nil), changes...)
for index := range changes {
changes[index].CommandOnly = true
}
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
}
plan.CommandOnly = true
_, err = e.adapter.Apply(ctx, plan)
return err
}
func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) {
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
@@ -246,7 +276,7 @@ func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes
return plan, filter.VerifyResult{}, err
}
if result.Verification != nil {
if !result.Verification.Matched {
if !result.Verification.Matched && !plan.CreatesOnly() {
if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil {
return plan, *result.Verification, errors.Join(filter.ErrVerificationFailed, rollbackErr)
}
@@ -255,9 +285,12 @@ func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes
}
verification, err := e.adapter.Verify(ctx, plan)
if err != nil {
if plan.CreatesOnly() {
return plan, verification, err
}
return plan, verification, e.rollback(ctx, plan, err)
}
if !verification.Matched {
if !verification.Matched && !plan.CreatesOnly() {
if rollbackErr := e.Rollback(ctx, plan); rollbackErr != nil {
return plan, verification, errors.Join(filter.ErrVerificationFailed, rollbackErr)
}
@@ -270,6 +303,8 @@ func (e *Engine) Rollback(ctx context.Context, plan filter.BackendPlan) error {
if !ok {
return fmt.Errorf("%w: provider %s does not support applied-plan rollback", filter.ErrAdapterUnavailable, e.adapter.Provider())
}
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
return rollbacker.Rollback(ctx, plan)
}

View File

@@ -3,9 +3,17 @@ package filter
import (
"errors"
"fmt"
"net/netip"
"strconv"
"strings"
)
var (
ErrRuleConflict = errors.New("firewall rule has identical conditions and an opposing action")
ErrRuleStale = errors.New("firewall rule state is stale")
ErrRuleOperation = errors.New("firewall rule operation is not allowed")
)
var ErrVerificationFailed = errors.New("firewall rule verification failed")
func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) {
@@ -127,3 +135,117 @@ func FindCommittedObserved(snapshot Snapshot, requested FirewallRule, plan Backe
}
return matches[0], nil
}
func RulesOverlap(left, right FirewallRule) bool {
left, leftErr := NormalizeRule(left)
right, rightErr := NormalizeRule(right)
if leftErr != nil || rightErr != nil || left.Scope.Key() != right.Scope.Key() {
return false
}
return familiesOverlap(left.Scope.Family, right.Scope.Family) &&
protocolsOverlap(left.Protocol, right.Protocol) &&
addressesOverlap(left.SourceAddress, right.SourceAddress) &&
addressesOverlap(left.DestinationAddress, right.DestinationAddress) &&
portsOverlap(left.SourcePort, right.SourcePort) &&
portsOverlap(left.DestinationPort, right.DestinationPort) &&
(left.Interface == "" || right.Interface == "" || left.Interface == right.Interface)
}
func familiesOverlap(left, right Family) bool {
return left == FamilyInet || right == FamilyInet || left == right
}
func protocolsOverlap(left, right string) bool {
return left == "all" || right == "all" || left == right
}
func addressesOverlap(left, right string) bool {
if left == "" || right == "" {
return true
}
leftPrefix, leftErr := netip.ParsePrefix(left)
rightPrefix, rightErr := netip.ParsePrefix(right)
if leftErr != nil || rightErr != nil {
return false
}
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 {
if left == "" || right == "" {
return true
}
leftIntervals, leftErr := portIntervals(left)
rightIntervals, rightErr := portIntervals(right)
if leftErr != nil || rightErr != nil {
return false
}
for _, leftInterval := range leftIntervals {
for _, rightInterval := range rightIntervals {
if leftInterval[0] <= rightInterval[1] && rightInterval[0] <= leftInterval[1] {
return true
}
}
}
return false
}
func portIntervals(value string) ([][2]int, error) {
parts := strings.Split(value, ",")
intervals := make([][2]int, 0, len(parts))
for _, part := range parts {
start, end, err := portInterval(strings.TrimSpace(part))
if err != nil {
return nil, err
}
intervals = append(intervals, [2]int{start, end})
}
return intervals, nil
}
func portInterval(value string) (int, int, error) {
parts := strings.Split(value, "-")
if len(parts) == 1 {
port, err := strconv.Atoi(parts[0])
return port, port, err
}
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid port interval %q", value)
}
start, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
}
end, err := strconv.Atoi(parts[1])
return start, end, err
}

View File

@@ -1,10 +1,12 @@
package providers
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -14,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
)
type iptablesBackend interface {
@@ -39,6 +42,9 @@ func (systemIptablesBackend) Run(table string, args ...string) error {
}
func (systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) {
if len(args) == 1 && args[0] == "-S" {
return iptables_helper.ReadTable(context.Background(), table, false)
}
return iptables_helper.RunWithStd(table, args...)
}
@@ -47,6 +53,9 @@ func (systemIptablesBackend) RunIPv6(table string, args ...string) error {
}
func (systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) {
if len(args) == 1 && args[0] == "-S" {
return iptables_helper.ReadTable(context.Background(), table, true)
}
return iptables_helper.RunIPv6WithStd(table, args...)
}
@@ -114,7 +123,7 @@ func (l *iptablesNATAdapter) Name() string {
}
func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers")
stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S")
if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
}
@@ -122,7 +131,7 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
if !l.backend.IPv6Available() {
return rules, nil
}
stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers")
stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S")
if err != nil {
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
}
@@ -549,40 +558,70 @@ func (l *iptablesNATAdapter) Replay() error {
func parseIptablesRules(stdout, family string) []forwarding.Rule {
var rules []forwarding.Rule
num := 0
lines:
for _, line := range strings.Split(stdout, "\n") {
fields := strings.Fields(line)
if len(fields) < 11 {
fields, err := shellwords.Parse(line)
if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != forwarding.ChainPreRouting {
continue
}
rule := forwarding.Rule{
Num: fields[0],
Family: family,
Protocol: loadProtocol(fields[4]),
Interface: fields[6],
}
for index := 10; index < len(fields); index++ {
field := fields[index]
if strings.HasPrefix(field, "dpt:") || strings.HasPrefix(field, "dpts:") {
rule.Port = loadSourcePort(field)
num++
rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family}
target := ""
for index := 2; index < len(fields); index++ {
var value *string
switch fields[index] {
case "!":
continue lines
case "-p", "--protocol":
value = &rule.Protocol
case "-i", "--in-interface":
value = &rule.Interface
case "--dport", "--destination-port":
value = &rule.Port
case "-j", "--jump":
value = &target
case "--to-destination":
if index+1 >= len(fields) {
continue lines
}
index++
rule.TargetIP, rule.TargetPort = parseIptablesTarget(fields[index])
case "--to-ports", "--to-port":
value = &rule.TargetPort
case "-m", "--match", "--comment":
if index+1 >= len(fields) {
continue lines
}
index++
}
if strings.HasPrefix(field, "to:") {
rule.TargetIP, rule.TargetPort = parseIptablesTarget(strings.TrimPrefix(field, "to:"))
}
if field == "redir" && index+2 < len(fields) && fields[index+1] == "ports" {
rule.TargetPort = fields[index+2]
if value != nil {
if index+1 >= len(fields) {
continue lines
}
index++
*value = fields[index]
}
}
if rule.Protocol == "" || rule.Port == "" || rule.TargetPort == "" {
continue
}
if rule.TargetIP == "" {
switch target {
case "REDIRECT":
rule.TargetIP = "127.0.0.1"
if family == forwarding.FamilyIPv6 {
rule.TargetIP = "::1"
} else {
rule.TargetIP = "127.0.0.1"
}
case "DNAT":
if rule.TargetIP == "" {
continue
}
default:
continue
}
rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":")
rule.Protocol = loadProtocol(rule.Protocol)
rule.Port = strings.ReplaceAll(rule.Port, ":", "-")
rule.TargetPort = strings.ReplaceAll(rule.TargetPort, ":", "-")
rules = append(rules, rule)
}
return rules
@@ -617,14 +656,3 @@ func loadProtocol(protocol string) string {
return protocol
}
}
func loadSourcePort(value string) string {
port := ""
if strings.Contains(value, "dpt:") {
port = strings.ReplaceAll(value, "dpt:", "")
}
if strings.Contains(value, "dpts:") {
port = strings.ReplaceAll(value, "dpts:", "")
}
return strings.ReplaceAll(port, ":", "-")
}

View File

@@ -34,7 +34,10 @@ func (n *nftablesAdapter) Name() string { return "nftables" }
func (n *nftablesAdapter) List() ([]forwarding.Rule, error) {
rules := make([]forwarding.Rule, 0)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
stdout, err := nftRun("-a", "list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting))
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting))
if errors.Is(err, nftables_helper.ErrChainNotFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("failed to list nftables %s forwarding rules: %w", family, err)
}

View File

@@ -0,0 +1,29 @@
package iptables_helper
import (
"context"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return "", err
}
executable := commands.IPv4
if ipv6 {
if !commands.IPv6Available() {
return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable)
}
executable = commands.IPv6
}
output, err := runTables(ctx, executable, table, false, true, "-S")
if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}

View File

@@ -0,0 +1,16 @@
package nftables_helper
import "errors"
var ErrChainNotFound = errors.New("nftables chain is not initialized")
func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) {
output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain)
if err != nil {
return "", err
}
if !exists {
return "", ErrChainNotFound
}
return output, nil
}

View File

@@ -9,15 +9,6 @@ const (
StatusBlocked Status = "blocked"
)
type Outcome string
const (
OutcomeApplied Outcome = "applied"
OutcomeSkipped Outcome = "skipped"
OutcomeRemoved Outcome = "removed"
OutcomeFailed Outcome = "failed"
)
type ReasonCode string
const (

View File

@@ -7,106 +7,107 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
func SupportsManagedOrder(provider filter.Provider) bool {
return provider == filter.ProviderIptables || provider == filter.ProviderNftables || provider == filter.ProviderUFW
}
func ManagedOrderDrift(snapshot filter.Snapshot, desiredMarkers []string) map[string]struct{} {
if !SupportsManagedOrder(snapshot.Scope.Provider) || len(desiredMarkers) < 2 {
return nil
func RuleOrder(snapshot filter.Snapshot, ordered []filter.InventoryItem) map[string]bool {
drifted := make(map[string]bool)
if snapshot.Scope.Provider == filter.ProviderFirewalld {
return drifted
}
expected := make(map[string]struct{}, len(desiredMarkers))
for _, marker := range desiredMarkers {
expected[marker] = struct{}{}
}
actual := make([]string, 0, len(desiredMarkers))
present := make(map[string]struct{}, len(desiredMarkers))
for _, observed := range snapshot.Rules {
if _, wanted := expected[observed.Marker]; wanted {
actual = append(actual, observed.Marker)
present[observed.Marker] = struct{}{}
markers := make([]string, 0, len(ordered))
projected := append([]filter.ObservedRule(nil), snapshot.Rules...)
for _, item := range ordered {
if item.Desired == nil || item.Desired.Marker == "" {
continue
}
markers = append(markers, item.Desired.Marker)
if item.Observed != nil {
for index := range projected {
if filter.SameLocator(projected[index].Locator, item.Observed.Locator) {
projected[index].Marker = item.Desired.Marker
break
}
}
}
}
desired := make([]string, 0, len(actual))
for _, marker := range desiredMarkers {
if _, exists := present[marker]; exists {
desired = append(desired, marker)
actual := make([]string, 0, len(markers))
presentMarkers := make(map[string]bool, len(markers))
for _, rule := range projected {
if slices.Contains(markers, rule.Marker) {
actual = append(actual, rule.Marker)
presentMarkers[rule.Marker] = true
}
}
if slices.Equal(actual, desired) {
return nil
present := make([]string, 0, len(actual))
for _, marker := range markers {
if presentMarkers[marker] {
present = append(present, marker)
}
}
drifted := make(map[string]struct{}, len(desired))
for index := range desired {
if actual[index] != desired[index] {
drifted[actual[index]] = struct{}{}
drifted[desired[index]] = struct{}{}
for index, marker := range present {
if actual[index] != marker {
drifted[marker] = true
drifted[actual[index]] = true
}
}
return drifted
}
func InsertionPosition(snapshot filter.Snapshot, desiredMarkers []string, targetMarker string) (int64, bool) {
if !SupportsManagedOrder(snapshot.Scope.Provider) {
return 0, false
}
targetIndex := slices.Index(desiredMarkers, targetMarker)
if targetIndex < 0 {
return 0, false
func InsertionPosition(snapshot filter.Snapshot, markers []string, target string) *int64 {
if snapshot.Scope.Provider == filter.ProviderFirewalld {
return nil
}
targetIndex := slices.Index(markers, target)
for index := targetIndex - 1; index >= 0; index-- {
if _, position, exists := ObservedByMarker(snapshot, desiredMarkers[index]); exists {
return int64(position + 1), true
for _, rule := range snapshot.Rules {
if rule.Marker == markers[index] && rule.Locator.Position != nil {
position := int64(*rule.Locator.Position + 1)
return &position
}
}
}
for index := targetIndex + 1; index < len(desiredMarkers); index++ {
if _, position, exists := ObservedByMarker(snapshot, desiredMarkers[index]); exists {
return int64(position), true
if targetIndex >= 0 {
for _, marker := range markers[targetIndex+1:] {
for _, rule := range snapshot.Rules {
if rule.Marker == marker && rule.Locator.Position != nil {
position := int64(*rule.Locator.Position)
return &position
}
}
}
}
return 0, false
return nil
}
func NextManagedOrderChange(snapshot filter.Snapshot, desiredMarkers []string) (string, int, bool, error) {
expected := make(map[string]struct{}, len(desiredMarkers))
for _, marker := range desiredMarkers {
expected[marker] = struct{}{}
func DeleteChange(snapshot filter.Snapshot, previous filter.ObservedRule, desired filter.DesiredRule) (filter.DesiredChange, error) {
beforeKey, err := filter.RuleKey(previous.Rule)
if err != nil {
return filter.DesiredChange{}, err
}
actual := make([]string, 0, len(desiredMarkers))
positions := make([]int, 0, len(desiredMarkers))
for index, observed := range snapshot.Rules {
if _, exists := expected[observed.Marker]; !exists {
matches := make([]filter.ObservedRule, 0, 1)
for _, current := range snapshot.Rules {
if previous.Marker != "" {
if current.Marker != previous.Marker {
continue
}
} else if current.Marker != "" || !filter.SameLocator(current.Locator, previous.Locator) {
continue
}
actual = append(actual, observed.Marker)
position := index + 1
if observed.Locator.Position != nil {
position = *observed.Locator.Position
}
positions = append(positions, position)
}
if len(actual) != len(desiredMarkers) {
return "", 0, false, filter.ErrRuleStale
}
for index := range desiredMarkers {
if actual[index] != desiredMarkers[index] {
return desiredMarkers[index], positions[index], false, nil
key, err := filter.RuleKey(current.Rule)
if err == nil && key == beforeKey {
matches = append(matches, current)
}
}
return "", 0, true, nil
}
func ObservedByMarker(snapshot filter.Snapshot, marker string) (filter.ObservedRule, int, bool) {
for index, observed := range snapshot.Rules {
if observed.Marker == marker {
position := index + 1
if observed.Locator.Position != nil {
position = *observed.Locator.Position
}
return observed, position, true
}
if len(matches) != 1 {
return filter.DesiredChange{}, filter.ErrRuleStale
}
return filter.ObservedRule{}, 0, false
current := matches[0]
if err := filter.GuardMutation(current); err != nil {
return filter.DesiredChange{}, err
}
before := ObservedRule(current)
if before.UUID == "" {
before.UUID = desired.Rule.UUID
}
return filter.DesiredChange{Operation: filter.ChangeDelete, Before: &before, Locator: &current.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil
}
func ObservedRule(observed filter.ObservedRule) filter.FirewallRule {

View File

@@ -18626,7 +18626,10 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18643,11 +18646,9 @@ const docTemplate = `{
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"mode"
],
"formatEN": "batch update Docker port guard policies [mode]",
"formatZH": "批量更新 Docker 端口防护策略 [mode]",
"bodyKeys": [],
"formatEN": "batch update Docker port guard policies",
"formatZH": "批量更新 Docker 端口防护策略",
"paramKeys": []
}
}
@@ -18670,7 +18671,10 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18885,7 +18889,10 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18996,6 +19003,7 @@ const docTemplate = `{
"consumes": [
"application/json"
],
"description": "Creation and import return a taskID immediately; validation and execution results are written to the task log.",
"parameters": [
{
"description": "request",
@@ -19035,7 +19043,7 @@ const docTemplate = `{
"Timestamp": []
}
],
"summary": "Create unified firewall v2 rules",
"summary": "Queue firewall rule creation",
"tags": [
"Firewall"
],
@@ -19048,7 +19056,7 @@ const docTemplate = `{
}
}
},
"/hosts/firewall/rules/check": {
"/hosts/firewall/rules/adopt": {
"post": {
"consumes": [
"application/json"
@@ -19060,16 +19068,13 @@ const docTemplate = `{
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallRuleCheck"
"$ref": "#/definitions/dto.FirewallRuleAdopt"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FirewallRuleCheckResponse"
}
"description": "OK"
},
"400": {
"description": "Bad Request",
@@ -19086,10 +19091,17 @@ const docTemplate = `{
"Timestamp": []
}
],
"summary": "Check unified firewall v2 rules for duplicates and conflicts",
"summary": "Adopt an external firewall rule",
"tags": [
"Firewall"
]
],
"x-panel-log": {
"bodyKeys": [],
"paramKeys": [],
"BeforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
}
}
},
"/hosts/firewall/rules/delete": {
@@ -35653,19 +35665,26 @@ const docTemplate = `{
],
"type": "object"
},
"dto.DockerPortGuardPolicyBatch": {
"dto.DockerPortGuardPolicy": {
"properties": {
"description": {
"maxLength": 256,
"type": "string"
},
"endpoints": {
"items": {
"$ref": "#/definitions/dto.DockerPortGuardEndpointIdentity"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
"family": {
"enum": [
"ipv4",
"ipv6"
],
"type": "string"
},
"hostIP": {
"maxLength": 45,
"type": "string"
},
"hostPort": {
"minimum": 1,
"type": "integer"
},
"mode": {
"enum": [
@@ -35675,18 +35694,41 @@ const docTemplate = `{
],
"type": "string"
},
"protocol": {
"enum": [
"tcp",
"udp"
],
"type": "string"
},
"sources": {
"items": {
"type": "string"
},
"maxItems": 256,
"type": "array"
}
},
"required": [
"endpoints",
"family",
"hostIP",
"hostPort",
"mode",
"sources"
"protocol"
],
"type": "object"
},
"dto.DockerPortGuardPolicyBatch": {
"properties": {
"policies": {
"items": {
"$ref": "#/definitions/dto.DockerPortGuardPolicy"
},
"minItems": 1,
"type": "array"
}
},
"required": [
"policies"
],
"type": "object"
},
@@ -35696,7 +35738,6 @@ const docTemplate = `{
"items": {
"type": "string"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36094,84 +36135,21 @@ const docTemplate = `{
],
"type": "object"
},
"dto.FirewallRuleCheck": {
"properties": {
"items": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleCheckItem"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
},
"dto.FirewallRuleAdopt": {
"type": "object",
"required": [
"items"
"scope",
"instanceKey"
],
"type": "object"
},
"dto.FirewallRuleCheckItem": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.FirewallRule"
"scope": {
"$ref": "#/definitions/filter.Scope"
},
"uuid": {
"type": "string"
"instanceKey": {
"type": "string",
"maxLength": 128
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallRuleCheckResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleCheckResult"
},
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleCheckResult": {
"properties": {
"allowedActions": {
"items": {
"$ref": "#/definitions/filter.CheckAction"
},
"type": "array"
},
"candidates": {
"items": {
"$ref": "#/definitions/filter.ObservedRule"
},
"type": "array"
},
"checkFlag": {
"type": "string"
},
"classification": {
"$ref": "#/definitions/filter.CheckClassification"
},
"decision": {
"$ref": "#/definitions/filter.CheckDecision"
},
"existingRuleUUID": {
"type": "string"
},
"reason": {
"type": "string"
},
"requestedRule": {
"$ref": "#/definitions/filter.FirewallRule"
},
"requestedRuleKey": {
"type": "string"
}
},
"type": "object"
}
},
"dto.FirewallRuleCreate": {
"properties": {
@@ -36179,7 +36157,6 @@ const docTemplate = `{
"items": {
"$ref": "#/definitions/dto.FirewallRuleCreateItem"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36208,15 +36185,6 @@ const docTemplate = `{
},
"dto.FirewallRuleCreateItem": {
"properties": {
"action": {
"$ref": "#/definitions/filter.CheckAction"
},
"adoptInstanceKey": {
"type": "string"
},
"checkFlag": {
"type": "string"
},
"rule": {
"$ref": "#/definitions/filter.FirewallRule"
},
@@ -36247,11 +36215,17 @@ const docTemplate = `{
"failed": {
"type": "integer"
},
"queued": {
"type": "boolean"
},
"skipped": {
"type": "integer"
},
"succeeded": {
"type": "integer"
},
"taskID": {
"type": "string"
}
},
"type": "object"
@@ -36262,7 +36236,6 @@ const docTemplate = `{
"items": {
"type": "string"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36529,10 +36502,14 @@ const docTemplate = `{
"items": {
"$ref": "#/definitions/dto.ForwardRuleOperation"
},
"type": "array"
"type": "array",
"minItems": 1
}
},
"type": "object"
"type": "object",
"required": [
"rules"
]
},
"dto.ForwardRuleOperation": {
"properties": {
@@ -41198,59 +41175,6 @@ const docTemplate = `{
"ActionReject"
]
},
"filter.CheckAction": {
"enum": [
"create",
"create_anyway",
"adopt",
"select_adopt",
"cancel"
],
"type": "string",
"x-enum-varnames": [
"CheckActionCreate",
"CheckActionCreateAnyway",
"CheckActionAdopt",
"CheckActionSelectAdopt",
"CheckActionCancel"
]
},
"filter.CheckClassification": {
"enum": [
"none",
"exact_managed",
"exact_external",
"covered",
"conflict",
"unsupported",
"protected"
],
"type": "string",
"x-enum-varnames": [
"CheckClassificationNone",
"CheckClassificationExactManaged",
"CheckClassificationExactExternal",
"CheckClassificationCovered",
"CheckClassificationConflict",
"CheckClassificationUnsupported",
"CheckClassificationProtected"
]
},
"filter.CheckDecision": {
"enum": [
"ready",
"confirmation_required",
"blocked",
"no_change"
],
"type": "string",
"x-enum-varnames": [
"CheckDecisionReady",
"CheckDecisionConfirmationRequired",
"CheckDecisionBlocked",
"CheckDecisionNoChange"
]
},
"filter.DesiredRule": {
"properties": {
"marker": {

View File

@@ -18622,7 +18622,10 @@
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18639,11 +18642,9 @@
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"mode"
],
"formatEN": "batch update Docker port guard policies [mode]",
"formatZH": "批量更新 Docker 端口防护策略 [mode]",
"bodyKeys": [],
"formatEN": "batch update Docker port guard policies",
"formatZH": "批量更新 Docker 端口防护策略",
"paramKeys": []
}
}
@@ -18666,7 +18667,10 @@
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18881,7 +18885,10 @@
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
}
},
"security": [
@@ -18992,6 +18999,7 @@
"consumes": [
"application/json"
],
"description": "Creation and import return a taskID immediately; validation and execution results are written to the task log.",
"parameters": [
{
"description": "request",
@@ -19031,7 +19039,7 @@
"Timestamp": []
}
],
"summary": "Create unified firewall v2 rules",
"summary": "Queue firewall rule creation",
"tags": [
"Firewall"
],
@@ -19044,7 +19052,7 @@
}
}
},
"/hosts/firewall/rules/check": {
"/hosts/firewall/rules/adopt": {
"post": {
"consumes": [
"application/json"
@@ -19056,16 +19064,13 @@
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallRuleCheck"
"$ref": "#/definitions/dto.FirewallRuleAdopt"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FirewallRuleCheckResponse"
}
"description": "OK"
},
"400": {
"description": "Bad Request",
@@ -19082,10 +19087,17 @@
"Timestamp": []
}
],
"summary": "Check unified firewall v2 rules for duplicates and conflicts",
"summary": "Adopt an external firewall rule",
"tags": [
"Firewall"
]
],
"x-panel-log": {
"bodyKeys": [],
"paramKeys": [],
"BeforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
}
}
},
"/hosts/firewall/rules/delete": {
@@ -35649,19 +35661,26 @@
],
"type": "object"
},
"dto.DockerPortGuardPolicyBatch": {
"dto.DockerPortGuardPolicy": {
"properties": {
"description": {
"maxLength": 256,
"type": "string"
},
"endpoints": {
"items": {
"$ref": "#/definitions/dto.DockerPortGuardEndpointIdentity"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
"family": {
"enum": [
"ipv4",
"ipv6"
],
"type": "string"
},
"hostIP": {
"maxLength": 45,
"type": "string"
},
"hostPort": {
"minimum": 1,
"type": "integer"
},
"mode": {
"enum": [
@@ -35671,18 +35690,41 @@
],
"type": "string"
},
"protocol": {
"enum": [
"tcp",
"udp"
],
"type": "string"
},
"sources": {
"items": {
"type": "string"
},
"maxItems": 256,
"type": "array"
}
},
"required": [
"endpoints",
"family",
"hostIP",
"hostPort",
"mode",
"sources"
"protocol"
],
"type": "object"
},
"dto.DockerPortGuardPolicyBatch": {
"properties": {
"policies": {
"items": {
"$ref": "#/definitions/dto.DockerPortGuardPolicy"
},
"minItems": 1,
"type": "array"
}
},
"required": [
"policies"
],
"type": "object"
},
@@ -35692,7 +35734,6 @@
"items": {
"type": "string"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36090,84 +36131,21 @@
],
"type": "object"
},
"dto.FirewallRuleCheck": {
"properties": {
"items": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleCheckItem"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
},
"dto.FirewallRuleAdopt": {
"type": "object",
"required": [
"items"
"scope",
"instanceKey"
],
"type": "object"
},
"dto.FirewallRuleCheckItem": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.FirewallRule"
"scope": {
"$ref": "#/definitions/filter.Scope"
},
"uuid": {
"type": "string"
"instanceKey": {
"type": "string",
"maxLength": 128
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallRuleCheckResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleCheckResult"
},
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleCheckResult": {
"properties": {
"allowedActions": {
"items": {
"$ref": "#/definitions/filter.CheckAction"
},
"type": "array"
},
"candidates": {
"items": {
"$ref": "#/definitions/filter.ObservedRule"
},
"type": "array"
},
"checkFlag": {
"type": "string"
},
"classification": {
"$ref": "#/definitions/filter.CheckClassification"
},
"decision": {
"$ref": "#/definitions/filter.CheckDecision"
},
"existingRuleUUID": {
"type": "string"
},
"reason": {
"type": "string"
},
"requestedRule": {
"$ref": "#/definitions/filter.FirewallRule"
},
"requestedRuleKey": {
"type": "string"
}
},
"type": "object"
}
},
"dto.FirewallRuleCreate": {
"properties": {
@@ -36175,7 +36153,6 @@
"items": {
"$ref": "#/definitions/dto.FirewallRuleCreateItem"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36204,15 +36181,6 @@
},
"dto.FirewallRuleCreateItem": {
"properties": {
"action": {
"$ref": "#/definitions/filter.CheckAction"
},
"adoptInstanceKey": {
"type": "string"
},
"checkFlag": {
"type": "string"
},
"rule": {
"$ref": "#/definitions/filter.FirewallRule"
},
@@ -36243,11 +36211,17 @@
"failed": {
"type": "integer"
},
"queued": {
"type": "boolean"
},
"skipped": {
"type": "integer"
},
"succeeded": {
"type": "integer"
},
"taskID": {
"type": "string"
}
},
"type": "object"
@@ -36258,7 +36232,6 @@
"items": {
"type": "string"
},
"maxItems": 256,
"minItems": 1,
"type": "array"
}
@@ -36525,10 +36498,14 @@
"items": {
"$ref": "#/definitions/dto.ForwardRuleOperation"
},
"type": "array"
"type": "array",
"minItems": 1
}
},
"type": "object"
"type": "object",
"required": [
"rules"
]
},
"dto.ForwardRuleOperation": {
"properties": {
@@ -41194,59 +41171,6 @@
"ActionReject"
]
},
"filter.CheckAction": {
"enum": [
"create",
"create_anyway",
"adopt",
"select_adopt",
"cancel"
],
"type": "string",
"x-enum-varnames": [
"CheckActionCreate",
"CheckActionCreateAnyway",
"CheckActionAdopt",
"CheckActionSelectAdopt",
"CheckActionCancel"
]
},
"filter.CheckClassification": {
"enum": [
"none",
"exact_managed",
"exact_external",
"covered",
"conflict",
"unsupported",
"protected"
],
"type": "string",
"x-enum-varnames": [
"CheckClassificationNone",
"CheckClassificationExactManaged",
"CheckClassificationExactExternal",
"CheckClassificationCovered",
"CheckClassificationConflict",
"CheckClassificationUnsupported",
"CheckClassificationProtected"
]
},
"filter.CheckDecision": {
"enum": [
"ready",
"confirmation_required",
"blocked",
"no_change"
],
"type": "string",
"x-enum-varnames": [
"CheckDecisionReady",
"CheckDecisionConfirmationRequired",
"CheckDecisionBlocked",
"CheckDecisionNoChange"
]
},
"filter.DesiredRule": {
"properties": {
"marker": {

View File

@@ -209,24 +209,6 @@ export namespace Firewall {
withDockerRestart?: boolean;
}
export type CheckDecision = 'ready' | 'confirmation_required' | 'blocked' | 'no_change';
export type CheckClassification =
'none' | 'exact_managed' | 'exact_external' | 'conflict' | 'unsupported' | 'protected';
export type CheckAction = 'create' | 'adopt' | 'select_adopt' | 'cancel';
export type ApplicableCheckAction = Exclude<CheckAction, 'cancel'>;
export interface RuleCheckResult {
decision: CheckDecision;
classification: CheckClassification;
reason: string;
requestedRule: Rule;
requestedRuleKey: string;
existingRuleUUID?: string;
candidates?: ObservedRule[];
allowedActions?: CheckAction[];
checkFlag: string;
}
export interface InventoryRequest extends ReqPage {
scopes: Scope[];
all?: boolean;
@@ -244,25 +226,13 @@ export namespace Firewall {
permanent: boolean;
}
export interface CheckItem {
uuid?: string;
rule: Rule;
adoptLocator?: Locator;
}
export interface CheckRequest {
items: CheckItem[];
}
export interface CheckResponse {
items: RuleCheckResult[];
export interface AdoptRequest {
scope: Scope;
instanceKey: string;
}
export interface CreateItem {
rule: Rule;
checkFlag: string;
action: ApplicableCheckAction;
adoptInstanceKey?: string;
sourceKind?: 'user' | 'panel' | 'security' | 'imported';
sourceID?: string;
}
@@ -272,6 +242,8 @@ export namespace Firewall {
}
export interface CreateResponse {
taskID?: string;
queued?: boolean;
succeeded: number;
failed: number;
skipped: number;
@@ -444,10 +416,7 @@ export namespace Firewall {
description: string;
}
export interface DockerGuardPolicyBatch {
endpoints: DockerGuardEndpointIdentity[];
mode: 'deny_sources' | 'allow_sources' | 'deny_all';
sources: string[];
description: string;
policies: DockerGuardPolicy[];
}
export interface DockerGuardPolicyBatchDelete {
uuids: string[];

View File

@@ -16,7 +16,9 @@ export const operateFire = (operation: string, withDockerRestart: boolean) =>
http.post('/hosts/firewall/operate', { operation, withDockerRestart }, TimeoutEnum.T_10M);
export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; forceDelete?: boolean }) =>
http.post('/hosts/firewall/forward/operate', request, TimeoutEnum.T_40S);
http.postWithConfig<Firewall.FilterChainOperationResult>('/hosts/firewall/forward/operate', request, {
skipErrorMessage: true,
});
export const enableForwarding = (taskID?: string) =>
http.post<Firewall.FilterChainOperationResult>(
@@ -44,8 +46,8 @@ export const loadFirewallNativeDetail = (request: Firewall.NativeDetailRequest)
return http.post<string>('/hosts/firewall/rules/native/detail', request, TimeoutEnum.T_40S);
};
export const checkFirewallRules = (request: Firewall.CheckRequest) => {
return http.post<Firewall.CheckResponse>('/hosts/firewall/rules/check', request, TimeoutEnum.T_3M);
export const adoptFirewallRule = (request: Firewall.AdoptRequest) => {
return http.post('/hosts/firewall/rules/adopt', request, TimeoutEnum.T_3M);
};
export const createFirewallRules = (request: Firewall.CreateRequest) => {
@@ -89,13 +91,12 @@ export const operateDockerPortGuard = (operation: 'initialize' | 'bind' | 'unbin
);
export const upsertDockerPortGuardPolicies = (request: Firewall.DockerGuardPolicyBatch) =>
http.postWithConfig('/hosts/firewall/docker/policies/batch', request, {
timeout: TimeoutEnum.T_60S,
http.postWithConfig<Firewall.FilterChainOperationResult>('/hosts/firewall/docker/policies/batch', request, {
skipErrorMessage: true,
});
export const deleteDockerPortGuardPolicies = (request: Firewall.DockerGuardPolicyBatchDelete) =>
http.postWithConfig('/hosts/firewall/docker/policies/delete/batch', request, {
http.postWithConfig<Firewall.FilterChainOperationResult>('/hosts/firewall/docker/policies/delete/batch', request, {
timeout: TimeoutEnum.T_60S,
skipErrorMessage: true,
});

View File

@@ -4097,20 +4097,12 @@ const message = {
drifted: 'Abnormal',
},
ruleTargetRequired: 'Enter at least one IP address or port',
batchRuleLimit: 'A maximum of {0} rules can be created at a time',
resolution_adopt: 'Take over management',
plan_duplicate_rules:
'Rules with identical conditions and actions cannot be adopted. Manually delete duplicate rules and retry.',
adoptRuleConfirm: 'After takeover, 1Panel can maintain and delete this existing rule. Continue?',
plan_exact_rule_conflict: 'A rule with identical matching conditions has an opposing allow or deny action.',
allRulesAlreadyExist: 'All {0} checked rules already exist. There are no new rules to create.',
ruleCheckResult: 'Rule check results',
ruleCheckStatus_creatable: 'Creatable',
ruleCheckStatus_existing: 'Exists',
ruleCheckStatus_error: 'Error',
ruleCheckExistingHelper: 'An identical rule already exists and will be skipped.',
ruleCheckReadyHelper: 'The check passed. This rule can be created.',
ruleCheckBlockedHelper: 'Rules with errors cannot be submitted. Go back, edit them, and check again.',
plan_managed_rule_drifted:
'The 1Panel management record differs from the live firewall rule. Check the system rule.',
plan_opaque_rule_in_target_scope:
@@ -4172,7 +4164,6 @@ const message = {
managedOrderDiffers: 'The managed rule order differs from the database sequence.',
managedOnlyInTarget: 'The managed rule exists only in the target firewall.',
managedRuntimeCannotRemove: 'The managed runtime rule cannot be removed safely.',
managedOrderBlocked: 'Managed rules cannot be reordered across external, unrecognized, or protected rules.',
missingFromTarget: 'The rule is missing from the target firewall.',
targetDiffers: 'The target rule differs from the database policy.',
alreadyExistsInTarget: 'The rule already exists in the target firewall.',
@@ -4283,8 +4274,6 @@ const message = {
forwardPortHelper: 'Supports port ranges, e.g. 8080-8089',
forwardInboundInterface: 'Forward Inbound Network Interface',
exportHelper: 'About to export {0} firewall rules. Continue?',
importSuccess: 'Successfully imported {0} rules',
importPartialSuccess: 'Import completed: {0} succeeded, {1} failed',
basicStatus: 'The current firewall is unbound. Bind it first.',
initMsg: 'About to initialize {0}, continue?',
initDirectBackendConflictMsg:
@@ -4317,8 +4306,7 @@ const message = {
deleteWildcardRuleConfirm:
'This rule allows {0} to access {1}. Deleting it may affect access to multiple services. Continue?',
deleteRiskRulesConfirm: 'Will delete {0} rules. {1} allow rules may affect service access. Continue?',
editRuleConfirm:
'The following fields will change: {0}. The rule will be applied and verified immediately. Continue?',
editRuleConfirm: 'Are you sure you want to modify this rule?',
},
runtime: {
importEnv: 'Import Environment Variables',

View File

@@ -4143,7 +4143,6 @@ const message = {
drifted: 'Desviada',
},
ruleTargetRequired: 'Introduce al menos una dirección IP o un puerto',
batchRuleLimit: 'Se pueden crear como máximo {0} reglas a la vez',
resolution_adopt: 'Asumir la gestión',
plan_duplicate_rules:
'No se pueden gestionar reglas duplicadas con las mismas condiciones y acciones. Elimine manualmente los duplicados y vuelva a intentarlo.',
@@ -4151,14 +4150,6 @@ const message = {
plan_exact_rule_conflict:
'Ya existe una regla con las mismas condiciones, pero con una acción opuesta de permitir o denegar.',
allRulesAlreadyExist: 'Las {0} reglas comprobadas ya existen. No hay reglas nuevas que crear.',
ruleCheckResult: 'Resultados de la comprobación de reglas',
ruleCheckStatus_creatable: 'Se puede crear',
ruleCheckStatus_existing: 'Ya existe',
ruleCheckStatus_error: 'Error',
ruleCheckExistingHelper: 'Ya existe una regla idéntica y se omitirá.',
ruleCheckReadyHelper: 'La comprobación se superó. Esta regla se puede crear.',
ruleCheckBlockedHelper:
'Las reglas con errores no se pueden enviar. Vuelve, modifícalas y compruébalas de nuevo.',
plan_managed_rule_drifted:
'La regla gestionada no coincide con el firewall activo. Resuelve primero la diferencia.',
plan_opaque_rule_in_target_scope:
@@ -4222,8 +4213,6 @@ const message = {
managedOrderDiffers: 'El orden de las reglas gestionadas difiere del orden de la base de datos.',
managedOnlyInTarget: 'La regla gestionada solo existe en el firewall de destino.',
managedRuntimeCannotRemove: 'La regla activa gestionada no se puede eliminar de forma segura.',
managedOrderBlocked:
'Las reglas gestionadas no se pueden reordenar por encima de reglas externas, no reconocidas o protegidas.',
missingFromTarget: 'La regla no existe en el firewall de destino.',
targetDiffers: 'La regla de destino difiere de la política de la base de datos.',
alreadyExistsInTarget: 'La regla ya existe en el firewall de destino.',
@@ -4337,8 +4326,6 @@ const message = {
forwardPortHelper: 'Admite rangos de puertos, ej.: 8080-8089',
forwardInboundInterface: 'Interfaz de Red de Entrada para Reenvío',
exportHelper: 'A punto de exportar {0} reglas de firewall. ¿Continuar?',
importSuccess: 'Se importaron correctamente {0} reglas',
importPartialSuccess: 'Importación completada: {0} correctas, {1} fallidas',
basicStatus: 'El firewall actual no está vinculado. Vincúlelo primero.',
initMsg: 'A punto de inicializar {0}, ¿continuar?',
initDirectBackendConflictMsg:
@@ -4373,8 +4360,7 @@ const message = {
'Esta regla permite que {0} acceda a {1}. Eliminarla puede afectar al acceso a varios servicios. ¿Continuar?',
deleteRiskRulesConfirm:
'Se eliminarán {0} reglas. {1} reglas de acceso pueden afectar al acceso a servicios. ¿Continuar?',
editRuleConfirm:
'Se modificarán los siguientes campos: {0}. La regla se aplicará y verificará inmediatamente. ¿Continuar?',
editRuleConfirm: '¿Confirma que desea modificar esta regla?',
},
runtime: {
importEnv: 'Importar variables de entorno',

View File

@@ -4056,20 +4056,12 @@ const message = {
protectedHelper: 'قانون حیاتی سیستم که برای جلوگیری از اختلال در دسترسی قابل ویرایش یا حذف نیست.',
stateShort: { managed: 'پنل', adopted: 'مدیریت', external: 'خارجی', protected: 'محافظت', drifted: 'ناهمگام' },
ruleTargetRequired: 'حداقل یک نشانی IP یا درگاه وارد کنید',
batchRuleLimit: 'در هر بار حداکثر می‌توان {0} قانون ایجاد کرد',
resolution_adopt: 'واگذاری مدیریت',
plan_duplicate_rules:
'قوانین تکراری با شرایط و اقدامات یکسان قابل پذیرش برای مدیریت نیستند. قوانین تکراری را به‌صورت دستی حذف کرده و دوباره تلاش کنید.',
adoptRuleConfirm: 'پس از واگذاری، 1Panel می‌تواند این قانون موجود را نگهداری و حذف کند. ادامه می‌دهید؟',
plan_exact_rule_conflict: 'قانونی با شرایط تطبیق یکسان، اما با اقدام مخالفِ اجازه یا رد وجود دارد.',
allRulesAlreadyExist: 'هر {0} قانون بررسی‌شده از قبل وجود دارند. قانون جدیدی برای ایجاد وجود ندارد.',
ruleCheckResult: 'نتایج بررسی قوانین',
ruleCheckStatus_creatable: 'قابل ایجاد',
ruleCheckStatus_existing: 'موجود',
ruleCheckStatus_error: 'خطا',
ruleCheckExistingHelper: 'یک قانون یکسان از قبل وجود دارد و نادیده گرفته خواهد شد.',
ruleCheckReadyHelper: 'بررسی با موفقیت انجام شد. این قانون قابل ایجاد است.',
ruleCheckBlockedHelper: 'قوانین دارای خطا قابل ارسال نیستند. بازگردید، آن‌ها را ویرایش و دوباره بررسی کنید.',
plan_managed_rule_drifted: 'قانون مدیریت‌شده با دیوارهٔ آتش فعال یکسان نیست. ابتدا ناهمگامی را رفع کنید.',
plan_opaque_rule_in_target_scope: 'در محدودهٔ هدف قانونی وجود دارد که تحلیل امن آن ممکن نیست. عملیات متوقف شد.',
plan_runtime_permanent_mismatch: 'پیکربندی فعال و دائمی دیوارهٔ آتش متفاوت است. ابتدا آن‌ها را همگام کنید.',
@@ -4128,8 +4120,6 @@ const message = {
managedOrderDiffers: 'ترتیب قوانین مدیریت‌شده با ترتیب پایگاه داده متفاوت است.',
managedOnlyInTarget: 'قانون مدیریت‌شده فقط در فایروال مقصد وجود دارد.',
managedRuntimeCannotRemove: 'قانون فعال مدیریت‌شده را نمی‌توان با ایمنی حذف کرد.',
managedOrderBlocked:
'قوانین مدیریت‌شده را نمی‌توان از میان قوانین خارجی، ناشناخته یا محافظت‌شده جابه‌جا کرد.',
missingFromTarget: 'این قانون در فایروال مقصد وجود ندارد.',
targetDiffers: 'قانون مقصد با سیاست پایگاه داده متفاوت است.',
alreadyExistsInTarget: 'این قانون از قبل در فایروال مقصد وجود دارد.',
@@ -4241,8 +4231,6 @@ const message = {
forwardPortHelper: 'پشتیبانی از محدوده پورت، مثلاً 8080-8089',
forwardInboundInterface: 'رابط شبکه ورودی انتقال',
exportHelper: 'در حال خروجی {0} قوانین دیواره آتش. ادامه می‌دهید؟',
importSuccess: 'واردات {0} قانون با موفقیت انجام شد',
importPartialSuccess: 'واردات انجام شد: {0} موفق، {1} ناموفق',
basicStatus: 'دیواره آتش فعلی متصل نیست. ابتدا آن را متصل کنید!',
initMsg: 'در حال راه‌اندازی {0}، ادامه می‌دهید؟',
initDirectBackendConflictMsg:
@@ -4276,7 +4264,7 @@ const message = {
'این قانون به {0} اجازه دسترسی به {1} را می‌دهد. حذف آن ممکن است دسترسی به چندین سرویس را تحت تأثیر قرار دهد. ادامه می‌دهید؟',
deleteRiskRulesConfirm:
'{0} قانون حذف خواهند شد. {1} قانون مجاز ممکن است دسترسی به سرویس‌ها را تحت تأثیر قرار دهد. ادامه می‌دهید؟',
editRuleConfirm: 'فیلدهای زیر تغییر می‌کنند: {0}. قانون بلافاصله اعمال و بررسی می‌شود. ادامه می‌دهید؟',
editRuleConfirm: 'آیا از تغییر این قانون مطمئن هستید؟',
},
runtime: {
importEnv: 'وارد کردن متغیرهای محیطی',

View File

@@ -4081,20 +4081,12 @@ const message = {
protectedHelper: 'アクセスへの影響を防ぐため編集や削除ができない重要なシステムルールです',
stateShort: { managed: 'パネル', adopted: '管理済', external: '外部', protected: '保護', drifted: '差異' },
ruleTargetRequired: 'IP アドレスまたはポートを1つ以上入力してください',
batchRuleLimit: '一度に作成できるルールは最大 {0} 件です',
resolution_adopt: '管理対象にする',
plan_duplicate_rules:
'条件とアクションが同じルールが重複しているため管理対象に追加できません重複ルールを手動で削除してから再試行してください',
adoptRuleConfirm: '管理対象にすると1Panel がこの既存ルールの保守と削除を行えるようになります続行しますか',
plan_exact_rule_conflict: '一致条件が同じで許可拒否の動作が逆のルールが存在します',
allRulesAlreadyExist: '確認した {0} 件のルールはすべて既に存在します新しく作成するルールはありません',
ruleCheckResult: 'ルール確認結果',
ruleCheckStatus_creatable: '作成可能',
ruleCheckStatus_existing: '既存',
ruleCheckStatus_error: 'エラー',
ruleCheckExistingHelper: '同じルールが既に存在するため今回はスキップします',
ruleCheckReadyHelper: '確認に合格しましたこのルールは作成できます',
ruleCheckBlockedHelper: 'エラーのあるルールは送信できません戻って修正しもう一度確認してください',
plan_managed_rule_drifted:
'管理対象ルールが実際のファイアウォールと一致しません先に不整合を解消してください',
plan_opaque_rule_in_target_scope: '対象範囲に安全に解析できないルールがあります操作を停止しました',
@@ -4157,8 +4149,6 @@ const message = {
managedOrderDiffers: '管理対象ルールの順序がデータベースの順序と異なります',
managedOnlyInTarget: '管理対象ルールは対象ファイアウォールにのみ存在します',
managedRuntimeCannotRemove: '管理対象の実行中ルールを安全に削除できません',
managedOrderBlocked:
'外部認識不能または保護されたルールを越えて管理対象ルールを並べ替えることはできません',
missingFromTarget: '対象ファイアウォールにこのルールがありません',
targetDiffers: '対象ルールがデータベースポリシーと異なります',
alreadyExistsInTarget: '対象ファイアウォールにこのルールは既に存在します',
@@ -4268,8 +4258,6 @@ const message = {
forwardPortHelper: 'ポート範囲をサポートします: 8080-8089',
forwardInboundInterface: '転送入站ネットワークインターフェース',
exportHelper: '{0} 件のファイアウォールルールをエクスポートします続行しますか',
importSuccess: '{0} 件のルールを正常にインポートしました',
importPartialSuccess: 'インポート完了: {0} 件成功{1} 件失敗',
basicStatus: '現在のファイアウォールはバインドされていません先にバインドしてください',
initMsg: '{0} を初期化します続行しますか',
initDirectBackendConflictMsg:
@@ -4303,7 +4291,7 @@ const message = {
'このルールは {0} {1} へのアクセスを許可しています削除すると複数のサービスへのアクセスに影響する可能性があります続行しますか',
deleteRiskRulesConfirm:
'{0} 個のルールを削除しますそのうち {1} 個の許可ルールがサービスへのアクセスに影響する可能性があります続行しますか',
editRuleConfirm: '次の項目を変更します{0}ルールは直ちに適用され再検証されます続行しますか',
editRuleConfirm: 'このルールを変更してもよろしいですか',
},
runtime: {
importEnv: '環境変数をインポート',

View File

@@ -4007,7 +4007,6 @@ const message = {
protectedHelper: '접속 장애를 방지하기 위해 편집하거나 삭제할 수 없는 중요한 시스템 규칙입니다.',
stateShort: { managed: '패널', adopted: '관리', external: '외부', protected: '보호', drifted: '불일치' },
ruleTargetRequired: 'IP 주소 또는 포트를 하나 이상 입력하세요',
batchRuleLimit: '한 번에 최대 {0}개의 규칙을 생성할 수 있습니다',
resolution_adopt: '관리 대상으로 전환',
plan_duplicate_rules:
'조건과 동작이 동일한 중복 규칙은 관리 대상으로 추가할 수 없습니다. 중복 규칙을 수동으로 삭제한 후 다시 시도하세요.',
@@ -4015,13 +4014,6 @@ const message = {
'관리 대상으로 전환하면 1Panel이 이 기존 규칙을 유지하고 삭제할 수 있습니다. 계속하시겠습니까?',
plan_exact_rule_conflict: '일치 조건이 같지만 허용 또는 거부 동작이 반대인 규칙이 이미 있습니다.',
allRulesAlreadyExist: '확인한 규칙 {0}개가 모두 이미 존재합니다. 새로 생성할 규칙이 없습니다.',
ruleCheckResult: '규칙 검사 결과',
ruleCheckStatus_creatable: '생성 가능',
ruleCheckStatus_existing: '이미 존재함',
ruleCheckStatus_error: '오류',
ruleCheckExistingHelper: '동일한 규칙이 이미 존재하므로 이번 작업에서는 건너뜁니다.',
ruleCheckReadyHelper: '검사를 통과했습니다. 이 규칙을 생성할 수 있습니다.',
ruleCheckBlockedHelper: '오류가 있는 규칙은 제출할 수 없습니다. 돌아가서 수정한 후 다시 검사하세요.',
plan_managed_rule_drifted: '관리 규칙이 실제 방화벽과 일치하지 않습니다. 먼저 불일치를 해결하세요.',
plan_opaque_rule_in_target_scope: '대상 범위에 안전하게 분석할 수 없는 규칙이 있어 작업을 중지했습니다.',
plan_runtime_permanent_mismatch: '실행 중 구성과 영구 방화벽 구성이 다릅니다. 먼저 동기화하세요.',
@@ -4080,7 +4072,6 @@ const message = {
managedOrderDiffers: '관리 규칙 순서가 데이터베이스 순서와 다릅니다.',
managedOnlyInTarget: '관리 규칙이 대상 방화벽에만 존재합니다.',
managedRuntimeCannotRemove: '관리 중인 런타임 규칙을 안전하게 삭제할 수 없습니다.',
managedOrderBlocked: '외부, 인식할 수 없거나 보호된 규칙을 넘어 관리 규칙의 순서를 변경할 수 없습니다.',
missingFromTarget: '대상 방화벽에 이 규칙이 없습니다.',
targetDiffers: '대상 규칙이 데이터베이스 정책과 다릅니다.',
alreadyExistsInTarget: '대상 방화벽에 이 규칙이 이미 존재합니다.',
@@ -4189,8 +4180,6 @@ const message = {
forwardPortHelper: '포트 범위를 지원합니다, 예: 8080-8089',
forwardInboundInterface: '포워딩 인바운드 네트워크 인터페이스',
exportHelper: '{0}개의 방화벽 규칙을 내보내려고 합니다. 계속하시겠습니까?',
importSuccess: '{0}개의 규칙을 성공적으로 가져왔습니다',
importPartialSuccess: '가져오기 완료: 성공 {0}건, 실패 {1}건',
basicStatus: '현재 방화벽이 바인딩되지 않았습니다. 먼저 바인딩하세요!',
initMsg: '{0}을(를) 초기화하려고 합니다. 계속하시겠습니까?',
initDirectBackendConflictMsg:
@@ -4223,7 +4212,7 @@ const message = {
'이 규칙은 {0}에서 {1}에 접근하도록 허용합니다. 삭제하면 여러 서비스에 대한 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?',
deleteRiskRulesConfirm:
'{0}개의 규칙을 삭제합니다. 그중 {1}개의 허용 규칙이 서비스 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?',
editRuleConfirm: '다음 필드가 변경됩니다: {0}. 규칙이 즉시 적용되고 다시 검증됩니다. 계속하시겠습니까?',
editRuleConfirm: '이 규칙을 수정하시겠습니까?',
},
runtime: {
importEnv: '환경 변수 가져오기',

View File

@@ -3978,20 +3978,12 @@ const message = {
protectedHelper: 'ກົດສຳຄັນຂອງລະບົບທີ່ບໍ່ສາມາດແກ້ໄຂ ຫຼື ລຶບໄດ້ ເພື່ອຫຼີກລ້ຽງການຂັດຂວາງການເຂົ້າເຖິງ.',
stateShort: { managed: 'ແຜງ', adopted: 'ຈັດການ', external: 'ພາຍນອກ', protected: 'ປົກປ້ອງ', drifted: 'ບໍ່ກົງ' },
ruleTargetRequired: 'ກະລຸນາປ້ອນ IP ຫຼື ພອດຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
batchRuleLimit: 'ສາມາດສ້າງກົດໄດ້ສູງສຸດ {0} ລາຍການຕໍ່ຄັ້ງ',
resolution_adopt: 'ຮັບເຂົ້າຈັດການ',
plan_duplicate_rules:
'ບໍ່ສາມາດນຳກົດທີ່ມີເງື່ອນໄຂ ແລະ ການກະທຳຄືກັນເຂົ້າມາຈັດການໄດ້. ກະລຸນາລຶບກົດທີ່ຊ້ຳກັນດ້ວຍຕົນເອງ ແລ້ວລອງອີກຄັ້ງ.',
adoptRuleConfirm: 'ຫຼັງຈາກຮັບເຂົ້າແລ້ວ 1Panel ຈະສາມາດຮັກສາແລະລຶບກົດນີ້ໄດ້. ສືບຕໍ່ບໍ?',
plan_exact_rule_conflict: 'ມີກົດທີ່ມີເງື່ອນໄຂຄືກັນ ແຕ່ການອະນຸຍາດຫຼືປະຕິເສດກົງກັນຂ້າມ.',
allRulesAlreadyExist: 'ກົດທັງໝົດ {0} ລາຍການທີ່ກວດສອບມີຢູ່ແລ້ວ. ບໍ່ມີກົດໃໝ່ທີ່ຕ້ອງສ້າງ.',
ruleCheckResult: 'ຜົນການກວດສອບກົດ',
ruleCheckStatus_creatable: 'ສາມາດສ້າງໄດ້',
ruleCheckStatus_existing: 'ມີຢູ່ແລ້ວ',
ruleCheckStatus_error: 'ຂໍ້ຜິດພາດ',
ruleCheckExistingHelper: 'ມີກົດທີ່ຄືກັນຢູ່ແລ້ວ ແລະ ຈະຖືກຂ້າມ.',
ruleCheckReadyHelper: 'ການກວດສອບຜ່ານ. ສາມາດສ້າງກົດນີ້ໄດ້.',
ruleCheckBlockedHelper: 'ບໍ່ສາມາດສົ່ງກົດທີ່ມີຂໍ້ຜິດພາດໄດ້. ກັບໄປແກ້ໄຂ ແລະກວດສອບໃໝ່.',
plan_managed_rule_drifted: 'ກົດທີ່ຈັດການບໍ່ກົງກັບໄຟວໍທີ່ໃຊ້ງານ. ກະລຸນາແກ້ໄຂກ່ອນ.',
plan_opaque_rule_in_target_scope: 'ມີກົດໃນຂອບເຂດທີ່ບໍ່ສາມາດວິເຄາະໄດ້ຢ່າງປອດໄພ. ການດຳເນີນງານຖືກຢຸດ.',
plan_runtime_permanent_mismatch: 'ຄ່າໄຟວໍທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນ. ກະລຸນາຊິງກ່ອນ.',
@@ -4049,7 +4041,6 @@ const message = {
managedOrderDiffers: 'ລຳດັບກົດທີ່ຈັດການແຕກຕ່າງຈາກລຳດັບໃນຖານຂໍ້ມູນ.',
managedOnlyInTarget: 'ກົດທີ່ຈັດການມີຢູ່ສະເພາະໃນໄຟວໍເປົ້າໝາຍ.',
managedRuntimeCannotRemove: 'ບໍ່ສາມາດລຶບກົດທີ່ກຳລັງໃຊ້ງານໄດ້ຢ່າງປອດໄພ.',
managedOrderBlocked: 'ບໍ່ສາມາດຈັດລຳດັບກົດທີ່ຈັດການຂ້າມກົດພາຍນອກ, ກົດທີ່ບໍ່ຮູ້ຈັກ ຫຼືກົດທີ່ຖືກປົກປ້ອງ.',
missingFromTarget: 'ບໍ່ມີກົດນີ້ໃນໄຟວໍເປົ້າໝາຍ.',
targetDiffers: 'ກົດເປົ້າໝາຍແຕກຕ່າງຈາກນະໂຍບາຍໃນຖານຂໍ້ມູນ.',
alreadyExistsInTarget: 'ກົດນີ້ມີຢູ່ແລ້ວໃນໄຟວໍເປົ້າໝາຍ.',
@@ -4154,8 +4145,6 @@ const message = {
forwardPortHelper: 'ຮອງຮັບຊ່ວງພອດ ຕົວຢ່າງ: 8080-8089',
forwardInboundInterface: 'ອິນເຕີເຟດເຄືອຂ່າຍຂາເຂົ້າທີ່ສົ່ງຕໍ່',
exportHelper: 'ກຳລັງຈະສົ່ງອອກກົດລະບຽບໄຟວໍ {0} ລາຍການ. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
importSuccess: 'ນຳເຂົ້າ {0} ກົດລະບຽບສຳເລັດແລ້ວ',
importPartialSuccess: 'ນຳເຂົ້າສຳເລັດ: ສຳເລັດ {0}, ລົ້ມເຫຼວ {1}',
basicStatus: 'ໄຟວໍປັດຈຸບັນຍັງບໍ່ໄດ້ຜູກມັດ. ກະລຸນາຜູກມັດກ່ອນ!',
initMsg: 'ກຳລັງຈະເລີ່ມຕົ້ນ {0}, ຕ້ອງການຕໍ່ຫຼືບໍ່?',
initDirectBackendConflictMsg:
@@ -4187,7 +4176,7 @@ const message = {
deleteWildcardRuleConfirm:
'ກົດນີ້ອະນຸຍາດໃຫ້ {0} ເຂົ້າເຖິງ {1}. ການລຶບອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງຫຼາຍບໍລິການ. ສືບຕໍ່ບໍ?',
deleteRiskRulesConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. {1} ກົດອະນຸຍາດອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງບໍລິການ. ສືບຕໍ່ບໍ?',
editRuleConfirm: 'ຈະປ່ຽນຟິວຕໍ່ໄປນີ້: {0}. ກົດຈະຖືກນຳໃຊ້ແລະກວດສອບທັນທີ. ສືບຕໍ່ບໍ?',
editRuleConfirm: 'ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການແກ້ໄຂກົດນີ້?',
},
runtime: {
importEnv: 'ນຳເຂົ້າຕົວແປສະພາບແວດລ້ອມ',

View File

@@ -4163,7 +4163,6 @@ const message = {
drifted: 'Berbeza',
},
ruleTargetRequired: 'Masukkan sekurang-kurangnya satu alamat IP atau port',
batchRuleLimit: 'Maksimum {0} peraturan boleh dibuat pada satu masa',
resolution_adopt: 'Ambil alih pengurusan',
plan_duplicate_rules:
'Peraturan pendua dengan syarat dan tindakan yang sama tidak boleh diambil alih untuk diurus. Padam peraturan pendua secara manual dan cuba lagi.',
@@ -4172,13 +4171,6 @@ const message = {
plan_exact_rule_conflict:
'Peraturan dengan syarat padanan yang sama mempunyai tindakan benarkan atau sekat yang bertentangan.',
allRulesAlreadyExist: 'Kesemua {0} peraturan yang diperiksa sudah wujud. Tiada peraturan baharu untuk dicipta.',
ruleCheckResult: 'Keputusan semakan peraturan',
ruleCheckStatus_creatable: 'Boleh dicipta',
ruleCheckStatus_existing: 'Sudah wujud',
ruleCheckStatus_error: 'Ralat',
ruleCheckExistingHelper: 'Peraturan yang sama sudah wujud dan akan dilangkau.',
ruleCheckReadyHelper: 'Semakan lulus. Peraturan ini boleh dicipta.',
ruleCheckBlockedHelper: 'Peraturan yang mempunyai ralat tidak boleh dihantar. Kembali, edit dan semak semula.',
plan_managed_rule_drifted:
'Peraturan terurus tidak sepadan dengan tembok api aktif. Selesaikan perbezaan dahulu.',
plan_opaque_rule_in_target_scope:
@@ -4242,8 +4234,6 @@ const message = {
managedOrderDiffers: 'Susunan peraturan terurus berbeza daripada turutan pangkalan data.',
managedOnlyInTarget: 'Peraturan terurus hanya wujud dalam tembok api sasaran.',
managedRuntimeCannotRemove: 'Peraturan aktif terurus tidak dapat dipadam dengan selamat.',
managedOrderBlocked:
'Peraturan terurus tidak boleh disusun semula merentasi peraturan luaran, tidak dikenali atau dilindungi.',
missingFromTarget: 'Peraturan tiada dalam tembok api sasaran.',
targetDiffers: 'Peraturan sasaran berbeza daripada dasar pangkalan data.',
alreadyExistsInTarget: 'Peraturan sudah wujud dalam tembok api sasaran.',
@@ -4352,8 +4342,6 @@ const message = {
forwardPortHelper: 'Menyokong julat port, cth: 8080-8089',
forwardInboundInterface: 'Antara Muka Rangkaian Masukan Penerusan',
exportHelper: 'Akan mengeksport {0} peraturan firewall. Teruskan?',
importSuccess: '{0} peraturan berjaya diimport',
importPartialSuccess: 'Import selesai: {0} berjaya, {1} gagal',
basicStatus: 'Firewall semasa tidak terikat. Sila ikat dahulu.',
initMsg: 'Akan memulakan {0}, teruskan?',
initDirectBackendConflictMsg:
@@ -4386,7 +4374,7 @@ const message = {
'Peraturan ini membenarkan {0} mengakses {1}. Memadamnya mungkin menjejaskan akses kepada beberapa perkhidmatan. Teruskan?',
deleteRiskRulesConfirm:
'Akan memadam {0} peraturan. {1} peraturan benarkan mungkin menjejaskan akses perkhidmatan. Teruskan?',
editRuleConfirm: 'Medan berikut akan diubah: {0}. Peraturan akan digunakan dan disahkan serta-merta. Teruskan?',
editRuleConfirm: 'Adakah anda pasti mahu mengubah peraturan ini?',
},
runtime: {
importEnv: 'Import Pemboleh Ubah Persekitaran',

View File

@@ -4182,7 +4182,6 @@ const message = {
drifted: 'Divergente',
},
ruleTargetRequired: 'Informe pelo menos um endereço IP ou uma porta',
batchRuleLimit: 'É possível criar no máximo {0} regras por vez',
resolution_adopt: 'Assumir gerenciamento',
plan_duplicate_rules:
'Regras duplicadas com condições e ações idênticas não podem ser gerenciadas. Exclua manualmente as regras duplicadas e tente novamente.',
@@ -4190,13 +4189,6 @@ const message = {
plan_exact_rule_conflict:
'Já existe uma regra com as mesmas condições, mas com uma ação oposta de permitir ou negar.',
allRulesAlreadyExist: 'Todas as {0} regras verificadas já existem. Não há novas regras para criar.',
ruleCheckResult: 'Resultados da verificação de regras',
ruleCheckStatus_creatable: 'Pode ser criada',
ruleCheckStatus_existing: 'Já existe',
ruleCheckStatus_error: 'Erro',
ruleCheckExistingHelper: 'Uma regra idêntica já existe e será ignorada.',
ruleCheckReadyHelper: 'A verificação foi aprovada. Esta regra pode ser criada.',
ruleCheckBlockedHelper: 'Regras com erros não podem ser enviadas. Volte, edite-as e verifique novamente.',
plan_managed_rule_drifted:
'A regra gerenciada não corresponde ao firewall ativo. Resolva a divergência primeiro.',
plan_opaque_rule_in_target_scope:
@@ -4259,8 +4251,6 @@ const message = {
managedOrderDiffers: 'A ordem das regras gerenciadas difere da sequência do banco de dados.',
managedOnlyInTarget: 'A regra gerenciada existe apenas no firewall de destino.',
managedRuntimeCannotRemove: 'A regra ativa gerenciada não pode ser removida com segurança.',
managedOrderBlocked:
'As regras gerenciadas não podem ser reordenadas através de regras externas, não reconhecidas ou protegidas.',
missingFromTarget: 'A regra não existe no firewall de destino.',
targetDiffers: 'A regra de destino difere da política do banco de dados.',
alreadyExistsInTarget: 'A regra já existe no firewall de destino.',
@@ -4372,8 +4362,6 @@ const message = {
forwardPortHelper: 'Suporta intervalos de portas, ex. 8080-8089',
forwardInboundInterface: 'Interface de Rede de Entrada para Encaminhamento',
exportHelper: 'Prestes a exportar {0} regras de firewall. Continuar?',
importSuccess: '{0} regras importadas com sucesso',
importPartialSuccess: 'Importação concluída: {0} sucesso, {1} falha',
basicStatus: 'O firewall atual não está vinculado. Vincule-o primeiro.',
initMsg: 'Prestes a inicializar {0}, continuar?',
initDirectBackendConflictMsg:
@@ -4408,8 +4396,7 @@ const message = {
'Esta regra permite que {0} acesse {1}. Excluí-la pode afetar o acesso a vários serviços. Continuar?',
deleteRiskRulesConfirm:
'Excluirá {0} regras. {1} regras de liberação podem afetar o acesso aos serviços. Continuar?',
editRuleConfirm:
'Os seguintes campos serão alterados: {0}. A regra será aplicada e verificada imediatamente. Continuar?',
editRuleConfirm: 'Tem certeza de que deseja modificar esta regra?',
},
runtime: {
importEnv: 'Importar variáveis de ambiente',

View File

@@ -4149,7 +4149,6 @@ const message = {
drifted: 'Различие',
},
ruleTargetRequired: 'Укажите хотя бы один IP-адрес или порт',
batchRuleLimit: 'За один раз можно создать не более {0} правил',
resolution_adopt: 'Взять под управление',
plan_duplicate_rules:
'Нельзя принять под управление правила с одинаковыми условиями и действиями. Удалите дубликаты вручную и повторите попытку.',
@@ -4157,13 +4156,6 @@ const message = {
plan_exact_rule_conflict:
'Уже существует правило с теми же условиями, но с противоположным действием разрешения или запрета.',
allRulesAlreadyExist: 'Все проверенные правила ({0}) уже существуют. Новых правил для создания нет.',
ruleCheckResult: 'Результаты проверки правил',
ruleCheckStatus_creatable: 'Можно создать',
ruleCheckStatus_existing: 'Уже существует',
ruleCheckStatus_error: 'Ошибка',
ruleCheckExistingHelper: 'Такое же правило уже существует и будет пропущено.',
ruleCheckReadyHelper: 'Проверка пройдена. Это правило можно создать.',
ruleCheckBlockedHelper: 'Правила с ошибками нельзя отправить. Вернитесь, исправьте их и повторите проверку.',
plan_managed_rule_drifted:
'Управляемое правило не совпадает с активным межсетевым экраном. Сначала устраните расхождение.',
plan_opaque_rule_in_target_scope:
@@ -4226,8 +4218,6 @@ const message = {
managedOrderDiffers: 'Порядок управляемых правил отличается от порядка в базе данных.',
managedOnlyInTarget: 'Управляемое правило существует только в целевом брандмауэре.',
managedRuntimeCannotRemove: 'Активное управляемое правило нельзя безопасно удалить.',
managedOrderBlocked:
'Управляемые правила нельзя перемещать через внешние, нераспознанные или защищённые правила.',
missingFromTarget: 'Правило отсутствует в целевом брандмауэре.',
targetDiffers: 'Целевое правило отличается от политики базы данных.',
alreadyExistsInTarget: 'Правило уже существует в целевом брандмауэре.',
@@ -4339,8 +4329,6 @@ const message = {
forwardPortHelper: 'Поддерживает диапазоны портов, напр. 8080-8089',
forwardInboundInterface: 'Сетевой интерфейс для пересылки входящего трафика',
exportHelper: 'Собираюсь экспортировать {0} правил брандмауэра. Продолжить?',
importSuccess: 'Успешно импортировано {0} правил',
importPartialSuccess: 'Импорт завершён: {0} успешно, {1} с ошибкой',
basicStatus: 'Текущий межсетевой экран не привязан. Сначала привяжите его.',
initMsg: 'Собираюсь инициализировать {0}, продолжить?',
initDirectBackendConflictMsg:
@@ -4374,7 +4362,7 @@ const message = {
'Это правило разрешает {0} доступ к {1}. Его удаление может повлиять на доступ к нескольким сервисам. Продолжить?',
deleteRiskRulesConfirm:
'Удалит {0} правил. {1} разрешающих правил могут повлиять на доступ к сервисам. Продолжить?',
editRuleConfirm: 'Будут изменены поля: {0}. Правило будет немедленно применено и проверено. Продолжить?',
editRuleConfirm: 'Вы уверены, что хотите изменить это правило?',
},
runtime: {
importEnv: 'Импорт переменных окружения',

View File

@@ -4167,7 +4167,6 @@ const message = {
drifted: 'Farklı',
},
ruleTargetRequired: 'En az bir IP adresi veya port girin',
batchRuleLimit: 'Bir seferde en fazla {0} kural oluşturulabilir',
resolution_adopt: 'Yönetimi devral',
plan_duplicate_rules:
'Koşulları ve eylemleri aynı olan yinelenen kurallar yönetime alınamaz. Yinelenen kuralları elle silip yeniden deneyin.',
@@ -4175,13 +4174,6 @@ const message = {
plan_exact_rule_conflict:
'Aynı eşleşme koşullarına sahip, ancak izin verme veya reddetme eylemi zıt olan bir kural zaten var.',
allRulesAlreadyExist: 'Kontrol edilen {0} kuralın tümü zaten mevcut. Oluşturulacak yeni kural yok.',
ruleCheckResult: 'Kural kontrol sonuçları',
ruleCheckStatus_creatable: 'Oluşturulabilir',
ruleCheckStatus_existing: 'Zaten mevcut',
ruleCheckStatus_error: 'Hata',
ruleCheckExistingHelper: 'Aynı kural zaten mevcut ve atlanacak.',
ruleCheckReadyHelper: 'Kontrol başarılı. Bu kural oluşturulabilir.',
ruleCheckBlockedHelper: 'Hatalı kurallar gönderilemez. Geri dönüp düzenleyin ve yeniden kontrol edin.',
plan_managed_rule_drifted: 'Yönetilen kural etkin güvenlik duvarıyla eşleşmiyor. Önce farkı giderin.',
plan_opaque_rule_in_target_scope: 'Hedef kapsamda güvenle ayrıştırılamayan bir kural var. İşlem durduruldu.',
plan_runtime_permanent_mismatch: 'Etkin ve kalıcı güvenlik duvarı yapılandırmaları farklı. Önce eşitleyin.',
@@ -4243,8 +4235,6 @@ const message = {
managedOrderDiffers: 'Yönetilen kuralların sırası veritabanı sırasından farklı.',
managedOnlyInTarget: 'Yönetilen kural yalnızca hedef güvenlik duvarında mevcut.',
managedRuntimeCannotRemove: 'Yönetilen etkin kural güvenli bir şekilde kaldırılamıyor.',
managedOrderBlocked:
'Yönetilen kurallar harici, tanınmayan veya korunan kuralların üzerinden yeniden sıralanamaz.',
missingFromTarget: 'Kural hedef güvenlik duvarında bulunmuyor.',
targetDiffers: 'Hedef kural veritabanı ilkesinden farklı.',
alreadyExistsInTarget: 'Kural hedef güvenlik duvarında zaten mevcut.',
@@ -4355,8 +4345,6 @@ const message = {
forwardPortHelper: 'Port aralıklarını destekler, örn.: 8080-8089',
forwardInboundInterface: 'İletme Gelen Ağ Arayüzü',
exportHelper: '{0} güvenlik duvarı kuralını dışa aktarmak üzere. Devam etmek istiyor musunuz?',
importSuccess: '{0} kural başarıyla içe aktarıldı',
importPartialSuccess: 'İçe aktarma tamamlandı: {0} başarılı, {1} başarısız',
basicStatus: 'Mevcut güvenlik duvarı bağlı değil. Önce bağlayın.',
initMsg: '{0} başlatılmak üzere, devam etmek istiyor musunuz?',
initDirectBackendConflictMsg:
@@ -4390,7 +4378,7 @@ const message = {
'Bu kural {0} için {1} erişimine izin veriyor. Silinmesi birden fazla hizmete erişimi etkileyebilir. Devam edilsin mi?',
deleteRiskRulesConfirm:
'{0} kural silinecek. {1} izin kuralı hizmet erişimini etkileyebilir. Devam edilsin mi?',
editRuleConfirm: 'Şu alanlar değiştirilecek: {0}. Kural hemen uygulanıp doğrulanacak. Devam edilsin mi?',
editRuleConfirm: 'Bu kuralı değiştirmek istediğinizden emin misiniz?',
},
runtime: {
importEnv: 'Ortam Değişkenlerini İçe Aktar',

View File

@@ -3827,19 +3827,11 @@ const message = {
drifted: '異常',
},
ruleTargetRequired: '請至少填寫一個 IP 位址或連接埠',
batchRuleLimit: '單次最多可建立 {0} 條規則',
resolution_adopt: '納管',
plan_duplicate_rules: '存在比對條件和動作相同的重複規則,無法納管。請手動刪除重複規則後重試。',
adoptRuleConfirm: '納入管理後1Panel 將負責維護及刪除這條現有規則,是否繼續?',
plan_exact_rule_conflict: '已存在比對條件相同、但允許與拒絕動作相反的規則。',
allRulesAlreadyExist: '本次檢查的 {0} 條規則均已存在,沒有需要建立的新規則。',
ruleCheckResult: '規則檢查結果',
ruleCheckStatus_creatable: '可建立',
ruleCheckStatus_existing: '已存在',
ruleCheckStatus_error: '錯誤',
ruleCheckExistingHelper: '系統中已存在相同規則,本次將自動略過。',
ruleCheckReadyHelper: '檢查通過,可以建立此規則。',
ruleCheckBlockedHelper: '存在錯誤規則,無法提交。請返回修改後重新檢查。',
plan_managed_rule_drifted: '1Panel 管理記錄與系統實際規則不一致,請檢查系統規則。',
plan_opaque_rule_in_target_scope: '目標範圍內有無法安全解析的規則,本次操作已停止。',
plan_runtime_permanent_mismatch: '防火牆執行設定與永久設定不一致,請先完成同步。',
@@ -3898,7 +3890,6 @@ const message = {
managedOrderDiffers: '受管規則順序與資料庫順序不一致。',
managedOnlyInTarget: '此受管規則僅存在於目標防火牆中。',
managedRuntimeCannotRemove: '無法安全刪除此受管執行中規則。',
managedOrderBlocked: '受管規則的排序不能跨越外部、無法識別或受保護的規則。',
missingFromTarget: '目標防火牆中缺少此規則。',
targetDiffers: '目標規則與資料庫策略不一致。',
alreadyExistsInTarget: '目標防火牆中已存在此規則。',
@@ -3995,8 +3986,6 @@ const message = {
forwardPortHelper: '支援埠範圍8080-8089',
forwardInboundInterface: '轉發入站網路介面',
exportHelper: '即將匯出 {0} 條防火牆規則,是否繼續?',
importSuccess: '成功匯入 {0} 條規則',
importPartialSuccess: '匯入完成:成功 {0} 條,失敗 {1} 條',
basicStatus: '目前防火牆尚未綁定,請先綁定',
initMsg: '即將初始化 {0}, 是否繼續?',
initDirectBackendConflictMsg:
@@ -4025,7 +4014,7 @@ const message = {
deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?',
deleteWildcardRuleConfirm: '該規則允許 {0} 存取 {1}。刪除後可能影響多個服務的存取,是否繼續?',
deleteRiskRulesConfirm: '將刪除 {0} 條規則,其中 {1} 條放行規則可能影響服務存取,是否繼續?',
editRuleConfirm: '將修改以下欄位:{0}。提交後會立即套用並回讀驗證,是否繼續',
editRuleConfirm: '是否確認修改此規則',
},
runtime: {
importEnv: '匯入環境變數',

View File

@@ -3873,19 +3873,11 @@ const message = {
drifted: '异常',
},
ruleTargetRequired: '请至少填写一个 IP 地址或端口',
batchRuleLimit: '单次最多可创建 {0} 条规则',
resolution_adopt: '纳管',
plan_duplicate_rules: '存在匹配条件和动作相同的重复规则,无法纳管。请手动删除重复规则后重试。',
adoptRuleConfirm: '纳入管理后1Panel 将负责维护和删除这条现有规则,是否继续?',
plan_exact_rule_conflict: '已存在匹配条件相同、但允许与拒绝动作相反的规则。',
allRulesAlreadyExist: '本次检查的 {0} 条规则均已存在,没有需要创建的新规则。',
ruleCheckResult: '规则检查结果',
ruleCheckStatus_creatable: '可创建',
ruleCheckStatus_existing: '已存在',
ruleCheckStatus_error: '错误',
ruleCheckExistingHelper: '系统中已存在相同规则,本次将自动跳过。',
ruleCheckReadyHelper: '检查通过,可以创建该规则。',
ruleCheckBlockedHelper: '存在错误规则,不能提交。请返回修改后重新检查。',
plan_managed_rule_drifted:
'这条规则在 1Panel 中的记录与防火墙实际状态不一致,请先刷新规则;如仍异常,请重新同步。',
plan_opaque_rule_in_target_scope:
@@ -3946,8 +3938,6 @@ const message = {
managedOrderDiffers: '防火墙中的规则顺序与 1Panel 保存的顺序不一致,本次将调整。',
managedOnlyInTarget: '该规则仍存在于防火墙中,但 1Panel 已不再保存,本次将删除。',
managedRuntimeCannotRemove: '该规则仅在当前运行中生效1Panel 无法确认删除后的影响,因此本次不会删除。',
managedOrderBlocked:
'调整规则顺序时会跨过外部规则、无法识别的规则或系统保护规则,为避免改变现有访问,本次不会调整。',
missingFromTarget: '防火墙中缺少该规则,本次将补充。',
targetDiffers: '防火墙中的规则与 1Panel 保存的设置不一致,本次将修复。',
alreadyExistsInTarget: '防火墙中已存在相同规则,无需重复添加。',
@@ -4043,8 +4033,6 @@ const message = {
forwardPortHelper: '支持端口范围8080-8089',
forwardInboundInterface: '转发入站网卡',
exportHelper: '即将导出 {0} 条防火墙规则,是否继续?',
importSuccess: '成功导入 {0} 条规则',
importPartialSuccess: '导入完成:成功 {0} 条,失败 {1} 条',
basicStatus: '当前防火墙未绑定,请先绑定!',
initMsg: '即将初始化 {0}, 是否继续?',
@@ -4074,7 +4062,7 @@ const message = {
deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?',
deleteWildcardRuleConfirm: '该规则允许 {0} 访问 {1}。删除后可能影响多个服务的访问,是否继续?',
deleteRiskRulesConfirm: '将删除 {0} 条规则,其中 {1} 条放行规则可能影响服务访问,是否继续?',
editRuleConfirm: '将修改以下字段:{0}。提交后会立即应用并回读验证,是否继续',
editRuleConfirm: '是否确认修改该规则',
},
runtime: {
importEnv: '导入环境变量',

View File

@@ -166,7 +166,9 @@
</el-form>
<template #footer>
<el-button @click="policyVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submitPolicy">{{ $t('commons.button.confirm') }}</el-button>
<el-button type="primary" :loading="savingPolicy" @click="submitPolicy">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DialogPro>
</template>
@@ -176,7 +178,9 @@ import { computed, nextTick, reactive, ref } from 'vue';
import { Firewall } from '@/api/interface/firewall';
import { deleteDockerPortGuardPolicies, upsertDockerPortGuardPolicies } from '@/api/modules/firewall';
import i18n from '@/lang';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { MsgError, MsgWarning } from '@/utils/message';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import {
dockerGuardEndpointManagementMessage,
@@ -187,18 +191,19 @@ import {
import { formatHostAddress, formatHostAddressList, splitTagValues } from '@/views/host/firewall/utils/validation';
const props = defineProps<{ base: Firewall.DockerGuardBase; containers: Firewall.DockerGuardContainer[] }>();
const emit = defineEmits<{ search: [] }>();
const emit = defineEmits<{ search: []; created: [taskID: string] }>();
const drawerVisible = ref(false);
const policyVisible = ref(false);
const savingPolicy = ref(false);
const activeContainerKey = ref('');
const selectedGroupKeys = ref<string[]>([]);
const policyEndpoints = ref<Firewall.DockerGuardEndpoint[]>([]);
const familyFilter = ref<'all' | Firewall.DockerGuardEndpoint['family']>('all');
const formRef = ref<FormInstance>();
const sourceAddressRefs = ref<Array<{ focus: () => void }>>([]);
type PolicyMode = Firewall.DockerGuardPolicyBatch['mode'];
type PolicyForm = Omit<Firewall.DockerGuardPolicyBatch, 'endpoints' | 'mode'> & { mode: PolicyMode | '' };
type PolicyMode = Firewall.DockerGuardPolicy['mode'];
type PolicyForm = Pick<Firewall.DockerGuardPolicy, 'sources' | 'description'> & { mode: PolicyMode | '' };
const form = reactive<PolicyForm>({
mode: 'deny_sources',
sources: [''],
@@ -340,30 +345,45 @@ const removeSourceAddress = (index: number) => {
if (form.sources.length === 0) form.sources.push('');
};
const submitPolicy = async () => {
if (savingPolicy.value) return;
const valid = await formRef.value?.validate().catch(() => false);
if (!valid || !form.mode) return;
const sources = splitTagValues(form.sources);
const mode = form.mode;
const sources = mode === 'deny_all' ? [] : splitTagValues(form.sources);
savingPolicy.value = true;
try {
await upsertDockerPortGuardPolicies({
endpoints: policyEndpoints.value.map(({ family, hostIP, hostPort, protocol }) => ({
family,
hostIP,
hostPort,
protocol,
})),
mode: form.mode,
sources: form.mode === 'deny_all' ? [] : sources,
description: form.description,
});
} catch {
emit('search');
return;
const result = (
await upsertDockerPortGuardPolicies({
policies: policyEndpoints.value.map(({ family, hostIP, hostPort, protocol }) => ({
family,
hostIP,
hostPort,
protocol,
mode,
sources,
description: form.description,
})),
})
).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
policyVisible.value = false;
drawerVisible.value = false;
selectedGroupKeys.value = [];
emit('created', result.taskID);
} catch (error) {
MsgError(
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError'),
);
} finally {
savingPolicy.value = false;
}
policyVisible.value = false;
selectedGroupKeys.value = [];
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
};
const remove = async (endpoints: Firewall.DockerGuardEndpoint[], batch: boolean) => {
const uuids = policyUUIDs(endpoints);
if (!uuids.length) return;
@@ -379,16 +399,21 @@ const remove = async (endpoints: Firewall.DockerGuardEndpoint[], batch: boolean)
return;
}
try {
for (let offset = 0; offset < uuids.length; offset += 256) {
await deleteDockerPortGuardPolicies({ uuids: uuids.slice(offset, offset + 256) });
const result = (await deleteDockerPortGuardPolicies({ uuids })).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
} catch {
emit('search');
return;
selectedGroupKeys.value = [];
drawerVisible.value = false;
emit('created', result.taskID);
} catch (error) {
MsgError(
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError'),
);
}
selectedGroupKeys.value = [];
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
};
const portMappingLabel = (row: Firewall.DockerGuardPortGroup) => {
const ports = row.endpoints.map((endpoint) => endpoint.containerPort).filter(Boolean) as number[];

View File

@@ -1,6 +1,7 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="w-70">
<el-alert class="mb-3" type="info" :closable="false" :title="$t('commons.msg.importHelper')" />
<el-alert v-if="submitError" class="mb-3" type="error" :closable="false" :title="submitError" />
<div class="import-file-bar mt-3">
<el-upload
ref="uploadRef"
@@ -60,20 +61,23 @@
import { Firewall } from '@/api/interface/firewall';
import { upsertDockerPortGuardPolicies } from '@/api/modules/firewall';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgError } from '@/utils/message';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type UploadRawFile } from 'element-plus';
import { ref } from 'vue';
import { dockerGuardEndpointKey, normalizeDockerGuardPolicy } from '@/views/host/firewall/docker/model';
import { formatHostAddressList } from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
const emit = defineEmits<{ (event: 'search'): void }>();
const emit = defineEmits<{ (event: 'created', taskID: string): void }>();
const visible = ref(false);
const loading = ref(false);
const policies = ref<Firewall.DockerGuardPolicy[]>([]);
const selects = ref<Firewall.DockerGuardPolicy[]>([]);
const uploadRef = ref();
const uploaderFiles = ref<UploadFile[]>([]);
const submitError = ref('');
const displaySources = (policy: Firewall.DockerGuardPolicy) => formatHostAddressList(policy.sources, policy.family);
const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
@@ -81,6 +85,7 @@ const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
loading.value = true;
policies.value = [];
selects.value = [];
submitError.value = '';
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
reader.onload = (event) => {
@@ -114,44 +119,25 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
};
const onImport = async () => {
if (loading.value || selects.value.length === 0) return;
loading.value = true;
let success = 0;
let failed = 0;
const groups = new Map<string, Firewall.DockerGuardPolicy[]>();
for (const policy of selects.value) {
const key = JSON.stringify([policy.mode, policy.sources, policy.description]);
groups.set(key, [...(groups.get(key) || []), policy]);
}
for (const group of groups.values()) {
const policy = group[0];
for (let offset = 0; offset < group.length; offset += 256) {
const batch = group.slice(offset, offset + 256);
try {
await upsertDockerPortGuardPolicies({
endpoints: batch.map(({ family, hostIP, hostPort, protocol }) => ({
family,
hostIP,
hostPort,
protocol,
})),
mode: policy.mode,
sources: policy.sources,
description: policy.description,
});
success += batch.length;
} catch {
failed += batch.length;
}
submitError.value = '';
try {
const result = (await upsertDockerPortGuardPolicies({ policies: selects.value })).data;
if (!result.taskID || !result.queued) {
submitError.value = i18n.global.t('commons.msg.operationFailed');
return;
}
}
loading.value = false;
if (failed === 0) {
MsgSuccess(i18n.global.t('firewall.importSuccess', [success]));
visible.value = false;
} else {
MsgError(i18n.global.t('firewall.importPartialSuccess', [success, failed]));
emit('created', result.taskID);
} catch (error) {
submitError.value =
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError');
} finally {
loading.value = false;
}
emit('search');
};
const modeLabel = (mode: Firewall.DockerGuardPolicy['mode']) => {
@@ -165,6 +151,7 @@ const acceptParams = () => {
policies.value = [];
selects.value = [];
uploaderFiles.value = [];
submitError.value = '';
uploadRef.value?.clearFiles();
visible.value = true;
};

View File

@@ -209,8 +209,14 @@
</template>
</DrawerPro>
<DockerGuardDetail ref="detailRef" :base="data.base" :containers="containerRows" @search="search" />
<DockerGuardImport ref="importRef" @search="search" />
<DockerGuardDetail
ref="detailRef"
:base="data.base"
:containers="containerRows"
@search="search"
@created="openRuleTask"
/>
<DockerGuardImport ref="importRef" @created="openRuleTask" />
<RuleSync ref="ruleSyncRef" @search="search" />
<ConfirmDialog ref="cleanupConfirmRef" @confirm="submitCleanupBackend" />
<TaskLog ref="taskLogRef" @close="search" />
@@ -234,7 +240,9 @@ import {
operateFirewallBackend,
} from '@/api/modules/firewall';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { ElMessageBox } from 'element-plus';
import { Lock } from '@element-plus/icons-vue';
import { downloadWithContent } from '@/utils/file';
@@ -255,6 +263,7 @@ const importRef = ref<InstanceType<typeof DockerGuardImport>>();
const ruleSyncRef = ref<InstanceType<typeof RuleSync>>();
const cleanupConfirmRef = ref<InstanceType<typeof ConfirmDialog>>();
const taskLogRef = ref<InstanceType<typeof TaskLog>>();
const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID, true);
const orphanDrawerVisible = ref(false);
const searchName = ref('');
const selects = ref<Firewall.DockerGuardContainer[]>([]);
@@ -518,13 +527,18 @@ const removePolicies = async (endpoints: Firewall.DockerGuardEndpoint[], batch:
}
loading.value = true;
try {
for (let offset = 0; offset < uuids.length; offset += 256) {
await deleteDockerPortGuardPolicies({ uuids: uuids.slice(offset, offset + 256) });
const result = (await deleteDockerPortGuardPolicies({ uuids })).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await search();
} catch {
await search();
openRuleTask(result.taskID);
} catch (error) {
MsgError(
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError'),
);
} finally {
loading.value = false;
}

View File

@@ -6,6 +6,7 @@
<div>{{ $t('commons.msg.importHelper') }}</div>
</template>
</el-alert>
<el-alert v-if="submitError" class="mb-3" type="error" :closable="false" :title="submitError" />
<div class="import-file-bar mt-3">
<el-upload
ref="uploadRef"
@@ -82,7 +83,7 @@
<el-button @click="visible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" :disabled="selects.length === 0" @click="onImport">
<el-button type="primary" :loading="loading" :disabled="selects.length === 0" @click="onImport">
{{ $t('commons.button.import') }}
</el-button>
</span>
@@ -93,8 +94,10 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type UploadRawFile } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgError } from '@/utils/message';
import i18n from '@/lang';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { getNetworkOptions } from '@/api/modules/host';
import { operateForwardRule, searchForwardRule } from '@/api/modules/firewall';
import { Firewall } from '@/api/interface/firewall';
@@ -106,10 +109,11 @@ import {
} from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
const emit = defineEmits<{ (e: 'search'): void }>();
const emit = defineEmits<{ (e: 'created', taskID: string): void }>();
const visible = ref(false);
const loading = ref(false);
const submitError = ref('');
const selects = ref<any>([]);
const displayData = ref<any>([]);
const currentRules = ref<Firewall.RuleInfo[]>([]);
@@ -127,6 +131,7 @@ const paginationConfig = reactive({
const acceptParams = async (fireName: string): Promise<void> => {
loading.value = false;
submitError.value = '';
displayData.value = [];
selects.value = [];
currentRules.value = [];
@@ -164,6 +169,7 @@ const search = () => {
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
if (!_uploadFile.raw) return;
loading.value = true;
submitError.value = '';
displayData.value = [];
pageData.value = [];
selects.value = [];
@@ -261,7 +267,9 @@ const compareRules = (importedRules: any[]) => {
};
const onImport = async () => {
if (loading.value || selects.value.length === 0) return;
loading.value = true;
submitError.value = '';
const rules: Firewall.RuleForward[] = [];
for (const rule of selects.value) {
rules.push({
@@ -275,16 +283,22 @@ const onImport = async () => {
});
}
await operateForwardRule({ rules })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
visible.value = false;
})
.catch(() => {
loading.value = false;
});
try {
const result = (await operateForwardRule({ rules })).data;
if (!result.taskID || !result.queued) {
submitError.value = i18n.global.t('commons.msg.operationFailed');
return;
}
visible.value = false;
emit('created', result.taskID);
} catch (error) {
submitError.value =
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError');
} finally {
loading.value = false;
}
};
defineExpose({

View File

@@ -124,8 +124,9 @@
</el-form>
</template>
</OpDialog>
<OperateDialog @search="search" ref="dialogRef" />
<ImportDialog @search="search" ref="dialogImportRef" />
<OperateDialog @created="openRuleTask" ref="dialogRef" />
<ImportDialog @created="openRuleTask" ref="dialogImportRef" />
<TaskLog ref="taskLogRef" @close="search" />
<RuleSync ref="ruleSyncRef" @search="refreshAfterSync" />
<ConfirmDialog ref="cleanupConfirmRef" @confirm="submitCleanupBackend" />
</div>
@@ -138,11 +139,14 @@ import RuleSync from '@/views/host/firewall/sync/index.vue';
import FireRouter from '@/views/host/firewall/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { operateFirewallBackend, operateForwardRule, searchForwardRule } from '@/api/modules/firewall';
import { Firewall } from '@/api/interface/firewall';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { downloadWithContent } from '@/utils/file';
import { getCurrentDateFormatted } from '@/utils/date';
import { ElMessageBox } from 'element-plus';
@@ -156,6 +160,8 @@ const fireName = ref();
const fireStatusRef = ref();
const ruleSyncRef = ref<InstanceType<typeof RuleSync>>();
const cleanupConfirmRef = ref<InstanceType<typeof ConfirmDialog>>();
const taskLogRef = ref<InstanceType<typeof TaskLog>>();
const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID, true);
const openRuleSync = () => {
if (fireName.value !== 'iptables' && fireName.value !== 'nftables') return;
@@ -295,16 +301,24 @@ const onDelete = async (row: Firewall.RuleForward | null) => {
});
};
const onSubmitDelete = async () => {
if (loading.value) return;
loading.value = true;
await operateForwardRule({ rules: operateRules.value, forceDelete: forceDelete.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
try {
const result = (await operateForwardRule({ rules: operateRules.value, forceDelete: forceDelete.value })).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
openRuleTask(result.taskID);
} catch (error) {
MsgError(
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError'),
);
} finally {
loading.value = false;
}
};
const onImport = () => {

View File

@@ -58,8 +58,10 @@
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { getErrorMessage } from '@/utils/misc';
import { isAxiosError } from 'axios';
import { ElForm } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgError } from '@/utils/message';
import { Firewall } from '@/api/interface/firewall';
import { getNetworkOptions } from '@/api/modules/host';
import { operateForwardRule } from '@/api/modules/firewall';
@@ -92,7 +94,7 @@ const acceptParams = (params: DialogProps): void => {
title.value = i18n.global.t('firewall.' + dialogData.value.title);
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const emit = defineEmits<{ (e: 'created', taskID: string): void }>();
const handleClose = () => {
drawerVisible.value = false;
@@ -128,7 +130,7 @@ type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
if (!formEl || loading.value) return;
const { rowData } = dialogData.value;
if (!rowData) return;
if (!isValidPortRange(rowData.port) || !isValidPortRange(rowData.targetPort)) {
@@ -136,49 +138,40 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
MsgError(i18n.global.t('commons.rule.port'));
return;
}
formEl.validate(async (valid) => {
if (!valid) return;
let rules = [];
rowData.port = normalizePortRange(rowData.port);
rowData.targetPort = normalizePortRange(rowData.targetPort);
rowData.operation = 'add';
if (rowData.targetIP === '') {
rowData.targetIP = rowData.family === 'ipv6' ? '::1' : '127.0.0.1';
}
if (rowData.interface === 'all') {
rowData.interface = '';
}
rules.push(rowData);
loading.value = true;
if (dialogData.value.title === 'create') {
await operateForwardRule({ rules: rules })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
const valid = await formEl.validate().catch(() => false);
if (!valid) return;
rowData.port = normalizePortRange(rowData.port);
rowData.targetPort = normalizePortRange(rowData.targetPort);
rowData.operation = 'add';
if (rowData.targetIP === '') {
rowData.targetIP = rowData.family === 'ipv6' ? '::1' : '127.0.0.1';
}
if (rowData.interface === 'all') {
rowData.interface = '';
}
const operations: Firewall.RuleForward[] = [];
if (dialogData.value.title !== 'create') {
operations.push({ ...oldRule.value, operation: 'remove' });
}
operations.push(rowData);
loading.value = true;
try {
const result = (await operateForwardRule({ rules: operations })).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
rules = [];
oldRule.value.operation = 'remove';
dialogData.value.rowData.operation = 'add';
rules.push(oldRule.value);
rules.push(dialogData.value.rowData);
await operateForwardRule({ rules: rules })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
drawerVisible.value = false;
emit('created', result.taskID);
} catch (error) {
MsgError(
(isAxiosError(error) && error.response?.data?.message) ||
(error && getErrorMessage(error)) ||
i18n.global.t('commons.res.commonError'),
);
} finally {
loading.value = false;
}
};
defineExpose({

View File

@@ -62,15 +62,15 @@
<script lang="ts" setup>
import { Firewall } from '@/api/interface/firewall';
import { checkFirewallRules, createFirewallRules } from '@/api/modules/firewall';
import { createFirewallRules } from '@/api/modules/firewall';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgError } from '@/utils/message';
import { formatHostAddress, inferAddressFamily } from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type UploadRawFile } from 'element-plus';
import { ref } from 'vue';
const emit = defineEmits<{ (event: 'search'): void }>();
const emit = defineEmits<{ (event: 'created', taskID: string): void }>();
const visible = ref(false);
const loading = ref(false);
const provider = ref<Firewall.Provider>('iptables');
@@ -131,36 +131,16 @@ const normalizeLegacyImportedRule = (value: unknown): Firewall.Rule[] | undefine
if (port && !['tcp', 'udp', 'tcp/udp'].includes(protocol)) return;
if (!port && protocol && !['all', 'any'].includes(protocol)) return;
const protocols = port ? (protocol === 'tcp/udp' ? ['tcp', 'udp'] : [protocol]) : ['all'];
return protocols.map((item) => ({
scope: targetScope(family),
protocol: item,
sourceAddress: rule.address as string,
destinationPort: port || undefined,
action: rule.strategy as Firewall.Action,
description: (rule.description as string | undefined) || '',
}));
};
const normalizeImportedRule = (rule: Firewall.Rule): Firewall.Rule[] => {
const splitInet = rule.scope.family === 'inet' && provider.value !== 'firewalld';
const addresses = [rule.sourceAddress, rule.destinationAddress].filter((value): value is string => Boolean(value));
const families: Firewall.Family[] = !splitInet
? [rule.scope.family]
: addresses.length === 0
? ['ipv4', 'ipv6']
: addresses.some((address) => address.includes(':'))
? ['ipv6']
: ['ipv4'];
return families.map((family) => ({
...rule,
uuid: undefined,
nativeKind: undefined,
priority: provider.value === 'firewalld' && rule.scope.provider === 'firewalld' ? rule.priority : undefined,
orderIndex: undefined,
orderBucket: undefined,
scope: targetScope(family),
}));
return [
{
scope: targetScope(family),
protocol: port ? protocol : 'all',
sourceAddress: rule.address as string,
destinationPort: port || undefined,
action: rule.strategy as Firewall.Action,
description: (rule.description as string | undefined) || '',
},
];
};
const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
@@ -178,7 +158,7 @@ const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
return;
}
const normalizedGroups = parsed.map((rule) => {
if (isRule(rule)) return normalizeImportedRule(rule);
if (isRule(rule)) return [rule];
return normalizeLegacyImportedRule(rule);
});
if (normalizedGroups.some((group) => !group)) {
@@ -208,69 +188,22 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value?.handleStart(file);
};
const importedCreateRequest = (plan: Firewall.RuleCheckResult): Firewall.CreateItem | undefined => {
if (plan.decision === 'no_change') return;
if (plan.decision === 'blocked') throw new Error(plan.reason);
const allowed = (plan.allowedActions || []).filter(
(value): value is Firewall.ApplicableCheckAction => value !== 'cancel',
);
let resolution = allowed[0];
if (plan.classification === 'exact_external') {
if (plan.candidates?.length !== 1 || !allowed.includes('adopt')) throw new Error(plan.reason);
resolution = 'adopt';
}
if (!resolution || resolution === 'select_adopt') throw new Error(plan.reason);
return {
checkFlag: plan.checkFlag,
action: resolution,
adoptInstanceKey: resolution === 'adopt' ? plan.candidates?.[0]?.instanceKey : undefined,
rule: plan.requestedRule,
sourceKind: 'imported',
};
};
const onImport = async () => {
if (loading.value || selects.value.length === 0) return;
loading.value = true;
let success = 0;
let failed = 0;
try {
const selected = [...selects.value].sort((left, right) =>
JSON.stringify(left.scope).localeCompare(JSON.stringify(right.scope)),
);
for (let offset = 0; offset < selected.length; offset += 256) {
const batch = selected.slice(offset, offset + 256);
const plans = (await checkFirewallRules({ items: batch.map((rule) => ({ rule })) })).data.items;
const items: Firewall.CreateItem[] = [];
for (const plan of plans) {
try {
const item = importedCreateRequest(plan);
if (item) {
items.push(item);
} else {
success++;
}
} catch {
failed++;
}
}
if (items.length > 0) {
const result = (await createFirewallRules({ items })).data;
success += result.succeeded;
failed += result.failed + result.skipped;
}
const result = (
await createFirewallRules({ items: selects.value.map((rule) => ({ rule, sourceKind: 'imported' })) })
).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
} catch {
failed += selects.value.length - success - failed;
visible.value = false;
emit('created', result.taskID);
} finally {
loading.value = false;
}
if (failed === 0) {
MsgSuccess(i18n.global.t('firewall.importSuccess', [success]));
visible.value = false;
} else {
MsgError(i18n.global.t('firewall.importPartialSuccess', [success, failed]));
}
emit('search');
};
const acceptParams = (value: Firewall.Provider) => {

View File

@@ -192,9 +192,14 @@
<template #default="{ row }">
<span
class="firewall-action"
:class="row.rule.action === 'accept' ? 'is-accept' : 'is-drop'"
:class="{
'is-accept': row.rule.action === 'accept',
'is-drop': isDenyAction(row.rule.action),
'is-unknown': !isKnownAction(row.rule.action),
}"
>
<i
v-if="isKnownAction(row.rule.action)"
class="iconfont firewall-action-icon"
:class="row.rule.action === 'accept' ? 'p-yunxu1' : 'p-a-44tubiao-226'"
aria-hidden="true"
@@ -364,8 +369,9 @@
</LayoutContent>
</div>
</div>
<RuleOperate ref="ruleOperateRef" @search="search" />
<RuleImport ref="ruleImportRef" @search="search" />
<RuleOperate ref="ruleOperateRef" @search="search" @created="openCreateTask" />
<RuleImport ref="ruleImportRef" @created="openCreateTask" />
<TaskLog ref="createTaskLogRef" @close="search" />
<RuleSync ref="ruleSyncRef" @search="search" />
<ProcessDetail ref="processDetailRef" />
<ConfirmDialog ref="resetConfirmRef" @confirm="prepareResetRules" />
@@ -382,8 +388,7 @@
import { Firewall } from '@/api/interface/firewall';
import { Process } from '@/api/interface/process';
import {
checkFirewallRules,
createFirewallRules,
adoptFirewallRule,
deleteFirewallRules,
loadDockerPublishedPorts,
loadFirewallNativeDetail,
@@ -405,6 +410,7 @@ import FireRouter from '@/views/host/firewall/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import ProcessDetail from '@/views/host/process/process/detail/index.vue';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
import { loadDockerStatus } from '@/api/modules/container';
import { computed, onMounted, reactive, ref } from 'vue';
@@ -464,6 +470,8 @@ const cacheFilterValues = (key: string, values: readonly string[]) => {
const fireStatusRef = ref<InstanceType<typeof FireStatus>>();
const ruleOperateRef = ref<InstanceType<typeof RuleOperate>>();
const ruleImportRef = ref<InstanceType<typeof RuleImport>>();
const createTaskLogRef = ref<InstanceType<typeof TaskLog>>();
const openCreateTask = (taskID: string) => createTaskLogRef.value?.openWithTaskID(taskID, true);
const ruleSyncRef = ref<InstanceType<typeof RuleSync>>();
const processDetailRef = ref<InstanceType<typeof ProcessDetail>>();
const resetConfirmRef = ref<InstanceType<typeof ConfirmDialog>>();
@@ -889,14 +897,20 @@ const scopeNoticeText = (notice: Firewall.ScopeNotice) => {
}
};
const actionLabel = (action: Firewall.Action) => {
const isDenyAction = (action: string) => action === 'drop' || action === 'reject';
const isKnownAction = (action: string) => action === 'accept' || isDenyAction(action);
const actionLabel = (action: string) => {
if (action === 'accept') {
return i18n.global.t('firewall.accept');
}
if (action === 'reject') {
return i18n.global.t('firewall.reject');
}
return i18n.global.t('firewall.drop');
if (action === 'drop') {
return i18n.global.t('firewall.drop');
}
return i18n.global.t('commons.status.unknown');
};
const ruleSourceLabel = (row: Firewall.InventoryItem) => {
@@ -1063,14 +1077,8 @@ const removeRules = async (selected: RuleRow[]) => {
loading.value = true;
const uuids = [...new Set(selected.flatMap((row) => (row.desired?.uuid ? [row.desired.uuid] : [])))];
try {
let succeeded = 0;
let failed = 0;
for (let offset = 0; offset < uuids.length; offset += 256) {
const batch = uuids.slice(offset, offset + 256);
const result = (await deleteFirewallRules({ uuids: batch })).data;
succeeded += result.succeeded;
failed += result.failed;
}
if (uuids.length === 0) return;
const { succeeded, failed } = (await deleteFirewallRules({ uuids })).data;
if (succeeded > 0) {
MsgSuccess(`${i18n.global.t('commons.msg.operationSuccess')} (${succeeded}/${uuids.length})`);
}
@@ -1161,47 +1169,11 @@ const adoptRule = async (row: RuleRow) => {
}
loading.value = true;
try {
const plan = (await checkFirewallRules({ items: [{ rule: row.rule, adoptLocator: row.observed.locator }] }))
.data.items[0];
if (plan.decision === 'no_change' && plan.existingRuleUUID) {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await search();
return;
}
if (plan.reason === 'duplicate_rules') {
MsgError(i18n.global.t('firewall.plan_duplicate_rules'));
return;
}
if (plan.decision === 'blocked' && plan.classification === 'exact_managed') {
MsgError(i18n.global.t('commons.rule.duplicate'));
return;
}
if (plan.decision !== 'confirmation_required' || plan.classification !== 'exact_external') {
if (!row.observed.instanceKey) {
MsgError(i18n.global.t('firewall.plan_blocked'));
return;
}
const candidate = plan.candidates?.[0];
if (plan.candidates?.length !== 1 || !candidate?.instanceKey) {
MsgError(i18n.global.t('firewall.plan_blocked'));
return;
}
const result = (
await createFirewallRules({
items: [
{
checkFlag: plan.checkFlag,
action: 'adopt',
adoptInstanceKey: candidate.instanceKey,
rule: plan.requestedRule,
sourceKind: 'user',
},
],
})
).data;
if (result.failed > 0 || result.skipped > 0) {
MsgError(result.errors?.[0]?.error || i18n.global.t('commons.msg.operationFailed'));
return;
}
await adoptFirewallRule({ scope: row.rule.scope, instanceKey: row.observed.instanceKey });
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await search();
} finally {
@@ -1337,6 +1309,10 @@ onMounted(() => {
&.is-drop .firewall-action-icon {
color: var(--el-color-info);
}
&.is-unknown {
color: var(--el-text-color-secondary);
}
}
.firewall-action-icon {

View File

@@ -1,147 +0,0 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.msg.operationFailed')" size="large" @close="handleDialogClose">
<div class="error-list">
<div
v-for="failure in result.errors || []"
:key="failure.index"
class="error-item"
:class="failure.status === 'skipped' ? 'is-skipped' : 'is-failed'"
>
<div class="error-item-header">
<span class="error-rule">{{ ruleSummary(failure) }}</span>
<span class="error-status">
{{
failure.status === 'skipped'
? $t('commons.status.unexecuted')
: $t('commons.msg.operationFailed')
}}
</span>
</div>
<pre v-if="failureReason(failure.error)" class="error-reason">{{ failureReason(failure.error) }}</pre>
</div>
</div>
<template #footer>
<el-button type="primary" @click="close">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DialogPro>
</template>
<script lang="ts" setup>
import { Firewall } from '@/api/interface/firewall';
import { formatHostAddress } from '@/views/host/firewall/utils/validation';
import { reactive, ref } from 'vue';
const emit = defineEmits<{ (event: 'close'): void }>();
const visible = ref(false);
const result = reactive<Firewall.CreateResponse>({
succeeded: 0,
failed: 0,
skipped: 0,
errors: [],
});
const wildcardAddress = (family: Firewall.Family) => {
if (family === 'ipv6') return '::/0';
if (family === 'inet') return '0.0.0.0/0, ::/0';
return '0.0.0.0/0';
};
const familyLabel = (family: Firewall.Family) => (family === 'inet' ? 'IPv4/IPv6' : family.toUpperCase());
const ruleSummary = (failure: Firewall.CreateFailure) => {
const rule = failure.rule;
const address = formatHostAddress(rule.sourceAddress, rule.scope.family) || wildcardAddress(rule.scope.family);
return `#${failure.index + 1} · ${familyLabel(rule.scope.family)} · ${rule.protocol.toUpperCase()} · ${address}${rule.destinationPort || '*'}`;
};
const failureReason = (error?: string) =>
(error || '')
.replace(/^execute UFW rule:\s*/i, '')
.replace(/^stderr:\s*/i, '')
.replace(/\s*,\s*err:\s*exit status \d+\s*$/i, '')
.trim();
const acceptParams = (data: Firewall.CreateResponse) => {
Object.assign(result, data);
result.errors = data.errors || [];
visible.value = true;
};
const close = () => {
visible.value = false;
emit('close');
};
const handleDialogClose = () => {
visible.value = false;
emit('close');
};
defineExpose({ acceptParams });
</script>
<style lang="scss" scoped>
.error-list {
max-height: 56vh;
padding-right: 6px;
overflow-y: auto;
}
.error-item {
padding: 14px 4px;
&.is-skipped {
opacity: 0.72;
}
}
.error-item + .error-item {
border-top: 1px solid var(--el-border-color-lighter);
}
.error-item-header {
display: flex;
align-items: flex-start;
gap: 12px;
}
.error-rule {
min-width: 0;
flex: 1;
color: var(--el-text-color-primary);
font-weight: 500;
line-height: 24px;
word-break: break-word;
}
.error-status {
padding: 3px 10px;
flex: none;
border: 1px solid var(--el-color-danger-light-7);
border-radius: 12px;
background: var(--el-color-danger-light-9);
color: var(--el-color-danger);
font-size: 12px;
}
.error-item.is-skipped .error-status {
border-color: var(--el-border-color-light);
background: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
}
.error-reason {
padding: 0;
margin: 8px 0 0;
color: var(--el-color-danger);
font-family: inherit;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>

View File

@@ -6,14 +6,7 @@
:auto-close="!loading"
@close="handleClose"
>
<el-form
v-if="!showingPreview"
ref="formRef"
v-loading="loading"
label-position="top"
:model="form"
:rules="rules"
>
<el-form ref="formRef" v-loading="loading" label-position="top" :model="form" :rules="rules">
<el-form-item :label="$t('firewall.action')" prop="action">
<el-radio-group v-model="form.action" :disabled="descriptionOnly">
<el-radio-button value="accept">
@@ -102,107 +95,25 @@
</el-form-item>
</el-form>
<div v-if="showingPreview" v-loading="loading" class="rule-preview">
<div class="rule-preview-title">
<span>{{ $t('firewall.ruleCheckResult') }}</span>
</div>
<div class="rule-check-groups">
<section v-for="group in ruleCheckGroups" :key="group.status" class="rule-check-group">
<div class="rule-check-group-header">
<div class="rule-check-group-title">
<div class="rule-check-group-status">
<span class="rule-check-group-label">{{ group.label }} · {{ group.items.length }}</span>
<span class="rule-check-group-description">
{{ ruleCheckGroupDescription(group.status) }}
</span>
</div>
</div>
</div>
<div class="rule-check-items">
<div v-for="item in group.items" :key="ruleCheckItemKey(item)" class="rule-check-item">
<div class="rule-check-item-main">
<div class="rule-check-rule-summary">
<span class="rule-check-protocol">
{{ previewProtocol(previewRule(item)) }}
</span>
<span class="rule-check-separator">·</span>
<span class="rule-check-address">{{ previewAddress(previewRule(item)) }}</span>
<span class="rule-check-arrow"></span>
<span>
{{ $t('commons.table.port') }}
{{ previewRule(item).destinationPort || '*' }}
</span>
</div>
</div>
<div class="rule-check-item-meta">
<span
class="rule-check-action"
:class="previewRule(item).action === 'accept' ? 'is-accept' : 'is-drop'"
>
<i
class="iconfont rule-check-action-icon"
:class="previewRule(item).action === 'accept' ? 'p-yunxu' : 'p-a-44tubiao-139'"
aria-hidden="true"
/>
{{ $t(`firewall.${previewRule(item).action === 'accept' ? 'accept' : 'drop'}`) }}
</span>
<span v-if="previewRulePriority(previewRule(item)) !== undefined">
{{ priorityFieldLabel }}{{ previewRulePriority(previewRule(item)) }}
</span>
<span v-if="previewRule(item).description" class="rule-check-description">
{{ previewRule(item).description }}
</span>
<span
v-if="ruleCheckStatus(item.plan) === 'error'"
class="rule-check-item-reason"
:class="`is-${ruleCheckStatus(item.plan)}`"
>
{{ planReasonMessage(item.plan.reason) }}
</span>
</div>
</div>
</div>
</section>
</div>
</div>
<template #footer>
<el-button :disabled="loading" @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button v-if="showingPreview" :disabled="loading" @click="backToForm">
{{ $t('commons.button.back') }}
</el-button>
<el-button
type="primary"
:disabled="loading || (showingPreview && hasBlockingRules)"
@click="onCheckOrSubmit"
>
{{
$t(
showingPreview
? 'commons.button.submit'
: directEdit
? 'commons.button.save'
: 'commons.button.check',
)
}}
<el-button type="primary" :disabled="loading" @click="onSubmit">
{{ $t('commons.button.submit') }}
</el-button>
</template>
</DrawerPro>
<ErrDialog ref="errDialogRef" @close="backToForm" />
</template>
<script lang="ts" setup>
import { Firewall } from '@/api/interface/firewall';
import { checkFirewallRules, createFirewallRules, updateFirewallRule } from '@/api/modules/firewall';
import { createFirewallRules, updateFirewallRule } from '@/api/modules/firewall';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import ErrDialog from './err-message.vue';
import {
formatHostAddress,
inferAddressFamily,
@@ -223,27 +134,16 @@ const loading = ref(false);
const formRef = ref<FormInstance>();
const sourceAddressRefs = ref<Array<{ focus: () => void }>>([]);
const destinationPortRefs = ref<Array<{ focus: () => void }>>([]);
const errDialogRef = ref<InstanceType<typeof ErrDialog>>();
const previewRules = ref<Firewall.Rule[]>([]);
const previewVisible = ref(false);
const checkCompleted = ref(false);
const positionRanges = ref<Partial<Record<Firewall.Family, Firewall.PositionRange>>>({});
const firewalldPrioritySupported = ref(true);
interface BatchPlanItem {
rule: Firewall.Rule;
plan: Firewall.RuleCheckResult;
resolution?: Firewall.ApplicableCheckAction;
}
interface SourceAddressItem {
family: Firewall.Family;
address: string;
}
const batchPlans = ref<BatchPlanItem[]>([]);
const form = reactive({
protocol: 'tcp',
sourceAddresses: [{ family: 'ipv4', address: '' }] as SourceAddressItem[],
@@ -299,13 +199,6 @@ const rules = reactive<FormRules>({
});
const portProtocol = computed(() => ['tcp', 'udp', 'tcp/udp'].includes(form.protocol));
const wildcardAddress = (family: Firewall.Family) => {
if (family === 'ipv6') return '::/0';
if (family === 'inet') return '0.0.0.0/0, ::/0';
return '0.0.0.0/0';
};
const wildcardAddressLabel = (family: Firewall.Family) =>
`${wildcardAddress(family)}${i18n.global.t('firewall.anyWhere')}`;
const isWildcardAddress = (_family: Firewall.Family, address?: string) => !address?.trim();
const priorityFieldLabel = computed(() => i18n.global.t('firewall.priority'));
const showPriorityField = computed(() => {
@@ -321,68 +214,6 @@ const selectedPositionRanges = computed(() => {
});
const priorityMin = computed(() => Math.max(...selectedPositionRanges.value.map((range) => range.min)));
const priorityMax = computed(() => Math.min(...selectedPositionRanges.value.map((range) => range.max)));
const showingPreview = computed(() => previewVisible.value && previewRules.value.length > 0);
const supportedPlanReasons = new Set([
'duplicate_rules',
'exact_rule_conflict',
'managed_rule_drifted',
'opaque_rule_in_target_scope',
'runtime_permanent_mismatch',
'protected_rule',
]);
const planReasonMessage = (reason: string) =>
i18n.global.t(`firewall.plan_${supportedPlanReasons.has(reason) ? reason : 'blocked'}`);
type RuleCheckDisplayStatus = 'creatable' | 'existing' | 'error';
const ruleCheckStatus = (result: Firewall.RuleCheckResult): RuleCheckDisplayStatus => {
if (result.decision === 'blocked') return 'error';
if (result.decision === 'no_change' || result.classification === 'exact_external') return 'existing';
return 'creatable';
};
const ruleCheckGroupDescription = (status: RuleCheckDisplayStatus) => {
if (status === 'error') return i18n.global.t('firewall.ruleCheckBlockedHelper');
if (status === 'existing') return i18n.global.t('firewall.ruleCheckExistingHelper');
return i18n.global.t('firewall.ruleCheckReadyHelper');
};
const ruleCheckCounts = computed(() => {
const counts: Record<RuleCheckDisplayStatus, number> = {
creatable: 0,
existing: 0,
error: 0,
};
for (const item of batchPlans.value) counts[ruleCheckStatus(item.plan)]++;
return counts;
});
const ruleCheckGroupOrder: RuleCheckDisplayStatus[] = ['error', 'creatable', 'existing'];
const ruleCheckGroups = computed(() =>
ruleCheckGroupOrder
.map((status) => ({
status,
label: i18n.global.t(`firewall.ruleCheckStatus_${status}`),
items: batchPlans.value.filter((item) => ruleCheckStatus(item.plan) === status),
}))
.filter((group) => group.items.length > 0),
);
const previewRule = (item: BatchPlanItem) => item.plan.requestedRule || item.rule;
const previewRulePriority = (rule: Firewall.Rule) => rule.priority ?? rule.orderIndex;
const ruleCheckItemKey = (item: BatchPlanItem) =>
[
item.plan.checkFlag,
previewRule(item).scope.family,
previewRule(item).protocol,
previewRule(item).sourceAddress,
previewRule(item).destinationPort,
].join(':');
const hasBlockingRules = computed(() => ruleCheckCounts.value.error > 0);
const existingRuleItems = computed(() => batchPlans.value.filter((item) => ruleCheckStatus(item.plan) === 'existing'));
const normalizeSourceAddresses = () => {
const seen = new Set<string>();
let normalized = form.sourceAddresses.flatMap((item) => {
@@ -533,24 +364,8 @@ const handleClose = () => {
const resetBatch = () => {
previewRules.value = [];
previewVisible.value = false;
checkCompleted.value = false;
batchPlans.value = [];
};
watch(
form,
() => {
if (!checkCompleted.value) return;
checkCompleted.value = false;
previewRules.value = [];
batchPlans.value = [];
},
{ deep: true },
);
const backToForm = () => resetBatch();
const changeProtocol = () => {
if (!portProtocol.value) {
form.sourcePort = '';
@@ -631,30 +446,9 @@ const changedRuleFields = (before: Firewall.Rule, after: Firewall.Rule) =>
.filter(([field]) => JSON.stringify(before[field] ?? '') !== JSON.stringify(after[field] ?? ''))
.map(([field]) => field);
const canSaveWithoutCheck = (before: Firewall.Rule, after: Firewall.Rule) =>
const isMetadataOnlyEdit = (before: Firewall.Rule, after: Firewall.Rule) =>
changedRuleFields(before, after).every((field) => ['description', 'orderIndex', 'priority'].includes(field));
const directEdit = computed(
() =>
mode.value === 'edit' &&
(descriptionOnly.value ||
Boolean(originalFormRule.value && canSaveWithoutCheck(originalFormRule.value, buildRule()))),
);
const availableResolutions = (result: Firewall.RuleCheckResult) =>
(result.allowedActions || []).filter((item): item is Firewall.ApplicableCheckAction => item !== 'cancel');
const previewAddress = (rule: Firewall.Rule) => {
if (rule.sourceAddress && !isWildcardAddress(rule.scope.family, rule.sourceAddress)) {
return formatHostAddress(rule.sourceAddress, rule.scope.family);
}
return wildcardAddressLabel(rule.scope.family);
};
const previewProtocol = (rule: Firewall.Rule) =>
rule.scope.provider === 'ufw' && rule.protocol === 'all' && rule.destinationPort
? 'TCP/UDP'
: rule.protocol.toUpperCase();
const buildPreviewRules = () => {
if (!normalizeSourceAddresses()) return false;
if (!normalizeDestinationPorts()) return false;
@@ -684,77 +478,21 @@ const buildPreviewRules = () => {
MsgError(i18n.global.t('commons.msg.notSupportOperation'));
return false;
}
if (rules.length > 256) {
MsgError(i18n.global.t('firewall.batchRuleLimit', [256]));
return false;
}
previewRules.value = rules;
return true;
};
const prepareBatchPlans = async () => {
checkCompleted.value = false;
batchPlans.value = [];
const results = (await checkFirewallRules({ items: previewRules.value.map((rule) => ({ rule })) })).data.items;
if (results.length !== previewRules.value.length) {
const submitCreateTask = async () => {
if (!(await prepareRulesFromForm())) return;
const result = (
await createFirewallRules({ items: previewRules.value.map((rule) => ({ rule, sourceKind: 'user' })) })
).data;
if (!result.taskID || !result.queued) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
for (let ruleIndex = 0; ruleIndex < previewRules.value.length; ruleIndex++) {
const rule = previewRules.value[ruleIndex];
const result = results[ruleIndex];
const available = availableResolutions(result);
batchPlans.value.push({
rule,
plan: result,
resolution:
ruleCheckStatus(result) === 'creatable'
? available.find((action) => action !== 'select_adopt')
: undefined,
});
}
previewRules.value = results.map((result) => result.requestedRule);
checkCompleted.value = true;
previewVisible.value = true;
};
const executeBatchPlans = async () => {
if (hasBlockingRules.value) {
MsgError(i18n.global.t('firewall.ruleCheckBlockedHelper'));
return;
}
const items: Firewall.CreateItem[] = [];
for (const item of batchPlans.value) {
if (ruleCheckStatus(item.plan) === 'existing') continue;
const available = availableResolutions(item.plan);
const selectedResolution = available.includes(item.resolution as Firewall.ApplicableCheckAction)
? item.resolution
: undefined;
if (!selectedResolution) {
MsgError(i18n.global.t('commons.msg.operationFailed'));
return;
}
items.push({
checkFlag: item.plan.checkFlag,
action: selectedResolution,
rule: item.plan.requestedRule,
sourceKind: 'user',
});
}
if (items.length === 0) {
MsgWarning(i18n.global.t('firewall.allRulesAlreadyExist', [existingRuleItems.value.length]));
drawerVisible.value = false;
return;
}
const result = (await createFirewallRules({ items })).data;
if (result.succeeded > 0) emit('search');
if (result.failed > 0 || (result.skipped || 0) > 0) {
errDialogRef.value?.acceptParams(result);
return;
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
drawerVisible.value = false;
emit('created', result.taskID);
};
const prepareRulesFromForm = async () => {
@@ -766,59 +504,8 @@ const prepareRulesFromForm = async () => {
return buildPreviewRules();
};
const checkRules = async () => {
if (descriptionOnly.value && editingUUID.value && originalFormRule.value) {
previewRules.value = [{ ...originalFormRule.value, description: form.description }];
batchPlans.value = [];
await executeEdit();
return;
}
if (directEdit.value && editingUUID.value && originalFormRule.value) {
const rule = buildRule();
const changed = changedRuleFields(originalFormRule.value, rule);
if (changed.some((field) => field === 'priority' || field === 'orderIndex')) {
const value = previewRulePriority(rule);
if (
typeof value !== 'number' ||
!Number.isInteger(value) ||
value < priorityMin.value ||
value > priorityMax.value
) {
MsgError(i18n.global.t('commons.rule.numberRange', [priorityMin.value, priorityMax.value]));
return;
}
}
previewRules.value = [rule];
batchPlans.value = [];
await executeEdit();
return;
}
if (!(await prepareRulesFromForm())) return;
if (mode.value === 'edit' && editingUUID.value) {
if (originalFormRule.value && canSaveWithoutCheck(originalFormRule.value, previewRules.value[0])) {
await executeEdit();
return;
}
const result = (
await checkFirewallRules({
items: [{ uuid: editingUUID.value, rule: previewRules.value[0] }],
})
).data.items[0];
previewRules.value = [result.requestedRule];
batchPlans.value = [{ rule: result.requestedRule, plan: result }];
checkCompleted.value = true;
previewVisible.value = true;
return;
}
await prepareBatchPlans();
};
const executeEdit = async () => {
if (!editingUUID.value || previewRules.value.length === 0) return;
if (hasBlockingRules.value) {
MsgError(i18n.global.t('firewall.ruleCheckBlockedHelper'));
return;
}
const updatedRule = previewRules.value[0];
const before = originalFormRule.value || editingRule.value;
const changed = before ? changedRuleFields(before, updatedRule) : [];
@@ -826,26 +513,17 @@ const executeEdit = async () => {
drawerVisible.value = false;
return;
}
if (!before || !canSaveWithoutCheck(before, updatedRule)) {
const labels = editableFieldLabels
.filter(([field]) => changed.includes(field))
.map(([, label]) => i18n.global.t(label));
try {
await ElMessageBox.confirm(
i18n.global.t('firewall.editRuleConfirm', [labels.join(', ')]),
i18n.global.t('firewall.edit'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
try {
await ElMessageBox.confirm(i18n.global.t('firewall.editRuleConfirm'), i18n.global.t('firewall.edit'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
});
} catch {
return;
}
let request: Firewall.UpdateRequest = { rule: updatedRule };
if (before && canSaveWithoutCheck(before, updatedRule)) {
if (before && isMetadataOnlyEdit(before, updatedRule)) {
const description = changed.includes('description') ? updatedRule.description || '' : undefined;
if (changed.includes('priority')) {
request = { priority: updatedRule.priority!, description };
@@ -861,29 +539,46 @@ const executeEdit = async () => {
drawerVisible.value = false;
};
const submitCheckedRules = async () => {
if (mode.value === 'edit') {
await executeEdit();
return;
}
await executeBatchPlans();
};
const onCheckOrSubmit = async () => {
const onSubmit = async () => {
if (loading.value) return;
loading.value = true;
try {
if (showingPreview.value) {
await submitCheckedRules();
} else {
await checkRules();
if (mode.value === 'create') {
await submitCreateTask();
} else if (
originalFormRule.value &&
(descriptionOnly.value || isMetadataOnlyEdit(originalFormRule.value, buildRule()))
) {
const rule = descriptionOnly.value
? { ...originalFormRule.value, description: form.description }
: buildRule();
const changed = changedRuleFields(originalFormRule.value, rule);
if (changed.some((field) => field === 'priority' || field === 'orderIndex')) {
const value = rule.priority ?? rule.orderIndex;
if (
typeof value !== 'number' ||
!Number.isInteger(value) ||
value < priorityMin.value ||
value > priorityMax.value
) {
MsgError(i18n.global.t('commons.rule.numberRange', [priorityMin.value, priorityMax.value]));
return;
}
}
previewRules.value = [rule];
await executeEdit();
} else if (await prepareRulesFromForm()) {
await executeEdit();
}
} finally {
loading.value = false;
}
};
const emit = defineEmits<{ (event: 'search'): void }>();
const emit = defineEmits<{
(event: 'search'): void;
(event: 'created', taskID: string): void;
}>();
defineExpose({ acceptParams });
</script>
@@ -909,10 +604,6 @@ defineExpose({ acceptParams });
gap: 8px;
}
.rule-check-alert {
margin-bottom: 12px;
}
.source-address-select {
flex: 1;
}
@@ -925,155 +616,4 @@ defineExpose({ acceptParams });
margin-left: 12px;
color: var(--el-text-color-secondary);
}
.rule-preview {
margin-top: 4px;
color: var(--el-text-color-primary);
}
.rule-preview-title {
display: flex;
align-items: center;
margin-bottom: 12px;
color: var(--el-text-color-primary);
font-weight: 500;
}
.rule-check-groups {
max-height: 460px;
overflow-y: auto;
}
.rule-check-group + .rule-check-group {
margin-top: 16px;
}
.rule-check-group-header {
display: flex;
align-items: center;
min-height: 32px;
padding: 0 4px;
color: var(--el-text-color-primary);
font-weight: 500;
}
.rule-check-group-title {
display: flex;
align-items: flex-start;
min-width: 0;
}
.rule-check-group-status {
display: flex;
align-items: center;
min-width: 0;
flex-wrap: wrap;
gap: 6px;
}
.rule-check-group-label {
flex: none;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
line-height: 20px;
}
.rule-check-group-description {
color: var(--el-text-color-secondary);
font-size: 11px;
font-weight: normal;
line-height: 18px;
}
.rule-check-items {
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
background: var(--el-fill-color-light);
}
.rule-check-item {
padding: 12px 14px;
background: var(--el-fill-color-light);
& + & {
border-top: 1px solid var(--el-border-color-lighter);
}
}
.rule-check-item-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.rule-check-rule-summary {
display: flex;
align-items: center;
min-width: 0;
gap: 8px;
color: var(--el-text-color-primary);
font-size: 13px;
}
.rule-check-protocol {
flex: none;
font-weight: 500;
}
.rule-check-separator,
.rule-check-arrow {
flex: none;
color: var(--el-text-color-placeholder);
}
.rule-check-address {
overflow-wrap: anywhere;
}
.rule-check-item-meta {
display: flex;
align-items: center;
min-width: 0;
margin-top: 7px;
gap: 12px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.rule-check-action {
display: inline-flex;
align-items: center;
flex: none;
gap: 4px;
color: var(--el-text-color-secondary);
line-height: 18px;
&.is-accept .rule-check-action-icon {
color: var(--el-color-primary);
}
&.is-drop .rule-check-action-icon {
color: var(--el-color-info);
}
}
.rule-check-action-icon {
font-size: 14px;
line-height: 1;
}
.rule-check-description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rule-check-item-reason {
&.is-error {
color: var(--el-color-danger);
}
}
</style>

View File

@@ -200,7 +200,7 @@ const syncDisabled = computed(() => {
);
return hasUnsupportedBlocker || (preview.value.ready === 0 && preview.value.removed === 0);
}
return preview.value.blocked > 0;
return preview.value.blocked > 0 || (preview.value.ready === 0 && preview.value.removed === 0);
});
const detailItems = computed(() => {
if (!preview.value || !detailFilter.value) return [];