Browse Source

Merge pull request #4925 from fatedier/dev

bump version
fatedier 1 week ago
parent
commit
22ae8166d3

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

@@ -23,14 +23,4 @@ jobs:
       uses: golangci/golangci-lint-action@v8
       uses: golangci/golangci-lint-action@v8
       with:
       with:
         # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
         # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
-        version: v2.1
-
-        # Optional: golangci-lint command line arguments.
-        # args: --issues-exit-code=0
-
-        # Optional: show only new issues if it's a pull request. The default value is `false`.
-        # only-new-issues: true
-
-        # Optional: if set to true then the all caching functionality will be complete disabled,
-        #           takes precedence over all other caching options.
-        # skip-cache: true
+        version: v2.3

+ 3 - 0
.golangci.yml

@@ -73,6 +73,9 @@ linters:
     - linters:
     - linters:
       - revive
       - revive
       text: unused-parameter
       text: unused-parameter
+    - linters:
+      - revive
+      text: "avoid meaningless package names"
     - linters:
     - linters:
       - unparam
       - unparam
       text: is always false
       text: is always false

+ 32 - 0
README.md

@@ -13,19 +13,36 @@ frp is an open source project with its ongoing development made possible entirel
 
 
 <h3 align="center">Gold Sponsors</h3>
 <h3 align="center">Gold Sponsors</h3>
 <!--gold sponsors start-->
 <!--gold sponsors start-->
+<p align="center">
+  <a href="https://go.warp.dev/frp" target="_blank">
+    <img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
+    <br>
+    <b>Warp, the intelligent terminal</b>
+    <br>
+	<sub>Available for macOS, Linux and Windows</sub>
+  </a>
+</p>
 <p align="center">
 <p align="center">
   <a href="https://jb.gg/frp" target="_blank">
   <a href="https://jb.gg/frp" target="_blank">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
+	<br>
+	<b>The complete IDE crafted for professional Go developers</b>
   </a>
   </a>
 </p>
 </p>
 <p align="center">
 <p align="center">
   <a href="https://github.com/daytonaio/daytona" target="_blank">
   <a href="https://github.com/daytonaio/daytona" target="_blank">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
+	<br>
+	<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
   </a>
   </a>
 </p>
 </p>
 <p align="center">
 <p align="center">
   <a href="https://github.com/beclab/Olares" target="_blank">
   <a href="https://github.com/beclab/Olares" target="_blank">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
     <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
+	<br>
+	<b>The sovereign cloud that puts you in control</b>
+	<br>
+	<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
   </a>
   </a>
 </p>
 </p>
 <!--gold sponsors end-->
 <!--gold sponsors end-->
@@ -612,6 +629,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b
 
 
 Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
 Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
 
 
+##### Token Source
+
+frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported.
+
+**File-based token source:**
+
+```toml
+# frpc.toml
+auth.method = "token"
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "/path/to/token/file"
+```
+
+The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons.
+
 #### OIDC Authentication
 #### OIDC Authentication
 
 
 When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
 When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.

+ 5 - 2
Release.md

@@ -1,4 +1,7 @@
 ## Features
 ## Features
 
 
-* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
-* Support for proxy protocol in UDP proxies to preserve real client IP addresses.
+* Support tokenSource for loading authentication tokens from files.
+
+## Fixes
+
+* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1.

+ 1 - 1
client/connector.go

@@ -48,7 +48,7 @@ type defaultConnectorImpl struct {
 	cfg *v1.ClientCommonConfig
 	cfg *v1.ClientCommonConfig
 
 
 	muxSession *fmux.Session
 	muxSession *fmux.Session
-	quicConn   quic.Connection
+	quicConn   *quic.Conn
 	closeOnce  sync.Once
 	closeOnce  sync.Once
 }
 }
 
 

+ 12 - 3
client/service.go

@@ -88,13 +88,16 @@ type ServiceOptions struct {
 }
 }
 
 
 // setServiceOptionsDefault sets the default values for ServiceOptions.
 // setServiceOptionsDefault sets the default values for ServiceOptions.
-func setServiceOptionsDefault(options *ServiceOptions) {
+func setServiceOptionsDefault(options *ServiceOptions) error {
 	if options.Common != nil {
 	if options.Common != nil {
-		options.Common.Complete()
+		if err := options.Common.Complete(); err != nil {
+			return err
+		}
 	}
 	}
 	if options.ConnectorCreator == nil {
 	if options.ConnectorCreator == nil {
 		options.ConnectorCreator = NewConnector
 		options.ConnectorCreator = NewConnector
 	}
 	}
+	return nil
 }
 }
 
 
 // Service is the client service that connects to frps and provides proxy services.
 // Service is the client service that connects to frps and provides proxy services.
