From e4edbdcef023f999d48e5da0854576b05933b9e0 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Mon, 19 Oct 2026 15:53:59 +0800 Subject: [PATCH] =?UTF-8?q?tcp=E6=96=B0=E5=A2=9E=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/frp/service.go | 12 ++++++++-- internal/frp/validate.go | 45 +++++++++++++++++++++++++++++++++++ internal/frp/validate_test.go | 45 ++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/internal/frp/service.go b/internal/frp/service.go index 1eccbfd..e516219 100644 --- a/internal/frp/service.go +++ b/internal/frp/service.go @@ -692,7 +692,11 @@ func (s *Service) CreateProxy(ctx context.Context, in ProxyInput) (Proxy, error) if err := validateProxyInput(in); err != nil { return Proxy{}, err } - if _, err := s.store.GetServer(ctx, in.ServerID); err != nil { + server, err := s.store.GetServer(ctx, in.ServerID) + if err != nil { + return Proxy{}, err + } + if err := validateRemotePortAllowed(server, in); err != nil { return Proxy{}, err } proxy, err := s.store.CreateProxy(ctx, in) @@ -711,7 +715,11 @@ func (s *Service) UpdateProxy(ctx context.Context, id int64, in ProxyInput) (Pro if err := validateProxyInput(in); err != nil { return Proxy{}, err } - if _, err := s.store.GetServer(ctx, in.ServerID); err != nil { + server, err := s.store.GetServer(ctx, in.ServerID) + if err != nil { + return Proxy{}, err + } + if err := validateRemotePortAllowed(server, in); err != nil { return Proxy{}, err } proxy, err := s.store.UpdateProxy(ctx, id, in) diff --git a/internal/frp/validate.go b/internal/frp/validate.go index 82b5b03..17c8457 100644 --- a/internal/frp/validate.go +++ b/internal/frp/validate.go @@ -6,6 +6,7 @@ import ( "net" "net/url" "regexp" + "strconv" "strings" ) @@ -394,6 +395,50 @@ func validHost(host string) bool { return validDomain(host) } +// validateRemotePortAllowed 检查 tcp/udp 规则的远程端口是否落在服务端的 allowPorts 白名单里。 +// +// frps 只认 allowPorts:端口不在白名单时规则会被服务端拒绝,frpc 会一直重试并刷 +// `[规则名] start error: port not allowed`。与其等用户去翻 frpc 日志,不如保存时就说清楚。 +func validateRemotePortAllowed(server Server, in ProxyInput) error { + allow := strings.TrimSpace(server.Options.AllowPorts) + if allow == "" || in.RemotePort == nil { + return nil + } + if in.Type != "tcp" && in.Type != "udp" { + return nil + } + if portAllowed(allow, *in.RemotePort) { + return nil + } + return fmt.Errorf( + "远程端口 %d 不在服务端「%s」的 allowPorts 白名单(%s)内:frps 会拒绝这条规则,frpc 会反复报 \"port not allowed\"。请到「公网服务端 → 服务端参数」把 %d 加进白名单(或清空 allowPorts 表示不限),保存后重新下发 frps 配置", + *in.RemotePort, server.Name, allow, *in.RemotePort) +} + +// portAllowed 按 allowPorts 的写法(单端口 / 起-止 区间,逗号分隔)判断端口是否放行。 +// 区间写反(6010-6000)、越界(0 或 >65535)与 renderAllowPorts 一样视为无效,不放行; +// 否则会出现「校验说放行、实际生成的 frps.toml 里这条白名单被丢弃」的偏差。 +func portAllowed(allowPorts string, port int) bool { + for _, part := range strings.Split(allowPorts, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + if start, end, ok := strings.Cut(part, "-"); ok { + startN, err1 := strconv.Atoi(strings.TrimSpace(start)) + endN, err2 := strconv.Atoi(strings.TrimSpace(end)) + if err1 == nil && err2 == nil && startN > 0 && endN >= startN && endN <= 65535 && port >= startN && port <= endN { + return true + } + continue + } + if n, err := strconv.Atoi(part); err == nil && n > 0 && n <= 65535 && n == port { + return true + } + } + return false +} + func validDomain(domain string) bool { domain = strings.TrimSuffix(strings.TrimSpace(domain), ".") if domain == "" || len(domain) > 253 || strings.Contains(domain, "..") { diff --git a/internal/frp/validate_test.go b/internal/frp/validate_test.go index d0d91b0..05f19c0 100644 --- a/internal/frp/validate_test.go +++ b/internal/frp/validate_test.go @@ -1,6 +1,9 @@ package frp -import "testing" +import ( + "strings" + "testing" +) func TestValidateProxyInput(t *testing.T) { port := 60022 @@ -80,3 +83,43 @@ func TestValidateServerInputVhostPorts(t *testing.T) { }) } } + +func TestValidateRemotePortAllowed(t *testing.T) { + port := func(value int) *int { return &value } + base := Server{Name: "雨云vps"} + cases := []struct { + name string + allow string + input ProxyInput + wantErr bool + }{ + {name: "单端口命中", allow: "6000,6095", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}}, + {name: "区间命中", allow: "6000-6100", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}}, + {name: "区间下界命中", allow: "6095-6200", input: ProxyInput{Type: "udp", RemotePort: port(6095)}}, + {name: "带空格写法", allow: " 6000 - 6100 , 7000", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}}, + {name: "不在白名单拒绝", allow: "6000-6010", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}, wantErr: true}, + {name: "区间写反视为无效", allow: "6100-6000", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}, wantErr: true}, + {name: "越界区间视为无效", allow: "6000-70000", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}, wantErr: true}, + {name: "白名单为空不限制", allow: "", input: ProxyInput{Type: "tcp", RemotePort: port(6095)}}, + {name: "HTTP 规则跳过校验", allow: "6000-6010", input: ProxyInput{Type: "http", RemotePort: port(6095)}}, + {name: "无远程端口跳过校验", allow: "6000-6010", input: ProxyInput{Type: "tcp"}}, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + server := base + server.Options.AllowPorts = testCase.allow + err := validateRemotePortAllowed(server, testCase.input) + if (err != nil) != testCase.wantErr { + t.Fatalf("validateRemotePortAllowed() error = %v, wantErr %v", err, testCase.wantErr) + } + if err == nil { + return + } + message := err.Error() + if !strings.Contains(message, "6095") || !strings.Contains(message, testCase.allow) { + t.Fatalf("错误信息应包含端口与白名单原文,实际为 %q", message) + } + }) + } +}