mirror of
https://github.com/fatedier/frp.git
synced 2025-12-09 01:19:16 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33ab7eedd6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 | ||
|
|
b5e90c03a1 | ||
|
|
b642a6323c | ||
|
|
6561107945 | ||
|
|
abf4942e8a | ||
|
|
7cfa546b55 | ||
|
|
0a798a7a69 | ||
|
|
604700cea5 | ||
|
|
610e5ed479 | ||
|
|
80d3f332e1 | ||
|
|
14253afe2f | ||
|
|
024c334d9d | ||
|
|
f795950742 | ||
|
|
024e4f5f1d | ||
|
|
dc3bc9182c | ||
|
|
e6dacf3a67 | ||
|
|
7fe295f4f4 | ||
|
|
c3bf952d8f | ||
|
|
f9065a6a78 | ||
|
|
61330d4d79 |
@@ -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
|
||||
|
||||
14
.github/workflows/golangci-lint.yml
vendored
14
.github/workflows/golangci-lint.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -39,6 +39,7 @@ linters:
|
||||
- G404
|
||||
- G501
|
||||
- G115
|
||||
- G204
|
||||
severity: low
|
||||
confidence: low
|
||||
govet:
|
||||
@@ -73,6 +74,9 @@ linters:
|
||||
- linters:
|
||||
- revive
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- revive
|
||||
text: "avoid meaningless package names"
|
||||
- linters:
|
||||
- unparam
|
||||
text: is always false
|
||||
|
||||
63
README.md
63
README.md
@@ -13,19 +13,55 @@ 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://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>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
|
||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</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">
|
||||
</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>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
@@ -502,7 +538,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 +648,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.
|
||||
|
||||
51
README_zh.md
51
README_zh.md
@@ -15,19 +15,54 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<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>
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
|
||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</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">
|
||||
</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>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
@@ -102,9 +137,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||
|
||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||
|
||||
### 知识星球
|
||||
|
||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
||||
|
||||

