Merge pull request #4999 from fatedier/dev

bump version
This commit is contained in:
fatedier
2025-09-25 20:23:42 +08:00
committed by GitHub
27 changed files with 329 additions and 71 deletions

View File

@@ -2,7 +2,7 @@ version: 2
jobs: jobs:
go-version-latest: go-version-latest:
docker: docker:
- image: cimg/go:1.23-node - image: cimg/go:1.24-node
resource_class: large resource_class: large
steps: steps:
- checkout - checkout

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
cache: false cache: false
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v8

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Make All - name: Make All
run: | run: |

View File

@@ -13,11 +13,18 @@ frp is an open source project with its ongoing development made possible entirel
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center">
<a href="https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp" target="_blank">
<b>Recall.ai - API for meeting recordings</b><br>
<br>
<sup>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.</sup>
</a>
</p>
<p align="center"> <p align="center">
<a href="https://go.warp.dev/frp" target="_blank"> <a href="https://go.warp.dev/frp" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png"> <img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
<br> <br>
<b>Warp, the intelligent terminal</b> <b>Warp, built for collaborating with AI Agents</b>
<br> <br>
<sub>Available for macOS, Linux and Windows</sub> <sub>Available for macOS, Linux and Windows</sub>
</a> </a>
@@ -519,7 +526,7 @@ name = "ssh"
type = "tcp" type = "tcp"
localIP = "127.0.0.1" localIP = "127.0.0.1"
localPort = 22 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: With the config above, variables can be passed into `frpc` program like this:

View File

@@ -15,19 +15,43 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center">
<a href="https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp" target="_blank">
<b>Recall.ai - API for meeting recordings</b><br>
<br>
<sup>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.</sup>
</a>
</p>
<p align="center">
<a href="https://go.warp.dev/frp" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
<br>
<b>Warp, built for collaborating with AI Agents</b>
<br>
<sub>Available for macOS, Linux and Windows</sub>
</a>
</p>
<p align="center"> <p align="center">
<a href="https://jb.gg/frp" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
<br>
<b>The complete IDE crafted for professional Go developers</b>
</a> </a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank"> <a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
<br>
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
</a> </a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/beclab/Olares" target="_blank"> <a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a> </a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->

View File

@@ -1,7 +1,5 @@
## Features ## Features
* Support tokenSource for loading authentication tokens from files. * 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.
## Fixes * 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.
* 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.

View File

