diff --git a/.circleci/config.yml b/.circleci/config.yml index ede1f5fe..017d75c9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: go-version-latest: docker: - - image: cimg/go:1.23-node + - image: cimg/go:1.24-node resource_class: large steps: - checkout diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d4faac70..eb631c24 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v8 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index d55913fd..652d156c 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - name: Make All run: | diff --git a/README.md b/README.md index baf9c734..00a3498c 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,18 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + Recall.ai - API for meeting recordings
+
+ If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. +
+


- Warp, the intelligent terminal + Warp, built for collaborating with AI Agents
Available for macOS, Linux and Windows
@@ -519,7 +526,7 @@ name = "ssh" type = "tcp" localIP = "127.0.0.1" localPort = 22 -remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}" +remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }} ``` With the config above, variables can be passed into `frpc` program like this: diff --git a/README_zh.md b/README_zh.md index d911e152..ea63d726 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,19 +15,43 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

Gold Sponsors

+

+ + Recall.ai - API for meeting recordings
+
+ If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. +
+

+

+ + +
+ Warp, built for collaborating with AI Agents +
+ 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

diff --git a/Release.md b/Release.md index 5b2724d8..2ea047fa 100644 --- a/Release.md +++ b/Release.md @@ -1,7 +1,5 @@ ## Features -* 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. +* Add NAT traversal configuration options for XTCP proxies and visitors. Support disabling assisted addresses to avoid using slow VPN connections during NAT hole punching. +* Enhanced OIDC client configuration with support for custom TLS certificate verification and proxy settings. Added `trustedCaFile`, `insecureSkipVerify`, and `proxyURL` options for OIDC token endpoint connections. +* Added detailed Prometheus metrics with `proxy_counts_detailed` metric that includes both proxy type and proxy name labels, enabling monitoring of individual proxy connections instead of just aggregate counts. diff --git a/client/connector.go b/client/connector.go index ab7c2fdd..03d7fc29 100644 --- a/client/connector.go +++ b/client/connector.go @@ -17,7 +17,6 @@ package client import ( "context" "crypto/tls" - "io" "net" "strconv" "strings" @@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error { fmuxCfg := fmux.DefaultConfig() fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xl) fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Client(conn, fmuxCfg) if err != nil { diff --git a/client/control.go b/client/control.go index 0dd70b8c..4bd6a2f7 100644 --- a/client/control.go +++ b/client/control.go @@ -276,10 +276,12 @@ func (ctl *Control) heartbeatWorker() { } func (ctl *Control) worker() { + xl := ctl.xl go ctl.heartbeatWorker() go ctl.msgDispatcher.Run() <-ctl.msgDispatcher.Done() + xl.Debugf("control message dispatcher exited") ctl.closeSession() ctl.pm.Close() diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 31f9ac89..6e1deac3 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -64,11 +64,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}) + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts) if err != nil { xl.Warnf("nathole prepare error: %v", err) return } + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) defer prepareResult.ListenConn.Close() diff --git a/client/service.go b/client/service.go index 9e1833b9..f906e4d0 100644 --- a/client/service.go +++ b/client/service.go @@ -149,9 +149,15 @@ func NewService(options ServiceOptions) (*Service, error) { } webServer = ws } + + authSetter, err := auth.NewAuthSetter(options.Common.Auth) + if err != nil { + return nil, err + } + s := &Service{ ctx: context.Background(), - authSetter: auth.NewAuthSetter(options.Common.Auth), + authSetter: authSetter, webServer: webServer, common: options.Common, configFilePath: options.ConfigFilePath, diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 99d25d8a..353577db 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -145,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { return case <-ticker.C: xl.Debugf("keepTunnelOpenWorker try to check tunnel...") - conn, err := sv.getTunnelConn() + conn, err := sv.getTunnelConn(sv.ctx) if err != nil { xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err) _ = sv.retryLimiter.Wait(sv.ctx) @@ -161,9 +161,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) - isConnTransfered := false + isConnTransferred := false defer func() { - if !isConnTransfered { + if !isConnTransferred { userConn.Close() } }() @@ -172,7 +172,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { // Open a tunnel connection to the server. If there is already a successful hole-punching connection, // it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout. - ctx := context.Background() + ctx := sv.ctx if sv.cfg.FallbackTo != "" { timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond) defer cancel() @@ -191,7 +191,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) return } - isConnTransfered = true + isConnTransferred = true return } @@ -219,40 +219,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { // openTunnel will open a tunnel connection to the target server. func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) { xl := xlog.FromContextSafe(sv.ctx) - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() - timeoutC := time.After(20 * time.Second) - immediateTrigger := make(chan struct{}, 1) - defer close(immediateTrigger) - immediateTrigger <- struct{}{} + timer := time.NewTimer(0) + defer timer.Stop() for { select { case <-sv.ctx.Done(): return nil, sv.ctx.Err() case <-ctx.Done(): - return nil, ctx.Err() - case <-immediateTrigger: - conn, err = sv.getTunnelConn() - case <-ticker.C: - conn, err = sv.getTunnelConn() - case <-timeoutC: - return nil, fmt.Errorf("open tunnel timeout") - } - - if err != nil { - if err != ErrNoTunnelSession { - xl.Warnf("get tunnel connection error: %v", err) + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, fmt.Errorf("open tunnel timeout") } - continue + return nil, ctx.Err() + case <-timer.C: + conn, err = sv.getTunnelConn(ctx) + if err != nil { + if !errors.Is(err, ErrNoTunnelSession) { + xl.Warnf("get tunnel connection error: %v", err) + } + timer.Reset(500 * time.Millisecond) + continue + } + return conn, nil } - return conn, nil } } -func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) { - conn, err := sv.session.OpenConn(sv.ctx) +func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) { + conn, err := sv.session.OpenConn(ctx) if err == nil { return conn, nil } @@ -279,11 +276,19 @@ func (sv *XTCPVisitor) makeNatHole() { } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}) + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts) if err != nil { xl.Warnf("nathole prepare error: %v", err) return } + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index d8d93a3f..6b86907e 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -55,6 +55,20 @@ auth.token = "12345678" # auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" # auth.oidc.additionalEndpointParams.var1 = "foobar" +# OIDC TLS and proxy configuration +# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate. +# This is useful when the OIDC provider uses a self-signed certificate or a custom CA. +# auth.oidc.trustedCaFile = "/path/to/ca.crt" + +# Skip TLS certificate verification for the OIDC token endpoint. +# INSECURE: Only use this for debugging purposes, not recommended for production. +# auth.oidc.insecureSkipVerify = false + +# Specify a proxy server for OIDC token endpoint connections. +# Supports http, https, socks5, and socks5h proxy protocols. +# If not specified, no proxy is used for OIDC connections. +# auth.oidc.proxyURL = "http://proxy.example.com:8080" + # Set admin address for control frpc's action by http api such as reload webServer.addr = "127.0.0.1" webServer.port = 7400 @@ -372,6 +386,14 @@ localPort = 22 # Otherwise, visitors from same user can connect. '*' means allow all users. allowUsers = ["user1", "user2"] +# NAT traversal configuration (optional) +[proxies.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# This can improve performance when you have slow VPN connections. +# Default: false +disableAssistedAddrs = false + [[proxies]] name = "vnet-server" type = "stcp" @@ -411,6 +433,13 @@ minRetryInterval = 90 # fallbackTo = "stcp_visitor" # fallbackTimeoutMs = 500 +# NAT traversal configuration (optional) +[visitors.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# Default: false +disableAssistedAddrs = false + [[visitors]] name = "vnet-visitor" type = "stcp" diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index d8d4437a..7d77a26d 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -1,4 +1,4 @@ -FROM golang:1.23 AS building +FROM golang:1.24 AS building COPY . /building WORKDIR /building diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 00fb51a8..489ce0f5 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -1,4 +1,4 @@ -FROM golang:1.23 AS building +FROM golang:1.24 AS building COPY . /building WORKDIR /building diff --git a/go.mod b/go.mod index 46e753e2..af633af4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.23.0 +go 1.24.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 @@ -82,4 +82,4 @@ require ( ) // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. -replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d +replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 diff --git a/go.sum b/go.sum index bd044c39..a411ff30 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= -github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo= -github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= +github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index ae706986..b954fc80 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -27,16 +27,19 @@ type Setter interface { SetNewWorkConn(*msg.NewWorkConn) error } -func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { +func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) { switch cfg.Method { case v1.AuthMethodToken: authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: - authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + if err != nil { + return nil, err + } default: - panic(fmt.Sprintf("wrong method: '%s'", cfg.Method)) + return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method) } - return authProvider + return authProvider, nil } type Verifier interface { diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 40ce060f..c5f63640 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -16,23 +16,72 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" + "net/url" + "os" "slices" "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) +// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests +func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) { + // Clone the default transport to get all reasonable defaults + transport := http.DefaultTransport.(*http.Transport).Clone() + + // Configure TLS settings + if trustedCAFile != "" || insecureSkipVerify { + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + + if trustedCAFile != "" && !insecureSkipVerify { + caCert, err := os.ReadFile(trustedCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile) + } + + tlsConfig.RootCAs = caCertPool + } + transport.TLSClientConfig = tlsConfig + } + + // Configure proxy settings + if proxyURL != "" { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err) + } + transport.Proxy = http.ProxyURL(parsedURL) + } else { + // Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment + transport.Proxy = nil + } + + return &http.Client{Transport: transport}, nil +} + type OidcAuthProvider struct { additionalAuthScopes []v1.AuthScope tokenGenerator *clientcredentials.Config + httpClient *http.Client } -func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider { +func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) { eps := make(map[string][]string) for k, v := range cfg.AdditionalEndpointParams { eps[k] = []string{v} @@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien EndpointParams: eps, } + // Create custom HTTP client if needed + var httpClient *http.Client + if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" { + var err error + httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) + if err != nil { + return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err) + } + } + return &OidcAuthProvider{ additionalAuthScopes: additionalAuthScopes, tokenGenerator: tokenGenerator, - } + httpClient: httpClient, + }, nil } func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { - tokenObj, err := auth.tokenGenerator.Token(context.Background()) + ctx := context.Background() + if auth.httpClient != nil { + ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient) + } + + tokenObj, err := auth.tokenGenerator.Token(ctx) if err != nil { return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) } diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index a830df99..c6cf97a6 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -228,6 +228,17 @@ type AuthOIDCClientConfig struct { // AdditionalEndpointParams specifies additional parameters to be sent // this field will be transfer to map[string][]string in OIDC token generator. AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` + + // TrustedCaFile specifies the path to a custom CA certificate file + // for verifying the OIDC token endpoint's TLS certificate. + TrustedCaFile string `json:"trustedCaFile,omitempty"` + // InsecureSkipVerify disables TLS certificate verification for the + // OIDC token endpoint. Only use this for debugging, not recommended for production. + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + // ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint. + // Supports http, https, socks5, and socks5h proxy protocols. + // If empty, no proxy is used for OIDC connections. + ProxyURL string `json:"proxyURL,omitempty"` } type VirtualNetConfig struct { diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index ddb23356..89898554 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() { } type TLSConfig struct { - // CertPath specifies the path of the cert file that client will load. + // CertFile specifies the path of the cert file that client will load. CertFile string `json:"certFile,omitempty"` - // KeyPath specifies the path of the secret key file that client will load. + // KeyFile specifies the path of the secret key file that client will load. KeyFile string `json:"keyFile,omitempty"` // TrustedCaFile specifies the path of the trusted ca file that will load. TrustedCaFile string `json:"trustedCaFile,omitempty"` @@ -96,6 +96,14 @@ type TLSConfig struct { ServerName string `json:"serverName,omitempty"` } +// NatTraversalConfig defines configuration options for NAT traversal +type NatTraversalConfig struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal. When enabled, + // only STUN-discovered public addresses will be used. + DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"` +} + type LogConfig struct { // This is destination where frp should write the logs. // If "console" is used, logs will be printed to stdout, otherwise, diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 34bd7125..37701b6d 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -422,6 +422,9 @@ type XTCPProxyConfig struct { Secretkey string `json:"secretKey,omitempty"` AllowUsers []string `json:"allowUsers,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` } func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index f00391c3..7629875a 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -160,6 +160,9 @@ type XTCPVisitorConfig struct { MinRetryInterval int `json:"minRetryInterval,omitempty"` FallbackTo string `json:"fallbackTo,omitempty"` FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` } func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { diff --git a/pkg/metrics/prometheus/server.go b/pkg/metrics/prometheus/server.go index 56dea6e8..a99bb1d5 100644 --- a/pkg/metrics/prometheus/server.go +++ b/pkg/metrics/prometheus/server.go @@ -14,11 +14,12 @@ const ( var ServerMetrics metrics.ServerMetrics = newServerMetrics() type serverMetrics struct { - clientCount prometheus.Gauge - proxyCount *prometheus.GaugeVec - connectionCount *prometheus.GaugeVec - trafficIn *prometheus.CounterVec - trafficOut *prometheus.CounterVec + clientCount prometheus.Gauge + proxyCount *prometheus.GaugeVec + proxyCountDetailed *prometheus.GaugeVec + connectionCount *prometheus.GaugeVec + trafficIn *prometheus.CounterVec + trafficOut *prometheus.CounterVec } func (m *serverMetrics) NewClient() { @@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() { m.clientCount.Dec() } -func (m *serverMetrics) NewProxy(_ string, proxyType string) { +func (m *serverMetrics) NewProxy(name string, proxyType string) { m.proxyCount.WithLabelValues(proxyType).Inc() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc() } -func (m *serverMetrics) CloseProxy(_ string, proxyType string) { +func (m *serverMetrics) CloseProxy(name string, proxyType string) { m.proxyCount.WithLabelValues(proxyType).Dec() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec() } func (m *serverMetrics) OpenConnection(name string, proxyType string) { @@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics { Name: "proxy_counts", Help: "The current proxy counts", }, []string{"type"}), + proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "proxy_counts_detailed", + Help: "The current number of proxies grouped by type and name", + }, []string{"type", "name"}), connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: serverSubsystem, @@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics { } prometheus.MustRegister(m.clientCount) prometheus.MustRegister(m.proxyCount) + prometheus.MustRegister(m.proxyCountDetailed) prometheus.MustRegister(m.connectionCount) prometheus.MustRegister(m.trafficIn) prometheus.MustRegister(m.trafficOut) diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index bdd0ee83..72522fac 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -68,6 +68,13 @@ var ( DetectRoleReceiver = "receiver" ) +// PrepareOptions defines options for NAT traversal preparation +type PrepareOptions struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal + DisableAssistedAddrs bool +} + type PrepareResult struct { Addrs []string AssistedAddrs []string @@ -108,7 +115,7 @@ func PreCheck( } // Prepare is used to do some preparation work before penetration. -func Prepare(stunServers []string) (*PrepareResult, error) { +func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) { // discover for Nat type addrs, localAddr, err := Discover(stunServers, "") if err != nil { @@ -133,9 +140,13 @@ func Prepare(stunServers []string) (*PrepareResult, error) { return nil, fmt.Errorf("listen local udp addr error: %v", err) } - assistedAddrs := make([]string, 0, len(localIPs)) - for _, ip := range localIPs { - assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) + // Apply NAT traversal options + var assistedAddrs []string + if !opts.DisableAssistedAddrs { + assistedAddrs = make([]string, 0, len(localIPs)) + for _, ip := range localIPs { + assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) + } } return &PrepareResult{ Addrs: addrs, diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index c6497e14..0f4ec433 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.64.0" +var version = "0.65.0" func Full() string { return version diff --git a/pkg/util/xlog/log_writer.go b/pkg/util/xlog/log_writer.go new file mode 100644 index 00000000..3fff7324 --- /dev/null +++ b/pkg/util/xlog/log_writer.go @@ -0,0 +1,65 @@ +// 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 xlog + +import "strings" + +// LogWriter forwards writes to frp's logger at configurable level. +// It is safe for concurrent use as long as the underlying Logger is thread-safe. +type LogWriter struct { + xl *Logger + logFunc func(string) +} + +func (w LogWriter) Write(p []byte) (n int, err error) { + msg := strings.TrimSpace(string(p)) + w.logFunc(msg) + return len(p), nil +} + +func NewTraceWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Tracef("%s", msg) }, + } +} + +func NewDebugWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Debugf("%s", msg) }, + } +} + +func NewInfoWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Infof("%s", msg) }, + } +} + +func NewWarnWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Warnf("%s", msg) }, + } +} + +func NewErrorWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Errorf("%s", msg) }, + } +} diff --git a/server/service.go b/server/service.go index fad0e143..7ca80dc8 100644 --- a/server/service.go +++ b/server/service.go @@ -19,7 +19,6 @@ import ( "context" "crypto/tls" "fmt" - "io" "net" "net/http" "os" @@ -516,7 +515,8 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal { fmuxCfg := fmux.DefaultConfig() fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xlog.FromContextSafe(ctx)) fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Server(frpConn, fmuxCfg) if err != nil {