tcp新增端口校验逻辑
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-10-19 15:53:59 +08:00
parent 5b9773b74a
commit e4edbdcef0
3 changed files with 99 additions and 3 deletions
+10 -2
View File
@@ -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)
+45
View File
@@ -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, "..") {
+44 -1
View File
@@ -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)
}
})
}
}