@@ -17,7 +17,6 @@ package client
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"io"
"net" "net"
"strconv" "strconv"
"strings" "strings"
@@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second 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 fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Client(conn, fmuxCfg) session, err := fmux.Client(conn, fmuxCfg)
if err != nil { if err != nil {

View File

@@ -276,10 +276,12 @@ func (ctl *Control) heartbeatWorker() {
} }
func (ctl *Control) worker() { func (ctl *Control) worker() {
xl := ctl.xl
go ctl.heartbeatWorker() go ctl.heartbeatWorker()
go ctl.msgDispatcher.Run() go ctl.msgDispatcher.Run()
<-ctl.msgDispatcher.Done() <-ctl.msgDispatcher.Done()
xl.Debugf("control message dispatcher exited")
ctl.closeSession() ctl.closeSession()
ctl.pm.Close() ctl.pm.Close()

View File

@@ -64,11 +64,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
} }
xl.Tracef("nathole prepare start") 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 { if err != nil {
xl.Warnf("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
defer prepareResult.ListenConn.Close() defer prepareResult.ListenConn.Close()

View File

@@ -149,9 +149,15 @@ func NewService(options ServiceOptions) (*Service, error) {
} }
webServer = ws webServer = ws
} }
authSetter, err := auth.NewAuthSetter(options.Common.Auth)
if err != nil {
return nil, err
}
s := &Service{ s := &Service{
ctx: context.Background(), ctx: context.Background(),
authSetter: auth.NewAuthSetter(options.Common.Auth), authSetter: authSetter,
webServer: webServer, webServer: webServer,
common: options.Common, common: options.Common,
configFilePath: options.ConfigFilePath, configFilePath: options.ConfigFilePath,

View File

@@ -145,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
return return
case <-ticker.C: case <-ticker.C:
xl.Debugf("keepTunnelOpenWorker try to check tunnel...") xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
conn, err := sv.getTunnelConn() conn, err := sv.getTunnelConn(sv.ctx)
if err != nil { if err != nil {
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err) xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
_ = sv.retryLimiter.Wait(sv.ctx) _ = sv.retryLimiter.Wait(sv.ctx)
@@ -161,9 +161,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
func (sv *XTCPVisitor) handleConn(userConn net.Conn) { func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
isConnTransfered := false isConnTransferred := false
defer func() { defer func() {
if !isConnTransfered { if !isConnTransferred {
userConn.Close() 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, // 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. // 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 != "" { if sv.cfg.FallbackTo != "" {
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond) timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
defer cancel() 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) xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return return
} }
isConnTransfered = true isConnTransferred = true
return return
} }
@@ -219,40 +219,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
// openTunnel will open a tunnel connection to the target server. // openTunnel will open a tunnel connection to the target server.
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) { func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
ticker := time.NewTicker(500 * time.Millisecond) ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer ticker.Stop() defer cancel()
timeoutC := time.After(20 * time.Second) timer := time.NewTimer(0)
immediateTrigger := make(chan struct{}, 1) defer timer.Stop()
defer close(immediateTrigger)
immediateTrigger <- struct{}{}
for { for {
select { select {
case <-sv.ctx.Done(): case <-sv.ctx.Done():
return nil, sv.ctx.Err() return nil, sv.ctx.Err()
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() if errors.Is(ctx.Err(), context.DeadlineExceeded) {
case <-immediateTrigger: return nil, fmt.Errorf("open tunnel timeout")
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)
} }
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) { func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
conn, err := sv.session.OpenConn(sv.ctx) conn, err := sv.session.OpenConn(ctx)
if err == nil { if err == nil {
return conn, nil return conn, nil
} }
@@ -279,11 +276,19 @@ func (sv *XTCPVisitor) makeNatHole() {
} }
xl.Tracef("nathole prepare start") 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 { if err != nil {
xl.Warnf("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)

View File

@@ -55,6 +55,20 @@ auth.token = "12345678"
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" # auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
# auth.oidc.additionalEndpointParams.var1 = "foobar" # 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 # Set admin address for control frpc's action by http api such as reload
webServer.addr = "127.0.0.1" webServer.addr = "127.0.0.1"
webServer.port = 7400 webServer.port = 7400
@@ -372,6 +386,14 @@ localPort = 22
# Otherwise, visitors from same user can connect. '*' means allow all users. # Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["user1", "user2"] 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]] [[proxies]]
name = "vnet-server" name = "vnet-server"
type = "stcp" type = "stcp"
@@ -411,6 +433,13 @@ minRetryInterval = 90
# fallbackTo = "stcp_visitor" # fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500 # 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]] [[visitors]]
name = "vnet-visitor" name = "vnet-visitor"
type = "stcp" type = "stcp"

View File

@@ -1,4 +1,4 @@
FROM golang:1.23 AS building FROM golang:1.24 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building

View File

@@ -1,4 +1,4 @@
FROM golang:1.23 AS building FROM golang:1.24 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building

4
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/fatedier/frp module github.com/fatedier/frp
go 1.23.0 go 1.24.0
require ( require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 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. // 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

4
go.sum
View File

@@ -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/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 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= 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-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 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 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 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= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=

View File

@@ -27,16 +27,19 @@ type Setter interface {
SetNewWorkConn(*msg.NewWorkConn) error SetNewWorkConn(*msg.NewWorkConn) error
} }
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
switch cfg.Method { switch cfg.Method {
case v1.AuthMethodToken: case v1.AuthMethodToken:
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC: case v1.AuthMethodOIDC:
authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
if err != nil {
return nil, err
}
default: 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 { type Verifier interface {

View File

@@ -16,23 +16,72 @@ package auth
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net/http"
"net/url"
"os"
"slices" "slices"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "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 { type OidcAuthProvider struct {
additionalAuthScopes []v1.AuthScope additionalAuthScopes []v1.AuthScope
tokenGenerator *clientcredentials.Config 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) eps := make(map[string][]string)
for k, v := range cfg.AdditionalEndpointParams { for k, v := range cfg.AdditionalEndpointParams {
eps[k] = []string{v} eps[k] = []string{v}
@@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
EndpointParams: eps, 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{ return &OidcAuthProvider{
additionalAuthScopes: additionalAuthScopes, additionalAuthScopes: additionalAuthScopes,
tokenGenerator: tokenGenerator, tokenGenerator: tokenGenerator,
} httpClient: httpClient,
}, nil
} }
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { 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 { if err != nil {
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
} }

View File

@@ -228,6 +228,17 @@ type AuthOIDCClientConfig struct {
// AdditionalEndpointParams specifies additional parameters to be sent // AdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator. // this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` 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 { type VirtualNetConfig struct {

View File

@@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() {
} }
type TLSConfig struct { 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"` 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"` KeyFile string `json:"keyFile,omitempty"`
// TrustedCaFile specifies the path of the trusted ca file that will load. // TrustedCaFile specifies the path of the trusted ca file that will load.
TrustedCaFile string `json:"trustedCaFile,omitempty"` TrustedCaFile string `json:"trustedCaFile,omitempty"`
@@ -96,6 +96,14 @@ type TLSConfig struct {
ServerName string `json:"serverName,omitempty"` 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 { type LogConfig struct {
// This is destination where frp should write the logs. // This is destination where frp should write the logs.
// If "console" is used, logs will be printed to stdout, otherwise, // If "console" is used, logs will be printed to stdout, otherwise,

View File

@@ -422,6 +422,9 @@ type XTCPProxyConfig struct {
Secretkey string `json:"secretKey,omitempty"` Secretkey string `json:"secretKey,omitempty"`
AllowUsers []string `json:"allowUsers,omitempty"` AllowUsers []string `json:"allowUsers,omitempty"`
// NatTraversal configuration for NAT traversal
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
} }
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {

View File

@@ -160,6 +160,9 @@ type XTCPVisitorConfig struct {
MinRetryInterval int `json:"minRetryInterval,omitempty"` MinRetryInterval int `json:"minRetryInterval,omitempty"`
FallbackTo string `json:"fallbackTo,omitempty"` FallbackTo string `json:"fallbackTo,omitempty"`
FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"`
// NatTraversal configuration for NAT traversal
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
} }
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {

View File

@@ -14,11 +14,12 @@ const (
var ServerMetrics metrics.ServerMetrics = newServerMetrics() var ServerMetrics metrics.ServerMetrics = newServerMetrics()
type serverMetrics struct { type serverMetrics struct {
clientCount prometheus.Gauge clientCount prometheus.Gauge
proxyCount *prometheus.GaugeVec proxyCount *prometheus.GaugeVec
connectionCount *prometheus.GaugeVec proxyCountDetailed *prometheus.GaugeVec
trafficIn *prometheus.CounterVec connectionCount *prometheus.GaugeVec
trafficOut *prometheus.CounterVec trafficIn *prometheus.CounterVec
trafficOut *prometheus.CounterVec
} }
func (m *serverMetrics) NewClient() { func (m *serverMetrics) NewClient() {
@@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() {
m.clientCount.Dec() m.clientCount.Dec()
} }
func (m *serverMetrics) NewProxy(_ string, proxyType string) { func (m *serverMetrics) NewProxy(name string, proxyType string) {
m.proxyCount.WithLabelValues(proxyType).Inc() 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.proxyCount.WithLabelValues(proxyType).Dec()
m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec()
} }
func (m *serverMetrics) OpenConnection(name string, proxyType string) { func (m *serverMetrics) OpenConnection(name string, proxyType string) {
@@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics {
Name: "proxy_counts", Name: "proxy_counts",
Help: "The current proxy counts", Help: "The current proxy counts",
}, []string{"type"}), }, []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{ connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace, Namespace: namespace,
Subsystem: serverSubsystem, Subsystem: serverSubsystem,
@@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics {
} }
prometheus.MustRegister(m.clientCount) prometheus.MustRegister(m.clientCount)
prometheus.MustRegister(m.proxyCount) prometheus.MustRegister(m.proxyCount)
prometheus.MustRegister(m.proxyCountDetailed)
prometheus.MustRegister(m.connectionCount) prometheus.MustRegister(m.connectionCount)
prometheus.MustRegister(m.trafficIn) prometheus.MustRegister(m.trafficIn)
prometheus.MustRegister(m.trafficOut) prometheus.MustRegister(m.trafficOut)

View File

@@ -68,6 +68,13 @@ var (
DetectRoleReceiver = "receiver" 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 { type PrepareResult struct {
Addrs []string Addrs []string
AssistedAddrs []string AssistedAddrs []string
@@ -108,7 +115,7 @@ func PreCheck(
} }
// Prepare is used to do some preparation work before penetration. // 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 // discover for Nat type
addrs, localAddr, err := Discover(stunServers, "") addrs, localAddr, err := Discover(stunServers, "")
if err != nil { if err != nil {
@@ -133,9 +140,13 @@ func Prepare(stunServers []string) (*PrepareResult, error) {
return nil, fmt.Errorf("listen local udp addr error: %v", err) return nil, fmt.Errorf("listen local udp addr error: %v", err)
} }
assistedAddrs := make([]string, 0, len(localIPs)) // Apply NAT traversal options
for _, ip := range localIPs { var assistedAddrs []string
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) 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{ return &PrepareResult{
Addrs: addrs, Addrs: addrs,

View File

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

View File

@@ -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) },
}
}

View File

@@ -19,7 +19,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -516,7 +515,8 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal { if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second 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 fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Server(frpConn, fmuxCfg) session, err := fmux.Server(frpConn, fmuxCfg)
if err != nil { if err != nil {