@@ -134,7 +137,9 @@ type Service struct {
 }
 }
 
 
 func NewService(options ServiceOptions) (*Service, error) {
 func NewService(options ServiceOptions) (*Service, error) {
-	setServiceOptionsDefault(&options)
+	if err := setServiceOptionsDefault(&options); err != nil {
+		return nil, err
+	}
 
 
 	var webServer *httppkg.Server
 	var webServer *httppkg.Server
 	if options.Common.WebServer.Port > 0 {
 	if options.Common.WebServer.Port > 0 {
@@ -398,6 +403,10 @@ func (svr *Service) stop() {
 		svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
 		svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
 		svr.ctl = nil
 		svr.ctl = nil
 	}
 	}
+	if svr.webServer != nil {
+		svr.webServer.Close()
+		svr.webServer = nil
+	}
 }
 }
 
 
 func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
 func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {

+ 1 - 1
client/visitor/xtcp.go

@@ -398,7 +398,7 @@ func (ks *KCPTunnelSession) Close() {
 }
 }
 
 
 type QUICTunnelSession struct {
 type QUICTunnelSession struct {
-	session    quic.Connection
+	session    *quic.Conn
 	listenConn *net.UDPConn
 	listenConn *net.UDPConn
 	mu         sync.RWMutex
 	mu         sync.RWMutex
 
 

+ 4 - 1
cmd/frpc/sub/nathole.go

@@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{
 		cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
 		cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
 		if err != nil {
 		if err != nil {
 			cfg = &v1.ClientCommonConfig{}
 			cfg = &v1.ClientCommonConfig{}
-			cfg.Complete()
+			if err := cfg.Complete(); err != nil {
+				fmt.Printf("failed to complete config: %v\n", err)
+				os.Exit(1)
+			}
 		}
 		}
 		if natHoleSTUNServer != "" {
 		if natHoleSTUNServer != "" {
 			cfg.NatHoleSTUNServer = natHoleSTUNServer
 			cfg.NatHoleSTUNServer = natHoleSTUNServer

+ 8 - 2
cmd/frpc/sub/proxy.go

@@ -73,7 +73,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
 		Use:   name,
 		Use:   name,
 		Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
 		Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
-			clientCfg.Complete()
+			if err := clientCfg.Complete(); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
 			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
 			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
 				fmt.Println(err)
 				fmt.Println(err)
 				os.Exit(1)
 				os.Exit(1)
@@ -99,7 +102,10 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
 		Use:   "visitor",
 		Use:   "visitor",
 		Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
 		Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
-			clientCfg.Complete()
+			if err := clientCfg.Complete(); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
 			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
 			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
 				fmt.Println(err)
 				fmt.Println(err)
 				os.Exit(1)
 				os.Exit(1)

+ 4 - 1
cmd/frps/root.go

@@ -70,7 +70,10 @@ var rootCmd = &cobra.Command{
 					"please use yaml/json/toml format instead!\n")
 					"please use yaml/json/toml format instead!\n")
 			}
 			}
 		} else {
 		} else {
-			serverCfg.Complete()
+			if err := serverCfg.Complete(); err != nil {
+				fmt.Printf("failed to complete server config: %v\n", err)
+				os.Exit(1)
+			}
 			svrCfg = &serverCfg
 			svrCfg = &serverCfg
 		}
 		}
 
 

+ 5 - 0
conf/frpc_full_example.toml

@@ -32,6 +32,11 @@ auth.method = "token"
 # auth token
 # auth token
 auth.token = "12345678"
 auth.token = "12345678"
 
 
+# alternatively, you can use tokenSource to load the token from a file
+# this is mutually exclusive with auth.token
+# auth.tokenSource.type = "file"
+# auth.tokenSource.file.path = "/etc/frp/token"
+
 # oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
 # oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
 # auth.oidc.clientID = ""
 # auth.oidc.clientID = ""
 # oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
 # oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.

+ 5 - 0
conf/frps_full_example.toml

@@ -105,6 +105,11 @@ auth.method = "token"
 # auth token
 # auth token
 auth.token = "12345678"
 auth.token = "12345678"
 
 
+# alternatively, you can use tokenSource to load the token from a file
+# this is mutually exclusive with auth.token
+# auth.tokenSource.type = "file"
+# auth.tokenSource.file.path = "/etc/frp/token"
+
 # oidc issuer specifies the issuer to verify OIDC tokens with.
 # oidc issuer specifies the issuer to verify OIDC tokens with.
 auth.oidc.issuer = ""
 auth.oidc.issuer = ""
 # oidc audience specifies the audience OIDC tokens should contain when validated.
 # oidc audience specifies the audience OIDC tokens should contain when validated.

+ 1 - 2
go.mod

@@ -16,7 +16,7 @@ require (
 	github.com/pion/stun/v2 v2.0.0
 	github.com/pion/stun/v2 v2.0.0
 	github.com/pires/go-proxyproto v0.7.0
 	github.com/pires/go-proxyproto v0.7.0
 	github.com/prometheus/client_golang v1.19.1
 	github.com/prometheus/client_golang v1.19.1
-	github.com/quic-go/quic-go v0.48.2
+	github.com/quic-go/quic-go v0.53.0
 	github.com/rodaine/table v1.2.0
 	github.com/rodaine/table v1.2.0
 	github.com/samber/lo v1.47.0
 	github.com/samber/lo v1.47.0
 	github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
 	github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
@@ -68,7 +68,6 @@ require (
 	github.com/vishvananda/netns v0.0.4 // indirect
 	github.com/vishvananda/netns v0.0.4 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/mock v0.5.0 // indirect
 	go.uber.org/mock v0.5.0 // indirect
-	golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
 	golang.org/x/mod v0.24.0 // indirect
 	golang.org/x/mod v0.24.0 // indirect
 	golang.org/x/sys v0.32.0 // indirect
 	golang.org/x/sys v0.32.0 // indirect
 	golang.org/x/text v0.24.0 // indirect
 	golang.org/x/text v0.24.0 // indirect

+ 2 - 4
go.sum

@@ -105,8 +105,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
-github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
+github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
+github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
 github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
@@ -167,8 +167,6 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
-golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

+ 6 - 2
pkg/config/load.go

@@ -212,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
 		}
 		}
 	}
 	}
 	if svrCfg != nil {
 	if svrCfg != nil {
-		svrCfg.Complete()
+		if err := svrCfg.Complete(); err != nil {
+			return nil, isLegacyFormat, err
+		}
 	}
 	}
 	return svrCfg, isLegacyFormat, nil
 	return svrCfg, isLegacyFormat, nil
 }
 }
