From 367cd40e0a6ff864961b3e1904c45554784c10c2 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 27 Aug 2026 18:40:52 +0800 Subject: [PATCH] fix: improve firewall rule management --- agent/app/api/v2/firewall.go | 15 ++ agent/app/dto/firewall.go | 2 + agent/app/service/docker.go | 113 +++++++++- agent/app/service/firewall.go | 140 +++++------- agent/app/service/firewall_docker.go | 15 ++ agent/app/service/firewall_selection.go | 12 +- agent/app/service/firewall_setting.go | 91 ++++---- agent/app/service/forward.go | 100 ++------- agent/constant/firewall.go | 9 +- agent/router/ro_host.go | 1 + agent/utils/firewall/docker_guard/policy.go | 4 +- agent/utils/firewall/filter/check.go | 52 +---- agent/utils/firewall/filter/check_flag.go | 2 +- agent/utils/firewall/filter/inventory.go | 20 +- agent/utils/firewall/filter/model.go | 17 +- agent/utils/firewall/filter/observed.go | 25 +++ .../firewall/filter/providers/ufw/adapter.go | 130 ++++++++--- agent/utils/firewall/lifecycle/lifecycle.go | 2 +- frontend/src/api/interface/firewall.ts | 7 +- frontend/src/api/modules/firewall.ts | 3 + frontend/src/lang/modules/en.ts | 24 +- frontend/src/lang/modules/es-es.ts | 26 ++- frontend/src/lang/modules/fa.ts | 25 ++- frontend/src/lang/modules/ja.ts | 25 ++- frontend/src/lang/modules/ko.ts | 24 +- frontend/src/lang/modules/lo.ts | 23 +- frontend/src/lang/modules/ms.ts | 25 ++- frontend/src/lang/modules/pt-br.ts | 26 ++- frontend/src/lang/modules/ru.ts | 26 ++- frontend/src/lang/modules/tr.ts | 25 ++- frontend/src/lang/modules/zh-Hant.ts | 19 +- frontend/src/lang/modules/zh.ts | 19 +- .../host/firewall/docker/detail/index.vue | 207 ++++++++++++++---- .../src/views/host/firewall/docker/index.vue | 124 +++++++++-- .../src/views/host/firewall/docker/model.ts | 2 +- .../src/views/host/firewall/forward/index.vue | 4 + .../views/host/firewall/rule/import/index.vue | 5 +- .../src/views/host/firewall/rule/index.vue | 72 ++++-- .../firewall/rule/operate/err-message.vue | 3 +- .../host/firewall/rule/operate/index.vue | 34 ++- .../src/views/host/firewall/setting/index.vue | 198 ++++++----------- .../src/views/host/firewall/status/index.vue | 33 ++- .../views/host/firewall/utils/validation.ts | 11 + 43 files changed, 1086 insertions(+), 654 deletions(-) create mode 100644 agent/utils/firewall/filter/observed.go diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index 2788a89a1b8b..80bbbcc2f9e6 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -489,6 +489,21 @@ func (b *BaseApi) ListDockerPortGuard(c *gin.Context) { helper.SuccessWithData(c, data) } +// @Tags Firewall +// @Summary List Docker published ports +// @Success 200 {array} dto.DockerPortGuardContainer +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/docker/endpoints [get] +func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) { + data, err := dockerPortGuardService.LoadPublishedPorts(c.Request.Context()) + if err != nil { + handleDockerPortGuardError(c, err) + return + } + helper.SuccessWithData(c, data) +} + // @Tags Firewall // @Summary Sync Docker port guard rules // @Success 200 diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 9f60eaf22e54..8e6c39f3ef87 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -16,6 +16,7 @@ type FirewallSubsystemStatus struct { Version string `json:"version"` PingStatus string `json:"pingStatus"` Message string `json:"message,omitempty"` + Reason string `json:"reason,omitempty"` SyncError string `json:"syncError,omitempty"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"` IPv6 FirewallBackendFamilyStatus `json:"ipv6"` @@ -33,6 +34,7 @@ type FirewallBackendOption struct { Initialized bool `json:"initialized"` Bound bool `json:"bound"` Supported bool `json:"supported"` + SupportReason string `json:"supportReason,omitempty"` Implementation string `json:"implementation,omitempty"` Message string `json:"message,omitempty"` IPv4 FirewallBackendFamilyStatus `json:"ipv4"` diff --git a/agent/app/service/docker.go b/agent/app/service/docker.go index 9fc06a1e6bfe..9051226bd892 100644 --- a/agent/app/service/docker.go +++ b/agent/app/service/docker.go @@ -2,6 +2,7 @@ package service import ( "bufio" + "bytes" "context" "encoding/json" "fmt" @@ -14,14 +15,18 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/docker" ) +const dockerNftablesMinVersion = "29.0.0" + type DockerService struct{} type IDockerService interface { UpdateConf(req dto.SettingUpdate, withRestart bool) error + UpdateFirewallBackend(backend string) error UpdateLogOption(req dto.LogOption) error UpdateIpv6Option(req dto.Ipv6Option) error UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error @@ -30,6 +35,110 @@ type IDockerService interface { OperateDocker(req dto.DockerOperation) error } +func loadDockerEngineVersion(ctx context.Context) string { + client, err := docker.NewDockerClient() + if err == nil { + defer client.Close() + if version, versionErr := client.ServerVersion(ctx); versionErr == nil && version.Version != "" { + return version.Version + } + } + if !cmd.Which("dockerd") { + return "" + } + stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("dockerd", "--version") + if err != nil { + return "" + } + return strings.TrimSpace(stdout) +} + +func dockerNftablesSupported(version string) bool { + return version != "" && common.CompareAppVersion(version, dockerNftablesMinVersion) +} + +func applyDockerFirewallBackendConfig(daemonMap map[string]interface{}, backend, version string) error { + switch backend { + case constant.FirewallProviderNftables: + if !dockerNftablesSupported(version) { + return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion) + } + daemonMap["experimental"] = true + daemonMap["firewall-backend"] = constant.FirewallProviderNftables + case constant.FirewallProviderIptables: + if dockerNftablesSupported(version) { + daemonMap["firewall-backend"] = constant.FirewallProviderIptables + } else { + delete(daemonMap, "firewall-backend") + } + default: + return fmt.Errorf("unsupported Docker firewall backend %q", backend) + } + return nil +} + +func (u *DockerService) UpdateFirewallBackend(backend string) error { + version := loadDockerEngineVersion(context.Background()) + if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) { + return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion) + } + + original, readErr := os.ReadFile(constant.DaemonJsonPath) + existed := readErr == nil + if readErr != nil && !os.IsNotExist(readErr) { + return readErr + } + daemonMap := make(map[string]interface{}) + if len(bytes.TrimSpace(original)) > 0 { + if err := json.Unmarshal(original, &daemonMap); err != nil { + return fmt.Errorf("failed to parse Docker configuration: %w", err) + } + } + if err := applyDockerFirewallBackendConfig(daemonMap, backend, version); err != nil { + return err + } + updated, err := json.MarshalIndent(daemonMap, "", "\t") + if err != nil { + return err + } + if existed && bytes.Equal(bytes.TrimSpace(original), bytes.TrimSpace(updated)) { + return nil + } + if err := os.MkdirAll(path.Dir(constant.DaemonJsonPath), 0755); err != nil { + return err + } + if err := os.WriteFile(constant.DaemonJsonPath, updated, 0640); err != nil { + return err + } + restore := func() error { + if existed { + return os.WriteFile(constant.DaemonJsonPath, original, 0640) + } + err := os.Remove(constant.DaemonJsonPath) + if os.IsNotExist(err) { + return nil + } + return err + } + if err := validateDockerConfig(); err != nil { + if restoreErr := restore(); restoreErr != nil { + return fmt.Errorf("%v; failed to restore Docker configuration: %w", err, restoreErr) + } + return err + } + if err := controller.HandleRestart("docker"); err != nil { + cause := fmt.Errorf("failed to restart Docker: %w", err) + if restoreErr := restore(); restoreErr != nil { + return fmt.Errorf("%v; failed to restore Docker configuration: %w", cause, restoreErr) + } + if restoreRestartErr := controller.HandleRestart("docker"); restoreRestartErr != nil { + return fmt.Errorf("%v; the previous configuration was restored but Docker could not be restarted: %w", cause, restoreRestartErr) + } + return cause + } + return nil +} + func NewIDockerService() IDockerService { return &DockerService{} } @@ -167,7 +276,9 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro delete(daemonMap, "ipv6") delete(daemonMap, "fixed-cidr-v6") delete(daemonMap, "ip6tables") - delete(daemonMap, "experimental") + if configuredDockerFirewallBackend() != constant.FirewallProviderNftables { + delete(daemonMap, "experimental") + } } case "LogOption": if req.Value == "disable" { diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 4b76bdb00291..5a1158b292bf 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -37,7 +37,6 @@ type FirewallService struct { cleanupInactiveBackend func(string) error resetBackend func(string) error baseClient func() (lifecycle.Client, error) - installedProviders func() []string } type firewallRuleRuntimeResolver interface { @@ -81,12 +80,14 @@ func newFirewallService() *FirewallService { cleanupInactiveBackend: cleanupInactiveSystemBackend, resetBackend: resetServiceFirewallBackend, baseClient: selectedSystemFirewallClient, - installedProviders: lifecycle.InstalledProviders, } } func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) { status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"} + if selected := configuredSystemFirewallBackend(); selected != "" { + status.Name, status.Backend = selected, selected + } loadClient := s.baseClient if loadClient == nil { loadClient = selectedSystemFirewallClient @@ -96,14 +97,12 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem if global.LOG != nil { global.LOG.Errorf("load firewall failed, err: %v", err) } - loadInstalled := s.installedProviders - if loadInstalled == nil { - loadInstalled = lifecycle.InstalledProviders - } - if len(loadInstalled()) > 0 { - status.IsExist = true - status.Message = err.Error() + if errors.Is(err, lifecycle.ErrNotInstalled) { + status.Reason = constant.FirewallBackendNotInstalled + return status, nil } + status.IsExist = true + status.Message = err.Error() return status, nil } status.IsExist = true @@ -122,40 +121,10 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem status.IsInit, status.IsBind = initialized, bound status.IPv4 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv4) status.IPv6 = loadSystemFirewallFamilyInfo(status.Name, constant.FirewallFamilyIPv6) - if chainGroup == "base" { - status.ConflictBackend = conflictingDirectFirewallBackend(runtimeStatus.Name) - } } return status, nil } -func conflictingDirectFirewallBackend(provider string) string { - other := "" - switch provider { - case constant.FirewallProviderIptables: - other = constant.FirewallProviderNftables - case constant.FirewallProviderNftables: - other = constant.FirewallProviderIptables - default: - return "" - } - for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { - var ( - bound bool - err error - ) - if other == constant.FirewallProviderIptables { - bound, err = iptables_helper.LoadFamilyBindStatus(family) - } else { - bound, err = nftables_helper.LoadFamilyBindStatus(filter.Family(family)) - } - if err == nil && bound { - return other - } - } - return "" -} - func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error { switch request.Operation { case "disableBanPing": @@ -1058,6 +1027,15 @@ func (s *FirewallService) deleteNativeRuleBatch(ctx context.Context, prepared [] for _, item := range prepared { observed, observeErr := filter.ManagedObserved(snapshot, item.desired) if observeErr != nil { + if errors.Is(observeErr, filter.ErrRuleStale) { + missing, mergeErr := managedFirewallRuleMissing(snapshot, item.desired) + if mergeErr != nil { + return mergeErr + } + if missing { + continue + } + } return observeErr } if observed.Locator.Position == nil { @@ -1077,18 +1055,24 @@ func (s *FirewallService) deleteNativeRuleBatch(ctx context.Context, prepared [] for _, item := range positioned { changes = append(changes, item.change) } - backendPlan, verification, err := runtime.Execute(ctx, snapshot, changes) - if err != nil { - return err - } - if !verification.Matched { - return filter.ErrVerificationFailed + var backendPlan filter.BackendPlan + if len(changes) > 0 { + var verification filter.VerifyResult + backendPlan, verification, err = runtime.Execute(ctx, snapshot, changes) + if err != nil { + return err + } + if !verification.Matched { + return filter.ErrVerificationFailed + } } deleted := make([]model.FirewallRule, 0, len(prepared)) for _, item := range prepared { if err = s.rules.DeleteWithRevision(ctx, item.stored.UUID, item.stored.Revision); err != nil { - err = rollbackFirewallPlan(ctx, runtime, backendPlan, err) + if len(changes) > 0 { + err = rollbackFirewallPlan(ctx, runtime, backendPlan, err) + } return s.restoreDeletedFirewallRecords(ctx, deleted, err) } deleted = append(deleted, item.stored) @@ -1257,20 +1241,10 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string) error observed, managedErr := filter.ManagedObserved(snapshot, desired) if managedErr != nil { if errors.Is(managedErr, filter.ErrRuleStale) { - items, mergeErr := filter.MergeInventory(filter.InventoryMergeInput{ - Observed: snapshot.Rules, - Desired: []filter.DesiredRule{desired}, - }) + missing, mergeErr := managedFirewallRuleMissing(snapshot, desired) if mergeErr != nil { return rollback(mergeErr) } - missing := false - for _, item := range items { - if item.Desired != nil && item.Desired.UUID == desired.UUID { - missing = item.Match == filter.InventoryMatchMissing && item.Observed == nil - break - } - } if missing { continue } @@ -1279,10 +1253,7 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string) error } restoreAtEnd := false if desired.Rule.Scope.Provider == filter.ProviderUFW && observed.Locator.Position != nil { - maxPosition, maxErr := runtime.MaxPosition(ctx, snapshot, desired.Rule) - if maxErr != nil { - return rollback(maxErr) - } + maxPosition := maxObservedFirewallPosition(snapshot) restoreAtEnd = int64(*observed.Locator.Position) == maxPosition } locator := observed.Locator @@ -1304,6 +1275,22 @@ func (s *FirewallService) deleteRule(ctx context.Context, ruleUUID string) error return nil } +func managedFirewallRuleMissing(snapshot filter.Snapshot, desired filter.DesiredRule) (bool, error) { + items, err := filter.MergeInventory(filter.InventoryMergeInput{ + Observed: snapshot.Rules, + Desired: []filter.DesiredRule{desired}, + }) + if err != nil { + return false, err + } + for _, item := range items { + if item.Desired != nil && item.Desired.UUID == desired.UUID { + return item.Match == filter.InventoryMatchMissing && item.Observed == nil, nil + } + } + return false, nil +} + func (s *FirewallService) updateRule(ctx context.Context, clientIP, ruleUUID string, requestedRule filter.FirewallRule) error { prepared, err := s.prepareManagedUpdate(ctx, clientIP, ruleUUID, requestedRule) if err != nil { @@ -1548,10 +1535,7 @@ func (s *FirewallService) executeManagedMutation(ctx context.Context, request ma } appendRule, restoreAtEnd := false, false if after.Scope.Provider == filter.ProviderUFW && request.AdapterOperation == filter.ChangeUpdate { - maxPosition, maxErr := request.Runtime.MaxPosition(ctx, request.Snapshot, after) - if maxErr != nil { - return maxErr - } + maxPosition := maxObservedFirewallPosition(request.Snapshot) appendRule = after.OrderIndex != nil && *after.OrderIndex == maxPosition restoreAtEnd = request.Locator.Position != nil && int64(*request.Locator.Position) == maxPosition } @@ -1598,6 +1582,16 @@ func (s *FirewallService) executeManagedMutation(ctx context.Context, request ma return nil } +func maxObservedFirewallPosition(snapshot filter.Snapshot) int64 { + var maximum int64 + for _, observed := range snapshot.Rules { + if observed.Locator.Position != nil && int64(*observed.Locator.Position) > maximum { + maximum = int64(*observed.Locator.Position) + } + } + return maximum +} + func (s *FirewallService) ensureFirewallRuleIdentityAvailable( ctx context.Context, requested model.FirewallRule, @@ -1705,13 +1699,8 @@ func (s *FirewallService) prepareSystemPortCreate(ctx context.Context, port dto. create.Action = filter.CheckActionSelectAdopt } create.AdoptInstanceKey = check.Candidates[0].InstanceKey - case filter.CheckClassificationCovered: - create.Action = filter.CheckActionCreateAnyway case filter.CheckClassificationConflict: - if !containsFirewallCheckAction(check.AllowedActions, filter.CheckActionCreateAnyway) { - return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason) - } - create.Action = filter.CheckActionCreateAnyway + return nil, fmt.Errorf("cannot manage accepted port %s/%s: %s", port.Port, port.Protocol, check.Reason) case filter.CheckClassificationExactManaged: return nil, nil case filter.CheckClassificationProtected: @@ -2197,15 +2186,6 @@ func firewallCheckFlagCodec() *filter.CheckFlagCodec { return filter.NewCheckFlagCodec(secret, constant.FirewallRuleCheckVersion) } -func containsFirewallCheckAction(actions []filter.CheckAction, expected filter.CheckAction) bool { - for _, action := range actions { - if action == expected { - return true - } - } - return false -} - func OperateFirewallPort(oldPorts, newPorts []int) error { client, err := selectedSystemFirewallClient() if err != nil { diff --git a/agent/app/service/firewall_docker.go b/agent/app/service/firewall_docker.go index 72b06d51cfa8..7281c3708c83 100644 --- a/agent/app/service/firewall_docker.go +++ b/agent/app/service/firewall_docker.go @@ -52,6 +52,7 @@ var ( type IDockerPortGuardService interface { LoadOverview(context.Context) (dto.DockerPortGuardList, error) + LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error) Operate(context.Context, dto.DockerPortGuardOperation) error DeletePolicies(context.Context, dto.DockerPortGuardPolicyBatchDelete) error UpsertPolicies(context.Context, dto.DockerPortGuardPolicyBatch) error @@ -83,6 +84,20 @@ func ReconcileDockerPortGuardBestEffort(ctx context.Context) { } } +func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) { + cli, err := s.client() + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDockerUnavailable, err) + } + defer cli.Close() + + endpoints, err := discoverDockerEndpoints(ctx, cli) + if err != nil { + return nil, err + } + return groupDockerGuardContainers(endpoints), nil +} + func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) { selectedBackend := selectedDockerFirewallBackend("") base := s.runtimeStatus(s.guardRuntime(selectedBackend), selectedBackend) diff --git a/agent/app/service/firewall_selection.go b/agent/app/service/firewall_selection.go index 07fe77d10430..dceb8a347537 100644 --- a/agent/app/service/firewall_selection.go +++ b/agent/app/service/firewall_selection.go @@ -33,8 +33,8 @@ func configuredDockerFirewallBackend() string { } func selectedSystemFirewallClient() (lifecycle.Client, error) { - if provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey); strings.TrimSpace(provider) != "" { - return lifecycle.NewClientFor(strings.TrimSpace(provider)) + if provider := configuredSystemFirewallBackend(); provider != "" { + return lifecycle.NewClientFor(provider) } client, err := lifecycle.NewClient() if err != nil { @@ -44,6 +44,14 @@ func selectedSystemFirewallClient() (lifecycle.Client, error) { return client, nil } +func configuredSystemFirewallBackend() string { + if global.DB == nil { + return "" + } + provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) + return strings.TrimSpace(provider) +} + func NewSelectedSystemFirewallClient() (lifecycle.Client, error) { return selectedSystemFirewallClient() } diff --git a/agent/app/service/firewall_setting.go b/agent/app/service/firewall_setting.go index 608d9923c101..884cf6c425f8 100644 --- a/agent/app/service/firewall_setting.go +++ b/agent/app/service/firewall_setting.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/constant" @@ -41,6 +40,13 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings for _, name := range lifecycle.InstalledProviders() { installed[name] = true } + result.System.Selected = configuredSystemFirewallBackend() + if result.System.Selected == "" { + if client, err := lifecycle.NewClient(); err == nil { + result.System.Selected = client.Name() + } + } + result.System.Current = result.System.Selected for _, name := range []string{ constant.FirewallProviderFirewalld, constant.FirewallProviderUFW, @@ -48,7 +54,7 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings constant.FirewallProviderNftables, } { option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} - if option.Installed { + if option.Installed && name == result.System.Selected { client, err := lifecycle.NewClientFor(name) if err != nil { option.Message = err.Error() @@ -63,25 +69,19 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings option.Message = err.Error() } } - if name == constant.FirewallProviderIptables { + if name == result.System.Selected && name == constant.FirewallProviderIptables { if commands, err := lifecycle.ResolveIptablesCommands(); err == nil { option.Implementation = commands.IPv4 } } result.System.Options = append(result.System.Options, option) } - result.System.Selected, _ = settingRepo.GetValueByKey(constant.FirewallSystemBackendKey) - if result.System.Selected == "" { - if client, err := lifecycle.NewClient(); err == nil { - result.System.Selected = client.Name() - } - } - result.System.Current = result.System.Selected - initializedForwarding := make([]string, 0, 2) + result.Forwarding.Selected = configuredForwardingBackend() + result.Forwarding.Current = result.Forwarding.Selected for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true} - if option.Installed { + if option.Installed && name == result.Forwarding.Selected { manager, err := newForwardingManagerFor(name) if err != nil { option.Message = err.Error() @@ -107,27 +107,19 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings } } } - if option.IPv4.Initialized || option.IPv6.Initialized { - initializedForwarding = append(initializedForwarding, name) - } - if name == constant.FirewallProviderIptables { + if name == result.Forwarding.Selected && name == constant.FirewallProviderIptables { if commands, err := lifecycle.ResolveIptablesCommands(); err == nil { option.Implementation = commands.IPv4 } } result.Forwarding.Options = append(result.Forwarding.Options, option) } - if len(initializedForwarding) == 1 { - result.Forwarding.Current = initializedForwarding[0] - } else if len(initializedForwarding) > 1 { - result.Forwarding.Current = strings.Join(initializedForwarding, " + ") - } - result.Forwarding.Selected, _ = settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) - if result.Forwarding.Selected == "" { - result.Forwarding.Selected = constant.FirewallProviderIptables - } dockerInstalled := cmd.Which("docker") + dockerVersion := "" + if dockerInstalled { + dockerVersion = loadDockerEngineVersion(ctx) + } result.Docker.Selected = configuredDockerFirewallBackend() result.Docker.Current = result.Docker.Selected for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} { @@ -135,15 +127,22 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings Name: name, Installed: installed[name], Supported: dockerInstalled, Active: dockerInstalled && installed[name] && result.Docker.Selected == name, } - guard := docker_guard.NewRuntime(name) - ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6) - option.Initialized = ipv4.Initialized || ipv6.Initialized - option.Bound = ipv4.Bound || ipv6.Bound - option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound - option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound - option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing - option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing - option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason + if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) { + option.Supported = false + option.SupportReason = "docker_version_unsupported" + option.Active = false + } + if option.Active { + guard := docker_guard.NewRuntime(name) + ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6) + option.Initialized = ipv4.Initialized || ipv6.Initialized + option.Bound = ipv4.Bound || ipv6.Bound + option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound + option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound + option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing + option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing + option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason + } result.Docker.Options = append(result.Docker.Options, option) } @@ -220,6 +219,12 @@ func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto. if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil { return err } + if request.Operation == "select" { + if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil { + _ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous) + return err + } + } if request.Operation == "initialize" { if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil { _ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous) @@ -288,6 +293,20 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat } func systemFirewallBackendInitialized(backend string) (bool, error) { + return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor) +} + +func systemFirewallBackendInitializedWithClientFactory( + backend string, + newClient func(string) (lifecycle.Client, error), +) (bool, error) { + client, err := newClient(backend) + if err != nil { + if errors.Is(err, lifecycle.ErrNotInstalled) { + return false, nil + } + return false, err + } if supportsManagedFilterChains(backend) { for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { initialized, _, err := loadSystemFirewallFamilyStatus(backend, family) @@ -300,10 +319,6 @@ func systemFirewallBackendInitialized(backend string) (bool, error) { } return false, nil } - client, err := lifecycle.NewClientFor(backend) - if err != nil { - return false, err - } return client.Status() } diff --git a/agent/app/service/forward.go b/agent/app/service/forward.go index 75e8edf15e40..edb008a99369 100644 --- a/agent/app/service/forward.go +++ b/agent/app/service/forward.go @@ -30,20 +30,13 @@ type IForwardingService interface { } type ForwardingService struct { - managerFactory func() (*forwarding.Manager, error) - rules repo.IForwardingRuleRepo - enabled func() (bool, error) - persistBackend func(string) error - markEnabled func() error - installedProviders func() []string + managerFactory func() (*forwarding.Manager, error) + rules repo.IForwardingRuleRepo + enabled func() (bool, error) + persistBackend func(string) error + markEnabled func() error } -type forwardingCandidate struct { - adapter forwarding.Adapter - runtime forwarding.RuntimeClient -} - -var errForwardingBackendConflict = errors.New("iptables and nftables forwarding backends are both initialized; remove one before continuing") var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected") var forwardingMutationMu sync.Mutex @@ -73,7 +66,6 @@ func newForwardingService() *ForwardingService { persistBackend: func(backend string) error { return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend) }, - installedProviders: lifecycle.InstalledProviders, } } @@ -83,23 +75,14 @@ type forwardingRuleSyncCandidate struct { } func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) { + selected := configuredForwardingBackend() baseInfo := dto.FirewallSubsystemStatus{ - Version: "-", Name: "-", Backend: "-", SyncError: lastForwardingSyncError(), + Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(), } manager, err := s.manager() if err != nil { if errors.Is(err, errForwardingBackendUnavailable) { - loadInstalled := s.installedProviders - if loadInstalled == nil { - loadInstalled = lifecycle.InstalledProviders - } - for _, provider := range loadInstalled() { - if provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables { - baseInfo.IsExist = true - baseInfo.Message = err.Error() - break - } - } + baseInfo.Reason = constant.FirewallBackendNotInstalled return baseInfo, nil } return baseInfo, err @@ -579,66 +562,29 @@ func (s *ForwardingService) manager() (*forwarding.Manager, error) { } func newForwardingManager() (*forwarding.Manager, error) { + return newForwardingManagerFor(configuredForwardingBackend()) +} + +func configuredForwardingBackend() string { selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey) - if strings.TrimSpace(selected) == "" { - selected = constant.FirewallProviderIptables + selected = strings.TrimSpace(selected) + if selected == "" { + return constant.FirewallProviderIptables } - return newForwardingManagerFor(strings.TrimSpace(selected)) + return selected } func newForwardingManagerFor(backend string) (*forwarding.Manager, error) { - clients, err := lifecycle.NewNetfilterClients() + client, err := lifecycle.NewClientFor(backend) if err != nil { - return nil, fmt.Errorf("%w: %v", errForwardingBackendUnavailable, err) - } - candidates := make([]forwardingCandidate, 0, len(clients)) - for _, client := range clients { - adapter, err := forwardingproviders.New(client.Name()) - if err != nil { - return nil, err - } - candidates = append(candidates, forwardingCandidate{adapter: adapter, runtime: client}) - } - if backend != "" { - for _, candidate := range candidates { - if candidate.adapter.Name() == backend { - return forwarding.NewManager(candidate.adapter, candidate.runtime), nil - } - } return nil, fmt.Errorf( - "%w: selected forwarding backend %s %w", - errForwardingBackendUnavailable, backend, lifecycle.ErrNotInstalled, + "%w: selected forwarding backend %s: %w", + errForwardingBackendUnavailable, backend, err, ) } - return selectForwardingManager(candidates) -} - -func selectForwardingManager(candidates []forwardingCandidate) (*forwarding.Manager, error) { - if len(candidates) == 0 { - return nil, errors.New("no supported forwarding backend detected") - } - selected := -1 - for index, candidate := range candidates { - ipv4Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv4) - if err != nil { - return nil, err - } - ipv6Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv6) - if err != nil { - return nil, err - } - initialized := ipv4Initialized || ipv6Initialized - if !initialized { - continue - } - if selected >= 0 { - return nil, errForwardingBackendConflict - } - selected = index - } - if selected < 0 { - selected = 0 + adapter, err := forwardingproviders.New(client.Name()) + if err != nil { + return nil, err } - candidate := candidates[selected] - return forwarding.NewManager(candidate.adapter, candidate.runtime), nil + return forwarding.NewManager(adapter, client), nil } diff --git a/agent/constant/firewall.go b/agent/constant/firewall.go index 20e1e5947126..ebfad368768f 100644 --- a/agent/constant/firewall.go +++ b/agent/constant/firewall.go @@ -1,10 +1,11 @@ package constant const ( - FirewallProviderFirewalld = "firewalld" - FirewallProviderUFW = "ufw" - FirewallProviderIptables = "iptables" - FirewallProviderNftables = "nftables" + FirewallProviderFirewalld = "firewalld" + FirewallProviderUFW = "ufw" + FirewallProviderIptables = "iptables" + FirewallProviderNftables = "nftables" + FirewallBackendNotInstalled = "backend_not_installed" FirewallFamilyIPv4 = "ipv4" FirewallFamilyIPv6 = "ipv6" diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index fdb9adc8265d..8e800628e714 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -43,6 +43,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain) hostRouter.GET("/firewall/docker/ports", baseApi.ListDockerPortGuard) + hostRouter.GET("/firewall/docker/endpoints", baseApi.ListDockerPublishedPorts) hostRouter.POST("/firewall/docker/sync", baseApi.SyncDockerPortGuard) hostRouter.POST("/firewall/docker/operate", baseApi.OperateDockerPortGuard) hostRouter.POST("/firewall/docker/policies/batch", baseApi.UpsertDockerPortGuardPolicies) diff --git a/agent/utils/firewall/docker_guard/policy.go b/agent/utils/firewall/docker_guard/policy.go index 22195886782d..d8c72d42212c 100644 --- a/agent/utils/firewall/docker_guard/policy.go +++ b/agent/utils/firewall/docker_guard/policy.go @@ -55,8 +55,8 @@ func NormalizePolicy(policy Policy) (Policy, error) { normalizedSources = append(normalizedSources, canonical) } } - if policy.Mode == ModeSources && len(normalizedSources) == 0 { - return Policy{}, fmt.Errorf("%w: deny_sources requires at least one source", ErrInvalidPolicy) + if policy.Mode != ModeAll && len(normalizedSources) == 0 { + return Policy{}, fmt.Errorf("%w: source-based modes require at least one source", ErrInvalidPolicy) } if policy.Mode == ModeAll { normalizedSources = []string{} diff --git a/agent/utils/firewall/filter/check.go b/agent/utils/firewall/filter/check.go index 1b2415254c39..95bb0d04b6f9 100644 --- a/agent/utils/firewall/filter/check.go +++ b/agent/utils/firewall/filter/check.go @@ -29,7 +29,6 @@ const ( CheckClassificationNone CheckClassification = "none" CheckClassificationExactManaged CheckClassification = "exact_managed" CheckClassificationExactExternal CheckClassification = "exact_external" - CheckClassificationCovered CheckClassification = "covered" CheckClassificationConflict CheckClassification = "conflict" CheckClassificationUnsupported CheckClassification = "unsupported" CheckClassificationProtected CheckClassification = "protected" @@ -38,11 +37,10 @@ const ( type CheckAction string const ( - CheckActionCreate CheckAction = "create" - CheckActionCreateAnyway CheckAction = "create_anyway" - CheckActionAdopt CheckAction = "adopt" - CheckActionSelectAdopt CheckAction = "select_adopt" - CheckActionCancel CheckAction = "cancel" + CheckActionCreate CheckAction = "create" + CheckActionAdopt CheckAction = "adopt" + CheckActionSelectAdopt CheckAction = "select_adopt" + CheckActionCancel CheckAction = "cancel" ) type RuleCheckResult struct { @@ -111,9 +109,6 @@ func CheckCreate( } exact := make([]ObservedRule, 0) - covered := make([]ObservedRule, 0) - conflicts := make([]ObservedRule, 0) - partialConflicts := make([]ObservedRule, 0) for _, observed := range snapshot.Rules { if observed.ParseStatus != ParseStatusSupported { if observed.Rule.Scope.Provider == ProviderFirewalld && observed.Rule.NativeKind == NativeKindZoneService { @@ -136,21 +131,6 @@ func CheckCreate( } if observedKey == ruleKey { exact = append(exact, observed) - continue - } - if RuleCovers(observedRule, normalized) && observedRule.Action == normalized.Action { - covered = append(covered, observed) - continue - } - if RulesOverlap(observedRule, normalized) && observedRule.Action != normalized.Action { - if allowsOrderedFirewalldException(observed, normalized) { - continue - } - if RuleCovers(observedRule, normalized) { - conflicts = append(conflicts, observed) - } else { - partialConflicts = append(partialConflicts, observed) - } } } @@ -177,24 +157,7 @@ func CheckCreate( result.Reason = "multiple_equivalent_external_rules" result.Candidates = exact result.AllowedActions = []CheckAction{CheckActionSelectAdopt, CheckActionCancel} - case len(conflicts) > 0: - result.Decision = CheckDecisionBlocked - result.Classification = CheckClassificationConflict - result.Reason = "overlapping_rule_with_different_action" - result.Candidates = conflicts case result.Classification == CheckClassificationUnsupported: - case len(partialConflicts) > 0: - result.Decision = CheckDecisionConfirmationRequired - result.Classification = CheckClassificationConflict - result.Reason = "partially_overlapping_rule_with_different_action" - result.Candidates = partialConflicts - result.AllowedActions = []CheckAction{CheckActionCreateAnyway, CheckActionCancel} - case len(covered) > 0: - result.Decision = CheckDecisionConfirmationRequired - result.Classification = CheckClassificationCovered - result.Reason = "requested_rule_is_covered" - result.Candidates = covered - result.AllowedActions = []CheckAction{CheckActionCreateAnyway, CheckActionCancel} default: result.AllowedActions = []CheckAction{CheckActionCreate} } @@ -215,13 +178,6 @@ func finishCheck(result RuleCheckResult) (RuleCheckResult, error) { return result, nil } -func allowsOrderedFirewalldException(existing ObservedRule, requested FirewallRule) bool { - return !existing.Protected && requested.Scope.Provider == ProviderFirewalld && - requested.NativeKind == NativeKindRichRule && requested.Priority != nil && *requested.Priority < 0 && - (requested.Action == ActionDrop || requested.Action == ActionReject) && - existing.Rule.NativeKind == NativeKindZonePort && existing.Rule.Action == ActionAccept -} - func containsPersistenceDrift(rules []ObservedRule) bool { for _, rule := range rules { if rule.Persistence != "" && rule.Persistence != PersistenceStatusConverged { diff --git a/agent/utils/firewall/filter/check_flag.go b/agent/utils/firewall/filter/check_flag.go index 2eee4b2d1a08..4e90c6e9ea8e 100644 --- a/agent/utils/firewall/filter/check_flag.go +++ b/agent/utils/firewall/filter/check_flag.go @@ -107,7 +107,7 @@ func (c *CheckFlagCodec) Authorize( } switch action { - case CheckActionCreate, CheckActionCreateAnyway: + case CheckActionCreate: if strings.TrimSpace(adoptInstanceKey) != "" { return CreateAuthorization{}, ErrRuleOperation } diff --git a/agent/utils/firewall/filter/inventory.go b/agent/utils/firewall/filter/inventory.go index aa414abf92f7..a7d4d753a3ad 100644 --- a/agent/utils/firewall/filter/inventory.go +++ b/agent/utils/firewall/filter/inventory.go @@ -86,6 +86,10 @@ func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) { byMarker := make(map[string][]int) for index, observed := range input.Observed { candidate := observedInventoryCandidate{rule: observed} + if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" { + markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker + byMarker[markerKey] = append(byMarker[markerKey], index) + } if observed.ParseStatus == ParseStatusSupported { normalized, err := NormalizeRule(observed.Rule) if err != nil { @@ -101,10 +105,6 @@ func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) { candidate.instanceKey = instanceKey byInstanceKey[instanceKey] = append(byInstanceKey[instanceKey], index) } - if marker := strings.TrimSpace(candidate.rule.Marker); marker != "" { - markerKey := candidate.rule.Rule.Scope.Key() + "\x00" + marker - byMarker[markerKey] = append(byMarker[markerKey], index) - } } candidates[index] = candidate } @@ -144,9 +144,16 @@ func MergeInventory(input InventoryMergeInput) ([]InventoryItem, error) { if desiredIndex, exists := desiredMatches[index]; exists { desired := normalizedDesired[desiredIndex] observed := candidate.rule + match := desiredMatchStates[desiredIndex] + if match == InventoryMatchExact && observed.ParseStatus != ParseStatusSupported { + orderIndex := observed.Rule.OrderIndex + observed.Rule = desired.Rule + observed.Rule.OrderIndex = orderIndex + observed.ParseStatus = ParseStatusSupported + observed.UncertainFields = nil + } displayRule := observed.Rule displayRule.Description = desired.Rule.Description - match := desiredMatchStates[desiredIndex] state := inventoryStateForDesired(desired, match) if observed.Protected { state = InventoryStateProtected @@ -202,7 +209,8 @@ func findObservedInventoryMatch( if marker := strings.TrimSpace(desired.Marker); marker != "" { markerKey := desired.Rule.Scope.Key() + "\x00" + marker match, status := uniqueUnclaimedCandidate(byMarker[markerKey], candidates) - if match >= 0 && candidates[match].ruleKey != desired.RuleKey { + if match >= 0 && candidates[match].rule.ParseStatus != ParseStatusOpaque && + !ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) { return match, InventoryMatchChanged } return match, status diff --git a/agent/utils/firewall/filter/model.go b/agent/utils/firewall/filter/model.go index f4b8dfc6e585..a4a9bb970572 100644 --- a/agent/utils/firewall/filter/model.go +++ b/agent/utils/firewall/filter/model.go @@ -334,14 +334,15 @@ type Locator struct { } type ObservedRule struct { - Rule FirewallRule `json:"rule"` - Locator Locator `json:"locator"` - InstanceKey string `json:"instanceKey,omitempty"` - Marker string `json:"marker,omitempty"` - ParseStatus ParseStatus `json:"parseStatus"` - Raw string `json:"raw,omitempty"` - Protected bool `json:"protected"` - Persistence PersistenceStatus `json:"persistence,omitempty"` + Rule FirewallRule `json:"rule"` + Locator Locator `json:"locator"` + InstanceKey string `json:"instanceKey,omitempty"` + Marker string `json:"marker,omitempty"` + ParseStatus ParseStatus `json:"parseStatus"` + UncertainFields []string `json:"uncertainFields,omitempty"` + Raw string `json:"raw,omitempty"` + Protected bool `json:"protected"` + Persistence PersistenceStatus `json:"persistence,omitempty"` } type Snapshot struct { diff --git a/agent/utils/firewall/filter/observed.go b/agent/utils/firewall/filter/observed.go new file mode 100644 index 000000000000..a1f458e3c838 --- /dev/null +++ b/agent/utils/firewall/filter/observed.go @@ -0,0 +1,25 @@ +package filter + +const ObservedFieldProtocol = "protocol" + +// ObservedRuleMatchesExpected compares the semantic fields that the backend +// could actually observe. Partial rules name fields omitted by their native +// listing; those fields are supplied from the expected rule before comparing +// normalized identities. +func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool { + if observed.ParseStatus == ParseStatusOpaque { + return false + } + hydrated := observed.Rule + for _, field := range observed.UncertainFields { + switch field { + case ObservedFieldProtocol: + hydrated.Protocol = expected.Protocol + default: + return false + } + } + gotKey, gotErr := RuleKey(hydrated) + wantKey, wantErr := RuleKey(expected) + return gotErr == nil && wantErr == nil && gotKey == wantKey +} diff --git a/agent/utils/firewall/filter/providers/ufw/adapter.go b/agent/utils/firewall/filter/providers/ufw/adapter.go index c649f4779473..2a7b7d389994 100644 --- a/agent/utils/firewall/filter/providers/ufw/adapter.go +++ b/agent/utils/firewall/filter/providers/ufw/adapter.go @@ -285,6 +285,8 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte command := insertCommand(position, normalized, marker) if change.Append { command = commentCommand(normalized, marker) + plan.Expected.Locator.NativeID = "" + plan.Expected.Locator.Position = nil } plan.Commands = []filter.NativeCommand{command} plan.RollbackCommands = []filter.NativeCommand{deleteRuleCommand(normalized, marker)} @@ -311,14 +313,27 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte } targetPosition = int(*normalized.OrderIndex) } + maximumPosition := maximumObservedPosition(snapshot) + if targetPosition > maximumPosition { + return filter.NativeRulePlan{}, fmt.Errorf( + "%w: update target position %d is out of range 1-%d", + filter.ErrInvalidRule, targetPosition, maximumPosition, + ) + } + appendAtEnd := change.Append || targetPosition == maximumPosition + restoreAtEnd := change.RestoreAtEnd || position == maximumPosition plan.Previous = &target plan.Expected = observedForRule(normalized, marker, targetPosition) + if appendAtEnd { + plan.Expected.Locator.NativeID = "" + plan.Expected.Locator.Position = nil + } plan.Commands = []filter.NativeCommand{ deletePositionCommand(position), - positionedCommand(targetPosition, normalized, marker, change.Append), + positionedCommand(targetPosition, normalized, marker, appendAtEnd), } plan.RollbackCommands = []filter.NativeCommand{ - positionedCommand(position, target.Rule, observedComment(target), change.RestoreAtEnd), + positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), deleteRuleCommand(normalized, marker), } case filter.ChangeDelete: @@ -330,8 +345,9 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte plan.Previous = &target plan.Expected = target plan.Commands = []filter.NativeCommand{deletePositionCommand(position)} + restoreAtEnd := change.RestoreAtEnd || position == maximumObservedPosition(snapshot) plan.RollbackCommands = []filter.NativeCommand{ - positionedCommand(position, target.Rule, observedComment(target), change.RestoreAtEnd), + positionedCommand(position, target.Rule, observedComment(target), restoreAtEnd), } case filter.ChangeReorder: return filter.NativeRulePlan{}, fmt.Errorf("%w: ufw reorder is not supported", filter.ErrUnsupportedScope) @@ -390,9 +406,6 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang if target.Protected { return filter.ObservedRule{}, filter.ErrProtectedRule } - if target.ParseStatus != filter.ParseStatusSupported { - return filter.ObservedRule{}, fmt.Errorf("%w: opaque ufw rules cannot be modified", filter.ErrInvalidRule) - } if requireOwned { if target.Marker != marker { return filter.ObservedRule{}, fmt.Errorf("%w: ufw rule is not owned by this rule UUID", filter.ErrInvalidRule) @@ -400,6 +413,17 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang } else if target.Marker != "" { return filter.ObservedRule{}, fmt.Errorf("%w: ufw adoption target is already marked", filter.ErrInvalidRule) } + if target.Marker == marker && target.ParseStatus != filter.ParseStatusSupported && + (target.ParseStatus == filter.ParseStatusOpaque || filter.ObservedRuleMatchesExpected(target, desired)) { + orderIndex := target.Rule.OrderIndex + target.Rule = desired + target.Rule.OrderIndex = orderIndex + target.ParseStatus = filter.ParseStatusSupported + target.UncertainFields = nil + } + if target.ParseStatus != filter.ParseStatusSupported { + return filter.ObservedRule{}, fmt.Errorf("%w: opaque ufw rules cannot be modified", filter.ErrInvalidRule) + } wantKey, err := filter.RuleKey(desired) if err != nil { return filter.ObservedRule{}, err @@ -418,14 +442,18 @@ func insertionPosition(snapshot filter.Snapshot, rule filter.FirewallRule) int { if rule.OrderIndex != nil && *rule.OrderIndex > 0 { return int(*rule.OrderIndex) } - maxPosition := 0 + position := maximumObservedPosition(snapshot) + 1 + return position +} + +func maximumObservedPosition(snapshot filter.Snapshot) int { + maximum := 0 for _, observed := range snapshot.Rules { - if observed.Locator.Position != nil && *observed.Locator.Position > maxPosition { - maxPosition = *observed.Locator.Position + if observed.Locator.Position != nil && *observed.Locator.Position > maximum { + maximum = *observed.Locator.Position } } - position := maxPosition + 1 - return position + return maximum } func observedForRule(rule filter.FirewallRule, marker string, position int) filter.ObservedRule { @@ -536,17 +564,21 @@ func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.V if count != 1 { return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil } - wantKey, err := filter.RuleKey(rulePlan.Expected.Rule) - if err != nil { - return filter.VerifyResult{}, err - } for _, observed := range snapshot.Rules { - if observed.Marker != marker || observed.ParseStatus != filter.ParseStatusSupported { + if observed.Marker != marker { continue } - gotKey, keyErr := filter.RuleKey(observed.Rule) - if keyErr != nil || gotKey != wantKey || observed.Locator.Position == nil || rulePlan.Expected.Locator.Position == nil || - *observed.Locator.Position != *rulePlan.Expected.Locator.Position { + 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 + } + if !semanticMatches || !positionMatches { return filter.VerifyResult{Snapshot: snapshot, Matched: false}, nil } return filter.VerifyResult{Snapshot: snapshot, Matched: true}, nil @@ -657,6 +689,7 @@ func parseNumberedRules(scope filter.Scope, output string) []filter.ObservedRule func parseNumberedRule(scope filter.Scope, position int, destination, action, direction, source, raw string) filter.ObservedRule { comment, source := splitComment(source) + marker := ownedMarker(comment) positionCopy := position locator := filter.Locator{ Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position), @@ -674,12 +707,13 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di opaque := func() filter.ObservedRule { return filter.ObservedRule{ Rule: partialRule, + Marker: marker, Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged, } } partial := func() filter.ObservedRule { return filter.ObservedRule{ - Rule: partialRule, Locator: locator, ParseStatus: filter.ParseStatusPartial, Raw: raw, + Rule: partialRule, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusPartial, Raw: raw, Persistence: filter.PersistenceStatusConverged, } } @@ -717,7 +751,7 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di SourceAddress: sourceAddress, Interface: profileInterface, Action: ruleAction, OrderIndex: &order, Description: profile, }, - Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw, + Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged, } } @@ -725,8 +759,10 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di partialRule.DestinationAddress = destinationAddress partialRule.DestinationPort = destinationPort partialRule.Interface = iface - partialRule.Description = comment - if partialRule.Description == "" { + if marker == "" { + partialRule.Description = comment + } + if partialRule.Description == "" && marker == "" { partialRule.Description = destinationAnnotation } if destinationAnnotation != "" && validApplicationProfileName(destinationAnnotation) { @@ -742,10 +778,7 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di SourceAddress: sourceAddress, DestinationAddress: destinationAddress, DestinationPort: destinationPort, Interface: iface, Action: ruleAction, OrderIndex: &order, } - marker := "" - if strings.HasPrefix(comment, "1panel-rule:") && strings.TrimSpace(strings.TrimPrefix(comment, "1panel-rule:")) != "" { - marker = comment - } else { + if marker == "" { rule.Description = comment if rule.Description == "" { rule.Description = destinationAnnotation @@ -756,9 +789,15 @@ func parseNumberedRule(scope filter.Scope, position int, destination, action, di return opaque() } locator.Canonical = canonicalRule(normalized) + parseStatus := filter.ParseStatusSupported + var uncertainFields []string + if normalized.Protocol == "all" && normalized.DestinationPort == "" { + parseStatus = filter.ParseStatusPartial + uncertainFields = []string{filter.ObservedFieldProtocol} + } return filter.ObservedRule{ - Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, - Raw: raw, Persistence: filter.PersistenceStatusConverged, + Rule: normalized, Locator: locator, Marker: marker, ParseStatus: parseStatus, + UncertainFields: uncertainFields, Raw: raw, Persistence: filter.PersistenceStatusConverged, } } @@ -834,7 +873,8 @@ func parseUnrecognizedNumberedRule(scope filter.Scope, raw string) (filter.Obser return filter.ObservedRule{}, "", false, false } positionCopy := position - _, familyInput := splitComment(matches[2]) + comment, familyInput := splitComment(matches[2]) + marker := ownedMarker(comment) family := filter.FamilyIPv4 if strings.Contains(familyInput, "(v6)") || containsIPv6Address(familyInput) { family = filter.FamilyIPv6 @@ -850,10 +890,18 @@ func parseUnrecognizedNumberedRule(scope filter.Scope, raw string) (filter.Obser Provider: filter.ProviderUFW, ScopeKey: scope.Key(), NativeID: strconv.Itoa(position), Canonical: normalizedDisplay(raw), Position: &positionCopy, }, - ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged, + Marker: marker, ParseStatus: filter.ParseStatusOpaque, Raw: raw, Persistence: filter.PersistenceStatusConverged, }, family, inbound, true } +func ownedMarker(comment string) string { + comment = strings.TrimSpace(comment) + if strings.HasPrefix(comment, "1panel-rule:") && strings.TrimSpace(strings.TrimPrefix(comment, "1panel-rule:")) != "" { + return comment + } + return "" +} + func splitComment(source string) (string, string) { index := strings.Index(source, "#") if index < 0 { @@ -887,6 +935,9 @@ func parseDestination(value string) (address, port, protocol, iface, annotation if isAnywhere(tokens[0]) { return "", "", "all", iface, annotation, true } + if parsedAddress, parsedProtocol, endpointOK := parseAddressProtocol(tokens[0]); endpointOK { + return parsedAddress, "", parsedProtocol, iface, annotation, true + } if parsedPort, parsedProtocol, portOK := parsePortProtocol(tokens[0]); portOK { return "", parsedPort, parsedProtocol, iface, annotation, true } @@ -903,6 +954,23 @@ func parseDestination(value string) (address, port, protocol, iface, annotation return "", "", "", "", "", false } +func parseAddressProtocol(value string) (string, string, bool) { + separator := strings.LastIndex(value, "/") + if separator <= 0 || separator == len(value)-1 { + return "", "", false + } + protocol := strings.ToLower(strings.TrimSpace(value[separator+1:])) + if protocol != "tcp" && protocol != "udp" { + return "", "", false + } + endpoint := strings.TrimSpace(value[:separator]) + if isAnywhere(endpoint) { + return "", protocol, true + } + address, ok := parseAddress(endpoint) + return address, protocol, ok +} + func splitDestinationAnnotation(value string) (string, string) { value = strings.TrimSpace(value) if !strings.HasSuffix(value, ")") { diff --git a/agent/utils/firewall/lifecycle/lifecycle.go b/agent/utils/firewall/lifecycle/lifecycle.go index 4637b03363d1..43926d5ab45b 100644 --- a/agent/utils/firewall/lifecycle/lifecycle.go +++ b/agent/utils/firewall/lifecycle/lifecycle.go @@ -125,7 +125,7 @@ func NewClientFor(provider string) (Client, error) { case "iptables": commands, err := ResolveIptablesCommands() if err != nil { - return nil, err + return nil, fmt.Errorf("iptables %w: %v", ErrNotInstalled, err) } return providers.NewIptables(commands.IPv4) case "nftables": diff --git a/frontend/src/api/interface/firewall.ts b/frontend/src/api/interface/firewall.ts index 6935ee72237b..15ed0ce19902 100644 --- a/frontend/src/api/interface/firewall.ts +++ b/frontend/src/api/interface/firewall.ts @@ -11,6 +11,7 @@ export namespace Firewall { initialized: boolean; bound: boolean; supported: boolean; + supportReason?: string; implementation?: string; message?: string; ipv4: BackendFamilyStatus; @@ -50,6 +51,7 @@ export namespace Firewall { version: string; pingStatus: string; message?: string; + reason?: string; syncError?: string; ipv4: BackendFamilyStatus; ipv6: BackendFamilyStatus; @@ -132,6 +134,7 @@ export namespace Firewall { instanceKey?: string; marker?: string; parseStatus: ParseStatus; + uncertainFields?: string[]; raw?: string; protected: boolean; persistence?: PersistenceStatus; @@ -194,8 +197,8 @@ export namespace Firewall { export type CheckDecision = 'ready' | 'confirmation_required' | 'blocked' | 'no_change'; export type CheckClassification = - 'none' | 'exact_managed' | 'exact_external' | 'covered' | 'conflict' | 'unsupported' | 'protected'; - export type CheckAction = 'create' | 'create_anyway' | 'adopt' | 'select_adopt' | 'cancel'; + 'none' | 'exact_managed' | 'exact_external' | 'conflict' | 'unsupported' | 'protected'; + export type CheckAction = 'create' | 'adopt' | 'select_adopt' | 'cancel'; export type ApplicableCheckAction = Exclude; export interface RuleCheckResult { diff --git a/frontend/src/api/modules/firewall.ts b/frontend/src/api/modules/firewall.ts index b6fd94e659bc..962942268e92 100644 --- a/frontend/src/api/modules/firewall.ts +++ b/frontend/src/api/modules/firewall.ts @@ -70,6 +70,9 @@ export const reorderFirewallRule = (uuid: string, request: Firewall.ReorderReque export const loadDockerPortGuard = () => http.get('/hosts/firewall/docker/ports', {}, { timeout: TimeoutEnum.T_40S }); +export const loadDockerPublishedPorts = () => + http.get('/hosts/firewall/docker/endpoints', {}, { timeout: TimeoutEnum.T_40S }); + export const syncDockerPortGuard = () => http.post('/hosts/firewall/docker/sync', {}, TimeoutEnum.T_60S); export const operateDockerPortGuard = (operation: 'initialize' | 'bind' | 'unbind') => diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 81b0e32894ff..4f78356eba35 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -4067,20 +4067,16 @@ const message = { plan_equivalent_external_rule: 'An identical external rule already exists. Take it over instead of creating a duplicate.', plan_multiple_equivalent_external_rules: 'Multiple identical external rules exist. Select one to take over.', - plan_requested_rule_is_covered: - 'An existing rule already covers this configuration. Continuing will create an overlapping rule.', plan_equivalent_managed_rule: 'An identical rule is already managed by 1Panel. No duplicate is needed.', 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_warning: 'Warning', ruleCheckStatus_error: 'Error', ruleCheckExistingHelper: 'An identical rule already exists and will be skipped.', ruleCheckReadyHelper: 'The check passed. This rule can be created.', ruleCheckExternalExists: 'An identical external rule already exists and will be skipped automatically.', ruleCheckBlockedHelper: 'Rules with errors cannot be submitted. Go back, edit them, and check again.', - ruleCheckWarningHelper: 'Check complete. Warnings do not block submission, so you can continue.', plan_managed_rule_drifted: 'The 1Panel management record differs from the live firewall rule. Check the system rule.', plan_opaque_rule_in_target_scope: @@ -4088,10 +4084,6 @@ const message = { plan_runtime_permanent_mismatch: 'The runtime and permanent firewall configurations differ. Synchronize them first.', plan_protected_rule: 'This rule is protected and cannot be taken over, changed, or deleted.', - plan_overlapping_rule_with_different_action: - 'An overlapping rule has the opposite action. The operation was stopped.', - plan_partially_overlapping_rule_with_different_action: - 'This rule partially overlaps a rule with the opposite action. The effective result depends on rule order.', plan_blocked: 'This rule cannot be applied safely. Refresh the rules and try again.', scopeDefaultMismatch: 'The system default zone is {0}; this page manages only the public zone.', scopeInactive: 'The managed scope is inactive. New rules may not affect current traffic.', @@ -4121,6 +4113,7 @@ const message = { systemFirewallHelper: 'Controls host port access and inbound firewall rules.', forwardingHelper: 'Manages port-forwarding rules.', dockerFirewallHelper: 'Selects how 1Panel manages container port protection.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', backendRecommendation: 'Using iptables or nftables is recommended.', configuredRules: '{0} rules configured', addressFamily: 'IP version', @@ -4187,11 +4180,14 @@ const message = { 'Delete all 1Panel Docker port protection rules and chains from {0}, retaining only database data', cleanupBeforeBackendSwitch: 'The current {0} backend still contains 1Panel runtime rules. Clean it up before switching to {1}.', - cleanupAction: 'Clean up', + cleanupAction: 'Reset', backendSwitchNotice: 'Clean the current backend before switching the system firewall, port forwarding, or Docker protection. Saved database policies are retained and can be initialized or synchronized after switching.', switchBackendHelper: 'Switch to {0}?', + switchDockerBackendHelper: 'Switch to {0}? This will update the Docker configuration and restart Docker.', uninstalledStatus: 'Not installed', + selectedBackendNotInstalled: + '{backend} was not detected. Install it manually from {library}, or switch the firewall backend in {settings}.', initializedStatus: 'Initialized', partiallyInitialized: 'Partially initialized', dockerGuardHelper: @@ -4224,7 +4220,10 @@ const message = { denySources: 'Deny specified sources', allowSources: 'Allow specified sources only', denyAll: 'Deny all access', - sourcesHelper: 'Enter one IPv4/IPv6 address or CIDR per line; an empty allowlist denies all access', + dockerGuardMixedFamilyHelper: + 'IPv4 and IPv6 are both selected. Configure source-based policies separately; deny all can be applied directly.', + dockerGuardInconsistentConfigHelper: + 'The selected rules have different configurations. Configure them again to overwrite all selected rules; an empty description clears all descriptions.', effective: 'Effective', forwardUnsynced: 'Unsynced', notEnabled: 'Not enabled', @@ -4284,7 +4283,10 @@ const message = { destinationPortPlaceholder: 'e.g. 80, 80,443, or 8080-8089', deleteRuleConfirm: 'Will delete {0} rules. Continue?', deleteUsedRuleConfirm: - 'This port is used by {0}. Deleting its allow rule may make the service unreachable. Continue?', + 'This rule covers listening services provided by {0}. Deleting it may make those services unreachable. Continue?', + 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?', }, diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 796f8dc32d9d..8d03e7321951 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4110,31 +4110,23 @@ const message = { plan_equivalent_external_rule: 'Ya existe una regla externa idéntica. Puedes asumir su gestión sin duplicarla.', plan_multiple_equivalent_external_rules: 'Existen varias reglas externas idénticas. Selecciona una para gestionarla.', - plan_requested_rule_is_covered: - 'Una regla existente ya cubre esta configuración. Continuar creará una regla superpuesta.', plan_equivalent_managed_rule: '1Panel ya gestiona una regla idéntica. No es necesario duplicarla.', 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_warning: 'Advertencia', ruleCheckStatus_error: 'Error', ruleCheckExistingHelper: 'Ya existe una regla idéntica y se omitirá.', ruleCheckReadyHelper: 'La comprobación se superó. Esta regla se puede crear.', ruleCheckExternalExists: 'Ya existe una regla externa idéntica y se omitirá automáticamente.', ruleCheckBlockedHelper: 'Las reglas con errores no se pueden enviar. Vuelve, modifícalas y compruébalas de nuevo.', - ruleCheckWarningHelper: 'Comprobación finalizada. Las advertencias no impiden el envío; puedes continuar.', plan_managed_rule_drifted: 'La regla gestionada no coincide con el firewall activo. Resuelve primero la diferencia.', plan_opaque_rule_in_target_scope: 'El ámbito contiene una regla que no puede analizarse con seguridad. La operación se detuvo.', plan_runtime_permanent_mismatch: 'Las configuraciones activa y permanente difieren. Sincronízalas primero.', plan_protected_rule: 'Esta regla está protegida y no se puede asumir, modificar ni eliminar.', - plan_overlapping_rule_with_different_action: - 'Existe una regla superpuesta con la acción opuesta. La operación se detuvo.', - plan_partially_overlapping_rule_with_different_action: - 'La regla se superpone parcialmente con otra de acción opuesta. El resultado depende del orden de las reglas.', plan_blocked: 'La regla no puede aplicarse de forma segura. Actualiza la lista e inténtalo de nuevo.', scopeDefaultMismatch: 'La zona predeterminada es {0}; esta página solo gestiona la zona public.', scopeInactive: 'El ámbito gestionado está inactivo. Las reglas nuevas podrían no afectar al tráfico actual.', @@ -4165,6 +4157,7 @@ const message = { systemFirewallHelper: 'Controla el acceso a los puertos del host y las reglas de entrada.', forwardingHelper: 'Gestiona las reglas de reenvío de puertos.', dockerFirewallHelper: 'Selecciona cómo gestiona 1Panel la protección de puertos de contenedores.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', backendRecommendation: 'Se recomienda usar iptables o nftables.', configuredRules: '{0} reglas configuradas', addressFamily: 'Versión de IP', @@ -4182,10 +4175,11 @@ const message = { 'Elimina de {0} todas las reglas y cadenas de protección de puertos Docker de 1Panel, conservando solo los datos de la base de datos', cleanupBeforeBackendSwitch: 'El backend actual {0} todavía contiene reglas de ejecución de 1Panel. Límpielo antes de cambiar a {1}.', - cleanupAction: 'Limpiar', + cleanupAction: 'Restablecer', backendSwitchNotice: 'Limpia el backend actual antes de cambiar el firewall del sistema, el reenvío de puertos o la protección Docker. Las políticas de la base de datos se conservan y pueden inicializarse o sincronizarse después.', switchBackendHelper: '¿Cambiar a {0}?', + switchDockerBackendHelper: '¿Cambiar a {0}? Esto actualizará la configuración y reiniciará Docker.', ruleSyncTitle: 'Sincronizar reglas', ruleSyncAction: 'Sincronizar reglas', ruleSyncHelper: @@ -4238,6 +4232,8 @@ const message = { blocked: 'No disponible', }, uninstalledStatus: 'No instalado', + selectedBackendNotInstalled: + 'No se detectó el servicio {backend}. Instálelo manualmente desde {library} o cambie el backend del firewall en {settings}.', initializedStatus: 'Inicializado', partiallyInitialized: 'Parcialmente inicializado', dockerGuardHelper: @@ -4271,8 +4267,10 @@ const message = { denySources: 'Denegar orígenes especificados', allowSources: 'Permitir solo los orígenes especificados', denyAll: 'Denegar todo acceso', - sourcesHelper: - 'Introduce una dirección IPv4/IPv6 o CIDR por línea; una lista de permitidos vacía deniega todo acceso', + dockerGuardMixedFamilyHelper: + 'Se han seleccionado IPv4 e IPv6. Configura por separado las políticas basadas en el origen; denegar todo puede aplicarse directamente.', + dockerGuardInconsistentConfigHelper: + 'Las reglas seleccionadas tienen configuraciones diferentes. Configúralas de nuevo para sobrescribirlas todas; una descripción vacía borrará todas las descripciones.', effective: 'En vigor', forwardUnsynced: 'Sin sincronizar', notEnabled: 'No habilitado', @@ -4335,7 +4333,11 @@ const message = { destinationPortPlaceholder: 'p. ej. 80, 80,443 o 8080-8089', deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?', deleteUsedRuleConfirm: - 'Este puerto está siendo utilizado por {0}. Eliminar la regla de acceso puede dejar el servicio inaccesible. ¿Continuar?', + 'Esta regla abarca servicios en escucha proporcionados por {0}. Eliminarla puede hacer que esos servicios sean inaccesibles. ¿Continuar?', + deleteWildcardRuleConfirm: + '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?', }, diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index b2f23c4be81e..a77920fe105b 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -4025,27 +4025,20 @@ const message = { adoptRuleConfirm: 'پس از واگذاری، 1Panel می‌تواند این قانون موجود را نگهداری و حذف کند. ادامه می‌دهید؟', plan_equivalent_external_rule: 'یک قانون خارجی یکسان وجود دارد. به‌جای ایجاد تکراری، مدیریت آن را واگذار کنید.', plan_multiple_equivalent_external_rules: 'چند قانون خارجی یکسان وجود دارد. یکی را برای مدیریت انتخاب کنید.', - plan_requested_rule_is_covered: - 'یک قانون موجود این تنظیم را پوشش می‌دهد. ادامه باعث ایجاد قانون هم‌پوشان می‌شود.', plan_equivalent_managed_rule: 'یک قانون یکسان از قبل توسط 1Panel مدیریت می‌شود و نیازی به تکرار نیست.', allRulesAlreadyExist: 'هر {0} قانون بررسی‌شده از قبل وجود دارند. قانون جدیدی برای ایجاد وجود ندارد.', ruleCheckResult: 'نتایج بررسی قوانین', ruleCheckStatus_creatable: 'قابل ایجاد', ruleCheckStatus_existing: 'موجود', - ruleCheckStatus_warning: 'هشدار', ruleCheckStatus_error: 'خطا', ruleCheckExistingHelper: 'یک قانون یکسان از قبل وجود دارد و نادیده گرفته خواهد شد.', ruleCheckReadyHelper: 'بررسی با موفقیت انجام شد. این قانون قابل ایجاد است.', ruleCheckExternalExists: 'یک قانون خارجی یکسان از قبل وجود دارد و به‌صورت خودکار نادیده گرفته می‌شود.', ruleCheckBlockedHelper: 'قوانین دارای خطا قابل ارسال نیستند. بازگردید، آن‌ها را ویرایش و دوباره بررسی کنید.', - ruleCheckWarningHelper: 'بررسی کامل شد. هشدارها مانع ارسال نمی‌شوند و می‌توانید ادامه دهید.', plan_managed_rule_drifted: 'قانون مدیریت‌شده با دیوارهٔ آتش فعال یکسان نیست. ابتدا ناهمگامی را رفع کنید.', plan_opaque_rule_in_target_scope: 'در محدودهٔ هدف قانونی وجود دارد که تحلیل امن آن ممکن نیست. عملیات متوقف شد.', plan_runtime_permanent_mismatch: 'پیکربندی فعال و دائمی دیوارهٔ آتش متفاوت است. ابتدا آن‌ها را همگام کنید.', plan_protected_rule: 'این قانون محافظت‌شده است و نمی‌توان آن را مدیریت، تغییر یا حذف کرد.', - plan_overlapping_rule_with_different_action: 'قانونی هم‌پوشان با عملکرد مخالف وجود دارد. عملیات متوقف شد.', - plan_partially_overlapping_rule_with_different_action: - 'این قانون تا حدی با قانونی با عملکرد مخالف هم‌پوشانی دارد. نتیجه به ترتیب قوانین بستگی دارد.', plan_blocked: 'این قانون با ایمنی قابل اعمال نیست. فهرست را تازه‌سازی و دوباره تلاش کنید.', scopeDefaultMismatch: 'ناحیهٔ پیش‌فرض سامانه {0} است؛ این صفحه فقط ناحیهٔ public را مدیریت می‌کند.', scopeInactive: 'محدودهٔ مدیریت‌شده فعال نیست. قوانین جدید ممکن است بر ترافیک فعلی اثر نگذارند.', @@ -4075,6 +4068,7 @@ const message = { systemFirewallHelper: 'دسترسی پورت‌های میزبان و قوانین ورودی را کنترل می‌کند.', forwardingHelper: 'قوانین انتقال پورت را مدیریت می‌کند.', dockerFirewallHelper: 'روش مدیریت محافظت از پورت کانتینر در 1Panel را انتخاب می‌کند.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0، آزمایشی', backendRecommendation: 'استفاده از iptables یا nftables توصیه می‌شود.', configuredRules: '{0} قانون پیکربندی شده', addressFamily: 'نسخه IP', @@ -4091,10 +4085,12 @@ const message = { 'همه قوانین و زنجیره‌های محافظت پورت Docker مربوط به 1Panel از {0} حذف می‌شوند و فقط داده‌های پایگاه داده حفظ می‌شوند', cleanupBeforeBackendSwitch: 'بک‌اند فعلی {0} هنوز شامل قوانین زمان اجرای 1Panel است. پیش از تغییر به {1} آن را پاک‌سازی کنید.', - cleanupAction: 'پاک‌سازی', + cleanupAction: 'بازنشانی', backendSwitchNotice: 'پیش از تغییر فایروال سیستم، انتقال پورت یا محافظت Docker، بک‌اند فعلی را پاک‌سازی کنید. سیاست‌های پایگاه داده حفظ می‌شوند و پس از تغییر قابل راه‌اندازی یا همگام‌سازی هستند.', switchBackendHelper: 'به {0} تغییر داده شود؟', + switchDockerBackendHelper: + 'به {0} تغییر داده شود؟ پیکربندی Docker به‌روزرسانی و Docker راه‌اندازی مجدد می‌شود.', ruleSyncTitle: 'همگام‌سازی قوانین', ruleSyncAction: 'همگام‌سازی قوانین', ruleSyncHelper: @@ -4145,6 +4141,8 @@ const message = { blocked: 'غیرقابل همگام‌سازی', }, uninstalledStatus: 'نصب‌نشده', + selectedBackendNotInstalled: + 'سرویس {backend} شناسایی نشد. آن را به‌صورت دستی از {library} نصب کنید یا بک‌اند فایروال را در {settings} تغییر دهید.', initializedStatus: 'راه‌اندازی شده', partiallyInitialized: 'تا حدی راه‌اندازی شده', dockerGuardHelper: @@ -4177,7 +4175,10 @@ const message = { denySources: 'رد منابع مشخص‌شده', allowSources: 'فقط اجازه به منابع مشخص‌شده', denyAll: 'رد همه دسترسی‌ها', - sourcesHelper: 'در هر خط یک آدرس IPv4/IPv6 یا CIDR وارد کنید؛ فهرست مجاز خالی همه دسترسی‌ها را رد می‌کند', + dockerGuardMixedFamilyHelper: + 'هر دو IPv4 و IPv6 انتخاب شده‌اند. سیاست‌های مبتنی بر مبدأ را جداگانه تنظیم کنید؛ رد همه دسترسی‌ها را می‌توان مستقیماً اعمال کرد.', + dockerGuardInconsistentConfigHelper: + 'پیکربندی قوانین انتخاب‌شده متفاوت است. برای بازنویسی همه، دوباره تنظیم کنید؛ توضیح خالی، توضیحات همه قوانین را پاک می‌کند.', effective: 'فعال', forwardUnsynced: 'همگام‌نشده', notEnabled: 'فعال نشده', @@ -4236,7 +4237,11 @@ const message = { destinationPortPlaceholder: 'مثلاً: 80، 80,443 یا 8080-8089', deleteRuleConfirm: '{0} قانون حذف خواهند شد. ادامه می‌دهید؟', deleteUsedRuleConfirm: - 'این پورت توسط {0} استفاده می‌شود. حذف قانون مجاز ممکن است سرویس را غیرقابل دسترس کند. ادامه می‌دهید؟', + 'این قانون سرویس‌های در حال گوش‌دادن ارائه‌شده توسط {0} را پوشش می‌دهد. حذف آن ممکن است این سرویس‌ها را غیرقابل دسترس کند. ادامه می‌دهید؟', + deleteWildcardRuleConfirm: + 'این قانون به {0} اجازه دسترسی به {1} را می‌دهد. حذف آن ممکن است دسترسی به چندین سرویس را تحت تأثیر قرار دهد. ادامه می‌دهید؟', + deleteRiskRulesConfirm: + '{0} قانون حذف خواهند شد. {1} قانون مجاز ممکن است دسترسی به سرویس‌ها را تحت تأثیر قرار دهد. ادامه می‌دهید؟', editRuleConfirm: 'فیلدهای زیر تغییر می‌کنند: {0}. قانون بلافاصله اعمال و بررسی می‌شود. ادامه می‌دهید؟', }, runtime: { diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 1204ce2717dd..19d6c34829a5 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -4050,28 +4050,21 @@ const message = { plan_equivalent_external_rule: '同じ外部ルールが既にあります。重複作成せず管理対象にできます。', plan_multiple_equivalent_external_rules: '同じ外部ルールが複数あります。管理対象にするルールを選択してください。', - plan_requested_rule_is_covered: - '既存ルールがこの設定を既に包含しています。続行すると重複するルールが作成されます。', plan_equivalent_managed_rule: '同じルールは既に 1Panel の管理対象です。重複作成は不要です。', allRulesAlreadyExist: '確認した {0} 件のルールはすべて既に存在します。新しく作成するルールはありません。', ruleCheckResult: 'ルール確認結果', ruleCheckStatus_creatable: '作成可能', ruleCheckStatus_existing: '既存', - ruleCheckStatus_warning: '警告', ruleCheckStatus_error: 'エラー', ruleCheckExistingHelper: '同じルールが既に存在するため、今回はスキップします。', ruleCheckReadyHelper: '確認に合格しました。このルールは作成できます。', ruleCheckExternalExists: '同じ外部ルールが既に存在するため、自動的にスキップされます。', ruleCheckBlockedHelper: 'エラーのあるルールは送信できません。戻って修正し、もう一度確認してください。', - ruleCheckWarningHelper: '確認が完了しました。警告は送信を妨げないため、そのまま続行できます。', plan_managed_rule_drifted: '管理対象ルールが実際のファイアウォールと一致しません。先に不整合を解消してください。', plan_opaque_rule_in_target_scope: '対象範囲に安全に解析できないルールがあります。操作を停止しました。', plan_runtime_permanent_mismatch: '実行中と永続設定のファイアウォール構成が異なります。先に同期してください。', plan_protected_rule: 'このルールは保護されており、管理対象化、変更、削除はできません。', - plan_overlapping_rule_with_different_action: '反対の動作を持つ重複ルールがあります。操作を停止しました。', - plan_partially_overlapping_rule_with_different_action: - '反対の動作を持つルールと一部重複しています。実際の動作はルールの順序によって決まります。', plan_blocked: 'このルールは安全に適用できません。ルールを更新して再試行してください。', scopeDefaultMismatch: 'システムの既定 zone は {0} です。このページでは public zone のみ管理します。', scopeInactive: '管理対象の範囲が有効ではありません。新しいルールが現在の通信に適用されない場合があります。', @@ -4102,6 +4095,7 @@ const message = { systemFirewallHelper: 'ホストのポートアクセスと受信ルールを管理します。', forwardingHelper: 'ポート転送ルールを管理します。', dockerFirewallHelper: '1Panel のコンテナポート保護の管理方法を選択します。', + dockerNftablesRequirement: 'Docker ≥ 29.0.0、実験的', backendRecommendation: 'iptables または nftables の使用を推奨します。', configuredRules: '{0} 件設定済み', addressFamily: 'IP バージョン', @@ -4119,10 +4113,11 @@ const message = { '{0} から 1Panel Docker ポート保護ルールとチェーンをすべて削除し、データベースデータのみを保持します', cleanupBeforeBackendSwitch: '現在の {0} バックエンドには 1Panel の実行時ルールが残っています。{1} に切り替える前にクリーンアップしてください。', - cleanupAction: 'クリーンアップ', + cleanupAction: 'リセット', backendSwitchNotice: 'システムファイアウォール、ポート転送、Docker 保護を切り替える前に現在のバックエンドをクリーンアップしてください。データベースポリシーは保持され、切り替え後に再初期化または同期できます。', switchBackendHelper: '{0} に切り替えますか?', + switchDockerBackendHelper: '{0} に切り替えますか?Docker の設定を更新し、Docker を再起動します。', ruleSyncTitle: 'ルールを同期', ruleSyncAction: 'ルールを同期', ruleSyncHelper: @@ -4173,6 +4168,8 @@ const message = { blocked: '同期不可', }, uninstalledStatus: '未インストール', + selectedBackendNotInstalled: + '{backend} サービスが検出されませんでした。{library} から手動でインストールするか、{settings} でファイアウォールバックエンドを切り替えてください。', initializedStatus: '初期化済み', partiallyInitialized: '一部初期化済み', dockerGuardHelper: @@ -4204,8 +4201,10 @@ const message = { denySources: '指定した送信元を拒否', allowSources: '指定した送信元のみ許可', denyAll: 'すべてのアクセスを拒否', - sourcesHelper: - '1 行につき 1 つの IPv4/IPv6 アドレスまたは CIDR を入力してください。許可リストが空の場合はすべて拒否します', + dockerGuardMixedFamilyHelper: + 'IPv4 と IPv6 の両方が選択されています。送信元指定は個別に設定してください。「すべて拒否」はそのまま適用できます。', + dockerGuardInconsistentConfigHelper: + '選択したルールの設定が異なります。再設定するとすべて統一して上書きされ、説明が空の場合はすべてクリアされます。', effective: '有効', forwardUnsynced: '未同期', notEnabled: '未有効化', @@ -4264,7 +4263,11 @@ const message = { destinationPortPlaceholder: '例: 80、80,443、8080-8089', deleteRuleConfirm: '{0} 個のルールを削除します。続行しますか?', deleteUsedRuleConfirm: - 'このポートは {0} が使用中です。許可ルールを削除するとサービスにアクセスできなくなる可能性があります。続行しますか?', + 'このルールは {0} が提供する待ち受け中のサービスを対象としています。削除するとそれらのサービスにアクセスできなくなる可能性があります。続行しますか?', + deleteWildcardRuleConfirm: + 'このルールは {0} に {1} へのアクセスを許可しています。削除すると複数のサービスへのアクセスに影響する可能性があります。続行しますか?', + deleteRiskRulesConfirm: + '{0} 個のルールを削除します。そのうち {1} 個の許可ルールがサービスへのアクセスに影響する可能性があります。続行しますか?', editRuleConfirm: '次の項目を変更します:{0}。ルールは直ちに適用され、再検証されます。続行しますか?', }, runtime: { diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index d0389e58889a..8b350f78e02a 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -3976,26 +3976,20 @@ const message = { plan_equivalent_external_rule: '동일한 외부 규칙이 이미 있습니다. 중복 생성 대신 관리 대상으로 전환할 수 있습니다.', plan_multiple_equivalent_external_rules: '동일한 외부 규칙이 여러 개 있습니다. 관리할 규칙을 선택하세요.', - plan_requested_rule_is_covered: '기존 규칙이 이미 이 설정을 포함합니다. 계속하면 겹치는 규칙이 생성됩니다.', plan_equivalent_managed_rule: '동일한 규칙을 1Panel이 이미 관리하고 있습니다. 중복 생성할 필요가 없습니다.', allRulesAlreadyExist: '확인한 규칙 {0}개가 모두 이미 존재합니다. 새로 생성할 규칙이 없습니다.', ruleCheckResult: '규칙 검사 결과', ruleCheckStatus_creatable: '생성 가능', ruleCheckStatus_existing: '이미 존재함', - ruleCheckStatus_warning: '경고', ruleCheckStatus_error: '오류', ruleCheckExistingHelper: '동일한 규칙이 이미 존재하므로 이번 작업에서는 건너뜁니다.', ruleCheckReadyHelper: '검사를 통과했습니다. 이 규칙을 생성할 수 있습니다.', ruleCheckExternalExists: '동일한 외부 규칙이 이미 존재하며 자동으로 건너뜁니다.', ruleCheckBlockedHelper: '오류가 있는 규칙은 제출할 수 없습니다. 돌아가서 수정한 후 다시 검사하세요.', - ruleCheckWarningHelper: '검사가 완료되었습니다. 경고는 제출을 막지 않으므로 계속할 수 있습니다.', plan_managed_rule_drifted: '관리 규칙이 실제 방화벽과 일치하지 않습니다. 먼저 불일치를 해결하세요.', plan_opaque_rule_in_target_scope: '대상 범위에 안전하게 분석할 수 없는 규칙이 있어 작업을 중지했습니다.', plan_runtime_permanent_mismatch: '실행 중 구성과 영구 방화벽 구성이 다릅니다. 먼저 동기화하세요.', plan_protected_rule: '이 규칙은 보호되어 관리 전환, 변경 또는 삭제할 수 없습니다.', - plan_overlapping_rule_with_different_action: '반대 동작을 가진 겹치는 규칙이 있어 작업을 중지했습니다.', - plan_partially_overlapping_rule_with_different_action: - '반대 동작을 가진 규칙과 일부 겹칩니다. 실제 동작은 규칙 순서에 따라 결정됩니다.', plan_blocked: '이 규칙을 안전하게 적용할 수 없습니다. 목록을 새로 고친 후 다시 시도하세요.', scopeDefaultMismatch: '시스템 기본 zone은 {0}이며 이 페이지는 public zone만 관리합니다.', scopeInactive: '관리 범위가 비활성 상태입니다. 새 규칙이 현재 트래픽에 적용되지 않을 수 있습니다.', @@ -4025,6 +4019,7 @@ const message = { systemFirewallHelper: '호스트 포트 접근과 인바운드 규칙을 관리합니다.', forwardingHelper: '포트 포워딩 규칙을 관리합니다.', dockerFirewallHelper: '1Panel 컨테이너 포트 보호 관리 방식을 선택합니다.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, 실험적', backendRecommendation: 'iptables 또는 nftables 사용을 권장합니다.', configuredRules: '{0}개 규칙 설정됨', addressFamily: 'IP 버전', @@ -4041,10 +4036,11 @@ const message = { '{0}의 모든 1Panel Docker 포트 보호 규칙과 체인을 삭제하고 데이터베이스 데이터만 유지합니다', cleanupBeforeBackendSwitch: '현재 {0} 백엔드에 1Panel 런타임 규칙이 남아 있습니다. {1}(으)로 전환하기 전에 먼저 정리하세요.', - cleanupAction: '정리', + cleanupAction: '재설정', backendSwitchNotice: '시스템 방화벽, 포트 전달 또는 Docker 보호를 전환하기 전에 현재 백엔드를 정리하세요. 데이터베이스 정책은 유지되며 전환 후 다시 초기화하거나 동기화할 수 있습니다.', switchBackendHelper: '{0}(으)로 전환하시겠습니까?', + switchDockerBackendHelper: '{0}(으)로 전환하시겠습니까? Docker 설정을 업데이트하고 Docker를 재시작합니다.', ruleSyncTitle: '규칙 동기화', ruleSyncAction: '규칙 동기화', ruleSyncHelper: @@ -4094,6 +4090,8 @@ const message = { blocked: '동기화 불가', }, uninstalledStatus: '설치되지 않음', + selectedBackendNotInstalled: + '{backend} 서비스를 감지하지 못했습니다. {library}에서 수동으로 설치하거나 {settings}에서 방화벽 백엔드를 전환하세요.', initializedStatus: '초기화됨', partiallyInitialized: '일부 초기화됨', dockerGuardHelper: @@ -4125,8 +4123,10 @@ const message = { denySources: '지정한 소스 거부', allowSources: '지정한 소스만 허용', denyAll: '모든 접근 거부', - sourcesHelper: - '한 줄에 하나의 IPv4/IPv6 주소 또는 CIDR을 입력하세요. 허용 목록이 비어 있으면 모든 접근을 거부합니다', + dockerGuardMixedFamilyHelper: + 'IPv4와 IPv6가 모두 선택되었습니다. 출발지 지정 정책은 각각 설정해야 하며, 모든 접근 거부는 바로 적용할 수 있습니다.', + dockerGuardInconsistentConfigHelper: + '선택한 규칙의 설정이 서로 다릅니다. 다시 설정하면 모두 동일하게 덮어쓰며, 설명이 비어 있으면 모든 설명을 지웁니다.', effective: '적용됨', forwardUnsynced: '미동기화', notEnabled: '활성화되지 않음', @@ -4183,7 +4183,11 @@ const message = { destinationPortPlaceholder: '예: 80, 80,443 또는 8080-8089', deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?', deleteUsedRuleConfirm: - '이 포트는 {0}에서 사용 중입니다. 허용 규칙을 삭제하면 서비스에 접근하지 못할 수 있습니다. 계속하시겠습니까?', + '이 규칙은 {0}이(가) 제공하는 대기 중인 서비스에 적용됩니다. 삭제하면 해당 서비스에 접근하지 못할 수 있습니다. 계속하시겠습니까?', + deleteWildcardRuleConfirm: + '이 규칙은 {0}에서 {1}에 접근하도록 허용합니다. 삭제하면 여러 서비스에 대한 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?', + deleteRiskRulesConfirm: + '{0}개의 규칙을 삭제합니다. 그중 {1}개의 허용 규칙이 서비스 접근에 영향을 줄 수 있습니다. 계속하시겠습니까?', editRuleConfirm: '다음 필드가 변경됩니다: {0}. 규칙이 즉시 적용되고 다시 검증됩니다. 계속하시겠습니까?', quickJump: '빠른 이동', }, diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index c153f0325b3f..d907eb4bbdd1 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -3947,26 +3947,20 @@ const message = { adoptRuleConfirm: 'ຫຼັງຈາກຮັບເຂົ້າແລ້ວ 1Panel ຈະສາມາດຮັກສາແລະລຶບກົດນີ້ໄດ້. ສືບຕໍ່ບໍ?', plan_equivalent_external_rule: 'ມີກົດພາຍນອກທີ່ຄືກັນແລ້ວ. ສາມາດຮັບເຂົ້າຈັດການແທນການສ້າງຊ້ຳ.', plan_multiple_equivalent_external_rules: 'ມີກົດພາຍນອກທີ່ຄືກັນຫຼາຍກົດ. ກະລຸນາເລືອກໜຶ່ງກົດ.', - plan_requested_rule_is_covered: 'ກົດທີ່ມີຢູ່ຄອບຄຸມຄ່ານີ້ແລ້ວ. ການສືບຕໍ່ຈະສ້າງກົດທີ່ຊ້ອນກັນ.', plan_equivalent_managed_rule: 'ກົດທີ່ຄືກັນຖືກ 1Panel ຈັດການແລ້ວ ບໍ່ຕ້ອງສ້າງຊ້ຳ.', allRulesAlreadyExist: 'ກົດທັງໝົດ {0} ລາຍການທີ່ກວດສອບມີຢູ່ແລ້ວ. ບໍ່ມີກົດໃໝ່ທີ່ຕ້ອງສ້າງ.', ruleCheckResult: 'ຜົນການກວດສອບກົດ', ruleCheckStatus_creatable: 'ສາມາດສ້າງໄດ້', ruleCheckStatus_existing: 'ມີຢູ່ແລ້ວ', - ruleCheckStatus_warning: 'ຄຳເຕືອນ', ruleCheckStatus_error: 'ຂໍ້ຜິດພາດ', ruleCheckExistingHelper: 'ມີກົດທີ່ຄືກັນຢູ່ແລ້ວ ແລະ ຈະຖືກຂ້າມ.', ruleCheckReadyHelper: 'ການກວດສອບຜ່ານ. ສາມາດສ້າງກົດນີ້ໄດ້.', ruleCheckExternalExists: 'ມີກົດພາຍນອກທີ່ຄືກັນຢູ່ແລ້ວ ແລະຈະຖືກຂ້າມອັດຕະໂນມັດ.', ruleCheckBlockedHelper: 'ບໍ່ສາມາດສົ່ງກົດທີ່ມີຂໍ້ຜິດພາດໄດ້. ກັບໄປແກ້ໄຂ ແລະກວດສອບໃໝ່.', - ruleCheckWarningHelper: 'ກວດສອບສຳເລັດ. ຄຳເຕືອນບໍ່ຂັດຂວາງການສົ່ງ ແລະສາມາດດຳເນີນຕໍ່ໄດ້.', plan_managed_rule_drifted: 'ກົດທີ່ຈັດການບໍ່ກົງກັບໄຟວໍທີ່ໃຊ້ງານ. ກະລຸນາແກ້ໄຂກ່ອນ.', plan_opaque_rule_in_target_scope: 'ມີກົດໃນຂອບເຂດທີ່ບໍ່ສາມາດວິເຄາະໄດ້ຢ່າງປອດໄພ. ການດຳເນີນງານຖືກຢຸດ.', plan_runtime_permanent_mismatch: 'ຄ່າໄຟວໍທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນ. ກະລຸນາຊິງກ່ອນ.', plan_protected_rule: 'ກົດນີ້ຖືກປົກປ້ອງ ແລະບໍ່ສາມາດຮັບເຂົ້າ, ແກ້ໄຂ ຫຼືລຶບໄດ້.', - plan_overlapping_rule_with_different_action: 'ມີກົດທີ່ຊ້ອນກັນແລະມີການກະທຳກົງກັນຂ້າມ. ການດຳເນີນງານຖືກຢຸດ.', - plan_partially_overlapping_rule_with_different_action: - 'ກົດນີ້ຊ້ອນກັນບາງສ່ວນກັບກົດທີ່ມີການກະທຳກົງກັນຂ້າມ. ຜົນຂຶ້ນກັບລຳດັບກົດ.', plan_blocked: 'ບໍ່ສາມາດນຳໃຊ້ກົດນີ້ໄດ້ຢ່າງປອດໄພ. ກະລຸນາໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.', scopeDefaultMismatch: 'zone ຄ່າເລີ່ມຕົ້ນແມ່ນ {0}; ໜ້ານີ້ຈັດການສະເພາະ public zone.', scopeInactive: 'ຂອບເຂດທີ່ຈັດການບໍ່ໄດ້ເປີດໃຊ້. ກົດໃໝ່ອາດບໍ່ມີຜົນຕໍ່ການຈະລາຈອນປັດຈຸບັນ.', @@ -3996,6 +3990,7 @@ const message = { systemFirewallHelper: 'ຄວບຄຸມການເຂົ້າເຖິງພອດໂຮສ ແລະ ກົດຂາເຂົ້າ.', forwardingHelper: 'ຈັດການກົດການສົ່ງຕໍ່ພອດ.', dockerFirewallHelper: 'ເລືອກວິທີທີ່ 1Panel ຈັດການການປ້ອງກັນພອດຄອນເທນເນີ.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, ທົດລອງ', backendRecommendation: 'ແນະນຳໃຫ້ໃຊ້ iptables ຫຼື nftables ໃນການຈັດການ.', configuredRules: 'ຕັ້ງຄ່າແລ້ວ {0} ກົດ', addressFamily: 'ລຸ້ນ IP', @@ -4011,10 +4006,11 @@ const message = { cleanupDockerBackendHelper: 'ລຶບກົດ ແລະ chain ປ້ອງກັນພອດ Docker ຂອງ 1Panel ທັງໝົດຈາກ {0}, ເກັບໄວ້ສະເພາະຂໍ້ມູນໃນຖານຂໍ້ມູນ', cleanupBeforeBackendSwitch: 'ແບັກເອັນປັດຈຸບັນ {0} ຍັງມີກົດ runtime ຂອງ 1Panel. ກະລຸນາລ້າງກ່ອນສະຫຼັບໄປ {1}.', - cleanupAction: 'ລ້າງ', + cleanupAction: 'ຣີເຊັດ', backendSwitchNotice: 'ລ້າງແບັກເອັນປັດຈຸບັນກ່ອນສະຫຼັບໄຟວໍລະບົບ, ການສົ່ງຕໍ່ພອດ ຫຼື ການປ້ອງກັນ Docker. ນະໂຍບາຍໃນຖານຂໍ້ມູນຈະຖືກເກັບໄວ້ ແລະສາມາດເລີ່ມ ຫຼືຊິງຫຼັງສະຫຼັບ.', switchBackendHelper: 'ຢືນຢັນການສະຫຼັບເປັນ {0} ບໍ?', + switchDockerBackendHelper: 'ປ່ຽນເປັນ {0} ບໍ? ການຕັ້ງຄ່າ Docker ຈະຖືກອັບເດດ ແລະ Docker ຈະເລີ່ມໃໝ່.', ruleSyncTitle: 'ຊິງກົດ', ruleSyncAction: 'ຊິງກົດ', ruleSyncHelper: @@ -4063,6 +4059,8 @@ const message = { blocked: 'ຊິງບໍ່ໄດ້', }, uninstalledStatus: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງ', + selectedBackendNotInstalled: + 'ບໍ່ພົບບໍລິການ {backend}. ກະລຸນາຕິດຕັ້ງແບບກຳນົດເອງຈາກ {library} ຫຼືປ່ຽນແບັກເອັນໄຟວໍໃນ {settings}.', initializedStatus: 'ເລີ່ມຕົ້ນແລ້ວ', partiallyInitialized: 'ເລີ່ມຕົ້ນບາງສ່ວນ', dockerGuardHelper: @@ -4094,7 +4092,10 @@ const message = { denySources: 'ປະຕິເສດແຫຼ່ງທີ່ລະບຸ', allowSources: 'ອະນຸຍາດສະເພາະແຫຼ່ງທີ່ລະບຸ', denyAll: 'ປະຕິເສດການເຂົ້າເຖິງທັງໝົດ', - sourcesHelper: 'ໃສ່ທີ່ຢູ່ IPv4/IPv6 ຫຼື CIDR ໜຶ່ງລາຍການຕໍ່ແຖວ; ລາຍການອະນຸຍາດຫວ່າງຈະປະຕິເສດທຸກການເຂົ້າເຖິງ', + dockerGuardMixedFamilyHelper: + 'ເລືອກທັງ IPv4 ແລະ IPv6 ແລ້ວ. ນະໂຍບາຍທີ່ລະບຸແຫຼ່ງທີ່ມາຕ້ອງຕັ້ງຄ່າແຍກກັນ; ການປະຕິເສດທັງໝົດສາມາດນຳໃຊ້ໄດ້ໂດຍກົງ.', + dockerGuardInconsistentConfigHelper: + 'ກົດທີ່ເລືອກມີການຕັ້ງຄ່າແຕກຕ່າງກັນ. ກຳນົດໃໝ່ເພື່ອຂຽນທັບທັງໝົດ; ຄຳອະທິບາຍຫວ່າງຈະລຶບຄຳອະທິບາຍທັງໝົດ.', effective: 'ມີຜົນແລ້ວ', forwardUnsynced: 'ຍັງບໍ່ຊິງຄ໌', notEnabled: 'ບໍ່ໄດ້ເປີດໃຊ້', @@ -4148,7 +4149,11 @@ const message = { sourceAddressPlaceholder: 'ຕົວຢ່າງ: 172.16.10.11, 172.16.0.0/24, 2001:db8::1 ຫຼື 2001:db8::/64', destinationPortPlaceholder: 'ຕົວຢ່າງ: 80, 80,443 ຫຼື 8080-8089', deleteRuleConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. ຕ້ອງການຕໍ່ຫຼືບໍ່?', - deleteUsedRuleConfirm: 'ພອດນີ້ກຳລັງຖືກໃຊ້ໂດຍ {0}. ການລຶບກົດອະນຸຍາດອາດເຮັດໃຫ້ບໍລິການເຂົ້າເຖິງບໍ່ໄດ້. ສືບຕໍ່ບໍ?', + deleteUsedRuleConfirm: + 'ກົດນີ້ຄອບຄຸມບໍລິການທີ່ກຳລັງຟັງຊຶ່ງສະໜອງໂດຍ {0}. ການລຶບອາດເຮັດໃຫ້ບໍລິການເຫຼົ່ານັ້ນເຂົ້າເຖິງບໍ່ໄດ້. ສືບຕໍ່ບໍ?', + deleteWildcardRuleConfirm: + 'ກົດນີ້ອະນຸຍາດໃຫ້ {0} ເຂົ້າເຖິງ {1}. ການລຶບອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງຫຼາຍບໍລິການ. ສືບຕໍ່ບໍ?', + deleteRiskRulesConfirm: 'ຈະລຶບ {0} ກົດລະບຽບ. {1} ກົດອະນຸຍາດອາດສົ່ງຜົນຕໍ່ການເຂົ້າເຖິງບໍລິການ. ສືບຕໍ່ບໍ?', editRuleConfirm: 'ຈະປ່ຽນຟິວຕໍ່ໄປນີ້: {0}. ກົດຈະຖືກນຳໃຊ້ແລະກວດສອບທັນທີ. ສືບຕໍ່ບໍ?', }, runtime: { diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index d228360c246d..3cab1fe93ff8 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4132,30 +4132,22 @@ const message = { plan_equivalent_external_rule: 'Peraturan luaran yang sama sudah wujud. Ambil alih tanpa mencipta pendua.', plan_multiple_equivalent_external_rules: 'Terdapat beberapa peraturan luaran yang sama. Pilih satu untuk diurus.', - plan_requested_rule_is_covered: - 'Peraturan sedia ada sudah meliputi konfigurasi ini. Meneruskan akan mencipta peraturan bertindih.', plan_equivalent_managed_rule: 'Peraturan yang sama sudah diurus oleh 1Panel. Pendua tidak diperlukan.', 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_warning: 'Amaran', ruleCheckStatus_error: 'Ralat', ruleCheckExistingHelper: 'Peraturan yang sama sudah wujud dan akan dilangkau.', ruleCheckReadyHelper: 'Semakan lulus. Peraturan ini boleh dicipta.', ruleCheckExternalExists: 'Peraturan luaran yang sama sudah wujud dan akan dilangkau secara automatik.', ruleCheckBlockedHelper: 'Peraturan yang mempunyai ralat tidak boleh dihantar. Kembali, edit dan semak semula.', - ruleCheckWarningHelper: 'Semakan selesai. Amaran tidak menghalang penghantaran; anda boleh meneruskan.', plan_managed_rule_drifted: 'Peraturan terurus tidak sepadan dengan tembok api aktif. Selesaikan perbezaan dahulu.', plan_opaque_rule_in_target_scope: 'Skop sasaran mengandungi peraturan yang tidak dapat dihuraikan dengan selamat. Operasi dihentikan.', plan_runtime_permanent_mismatch: 'Konfigurasi tembok api aktif dan kekal berbeza. Segerakkan dahulu.', plan_protected_rule: 'Peraturan ini dilindungi dan tidak boleh diambil alih, diubah atau dipadam.', - plan_overlapping_rule_with_different_action: - 'Terdapat peraturan bertindih dengan tindakan bertentangan. Operasi dihentikan.', - plan_partially_overlapping_rule_with_different_action: - 'Peraturan ini bertindih sebahagian dengan peraturan bertentangan. Hasil bergantung pada susunan peraturan.', plan_blocked: 'Peraturan ini tidak dapat digunakan dengan selamat. Muat semula senarai dan cuba lagi.', scopeDefaultMismatch: 'Zon lalai sistem ialah {0}; halaman ini hanya mengurus zon public.', scopeInactive: 'Skop terurus tidak aktif. Peraturan baharu mungkin tidak mempengaruhi trafik semasa.', @@ -4187,6 +4179,7 @@ const message = { systemFirewallHelper: 'Mengawal akses port hos dan peraturan masuk.', forwardingHelper: 'Mengurus peraturan pemajuan port.', dockerFirewallHelper: 'Memilih cara 1Panel mengurus perlindungan port bekas.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, percubaan', backendRecommendation: 'Disyorkan untuk menggunakan iptables atau nftables.', configuredRules: '{0} peraturan dikonfigurasi', addressFamily: 'Versi IP', @@ -4204,10 +4197,11 @@ const message = { 'Padam semua peraturan dan rantaian perlindungan port Docker 1Panel daripada {0}, hanya mengekalkan data pangkalan data', cleanupBeforeBackendSwitch: 'Bahagian belakang semasa {0} masih mengandungi peraturan masa jalan 1Panel. Bersihkannya sebelum bertukar kepada {1}.', - cleanupAction: 'Bersihkan', + cleanupAction: 'Tetapkan semula', backendSwitchNotice: 'Bersihkan bahagian belakang semasa sebelum menukar tembok api sistem, pemajuan port atau perlindungan Docker. Dasar pangkalan data dikekalkan dan boleh dimulakan atau disegerakkan selepas penukaran.', switchBackendHelper: 'Tukar kepada {0}?', + switchDockerBackendHelper: 'Tukar kepada {0}? Ini akan mengemas kini konfigurasi dan memulakan semula Docker.', ruleSyncTitle: 'Segerakkan peraturan', ruleSyncAction: 'Segerakkan peraturan', ruleSyncHelper: @@ -4259,6 +4253,8 @@ const message = { blocked: 'Tidak tersedia', }, uninstalledStatus: 'Belum dipasang', + selectedBackendNotInstalled: + 'Perkhidmatan {backend} tidak dikesan. Pasang secara manual daripada {library}, atau tukar backend firewall dalam {settings}.', initializedStatus: 'Dimulakan', partiallyInitialized: 'Dimulakan sebahagian', dockerGuardHelper: @@ -4290,7 +4286,10 @@ const message = { denySources: 'Tolak sumber tertentu', allowSources: 'Benarkan sumber tertentu sahaja', denyAll: 'Tolak semua akses', - sourcesHelper: 'Masukkan satu alamat IPv4/IPv6 atau CIDR setiap baris; senarai izin kosong menolak semua akses', + dockerGuardMixedFamilyHelper: + 'IPv4 dan IPv6 dipilih. Konfigurasikan dasar berasaskan sumber secara berasingan; tolak semua boleh digunakan terus.', + dockerGuardInconsistentConfigHelper: + 'Peraturan yang dipilih mempunyai konfigurasi berbeza. Tetapkan semula untuk menulis ganti semuanya; keterangan kosong akan mengosongkan semua keterangan.', effective: 'Berkuat kuasa', forwardUnsynced: 'Belum disegerak', notEnabled: 'Tidak didayakan', @@ -4349,7 +4348,11 @@ const message = { destinationPortPlaceholder: 'contoh: 80, 80,443 atau 8080-8089', deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?', deleteUsedRuleConfirm: - 'Port ini sedang digunakan oleh {0}. Memadam peraturan benarkan mungkin menyebabkan perkhidmatan tidak dapat dicapai. Teruskan?', + 'Peraturan ini meliputi perkhidmatan mendengar yang disediakan oleh {0}. Memadamnya mungkin menyebabkan perkhidmatan tersebut tidak dapat dicapai. Teruskan?', + deleteWildcardRuleConfirm: + '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?', }, runtime: { diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 9376682c7d57..7683254cbc52 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4152,30 +4152,22 @@ const message = { 'Já existe uma regra externa idêntica. Assuma seu gerenciamento sem criar uma duplicata.', plan_multiple_equivalent_external_rules: 'Existem várias regras externas idênticas. Selecione uma para gerenciar.', - plan_requested_rule_is_covered: - 'Uma regra existente já cobre esta configuração. Continuar criará uma regra sobreposta.', plan_equivalent_managed_rule: 'Uma regra idêntica já é gerenciada pelo 1Panel. Não é necessário duplicá-la.', 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_warning: 'Aviso', ruleCheckStatus_error: 'Erro', ruleCheckExistingHelper: 'Uma regra idêntica já existe e será ignorada.', ruleCheckReadyHelper: 'A verificação foi aprovada. Esta regra pode ser criada.', ruleCheckExternalExists: 'Uma regra externa idêntica já existe e será ignorada automaticamente.', ruleCheckBlockedHelper: 'Regras com erros não podem ser enviadas. Volte, edite-as e verifique novamente.', - ruleCheckWarningHelper: 'Verificação concluída. Os avisos não impedem o envio; você pode continuar.', plan_managed_rule_drifted: 'A regra gerenciada não corresponde ao firewall ativo. Resolva a divergência primeiro.', plan_opaque_rule_in_target_scope: 'O escopo contém uma regra que não pode ser analisada com segurança. A operação foi interrompida.', plan_runtime_permanent_mismatch: 'As configurações ativa e permanente são diferentes. Sincronize-as primeiro.', plan_protected_rule: 'Esta regra está protegida e não pode ser assumida, alterada ou excluída.', - plan_overlapping_rule_with_different_action: - 'Existe uma regra sobreposta com ação oposta. A operação foi interrompida.', - plan_partially_overlapping_rule_with_different_action: - 'A regra se sobrepõe parcialmente a uma regra com ação oposta. O resultado depende da ordem das regras.', plan_blocked: 'A regra não pode ser aplicada com segurança. Atualize a lista e tente novamente.', scopeDefaultMismatch: 'A zona padrão do sistema é {0}; esta página gerencia apenas a zona public.', scopeInactive: 'O escopo gerenciado está inativo. Novas regras podem não afetar o tráfego atual.', @@ -4206,6 +4198,7 @@ const message = { systemFirewallHelper: 'Controla o acesso às portas do host e as regras de entrada.', forwardingHelper: 'Gerencia regras de encaminhamento de portas.', dockerFirewallHelper: 'Seleciona como o 1Panel gerencia a proteção de portas de contêineres.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, experimental', backendRecommendation: 'Recomenda-se usar iptables ou nftables.', configuredRules: '{0} regras configuradas', addressFamily: 'Versão do IP', @@ -4223,10 +4216,11 @@ const message = { 'Exclua de {0} todas as regras e cadeias de proteção de portas Docker do 1Panel, mantendo apenas os dados do banco de dados', cleanupBeforeBackendSwitch: 'O backend atual {0} ainda contém regras de execução do 1Panel. Limpe-o antes de mudar para {1}.', - cleanupAction: 'Limpar', + cleanupAction: 'Redefinir', backendSwitchNotice: 'Limpe o backend atual antes de trocar o firewall do sistema, o encaminhamento de portas ou a proteção Docker. As políticas do banco são mantidas e podem ser inicializadas ou sincronizadas após a troca.', switchBackendHelper: 'Mudar para {0}?', + switchDockerBackendHelper: 'Mudar para {0}? Isso atualizará a configuração e reiniciará o Docker.', ruleSyncTitle: 'Sincronizar regras', ruleSyncAction: 'Sincronizar regras', ruleSyncHelper: @@ -4279,6 +4273,8 @@ const message = { blocked: 'Indisponível', }, uninstalledStatus: 'Não instalado', + selectedBackendNotInstalled: + 'O serviço {backend} não foi detectado. Instale-o manualmente pela {library} ou altere o backend do firewall em {settings}.', initializedStatus: 'Inicializado', partiallyInitialized: 'Parcialmente inicializado', dockerGuardHelper: @@ -4310,8 +4306,10 @@ const message = { denySources: 'Negar origens especificadas', allowSources: 'Permitir somente origens especificadas', denyAll: 'Negar todo o acesso', - sourcesHelper: - 'Insira um endereço IPv4/IPv6 ou CIDR por linha; uma lista de permissões vazia nega todo o acesso', + dockerGuardMixedFamilyHelper: + 'IPv4 e IPv6 estão selecionados. Configure as políticas baseadas em origem separadamente; bloquear tudo pode ser aplicado diretamente.', + dockerGuardInconsistentConfigHelper: + 'As regras selecionadas têm configurações diferentes. Configure-as novamente para substituir todas; uma descrição vazia limpará todas as descrições.', effective: 'Em vigor', forwardUnsynced: 'Não sincronizado', notEnabled: 'Não habilitado', @@ -4373,7 +4371,11 @@ const message = { destinationPortPlaceholder: 'por exemplo: 80, 80,443 ou 8080-8089', deleteRuleConfirm: 'Excluirá {0} regras. Continuar?', deleteUsedRuleConfirm: - 'Esta porta está sendo usada por {0}. Excluir a regra de liberação pode tornar o serviço inacessível. Continuar?', + 'Esta regra abrange serviços em escuta fornecidos por {0}. Excluí-la pode tornar esses serviços inacessíveis. Continuar?', + deleteWildcardRuleConfirm: + '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?', }, diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 84f473b42609..df487a6859bb 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4119,30 +4119,22 @@ const message = { 'Такое внешнее правило уже существует. Возьмите его под управление без создания дубликата.', plan_multiple_equivalent_external_rules: 'Существует несколько одинаковых внешних правил. Выберите одно для управления.', - plan_requested_rule_is_covered: - 'Существующее правило уже покрывает эту настройку. Продолжение создаст пересекающееся правило.', plan_equivalent_managed_rule: 'Такое правило уже управляется 1Panel. Дубликат не требуется.', allRulesAlreadyExist: 'Все проверенные правила ({0}) уже существуют. Новых правил для создания нет.', ruleCheckResult: 'Результаты проверки правил', ruleCheckStatus_creatable: 'Можно создать', ruleCheckStatus_existing: 'Уже существует', - ruleCheckStatus_warning: 'Предупреждение', ruleCheckStatus_error: 'Ошибка', ruleCheckExistingHelper: 'Такое же правило уже существует и будет пропущено.', ruleCheckReadyHelper: 'Проверка пройдена. Это правило можно создать.', ruleCheckExternalExists: 'Идентичное внешнее правило уже существует и будет автоматически пропущено.', ruleCheckBlockedHelper: 'Правила с ошибками нельзя отправить. Вернитесь, исправьте их и повторите проверку.', - ruleCheckWarningHelper: 'Проверка завершена. Предупреждения не блокируют отправку, можно продолжить.', plan_managed_rule_drifted: 'Управляемое правило не совпадает с активным межсетевым экраном. Сначала устраните расхождение.', plan_opaque_rule_in_target_scope: 'В целевой области есть правило, которое нельзя безопасно разобрать. Операция остановлена.', plan_runtime_permanent_mismatch: 'Активная и постоянная конфигурации различаются. Сначала синхронизируйте их.', plan_protected_rule: 'Правило защищено: его нельзя взять под управление, изменить или удалить.', - plan_overlapping_rule_with_different_action: - 'Есть пересекающееся правило с противоположным действием. Операция остановлена.', - plan_partially_overlapping_rule_with_different_action: - 'Правило частично пересекается с правилом противоположного действия. Результат зависит от порядка правил.', plan_blocked: 'Правило нельзя безопасно применить. Обновите список и повторите попытку.', scopeDefaultMismatch: 'Системная зона по умолчанию — {0}; на этой странице управляется только зона public.', scopeInactive: 'Управляемая область не активна. Новые правила могут не влиять на текущий трафик.', @@ -4173,6 +4165,7 @@ const message = { systemFirewallHelper: 'Управляет доступом к портам хоста и входящими правилами.', forwardingHelper: 'Управляет правилами перенаправления портов.', dockerFirewallHelper: 'Выбирает способ управления защитой портов контейнеров в 1Panel.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, экспериментально', backendRecommendation: 'Рекомендуется использовать iptables или nftables.', configuredRules: 'Настроено правил: {0}', addressFamily: 'Версия IP', @@ -4190,10 +4183,11 @@ const message = { 'Удалить из {0} все правила и цепочки защиты портов Docker 1Panel, сохранив только данные в базе данных', cleanupBeforeBackendSwitch: 'Текущий бэкенд {0} всё ещё содержит правила времени выполнения 1Panel. Очистите его перед переключением на {1}.', - cleanupAction: 'Очистить', + cleanupAction: 'Сбросить', backendSwitchNotice: 'Перед переключением системного брандмауэра, переадресации портов или защиты Docker очистите текущий бэкенд. Политики базы данных сохраняются и могут быть инициализированы или синхронизированы после переключения.', switchBackendHelper: 'Переключиться на {0}?', + switchDockerBackendHelper: 'Переключиться на {0}? Конфигурация Docker будет обновлена, а Docker перезапущен.', ruleSyncTitle: 'Синхронизация правил', ruleSyncAction: 'Синхронизировать правила', ruleSyncHelper: @@ -4244,6 +4238,8 @@ const message = { blocked: 'Недоступно', }, uninstalledStatus: 'Не установлено', + selectedBackendNotInstalled: + 'Служба {backend} не обнаружена. Установите её вручную из {library} или переключите бэкенд межсетевого экрана в разделе {settings}.', initializedStatus: 'Инициализировано', partiallyInitialized: 'Частично инициализировано', dockerGuardHelper: @@ -4276,8 +4272,10 @@ const message = { denySources: 'Запретить указанные источники', allowSources: 'Разрешить только указанные источники', denyAll: 'Запретить любой доступ', - sourcesHelper: - 'Введите по одному адресу IPv4/IPv6 или CIDR в строке; пустой список разрешений запрещает любой доступ', + dockerGuardMixedFamilyHelper: + 'Выбраны IPv4 и IPv6. Правила с указанными источниками настройте отдельно; запретить любой доступ можно сразу.', + dockerGuardInconsistentConfigHelper: + 'Выбранные правила имеют разные настройки. Задайте их заново, чтобы перезаписать все; пустое описание очистит описания у всех правил.', effective: 'Действует', forwardUnsynced: 'Не синхронизировано', notEnabled: 'Не включено', @@ -4338,7 +4336,11 @@ const message = { destinationPortPlaceholder: 'например: 80, 80,443 или 8080-8089', deleteRuleConfirm: 'Удалит {0} правил. Продолжить?', deleteUsedRuleConfirm: - 'Этот порт используется: {0}. Удаление разрешающего правила может сделать сервис недоступным. Продолжить?', + 'Это правило охватывает слушающие сервисы, предоставляемые {0}. Его удаление может сделать эти сервисы недоступными. Продолжить?', + deleteWildcardRuleConfirm: + 'Это правило разрешает {0} доступ к {1}. Его удаление может повлиять на доступ к нескольким сервисам. Продолжить?', + deleteRiskRulesConfirm: + 'Удалит {0} правил. {1} разрешающих правил могут повлиять на доступ к сервисам. Продолжить?', editRuleConfirm: 'Будут изменены поля: {0}. Правило будет немедленно применено и проверено. Продолжить?', }, runtime: { diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 10dc946cc7f6..73851b9b01f2 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4134,27 +4134,20 @@ const message = { adoptRuleConfirm: 'Devraldıktan sonra 1Panel bu mevcut kuralı yönetebilir ve silebilir. Devam edilsin mi?', plan_equivalent_external_rule: 'Aynı harici kural zaten var. Kopya oluşturmak yerine yönetimi devralın.', plan_multiple_equivalent_external_rules: 'Birden fazla aynı harici kural var. Yönetilecek kuralı seçin.', - plan_requested_rule_is_covered: - 'Mevcut bir kural bu yapılandırmayı zaten kapsıyor. Devam etmek örtüşen bir kural oluşturur.', plan_equivalent_managed_rule: 'Aynı kural zaten 1Panel tarafından yönetiliyor. Kopya gerekmez.', 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_warning: 'Uyarı', ruleCheckStatus_error: 'Hata', ruleCheckExistingHelper: 'Aynı kural zaten mevcut ve atlanacak.', ruleCheckReadyHelper: 'Kontrol başarılı. Bu kural oluşturulabilir.', ruleCheckExternalExists: 'Aynı harici kural zaten mevcut ve otomatik olarak atlanacak.', ruleCheckBlockedHelper: 'Hatalı kurallar gönderilemez. Geri dönüp düzenleyin ve yeniden kontrol edin.', - ruleCheckWarningHelper: 'Kontrol tamamlandı. Uyarılar gönderimi engellemez; devam edebilirsiniz.', 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.', plan_protected_rule: 'Bu kural korumalıdır; yönetimi devralınamaz, değiştirilemez veya silinemez.', - plan_overlapping_rule_with_different_action: 'Karşıt eylemli örtüşen bir kural var. İşlem durduruldu.', - plan_partially_overlapping_rule_with_different_action: - 'Kural, karşıt eylemli bir kuralla kısmen örtüşüyor. Sonuç kural sırasına bağlıdır.', plan_blocked: 'Bu kural güvenle uygulanamıyor. Listeyi yenileyip tekrar deneyin.', scopeDefaultMismatch: 'Sistemin varsayılan zone değeri {0}; bu sayfa yalnızca public zone alanını yönetir.', scopeInactive: 'Yönetilen kapsam etkin değil. Yeni kurallar mevcut trafiği etkilemeyebilir.', @@ -4185,6 +4178,7 @@ const message = { systemFirewallHelper: 'Ana makine port erişimini ve gelen kuralları yönetir.', forwardingHelper: 'Port yönlendirme kurallarını yönetir.', dockerFirewallHelper: '1Panel konteyner portu korumasının nasıl yönetileceğini seçer.', + dockerNftablesRequirement: 'Docker ≥ 29.0.0, deneysel', backendRecommendation: 'iptables veya nftables kullanılması önerilir.', configuredRules: '{0} kural yapılandırıldı', addressFamily: 'IP sürümü', @@ -4202,10 +4196,12 @@ const message = { '{0} içindeki tüm 1Panel Docker bağlantı noktası koruma kurallarını ve zincirlerini silin, yalnızca veritabanı verilerini koruyun', cleanupBeforeBackendSwitch: 'Mevcut {0} arka ucu hâlâ 1Panel çalışma zamanı kuralları içeriyor. {1} arka ucuna geçmeden önce temizleyin.', - cleanupAction: 'Temizle', + cleanupAction: 'Sıfırla', backendSwitchNotice: 'Sistem güvenlik duvarı, bağlantı noktası yönlendirme veya Docker korumasını değiştirmeden önce mevcut arka ucu temizleyin. Veritabanı ilkeleri korunur ve geçişten sonra yeniden başlatılabilir veya eşitlenebilir.', switchBackendHelper: '{0} arka ucuna geçilsin mi?', + switchDockerBackendHelper: + '{0} arka ucuna geçilsin mi? Docker yapılandırması güncellenecek ve Docker yeniden başlatılacak.', ruleSyncTitle: 'Kuralları eşitle', ruleSyncAction: 'Kuralları eşitle', ruleSyncHelper: @@ -4256,6 +4252,8 @@ const message = { blocked: 'Kullanılamaz', }, uninstalledStatus: 'Yüklü değil', + selectedBackendNotInstalled: + '{backend} hizmeti algılanmadı. {library} üzerinden manuel olarak yükleyin veya {settings} bölümünden güvenlik duvarı arka ucunu değiştirin.', initializedStatus: 'Başlatıldı', partiallyInitialized: 'Kısmen başlatıldı', dockerGuardHelper: @@ -4288,7 +4286,10 @@ const message = { denySources: 'Belirtilen kaynakları reddet', allowSources: 'Yalnızca belirtilen kaynaklara izin ver', denyAll: 'Tüm erişimi reddet', - sourcesHelper: 'Her satıra bir IPv4/IPv6 adresi veya CIDR girin; boş izin listesi tüm erişimi reddeder', + dockerGuardMixedFamilyHelper: + 'IPv4 ve IPv6 birlikte seçildi. Kaynak tabanlı ilkeleri ayrı ayrı yapılandırın; tüm erişimi reddet doğrudan uygulanabilir.', + dockerGuardInconsistentConfigHelper: + 'Seçili kuralların yapılandırmaları farklı. Tümünün üzerine yazmak için yeniden ayarlayın; açıklamanın boş bırakılması tüm açıklamaları temizler.', effective: 'Etkin', forwardUnsynced: 'Senkronize değil', notEnabled: 'Etkin değil', @@ -4347,7 +4348,11 @@ const message = { destinationPortPlaceholder: 'örn. 80, 80,443 veya 8080-8089', deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?', deleteUsedRuleConfirm: - 'Bu port {0} tarafından kullanılıyor. İzin kuralını silmek hizmeti erişilemez hale getirebilir. Devam edilsin mi?', + 'Bu kural, {0} tarafından sağlanan dinleme hizmetlerini kapsıyor. Silinmesi bu hizmetleri erişilemez hale getirebilir. Devam edilsin mi?', + deleteWildcardRuleConfirm: + '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?', }, runtime: { diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 1632442b38ee..d29ba3e86944 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3797,26 +3797,20 @@ const message = { adoptRuleConfirm: '納入管理後,1Panel 將負責維護及刪除這條現有規則,是否繼續?', plan_equivalent_external_rule: '系統中已有相同的外部規則,可將它納入管理,無需重複建立。', plan_multiple_equivalent_external_rules: '系統中有多條相同的外部規則,請選擇其中一條納入管理。', - plan_requested_rule_is_covered: '現有規則已覆蓋本次設定,繼續建立會產生重疊規則。', plan_equivalent_managed_rule: '相同規則已由 1Panel 管理,無需重複建立。', allRulesAlreadyExist: '本次檢查的 {0} 條規則均已存在,沒有需要建立的新規則。', ruleCheckResult: '規則檢查結果', ruleCheckStatus_creatable: '可建立', ruleCheckStatus_existing: '已存在', - ruleCheckStatus_warning: '警告', ruleCheckStatus_error: '錯誤', ruleCheckExistingHelper: '系統中已存在相同規則,本次將自動略過。', ruleCheckReadyHelper: '檢查通過,可以建立此規則。', ruleCheckExternalExists: '系統中已有相同的外部規則,本次將自動略過。', ruleCheckBlockedHelper: '存在錯誤規則,無法提交。請返回修改後重新檢查。', - ruleCheckWarningHelper: '檢查完成。警告不會阻止提交,可以直接繼續。', plan_managed_rule_drifted: '1Panel 管理記錄與系統實際規則不一致,請檢查系統規則。', plan_opaque_rule_in_target_scope: '目標範圍內有無法安全解析的規則,本次操作已停止。', plan_runtime_permanent_mismatch: '防火牆執行設定與永久設定不一致,請先完成同步。', plan_protected_rule: '此規則受到保護,不能認領、修改或刪除。', - plan_overlapping_rule_with_different_action: '有範圍重疊但動作相反的規則,本次操作已停止。', - plan_partially_overlapping_rule_with_different_action: - '與相反動作的規則部分重疊,實際效果取決於規則順序,確認後可繼續。', plan_blocked: '目前規則無法安全套用,請重新整理規則後再試。', scopeDefaultMismatch: '系統預設 zone 為 {0},目前頁面僅管理 public zone。', scopeInactive: '目前託管範圍未啟用,新增規則可能不會作用於現有流量。', @@ -3846,6 +3840,7 @@ const message = { systemFirewallHelper: '用於管理主機連接埠存取和入站規則。', forwardingHelper: '用於管理連接埠轉發規則。', dockerFirewallHelper: '用於選擇 1Panel 容器連接埠防護的管理方式。', + dockerNftablesRequirement: 'Docker ≥ 29.0.0,實驗性', backendRecommendation: '建議使用 iptables 或 nftables 管理。', configuredRules: '已設定 {0} 條規則', addressFamily: 'IP 版本', @@ -3858,10 +3853,11 @@ const message = { cleanupForwardingBackendHelper: '刪除 {0} 中全部 1Panel 連接埠轉送規則及規則鏈,僅保留資料庫資料', cleanupDockerBackendHelper: '刪除 {0} 中全部 1Panel Docker 連接埠防護規則及規則鏈,僅保留資料庫資料', cleanupBeforeBackendSwitch: '目前後端 {0} 仍存在 1Panel 執行時規則,請先清理該後端,再切換到 {1}。', - cleanupAction: '清理', + cleanupAction: '重設', backendSwitchNotice: '主機防火牆、連接埠轉送和 Docker 防護切換前都必須先清理目前後端;資料庫策略會保留,切換後可重新初始化或同步。', switchBackendHelper: '確認切換為 {0}?', + switchDockerBackendHelper: '確認切換為 {0}?此操作會更新 Docker 設定並重新啟動 Docker 服務。', ruleSyncTitle: '同步規則', ruleSyncAction: '同步規則', ruleSyncHelper: '將所選防火牆中由 1Panel 管理的規則同步至目前防火牆。原規則不會刪除,外部規則不會同步。', @@ -3910,6 +3906,8 @@ const message = { blocked: '不可同步', }, uninstalledStatus: '未安裝', + selectedBackendNotInstalled: + '未偵測到 {backend} 服務,請前往{library}頁面手動安裝,或在{settings}中切換防火牆後端。', initializedStatus: '已初始化', partiallyInitialized: '部分初始化', dockerGuardHelper: '為 Docker 容器發佈到主機的連接埠設定存取限制;未設定防護的連接埠維持 Docker 預設存取方式。', @@ -3938,7 +3936,8 @@ const message = { denySources: '禁止指定來源', allowSources: '僅允許指定來源', denyAll: '禁止所有存取', - sourcesHelper: '每行輸入一個 IPv4/IPv6 位址或 CIDR;允許清單為空時禁止所有存取', + dockerGuardMixedFamilyHelper: '已同時選擇 IPv4 和 IPv6。指定來源需分別設定,禁止所有可直接套用。', + dockerGuardInconsistentConfigHelper: '所選規則設定不一致,請重新設定;提交後將統一覆寫,空白描述將統一清除。', effective: '已生效', forwardUnsynced: '未同步', notEnabled: '未啟用', @@ -3991,7 +3990,9 @@ const message = { sourceAddressPlaceholder: '例如:172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', destinationPortPlaceholder: '例如:80、80,443 或 8080-8089', deleteRuleConfirm: '將刪除 {0} 條規則,是否繼續?', - deleteUsedRuleConfirm: '該連接埠正被 {0} 使用。刪除放行規則可能導致服務無法存取,是否繼續?', + deleteUsedRuleConfirm: '該規則涵蓋由 {0} 提供的監聽服務。刪除後可能導致相關服務無法存取,是否繼續?', + deleteWildcardRuleConfirm: '該規則允許 {0} 存取 {1}。刪除後可能影響多個服務的存取,是否繼續?', + deleteRiskRulesConfirm: '將刪除 {0} 條規則,其中 {1} 條放行規則可能影響服務存取,是否繼續?', editRuleConfirm: '將修改以下欄位:{0}。提交後會立即套用並回讀驗證,是否繼續?', }, runtime: { diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 7506ca240d40..0ed864e0e7ca 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3843,26 +3843,20 @@ const message = { adoptRuleConfirm: '纳入管理后,1Panel 将负责维护和删除这条现有规则,是否继续?', plan_equivalent_external_rule: '系统中已有相同的外部规则,可以将它纳入管理,无需重复创建。', plan_multiple_equivalent_external_rules: '系统中有多条相同的外部规则,请选择其中一条纳入管理。', - plan_requested_rule_is_covered: '现有规则已经覆盖本次配置,继续创建会产生重叠规则。', plan_equivalent_managed_rule: '相同规则已由 1Panel 管理,无需重复创建。', allRulesAlreadyExist: '本次检查的 {0} 条规则均已存在,没有需要创建的新规则。', ruleCheckResult: '规则检查结果', ruleCheckStatus_creatable: '可创建', ruleCheckStatus_existing: '已存在', - ruleCheckStatus_warning: '警告', ruleCheckStatus_error: '错误', ruleCheckExistingHelper: '系统中已存在相同规则,本次将自动跳过。', ruleCheckReadyHelper: '检查通过,可以创建该规则。', ruleCheckExternalExists: '系统中已有相同的外部规则,本次将自动跳过。', ruleCheckBlockedHelper: '存在错误规则,不能提交。请返回修改后重新检查。', - ruleCheckWarningHelper: '检查完成。警告不会阻止提交,可以直接继续。', plan_managed_rule_drifted: '1Panel 管理记录与系统实际规则不一致,请检查系统规则。', plan_opaque_rule_in_target_scope: '目标范围内存在无法安全解析的规则,本次操作已停止。', plan_runtime_permanent_mismatch: '防火墙运行配置与永久配置不一致,请先完成同步。', plan_protected_rule: '该规则受到保护,不能认领、修改或删除。', - plan_overlapping_rule_with_different_action: '存在范围重叠但动作相反的规则,本次操作已停止。', - plan_partially_overlapping_rule_with_different_action: - '与相反动作的规则部分重叠,实际效果取决于规则顺序,确认后可继续。', plan_blocked: '当前规则无法安全应用,请刷新规则后重试。', scopeDefaultMismatch: '系统默认 zone 为 {0},当前页面仅管理 public zone。', scopeInactive: '当前托管范围未激活,新增规则可能不会作用于现有流量。', @@ -3892,6 +3886,7 @@ const message = { systemFirewallHelper: '用于管理主机端口访问和入站规则。', forwardingHelper: '用于管理端口转发规则。', dockerFirewallHelper: '用于选择 1Panel 容器端口防护的管理方式。', + dockerNftablesRequirement: 'Docker ≥ 29.0.0,实验性', backendRecommendation: '建议使用 iptables 或 nftables 管理。', configuredRules: '已配置 {0} 条规则', addressFamily: 'IP 版本', @@ -3951,11 +3946,14 @@ const message = { cleanupForwardingBackendHelper: '删除 {0} 中全部 1Panel 端口转发规则及规则链,仅保留数据库数据', cleanupDockerBackendHelper: '删除 {0} 中全部 1Panel Docker 端口防护规则及规则链,仅保留数据库数据', cleanupBeforeBackendSwitch: '当前后端 {0} 仍存在 1Panel 运行时规则,请先清理该后端,再切换到 {1}。', - cleanupAction: '清理', + cleanupAction: '重置', backendSwitchNotice: '主机防火墙、端口转发和 Docker 防护切换前都需要先清理当前后端;数据库策略会保留,切换后可以重新初始化或同步。', switchBackendHelper: '确认切换为 {0}?', + switchDockerBackendHelper: '确认切换为 {0}?该操作会更新 Docker 配置并重启 Docker 服务。', uninstalledStatus: '未安装', + selectedBackendNotInstalled: + '未检测到 {backend} 服务,请前往{library}页面手动安装,或在{settings}中切换防火墙后端。', initializedStatus: '已初始化', partiallyInitialized: '部分初始化', dockerGuardHelper: '为 Docker 容器发布到宿主机的端口配置访问限制;未设置防护的端口保持 Docker 默认访问方式。', @@ -3983,7 +3981,8 @@ const message = { denySources: '禁止指定来源', allowSources: '仅允许指定来源', denyAll: '禁止所有访问', - sourcesHelper: '每行输入一个 IPv4/IPv6 地址或 CIDR;白名单为空时禁止所有访问', + dockerGuardMixedFamilyHelper: '已同时选择 IPv4 和 IPv6。指定来源需分别设置,禁止所有可直接应用。', + dockerGuardInconsistentConfigHelper: '所选规则配置不一致,请重新设置;提交后将统一覆盖,空描述将统一清空。', effective: '已生效', forwardUnsynced: '未同步', notEnabled: '未启用', @@ -4037,7 +4036,9 @@ const message = { sourceAddressPlaceholder: '例如:172.16.10.11、172.16.0.0/24、2001:db8::1 或 2001:db8::/64', destinationPortPlaceholder: '例如:80、80,443 或 8080-8089', deleteRuleConfirm: '将删除 {0} 条规则,是否继续?', - deleteUsedRuleConfirm: '该端口正在被 {0} 使用。删除放行规则可能导致服务无法访问,是否继续?', + deleteUsedRuleConfirm: '该规则覆盖了由 {0} 提供的监听服务。删除后可能导致相关服务无法访问,是否继续?', + deleteWildcardRuleConfirm: '该规则允许 {0} 访问 {1}。删除后可能影响多个服务的访问,是否继续?', + deleteRiskRulesConfirm: '将删除 {0} 条规则,其中 {1} 条放行规则可能影响服务访问,是否继续?', editRuleConfirm: '将修改以下字段:{0}。提交后会立即应用并回读验证,是否继续?', }, runtime: { diff --git a/frontend/src/views/host/firewall/docker/detail/index.vue b/frontend/src/views/host/firewall/docker/detail/index.vue index 713fb4ccdcaf..110af4e683f0 100644 --- a/frontend/src/views/host/firewall/docker/detail/index.vue +++ b/frontend/src/views/host/firewall/docker/detail/index.vue @@ -28,12 +28,22 @@ :disabled="selectedPolicyUUIDs.length === 0" @click="remove(selectedEndpoints, true)" > - {{ $t('commons.button.clean') }} + {{ $t('commons.button.delete') }} + + + + +
- - + + + + - {{ $t('firewall.denySources') }} - {{ $t('firewall.allowSources') }} + + {{ $t('firewall.denySources') }} + + + {{ $t('firewall.allowSources') }} + {{ $t('firewall.denyAll') }} - - + +
+ + + +
+ + {{ $t('commons.button.add') }} +
@@ -107,13 +153,14 @@