Compare commits

...

21 Commits

Author SHA1 Message Date
fatedier
2f5e1f7945 Merge pull request #4999 from fatedier/dev
Some checks failed
golangci-lint / lint (push) Has been cancelled
bump version
2025-09-25 20:23:42 +08:00
fatedier
b5e90c03a1 bump version to v0.65.0 and update release notes (#4998)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-09-25 20:11:17 +08:00
fatedier
b642a6323c update sponsors info (#4997) 2025-09-25 16:50:14 +08:00
juejinyuxitu
6561107945 chore: fix struct field name in comment (#4993)
Signed-off-by: juejinyuxitu <juejinyuxitu@outlook.com>
2025-09-25 16:47:33 +08:00
fatedier
abf4942e8a auth: enhance OIDC client with TLS and proxy configuration options (#4990)
Some checks failed
golangci-lint / lint (push) Has been cancelled
2025-09-25 10:19:19 +08:00
Charlie Blevins
7cfa546b55 add proxy name label to the proxy_count prometheus metric (#4985)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
* add proxy name label to the proxy_count metric

* undo label addition in favor of a new metric - this change should not break existing queries

* also register this new metric

* add type label to proxy_counts_detailed

* Update pkg/metrics/prometheus/server.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 00:18:49 +08:00
fatedier
0a798a7a69 update go version to 1.24 (#4960)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-08-27 15:10:36 +08:00
fatedier
604700cea5 update README (#4957)
Some checks failed
golangci-lint / lint (push) Has been cancelled
2025-08-27 11:07:18 +08:00
fatedier
610e5ed479 improve yamux logging (#4952)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-08-25 17:52:58 +08:00
fatedier
80d3f332e1 xtcp: add configuration to disable assisted addresses in NAT traversal (#4951) 2025-08-25 15:52:52 +08:00
immomo808
14253afe2f remove quotes (#4938)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-08-15 16:11:06 +08:00
fatedier
024c334d9d Merge pull request #4928 from fatedier/xtcp
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
golangci-lint / lint (push) Has been cancelled
improve context and polling logic in xtcp visitor
2025-08-12 01:48:26 +08:00
fatedier
22ae8166d3 Merge pull request #4925 from fatedier/dev
Some checks failed
golangci-lint / lint (push) Has been cancelled
bump version
2025-08-10 23:26:32 +08:00
fatedier
f795950742 bump version to v0.64.0 (#4924)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-08-10 23:11:50 +08:00
fatedier
024e4f5f1d improve random TLS certificate generation (#4923)
Some checks failed
golangci-lint / lint (push) Has been cancelled
2025-08-10 22:59:28 +08:00
fatedier
dc3bc9182c update sponsor info (#4917)
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
golangci-lint / lint (push) Has been cancelled
2025-08-08 22:28:17 +08:00
fatedier
e6dacf3a67 Fix SSH tunnel gateway binding address issue #4900 (#4902)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
- Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr
- This caused external connections to fail when proxyBindAddr was set to 127.0.0.1
- SSH tunnel gateway now correctly binds to bindAddr for external accessibility
- Update Release.md with bug fix description

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-28 15:19:56 +08:00
fatedier
7fe295f4f4 update golangci-lint version (#4897)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-07-25 17:10:32 +08:00
maguowei
c3bf952d8f fix webserver port not being released on frpc svr.Close() (#4896)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-07-24 10:16:44 +08:00
fatedier
f9065a6a78 add tokenSource support for auth configuration (#4865)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2025-07-03 13:17:21 +08:00
fatedier
61330d4d79 Update quic-go dependency from v0.48.2 to v0.53.0 (#4862)
Some checks failed
golangci-lint / lint (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
- Update go.mod to use github.com/quic-go/quic-go v0.53.0
- Replace quic.Connection interface with *quic.Conn struct
- Replace quic.Stream interface with *quic.Stream struct
- Update all affected files to use new API:
  - pkg/util/net/conn.go: Update QuicStreamToNetConn function and wrapQuicStream struct
  - server/service.go: Update HandleQUICListener function parameter
  - client/visitor/xtcp.go: Update QUICTunnelSession struct field
  - client/connector.go: Update defaultConnectorImpl struct field

Fixes #4852

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-01 18:56:46 +08:00
45 changed files with 1223 additions and 119 deletions

View File

@@ -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

View File

@@ -17,20 +17,10 @@ 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
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v2.1
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
# skip-cache: true
version: v2.3

View File

@@ -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: |

View File

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

View File

@@ -13,19 +13,43 @@ frp is an open source project with its ongoing development made possible entirel
<h3 align="center">Gold Sponsors</h3>
<!--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">
<a href="https://jb.gg/frp" target="_blank">
<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>
</p>
<p align="center">
<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">
<br>
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
</a>
</p>
<p align="center">
<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">
<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>
</p>
<!--gold sponsors end-->
@@ -502,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:
@@ -612,6 +636,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b
Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
##### Token Source
frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported.
**File-based token source:**
```toml
# frpc.toml
auth.method = "token"
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "/path/to/token/file"
```
The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons.
#### OIDC Authentication
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.

View File

@@ -15,19 +15,43 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3>
<!--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">
<a href="https://jb.gg/frp" target="_blank">
<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>
</p>
<p align="center">
<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">
<br>
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
</a>
</p>
<p align="center">
<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">
<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>
</p>
<!--gold sponsors end-->

View File

@@ -1,4 +1,5 @@
## Features
* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.
* 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.

View File

@@ -17,7 +17,6 @@ package client
import (
"context"
"crypto/tls"
"io"
"net"
"strconv"
"strings"
@@ -48,7 +47,7 @@ type defaultConnectorImpl struct {
cfg *v1.ClientCommonConfig
muxSession *fmux.Session
quicConn quic.Connection
quicConn *quic.Conn
closeOnce sync.Once
}
@@ -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 {

View File

@@ -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()

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

7
go.mod
View File

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

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"errors"
"fmt"
"os"
"strings"
)
// ValueSource provides a way to dynamically resolve configuration values
// from various sources like files, environment variables, or external services.
type ValueSource struct {
Type string `json:"type"`
File *FileSource `json:"file,omitempty"`
}
// FileSource specifies how to load a value from a file.
type FileSource struct {
Path string `json:"path"`
}
// Validate validates the ValueSource configuration.
func (v *ValueSource) Validate() error {
if v == nil {
return errors.New("valueSource cannot be nil")
}
switch v.Type {
case "file":
if v.File == nil {
return errors.New("file configuration is required when type is 'file'")
}
return v.File.Validate()
default:
return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
}
}
// Resolve resolves the value from the configured source.
func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
if err := v.Validate(); err != nil {
return "", err
}
switch v.Type {
case "file":
return v.File.Resolve(ctx)
default:
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
}
}
// Validate validates the FileSource configuration.
func (f *FileSource) Validate() error {
if f == nil {
return errors.New("fileSource cannot be nil")
}
if f.Path == "" {
return errors.New("file path cannot be empty")
}
return nil
}
// Resolve reads and returns the content from the specified file.
func (f *FileSource) Resolve(_ context.Context) (string, error) {
if err := f.Validate(); err != nil {
return "", err
}
content, err := os.ReadFile(f.Path)
if err != nil {
return "", fmt.Errorf("failed to read file %s: %v", f.Path, err)
}
// Trim whitespace, which is important for file-based tokens
return strings.TrimSpace(string(content)), nil
}

View File

@@ -0,0 +1,246 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestValueSource_Validate(t *testing.T) {
tests := []struct {
name string
vs *ValueSource
wantErr bool
}{
{
name: "nil valueSource",
vs: nil,
wantErr: true,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
wantErr: true,
},
{
name: "file type without file config",
vs: &ValueSource{
Type: "file",
File: nil,
},
wantErr: true,
},
{
name: "valid file type with absolute path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "/tmp/test",
},
},
wantErr: false,
},
{
name: "valid file type with relative path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "configs/token",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.vs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Validate(t *testing.T) {
tests := []struct {
name string
fs *FileSource
wantErr bool
}{
{
name: "nil fileSource",
fs: nil,
wantErr: true,
},
{
name: "empty path",
fs: &FileSource{
Path: "",
},
wantErr: true,
},
{
name: "relative path (allowed)",
fs: &FileSource{
Path: "relative/path",
},
wantErr: false,
},
{
name: "absolute path",
fs: &FileSource{
Path: "/absolute/path",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.fs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value\n\t "
expectedContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
fs *FileSource
want string
wantErr bool
}{
{
name: "valid file path",
fs: &FileSource{
Path: testFile,
},
want: expectedContent,
wantErr: false,
},
{
name: "non-existent file",
fs: &FileSource{
Path: "/non/existent/file",
},
want: "",
wantErr: true,
},
{
name: "path traversal attempt (should fail validation)",
fs: &FileSource{
Path: "../../../etc/passwd",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fs.Resolve(context.Background())
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}
func TestValueSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
vs *ValueSource
want string
wantErr bool
}{
{
name: "valid file type",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: testFile,
},
},
want: testContent,
wantErr: false,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
want: "",
wantErr: true,
},
{
name: "file type with path traversal",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "../../../etc/passwd",
},
},
want: "",
wantErr: true,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.vs.Resolve(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
package version
var version = "0.63.0"
var version = "0.65.0"
func Full() string {
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

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

View File

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

View File

@@ -0,0 +1,217 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package basic
import (
"fmt"
"os"
"path/filepath"
"github.com/onsi/ginkgo/v2"
"github.com/fatedier/frp/test/e2e/framework"
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/pkg/port"
)
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
f := framework.NewDefaultFramework()
ginkgo.Describe("File-based token loading", func() {
ginkgo.It("should work with file tokenSource", func() {
// Create a temporary token file
tmpDir := f.TempDirectory
tokenFile := filepath.Join(tmpDir, "test_token")
tokenContent := "test-token-123"
err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, tokenFile)
// Client config with matching token
clientConf += fmt.Sprintf(`
auth.token = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenContent, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should work with client tokenSource", func() {
// Create a temporary token file
tmpDir := f.TempDirectory
tokenFile := filepath.Join(tmpDir, "client_token")
tokenContent := "client-token-456"
err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with matching token
serverConf += fmt.Sprintf(`
auth.token = "%s"
`, tokenContent)
// Client config with tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should work with both server and client tokenSource", func() {
// Create temporary token files
tmpDir := f.TempDirectory
serverTokenFile := filepath.Join(tmpDir, "server_token")
clientTokenFile := filepath.Join(tmpDir, "client_token")
tokenContent := "shared-token-789"
err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)
// Client config with tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure()
})
ginkgo.It("should fail with mismatched tokens", func() {
// Create temporary token files with different content
tmpDir := f.TempDirectory
serverTokenFile := filepath.Join(tmpDir, "server_token")
clientTokenFile := filepath.Join(tmpDir, "client_token")
err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600)
framework.ExpectNoError(err)
err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600)
framework.ExpectNoError(err)
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
portName := port.GenName("TCP")
// Server config with tokenSource
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)
// Client config with different tokenSource
clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)
f.RunProcesses([]string{serverConf}, []string{clientConf})
// This should fail due to token mismatch - the client should not be able to connect
// We expect the request to fail because the proxy tunnel is not established
framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure()
})
ginkgo.It("should fail with non-existent token file", func() {
// This test verifies that server fails to start when tokenSource points to non-existent file
// We'll verify this by checking that the configuration loading itself fails
// Create a config that references a non-existent file
tmpDir := f.TempDirectory
nonExistentFile := filepath.Join(tmpDir, "non_existent_token")
serverConf := consts.DefaultServerConfig
// Server config with non-existent tokenSource file
serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, nonExistentFile)
// The test expectation is that this will fail during the RunProcesses call
// because the server cannot load the configuration due to missing token file
defer func() {
if r := recover(); r != nil {
// Expected: server should fail to start due to missing file
ginkgo.By(fmt.Sprintf("Server correctly failed to start: %v", r))
}
}()
// This should cause a panic or error during server startup
f.RunProcesses([]string{serverConf}, []string{})
})
})
})