From 61330d4d794180c38d1f8ff7e9024b7f0f69d717 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 1 Jul 2025 18:56:46 +0800 Subject: [PATCH 1/8] Update quic-go dependency from v0.48.2 to v0.53.0 (#4862) - Update go.mod to use github.com/quic-go/quic-go v0.53.0 - Replace quic.Connection interface with *quic.Conn struct - Replace quic.Stream interface with *quic.Stream struct - Update all affected files to use new API: - pkg/util/net/conn.go: Update QuicStreamToNetConn function and wrapQuicStream struct - server/service.go: Update HandleQUICListener function parameter - client/visitor/xtcp.go: Update QUICTunnelSession struct field - client/connector.go: Update defaultConnectorImpl struct field Fixes #4852 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- client/connector.go | 2 +- client/visitor/xtcp.go | 2 +- go.mod | 3 +-- go.sum | 6 ++---- pkg/util/net/conn.go | 6 +++--- server/service.go | 2 +- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/client/connector.go b/client/connector.go index 64aa71c0..ab7c2fdd 100644 --- a/client/connector.go +++ b/client/connector.go @@ -48,7 +48,7 @@ type defaultConnectorImpl struct { cfg *v1.ClientCommonConfig muxSession *fmux.Session - quicConn quic.Connection + quicConn *quic.Conn closeOnce sync.Once } diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 51f29ad2..99d25d8a 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -398,7 +398,7 @@ func (ks *KCPTunnelSession) Close() { } type QUICTunnelSession struct { - session quic.Connection + session *quic.Conn listenConn *net.UDPConn mu sync.RWMutex diff --git a/go.mod b/go.mod index e3bdc711..46e753e2 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pion/stun/v2 v2.0.0 github.com/pires/go-proxyproto v0.7.0 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/samber/lo v1.47.0 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 @@ -68,7 +68,6 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect go.uber.org/automaxprocs v1.6.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/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/go.sum b/go.sum index a65c3033..bd044c39 100644 --- a/go.sum +++ b/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/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index ff7d1c37..20468f1b 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -197,11 +197,11 @@ func (statsConn *StatsConn) Close() (err error) { } 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{ Stream: s, c: c, diff --git a/server/service.go b/server/service.go index b9abaa80..514afb51 100644 --- a/server/service.go +++ b/server/service.go @@ -550,7 +550,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) { return } // Start a new goroutine to handle connection. - go func(ctx context.Context, frpConn quic.Connection) { + go func(ctx context.Context, frpConn *quic.Conn) { for { stream, err := frpConn.AcceptStream(context.Background()) if err != nil { From f9065a6a78f91f31ca9522209194346755ac4d87 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 3 Jul 2025 13:17:21 +0800 Subject: [PATCH 2/8] add tokenSource support for auth configuration (#4865) --- README.md | 15 ++ Release.md | 3 +- client/service.go | 11 +- cmd/frpc/sub/nathole.go | 5 +- cmd/frpc/sub/proxy.go | 10 +- cmd/frps/root.go | 5 +- conf/frpc_full_example.toml | 5 + conf/frps_full_example.toml | 5 + pkg/config/load.go | 8 +- pkg/config/v1/client.go | 30 +++- pkg/config/v1/client_test.go | 72 ++++++++- pkg/config/v1/server.go | 25 ++- pkg/config/v1/server_test.go | 72 ++++++++- pkg/config/v1/validation/client.go | 12 ++ pkg/config/v1/validation/server.go | 12 ++ pkg/config/v1/value_source.go | 93 +++++++++++ pkg/config/v1/value_source_test.go | 246 +++++++++++++++++++++++++++++ pkg/ssh/server.go | 5 +- pkg/virtual/client.go | 4 +- test/e2e/v1/basic/token_source.go | 217 +++++++++++++++++++++++++ 20 files changed, 832 insertions(+), 23 deletions(-) create mode 100644 pkg/config/v1/value_source.go create mode 100644 pkg/config/v1/value_source_test.go create mode 100644 test/e2e/v1/basic/token_source.go diff --git a/README.md b/README.md index f0ab4273..38bafab4 100644 --- a/README.md +++ b/README.md @@ -612,6 +612,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 +##### 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 When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. diff --git a/Release.md b/Release.md index 19b79d64..7ee50ea0 100644 --- a/Release.md +++ b/Release.md @@ -1,4 +1,3 @@ ## 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. \ No newline at end of file +* Support tokenSource for loading authentication tokens from files \ No newline at end of file diff --git a/client/service.go b/client/service.go index d6a12970..337f8f2b 100644 --- a/client/service.go +++ b/client/service.go @@ -88,13 +88,16 @@ type ServiceOptions struct { } // setServiceOptionsDefault sets the default values for ServiceOptions. -func setServiceOptionsDefault(options *ServiceOptions) { +func setServiceOptionsDefault(options *ServiceOptions) error { if options.Common != nil { - options.Common.Complete() + if err := options.Common.Complete(); err != nil { + return err + } } if options.ConnectorCreator == nil { options.ConnectorCreator = NewConnector } + return nil } // 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) { - setServiceOptionsDefault(&options) + if err := setServiceOptionsDefault(&options); err != nil { + return nil, err + } var webServer *httppkg.Server if options.Common.WebServer.Port > 0 { diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index fb5b0807..a07d6852 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { 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 != "" { cfg.NatHoleSTUNServer = natHoleSTUNServer diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index c5d76b1e..67bd774f 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -73,7 +73,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm Use: name, Short: fmt.Sprintf("Run frpc with a single %s proxy", name), 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 { fmt.Println(err) os.Exit(1) @@ -99,7 +102,10 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client Use: "visitor", Short: fmt.Sprintf("Run frpc with a single %s visitor", name), 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 { fmt.Println(err) os.Exit(1) diff --git a/cmd/frps/root.go b/cmd/frps/root.go index fff487d1..c1bfc880 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -70,7 +70,10 @@ var rootCmd = &cobra.Command{ "please use yaml/json/toml format instead!\n") } } else { - serverCfg.Complete() + if err := serverCfg.Complete(); err != nil { + fmt.Printf("failed to complete server config: %v\n", err) + os.Exit(1) + } svrCfg = &serverCfg } diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 7d4838cd..d8d93a3f 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -32,6 +32,11 @@ auth.method = "token" # auth token 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. # auth.oidc.clientID = "" # oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication. diff --git a/conf/frps_full_example.toml b/conf/frps_full_example.toml index a4fc2736..aba37435 100644 --- a/conf/frps_full_example.toml +++ b/conf/frps_full_example.toml @@ -105,6 +105,11 @@ auth.method = "token" # auth token 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. auth.oidc.issuer = "" # oidc audience specifies the audience OIDC tokens should contain when validated. diff --git a/pkg/config/load.go b/pkg/config/load.go index bb050b40..3852af9a 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -212,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) } } if svrCfg != nil { - svrCfg.Complete() + if err := svrCfg.Complete(); err != nil { + return nil, isLegacyFormat, err + } } return svrCfg, isLegacyFormat, nil } @@ -280,7 +282,9 @@ func LoadClientConfig(path string, strict bool) ( } if cliCfg != nil { - cliCfg.Complete() + if err := cliCfg.Complete(); err != nil { + return nil, nil, nil, isLegacyFormat, err + } } for _, c := range proxyCfgs { c.Complete(cliCfg.User) diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index d616fc0a..a830df99 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -15,6 +15,8 @@ package v1 import ( + "context" + "fmt" "os" "github.com/samber/lo" @@ -77,18 +79,21 @@ type ClientCommonConfig struct { 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.ServerPort = util.EmptyOr(c.ServerPort, 7000) c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true)) 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.Transport.Complete() c.WebServer.Complete() c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) + return nil } type ClientTransportConfig struct { @@ -184,12 +189,27 @@ type AuthClientConfig struct { // 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 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") + + // 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 { diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go index 9ff7c287..120c4fd4 100644 --- a/pkg/config/v1/client_test.go +++ b/pkg/config/v1/client_test.go @@ -15,6 +15,8 @@ package v1 import ( + "os" + "path/filepath" "testing" "github.com/samber/lo" @@ -24,7 +26,8 @@ import ( func TestClientConfigComplete(t *testing.T) { require := require.New(t) c := &ClientConfig{} - c.Complete() + err := c.Complete() + require.NoError(err) require.EqualValues("token", c.Auth.Method) 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.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") + } + }) + } +} diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 3108cd34..54aac080 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -15,6 +15,9 @@ package v1 import ( + "context" + "fmt" + "github.com/samber/lo" "github.com/fatedier/frp/pkg/config/types" @@ -98,8 +101,10 @@ type ServerConfig struct { 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.Transport.Complete() c.WebServer.Complete() @@ -120,17 +125,31 @@ func (c *ServerConfig) Complete() { c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24) + return nil } type AuthServerConfig struct { Method AuthMethod `json:"method,omitempty"` AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` Token string `json:"token,omitempty"` + TokenSource *ValueSource `json:"tokenSource,omitempty"` OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` } -func (c *AuthServerConfig) Complete() { +func (c *AuthServerConfig) Complete() error { 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 { diff --git a/pkg/config/v1/server_test.go b/pkg/config/v1/server_test.go index 3100fc4b..21d18fb7 100644 --- a/pkg/config/v1/server_test.go +++ b/pkg/config/v1/server_test.go @@ -15,6 +15,8 @@ package v1 import ( + "os" + "path/filepath" "testing" "github.com/samber/lo" @@ -24,9 +26,77 @@ import ( func TestServerConfigComplete(t *testing.T) { require := require.New(t) c := &ServerConfig{} - c.Complete() + err := c.Complete() + require.NoError(err) require.EqualValues("token", c.Auth.Method) require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) 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") + } + }) + } +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index bae21fda..0c8575c9 100644 --- a/pkg/config/v1/validation/client.go +++ b/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)) } + // 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 { errs = AppendError(errs, err) } diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go index cdb80ea3..56942272 100644 --- a/pkg/config/v1/validation/server.go +++ b/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)) } + // 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 { errs = AppendError(errs, err) } diff --git a/pkg/config/v1/value_source.go b/pkg/config/v1/value_source.go new file mode 100644 index 00000000..624a2658 --- /dev/null +++ b/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 +} diff --git a/pkg/config/v1/value_source_test.go b/pkg/config/v1/value_source_test.go new file mode 100644 index 00000000..685151f4 --- /dev/null +++ b/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) + } + }) + } +} diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index 84b744fb..378c6098 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error { s.writeToClient(err.Error()) 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 { clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) } diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go index 96835a48..8fec28c8 100644 --- a/pkg/virtual/client.go +++ b/pkg/virtual/client.go @@ -37,7 +37,9 @@ type Client struct { func NewClient(options ClientOptions) (*Client, error) { if options.Common != nil { - options.Common.Complete() + if err := options.Common.Complete(); err != nil { + return nil, err + } } ln := netpkg.NewInternalListener() diff --git a/test/e2e/v1/basic/token_source.go b/test/e2e/v1/basic/token_source.go new file mode 100644 index 00000000..95bb8dd4 --- /dev/null +++ b/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{}) + }) + }) +}) From c3bf952d8f49dfb23ef9dfc430cec1d9dab1c967 Mon Sep 17 00:00:00 2001 From: maguowei Date: Thu, 24 Jul 2025 10:16:44 +0800 Subject: [PATCH 3/8] fix webserver port not being released on frpc svr.Close() (#4896) --- client/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/service.go b/client/service.go index 337f8f2b..9e1833b9 100644 --- a/client/service.go +++ b/client/service.go @@ -403,6 +403,10 @@ func (svr *Service) stop() { svr.ctl.GracefulClose(svr.gracefulShutdownDuration) svr.ctl = nil } + if svr.webServer != nil { + svr.webServer.Close() + svr.webServer = nil + } } func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { From 7fe295f4f4f8edf7f5b1a8b7452a9f8dec4b85f0 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 25 Jul 2025 17:10:32 +0800 Subject: [PATCH 4/8] update golangci-lint version (#4897) --- .github/workflows/golangci-lint.yml | 12 +----------- .golangci.yml | 3 +++ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 40aba1ce..d4faac70 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -23,14 +23,4 @@ jobs: uses: golangci/golangci-lint-action@v8 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: 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 diff --git a/.golangci.yml b/.golangci.yml index 09848bc7..3ba2c60f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,6 +73,9 @@ linters: - linters: - revive text: unused-parameter + - linters: + - revive + text: "avoid meaningless package names" - linters: - unparam text: is always false From e6dacf3a67c127c005f5e7416bf87ec887f47e15 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 28 Jul 2025 15:19:56 +0800 Subject: [PATCH 5/8] Fix SSH tunnel gateway binding address issue #4900 (#4902) - Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr - This caused external connections to fail when proxyBindAddr was set to 127.0.0.1 - SSH tunnel gateway now correctly binds to bindAddr for external accessibility - Update Release.md with bug fix description Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- Release.md | 6 +++++- server/service.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Release.md b/Release.md index 7ee50ea0..9e1ce466 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,7 @@ ## Features -* Support tokenSource for loading authentication tokens from files \ No newline at end of file +* 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 diff --git a/server/service.go b/server/service.go index 514afb51..fad0e143 100644 --- a/server/service.go +++ b/server/service.go @@ -262,7 +262,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { } 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 { return nil, fmt.Errorf("create ssh gateway error: %v", err) } From dc3bc9182c403c897830c0366c720c691df75545 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 8 Aug 2025 22:28:17 +0800 Subject: [PATCH 6/8] update sponsor info (#4917) --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 38bafab4..baf9c734 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,36 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + +
+ Warp, the intelligent terminal +
+ Available for macOS, Linux and Windows +
+

+
+ The complete IDE crafted for professional Go developers

+
+ Secure and Elastic Infrastructure for Running Your AI-Generated Code

+
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy

From 024e4f5f1dd0a408f588ce53eecbc0851613f134 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 10 Aug 2025 22:59:28 +0800 Subject: [PATCH 7/8] improve random TLS certificate generation (#4923) --- Release.md | 4 ++-- pkg/transport/tls.go | 36 +++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Release.md b/Release.md index 9e1ce466..5b2724d8 100644 --- a/Release.md +++ b/Release.md @@ -1,7 +1,7 @@ ## Features -* Support tokenSource for loading authentication tokens from files +* 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 +* 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. diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index 5bc75921..e8d2bf48 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -22,6 +22,7 @@ import ( "encoding/pem" "math/big" "os" + "time" ) func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { @@ -32,12 +33,30 @@ func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { return &tlsCert, nil } -func newRandomTLSKeyPair() *tls.Certificate { +func newRandomTLSKeyPair() (*tls.Certificate, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - panic(err) + return nil, err } - template := x509.Certificate{SerialNumber: big.NewInt(1)} + + // 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), + } + certDER, err := x509.CreateCertificate( rand.Reader, &template, @@ -45,16 +64,16 @@ func newRandomTLSKeyPair() *tls.Certificate { &key.PublicKey, key) if err != nil { - panic(err) + return nil, err } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { - panic(err) + return nil, err } - return &tlsCert + return &tlsCert, nil } // Only support one ca file to add @@ -76,7 +95,10 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { if certPath == "" || keyPath == "" { // server will generate tls conf by itself - cert := newRandomTLSKeyPair() + cert, err := newRandomTLSKeyPair() + if err != nil { + return nil, err + } base.Certificates = []tls.Certificate{*cert} } else { cert, err := newCustomTLSKeyPair(certPath, keyPath) From f795950742a9edb8174cf0cdf97e53eb49865914 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 10 Aug 2025 23:11:50 +0800 Subject: [PATCH 8/8] bump version to v0.64.0 (#4924) --- pkg/util/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 966a942f..c6497e14 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.63.0" +var version = "0.64.0" func Full() string { return version