mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
refactor(firewall): queue rule operations and simplify synchronization (#13784)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
58
agent/app/service/firewall_rule_task.go
Normal file
58
agent/app/service/firewall_rule_task.go
Normal 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
@@ -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 {
|
||||
|
||||
@@ -32,7 +32,6 @@ const (
|
||||
|
||||
const (
|
||||
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
|
||||
FirewallRuleCheckVersion = 1
|
||||
|
||||
FirewallRuleOriginCreated = "created"
|
||||
FirewallRuleOriginAdopted = "adopted"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: 'وظیفه فرعی'
|
||||
|
||||
@@ -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: 'サブタスク'
|
||||
|
||||
@@ -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: '하위 작업'
|
||||
|
||||
@@ -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: 'ວຽກຍ່ອຍ'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: 'Подзадача'
|
||||
|
||||
@@ -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.35’ten eskiyse UFW’yi 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'
|
||||
|
||||
@@ -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: '子任務'
|
||||
|
||||
@@ -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: "子任务"
|
||||
|
||||
@@ -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)
|
||||
|
||||
42
agent/utils/firewall/docker_guard/forward_policy.go
Normal file
42
agent/utils/firewall/docker_guard/forward_policy.go
Normal 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
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, ":", "-")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
29
agent/utils/firewall/iptables_helper/read.go
Normal file
29
agent/utils/firewall/iptables_helper/read.go
Normal 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
|
||||
}
|
||||
16
agent/utils/firewall/nftables_helper/read.go
Normal file
16
agent/utils/firewall/nftables_helper/read.go
Normal 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
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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: ¤t.Locator, UnmarkedAdopted: current.Marker == "" && desired.Origin == filter.RuleOriginAdopted}, nil
|
||||
}
|
||||
|
||||
func ObservedRule(observed filter.ObservedRule) filter.FirewallRule {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'وارد کردن متغیرهای محیطی',
|
||||
|
||||
@@ -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: '環境変数をインポート',
|
||||
|
||||
@@ -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: '환경 변수 가져오기',
|
||||
|
||||
@@ -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: 'ນຳເຂົ້າຕົວແປສະພາບແວດລ້ອມ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Импорт переменных окружения',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '匯入環境變數',
|
||||
|
||||
@@ -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: '导入环境变量',
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user