From 19bb823b050a7f5818f01514b947d2b2eb7de01c Mon Sep 17 00:00:00 2001 From: ssongliu Date: Mon, 21 Sep 2026 17:12:17 +0800 Subject: [PATCH] fix(firewall): guard IPv6 forwarding against RA disruption (#13882) --- agent/app/dto/firewall.go | 11 ++- agent/app/service/firewall_utils.go | 25 +++-- agent/app/service/forward.go | 14 +++ agent/utils/firewall/forwarding/iptables.go | 60 +++++++---- agent/utils/firewall/forwarding/nftables.go | 82 +++++++++++---- agent/utils/firewall/forwarding/sysctl.go | 99 ++++++++++++++++++- frontend/src/api/interface/firewall.ts | 1 + frontend/src/lang/modules/en.ts | 6 ++ frontend/src/lang/modules/es-es.ts | 6 ++ frontend/src/lang/modules/fa.ts | 5 + frontend/src/lang/modules/ja.ts | 5 + frontend/src/lang/modules/ko.ts | 6 ++ frontend/src/lang/modules/lo.ts | 5 + frontend/src/lang/modules/ms.ts | 6 ++ frontend/src/lang/modules/pt-br.ts | 6 ++ frontend/src/lang/modules/ru.ts | 6 ++ frontend/src/lang/modules/tr.ts | 6 ++ frontend/src/lang/modules/zh-Hant.ts | 4 + frontend/src/lang/modules/zh.ts | 4 + .../src/views/host/firewall/status/index.vue | 26 +++-- 20 files changed, 320 insertions(+), 63 deletions(-) diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 8ee530aae..300e47fb8 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -49,11 +49,12 @@ type FirewallBackendOption struct { } type FirewallBackendFamilyStatus struct { - Available bool `json:"available"` - Initialized bool `json:"initialized"` - Bound bool `json:"bound"` - Reason string `json:"reason,omitempty"` - ForwardPolicy string `json:"forwardPolicy,omitempty"` + Available bool `json:"available"` + Initialized bool `json:"initialized"` + Bound bool `json:"bound"` + Reason string `json:"reason,omitempty"` + ForwardPolicy string `json:"forwardPolicy,omitempty"` + RAInterfaces []string `json:"raInterfaces,omitempty"` } type FirewallBackendGroup struct { diff --git a/agent/app/service/firewall_utils.go b/agent/app/service/firewall_utils.go index 33058c42b..ed58ff0fc 100644 --- a/agent/app/service/firewall_utils.go +++ b/agent/app/service/firewall_utils.go @@ -2413,18 +2413,23 @@ func firewallDeleteChange(current filter.ObservedRule, desired filter.DesiredRul func loadForwardingFirewallOverview(manager forwarding.Adapter) (dto.FirewallSubsystemStatus, error) { ipv4, ipv4Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv4) ipv6, ipv6Err := loadForwardingFamilyInfo(manager, manager.Name(), constant.FirewallFamilyIPv6) - initialized, bound, statusErr := ipv4.Initialized, ipv4.Bound, ipv4Err - if manager.Name() == constant.FirewallProviderNftables { - if statusErr == nil && initialized && bound { - initialized, bound, statusErr = ipv6.Initialized, ipv6.Bound, ipv6Err - } - } else { - statusErr = errors.Join(statusErr, ipv6Err) - if ipv6.Available { - initialized, bound = initialized && ipv6.Initialized, bound && ipv6.Bound + if ipv6.Available { + interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile) + var pathError *os.PathError + switch { + case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6": + ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false + case err != nil: + ipv6.Reason = "ipv6_ra_check_failed" + ipv6.Bound = false + case len(interfaces) > 0: + ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces + ipv6.Bound = false + case !ipv6.Bound: + ipv6.Reason = "ipv6_forwarding_not_enabled" } } - return dto.FirewallSubsystemStatus{IsInit: initialized, IsBind: bound, IPv4: ipv4, IPv6: ipv6}, statusErr + return dto.FirewallSubsystemStatus{IsInit: ipv4.Initialized || ipv6.Initialized, IsBind: ipv4.Bound || ipv6.Bound, IPv4: ipv4, IPv6: ipv6}, errors.Join(ipv4Err, ipv6Err) } func loadForwardingFamilyInfo(manager forwarding.Adapter, backend, family string) (dto.FirewallBackendFamilyStatus, error) { diff --git a/agent/app/service/forward.go b/agent/app/service/forward.go index 492bf7216..23dcfba57 100644 --- a/agent/app/service/forward.go +++ b/agent/app/service/forward.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "strconv" "strings" "sync" @@ -459,6 +460,19 @@ func (s *ForwardingService) operateRules(ctx context.Context, request dto.Forwar } return nil } + for _, rule := range groups[1].rules { + if rule.Family != forwarding.FamilyIPv6 { + continue + } + interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile) + if err != nil { + return fmt.Errorf("check IPv6 Router Advertisement: %w", err) + } + if len(interfaces) > 0 { + return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", ")) + } + break + } } var client forwarding.Adapter var failures []error diff --git a/agent/utils/firewall/forwarding/iptables.go b/agent/utils/firewall/forwarding/iptables.go index cb29ffdd3..a6e3689b2 100644 --- a/agent/utils/firewall/forwarding/iptables.go +++ b/agent/utils/firewall/forwarding/iptables.go @@ -90,7 +90,6 @@ func (systemIptablesBackend) LoadIPv6RulesFromFile(table, chain, fileName string type forwardingSystem interface { ReadFile(name string) ([]byte, error) WriteFile(name string, data []byte, perm os.FileMode) error - RunWithOptionalSudo(name string, args ...string) error } type defaultForwardingSystem struct{} @@ -103,10 +102,6 @@ func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileM return cmd.WriteFileWithOptionalSudo(name, data, perm) } -func (defaultForwardingSystem) RunWithOptionalSudo(name string, args ...string) error { - return cmd.NewCommandMgr().RunWithOptionalSudo(name, args...) -} - type Iptables struct { provider string backend iptablesBackend @@ -151,15 +146,33 @@ func (l *Iptables) ReplaceRules(rules []Rule) error { if err != nil { return err } - if normalized.Family == FamilyIPv6 && !l.backend.IPv6Available() { - return fmt.Errorf("ip6tables command family is unavailable") - } byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized) } + var failures []error for _, family := range []string{FamilyIPv4, FamilyIPv6} { if family == FamilyIPv6 && !l.backend.IPv6Available() { + if len(byFamily[family]) > 0 { + failures = append(failures, fmt.Errorf("ip6tables command family is unavailable")) + } continue } + if family == FamilyIPv6 { + if len(byFamily[family]) > 0 { + if err := ensureForwardingSysctls(l.system, true); err != nil { + failures = append(failures, err) + continue + } + } else { + initialized, _, err := l.familyInitStatus(family) + if err != nil { + failures = append(failures, err) + continue + } + if !initialized { + continue + } + } + } if err := l.batchEnsureChains(family); err != nil { return err } @@ -171,7 +184,7 @@ func (l *Iptables) ReplaceRules(rules []Rule) error { return fmt.Errorf("restore %s forwarding rules: %w", family, err) } } - return nil + return errors.Join(failures...) } func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error { @@ -186,6 +199,17 @@ func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error { if family == "" { family = FamilyIPv4 } + if family == FamilyIPv6 { + if !l.backend.IPv6Available() { + return fmt.Errorf("ip6tables command family is unavailable") + } + if err := ensureForwardingSysctls(l.system, true); err != nil { + return err + } + if err := l.batchEnsureChains(family); err != nil { + return err + } + } return l.backend.Restore(ctx, family, script) } @@ -287,19 +311,10 @@ func isRemoteTarget(family, target string) bool { } func (l *Iptables) Enable() error { - if err := ensureForwardingSysctls(l.system, l.backend.IPv6Available()); err != nil { + if err := ensureForwardingSysctls(l.system, false); err != nil { return err } - - for _, family := range []string{FamilyIPv4, FamilyIPv6} { - if family == FamilyIPv6 && !l.backend.IPv6Available() { - continue - } - if err := l.batchEnsureChains(family); err != nil { - return err - } - } - return nil + return l.batchEnsureChains(FamilyIPv4) } func (l *Iptables) batchEnsureChains(family string) error { @@ -506,7 +521,7 @@ func (l *Iptables) InitStatus() (bool, bool, error) { if err != nil { return false, false, err } - return ipv4Init && ipv6Init, ipv4Bind && ipv6Bind, nil + return ipv4Init || ipv6Init, ipv4Bind || ipv6Bind, nil } func (l *Iptables) familyInitStatus(family string) (bool, bool, error) { @@ -519,6 +534,9 @@ func (l *Iptables) familyInitStatus(family string) (bool, bool, error) { list = l.backend.RunIPv6WithStd } data, err := l.system.ReadFile(sysctlPath) + if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { + return false, false, nil + } if err != nil { return false, false, fmt.Errorf("read %s forwarding status: %w", label, err) } diff --git a/agent/utils/firewall/forwarding/nftables.go b/agent/utils/firewall/forwarding/nftables.go index 789d5b410..bb0bb900d 100644 --- a/agent/utils/firewall/forwarding/nftables.go +++ b/agent/utils/firewall/forwarding/nftables.go @@ -48,14 +48,45 @@ func (n *Nftables) List() ([]Rule, error) { } func (n *Nftables) ReplaceRules(rules []Rule) error { - if err := ensureNftForwardTables(); err != nil { - return fmt.Errorf("initialize nftables forwarding table: %w", err) + byFamily := map[string][]Rule{FamilyIPv4: nil, FamilyIPv6: nil} + for _, rule := range rules { + normalized, err := NormalizeRule(rule) + if err != nil { + return err + } + byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized) } - commands, err := rebuildNftForwardCommands(rules) - if err != nil { - return err + var failures []error + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + if family == FamilyIPv6 { + if len(byFamily[family]) > 0 { + if err := ensureForwardingSysctls(n.system, true); err != nil { + failures = append(failures, err) + continue + } + } else { + _, exists, err := nftables_helper.ReadTable(nftRun, nftTableFamily(family), nftForwardTable) + if err != nil { + failures = append(failures, err) + continue + } + if !exists { + continue + } + } + } + if err := ensureNftForwardTables(family); err != nil { + return fmt.Errorf("initialize nftables forwarding table: %w", err) + } + commands, err := rebuildNftForwardCommands(byFamily[family], family) + if err != nil { + return err + } + if err := nftRunCommands(context.Background(), commands); err != nil { + return err + } } - return nftRunCommands(context.Background(), commands) + return errors.Join(failures...) } func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error { @@ -66,6 +97,17 @@ func (n *Nftables) CreateRules(ctx context.Context, rules []Rule) error { if err != nil { return err } + for _, rule := range rules { + if strings.EqualFold(strings.TrimSpace(rule.Family), FamilyIPv6) { + if err := ensureForwardingSysctls(n.system, true); err != nil { + return err + } + if err := ensureNftForwardTables(FamilyIPv6); err != nil { + return err + } + break + } + } return nftRunCommands(ctx, commands) } @@ -113,10 +155,10 @@ func (n *Nftables) DeleteRules(ctx context.Context, rules []Rule) error { } func (n *Nftables) Enable() error { - if err := ensureForwardingSysctls(n.system, true); err != nil { + if err := ensureForwardingSysctls(n.system, false); err != nil { return err } - if err := ensureNftForwardTables(); err != nil { + if err := ensureNftForwardTables(FamilyIPv4); err != nil { return fmt.Errorf("initialize nftables forwarding table: %w", err) } return nil @@ -144,13 +186,16 @@ func (n *Nftables) Cleanup() error { } func (n *Nftables) InitStatus() (bool, bool, error) { + var anyInitialized, anyBound bool for _, family := range []string{FamilyIPv4, FamilyIPv6} { initialized, bound, err := n.FamilyStatus(family) - if err != nil || !initialized || !bound { - return initialized, bound, err + if err != nil { + return false, false, err } + anyInitialized = anyInitialized || initialized + anyBound = anyBound || bound } - return true, true, nil + return anyInitialized, anyBound, nil } func (n *Nftables) FamilyStatus(family string) (bool, bool, error) { @@ -159,6 +204,9 @@ func (n *Nftables) FamilyStatus(family string) (bool, bool, error) { sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" } data, err := n.system.ReadFile(sysctlPath) + if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) { + return false, false, nil + } if err != nil { return false, false, fmt.Errorf("read %s forwarding status: %w", family, err) } @@ -197,9 +245,9 @@ func (n *Nftables) Replay() error { return nftRunCommand("-f", file) } -func ensureNftForwardTables() error { +func ensureNftForwardTables(families ...string) error { commands := make([][]string, 0, 8) - for _, family := range []string{FamilyIPv4, FamilyIPv6} { + for _, family := range families { tableFamily := nftTableFamily(family) output, tableExists, err := nftables_helper.ReadTable(nftRun, tableFamily, nftForwardTable) if err != nil { @@ -232,12 +280,10 @@ func ensureNftForwardTables() error { return nftRunCommands(context.Background(), commands) } -func rebuildNftForwardCommands(rules []Rule) ([][]string, error) { +func rebuildNftForwardCommands(rules []Rule, family string) ([][]string, error) { commands := make([][]string, 0, 6+len(rules)*4) - for _, family := range []string{FamilyIPv4, FamilyIPv6} { - for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { - commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, "NFT_" + chain}) - } + for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { + commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, "NFT_" + chain}) } additions, err := createNftForwardCommands(rules) return append(commands, additions...), err diff --git a/agent/utils/firewall/forwarding/sysctl.go b/agent/utils/firewall/forwarding/sysctl.go index 64f22600e..4e294be19 100644 --- a/agent/utils/firewall/forwarding/sysctl.go +++ b/agent/utils/firewall/forwarding/sysctl.go @@ -4,12 +4,24 @@ import ( "errors" "fmt" "os" + "path/filepath" + "sort" + "strconv" "strings" "github.com/1Panel-dev/1Panel/agent/constant" ) func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { + if withIPv6 { + interfaces, err := IPv6RAInterfaces(system.ReadFile) + if err != nil { + return fmt.Errorf("check IPv6 Router Advertisement: %w", err) + } + if len(interfaces) > 0 { + return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", ")) + } + } paths := []string{"/proc/sys/net/ipv4/ip_forward"} if withIPv6 { paths = append(paths, "/proc/sys/net/ipv6/conf/all/forwarding") @@ -27,12 +39,93 @@ func ensureForwardingSysctls(system forwardingSystem, withIPv6 bool) error { if err := system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm); err != nil { return fmt.Errorf("failed to persist IP forwarding: %w", err) } - if err := system.RunWithOptionalSudo("sysctl", "-p"); err != nil { - return fmt.Errorf("failed to apply IP forwarding: %w", err) - } return nil } +func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) { + addresses, err := readFile("/proc/net/if_inet6") + if err != nil { + return nil, err + } + const permanentAddress = 0x80 + globalAddresses := make(map[string]bool) + raInterfaces := make(map[string]bool) + for _, line := range strings.Split(string(addresses), "\n") { + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + if len(fields) != 6 { + return nil, fmt.Errorf("invalid IPv6 address entry: %q", line) + } + name := fields[5] + if name == "lo" { + continue + } + flags, err := strconv.ParseUint(fields[4], 16, 32) + if err != nil { + return nil, err + } + if _, exists := globalAddresses[name]; !exists { + globalAddresses[name] = false + } + if fields[3] == "00" { + globalAddresses[name] = true + if flags&permanentAddress == 0 { + raInterfaces[name] = true + } + } + } + routes, err := readFile("/proc/net/ipv6_route") + if err != nil { + return nil, err + } + const raRouteFlags = 0x00010000 | 0x00800000 + for _, line := range strings.Split(string(routes), "\n") { + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + if len(fields) != 10 { + return nil, fmt.Errorf("invalid IPv6 route entry: %q", line) + } + flags, err := strconv.ParseUint(fields[8], 16, 32) + if err != nil { + return nil, err + } + if flags&raRouteFlags != 0 { + raInterfaces[fields[9]] = true + } + } + for name, hasGlobalAddress := range globalAddresses { + if !hasGlobalAddress { + raInterfaces[name] = true + } + } + var interfaces []string + for name := range raInterfaces { + if name == "lo" { + continue + } + if name == "." || name == ".." || filepath.Base(name) != name { + return nil, fmt.Errorf("invalid IPv6 interface %q", name) + } + value, err := readFile("/proc/sys/net/ipv6/conf/" + name + "/accept_ra") + if err != nil { + return nil, err + } + switch strings.TrimSpace(string(value)) { + case "1": + interfaces = append(interfaces, name) + case "0", "2": + default: + return nil, fmt.Errorf("invalid accept_ra value for %s", name) + } + } + sort.Strings(interfaces) + return interfaces, nil +} + func enableForwardingSysctls(content string, withIPv6 bool) string { lines := strings.Split(strings.TrimRight(content, "\n"), "\n") wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"} diff --git a/frontend/src/api/interface/firewall.ts b/frontend/src/api/interface/firewall.ts index beec87868..9152d0a58 100644 --- a/frontend/src/api/interface/firewall.ts +++ b/frontend/src/api/interface/firewall.ts @@ -23,6 +23,7 @@ export namespace Firewall { bound: boolean; reason?: string; forwardPolicy?: 'ACCEPT' | 'DROP'; + raInterfaces?: string[]; } export interface BackendGroup { selected: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 428a587cd..961f9abf3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -4294,6 +4294,12 @@ const message = { dockerInputNotProtected: 'Host INPUT rules do not directly protect this Docker published port. Click to open Container Port Guard.', notInitialized: 'Not initialized', + ipv6RARisk: + 'IPv6 forwarding is blocked: interfaces {0} have accept_ra=1 and may rely on RA/SLAAC. Review the network configuration, persist accept_ra=2 on interfaces that need RA, then retry.', + ipv6RACheckFailed: + 'Cannot check IPv6 RA configuration. Enabling IPv6 forwarding is blocked. Check the system network configuration and retry.', + ipv6ForwardingOnDemand: + 'IPv6 forwarding is not enabled. It will be checked and enabled when IPv6 rules are added or restored.', familyUnsupported: 'The system does not support {0}', familyChainIssue: '{0} · {1} chain · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index e226cb875..f43610cb3 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4341,6 +4341,12 @@ const message = { dockerInputNotProtected: 'Las reglas INPUT del host no protegen directamente este puerto publicado por Docker. Haz clic para abrir la protección de puertos de contenedores.', notInitialized: 'No inicializado', + ipv6RARisk: + 'El reenvío IPv6 está bloqueado: las interfaces {0} tienen accept_ra=1 y pueden depender de RA/SLAAC. Revise la configuración de red, configure accept_ra=2 de forma persistente en las interfaces que necesitan RA y vuelva a intentarlo.', + ipv6RACheckFailed: + 'No se puede comprobar la configuración RA de IPv6. Se ha bloqueado la activación del reenvío IPv6. Revise la configuración de red y vuelva a intentarlo.', + ipv6ForwardingOnDemand: + 'El reenvío IPv6 no está habilitado. Se comprobará y habilitará al añadir o restaurar reglas IPv6.', familyUnsupported: 'El sistema no admite {0}', familyChainIssue: '{0} · cadena {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 2ed484008..3391427af 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -4251,6 +4251,11 @@ const message = { dockerInputNotProtected: 'قوانین INPUT میزبان مستقیماً از این پورت منتشرشده Docker محافظت نمی‌کنند. برای باز کردن محافظت از پورت کانتینر کلیک کنید.', notInitialized: 'راه‌اندازی نشده', + ipv6RARisk: + 'هدایت IPv6 مسدود شد: رابط‌های {0} دارای accept_ra=1 هستند و ممکن است به RA/SLAAC وابسته باشند. تنظیمات شبکه را بررسی کنید، برای رابط‌های نیازمند RA مقدار accept_ra=2 را به‌صورت پایدار تنظیم کنید و دوباره تلاش کنید.', + ipv6RACheckFailed: + 'بررسی تنظیمات RA در IPv6 ممکن نیست. فعال‌سازی هدایت IPv6 مسدود شده است. تنظیمات شبکه سیستم را بررسی کرده و دوباره تلاش کنید.', + ipv6ForwardingOnDemand: 'هدایت IPv6 فعال نیست. هنگام افزودن یا بازیابی قوانین IPv6 بررسی و فعال خواهد شد.', familyUnsupported: 'سیستم از {0} پشتیبانی نمی‌کند', familyChainIssue: '{0} · زنجیره {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index ff21ac0fe..a9d7204d0 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -4276,6 +4276,11 @@ const message = { dockerInputNotProtected: 'ホストの INPUT ルールでは、この Docker 公開ポートを直接保護できません。クリックしてコンテナポート保護を開きます。', notInitialized: '未初期化', + ipv6RARisk: + 'IPv6 転送がブロックされています。インターフェース {0} は accept_ra=1 で、RA/SLAAC に依存している可能性があります。ネットワーク設定を確認し、RA が必要なインターフェースで accept_ra=2 を永続的に設定してから再試行してください。', + ipv6RACheckFailed: + 'IPv6 RA 設定を確認できないため、IPv6 転送の有効化をブロックしました。システムのネットワーク設定を確認して再試行してください。', + ipv6ForwardingOnDemand: 'IPv6 転送は無効です。IPv6 ルールの追加または復元時に確認して有効化します。', familyUnsupported: 'システムは {0} をサポートしていません', familyChainIssue: '{0} · {1} チェーン · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 70c66c2c3..f309fa631 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -4199,6 +4199,12 @@ const message = { dockerInputNotProtected: '호스트 INPUT 규칙은 이 Docker 게시 포트를 직접 보호하지 않습니다. 클릭하여 컨테이너 포트 보호를 여세요.', notInitialized: '초기화되지 않음', + ipv6RARisk: + 'IPv6 전달이 차단되었습니다. 인터페이스 {0}의 accept_ra=1 설정으로 인해 RA/SLAAC에 의존할 수 있습니다. 네트워크 설정을 확인하고 RA가 필요한 인터페이스에 accept_ra=2를 영구 설정한 후 다시 시도하세요.', + ipv6RACheckFailed: + 'IPv6 RA 설정을 확인할 수 없어 IPv6 전달 활성화를 차단했습니다. 시스템 네트워크 설정을 확인한 후 다시 시도하세요.', + ipv6ForwardingOnDemand: + 'IPv6 전달이 활성화되지 않았습니다. IPv6 규칙을 추가하거나 복원할 때 확인 후 활성화됩니다.', familyUnsupported: '시스템에서 {0}을 지원하지 않습니다', familyChainIssue: '{0} · {1} 체인 · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index b2f68fe90..757b73f2a 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -4163,6 +4163,11 @@ const message = { dockerInputNotProtected: 'ກົດ INPUT ຂອງໂຮສບໍ່ໄດ້ປ້ອງກັນພອດ Docker ນີ້ໂດຍກົງ. ຄລິກເພື່ອເປີດການປ້ອງກັນພອດຄອນເທນເນີ.', notInitialized: 'ຍັງບໍ່ໄດ້ເລີ່ມຕົ້ນ', + ipv6RARisk: + 'ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກ: ອິນເຕີເຟດ {0} ມີ accept_ra=1 ແລະອາດອາໄສ RA/SLAAC. ກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍ, ຕັ້ງ accept_ra=2 ແບບຖາວອນໃຫ້ອິນເຕີເຟດທີ່ຕ້ອງການ RA ແລ້ວລອງໃໝ່.', + ipv6RACheckFailed: + 'ບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າ IPv6 RA ໄດ້. ການເປີດໃຊ້ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກ. ກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍແລ້ວລອງໃໝ່.', + ipv6ForwardingOnDemand: 'ຍັງບໍ່ໄດ້ເປີດໃຊ້ການສົ່ງຕໍ່ IPv6. ລະບົບຈະກວດສອບແລະເປີດໃຊ້ເມື່ອເພີ່ມຫຼືກູ້ຄືນກົດ IPv6.', familyUnsupported: 'ລະບົບບໍ່ຮອງຮັບ {0}', familyChainIssue: '{0} · chain {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index daaf32f7a..6a133e11e 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4363,6 +4363,12 @@ const message = { dockerInputNotProtected: 'Peraturan INPUT hos tidak melindungi port terbitan Docker ini secara langsung. Klik untuk membuka perlindungan port bekas.', notInitialized: 'Belum dimulakan', + ipv6RARisk: + 'Pemajuan IPv6 disekat: antara muka {0} mempunyai accept_ra=1 dan mungkin bergantung pada RA/SLAAC. Semak konfigurasi rangkaian, tetapkan accept_ra=2 secara kekal pada antara muka yang memerlukan RA, kemudian cuba lagi.', + ipv6RACheckFailed: + 'Konfigurasi RA IPv6 tidak dapat diperiksa. Pengaktifan pemajuan IPv6 disekat. Semak konfigurasi rangkaian sistem dan cuba lagi.', + ipv6ForwardingOnDemand: + 'Pemajuan IPv6 belum diaktifkan. Ia akan diperiksa dan diaktifkan apabila peraturan IPv6 ditambah atau dipulihkan.', familyUnsupported: 'Sistem tidak menyokong {0}', familyChainIssue: '{0} · rantaian {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index ab1573a04..e987277b8 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4380,6 +4380,12 @@ const message = { dockerInputNotProtected: 'As regras INPUT do host não protegem diretamente esta porta publicada pelo Docker. Clique para abrir a proteção de portas de contêineres.', notInitialized: 'Não inicializado', + ipv6RARisk: + 'O encaminhamento IPv6 foi bloqueado: as interfaces {0} têm accept_ra=1 e podem depender de RA/SLAAC. Verifique a configuração de rede, configure accept_ra=2 de forma persistente nas interfaces que precisam de RA e tente novamente.', + ipv6RACheckFailed: + 'Não foi possível verificar a configuração RA do IPv6. A ativação do encaminhamento IPv6 foi bloqueada. Verifique a configuração de rede e tente novamente.', + ipv6ForwardingOnDemand: + 'O encaminhamento IPv6 não está ativado. Ele será verificado e ativado ao adicionar ou restaurar regras IPv6.', familyUnsupported: 'O sistema não oferece suporte a {0}', familyChainIssue: '{0} · cadeia {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index a28525ad9..eafbd287e 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4348,6 +4348,12 @@ const message = { dockerInputNotProtected: 'Правила INPUT хоста не защищают этот опубликованный Docker-порт напрямую. Нажмите, чтобы открыть защиту портов контейнеров.', notInitialized: 'Не инициализировано', + ipv6RARisk: + 'Пересылка IPv6 заблокирована: интерфейсы {0} имеют accept_ra=1 и могут зависеть от RA/SLAAC. Проверьте настройки сети, сохраните accept_ra=2 для интерфейсов, которым нужен RA, и повторите попытку.', + ipv6RACheckFailed: + 'Не удалось проверить настройки RA для IPv6. Включение пересылки IPv6 заблокировано. Проверьте настройки сети и повторите попытку.', + ipv6ForwardingOnDemand: + 'Пересылка IPv6 не включена. Проверка и включение выполняются при добавлении или восстановлении правил IPv6.', familyUnsupported: 'Система не поддерживает {0}', familyChainIssue: '{0} · цепочка {1} · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 982222a41..477404665 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4366,6 +4366,12 @@ const message = { dockerInputNotProtected: 'Ana makine INPUT kuralları bu Docker yayımlanmış portunu doğrudan korumaz. Konteyner portu korumasını açmak için tıklayın.', notInitialized: 'Başlatılmadı', + ipv6RARisk: + 'IPv6 yönlendirme engellendi: {0} arayüzlerinde accept_ra=1 ayarlı ve bu arayüzler RA/SLAAC kullanıyor olabilir. Ağ yapılandırmasını kontrol edin, RA gereken arayüzlerde accept_ra=2 ayarını kalıcı olarak uygulayın ve yeniden deneyin.', + ipv6RACheckFailed: + 'IPv6 RA yapılandırması kontrol edilemedi. IPv6 yönlendirmeyi etkinleştirme engellendi. Sistem ağ yapılandırmasını kontrol edip yeniden deneyin.', + ipv6ForwardingOnDemand: + 'IPv6 yönlendirme etkin değil. IPv6 kuralları eklenirken veya geri yüklenirken kontrol edilip etkinleştirilecektir.', familyUnsupported: 'Sistem {0} desteğini sunmuyor', familyChainIssue: '{0} · {1} zinciri · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 032505083..60d342f55 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -4004,6 +4004,10 @@ const message = { dockerInputUseHostFirewall: '此連接埠需要透過主機防火牆設定存取規則,請前往主機防火牆進行設定。', dockerInputNotProtected: '主機 INPUT 規則無法直接保護此 Docker 發佈連接埠,點擊前往容器連接埠防護。', notInitialized: '未初始化', + ipv6RARisk: + 'IPv6 轉送已阻止:網路介面 {0} 的 accept_ra=1,可能依賴 RA/SLAAC。請確認網路設定,為需要接收 RA 的介面持久化設定 accept_ra=2 後重試。', + ipv6RACheckFailed: '無法檢查 IPv6 RA 設定,已阻止啟用 IPv6 轉送。請檢查系統網路設定後重試。', + ipv6ForwardingOnDemand: 'IPv6 轉送尚未啟用,將在新增或還原 IPv6 規則時檢查並啟用。', familyUnsupported: '系統不支援 {0}', familyChainIssue: '{0} · {1} 鏈 · {2}', dockerGuardUnbindConfirm: diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index c8c3cf82a..db4602e02 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -4052,6 +4052,10 @@ const message = { dockerInputUseHostFirewall: '该端口需要通过主机防火墙设置访问规则,请前往主机防火墙进行配置。', dockerInputNotProtected: '宿主机 INPUT 规则不能直接保护此 Docker 发布端口,点击前往容器端口防护。', notInitialized: '未初始化', + ipv6RARisk: + 'IPv6 转发已阻止:网卡 {0} 的 accept_ra=1,可能依赖 RA/SLAAC。请确认网络配置,为需要接收 RA 的网卡持久化设置 accept_ra=2 后重试。', + ipv6RACheckFailed: '无法检查 IPv6 RA 配置,已阻止启用 IPv6 转发。请检查系统网络配置后重试。', + ipv6ForwardingOnDemand: 'IPv6 转发尚未启用,将在添加或恢复 IPv6 规则时检查并启用。', familyUnsupported: '系统不支持 {0}', familyChainIssue: '{0} · {1} 链 · {2}', dockerGuardUnbindConfirm: '解绑后,所有容器端口将暂时恢复 Docker 默认访问方式,现有防护设置会保留。是否继续?', diff --git a/frontend/src/views/host/firewall/status/index.vue b/frontend/src/views/host/firewall/status/index.vue index ab6968844..f53b193d1 100644 --- a/frontend/src/views/host/firewall/status/index.vue +++ b/frontend/src/views/host/firewall/status/index.vue @@ -285,15 +285,17 @@ const allAvailableFamiliesInitialized = computed( () => availableFamilies.value.length > 0 && availableFamilies.value.every((item) => item.status.initialized), ); const anyFamilyBound = computed(() => availableFamilies.value.some((item) => item.status.bound)); -interface FamilyIssue { +interface FamilyIssue extends Firewall.BackendFamilyStatus { family: 'IPv4' | 'IPv6'; - available: boolean; - initialized: boolean; - bound: boolean; } const managedChainName = computed(() => (props.currentTab === 'forward' ? '1PANEL_FORWARD' : '1PANEL_BASIC')); const familyIssues = computed(() => { - if (!isDirectManaged.value || !anyFamilyBound.value) return []; + if (!isDirectManaged.value) return []; + if (isDirectForward.value) { + if (familyStatuses.value.every((item) => item.status.bound && !item.status.reason)) return []; + return familyStatuses.value.map((item) => ({ family: item.family, ...item.status })); + } + if (!anyFamilyBound.value) return []; return familyStatuses.value .filter((item) => !item.status.available || !item.status.initialized || !item.status.bound) .map((item) => ({ @@ -303,9 +305,21 @@ const familyIssues = computed(() => { bound: item.status.bound, })); }); -const retryableFamilyIssues = computed(() => familyIssues.value.filter((item) => item.available)); +const retryableFamilyIssues = computed(() => + familyIssues.value.filter( + (item) => + item.available && + !item.bound && + (!item.reason || (isDirectForward.value && item.reason === 'ipv6_forwarding_not_enabled')), + ), +); const familyIssueText = (issue: FamilyIssue) => { if (!issue.available) return i18n.global.t('firewall.familyUnsupported', [issue.family]); + if (issue.reason === 'ipv6_ra_required') { + return i18n.global.t('firewall.ipv6RARisk', [issue.raInterfaces?.join(', ') || '-']); + } + if (issue.reason === 'ipv6_ra_check_failed') return i18n.global.t('firewall.ipv6RACheckFailed'); + if (issue.reason === 'ipv6_forwarding_not_enabled') return i18n.global.t('firewall.ipv6ForwardingOnDemand'); const status = i18n.global.t( !issue.initialized ? 'firewall.notInitialized' : issue.bound ? 'commons.status.bound' : 'commons.status.unbind', );