diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index b36ddb871b1d..dde68c46ac20 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -337,3 +337,26 @@ func (b *BaseApi) LoadChainStatus(c *gin.Context) { helper.SuccessWithData(c, iptablesService.LoadChainStatus(req)) } + +// @Tags Firewall +// @Summary Get port security overview +// @Accept json +// @Param request body dto.PortSecuritySearch true "request" +// @Success 200 {object} dto.PortSecurityOverview +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/port/security [post] +func (b *BaseApi) GetPortSecurity(c *gin.Context) { + var req dto.PortSecuritySearch + if err := helper.CheckBind(&req, c); err != nil { + return + } + + data, err := firewallService.GetPortSecurityOverview(req) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.SuccessWithData(c, data) +} diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index d12a9656b877..ed54c59bcac7 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -111,3 +111,38 @@ type IptablesChainStatus struct { IsBind bool `json:"isBind"` DefaultStrategy string `json:"defaultStrategy"` } + +type PortSecuritySearch struct { + Info string `json:"info"` + Status string `json:"status"` +} + +type PortSecurityOverview struct { + Items []PortSecurityItem `json:"items"` + Summary PortSecuritySummary `json:"summary"` + FireActive bool `json:"fireActive"` + DockerExist bool `json:"dockerExist"` +} + +type PortSecuritySummary struct { + Total int `json:"total"` + Protected int `json:"protected"` + Unprotected int `json:"unprotected"` + DockerBypassed int `json:"dockerBypassed"` + LocalOnly int `json:"localOnly"` +} + +type PortSecurityItem struct { + Port uint32 `json:"port"` + Protocol string `json:"protocol"` + BindAddress string `json:"bindAddress"` + ProcessName string `json:"processName"` + PID int32 `json:"pid"` + SourceType string `json:"sourceType"` + ContainerName string `json:"containerName"` + AppName string `json:"appName"` + Status string `json:"status"` + HasRule bool `json:"hasRule"` + RuleStrategy string `json:"ruleStrategy"` + RuleAddress string `json:"ruleAddress"` +} diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index b06c2d0e135a..65f395ef57be 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -34,6 +34,7 @@ type IFirewallService interface { UpdateAddrRule(req dto.AddrRuleUpdate) error UpdateDescription(req dto.UpdateFirewallDescription) error BatchOperateRule(req dto.BatchRuleOperate) error + GetPortSecurityOverview(req dto.PortSecuritySearch) (*dto.PortSecurityOverview, error) } func NewIFirewallService() IFirewallService { diff --git a/agent/app/service/firewall_port_security.go b/agent/app/service/firewall_port_security.go new file mode 100644 index 000000000000..206e652e029c --- /dev/null +++ b/agent/app/service/firewall_port_security.go @@ -0,0 +1,467 @@ +package service + +import ( + "context" + "fmt" + stdnet "net" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" + fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client" + "github.com/docker/docker/api/types/container" + "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" +) + +type portKey struct { + port uint32 + protocol string +} + +type dockerPortInfo struct { + containerName string + hostIP string +} + +type firewallRuleEntry struct { + strategy string + portStr string + protocol string + address string +} + +// GetPortSecurityOverview scans all listening ports and cross-references them with +// Docker container bindings and firewall rules to produce a security status overview. +func (u *FirewallService) GetPortSecurityOverview(req dto.PortSecuritySearch) (*dto.PortSecurityOverview, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var ( + wg sync.WaitGroup + connections []net.ConnectionStat + containers []container.Summary + firewallRules []fireClient.FireInfo + firewallActive bool + dockerExists bool + connectionErr error + dockerErr error + firewallErr error + ) + + wg.Add(3) + go func() { + defer wg.Done() + connections, connectionErr = net.ConnectionsMaxWithContext(ctx, "inet", 32768) + }() + go func() { + defer wg.Done() + cli, err := docker.NewDockerClient() + if err != nil { + dockerErr = err + return + } + defer cli.Close() + dockerExists = true + containers, dockerErr = cli.ContainerList(ctx, container.ListOptions{All: false}) + }() + go func() { + defer wg.Done() + fwClient, err := firewall.NewFirewallClient() + if err != nil { + firewallErr = err + return + } + firewallActive, _ = fwClient.Status() + firewallRules, firewallErr = fwClient.ListPort() + }() + wg.Wait() + + if connectionErr != nil { + return nil, fmt.Errorf("failed to get listening ports: %w", connectionErr) + } + + dockerPortMap := buildDockerPortMap(containers, dockerErr) + ruleIndex := buildFirewallRuleIndex(firewallRules, firewallErr) + appPortMap, systemPort := buildAppPortMaps(ctx) + + seenIndex := make(map[portKey]int) + items := make([]dto.PortSecurityItem, 0) + + for _, conn := range connections { + if conn.Pid == 0 { + continue + } + isListen := conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM + isUDP := conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0 + if !isListen && !isUDP { + continue + } + + proto := "tcp" + if conn.Type == syscall.SOCK_DGRAM { + proto = "udp" + } + + bindAddr := conn.Laddr.IP + if bindAddr == "" { + bindAddr = "0.0.0.0" + } + + key := portKey{port: conn.Laddr.Port, protocol: proto} + if idx, exists := seenIndex[key]; exists { + if isWildcardAddress(bindAddr) && !isWildcardAddress(items[idx].BindAddress) { + items[idx].BindAddress = bindAddr + } + continue + } + seenIndex[key] = len(items) + + item := dto.PortSecurityItem{ + Port: conn.Laddr.Port, + Protocol: proto, + BindAddress: bindAddr, + ProcessName: getProcessNameByPID(conn.Pid), + PID: conn.Pid, + SourceType: "host", + } + + if dInfo, ok := dockerPortMap[key]; ok { + item.SourceType = "docker" + item.ContainerName = dInfo.containerName + if dInfo.hostIP != "" { + item.BindAddress = dInfo.hostIP + } + } + + // Only tag with App Store appName when the port is actually owned by a Docker + // container — prevents mis-labelling a host process that happens to listen on + // a port matching a stopped app's record. + if item.SourceType == "docker" { + if appName, ok := appPortMap[conn.Laddr.Port]; ok { + item.AppName = appName + item.SourceType = "appStore" + } + } + if systemPort != 0 && conn.Laddr.Port == systemPort { + item.AppName = "1panel" + } + + ruleStrategy, hasRule, ruleAddress := matchFirewallRule(ruleIndex, conn.Laddr.Port, proto) + item.HasRule = hasRule + item.RuleStrategy = ruleStrategy + item.RuleAddress = ruleAddress + + items = append(items, item) + } + + // Add synthetic entries for Docker container ports that have no host-side LISTEN + // socket — happens when Docker daemon runs with `userland-proxy: false`, where + // traffic is forwarded purely via iptables DNAT and no docker-proxy process binds + // the host port. Without this, those ports silently disappear from the overview. + for key, dInfo := range dockerPortMap { + if _, exists := seenIndex[key]; exists { + continue + } + bindAddr := dInfo.hostIP + if bindAddr == "" { + bindAddr = "0.0.0.0" + } + item := dto.PortSecurityItem{ + Port: key.port, + Protocol: key.protocol, + BindAddress: bindAddr, + ContainerName: dInfo.containerName, + SourceType: "docker", + } + if appName, ok := appPortMap[key.port]; ok { + item.AppName = appName + item.SourceType = "appStore" + } + ruleStrategy, hasRule, ruleAddress := matchFirewallRule(ruleIndex, key.port, key.protocol) + item.HasRule = hasRule + item.RuleStrategy = ruleStrategy + item.RuleAddress = ruleAddress + seenIndex[key] = len(items) + items = append(items, item) + } + + for i := range items { + items[i].Status = determineStatus(items[i].BindAddress, items[i].SourceType, firewallActive, items[i].HasRule, items[i].RuleStrategy) + } + + // Sort by status priority > protocol > port number + sort.Slice(items, func(i, j int) bool { + pi, pj := statusSortPriority(items[i].Status), statusSortPriority(items[j].Status) + if pi != pj { + return pi < pj + } + if items[i].Protocol != items[j].Protocol { + return items[i].Protocol < items[j].Protocol + } + return items[i].Port < items[j].Port + }) + + // Summary is computed before filtering so it reflects overall status + summary := dto.PortSecuritySummary{Total: len(items)} + for _, item := range items { + switch item.Status { + case "protected", "blocked": + summary.Protected++ + case "noRule": + summary.Unprotected++ + case "dockerBypass": + summary.DockerBypassed++ + case "localOnly": + summary.LocalOnly++ + case "firewallInactive": + summary.Unprotected++ + } + } + + // Apply filters after summary (so summary reflects totals, filters narrow the list) + if req.Info != "" || req.Status != "" { + filtered := make([]dto.PortSecurityItem, 0) + keyword := strings.ToLower(req.Info) + for _, item := range items { + if req.Status != "" && item.Status != req.Status { + continue + } + if keyword != "" { + portStr := strconv.FormatUint(uint64(item.Port), 10) + if !strings.Contains(portStr, keyword) && + !strings.Contains(strings.ToLower(item.ProcessName), keyword) && + !strings.Contains(strings.ToLower(item.ContainerName), keyword) && + !strings.Contains(strings.ToLower(item.AppName), keyword) { + continue + } + } + filtered = append(filtered, item) + } + items = filtered + } + + return &dto.PortSecurityOverview{ + Items: items, + Summary: summary, + FireActive: firewallActive, + DockerExist: dockerExists, + }, nil +} + +// buildDockerPortMap creates a lookup map from host port to container info. +func buildDockerPortMap(containers []container.Summary, err error) map[portKey]dockerPortInfo { + result := make(map[portKey]dockerPortInfo) + if err != nil || containers == nil { + return result + } + for _, c := range containers { + name := "" + if len(c.Names) > 0 { + name = strings.TrimPrefix(c.Names[0], "/") + } + for _, p := range c.Ports { + if p.PublicPort == 0 { + continue + } + key := portKey{port: uint32(p.PublicPort), protocol: p.Type} + result[key] = dockerPortInfo{ + containerName: name, + hostIP: p.IP, + } + } + } + return result +} + +// buildFirewallRuleIndex converts firewall rules into a list for port matching. +func buildFirewallRuleIndex(rules []fireClient.FireInfo, err error) []firewallRuleEntry { + if err != nil || rules == nil { + return nil + } + var entries []firewallRuleEntry + for _, r := range rules { + entries = append(entries, firewallRuleEntry{ + strategy: r.Strategy, + portStr: r.Port, + protocol: r.Protocol, + address: r.Address, + }) + } + return entries +} + +// buildAppPortMaps returns two lookup tables: ports owned by App Store installed +// apps (keyed by host port → app key), and the 1Panel system port. The system +// port is returned separately because it's served by a host process (1panel-core), +// not a Docker container, so it must not go through the docker→appStore promotion. +func buildAppPortMaps(ctx context.Context) (map[uint32]string, uint32) { + appPorts := make(map[uint32]string) + apps, err := appInstallRepo.ListBy(ctx) + if err == nil { + for _, app := range apps { + if app.HttpPort > 0 { + appPorts[uint32(app.HttpPort)] = app.App.Key + } + if app.HttpsPort > 0 { + appPorts[uint32(app.HttpsPort)] = app.App.Key + } + } + } + var systemPort uint32 + if setting, err := settingRepo.Get(settingRepo.WithByKey("ServerPort")); err == nil && setting.Value != "" { + if port, e := strconv.ParseUint(setting.Value, 10, 32); e == nil { + systemPort = uint32(port) + } + } + return appPorts, systemPort +} + +// matchFirewallRule checks if a port has a matching firewall rule, respecting protocol. +// When multiple rules match (e.g. an accept and a drop on the same port), drop/reject +// takes precedence over accept. This is security-conservative — if any deny rule applies, +// the port is reported as denied. It also keeps the result deterministic regardless of +// the underlying backend's listing order (firewalld lists --list-ports accepts before +// rich-rule drops, iptables lists by line number, ufw by rule index). +func matchFirewallRule(rules []firewallRuleEntry, port uint32, proto string) (string, bool, string) { + portStr := strconv.FormatUint(uint64(port), 10) + acceptStrategy := "" + acceptAddress := "" + foundAccept := false + for _, r := range rules { + if r.protocol != "" && r.protocol != "tcp/udp" && r.protocol != proto { + continue + } + if !portMatchesRule(portStr, port, r.portStr) { + continue + } + if r.strategy == "drop" || r.strategy == "reject" { + return r.strategy, true, r.address + } + if !foundAccept { + acceptStrategy = r.strategy + acceptAddress = r.address + foundAccept = true + } + } + return acceptStrategy, foundAccept, acceptAddress +} + +func portMatchesRule(portStr string, portNum uint32, rulePort string) bool { + if rulePort == portStr { + return true + } + if strings.Contains(rulePort, ",") { + for _, p := range strings.Split(rulePort, ",") { + if strings.TrimSpace(p) == portStr { + return true + } + } + return false + } + sep := "" + if strings.Contains(rulePort, "-") { + sep = "-" + } else if strings.Contains(rulePort, ":") { + sep = ":" + } + if sep != "" { + parts := strings.Split(rulePort, sep) + if len(parts) == 2 { + start, err1 := strconv.ParseUint(strings.TrimSpace(parts[0]), 10, 32) + end, err2 := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 32) + if err1 == nil && err2 == nil && uint64(portNum) >= start && uint64(portNum) <= end { + return true + } + } + } + return false +} + +// determineStatus assigns a security status to a port based on its bind address, +// source type, firewall state, and rule coverage. Priority order: +// 1. Loopback or link-local bind (127.0.0.0/8, ::1, fe80::/10) → localOnly (unreachable from outside) +// 2. Docker/appStore source with wildcard bind → dockerBypass (rule applies to INPUT +// chain but Docker traffic goes through FORWARD, so the rule is silently ineffective) +// 3. Firewall inactive → firewallInactive +// 4. Matching rule with drop/reject strategy → blocked +// 5. Matching rule with accept strategy → protected +// 6. Otherwise → noRule +// +// Note: a port bound to a specific non-loopback host IP (e.g. the server's public NIC IP +// or the docker bridge gateway 172.17.0.1) is reachable on that interface and therefore +// goes through the firewall-rule check rather than being labelled localOnly. +func determineStatus(bindAddr, sourceType string, firewallActive, hasRule bool, ruleStrategy string) string { + if isLoopbackOrLinkLocal(bindAddr) { + return "localOnly" + } + if sourceType == "docker" || sourceType == "appStore" { + return "dockerBypass" + } + if !firewallActive { + return "firewallInactive" + } + if hasRule { + if ruleStrategy == "drop" || ruleStrategy == "reject" { + return "blocked" + } + return "protected" + } + return "noRule" +} + +func statusSortPriority(status string) int { + switch status { + case "firewallInactive": + return 0 + case "dockerBypass": + return 1 + case "noRule": + return 2 + case "protected": + return 3 + case "blocked": + return 4 + case "localOnly": + return 5 + default: + return 6 + } +} + +// isWildcardAddress returns true if the address binds to all interfaces. +func isWildcardAddress(addr string) bool { + return addr == "0.0.0.0" || addr == "::" || addr == "" +} + +// isLoopbackOrLinkLocal returns true if the address is provably unreachable from +// outside the host: loopback (127.0.0.0/8, ::1) or link-local (169.254.0.0/16, +// fe80::/10). Wildcard addresses (0.0.0.0, ::) are NOT loopback — they bind every +// interface including public ones. A specific non-loopback IP (a public NIC IP, a +// docker bridge gateway like 172.17.0.1, etc.) is reachable on that interface and +// must go through firewall-rule evaluation, not be labelled localOnly outright. +func isLoopbackOrLinkLocal(addr string) bool { + if addr == "" { + return false + } + ip := stdnet.ParseIP(addr) + if ip == nil { + return false + } + return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() +} + +func getProcessNameByPID(pid int32) string { + proc, err := process.NewProcess(pid) + if err != nil { + return "" + } + name, _ := proc.Name() + return name +} diff --git a/agent/app/service/firewall_port_security_test.go b/agent/app/service/firewall_port_security_test.go new file mode 100644 index 000000000000..114caa517d0f --- /dev/null +++ b/agent/app/service/firewall_port_security_test.go @@ -0,0 +1,174 @@ +package service + +import ( + "testing" +) + +func TestPortMatchesRule(t *testing.T) { + tests := []struct { + name string + portStr string + portNum uint32 + rule string + want bool + }{ + {"exact match", "80", 80, "80", true}, + {"no match", "80", 80, "443", false}, + {"dash range match", "8080", 8080, "8000-9000", true}, + {"dash range lower bound", "8000", 8000, "8000-9000", true}, + {"dash range upper bound", "9000", 9000, "8000-9000", true}, + {"dash range miss below", "7999", 7999, "8000-9000", false}, + {"dash range miss above", "9001", 9001, "8000-9000", false}, + {"colon range match", "8500", 8500, "8000:9000", true}, + {"colon range miss", "7999", 7999, "8000:9000", false}, + {"comma list first", "80", 80, "80,443,8080", true}, + {"comma list middle", "443", 443, "80,443,8080", true}, + {"comma list last", "8080", 8080, "80,443,8080", true}, + {"comma list miss", "22", 22, "80,443,8080", false}, + {"comma with spaces", "443", 443, "80, 443, 8080", true}, + {"empty rule", "80", 80, "", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := portMatchesRule(tt.portStr, tt.portNum, tt.rule) + if got != tt.want { + t.Errorf("portMatchesRule(%q, %d, %q) = %v, want %v", tt.portStr, tt.portNum, tt.rule, got, tt.want) + } + }) + } +} + +func TestMatchFirewallRule(t *testing.T) { + rules := []firewallRuleEntry{ + {strategy: "accept", portStr: "22", protocol: "tcp", address: ""}, + {strategy: "accept", portStr: "3306", protocol: "tcp", address: "10.0.0.0/8"}, + {strategy: "drop", portStr: "3306", protocol: "tcp", address: "1.2.3.4"}, + {strategy: "accept", portStr: "8000-9000", protocol: "tcp", address: ""}, + {strategy: "accept", portStr: "53", protocol: "tcp", address: ""}, + {strategy: "accept", portStr: "123", protocol: "tcp/udp", address: ""}, + {strategy: "accept", portStr: "80,443", protocol: "tcp", address: ""}, + } + + tests := []struct { + name string + port uint32 + proto string + wantStrategy string + wantFound bool + wantAddress string + }{ + {"exact match tcp/22", 22, "tcp", "accept", true, ""}, + {"no match udp/22", 22, "udp", "", false, ""}, + {"drop wins over accept for 3306", 3306, "tcp", "drop", true, "1.2.3.4"}, + {"range match 8500", 8500, "tcp", "accept", true, ""}, + {"protocol mismatch udp/8500", 8500, "udp", "", false, ""}, + {"source-scoped accept 3306 returns address", 3306, "tcp", "drop", true, "1.2.3.4"}, + {"no match port 9999", 9999, "tcp", "", false, ""}, + {"comma list match 443", 443, "tcp", "accept", true, ""}, + {"comma list match 80", 80, "tcp", "accept", true, ""}, + {"tcp/udp protocol matches tcp", 123, "tcp", "accept", true, ""}, + {"tcp/udp protocol matches udp", 123, "udp", "accept", true, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strategy, found, address := matchFirewallRule(rules, tt.port, tt.proto) + if strategy != tt.wantStrategy || found != tt.wantFound || address != tt.wantAddress { + t.Errorf("matchFirewallRule(port=%d, proto=%s) = (%q, %v, %q), want (%q, %v, %q)", + tt.port, tt.proto, strategy, found, address, tt.wantStrategy, tt.wantFound, tt.wantAddress) + } + }) + } +} + +func TestDetermineStatus(t *testing.T) { + tests := []struct { + name string + bindAddr string + sourceType string + firewallActive bool + hasRule bool + ruleStrategy string + want string + }{ + {"loopback ipv4", "127.0.0.1", "host", true, false, "", "localOnly"}, + {"loopback ipv4 alt", "127.0.0.53", "host", true, false, "", "localOnly"}, + {"loopback ipv6", "::1", "host", true, false, "", "localOnly"}, + {"link-local ipv6", "fe80::1", "host", true, false, "", "localOnly"}, + {"wildcard ipv4", "0.0.0.0", "host", true, false, "", "noRule"}, + {"wildcard ipv6", "::", "host", true, false, "", "noRule"}, + {"empty addr", "", "host", true, false, "", "noRule"}, + {"public ip host", "114.212.85.247", "host", true, false, "", "noRule"}, + {"bridge gateway host", "172.17.0.1", "host", true, false, "", "noRule"}, + {"docker wildcard", "0.0.0.0", "docker", true, false, "", "dockerBypass"}, + {"docker loopback", "127.0.0.1", "docker", true, false, "", "localOnly"}, + {"docker specific ip", "172.17.0.1", "docker", true, false, "", "dockerBypass"}, + {"appStore wildcard", "0.0.0.0", "appStore", true, false, "", "dockerBypass"}, + {"firewall inactive host", "0.0.0.0", "host", false, false, "", "firewallInactive"}, + {"firewall inactive docker", "0.0.0.0", "docker", false, false, "", "dockerBypass"}, + {"accept rule", "0.0.0.0", "host", true, true, "accept", "protected"}, + {"drop rule", "0.0.0.0", "host", true, true, "drop", "blocked"}, + {"reject rule", "0.0.0.0", "host", true, true, "reject", "blocked"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := determineStatus(tt.bindAddr, tt.sourceType, tt.firewallActive, tt.hasRule, tt.ruleStrategy) + if got != tt.want { + t.Errorf("determineStatus(%q, %q, active=%v, rule=%v, %q) = %q, want %q", + tt.bindAddr, tt.sourceType, tt.firewallActive, tt.hasRule, tt.ruleStrategy, got, tt.want) + } + }) + } +} + +func TestIsLoopbackOrLinkLocal(t *testing.T) { + tests := []struct { + addr string + want bool + }{ + {"127.0.0.1", true}, + {"127.0.0.53", true}, + {"127.0.0.54", true}, + {"::1", true}, + {"fe80::1", true}, + {"fe80::be24:11ff:fe31:1da9", true}, + {"0.0.0.0", false}, + {"::", false}, + {"", false}, + {"114.212.85.247", false}, + {"172.17.0.1", false}, + {"10.0.0.1", false}, + {"192.168.1.1", false}, + {"2001:db8::1", false}, + } + for _, tt := range tests { + t.Run(tt.addr, func(t *testing.T) { + got := isLoopbackOrLinkLocal(tt.addr) + if got != tt.want { + t.Errorf("isLoopbackOrLinkLocal(%q) = %v, want %v", tt.addr, got, tt.want) + } + }) + } +} + +func TestIsWildcardAddress(t *testing.T) { + tests := []struct { + addr string + want bool + }{ + {"0.0.0.0", true}, + {"::", true}, + {"", true}, + {"127.0.0.1", false}, + {"172.17.0.1", false}, + } + for _, tt := range tests { + t.Run(tt.addr, func(t *testing.T) { + got := isWildcardAddress(tt.addr) + if got != tt.want { + t.Errorf("isWildcardAddress(%q) = %v, want %v", tt.addr, got, tt.want) + } + }) + } +} diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index bb4446842870..b2fd38e197cf 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -37,6 +37,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/filter/rule/batch", baseApi.BatchOperateFilterRule) hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain) hostRouter.POST("/firewall/filter/chain/status", baseApi.LoadChainStatus) + hostRouter.POST("/firewall/port/security", baseApi.GetPortSecurity) hostRouter.POST("/monitor/search", baseApi.LoadMonitor) hostRouter.POST("/monitor/gpu/search", baseApi.LoadGPUMonitor) diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index f6ab0343cd6e..70c790897f24 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -140,6 +140,38 @@ export namespace Host { rules: Array; } + export interface PortSecuritySearch { + info: string; + status: string; + } + export interface PortSecurityItem { + port: number; + protocol: string; + bindAddress: string; + processName: string; + pid: number; + sourceType: string; + containerName: string; + appName: string; + status: string; + hasRule: boolean; + ruleStrategy: string; + ruleAddress: string; + } + export interface PortSecuritySummary { + total: number; + protected: number; + unprotected: number; + dockerBypassed: number; + localOnly: number; + } + export interface PortSecurityOverview { + items: Array; + summary: PortSecuritySummary; + fireActive: boolean; + dockerExist: boolean; + } + export interface MonitorSetting { defaultNetwork: string; defaultIO: string; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 3357db136be9..8aca89176696 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -43,6 +43,9 @@ export const updateFirewallDescription = (params: Host.UpdateDescription) => { export const batchOperateRule = (params: Host.BatchRule) => { return http.post(`/hosts/firewall/batch`, params, TimeoutEnum.T_60S); }; +export const getPortSecurity = (params: Host.PortSecuritySearch) => { + return http.post(`/hosts/firewall/port/security`, params, TimeoutEnum.T_40S); +}; // Iptables Filter export const searchFilterRules = (params: Host.IptablesFilterRuleSearch) => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index df08fc3e6a75..ecd685b2ffb5 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -3361,6 +3361,32 @@ const message = { cookieBlockList: 'Cookie blocklist', dockerHelper: 'The current firewall cannot disable container port mapping. Installed applications can go to the [Installed] page to edit application parameters and configure port release rules.', + portSecurity: 'Port Security Scan', + portSecurityAllSafe: 'No issues found in this scan', + portSecurityScanLimit: + 'Scope: host-side listeners and Docker default bridge networking. macvlan, ipvlan, overlay, Swarm ingress and external NAT/load-balancer paths are not evaluated.', + portSecurityScanFailed: 'Port security scan failed. Please refresh and try again.', + dockerBypass: 'Docker Bypass', + dockerBypassTip: + 'Docker publishes this port directly, so firewall rules on the host do not apply. To limit access, bind the container to 127.0.0.1 or a specific interface.', + dockerBypassSuggest: + 'Change the port mapping to 127.0.0.1:{0}:{0} (instead of binding to all interfaces) and restart the container.', + dockerBypassHasRule: + 'A firewall rule exists for this port ({0} {1}/{2}), but it does not apply to Docker-published ports.', + noRule: 'No matching rule', + localOnly: 'Local only', + protected: 'Allowed by rule', + blocked: 'Blocked by rule', + firewallInactive: 'Firewall not running', + goToApp: 'Go to App Settings', + dockerBypassDetail: 'How to fix', + createRuleQuick: 'Create Rule', + portSecurityRiskLabel: 'need fixing', + portSecurityPendingLabel: 'to review', + portSecuritySafeLabel: 'OK', + portSecuritySource: 'Process / Container', + portSecurityBindAddr: 'Bind Address', + dockerLabel: 'Container', iptablesHelper: 'Detected that the system is using {0} firewall. To switch to iptables, please uninstall it manually first!', quickJump: 'Quick access', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index b3728ff5c03c..60028f7e3cb0 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -3394,6 +3394,29 @@ const message = { cookieBlockList: 'Lista negra de cookies', dockerHelper: 'El firewall actual no puede deshabilitar el mapeo de puertos de contenedores. Las aplicaciones instaladas pueden ir a la página [Instaladas] para editar los parámetros de la aplicación y configurar reglas de liberación de puertos.', + portSecurity: 'Análisis de Seguridad de Puertos', + portSecurityAllSafe: 'No se encontraron problemas en este análisis', + portSecurityScanLimit: + 'Ámbito: escuchadores del host y la red bridge por defecto de Docker. macvlan, ipvlan, overlay, Swarm ingress y rutas externas de NAT/balanceador no se evalúan.', + portSecurityScanFailed: 'El análisis de seguridad de puertos ha fallado. Actualice y vuelva a intentarlo.', + dockerBypass: 'Bypass de Docker', + dockerBypassTip: 'Este puerto es publicado directamente por Docker, por lo que las reglas del cortafuegos del host no se aplican. Para restringir el acceso, vincule el contenedor a 127.0.0.1 o a una interfaz específica.', + dockerBypassSuggest: 'Cambie el mapeo de puertos a 127.0.0.1:{0}:{0} (en lugar de vincular a todas las interfaces) y reinicie el contenedor.', + dockerBypassHasRule: 'Existe una regla de cortafuegos para este puerto ({0} {1}/{2}), pero no se aplica a los puertos publicados por Docker.', + noRule: 'Sin regla coincidente', + localOnly: 'Solo local', + protected: 'Permitido por regla', + blocked: 'Bloqueado por regla', + firewallInactive: 'Cortafuegos no está en ejecución', + goToApp: 'Ir a Configuración de la App', + dockerBypassDetail: 'Cómo solucionarlo', + createRuleQuick: 'Crear Regla', + portSecurityRiskLabel: 'a corregir', + portSecurityPendingLabel: 'para revisar', + portSecuritySafeLabel: 'OK', + portSecuritySource: 'Proceso / Contenedor', + portSecurityBindAddr: 'Dirección de Enlace', + dockerLabel: 'Contenedor', iptablesHelper: 'Se detectó que el sistema está usando el firewall {0}. Para cambiar a iptables, ¡desinstálelo manualmente primero!', quickJump: 'Acceso rápido', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index ac49b4333f9e..7054c9779219 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -3384,6 +3384,29 @@ const message = { cookieBlockList: 'クッキーブロックリスト', dockerHelper: '現在のファイアウォールではコンテナのポートマッピングを無効にできません。インストール済みアプリケーションは【インストール済み】ページでアプリケーションパラメータを編集し、ポート開放ルールを設定できます。', + portSecurity: 'ポートセキュリティスキャン', + portSecurityAllSafe: '今回のスキャンでは問題は検出されませんでした', + portSecurityScanLimit: + '検出範囲:ホスト側リスナーと Docker デフォルトの bridge ネットワーク。macvlan / ipvlan / overlay / Swarm ingress、および外部 NAT・ロードバランサ経路は評価されません。', + portSecurityScanFailed: 'ポートセキュリティスキャンに失敗しました。更新して再試行してください。', + dockerBypass: 'Docker バイパス', + dockerBypassTip: 'このポートは Docker によって直接公開されているため、ホスト上のファイアウォールルールは適用されません。アクセスを制限するには、コンテナを 127.0.0.1 または特定のインターフェースにバインドしてください。', + dockerBypassSuggest: 'ポートマッピングを 127.0.0.1:{0}:{0} に変更し(すべてのインターフェースにバインドしない)、コンテナを再起動してください。', + dockerBypassHasRule: 'このポートにはファイアウォールルール({0} {1}/{2})が存在しますが、Docker が公開するポートには適用されません。', + noRule: '一致するルールなし', + localOnly: 'ローカルのみ', + protected: 'ルールで許可', + blocked: 'ルールで遮断', + firewallInactive: 'ファイアウォール無効', + goToApp: 'アプリ設定へ移動', + dockerBypassDetail: '修正方法', + createRuleQuick: 'ルールを作成', + portSecurityRiskLabel: '件要修正', + portSecurityPendingLabel: '件要確認', + portSecuritySafeLabel: '件正常', + portSecuritySource: 'プロセス / コンテナ', + portSecurityBindAddr: 'バインドアドレス', + dockerLabel: 'コンテナ', iptablesHelper: 'システムが {0} ファイアウォールを使用していることを検出しました。iptables に切り替えるには、まず手動でアンインストールしてください!', quickJump: 'クイックアクセス', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index b221b0a9c41e..66565299f7cb 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -3306,6 +3306,29 @@ const message = { cookieBlockList: '쿠키 차단 목록', dockerHelper: '현재 방화벽은 컨테이너 포트 매핑을 비활성화할 수 없습니다. 설치된 애플리케이션은 [설치됨] 페이지에서 애플리케이션 매개변수를 편집하고 포트 해제 규칙을 구성할 수 있습니다.', + portSecurity: '포트 보안 스캔', + portSecurityAllSafe: '이번 스캔에서 문제가 발견되지 않았습니다', + portSecurityScanLimit: + '검사 범위: 호스트 리스너와 Docker 기본 bridge 네트워크. macvlan / ipvlan / overlay / Swarm ingress 및 외부 NAT・로드밸런서 경로는 평가되지 않습니다.', + portSecurityScanFailed: '포트 보안 스캔에 실패했습니다. 새로고침 후 다시 시도하세요.', + dockerBypass: 'Docker 우회', + dockerBypassTip: '이 포트는 Docker가 직접 게시한 것으로, 호스트의 방화벽 규칙이 적용되지 않습니다. 접근을 제한하려면 컨테이너를 127.0.0.1 또는 특정 인터페이스에 바인드하세요.', + dockerBypassSuggest: '포트 매핑을 127.0.0.1:{0}:{0}으로 변경(모든 인터페이스에 바인딩하지 않음)하고 컨테이너를 재시작하세요.', + dockerBypassHasRule: '이 포트에 방화벽 규칙({0} {1}/{2})이 있지만 Docker가 게시한 포트에는 적용되지 않습니다.', + noRule: '일치하는 규칙 없음', + localOnly: '로컬 전용', + protected: '규칙 허용', + blocked: '규칙 차단', + firewallInactive: '방화벽 비활성', + goToApp: '앱 설정으로 이동', + dockerBypassDetail: '해결 방법', + createRuleQuick: '규칙 생성', + portSecurityRiskLabel: '건 수정 필요', + portSecurityPendingLabel: '건 검토 필요', + portSecuritySafeLabel: '건 정상', + portSecuritySource: '프로세스 / 컨테이너', + portSecurityBindAddr: '바인드 주소', + dockerLabel: '컨테이너', iptablesHelper: '시스템이 {0} 방화벽을 사용 중인 것으로 감지되었습니다. iptables로 전환하려면 먼저 수동으로 제거하세요!', used: '사용됨', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 59e8eed9d2e5..9389854db3aa 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -3432,6 +3432,29 @@ const message = { cookieBlockList: 'Senarai blok Cookie', dockerHelper: 'Firewall semasa tidak boleh melumpuhkan pemetaan port bekas. Aplikasi yang dipasang boleh pergi ke halaman [Dipasang] untuk mengedit parameter aplikasi dan mengkonfigurasi peraturan pelepasan port.', + portSecurity: 'Imbasan Keselamatan Port', + portSecurityAllSafe: 'Tiada masalah ditemui dalam imbasan ini', + portSecurityScanLimit: + 'Skop: pendengar pada hos dan rangkaian bridge lalai Docker. macvlan, ipvlan, overlay, Swarm ingress dan laluan NAT/penyeimbang beban luaran tidak dinilai.', + portSecurityScanFailed: 'Imbasan keselamatan port gagal. Sila muat semula dan cuba lagi.', + dockerBypass: 'Pintasan Docker', + dockerBypassTip: 'Port ini diterbitkan secara langsung oleh Docker, jadi peraturan firewall pada hos tidak terpakai. Untuk menyekat akses, ikat kontena kepada 127.0.0.1 atau antara muka tertentu.', + dockerBypassSuggest: 'Ubah pemetaan port kepada 127.0.0.1:{0}:{0} (bukan mengikat kepada semua antara muka) dan mulakan semula kontena.', + dockerBypassHasRule: 'Terdapat peraturan firewall untuk port ini ({0} {1}/{2}), tetapi ia tidak terpakai pada port yang diterbitkan oleh Docker.', + noRule: 'Tiada peraturan sepadan', + localOnly: 'Setempat sahaja', + protected: 'Dibenarkan oleh peraturan', + blocked: 'Disekat oleh peraturan', + firewallInactive: 'Firewall tidak berjalan', + goToApp: 'Pergi ke Tetapan Aplikasi', + dockerBypassDetail: 'Cara membaiki', + createRuleQuick: 'Cipta Peraturan', + portSecurityRiskLabel: 'perlu dibaiki', + portSecurityPendingLabel: 'untuk disemak', + portSecuritySafeLabel: 'OK', + portSecuritySource: 'Proses / Kontena', + portSecurityBindAddr: 'Alamat Bind', + dockerLabel: 'Kontena', iptablesHelper: 'Dikesan sistem menggunakan firewall {0}. Untuk beralih ke iptables, sila nyahpasang secara manual dahulu!', quickJump: 'Akses pantas', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1c8d80d86dcd..d25a8ffef28e 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -3569,6 +3569,29 @@ const message = { cookieBlockList: 'Lista de cookies bloqueados', dockerHelper: 'O firewall atual não pode desativar o mapeamento de porta de contêiner. Aplicativos instalados podem ir para a página [Instalados] para editar parâmetros do aplicativo e configurar regras de liberação de porta.', + portSecurity: 'Verificação de Segurança de Portas', + portSecurityAllSafe: 'Nenhum problema encontrado nesta verificação', + portSecurityScanLimit: + 'Escopo: escutadores no host e a rede bridge padrão do Docker. macvlan, ipvlan, overlay, Swarm ingress e caminhos externos de NAT/balanceador de carga não são avaliados.', + portSecurityScanFailed: 'Falha na verificação de segurança de portas. Atualize e tente novamente.', + dockerBypass: 'Bypass do Docker', + dockerBypassTip: 'Esta porta é publicada diretamente pelo Docker, portanto as regras de firewall no host não se aplicam. Para restringir o acesso, vincule o contêiner a 127.0.0.1 ou a uma interface específica.', + dockerBypassSuggest: 'Altere o mapeamento de porta para 127.0.0.1:{0}:{0} (em vez de vincular a todas as interfaces) e reinicie o contêiner.', + dockerBypassHasRule: 'Existe uma regra de firewall para esta porta ({0} {1}/{2}), mas ela não se aplica a portas publicadas pelo Docker.', + noRule: 'Sem regra correspondente', + localOnly: 'Apenas local', + protected: 'Permitido por regra', + blocked: 'Bloqueado por regra', + firewallInactive: 'Firewall não está em execução', + goToApp: 'Ir para Configurações do App', + dockerBypassDetail: 'Como corrigir', + createRuleQuick: 'Criar Regra', + portSecurityRiskLabel: 'a corrigir', + portSecurityPendingLabel: 'para revisar', + portSecuritySafeLabel: 'OK', + portSecuritySource: 'Processo / Contêiner', + portSecurityBindAddr: 'Endereço de Bind', + dockerLabel: 'Contêiner', iptablesHelper: 'Detectado que o sistema está usando o firewall {0}. Para mudar para iptables, desinstale-o manualmente primeiro!', quickJump: 'Acesso rápido', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 07010dd34921..d98692d507c7 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -3426,6 +3426,29 @@ const message = { cookieBlockList: 'Черный список Cookie', dockerHelper: 'Текущий брандмауэр не может отключить сопоставление портов контейнера. Установленные приложения могут перейти на страницу [Установленные], чтобы редактировать параметры приложения и настраивать правила открытия портов.', + portSecurity: 'Сканирование безопасности портов', + portSecurityAllSafe: 'В этом сканировании проблем не обнаружено', + portSecurityScanLimit: + 'Область сканирования: хост-слушатели и стандартная сеть Docker bridge. macvlan, ipvlan, overlay, Swarm ingress и внешние пути NAT/балансировщика не оцениваются.', + portSecurityScanFailed: 'Сканирование безопасности портов не удалось. Обновите страницу и попробуйте снова.', + dockerBypass: 'Обход Docker', + dockerBypassTip: 'Этот порт публикуется Docker напрямую, поэтому правила брандмауэра на хосте к нему не применяются. Чтобы ограничить доступ, привяжите контейнер к 127.0.0.1 или конкретному интерфейсу.', + dockerBypassSuggest: 'Измените маппинг порта на 127.0.0.1:{0}:{0} (вместо привязки ко всем интерфейсам) и перезапустите контейнер.', + dockerBypassHasRule: 'Для этого порта существует правило брандмауэра ({0} {1}/{2}), но оно не применяется к портам, опубликованным Docker.', + noRule: 'Нет соответствующего правила', + localOnly: 'Только локально', + protected: 'Разрешено правилом', + blocked: 'Заблокировано правилом', + firewallInactive: 'Брандмауэр не работает', + goToApp: 'Перейти к настройкам приложения', + dockerBypassDetail: 'Как исправить', + createRuleQuick: 'Создать правило', + portSecurityRiskLabel: 'требует исправления', + portSecurityPendingLabel: 'на проверку', + portSecuritySafeLabel: 'в норме', + portSecuritySource: 'Процесс / Контейнер', + portSecurityBindAddr: 'Адрес привязки', + dockerLabel: 'Контейнер', iptablesHelper: 'Обнаружено, что система использует брандмауэр {0}. Чтобы переключиться на iptables, сначала удалите его вручную!', quickJump: 'Быстрый доступ', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 2d3484cd6814..e52d8197c16d 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -3427,6 +3427,29 @@ const message = { cookieBlockList: 'Çerez engelleme listesi', dockerHelper: 'Mevcut güvenlik duvarı konteyner port eşlemesini devre dışı bırakamaz. Yüklü uygulamalar, uygulama parametrelerini düzenlemek ve port serbest bırakma kurallarını yapılandırmak için [Yüklü] sayfasına gidebilir.', + portSecurity: 'Port Güvenlik Taraması', + portSecurityAllSafe: 'Bu taramada sorun bulunamadı', + portSecurityScanLimit: + 'Tarama kapsamı: ana makine dinleyicileri ve Docker varsayılan bridge ağı. macvlan / ipvlan / overlay / Swarm ingress ve dış NAT/yük dengeleyici yolları değerlendirilmez.', + portSecurityScanFailed: 'Port güvenlik taraması başarısız oldu. Lütfen yenileyip tekrar deneyin.', + dockerBypass: 'Docker Atlatma', + dockerBypassTip: 'Bu port Docker tarafından doğrudan yayımlanır, bu nedenle ana makinedeki güvenlik duvarı kuralları geçerli olmaz. Erişimi kısıtlamak için konteyneri 127.0.0.1 veya belirli bir arayüze bağlayın.', + dockerBypassSuggest: 'Port eşlemesini 127.0.0.1:{0}:{0} olarak değiştirin (tüm arayüzlere bağlamak yerine) ve konteyneri yeniden başlatın.', + dockerBypassHasRule: 'Bu port için bir güvenlik duvarı kuralı ({0} {1}/{2}) mevcut, ancak Docker tarafından yayımlanan portlara uygulanmaz.', + noRule: 'Eşleşen kural yok', + localOnly: 'Yalnızca yerel', + protected: 'Kural izin veriyor', + blocked: 'Kural engelliyor', + firewallInactive: 'Güvenlik duvarı çalışmıyor', + goToApp: 'Uygulama Ayarlarına Git', + dockerBypassDetail: 'Nasıl düzeltilir', + createRuleQuick: 'Kural Oluştur', + portSecurityRiskLabel: 'düzeltilmesi gerekir', + portSecurityPendingLabel: 'gözden geçirilecek', + portSecuritySafeLabel: 'normal', + portSecuritySource: 'İşlem / Konteyner', + portSecurityBindAddr: 'Bağlama Adresi', + dockerLabel: 'Konteyner', iptablesHelper: "Sistemin {0} güvenlik duvarını kullandığı tespit edildi. iptables'a geçmek için lütfen önce manuel olarak kaldırın!", quickJump: 'Hızlı erişim', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index bf21b1333222..7dc0d4f34653 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3112,6 +3112,28 @@ const message = { postCheck: 'POST 參數校驗', cookieBlockList: 'Cookie 黑名單', dockerHelper: '目前防火牆無法停用容器埠映射,已安裝應用可前往【已安裝】頁面編輯應用參數,設定埠放行規則。', + portSecurity: '連接埠安全檢測', + portSecurityAllSafe: '本次掃描未發現問題', + portSecurityScanLimit: '檢測範圍:宿主機監聽連接埠與 Docker 預設 bridge 網路。macvlan / ipvlan / overlay / Swarm ingress 以及外部 NAT、負載均衡路徑不在評估範圍。', + portSecurityScanFailed: '連接埠安全檢測失敗,請刷新重試。', + dockerBypass: 'Docker 繞過', + dockerBypassTip: '此連接埠由 Docker 直接發布,宿主機上的防火牆規則對它無效。如需限制存取,請將容器綁定到 127.0.0.1 或指定網卡。', + dockerBypassSuggest: '將連接埠映射改為 127.0.0.1:{0}:{0}(而非綁定所有網卡),然後重啟容器。', + dockerBypassHasRule: '此連接埠存在防火牆規則({0} {1}/{2}),但該規則對 Docker 發布的連接埠不生效。', + noRule: '無匹配規則', + localOnly: '僅本機', + protected: '規則放行', + blocked: '規則攔截', + firewallInactive: '防火牆未啟用', + goToApp: '前往容器設定', + dockerBypassDetail: '如何修復', + createRuleQuick: '建立規則', + portSecurityRiskLabel: '個需修復', + portSecurityPendingLabel: '個待複核', + portSecuritySafeLabel: '個正常', + portSecuritySource: '處理程序 / 容器', + portSecurityBindAddr: '綁定位址', + dockerLabel: '容器', iptablesHelper: '偵測到系統正在使用 {0} 防火牆,如需切換至 iptables,請先手動解除安裝', quickJump: '快速跳轉', used: '已使用', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8dfb25a94b9b..ed69b0768ac6 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3112,6 +3112,28 @@ const message = { cookieBlockList: 'Cookie 黑名单', dockerHelper: '当前防火墙无法禁用容器端口映射,已安装应用可前往【已安装】页面编辑应用参数,配置端口放行规则。', + portSecurity: '端口安全检测', + portSecurityAllSafe: '本次扫描未发现问题', + portSecurityScanLimit: '检测范围:宿主机监听端口与 Docker 默认 bridge 网络。macvlan / ipvlan / overlay / Swarm ingress 以及外部 NAT、负载均衡路径不在评估范围。', + portSecurityScanFailed: '端口安全检测失败,请刷新重试。', + dockerBypass: 'Docker 绕过', + dockerBypassTip: '该端口由 Docker 直接发布,宿主机上的防火墙规则对它无效。如需限制访问,请将容器绑定到 127.0.0.1 或指定网卡。', + dockerBypassSuggest: '将端口映射改为 127.0.0.1:{0}:{0}(而非绑定所有网卡),然后重启容器。', + dockerBypassHasRule: '该端口存在防火墙规则({0} {1}/{2}),但该规则对 Docker 发布的端口不生效。', + noRule: '无匹配规则', + localOnly: '仅本机', + protected: '规则放行', + blocked: '规则拦截', + firewallInactive: '防火墙未启用', + goToApp: '前往容器设置', + dockerBypassDetail: '如何修复', + createRuleQuick: '创建规则', + portSecurityRiskLabel: '个需修复', + portSecurityPendingLabel: '个待复核', + portSecuritySafeLabel: '个正常', + portSecuritySource: '进程 / 容器', + portSecurityBindAddr: '绑定地址', + dockerLabel: '容器', iptablesHelper: '检测到系统正在使用 {0} 防火墙,如需切换至 iptables,请先手动卸载!', quickJump: '快速跳转', used: '已使用', diff --git a/frontend/src/views/host/firewall/port/awareness/index.vue b/frontend/src/views/host/firewall/port/awareness/index.vue new file mode 100644 index 000000000000..d078dce3ecca --- /dev/null +++ b/frontend/src/views/host/firewall/port/awareness/index.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/frontend/src/views/host/firewall/port/index.vue b/frontend/src/views/host/firewall/port/index.vue index 5d5c98f95307..c540a568f54a 100644 --- a/frontend/src/views/host/firewall/port/index.vue +++ b/frontend/src/views/host/firewall/port/index.vue @@ -14,13 +14,19 @@ current-tab="base" />
- + {{ $t('firewall.firewallNotStart') }} {{ $t('firewall.basicStatus', ['1PANEL_BASIC']) }} + +