|
||||
|
||||
12
Release.md
12
Release.md
@@ -1,4 +1,12 @@
|
||||
## 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.
|
||||
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
|
||||
* Individual frpc proxies and visitors now accept an `enabled` flag (defaults to true), letting you disable specific entries without relying on the global `start` list—disabled blocks are skipped when client configs load.
|
||||
|
||||
## Improvements
|
||||
|
||||
* **VirtualNet**: Implemented intelligent reconnection with exponential backoff. When connection errors occur repeatedly, the reconnect interval increases from 60s to 300s (max), reducing unnecessary reconnection attempts. Normal disconnections still reconnect quickly at 10s intervals.
|
||||
|
||||
## Fixes
|
||||
|
||||
* Fix deadlock issue when TCP connection is closed. Previously, sending messages could block forever if the connection handler had already stopped.
|
||||
|
||||
@@ -92,7 +92,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
|
||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -100,7 +100,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||
}
|
||||
ctl.registerMsgHandlers()
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||
|
||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
|
||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
@@ -64,6 +65,8 @@ type ServiceOptions struct {
|
||||
ProxyCfgs []v1.ProxyConfigurer
|
||||
VisitorCfgs []v1.VisitorConfigurer
|
||||
|
||||
UnsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||
// If it is empty, it means that the configuration file is not used for initialization.
|
||||
// It may be initialized using command line parameters or called directly.
|
||||
@@ -88,13 +91,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.
|
||||
@@ -119,6 +125,8 @@ type Service struct {
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
clientSpec *msg.ClientSpec
|
||||
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
configFilePath string
|
||||
@@ -134,7 +142,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,12 +154,19 @@ 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,
|
||||
unsafeFeatures: options.UnsafeFeatures,
|
||||
proxyCfgs: options.ProxyCfgs,
|
||||
visitorCfgs: options.VisitorCfgs,
|
||||
clientSpec: options.ClientSpec,
|
||||
@@ -398,6 +415,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) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
@@ -81,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() {
|
||||
|
||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
defer userConn.Close()
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
// If there was an error and connection supports CloseWithError, use it
|
||||
if tunnelErr != nil {
|
||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||
_ = eConn.CloseWithError(tunnelErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}()
|
||||
|
||||
xl.Debugf("get a new stcp user connection")
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
if err != nil {
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
defer visitorConn.Close()
|
||||
@@ -102,6 +114,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,12 +123,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,6 +140,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func NewVisitor(
|
||||
Name: cfg.GetBaseConfig().Name,
|
||||
Ctx: ctx,
|
||||
VnetController: helper.VNetController(),
|
||||
HandleConn: func(conn net.Conn) {
|
||||
SendConnToVisitor: func(conn net.Conn) {
|
||||
_ = baseVisitor.AcceptConn(conn)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,17 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||
|
||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
isConnTransfered := false
|
||||
isConnTransferred := false
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
if !isConnTransfered {
|
||||
if !isConnTransferred {
|
||||
// If there was an error and connection supports CloseWithError, use it
|
||||
if tunnelErr != nil {
|
||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||
_ = eConn.CloseWithError(tunnelErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}
|
||||
}()
|
||||
@@ -172,7 +180,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()
|
||||
@@ -181,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
tunnelConn, err := sv.openTunnel(ctx)
|
||||
if err != nil {
|
||||
xl.Errorf("open tunnel error: %v", err)
|
||||
tunnelErr = err
|
||||
|
||||
// no fallback, just return
|
||||
if sv.cfg.FallbackTo == "" {
|
||||
return
|
||||
@@ -191,7 +201,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
|
||||
}
|
||||
|
||||
@@ -200,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -219,40 +230,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 +287,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 +414,7 @@ func (ks *KCPTunnelSession) Close() {
|
||||
}
|
||||
|
||||
type QUICTunnelSession struct {
|
||||
session quic.Connection
|
||||
session *quic.Conn
|
||||
listenConn *net.UDPConn
|
||||
mu sync.RWMutex
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
var proxyTypes = []v1.ProxyType{
|
||||
@@ -73,8 +74,13 @@ 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 := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -85,7 +91,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -99,8 +105,12 @@ 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 := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -111,7 +121,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -31,7 +32,8 @@ import (
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
)
|
||||
@@ -41,6 +43,7 @@ var (
|
||||
cfgDir string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -48,6 +51,9 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -59,15 +65,17 @@ var rootCmd = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
|
||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||
if cfgDir != "" {
|
||||
_ = runMultipleClients(cfgDir)
|
||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not show command usage here.
|
||||
err := runClient(cfgFile)
|
||||
err := runClient(cfgFile, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -76,7 +84,7 @@ var rootCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runMultipleClients(cfgDir string) error {
|
||||
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
var wg sync.WaitGroup
|
||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
@@ -86,7 +94,7 @@ func runMultipleClients(cfgDir string) error {
|
||||
time.Sleep(time.Millisecond)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := runClient(path)
|
||||
err := runClient(path, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||
}
|
||||
@@ -111,7 +119,7 @@ func handleTermSignal(svr *client.Service) {
|
||||
svr.GracefulClose(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func runClient(cfgFilePath string) error {
|
||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -127,20 +135,21 @@ func runClient(cfgFilePath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
|
||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
func startService(
|
||||
cfg *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) error {
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
@@ -153,6 +162,7 @@ func startService(
|
||||
Common: cfg,
|
||||
ProxyCfgs: proxyCfgs,
|
||||
VisitorCfgs: visitorCfgs,
|
||||
UnsafeFeatures: unsafeFeatures,
|
||||
ConfigFilePath: cfgFile,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -42,7 +43,8 @@ var verifyCmd = &cobra.Command{
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -124,6 +143,11 @@ transport.tls.enable = true
|
||||
# Default is empty, means all proxies.
|
||||
# start = ["ssh", "dns"]
|
||||
|
||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||
# Set 'enabled = false' in a proxy configuration to disable it.
|
||||
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
||||
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
||||
|
||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
@@ -150,6 +174,8 @@ metadatas.var2 = "123"
|
||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||
# enabled = true
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# Limit bandwidth for this proxy, unit is KB and MB
|
||||
@@ -234,6 +260,8 @@ healthCheck.httpHeaders=[
|
||||
[[proxies]]
|
||||
name = "web02"
|
||||
type = "https"
|
||||
# Disable this proxy by setting enabled to false
|
||||
# enabled = false
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 8000
|
||||
subdomain = "web02"
|
||||
@@ -367,6 +395,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 +442,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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
BIN
doc/pic/zsxq.jpg
BIN
doc/pic/zsxq.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23 AS building
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
WORKDIR /building
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23 AS building
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
WORKDIR /building
|
||||
|
||||
22
go.mod
22
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/fatedier/frp
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
@@ -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.55.0
|
||||
github.com/rodaine/table v1.2.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
@@ -26,10 +26,10 @@ require (
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@@ -67,12 +67,10 @@ require (
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
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
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -83,4 +81,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
|
||||
|
||||
46
go.sum
46
go.sum
@@ -22,8 +22,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
||||
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
@@ -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.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
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=
|
||||
@@ -156,26 +156,24 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
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/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
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=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -189,8 +187,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
@@ -199,8 +197,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -215,24 +213,24 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -243,8 +241,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
|
||||
@@ -27,16 +27,23 @@ 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)
|
||||
if cfg.OIDC.TokenSource != nil {
|
||||
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
|
||||
} else {
|
||||
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 {
|
||||
|
||||
116
pkg/auth/oidc.go
116
pkg/auth/oidc.go
@@ -16,23 +16,72 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
|
||||
func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) {
|
||||
// Clone the default transport to get all reasonable defaults
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// Configure TLS settings
|
||||
if trustedCAFile != "" || insecureSkipVerify {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
if trustedCAFile != "" && !insecureSkipVerify {
|
||||
caCert, err := os.ReadFile(trustedCAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile)
|
||||
}
|
||||
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
// Configure proxy settings
|
||||
if proxyURL != "" {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err)
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(parsedURL)
|
||||
} else {
|
||||
// Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment
|
||||
transport.Proxy = nil
|
||||
}
|
||||
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}
|
||||
|
||||
type OidcAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenGenerator *clientcredentials.Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||
eps := make(map[string][]string)
|
||||
for k, v := range cfg.AdditionalEndpointParams {
|
||||
eps[k] = []string{v}
|
||||
@@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
||||
EndpointParams: eps,
|
||||
}
|
||||
|
||||
// Create custom HTTP client if needed
|
||||
var httpClient *http.Client
|
||||
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
||||
var err error
|
||||
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &OidcAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
tokenGenerator: tokenGenerator,
|
||||
}
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||
ctx := context.Background()
|
||||
if auth.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
|
||||
}
|
||||
|
||||
tokenObj, err := auth.tokenGenerator.Token(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||
}
|
||||
@@ -87,6 +152,51 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
||||
return err
|
||||
}
|
||||
|
||||
type OidcTokenSourceAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
valueSource *v1.ValueSource
|
||||
}
|
||||
|
||||
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
|
||||
return &OidcTokenSourceAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
valueSource: valueSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
ctx := context.Background()
|
||||
accessToken, err = auth.valueSource.Resolve(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
type TokenVerifier interface {
|
||||
Verify(context.Context, string) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -279,8 +281,21 @@ func LoadClientConfig(path string, strict bool) (
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by enabled field in each proxy
|
||||
// nil or true means enabled, false means disabled
|
||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@@ -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,21 @@ 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"`
|
||||
|
||||
// TokenSource specifies a custom dynamic source for the authorization token.
|
||||
// This is mutually exclusive with every other field of this structure.
|
||||
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||
}
|
||||
|
||||
type VirtualNetConfig struct {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -108,8 +108,11 @@ type DomainConfig struct {
|
||||
}
|
||||
|
||||
type ProxyBaseConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
||||
// This allows individual control over each proxy, complementing the global "start" field.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Transport ProxyTransport `json:"transport,omitempty"`
|
||||
// metadata info for each proxy
|
||||
@@ -422,6 +425,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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,43 +23,111 @@ import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
|
||||
var (
|
||||
warnings Warning
|
||||
errs error
|
||||
)
|
||||
// validate feature gates
|
||||
if c.VirtualNet.Address != "" {
|
||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||
}
|
||||
|
||||
validators := []func() (Warning, error){
|
||||
func() (Warning, error) { return validateFeatureGates(c) },
|
||||
func() (Warning, error) { return validateAuthConfig(&c.Auth, unsafeFeatures) },
|
||||
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
|
||||
for _, v := range validators {
|
||||
w, err := v()
|
||||
warnings = AppendError(warnings, w)
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
if c.VirtualNet.Address != "" {
|
||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
|
||||
var errs error
|
||||
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||
}
|
||||
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
|
||||
if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||
}
|
||||
|
||||
if err := validateLogConfig(&c.Log); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
// Validate token/tokenSource mutual exclusivity
|
||||
if c.Token != "" && c.TokenSource != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||
}
|
||||
|
||||
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
// Validate tokenSource if specified
|
||||
if c.TokenSource != nil {
|
||||
if c.TokenSource.Type == "exec" {
|
||||
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
|
||||
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
|
||||
}
|
||||
}
|
||||
if err := c.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
|
||||
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
|
||||
if err := validateOIDCConfig(&c.OIDC, unsafeFeatures); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
if c.TokenSource == nil {
|
||||
return nil
|
||||
}
|
||||
var errs error
|
||||
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
|
||||
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
|
||||
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
|
||||
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
||||
}
|
||||
if c.TokenSource.Type == "exec" {
|
||||
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
|
||||
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
|
||||
}
|
||||
}
|
||||
if err := c.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
||||
var (
|
||||
warnings Warning
|
||||
errs error
|
||||
)
|
||||
|
||||
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
|
||||
if c.HeartbeatTimeout < c.HeartbeatInterval {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
||||
}
|
||||
}
|
||||
|
||||
if !lo.FromPtr(c.Transport.TLS.Enable) {
|
||||
if !lo.FromPtr(c.TLS.Enable) {
|
||||
checkTLSConfig := func(name string, value string) Warning {
|
||||
if value != "" {
|
||||
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
||||
@@ -67,16 +135,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
|
||||
if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||
}
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
for _, f := range c.IncludeConfigFiles {
|
||||
func validateIncludeFiles(files []string) (Warning, error) {
|
||||
var errs error
|
||||
for _, f := range files {
|
||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||
if err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
||||
@@ -86,13 +158,18 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
||||
}
|
||||
}
|
||||
return warnings, errs
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
|
||||
func ValidateAllClientConfig(
|
||||
c *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
) (Warning, error) {
|
||||
var warnings Warning
|
||||
if c != nil {
|
||||
warning, err := ValidateClientCommonConfig(c)
|
||||
warning, err := ValidateClientCommonConfig(c, unsafeFeatures)
|
||||
warnings = AppendError(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
158
pkg/config/v1/value_source.go
Normal file
158
pkg/config/v1/value_source.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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"
|
||||
"os/exec"
|
||||
"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"`
|
||||
Exec *ExecSource `json:"exec,omitempty"`
|
||||
}
|
||||
|
||||
// FileSource specifies how to load a value from a file.
|
||||
type FileSource struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// ExecSource specifies how to get a value from another program launched as subprocess.
|
||||
type ExecSource struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
Env []ExecEnvVar `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
type ExecEnvVar struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// 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()
|
||||
case "exec":
|
||||
if v.Exec == nil {
|
||||
return errors.New("exec configuration is required when type is 'exec'")
|
||||
}
|
||||
return v.Exec.Validate()
|
||||
default:
|
||||
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are 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)
|
||||
case "exec":
|
||||
return v.Exec.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
|
||||
}
|
||||
|
||||
// Validate validates the ExecSource configuration.
|
||||
func (e *ExecSource) Validate() error {
|
||||
if e == nil {
|
||||
return errors.New("execSource cannot be nil")
|
||||
}
|
||||
|
||||
if e.Command == "" {
|
||||
return errors.New("exec command cannot be empty")
|
||||
}
|
||||
|
||||
for _, env := range e.Env {
|
||||
if env.Name == "" {
|
||||
return errors.New("exec env name cannot be empty")
|
||||
}
|
||||
if strings.Contains(env.Name, "=") {
|
||||
return errors.New("exec env name cannot contain '='")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve reads and returns the content captured from stdout of launched subprocess.
|
||||
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
|
||||
if err := e.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
|
||||
if len(e.Env) != 0 {
|
||||
cmd.Env = os.Environ()
|
||||
for _, env := range e.Env {
|
||||
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
|
||||
}
|
||||
|
||||
// Trim whitespace, which is important for exec-based tokens
|
||||
return strings.TrimSpace(string(content)), nil
|
||||
}
|
||||
246
pkg/config/v1/value_source_test.go
Normal file
246
pkg/config/v1/value_source_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,11 @@ type VisitorTransport struct {
|
||||
}
|
||||
|
||||
type VisitorBaseConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
||||
// This allows individual control over each visitor, complementing the global "start" field.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Transport VisitorTransport `json:"transport,omitempty"`
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
// if the server user is not set, it defaults to the current user
|
||||
@@ -160,6 +163,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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) SendChannel() chan Message {
|
||||
return d.sendCh
|
||||
}
|
||||
|
||||
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
||||
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,11 +23,20 @@ import (
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
// PluginContext provides the necessary context and callbacks for visitor plugins.
|
||||
type PluginContext struct {
|
||||
Name string
|
||||
Ctx context.Context
|
||||
// Name is the unique identifier for this visitor, used for logging and routing.
|
||||
Name string
|
||||
|
||||
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
|
||||
Ctx context.Context
|
||||
|
||||
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
|
||||
VnetController *vnet.Controller
|
||||
HandleConn func(net.Conn)
|
||||
|
||||
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
|
||||
// Does not return error; failures are handled by closing the connection.
|
||||
SendConnToVisitor func(net.Conn)
|
||||
}
|
||||
|
||||
// Creators is used for create plugins to handle connections.
|
||||
|
||||
@@ -42,6 +42,8 @@ type VirtualNetPlugin struct {
|
||||
controllerConn net.Conn
|
||||
closeSignal chan struct{}
|
||||
|
||||
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -98,7 +100,6 @@ func (p *VirtualNetPlugin) Start() {
|
||||
|
||||
func (p *VirtualNetPlugin) run() {
|
||||
xl := xlog.FromContextSafe(p.ctx)
|
||||
reconnectDelay := 10 * time.Second
|
||||
|
||||
for {
|
||||
currentCloseSignal := make(chan struct{})
|
||||
@@ -121,7 +122,10 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.controllerConn = controllerConn
|
||||
p.mu.Unlock()
|
||||
|
||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
|
||||
// Wrap with CloseNotifyConn which supports both close notification and error recording
|
||||
var closeErr error
|
||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
|
||||
closeErr = err
|
||||
close(currentCloseSignal) // Signal the run loop on close.
|
||||
})
|
||||
|
||||
@@ -129,9 +133,9 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
||||
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
||||
|
||||
// Pass the CloseNotifyConn to HandleConn.
|
||||
// HandleConn is responsible for calling Close() on pluginNotifyConn.
|
||||
p.pluginCtx.HandleConn(pluginNotifyConn)
|
||||
// Pass the CloseNotifyConn to the visitor for handling.
|
||||
// The visitor can call CloseWithError to record the failure reason.
|
||||
p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
|
||||
|
||||
// Wait for context cancellation or connection close.
|
||||
select {
|
||||
@@ -140,8 +144,32 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.cleanupControllerConn(xl)
|
||||
return
|
||||
case <-currentCloseSignal:
|
||||
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
|
||||
// HandleConn closed the plugin side. Close the controller side.
|
||||
// Determine reconnect delay based on error with exponential backoff
|
||||
var reconnectDelay time.Duration
|
||||
if closeErr != nil {
|
||||
p.consecutiveErrors++
|
||||
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
|
||||
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
|
||||
|
||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||
baseDelay := 60 * time.Second
|
||||
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
||||
if reconnectDelay > 300*time.Second {
|
||||
reconnectDelay = 300 * time.Second
|
||||
}
|
||||
} else {
|
||||
// Reset consecutive errors on successful connection
|
||||
if p.consecutiveErrors > 0 {
|
||||
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
|
||||
p.pluginCtx.Name, p.consecutiveErrors)
|
||||
p.consecutiveErrors = 0
|
||||
} else {
|
||||
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
|
||||
}
|
||||
reconnectDelay = 10 * time.Second
|
||||
}
|
||||
|
||||
// The visitor closed the plugin side. Close the controller side.
|
||||
p.cleanupControllerConn(xl)
|
||||
|
||||
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
||||
@@ -184,7 +212,7 @@ func (p *VirtualNetPlugin) Close() error {
|
||||
}
|
||||
|
||||
// Explicitly close the controller side of the pipe.
|
||||
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
|
||||
// This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
|
||||
p.cleanupControllerConn(xl)
|
||||
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
||||
|
||||
|
||||
34
pkg/policy/security/unsafe.go
Normal file
34
pkg/policy/security/unsafe.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package security
|
||||
|
||||
const (
|
||||
TokenSourceExec = "TokenSourceExec"
|
||||
)
|
||||
|
||||
var (
|
||||
ClientUnsafeFeatures = []string{
|
||||
TokenSourceExec,
|
||||
}
|
||||
|
||||
ServerUnsafeFeatures = []string{
|
||||
TokenSourceExec,
|
||||
}
|
||||
)
|
||||
|
||||
type UnsafeFeatures struct {
|
||||
features map[string]bool
|
||||
}
|
||||
|
||||
func NewUnsafeFeatures(allowed []string) *UnsafeFeatures {
|
||||
features := make(map[string]bool)
|
||||
for _, f := range allowed {
|
||||
features[f] = true
|
||||
}
|
||||
return &UnsafeFeatures{features: features}
|
||||
}
|
||||
|
||||
func (u *UnsafeFeatures) IsEnabled(feature string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
return u.features[feature]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -35,15 +35,19 @@ type MessageTransporter interface {
|
||||
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
||||
}
|
||||
|
||||
func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter {
|
||||
type MessageSender interface {
|
||||
Send(msg.Message) error
|
||||
}
|
||||
|
||||
func NewMessageTransporter(sender MessageSender) MessageTransporter {
|
||||
return &transporterImpl{
|
||||
sendCh: sendCh,
|
||||
sender: sender,
|
||||
registry: make(map[string]map[string]chan msg.Message),
|
||||
}
|
||||
}
|
||||
|
||||
type transporterImpl struct {
|
||||
sendCh chan msg.Message
|
||||
sender MessageSender
|
||||
|
||||
// First key is message type and second key is lane key.
|
||||
// Dispatch will dispatch message to related channel by its message type
|
||||
@@ -53,9 +57,7 @@ type transporterImpl struct {
|
||||
}
|
||||
|
||||
func (impl *transporterImpl) Send(m msg.Message) error {
|
||||
return errors.PanicToError(func() {
|
||||
impl.sendCh <- m
|
||||
})
|
||||
return impl.sender.Send(m)
|
||||
}
|
||||
|
||||
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
|
||||
// 1 means closed
|
||||
closeFlag int32
|
||||
|
||||
closeFn func()
|
||||
closeFn func(error)
|
||||
}
|
||||
|
||||
// closeFn will be only called once
|
||||
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||
// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
|
||||
func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
||||
return &CloseNotifyConn{
|
||||
Conn: c,
|
||||
closeFn: closeFn,
|
||||
@@ -149,14 +149,27 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||
func (cc *CloseNotifyConn) Close() (err error) {
|
||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||
if pflag == 0 {
|
||||
err = cc.Close()
|
||||
err = cc.Conn.Close()
|
||||
if cc.closeFn != nil {
|
||||
cc.closeFn()
|
||||
cc.closeFn(nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CloseWithError closes the connection and passes the error to the close callback.
|
||||
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||
if pflag == 0 {
|
||||
closeErr := cc.Conn.Close()
|
||||
if cc.closeFn != nil {
|
||||
cc.closeFn(err)
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StatsConn struct {
|
||||
net.Conn
|
||||
|
||||
@@ -197,11 +210,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,
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
||||
muxer := http.NewServeMux()
|
||||
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
||||
notifyCh := make(chan struct{})
|
||||
conn := WrapCloseNotifyConn(c, func() {
|
||||
conn := WrapCloseNotifyConn(c, func(_ error) {
|
||||
close(notifyCh)
|
||||
})
|
||||
wl.acceptCh <- conn
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
package version
|
||||
|
||||
var version = "0.63.0"
|
||||
var version = "0.65.0"
|
||||
|
||||
func Full() string {
|
||||
return version
|
||||
|
||||
65
pkg/util/xlog/log_writer.go
Normal file
65
pkg/util/xlog/log_writer.go
Normal 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) },
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -195,7 +195,7 @@ func NewControl(
|
||||
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
||||
}
|
||||
ctl.registerMsgHandlers()
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||
return ctl, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ type ResourceController struct {
|
||||
// HTTP Group Controller
|
||||
HTTPGroupCtl *group.HTTPGroupController
|
||||
|
||||
// HTTPS Group Controller
|
||||
HTTPSGroupCtl *group.HTTPSGroupController
|
||||
|
||||
// TCP Mux Group Controller
|
||||
TCPMuxGroupCtl *group.TCPMuxGroupCtl
|
||||
|
||||
|
||||
197
server/group/https.go
Normal file
197
server/group/https.go
Normal file
@@ -0,0 +1,197 @@
|
||||
// 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
gerr "github.com/fatedier/golib/errors"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/vhost"
|
||||
)
|
||||
|
||||
type HTTPSGroupController struct {
|
||||
groups map[string]*HTTPSGroup
|
||||
|
||||
httpsMuxer *vhost.HTTPSMuxer
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
|
||||
return &HTTPSGroupController{
|
||||
groups: make(map[string]*HTTPSGroup),
|
||||
httpsMuxer: httpsMuxer,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctl *HTTPSGroupController) Listen(
|
||||
ctx context.Context,
|
||||
group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (l net.Listener, err error) {
|
||||
indexKey := group
|
||||
ctl.mu.Lock()
|
||||
g, ok := ctl.groups[indexKey]
|
||||
if !ok {
|
||||
g = NewHTTPSGroup(ctl)
|
||||
ctl.groups[indexKey] = g
|
||||
}
|
||||
ctl.mu.Unlock()
|
||||
|
||||
return g.Listen(ctx, group, groupKey, routeConfig)
|
||||
}
|
||||
|
||||
func (ctl *HTTPSGroupController) RemoveGroup(group string) {
|
||||
ctl.mu.Lock()
|
||||
defer ctl.mu.Unlock()
|
||||
delete(ctl.groups, group)
|
||||
}
|
||||
|
||||
type HTTPSGroup struct {
|
||||
group string
|
||||
groupKey string
|
||||
domain string
|
||||
|
||||
acceptCh chan net.Conn
|
||||
httpsLn *vhost.Listener
|
||||
lns []*HTTPSGroupListener
|
||||
ctl *HTTPSGroupController
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
|
||||
return &HTTPSGroup{
|
||||
lns: make([]*HTTPSGroupListener, 0),
|
||||
ctl: ctl,
|
||||
acceptCh: make(chan net.Conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *HTTPSGroup) Listen(
|
||||
ctx context.Context,
|
||||
group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (ln *HTTPSGroupListener, err error) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if len(g.lns) == 0 {
|
||||
// the first listener, listen on the real address
|
||||
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
|
||||
if errRet != nil {
|
||||
return nil, errRet
|
||||
}
|
||||
ln = newHTTPSGroupListener(group, g, httpsLn.Addr())
|
||||
|
||||
g.group = group
|
||||
g.groupKey = groupKey
|
||||
g.domain = routeConfig.Domain
|
||||
g.httpsLn = httpsLn
|
||||
g.lns = append(g.lns, ln)
|
||||
go g.worker()
|
||||
} else {
|
||||
// route config in the same group must be equal
|
||||
if g.group != group || g.domain != routeConfig.Domain {
|
||||
return nil, ErrGroupParamsInvalid
|
||||
}
|
||||
if g.groupKey != groupKey {
|
||||
return nil, ErrGroupAuthFailed
|
||||
}
|
||||
ln = newHTTPSGroupListener(group, g, g.lns[0].Addr())
|
||||
g.lns = append(g.lns, ln)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *HTTPSGroup) worker() {
|
||||
for {
|
||||
c, err := g.httpsLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gerr.PanicToError(func() {
|
||||
g.acceptCh <- c
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *HTTPSGroup) Accept() <-chan net.Conn {
|
||||
return g.acceptCh
|
||||
}
|
||||
|
||||
func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
for i, tmpLn := range g.lns {
|
||||
if tmpLn == ln {
|
||||
g.lns = append(g.lns[:i], g.lns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(g.lns) == 0 {
|
||||
close(g.acceptCh)
|
||||
if g.httpsLn != nil {
|
||||
g.httpsLn.Close()
|
||||
}
|
||||
g.ctl.RemoveGroup(g.group)
|
||||
}
|
||||
}
|
||||
|
||||
type HTTPSGroupListener struct {
|
||||
groupName string
|
||||
group *HTTPSGroup
|
||||
|
||||
addr net.Addr
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener {
|
||||
return &HTTPSGroupListener{
|
||||
groupName: name,
|
||||
group: group,
|
||||
addr: addr,
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) {
|
||||
var ok bool
|
||||
select {
|
||||
case <-ln.closeCh:
|
||||
return nil, ErrListenerClosed
|
||||
case c, ok = <-ln.group.Accept():
|
||||
if !ok {
|
||||
return nil, ErrListenerClosed
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ln *HTTPSGroupListener) Addr() net.Addr {
|
||||
return ln.addr
|
||||
}
|
||||
|
||||
func (ln *HTTPSGroupListener) Close() (err error) {
|
||||
close(ln.closeCh)
|
||||
|
||||
// remove self from HTTPSGroup
|
||||
ln.group.CloseListener(ln)
|
||||
return
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
@@ -58,27 +59,24 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
routeConfig.Domain = domain
|
||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
||||
pxy.listeners = append(pxy.listeners, l)
|
||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
||||
addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort))
|
||||
xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group)
|
||||
}
|
||||
|
||||
if pxy.cfg.SubDomain != "" {
|
||||
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
||||
pxy.listeners = append(pxy.listeners, l)
|
||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
||||
addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort))
|
||||
xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group)
|
||||
}
|
||||
|
||||
pxy.startCommonTCPListenersHandler()
|
||||
@@ -89,3 +87,18 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||
func (pxy *HTTPSProxy) Close() {
|
||||
pxy.BaseProxy.Close()
|
||||
}
|
||||
|
||||
func (pxy *HTTPSProxy) listenForDomain(routeConfig *vhost.RouteConfig, domain string) (net.Listener, error) {
|
||||
tmpRouteConfig := *routeConfig
|
||||
tmpRouteConfig.Domain = domain
|
||||
|
||||
if pxy.cfg.LoadBalancer.Group != "" {
|
||||
return pxy.rc.HTTPSGroupCtl.Listen(
|
||||
pxy.ctx,
|
||||
pxy.cfg.LoadBalancer.Group,
|
||||
pxy.cfg.LoadBalancer.GroupKey,
|
||||
tmpRouteConfig,
|
||||
)
|
||||
}
|
||||
return pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &tmpRouteConfig)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -323,6 +322,9 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
|
||||
}
|
||||
|
||||
// Init HTTPS group controller after HTTPSMuxer is created
|
||||
svr.rc.HTTPSGroupCtl = group.NewHTTPSGroupController(svr.rc.VhostHTTPSMuxer)
|
||||
}
|
||||
|
||||
// frp tls listener
|
||||
@@ -516,7 +518,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 +553,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 {
|
||||
|
||||
@@ -75,8 +75,8 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
|
||||
if err != nil {
|
||||
return p, p.StdOutput(), err
|
||||
}
|
||||
// sleep for a while to get std output
|
||||
time.Sleep(2 * time.Second)
|
||||
// Give frps extra time to finish binding ports before proceeding.
|
||||
time.Sleep(4 * time.Second)
|
||||
return p, p.StdOutput(), nil
|
||||
}
|
||||
|
||||
|
||||
217
test/e2e/v1/basic/token_source.go
Normal file
217
test/e2e/v1/basic/token_source.go
Normal 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{})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
package features
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/test/e2e/framework"
|
||||
"github.com/fatedier/frp/test/e2e/framework/consts"
|
||||
"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
|
||||
@@ -112,6 +114,80 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
|
||||
|
||||
framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
|
||||
})
|
||||
|
||||
ginkgo.It("HTTPS", func() {
|
||||
vhostHTTPSPort := f.AllocPort()
|
||||
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
|
||||
vhostHTTPSPort = %d
|
||||
`, vhostHTTPSPort)
|
||||
clientConf := consts.DefaultClientConfig
|
||||
|
||||
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
fooPort := f.AllocPort()
|
||||
fooServer := httpserver.New(
|
||||
httpserver.WithBindPort(fooPort),
|
||||
httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("foo"))),
|
||||
httpserver.WithTLSConfig(tlsConfig),
|
||||
)
|
||||
f.RunServer("", fooServer)
|
||||
|
||||
barPort := f.AllocPort()
|
||||
barServer := httpserver.New(
|
||||
httpserver.WithBindPort(barPort),
|
||||
httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("bar"))),
|
||||
httpserver.WithTLSConfig(tlsConfig),
|
||||
)
|
||||
f.RunServer("", barServer)
|
||||
|
||||
clientConf += fmt.Sprintf(`
|
||||
[[proxies]]
|
||||
name = "foo"
|
||||
type = "https"
|
||||
localPort = %d
|
||||
customDomains = ["example.com"]
|
||||
loadBalancer.group = "test"
|
||||
loadBalancer.groupKey = "123"
|
||||
|
||||
[[proxies]]
|
||||
name = "bar"
|
||||
type = "https"
|
||||
localPort = %d
|
||||
customDomains = ["example.com"]
|
||||
loadBalancer.group = "test"
|
||||
loadBalancer.groupKey = "123"
|
||||
`, fooPort, barPort)
|
||||
|
||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||
|
||||
fooCount := 0
|
||||
barCount := 0
|
||||
for i := 0; i < 10; i++ {
|
||||
framework.NewRequestExpect(f).
|
||||
Explain("times " + strconv.Itoa(i)).
|
||||
Port(vhostHTTPSPort).
|
||||
RequestModify(func(r *request.Request) {
|
||||
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
|
||||
ServerName: "example.com",
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
}).
|
||||
Ensure(func(resp *request.Response) bool {
|
||||
switch string(resp.Content) {
|
||||
case "foo":
|
||||
fooCount++
|
||||
case "bar":
|
||||
barCount++
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Health Check", func() {
|
||||
|
||||
Reference in New Issue
Block a user