@@ -280,7 +282,9 @@ func LoadClientConfig(path string, strict bool) (
 	}
 	}
 
 
 	if cliCfg != nil {
 	if cliCfg != nil {
-		cliCfg.Complete()
+		if err := cliCfg.Complete(); err != nil {
+			return nil, nil, nil, isLegacyFormat, err
+		}
 	}
 	}
 	for _, c := range proxyCfgs {
 	for _, c := range proxyCfgs {
 		c.Complete(cliCfg.User)
 		c.Complete(cliCfg.User)

+ 25 - 5
pkg/config/v1/client.go

@@ -15,6 +15,8 @@
 package v1
 package v1
 
 
 import (
 import (
+	"context"
+	"fmt"
 	"os"
 	"os"
 
 
 	"github.com/samber/lo"
 	"github.com/samber/lo"
@@ -77,18 +79,21 @@ type ClientCommonConfig struct {
 	IncludeConfigFiles []string `json:"includes,omitempty"`
 	IncludeConfigFiles []string `json:"includes,omitempty"`
 }
 }
 
 
-func (c *ClientCommonConfig) Complete() {
+func (c *ClientCommonConfig) Complete() error {
 	c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
 	c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
 	c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
 	c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
 	c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
 	c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
 	c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
 	c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
 
 
-	c.Auth.Complete()
+	if err := c.Auth.Complete(); err != nil {
+		return err
+	}
 	c.Log.Complete()
 	c.Log.Complete()
 	c.Transport.Complete()
 	c.Transport.Complete()
 	c.WebServer.Complete()
 	c.WebServer.Complete()
 
 
 	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
 	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
+	return nil
 }
 }
 
 
 type ClientTransportConfig struct {
 type ClientTransportConfig struct {
@@ -184,12 +189,27 @@ type AuthClientConfig struct {
 	// Token specifies the authorization token used to create keys to be sent
 	// Token specifies the authorization token used to create keys to be sent
 	// to the server. The server must have a matching token for authorization
 	// to the server. The server must have a matching token for authorization
 	// to succeed.  By default, this value is "".
 	// to succeed.  By default, this value is "".
-	Token string               `json:"token,omitempty"`
-	OIDC  AuthOIDCClientConfig `json:"oidc,omitempty"`
+	Token string `json:"token,omitempty"`
+	// TokenSource specifies a dynamic source for the authorization token.
+	// This is mutually exclusive with Token field.
+	TokenSource *ValueSource         `json:"tokenSource,omitempty"`
+	OIDC        AuthOIDCClientConfig `json:"oidc,omitempty"`
 }
 }
 
 
-func (c *AuthClientConfig) Complete() {
+func (c *AuthClientConfig) Complete() error {
 	c.Method = util.EmptyOr(c.Method, "token")
 	c.Method = util.EmptyOr(c.Method, "token")
+
+	// Resolve tokenSource during configuration loading
+	if c.Method == AuthMethodToken && c.TokenSource != nil {
+		token, err := c.TokenSource.Resolve(context.Background())
+		if err != nil {
+			return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
+		}
+		// Move the resolved token to the Token field and clear TokenSource
+		c.Token = token
+		c.TokenSource = nil
+	}
+	return nil
 }
 }
 
 
 type AuthOIDCClientConfig struct {
 type AuthOIDCClientConfig struct {

+ 71 - 1
pkg/config/v1/client_test.go

@@ -15,6 +15,8 @@
 package v1
 package v1
 
 
 import (
 import (
+	"os"
+	"path/filepath"
 	"testing"
 	"testing"
 
 
 	"github.com/samber/lo"
 	"github.com/samber/lo"
@@ -24,7 +26,8 @@ import (
 func TestClientConfigComplete(t *testing.T) {
 func TestClientConfigComplete(t *testing.T) {
 	require := require.New(t)
 	require := require.New(t)
 	c := &ClientConfig{}
 	c := &ClientConfig{}
-	c.Complete()
+	err := c.Complete()
+	require.NoError(err)
 
 
 	require.EqualValues("token", c.Auth.Method)
 	require.EqualValues("token", c.Auth.Method)
 	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
 	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
@@ -33,3 +36,70 @@ func TestClientConfigComplete(t *testing.T) {
 	require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
 	require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
 	require.NotEmpty(c.NatHoleSTUNServer)
 	require.NotEmpty(c.NatHoleSTUNServer)
 }
 }
+
+func TestAuthClientConfig_Complete(t *testing.T) {
+	// Create a temporary file for testing
+	tmpDir := t.TempDir()
+	testFile := filepath.Join(tmpDir, "test_token")
+	testContent := "client-token-value"
+	err := os.WriteFile(testFile, []byte(testContent), 0o600)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name        string
+		config      AuthClientConfig
+		expectToken string
+		expectPanic bool
+	}{
+		{
+			name: "tokenSource resolved to token",
+			config: AuthClientConfig{
+				Method: AuthMethodToken,
+				TokenSource: &ValueSource{
+					Type: "file",
+					File: &FileSource{
+						Path: testFile,
+					},
+				},
+			},
+			expectToken: testContent,
+			expectPanic: false,
+		},
+		{
+			name: "direct token unchanged",
+			config: AuthClientConfig{
+				Method: AuthMethodToken,
+				Token:  "direct-token",
+			},
+			expectToken: "direct-token",
+			expectPanic: false,
+		},
+		{
+			name: "invalid tokenSource should panic",
+			config: AuthClientConfig{
+				Method: AuthMethodToken,
+				TokenSource: &ValueSource{
+					Type: "file",
+					File: &FileSource{
+						Path: "/non/existent/file",
+					},
+				},
+			},
+			expectPanic: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectPanic {
+				err := tt.config.Complete()
+				require.Error(t, err)
+			} else {
+				err := tt.config.Complete()
+				require.NoError(t, err)
+				require.Equal(t, tt.expectToken, tt.config.Token)
+				require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
+			}
+		})
+	}
+}

+ 22 - 3
pkg/config/v1/server.go

@@ -15,6 +15,9 @@
 package v1
 package v1
 
 
 import (
 import (
+	"context"
+	"fmt"
+
 	"github.com/samber/lo"
 	"github.com/samber/lo"
 
 
 	"github.com/fatedier/frp/pkg/config/types"
 	"github.com/fatedier/frp/pkg/config/types"
@@ -98,8 +101,10 @@ type ServerConfig struct {
 	HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
 	HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
 }
 }
 
 
-func (c *ServerConfig) Complete() {
-	c.Auth.Complete()
+func (c *ServerConfig) Complete() error {
+	if err := c.Auth.Complete(); err != nil {
+		return err
+	}
 	c.Log.Complete()
 	c.Log.Complete()
 	c.Transport.Complete()
 	c.Transport.Complete()
 	c.WebServer.Complete()
 	c.WebServer.Complete()
@@ -120,17 +125,31 @@ func (c *ServerConfig) Complete() {
 	c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
 	c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
 	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
 	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
 	c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
 	c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
+	return nil
 }
 }
 
 
 type AuthServerConfig struct {
 type AuthServerConfig struct {
 	Method           AuthMethod           `json:"method,omitempty"`
 	Method           AuthMethod           `json:"method,omitempty"`
 	AdditionalScopes []AuthScope          `json:"additionalScopes,omitempty"`
 	AdditionalScopes []AuthScope          `json:"additionalScopes,omitempty"`
 	Token            string               `json:"token,omitempty"`
 	Token            string               `json:"token,omitempty"`
+	TokenSource      *ValueSource         `json:"tokenSource,omitempty"`
 	OIDC             AuthOIDCServerConfig `json:"oidc,omitempty"`
 	OIDC             AuthOIDCServerConfig `json:"oidc,omitempty"`
 }
 }
 
 
-func (c *AuthServerConfig) Complete() {
+func (c *AuthServerConfig) Complete() error {
 	c.Method = util.EmptyOr(c.Method, "token")
 	c.Method = util.EmptyOr(c.Method, "token")
+
+	// Resolve tokenSource during configuration loading
+	if c.Method == AuthMethodToken && c.TokenSource != nil {
+		token, err := c.TokenSource.Resolve(context.Background())
+		if err != nil {
+			return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
+		}
+		// Move the resolved token to the Token field and clear TokenSource
+		c.Token = token
+		c.TokenSource = nil
+	}
+	return nil
 }
 }
 
 
 type AuthOIDCServerConfig struct {
 type AuthOIDCServerConfig struct {

+ 71 - 1
pkg/config/v1/server_test.go

@@ -15,6 +15,8 @@
 package v1
 package v1
 
 
 import (
 import (
+	"os"
+	"path/filepath"
 	"testing"
 	"testing"
 
 
 	"github.com/samber/lo"
 	"github.com/samber/lo"
@@ -24,9 +26,77 @@ import (
 func TestServerConfigComplete(t *testing.T) {
 func TestServerConfigComplete(t *testing.T) {
 	require := require.New(t)
 	require := require.New(t)
 	c := &ServerConfig{}
 	c := &ServerConfig{}
-	c.Complete()
+	err := c.Complete()
+	require.NoError(err)
 
 
 	require.EqualValues("token", c.Auth.Method)
 	require.EqualValues("token", c.Auth.Method)
 	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
 	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
 	require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
 	require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
 }
 }
+
+func TestAuthServerConfig_Complete(t *testing.T) {
+	// Create a temporary file for testing
+	tmpDir := t.TempDir()
+	testFile := filepath.Join(tmpDir, "test_token")
+	testContent := "file-token-value"
+	err := os.WriteFile(testFile, []byte(testContent), 0o600)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name        string
+		config      AuthServerConfig
+		expectToken string
+		expectPanic bool
+	}{
+		{
+			name: "tokenSource resolved to token",
+			config: AuthServerConfig{
+				Method: AuthMethodToken,
+				TokenSource: &ValueSource{
+					Type: "file",
+					File: &FileSource{
+						Path: testFile,
+					},
+				},
+			},
+			expectToken: testContent,
+			expectPanic: false,
+		},
+		{
+			name: "direct token unchanged",
+			config: AuthServerConfig{
+				Method: AuthMethodToken,
+				Token:  "direct-token",
+			},
+			expectToken: "direct-token",
+			expectPanic: false,
+		},
+		{
+			name: "invalid tokenSource should panic",
+			config: AuthServerConfig{
+				Method: AuthMethodToken,
+				TokenSource: &ValueSource{
+					Type: "file",
+					File: &FileSource{
+						Path: "/non/existent/file",
+					},
+				},
+			},
+			expectPanic: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectPanic {
+				err := tt.config.Complete()
+				require.Error(t, err)
+			} else {
+				err := tt.config.Complete()
+				require.NoError(t, err)
+				require.Equal(t, tt.expectToken, tt.config.Token)
+				require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
+			}
+		})
+	}
+}

+ 12 - 0
pkg/config/v1/validation/client.go

@@ -45,6 +45,18 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
 		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
 		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
 	}
 	}
 
 
+	// Validate token/tokenSource mutual exclusivity
+	if c.Auth.Token != "" && c.Auth.TokenSource != nil {
+		errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
+	}
+
+	// Validate tokenSource if specified
+	if c.Auth.TokenSource != nil {
+		if err := c.Auth.TokenSource.Validate(); err != nil {
+			errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
+		}
+	}
+
 	if err := validateLogConfig(&c.Log); err != nil {
 	if err := validateLogConfig(&c.Log); err != nil {
 		errs = AppendError(errs, err)
 		errs = AppendError(errs, err)
 	}
 	}

+ 12 - 0
pkg/config/v1/validation/server.go

@@ -35,6 +35,18 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
 		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
 		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
 	}
 	}
 
 
+	// Validate token/tokenSource mutual exclusivity
+	if c.Auth.Token != "" && c.Auth.TokenSource != nil {
+		errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
+	}
+
+	// Validate tokenSource if specified
+	if c.Auth.TokenSource != nil {
+		if err := c.Auth.TokenSource.Validate(); err != nil {
+			errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
+		}
+	}
+
 	if err := validateLogConfig(&c.Log); err != nil {
 	if err := validateLogConfig(&c.Log); err != nil {
 		errs = AppendError(errs, err)
 		errs = AppendError(errs, err)
 	}
 	}

+ 93 - 0
pkg/config/v1/value_source.go

@@ -0,0 +1,93 @@
+// Copyright 2025 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 v1
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+)
+
+// ValueSource provides a way to dynamically resolve configuration values
+// from various sources like files, environment variables, or external services.
+type ValueSource struct {
+	Type string      `json:"type"`
+	File *FileSource `json:"file,omitempty"`
+}
+
+// FileSource specifies how to load a value from a file.
+type FileSource struct {
+	Path string `json:"path"`
+}
+
+// Validate validates the ValueSource configuration.
+func (v *ValueSource) Validate() error {
+	if v == nil {
+		return errors.New("valueSource cannot be nil")
+	}
+
+	switch v.Type {
+	case "file":
+		if v.File == nil {
+			return errors.New("file configuration is required when type is 'file'")
+		}
+		return v.File.Validate()
+	default:
+		return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
+	}
+}
+
+// Resolve resolves the value from the configured source.
+func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
+	if err := v.Validate(); err != nil {
+		return "", err
+	}
+
+	switch v.Type {
+	case "file":
+		return v.File.Resolve(ctx)
+	default:
+		return "", fmt.Errorf("unsupported value source type: %s", v.Type)
+	}
+}
+
+// Validate validates the FileSource configuration.
+func (f *FileSource) Validate() error {
+	if f == nil {
+		return errors.New("fileSource cannot be nil")
+	}
+
+	if f.Path == "" {
+		return errors.New("file path cannot be empty")
+	}
+	return nil
+}
+
+// Resolve reads and returns the content from the specified file.
+func (f *FileSource) Resolve(_ context.Context) (string, error) {
+	if err := f.Validate(); err != nil {
+		return "", err
+	}
+
+	content, err := os.ReadFile(f.Path)
+	if err != nil {
+		return "", fmt.Errorf("failed to read file %s: %v", f.Path, err)
+	}
+
+	// Trim whitespace, which is important for file-based tokens
+	return strings.TrimSpace(string(content)), nil
+}

+ 246 - 0
pkg/config/v1/value_source_test.go

@@ -0,0 +1,246 @@
+// Copyright 2025 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 v1
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestValueSource_Validate(t *testing.T) {
+	tests := []struct {
+		name    string
+		vs      *ValueSource
+		wantErr bool
+	}{
+		{
+			name:    "nil valueSource",
+			vs:      nil,
+			wantErr: true,
+		},
+		{
+			name: "unsupported type",
+			vs: &ValueSource{
+				Type: "unsupported",
+			},
+			wantErr: true,
+		},
+		{
+			name: "file type without file config",
+			vs: &ValueSource{
+				Type: "file",
+				File: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid file type with absolute path",
+			vs: &ValueSource{
+				Type: "file",
+				File: &FileSource{
+					Path: "/tmp/test",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid file type with relative path",
+			vs: &ValueSource{
+				Type: "file",
+				File: &FileSource{
+					Path: "configs/token",
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.vs.Validate()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestFileSource_Validate(t *testing.T) {
+	tests := []struct {
+		name    string
+		fs      *FileSource
+		wantErr bool
+	}{
+		{
+			name:    "nil fileSource",
+			fs:      nil,
+			wantErr: true,
+		},
+		{
+			name: "empty path",
+			fs: &FileSource{
+				Path: "",
+			},
+			wantErr: true,
+		},
+		{
+			name: "relative path (allowed)",
+			fs: &FileSource{
+				Path: "relative/path",
+			},
+			wantErr: false,
+		},
+		{
+			name: "absolute path",
+			fs: &FileSource{
+				Path: "/absolute/path",
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.fs.Validate()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestFileSource_Resolve(t *testing.T) {
+	// Create a temporary file for testing
+	tmpDir := t.TempDir()
+	testFile := filepath.Join(tmpDir, "test_token")
+	testContent := "test-token-value\n\t "
+	expectedContent := "test-token-value"
+
+	err := os.WriteFile(testFile, []byte(testContent), 0o600)
+	if err != nil {
+		t.Fatalf("failed to create test file: %v", err)
+	}
+
+	tests := []struct {
+		name    string
+		fs      *FileSource
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "valid file path",
+			fs: &FileSource{
+				Path: testFile,
+			},
+			want:    expectedContent,
+			wantErr: false,
+		},
+		{
+			name: "non-existent file",
+			fs: &FileSource{
+				Path: "/non/existent/file",
+			},
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name: "path traversal attempt (should fail validation)",
+			fs: &FileSource{
+				Path: "../../../etc/passwd",
+			},
+			want:    "",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.fs.Resolve(context.Background())
+			if (err != nil) != tt.wantErr {
+				t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestValueSource_Resolve(t *testing.T) {
+	// Create a temporary file for testing
+	tmpDir := t.TempDir()
+	testFile := filepath.Join(tmpDir, "test_token")
+	testContent := "test-token-value"
+
+	err := os.WriteFile(testFile, []byte(testContent), 0o600)
+	if err != nil {
+		t.Fatalf("failed to create test file: %v", err)
+	}
+
+	tests := []struct {
+		name    string
+		vs      *ValueSource
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "valid file type",
+			vs: &ValueSource{
+				Type: "file",
+				File: &FileSource{
+					Path: testFile,
+				},
+			},
+			want:    testContent,
+			wantErr: false,
+		},
+		{
+			name: "unsupported type",
+			vs: &ValueSource{
+				Type: "unsupported",
+			},
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name: "file type with path traversal",
+			vs: &ValueSource{
+				Type: "file",
+				File: &FileSource{
+					Path: "../../../etc/passwd",
+				},
+			},
+			want:    "",
+			wantErr: true,
+		},
+	}
+
+	ctx := context.Background()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.vs.Resolve(ctx)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 4 - 1
pkg/ssh/server.go

@@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error {
 		s.writeToClient(err.Error())
 		s.writeToClient(err.Error())
 		return fmt.Errorf("parse flags from ssh client error: %v", err)
 		return fmt.Errorf("parse flags from ssh client error: %v", err)
 	}
 	}
-	clientCfg.Complete()
+	if err := clientCfg.Complete(); err != nil {
+		s.writeToClient(fmt.Sprintf("failed to complete client config: %v", err))
+		return fmt.Errorf("complete client config error: %v", err)
+	}
 	if sshConn.Permissions != nil {
 	if sshConn.Permissions != nil {
 		clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
 		clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
 	}
 	}

+ 29 - 7
pkg/transport/tls.go

@@ -22,6 +22,7 @@ import (
 	"encoding/pem"
 	"encoding/pem"
 	"math/big"
 	"math/big"
 	"os"
 	"os"
+	"time"
 )
 )
 
 
 func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) {
 func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) {
@@ -32,12 +33,30 @@ func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) {
 	return &tlsCert, nil
 	return &tlsCert, nil
 }
 }
 
 
-func newRandomTLSKeyPair() *tls.Certificate {
+func newRandomTLSKeyPair() (*tls.Certificate, error) {
 	key, err := rsa.GenerateKey(rand.Reader, 2048)
 	key, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		return nil, err
+	}
+
+	// Generate a random positive serial number with 128 bits of entropy.
+	// RFC 5280 requires serial numbers to be positive integers (not zero).
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return nil, err
+	}
+	// Ensure serial number is positive (not zero)
+	if serialNumber.Sign() == 0 {
+		serialNumber = big.NewInt(1)
+	}
+
+	template := x509.Certificate{
+		SerialNumber: serialNumber,
+		NotBefore:    time.Now().Add(-1 * time.Hour),
+		NotAfter:     time.Now().Add(365 * 24 * time.Hour * 10),
 	}
 	}
-	template := x509.Certificate{SerialNumber: big.NewInt(1)}
+
 	certDER, err := x509.CreateCertificate(
 	certDER, err := x509.CreateCertificate(
 		rand.Reader,
 		rand.Reader,
 		&template,
 		&template,
@@ -45,16 +64,16 @@ func newRandomTLSKeyPair() *tls.Certificate {
 		&key.PublicKey,
 		&key.PublicKey,
 		key)
 		key)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
 	}
 	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
 	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
 	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 
 
 	tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
 	tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
 	}
-	return &tlsCert
+	return &tlsCert, nil
 }
 }
 
 
 // Only support one ca file to add
 // Only support one ca file to add
@@ -76,7 +95,10 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) {
 
 
 	if certPath == "" || keyPath == "" {
 	if certPath == "" || keyPath == "" {
 		// server will generate tls conf by itself
 		// server will generate tls conf by itself
-		cert := newRandomTLSKeyPair()
+		cert, err := newRandomTLSKeyPair()
+		if err != nil {
+			return nil, err
+		}
 		base.Certificates = []tls.Certificate{*cert}
 		base.Certificates = []tls.Certificate{*cert}
 	} else {
 	} else {
 		cert, err := newCustomTLSKeyPair(certPath, keyPath)
 		cert, err := newCustomTLSKeyPair(certPath, keyPath)

+ 3 - 3
pkg/util/net/conn.go

@@ -197,11 +197,11 @@ func (statsConn *StatsConn) Close() (err error) {
 }
 }
 
 
 type wrapQuicStream struct {
 type wrapQuicStream struct {
-	quic.Stream
-	c quic.Connection
+	*quic.Stream
+	c *quic.Conn
 }
 }
 
 
-func QuicStreamToNetConn(s quic.Stream, c quic.Connection) net.Conn {
+func QuicStreamToNetConn(s *quic.Stream, c *quic.Conn) net.Conn {
 	return &wrapQuicStream{
 	return &wrapQuicStream{
 		Stream: s,
 		Stream: s,
 		c:      c,
 		c:      c,

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

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

+ 3 - 1
pkg/virtual/client.go

@@ -37,7 +37,9 @@ type Client struct {
 
 
 func NewClient(options ClientOptions) (*Client, error) {
 func NewClient(options ClientOptions) (*Client, error) {
 	if options.Common != nil {
 	if options.Common != nil {
-		options.Common.Complete()
+		if err := options.Common.Complete(); err != nil {
+			return nil, err
+		}
 	}
 	}
 
 
 	ln := netpkg.NewInternalListener()
 	ln := netpkg.NewInternalListener()

+ 2 - 2
server/service.go

@@ -262,7 +262,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
 	}
 	}
 
 
 	if cfg.SSHTunnelGateway.BindPort > 0 {
 	if cfg.SSHTunnelGateway.BindPort > 0 {
-		sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.sshTunnelListener)
+		sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.BindAddr, svr.sshTunnelListener)
 		if err != nil {
 		if err != nil {
 			return nil, fmt.Errorf("create ssh gateway error: %v", err)
 			return nil, fmt.Errorf("create ssh gateway error: %v", err)
 		}
 		}
@@ -550,7 +550,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
 			return
 			return
 		}
 		}
 		// Start a new goroutine to handle connection.
 		// Start a new goroutine to handle connection.
-		go func(ctx context.Context, frpConn quic.Connection) {
+		go func(ctx context.Context, frpConn *quic.Conn) {
 			for {
 			for {
 				stream, err := frpConn.AcceptStream(context.Background())
 				stream, err := frpConn.AcceptStream(context.Background())
 				if err != nil {
 				if err != nil {

+ 217 - 0
test/e2e/v1/basic/token_source.go

@@ -0,0 +1,217 @@
+// Copyright 2025 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 basic
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+)
+
+var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("File-based token loading", func() {
+		ginkgo.It("should work with file tokenSource", func() {
+			// Create a temporary token file
+			tmpDir := f.TempDirectory
+			tokenFile := filepath.Join(tmpDir, "test_token")
+			tokenContent := "test-token-123"
+
+			err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
+			framework.ExpectNoError(err)
+
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			portName := port.GenName("TCP")
+
+			// Server config with tokenSource
+			serverConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+`, tokenFile)
+
+			// Client config with matching token
+			clientConf += fmt.Sprintf(`
+auth.token = "%s"
+
+[[proxies]]
+name = "tcp"
+type = "tcp"
+localPort = {{ .%s }}
+remotePort = {{ .%s }}
+`, tokenContent, framework.TCPEchoServerPort, portName)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).PortName(portName).Ensure()
+		})
+
+		ginkgo.It("should work with client tokenSource", func() {
+			// Create a temporary token file
+			tmpDir := f.TempDirectory
+			tokenFile := filepath.Join(tmpDir, "client_token")
+			tokenContent := "client-token-456"
+
+			err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
+			framework.ExpectNoError(err)
+
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			portName := port.GenName("TCP")
+
+			// Server config with matching token
+			serverConf += fmt.Sprintf(`
+auth.token = "%s"
+`, tokenContent)
+
+			// Client config with tokenSource
+			clientConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+
+[[proxies]]
+name = "tcp"
+type = "tcp"
+localPort = {{ .%s }}
+remotePort = {{ .%s }}
+`, tokenFile, framework.TCPEchoServerPort, portName)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).PortName(portName).Ensure()
+		})
+
+		ginkgo.It("should work with both server and client tokenSource", func() {
+			// Create temporary token files
+			tmpDir := f.TempDirectory
+			serverTokenFile := filepath.Join(tmpDir, "server_token")
+			clientTokenFile := filepath.Join(tmpDir, "client_token")
+			tokenContent := "shared-token-789"
+
+			err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600)
+			framework.ExpectNoError(err)
+
+			err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600)
+			framework.ExpectNoError(err)
+
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			portName := port.GenName("TCP")
+
+			// Server config with tokenSource
+			serverConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+`, serverTokenFile)
+
+			// Client config with tokenSource
+			clientConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+
+[[proxies]]
+name = "tcp"
+type = "tcp"
+localPort = {{ .%s }}
+remotePort = {{ .%s }}
+`, clientTokenFile, framework.TCPEchoServerPort, portName)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).PortName(portName).Ensure()
+		})
+
+		ginkgo.It("should fail with mismatched tokens", func() {
+			// Create temporary token files with different content
+			tmpDir := f.TempDirectory
+			serverTokenFile := filepath.Join(tmpDir, "server_token")
+			clientTokenFile := filepath.Join(tmpDir, "client_token")
+
+			err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600)
+			framework.ExpectNoError(err)
+
+			err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600)
+			framework.ExpectNoError(err)
+
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			portName := port.GenName("TCP")
+
+			// Server config with tokenSource
+			serverConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+`, serverTokenFile)
+
+			// Client config with different tokenSource
+			clientConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+
+[[proxies]]
+name = "tcp"
+type = "tcp"
+localPort = {{ .%s }}
+remotePort = {{ .%s }}
+`, clientTokenFile, framework.TCPEchoServerPort, portName)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			// This should fail due to token mismatch - the client should not be able to connect
+			// We expect the request to fail because the proxy tunnel is not established
+			framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure()
+		})
+
+		ginkgo.It("should fail with non-existent token file", func() {
+			// This test verifies that server fails to start when tokenSource points to non-existent file
+			// We'll verify this by checking that the configuration loading itself fails
+
+			// Create a config that references a non-existent file
+			tmpDir := f.TempDirectory
+			nonExistentFile := filepath.Join(tmpDir, "non_existent_token")
+
+			serverConf := consts.DefaultServerConfig
+
+			// Server config with non-existent tokenSource file
+			serverConf += fmt.Sprintf(`
+auth.tokenSource.type = "file"
+auth.tokenSource.file.path = "%s"
+`, nonExistentFile)
+
+			// The test expectation is that this will fail during the RunProcesses call
+			// because the server cannot load the configuration due to missing token file
+			defer func() {
+				if r := recover(); r != nil {
+					// Expected: server should fail to start due to missing file
+					ginkgo.By(fmt.Sprintf("Server correctly failed to start: %v", r))
+				}
+			}()
+
+			// This should cause a panic or error during server startup
+			f.RunProcesses([]string{serverConf}, []string{})
+		})
+	})
+})