1
0
Эх сурвалжийг харах

Merge pull request #4092 from fatedier/dev

bump v0.56.0
fatedier 10 сар өмнө
parent
commit
5a6d9f60c2

+ 1 - 1
.github/workflows/golangci-lint.yml

@@ -23,7 +23,7 @@ jobs:
       uses: golangci/golangci-lint-action@v4
       with:
         # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
-        version: v1.56
+        version: v1.57
 
         # Optional: golangci-lint command line arguments.
         # args: --issues-exit-code=0

+ 9 - 18
.golangci.yml

@@ -1,5 +1,5 @@
 service:
-  golangci-lint-version: 1.56.x # use the fixed version to not introduce new linters unexpectedly
+  golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly
   
 run:
   concurrency: 4
@@ -8,23 +8,6 @@ run:
   build-tags:
   - integ
   - integfuzz
-  # which dirs to skip: they won't be analyzed;
-  # can use regexp here: generated.*, regexp is applied on full path;
-  # default value is empty list, but next dirs are always skipped independently
-  # from this option's value:
-  #       vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
-  skip-dirs:
-    - genfiles$
-    - vendor$
-    - bin$
-
-  # which files to skip: they will be analyzed, but issues from them
-  # won't be reported. Default value is empty list, but there is
-  # no need to include all autogenerated files, we confidently recognize
-  # autogenerated files. If it's not please let us know.
-  skip-files:
-    - ".*\\.pb\\.go"
-    - ".*\\.gen\\.go"
 
 linters:
   disable-all: true
@@ -136,6 +119,14 @@ issues:
     - unparam
     text: "is always false"
 
+  exclude-dirs:
+  - genfiles$
+  - vendor$
+  - bin$
+  exclude-files:
+  - ".*\\.pb\\.go"
+  - ".*\\.gen\\.go"
+
   # Independently from option `exclude` we use default exclude patterns,
   # it can be disabled by this option. To list all
   # excluded by default patterns execute `golangci-lint run --help`.

+ 1 - 1
Makefile.cross-compiles

@@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin
 export GO111MODULE=on
 LDFLAGS := -s -w
 
-os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
+os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
 
 all: build
 

+ 19 - 0
README.md

