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 {