@@ -78,6 +78,7 @@ frp also offers a P2P connect mode.
     * [URL Routing](#url-routing)
     * [TCP Port Multiplexing](#tcp-port-multiplexing)
     * [Connecting to frps via PROXY](#connecting-to-frps-via-proxy)
+    * [Port range mapping](#port-range-mapping)
     * [Client Plugins](#client-plugins)
     * [Server Manage Plugins](#server-manage-plugins)
     * [SSH Tunnel Gateway](#ssh-tunnel-gateway)
@@ -1158,6 +1159,24 @@ serverPort = 7000
 transport.proxyURL = "http://user:pwd@192.168.1.128:8080"
 ```
 
+### Port range mapping
+
+*Added in v0.56.0*
+
+We can use the range syntax of Go template combined with the built-in `parseNumberRangePair` function to achieve port range mapping.
+
+The following example, when run, will create 8 proxies named `test-6000, test-6001 ... test-6007`, each mapping the remote port to the local port.
+
+```
+{{- range $_, $v := parseNumberRangePair "6000-6006,6007" "6000-6006,6007" }}
+[[proxies]]
+name = "tcp-{{ $v.First }}"
+type = "tcp"
+localPort = {{ $v.First }}
+remotePort = {{ $v.Second }}
+{{- end }}
+```
+
 ### Client Plugins
 
 frpc only forwards requests to local TCP or UDP ports by default.

+ 25 - 1
Release.md

@@ -1 +1,25 @@
-No feature changes, just a fix for the issue of no released assets in version 0.55.0.
+### Features
+
+* Support range ports mapping in TOML/YAML/JSON configuration file by using go template syntax.
+
+  For example:
+
+  ```
+  {{- range $_, $v := parseNumberRangePair "6000-6006,6007" "6000-6006,6007" }}
+  [[proxies]]
+  name = "tcp-{{ $v.First }}"
+  type = "tcp"
+  localPort = {{ $v.First }}
+  remotePort = {{ $v.Second }}
+  {{- end }}
+  ```
+
+  This will create 8 proxies such as `tcp-6000, tcp-6001, ... tcp-6007`.
+
+* Health check supports custom request headers.
+* Enable compatibility mode for the Android system to solve the issues of incorrect log time caused by time zone problems and default DNS resolution failures.
+
+### Fixes
+
+* Fix the issue of incorrect interval time for rotating the log by day.
+* Disable quic-go's ECN support by default. It may cause issues on certain operating systems.

+ 10 - 2
client/health/health.go

@@ -40,8 +40,8 @@ type Monitor struct {
 	addr string
 
 	// For http
-	url string
-
+	url            string
+	header         http.Header
 	failedTimes    uint64
 	statusOK       bool
 	statusNormalFn func()
@@ -73,6 +73,11 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
 		}
 		url = s + cfg.Path
 	}
+	header := make(http.Header)
+	for _, h := range cfg.HTTPHeaders {
+		header.Set(h.Name, h.Value)
+	}
+
 	return &Monitor{
 		checkType:      cfg.Type,
 		interval:       time.Duration(cfg.IntervalSeconds) * time.Second,
@@ -80,6 +85,7 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
 		maxFailedTimes: cfg.MaxFailed,
 		addr:           addr,
 		url:            url,
+		header:         header,
 		statusOK:       false,
 		statusNormalFn: statusNormalFn,
 		statusFailedFn: statusFailedFn,
@@ -163,6 +169,8 @@ func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
+	req.Header = monitor.header
+	req.Host = monitor.header.Get("Host")
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		return err

+ 7 - 0
client/service.go

@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"os"
 	"runtime"
 	"sync"
 	"time"
@@ -40,6 +41,12 @@ import (
 
 func init() {
 	crypto.DefaultSalt = "frp"
+	// Disable quic-go's receive buffer warning.
+	os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
+	// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
+	if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
+		os.Setenv("QUIC_GO_DISABLE_ECN", "true")
+	}
 }
 
 type cancelErr struct {

+ 2 - 0
cmd/frpc/main.go

@@ -17,8 +17,10 @@ package main
 import (
 	_ "github.com/fatedier/frp/assets/frpc"
 	"github.com/fatedier/frp/cmd/frpc/sub"
+	"github.com/fatedier/frp/pkg/util/system"
 )
 
 func main() {
+	system.EnableCompatibilityMode()
 	sub.Execute()
 }

+ 2 - 3
cmd/frps/main.go

@@ -15,13 +15,12 @@
 package main
 
 import (
-	"github.com/fatedier/golib/crypto"
-
 	_ "github.com/fatedier/frp/assets/frps"
 	_ "github.com/fatedier/frp/pkg/metrics"
+	"github.com/fatedier/frp/pkg/util/system"
 )
 
 func main() {
-	crypto.DefaultSalt = "frp"
+	system.EnableCompatibilityMode()
 	Execute()
 }

+ 4 - 0
conf/frpc_full_example.toml

@@ -216,6 +216,10 @@ healthCheck.path = "/status"
 healthCheck.intervalSeconds = 10
 healthCheck.maxFailed = 3
 healthCheck.timeoutSeconds = 3
+# set health check headers
+healthCheck.httpHeaders=[
+    { name = "x-from-where", value = "frp" }
+]
 
 [[proxies]]
 name = "web02"

+ 1 - 1
go.mod

@@ -5,7 +5,7 @@ go 1.22
 require (
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc/v3 v3.6.0
-	github.com/fatedier/golib v0.4.0
+	github.com/fatedier/golib v0.4.2
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0

+ 2 - 2
go.sum

@@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatedier/golib v0.4.0 h1:lafvYRMhFmqrfIUChKy/f5AXqs1eDSk+GAUtLexN5bU=
-github.com/fatedier/golib v0.4.0/go.mod h1:gpu+1vXxtJ072NYaNsn/YWgojDL8Ap2kFZQtbzT2qkg=
+github.com/fatedier/golib v0.4.2 h1:k+ZBdUFTTipnP1RHfEhGbzyShRdz/rZtFGnjpXG9D9c=
+github.com/fatedier/golib v0.4.2/go.mod h1:gpu+1vXxtJ072NYaNsn/YWgojDL8Ap2kFZQtbzT2qkg=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=

+ 4 - 1
pkg/config/load.go

@@ -80,7 +80,10 @@ func DetectLegacyINIFormatFromFile(path string) bool {
 }
 
 func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
-	tmpl, err := template.New("frp").Parse(string(in))
+	tmpl, err := template.New("frp").Funcs(template.FuncMap{
+		"parseNumberRange":     parseNumberRange,
+		"parseNumberRangePair": parseNumberRangePair,
+	}).Parse(string(in))
 	if err != nil {
 		return nil, err
 	}

+ 52 - 0
pkg/config/template.go

@@ -0,0 +1,52 @@
+// Copyright 2024 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"fmt"
+
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type NumberPair struct {
+	First  int64
+	Second int64
+}
+
+func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, error) {
+	firstRangeNumbers, err := util.ParseRangeNumbers(firstRangeStr)
+	if err != nil {
+		return nil, err
+	}
+	secondRangeNumbers, err := util.ParseRangeNumbers(secondRangeStr)
+	if err != nil {
+		return nil, err
+	}
+	if len(firstRangeNumbers) != len(secondRangeNumbers) {
+		return nil, fmt.Errorf("first and second range numbers are not in pairs")
+	}
+	pairs := make([]NumberPair, 0, len(firstRangeNumbers))
+	for i := 0; i < len(firstRangeNumbers); i++ {
+		pairs = append(pairs, NumberPair{
+			First:  firstRangeNumbers[i],
+			Second: secondRangeNumbers[i],
+		})
+	}
+	return pairs, nil
+}
+
+func parseNumberRange(firstRangeStr string) ([]int64, error) {
+	return util.ParseRangeNumbers(firstRangeStr)
+}

+ 5 - 0
pkg/config/v1/common.go

@@ -129,3 +129,8 @@ type HTTPPluginOptions struct {
 type HeaderOperations struct {
 	Set map[string]string `json:"set,omitempty"`
 }
+
+type HTTPHeader struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}

+ 3 - 0
pkg/config/v1/proxy.go

@@ -97,6 +97,9 @@ type HealthCheckConfig struct {
 	// Path specifies the path to send health checks to if the
 	// health check type is "http".
 	Path string `json:"path,omitempty"`
+	// HTTPHeaders specifies the headers to send with the health request, if
+	// the health check type is "http".
+	HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
 }
 
 type DomainConfig struct {

+ 3 - 3
pkg/nathole/nathole.go

@@ -17,7 +17,7 @@ package nathole
 import (
 	"context"
 	"fmt"
-	"math/rand"
+	"math/rand/v2"
 	"net"
 	"slices"
 	"strconv"
@@ -341,7 +341,7 @@ func sendSidMessage(
 		TransactionID: transactionID,
 		Sid:           sid,
 		Response:      false,
-		Nonce:         strings.Repeat("0", rand.Intn(20)),
+		Nonce:         strings.Repeat("0", rand.IntN(20)),
 	}
 	buf, err := EncodeMessage(m, key)
 	if err != nil {
@@ -398,7 +398,7 @@ func sendSidMessageToRandomPorts(
 	used := sets.New[int]()
 	getUnusedPort := func() int {
 		for i := 0; i < 10; i++ {
-			port := rand.Intn(65535-1024) + 1024
+			port := rand.IntN(65535-1024) + 1024
 			if !used.Has(port) {
 				used.Insert(port)
 				return port

+ 2 - 2
pkg/util/net/dns.go

@@ -26,8 +26,8 @@ func SetDefaultDNSAddress(dnsAddress string) {
 	// Change default dns server
 	net.DefaultResolver = &net.Resolver{
 		PreferGo: true,
-		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
-			return net.Dial("udp", dnsAddress)
+		Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
+			return net.Dial(network, dnsAddress)
 		},
 	}
 }

+ 22 - 0
pkg/util/system/system.go

@@ -0,0 +1,22 @@
+// Copyright 2024 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !android
+
+package system
+
+// EnableCompatibilityMode enables compatibility mode for different system.
+// For example, on Android, the inability to obtain the correct time zone will result in incorrect log time output.
+func EnableCompatibilityMode() {
+}

+ 66 - 0
pkg/util/system/system_android.go

@@ -0,0 +1,66 @@
+// Copyright 2024 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package system
+
+import (
+	"context"
+	"net"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+func EnableCompatibilityMode() {
+	fixTimezone()
+	fixDNSResolver()
+}
+
+// fixTimezone is used to try our best to fix timezone issue on some Android devices.
+func fixTimezone() {
+	out, err := exec.Command("/system/bin/getprop", "persist.sys.timezone").Output()
+	if err != nil {
+		return
+	}
+	loc, err := time.LoadLocation(strings.TrimSpace(string(out)))
+	if err != nil {
+		return
+	}
+	time.Local = loc
+}
+
+// fixDNSResolver will first attempt to resolve google.com to check if the current DNS is available.
+// If it is not available, it will default to using 8.8.8.8 as the DNS server.
+// This is a workaround for the issue that golang can't get the default DNS servers on Android.
+func fixDNSResolver() {
+	// First, we attempt to resolve a domain. If resolution is successful, no modifications are necessary.
+	// In real-world scenarios, users may have already configured /etc/resolv.conf, or compiled directly
+	// in the Android environment instead of using cross-platform compilation, so this issue does not arise.
+	if net.DefaultResolver != nil {
+		timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
+		defer cancel()
+		_, err := net.DefaultResolver.LookupHost(timeoutCtx, "google.com")
+		if err == nil {
+			return
+		}
+	}
+	// If the resolution fails, use 8.8.8.8 as the DNS server.
+	// Note: If there are other methods to obtain the default DNS servers, the default DNS servers should be used preferentially.
+	net.DefaultResolver = &net.Resolver{
+		PreferGo: true,
+		Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
+			return net.Dial(network, "8.8.8.8:53")
+		},
+	}
+}

+ 2 - 2
pkg/util/util/util.go

@@ -20,7 +20,7 @@ import (
 	"crypto/subtle"
 	"encoding/hex"
 	"fmt"
-	mathrand "math/rand"
+	mathrand "math/rand/v2"
 	"net"
 	"strconv"
 	"strings"
@@ -124,7 +124,7 @@ func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Durati
 	if max <= min {
 		n = min
 	} else {
-		n = mathrand.Int63n(max-min) + min
+		n = mathrand.Int64N(max-min) + min
 	}
 	d := duration * time.Duration(n) / time.Duration(1000)
 	time.Sleep(d)

+ 1 - 1
pkg/util/version/version.go

@@ -14,7 +14,7 @@
 
 package version
 
-var version = "0.55.1"
+var version = "0.56.0"
 
 func Full() string {
 	return version

+ 1 - 1
pkg/util/wait/backoff.go

@@ -15,7 +15,7 @@
 package wait
 
 import (
-	"math/rand"
+	"math/rand/v2"
 	"time"
 
 	"github.com/fatedier/frp/pkg/util/util"

+ 12 - 0
server/service.go

@@ -22,9 +22,11 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"os"
 	"strconv"
 	"time"
 
+	"github.com/fatedier/golib/crypto"
 	"github.com/fatedier/golib/net/mux"
 	fmux "github.com/hashicorp/yamux"
 	quic "github.com/quic-go/quic-go"
@@ -59,6 +61,16 @@ const (
 	vhostReadWriteTimeout time.Duration = 30 * time.Second
 )
 
+func init() {
+	crypto.DefaultSalt = "frp"
+	// Disable quic-go's receive buffer warning.
+	os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
+	// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
+	if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
+		os.Setenv("QUIC_GO_DISABLE_ECN", "true")
+	}
+}
+
 // Server service
 type Service struct {
 	// Dispatch connections to different handlers listen on same port

+ 5 - 7
test/e2e/framework/client.go

@@ -1,11 +1,9 @@
 package framework
 
-type FRPClient struct {
-	port int
-}
+import (
+	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
+)
 
-func (f *Framework) FRPClient(port int) *FRPClient {
-	return &FRPClient{
-		port: port,
-	}
+func (f *Framework) APIClientForFrpc(port int) *clientsdk.Client {
+	return clientsdk.New("127.0.0.1", port)
 }

+ 4 - 0
test/e2e/framework/expect.go

@@ -43,6 +43,10 @@ func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) {
 	gomega.ExpectWithOffset(1+offset, err).NotTo(gomega.HaveOccurred(), explain...)
 }
 
+func ExpectContainSubstring(actual, substr string, explain ...interface{}) {
+	gomega.ExpectWithOffset(1, actual).To(gomega.ContainSubstring(substr), explain...)
+}
+
 // ExpectConsistOf expects actual contains precisely the extra elements.  The ordering of the elements does not matter.
 func ExpectConsistOf(actual interface{}, extra interface{}, explain ...interface{}) {
 	gomega.ExpectWithOffset(1, actual).To(gomega.ConsistOf(extra), explain...)

+ 1 - 1
test/e2e/framework/framework.go

@@ -217,7 +217,7 @@ func (f *Framework) RenderTemplates(templates []string) (outs []string, ports ma
 	}
 
 	for _, t := range templates {
-		tmpl, err := template.New("").Parse(t)
+		tmpl, err := template.New("frp-e2e").Parse(t)
 		if err != nil {
 			return nil, nil, err
 		}

+ 2 - 3
test/e2e/legacy/basic/client.go

@@ -8,7 +8,6 @@ import (
 
 	"github.com/onsi/ginkgo/v2"
 
-	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
@@ -54,7 +53,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 		framework.NewRequestExpect(f).Port(p2Port).Ensure()
 		framework.NewRequestExpect(f).Port(p3Port).Ensure()
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 		conf, err := client.GetConfig()
 		framework.ExpectNoError(err)
 
@@ -120,7 +119,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 
 		framework.NewRequestExpect(f).Port(testPort).Ensure()
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 		err := client.Stop()
 		framework.ExpectNoError(err)
 

+ 1 - 2
test/e2e/legacy/basic/server.go

@@ -7,7 +7,6 @@ import (
 
 	"github.com/onsi/ginkgo/v2"
 
-	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/pkg/port"
@@ -99,7 +98,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 
 		// tcp random port
 		status, err := client.GetProxyStatus("tcp")

+ 2 - 3
test/e2e/v1/basic/client.go

@@ -8,7 +8,6 @@ import (
 
 	"github.com/onsi/ginkgo/v2"
 
-	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
@@ -57,7 +56,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 		framework.NewRequestExpect(f).Port(p2Port).Ensure()
 		framework.NewRequestExpect(f).Port(p3Port).Ensure()
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 		conf, err := client.GetConfig()
 		framework.ExpectNoError(err)
 
@@ -124,7 +123,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 
 		framework.NewRequestExpect(f).Port(testPort).Ensure()
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 		err := client.Stop()
 		framework.ExpectNoError(err)
 

+ 45 - 0
test/e2e/v1/basic/config.go

@@ -38,6 +38,51 @@ var _ = ginkgo.Describe("[Feature: Config]", func() {
 
 			framework.NewRequestExpect(f).PortName(portName).Ensure()
 		})
+
+		ginkgo.It("Range ports mapping", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			adminPort := f.AllocPort()
+
+			localPortsRange := "13010-13012,13014"
+			remotePortsRange := "23010-23012,23014"
+			escapeTemplate := func(s string) string {
+				return "{{ `" + s + "` }}"
+			}
+			clientConf += fmt.Sprintf(`
+			webServer.port = %d
+
+			%s
+			[[proxies]]
+			name = "tcp-%s"
+			type = "tcp"
+			localPort = %s
+			remotePort = %s
+			%s
+			`, adminPort,
+				escapeTemplate(fmt.Sprintf(`{{- range $_, $v := parseNumberRangePair "%s" "%s" }}`, localPortsRange, remotePortsRange)),
+				escapeTemplate("{{ $v.First }}"),
+				escapeTemplate("{{ $v.First }}"),
+				escapeTemplate("{{ $v.Second }}"),
+				escapeTemplate("{{- end }}"),
+			)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			client := f.APIClientForFrpc(adminPort)
+			checkProxyFn := func(name string, localPort, remotePort int) {
+				status, err := client.GetProxyStatus(name)
+				framework.ExpectNoError(err)
+
+				framework.ExpectContainSubstring(status.LocalAddr, fmt.Sprintf(":%d", localPort))
+				framework.ExpectContainSubstring(status.RemoteAddr, fmt.Sprintf(":%d", remotePort))
+			}
+			checkProxyFn("tcp-13010", 13010, 23010)
+			checkProxyFn("tcp-13011", 13011, 23011)
+			checkProxyFn("tcp-13012", 13012, 23012)
+			checkProxyFn("tcp-13014", 13014, 23014)
+		})
 	})
 
 	ginkgo.Describe("Includes", func() {

+ 1 - 2
test/e2e/v1/basic/server.go

@@ -7,7 +7,6 @@ import (
 
 	"github.com/onsi/ginkgo/v2"
 
-	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/pkg/port"
@@ -110,7 +109,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		client := clientsdk.New("127.0.0.1", adminPort)
+		client := f.APIClientForFrpc(adminPort)
 
 		// tcp random port
 		status, err := client.GetProxyStatus("tcp")