Compare commits

..

84 Commits

Author SHA1 Message Date
Devin AI
42ab43374f Fix SSH tunnel gateway binding address issue #4900
- Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr
- This caused external connections to fail when proxyBindAddr was set to 127.0.0.1
- SSH tunnel gateway now correctly binds to bindAddr for external accessibility
- Update Release.md with bug fix description

Co-Authored-By: fatedier <fatedier@gmail.com>
2025-07-28 07:14:17 +00:00
fatedier
7fe295f4f4 update golangci-lint version (#4897) 2025-07-25 17:10:32 +08:00
maguowei
c3bf952d8f fix webserver port not being released on frpc svr.Close() (#4896) 2025-07-24 10:16:44 +08:00
fatedier
f9065a6a78 add tokenSource support for auth configuration (#4865) 2025-07-03 13:17:21 +08:00
fatedier
61330d4d79 Update quic-go dependency from v0.48.2 to v0.53.0 (#4862)
- Update go.mod to use github.com/quic-go/quic-go v0.53.0
- Replace quic.Connection interface with *quic.Conn struct
- Replace quic.Stream interface with *quic.Stream struct
- Update all affected files to use new API:
  - pkg/util/net/conn.go: Update QuicStreamToNetConn function and wrapQuicStream struct
  - server/service.go: Update HandleQUICListener function parameter
  - client/visitor/xtcp.go: Update QUICTunnelSession struct field
  - client/connector.go: Update defaultConnectorImpl struct field

Fixes #4852

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-01 18:56:46 +08:00
fatedier
c777891f75 update .golangci.yml (#4848) 2025-06-25 11:40:23 +08:00
fatedier
43cf1688e4 update golangci-lint version (#4817) 2025-06-25 11:40:23 +08:00
fatedier
720c09c06b update test package (#4814) 2025-06-25 11:40:23 +08:00
fatedier
3fa76b72f3 add proxy protocol support for UDP proxies (#4810) 2025-06-25 11:40:23 +08:00
fatedier
8eb525a648 feat: support YAML merge in strict configuration mode (#4809) 2025-06-25 11:40:23 +08:00
scientificworld
077ba80ba3 fix: type error in server_plugin doc (#4799) 2025-06-25 11:40:23 +08:00
CrynTox
c99986fa28 build: add x64 openbsd (#4780)
Co-authored-by: CrynTox <>
2025-06-25 11:40:23 +08:00
fatedier
b41d8f8e40 update release notes (#4772) 2025-04-27 15:46:22 +08:00
fatedier
3c8d648ddc vnet: fix issues (#4771) 2025-04-27 15:46:22 +08:00
fatedier
27f66baf54 update feature gates doc (#4755) 2025-04-27 15:46:22 +08:00
fatedier
c5a8f6ef4a Merge pull request #4753 from fatedier/dev
bump version
2025-04-16 16:21:11 +08:00
fatedier
31b44c1feb Merge pull request #4700 from fatedier/dev
bump version
2025-03-07 17:26:55 +08:00
fatedier
2a7aa69890 Merge pull request #4590 from fatedier/dev
bump version
2024-12-16 19:41:22 +08:00
fatedier
4bbec09d57 Merge pull request #4496 from fatedier/dev
bump version
2024-10-17 17:28:10 +08:00
fatedier
ccfe8c97f4 Merge pull request #4392 from fatedier/dev
bump version
2024-08-19 13:47:36 +08:00
fatedier
243ca994e0 Merge pull request #4324 from fatedier/dev
bump version
2024-07-09 10:55:15 +08:00
fatedier
e649692217 Merge pull request #4253 from fatedier/dev
bump version
2024-05-31 14:34:47 +08:00
fatedier
4e8e9e1dec Merge pull request #4205 from fatedier/dev
bump version
2024-05-07 18:08:48 +08:00
fatedier
8f23733f47 Merge pull request #4137 from fatedier/dev
bump version
2024-04-09 11:45:41 +08:00
fatedier
5a6d9f60c2 Merge pull request #4092 from fatedier/dev
bump v0.56.0
2024-03-21 17:37:39 +08:00
fatedier
a5b7abfc8b Merge pull request #4060 from fatedier/dev
bump version
2024-03-12 18:11:37 +08:00
fatedier
1e650ea9a7 Merge pull request #4056 from fatedier/dev
bump version
2024-03-12 16:55:25 +08:00
fatedier
d689f0fc53 Merge pull request #3968 from fatedier/dev
bump version
2024-02-01 14:29:17 +08:00
fatedier
d505ecb473 Merge pull request #3880 from fatedier/dev
fix login retry interval (#3879)
2023-12-21 21:42:47 +08:00
fatedier
2b83436a97 Merge pull request #3878 from fatedier/dev
bump version
2023-12-21 21:25:01 +08:00
fatedier
051299ec25 Merge pull request #3845 from fatedier/dev
bump version to v0.53.0
2023-12-14 20:58:11 +08:00
fatedier
44985f574d Merge pull request #3722 from fatedier/dev
bump version
2023-10-24 10:47:16 +08:00
fatedier
c9ca9353cf Merge pull request #3714 from fatedier/dev
bump version
2023-10-23 10:51:50 +08:00
fatedier
31fa3f021a Merge pull request #3668 from fatedier/dev
bump version to v0.52.1
2023-10-11 17:16:07 +08:00
fatedier
2d3af8a108 Merge pull request #3651 from fatedier/dev
bump version to v0.52.0
2023-10-10 17:24:07 +08:00
fatedier
466d69eae0 Merge pull request #3574 from fatedier/dev
release v0.51.3
2023-08-14 11:59:09 +08:00
fatedier
7c8cbeb250 Merge pull request #3550 from fatedier/dev
release v0.51.2
2023-07-25 21:35:08 +08:00
fatedier
4fd6301577 Merge pull request #3537 from fatedier/dev
release v0.51.1
2023-07-20 22:38:48 +08:00
fatedier
53626b370c Merge pull request #3517 from fatedier/dev
bump version to v0.51.0
2023-07-05 20:39:25 +08:00
fatedier
4fd800bc48 Merge pull request #3499 from fatedier/dev
release v0.50.0
2023-06-26 17:03:56 +08:00
fatedier
0d6d968fe8 Merge pull request #3454 from fatedier/dev
release v0.49.0
2023-05-29 01:12:26 +08:00
fatedier
8fb99ef7a9 Merge pull request #3348 from fatedier/dev
bump version
2023-03-08 11:40:31 +08:00
fatedier
88e74ff24d Merge pull request #3300 from fatedier/dev
sync
2023-02-10 01:12:00 +08:00
fatedier
534dc99d55 Merge pull request #3299 from fatedier/dev
sync
2023-02-09 23:06:14 +08:00
fatedier
595aba5a9b Merge pull request #3248 from fatedier/dev
bump version
2023-01-10 10:26:56 +08:00
fatedier
a4189ba474 Merge branch 'dev' 2022-12-18 19:27:22 +08:00
fatedier
9ec84f8143 Merge pull request #3218 from fatedier/dev
release v0.46.0
2022-12-18 18:46:52 +08:00
fatedier
8ab474cc97 remove unsupported platform (#3148) 2022-10-27 10:22:47 +08:00
fatedier
a301046f3d Merge pull request #3147 from fatedier/dev
bump version
2022-10-26 23:18:40 +08:00
fatedier
8888610d83 Merge pull request #3010 from fatedier/dev
release v0.44.0
2022-07-11 00:10:43 +08:00
fatedier
fe5fb0326b Merge pull request #2955 from fatedier/dev
bump version to v0.43.0
2022-05-27 16:27:19 +08:00
fatedier
eb1e19a821 Merge pull request #2906 from fatedier/dev
bump version
2022-04-22 11:32:27 +08:00
fatedier
10f2620131 Merge pull request #2869 from fatedier/dev
bump version to v0.41.0
2022-03-23 21:19:59 +08:00
fatedier
ce677820c6 Merge pull request #2834 from fatedier/dev
bump version
2022-03-11 19:51:32 +08:00
fatedier
88fcc079e8 Merge pull request #2792 from fatedier/dev
bump version
2022-02-09 16:11:20 +08:00
fatedier
2dab5d0bca Merge pull request #2782 from fatedier/dev
bump version
2022-01-26 20:17:54 +08:00
fatedier
143750901e Merge pull request #2638 from fatedier/dev
bump version to v0.38.0
2021-10-25 20:31:13 +08:00
fatedier
997d406ec2 Merge pull request #2508 from fatedier/dev
bump version
2021-08-03 23:13:31 +08:00
fatedier
cfd1a3128a Merge pull request #2426 from fatedier/dev
update workflow file
2021-06-03 00:59:21 +08:00
fatedier
57577ea044 Merge pull request #2425 from fatedier/dev
bump version
2021-06-03 00:14:32 +08:00
fatedier
c5c79e4148 Merge pull request #2324 from fatedier/dev
bump version v0.36.2
2021-03-22 14:56:48 +08:00
fatedier
55da58eca4 Merge pull request #2310 from fatedier/dev
bump version
2021-03-18 11:14:56 +08:00
fatedier
76a1efccd9 update 2021-03-17 11:43:23 +08:00
fatedier
980f084ad1 Merge pull request #2302 from fatedier/dev
bump version
2021-03-15 21:54:52 +08:00
fatedier
3bf1eb8565 Merge pull request #2216 from fatedier/dev
bump version
2021-01-25 16:15:52 +08:00
fatedier
b2ae433e18 Merge pull request #2206 from fatedier/dev
bump version
2021-01-19 20:56:06 +08:00
fatedier
aa0a41ee4e Merge pull request #2088 from fatedier/dev
bump version to v0.34.3
2020-11-20 17:04:55 +08:00
fatedier
1ea1530b36 Merge pull request #2058 from fatedier/dev
bump version to v0.34.2
2020-11-06 14:50:50 +08:00
fatedier
e0c45a1aca Merge pull request #2018 from fatedier/dev
bump version to v0.34.1
2020-09-30 15:13:08 +08:00
fatedier
813c45f5c2 Merge pull request #1993 from fatedier/dev
bump version to v0.34.0
2020-09-20 00:30:51 +08:00
fatedier
aa74dc4646 Merge pull request #1990 from fatedier/dev
bump version to v0.34.0
2020-09-20 00:10:32 +08:00
fatedier
2406ecdfea Merge pull request #1780 from fatedier/dev
bump version
2020-04-27 16:50:34 +08:00
fatedier
8668fef136 Merge pull request #1728 from fatedier/dev
bump version to v0.32.1
2020-04-03 01:14:58 +08:00
fatedier
ea62bc5a34 remove vendor (#1697) 2020-03-11 14:39:43 +08:00
fatedier
23bb76397a Merge pull request #1696 from fatedier/dev
bump version to v0.32.0
2020-03-11 14:30:47 +08:00
fatedier
487c8d7c29 Merge pull request #1637 from fatedier/dev
bump version to v0.31.2
2020-02-04 21:54:28 +08:00
fatedier
f480160e2d Merge pull request #1596 from fatedier/dev
v0.31.1, fix bugs
2020-01-06 15:55:44 +08:00
fatedier
30c246c488 Merge pull request #1588 from fatedier/dev
bump version to v0.31.0
2020-01-03 11:45:22 +08:00
fatedier
75f3bce04d Merge pull request #1542 from fatedier/dev
bump version to v0.30.0
2019-11-28 14:21:27 +08:00
fatedier
adc3adc13b Merge pull request #1494 from fatedier/dev
bump version to v0.29.1
2019-11-02 21:14:50 +08:00
fatedier
e62d9a5242 Merge pull request #1415 from fatedier/dev
bump version to v0.29.0
2019-08-29 21:22:30 +08:00
fatedier
134a46c00b Merge pull request #1369 from fatedier/dev
bump version to v0.28.2
2019-08-09 12:59:13 +08:00
fatedier
ae08811636 Merge pull request #1364 from fatedier/dev
bump version to v0.28.1 and remove support for go1.11
2019-08-08 17:32:57 +08:00
fatedier
6451583e60 Merge pull request #1349 from fatedier/dev
bump version to v0.28.0
2019-08-01 14:04:55 +08:00
55 changed files with 1514 additions and 322 deletions

View File

@@ -20,23 +20,7 @@ jobs:
go-version: '1.23'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
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: v1.61
# 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
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true
version: v2.3

View File

@@ -1,4 +1,4 @@
name: "Close stale issues"
name: "Close stale issues and PRs"
on:
schedule:
- cron: "20 0 * * *"

3
.gitignore vendored
View File

@@ -39,3 +39,6 @@ client.key
# Cache
*.swp
# AI
CLAUDE.md

View File

@@ -1,139 +1,115 @@
service:
golangci-lint-version: 1.61.x # use the fixed version to not introduce new linters unexpectedly
version: "2"
run:
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 20m
build-tags:
- integ
- integfuzz
linters:
disable-all: true
default: none
enable:
- unused
- errcheck
- asciicheck
- copyloopvar
- errcheck
- gocritic
- gofumpt
- goimports
- revive
- gosimple
- gosec
- govet
- ineffassign
- lll
- makezero
- misspell
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- gci
- gosec
- asciicheck
- prealloc
- predeclared
- makezero
fast: false
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
disable:
- shadow
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- cancelled
- marshalled
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 160
# tab width in spaces. Default to 1.
tab-width: 1
gocritic:
disabled-checks:
- exitAfterDefer
unused:
check-exported: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
gci:
sections:
- standard
- default
- prefix(github.com/fatedier/frp/)
gosec:
severity: "low"
confidence: "low"
excludes:
- G401
- G402
- G404
- G501
- G115 # integer overflow conversion
- revive
- staticcheck
- unconvert
- unparam
- unused
settings:
errcheck:
check-type-assertions: false
check-blank: false
gocritic:
disabled-checks:
- exitAfterDefer
gosec:
excludes:
- G401
- G402
- G404
- G501
- G115
severity: low
confidence: low
govet:
disable:
- shadow
lll:
line-length: 160
tab-width: 1
misspell:
locale: US
ignore-rules:
- cancelled
- marshalled
unparam:
check-exported: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
- maligned
path: _test\.go$|^tests/|^samples/
- linters:
- revive
- staticcheck
text: use underscores in Go names
- linters:
- revive
text: unused-parameter
- linters:
- revive
text: "avoid meaningless package names"
- linters:
- unparam
text: is always false
paths:
- .*\.pb\.go
- .*\.gen\.go
- genfiles$
- vendor$
- bin$
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/fatedier/frp/)
exclusions:
generated: lax
paths:
- .*\.pb\.go
- .*\.gen\.go
- genfiles$
- vendor$
- bin$
- third_party$
- builtin$
- examples$
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
# exclude:
# - composite literal uses unkeyed fields
exclude-rules:
# Exclude some linters from running on test files.
- path: _test\.go$|^tests/|^samples/
linters:
- errcheck
- maligned
- linters:
- revive
- stylecheck
text: "use underscores in Go names"
- linters:
- revive
text: "unused-parameter"
- linters:
- unparam
text: "is always false"
exclude-dirs:
- genfiles$
- vendor$
- bin$
exclude-files:
- ".*\\.pb\\.go"
- ".*\\.gen\\.go"
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-issues-per-linter: 0
max-same-issues: 0

View File

@@ -612,6 +612,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.
@@ -1025,7 +1040,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`.
#### Proxy Protocol
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP.
frp supports Proxy Protocol to send user's real IP to local services.
Here is an example for https service:

View File

@@ -1,3 +1,7 @@
### Bug Fixes
## Features
* **VirtualNet:** Resolved various issues related to connection handling, TUN device management, and stability in the virtual network feature.
* Support tokenSource for loading authentication tokens from files
## Fixes
* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1

View File

@@ -48,7 +48,7 @@ type defaultConnectorImpl struct {
cfg *v1.ClientCommonConfig
muxSession *fmux.Session
quicConn quic.Connection
quicConn *quic.Conn
closeOnce sync.Once
}

View File

@@ -20,13 +20,11 @@ import (
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"
libio "github.com/fatedier/golib/io"
libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config/types"
@@ -35,6 +33,7 @@ import (
plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
)
@@ -176,24 +175,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
}
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
h := &pp.Header{
Command: pp.PROXY,
SourceAddr: connInfo.SrcAddr,
DestinationAddr: connInfo.DstAddr,
}
if strings.Contains(m.SrcAddr, ".") {
h.TransportProtocol = pp.TCPv4
} else {
h.TransportProtocol = pp.TCPv6
}
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
h.Version = 1
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
connInfo.ProxyProtocolHeader = h
// Use the common proxy protocol builder function
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
connInfo.ProxyProtocolHeader = header
}
connInfo.Conn = remote
connInfo.UnderlyingConn = workConn

View File

@@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
}

View File

@@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh)
go heartbeatFn(pxy.sendCh)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
}

View File

@@ -88,13 +88,16 @@ type ServiceOptions struct {
}
// setServiceOptionsDefault sets the default values for ServiceOptions.
func setServiceOptionsDefault(options *ServiceOptions) {
func setServiceOptionsDefault(options *ServiceOptions) error {
if options.Common != nil {
options.Common.Complete()
if err := options.Common.Complete(); err != nil {
return err
}
}
if options.ConnectorCreator == nil {
options.ConnectorCreator = NewConnector
}
return nil
}
// Service is the client service that connects to frps and provides proxy services.
@@ -134,7 +137,9 @@ type Service struct {
}
func NewService(options ServiceOptions) (*Service, error) {
setServiceOptionsDefault(&options)
if err := setServiceOptionsDefault(&options); err != nil {
return nil, err
}
var webServer *httppkg.Server
if options.Common.WebServer.Port > 0 {
@@ -325,10 +330,9 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
proxyCfgs := svr.proxyCfgs
visitorCfgs := svr.visitorCfgs
svr.cfgMu.RUnlock()
connEncrypted := true
if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" {
connEncrypted = false
}
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
sessionCtx := &SessionContext{
Common: svr.common,
RunID: svr.runID,
@@ -399,6 +403,10 @@ func (svr *Service) stop() {
svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
svr.ctl = nil
}
if svr.webServer != nil {
svr.webServer.Close()
svr.webServer = nil
}
}
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
go.mod
View File

@@ -10,13 +10,13 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.34.2
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.36.3
github.com/pelletier/go-toml/v2 v2.2.0
github.com/pion/stun/v2 v2.0.0
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.48.2
github.com/quic-go/quic-go v0.53.0
github.com/rodaine/table v1.2.0
github.com/samber/lo v1.47.0
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
@@ -46,12 +46,11 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
@@ -67,14 +66,14 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect
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.22.0 // 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.28.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.34.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect

38
go.sum
View File

@@ -14,7 +14,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -50,10 +49,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8=
github.com/google/pprof v0.0.0-20241206021119-61a79c692802/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -72,10 +72,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
@@ -94,6 +94,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -103,8 +105,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
@@ -152,6 +154,8 @@ github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -163,15 +167,13 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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/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=
@@ -239,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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
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/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=
@@ -261,8 +263,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -3,10 +3,10 @@
SCRIPT=$(readlink -f "$0")
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
ginkgo_command=$(which ginkgo 2>/dev/null)
if [ -z "$ginkgo_command" ]; then
# Check if ginkgo is available
if ! command -v ginkgo >/dev/null 2>&1; then
echo "ginkgo not found, try to install..."
go install github.com/onsi/ginkgo/v2/ginkgo@v2.17.1
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
fi
debug=false

View File

@@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) {
}
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
return common, nil
}
@@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni(
startProxy[s] = struct{}{}
}
startAll := true
if len(startProxy) > 0 {
startAll = false
}
startAll := len(startProxy) == 0
// Build template sections from range section And append to ini.File.
rangeSections := make([]*ini.Section, 0)

View File

@@ -26,20 +26,20 @@ import (
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
out := &v1.ClientCommonConfig{}
out.User = conf.User
out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod)
out.Auth.Token = conf.ClientConfig.Token
if conf.ClientConfig.AuthenticateHeartBeats {
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
out.Auth.Token = conf.Token
if conf.AuthenticateHeartBeats {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
}
if conf.ClientConfig.AuthenticateNewWorkConns {
if conf.AuthenticateNewWorkConns {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
}
out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience
out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope
out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL
out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams
out.Auth.OIDC.ClientID = conf.OidcClientID
out.Auth.OIDC.ClientSecret = conf.OidcClientSecret
out.Auth.OIDC.Audience = conf.OidcAudience
out.Auth.OIDC.Scope = conf.OidcScope
out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL
out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams
out.ServerAddr = conf.ServerAddr
out.ServerPort = conf.ServerPort
@@ -59,10 +59,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile
out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile
out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile
out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName
out.Transport.TLS.CertFile = conf.TLSCertFile
out.Transport.TLS.KeyFile = conf.TLSKeyFile
out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
out.Transport.TLS.ServerName = conf.TLSServerName
out.Log.To = conf.LogFile
out.Log.Level = conf.LogLevel
@@ -87,18 +87,18 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
out := &v1.ServerConfig{}
out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod)
out.Auth.Token = conf.ServerConfig.Token
if conf.ServerConfig.AuthenticateHeartBeats {
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
out.Auth.Token = conf.Token
if conf.AuthenticateHeartBeats {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
}
if conf.ServerConfig.AuthenticateNewWorkConns {
if conf.AuthenticateNewWorkConns {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
}
out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck
out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck
out.Auth.OIDC.Audience = conf.OidcAudience
out.Auth.OIDC.Issuer = conf.OidcIssuer
out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck
out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck
out.BindAddr = conf.BindAddr
out.BindPort = conf.BindPort

View File

@@ -206,7 +206,7 @@ func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section)
}
// plugin_xxx
cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
return nil
}

View File

@@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
return LoadConfigure(content, c, strict)
}
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
// This function handles both cases efficiently: with or without dot fields
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
var temp any
if err := yaml.Unmarshal(content, &temp); err != nil {
return err
}
// Remove dot fields if it's a map
if tempMap, ok := temp.(map[string]any); ok {
for key := range tempMap {
if strings.HasPrefix(key, ".") {
delete(tempMap, key)
}
}
}
// Convert to JSON and decode with strict validation
jsonBytes, err := json.Marshal(temp)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
func LoadConfigure(b []byte, c any, strict bool) error {
@@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error {
}
return decoder.Decode(c)
}
// It wasn't JSON. Unmarshal as YAML.
// Handle YAML content
if strict {
return yaml.UnmarshalStrict(b, c)
// In strict mode, always use our custom handler to support YAML merge
return parseYAMLWithDotFieldsHandling(b, c)
}
// Non-strict mode, parse normally
return yaml.Unmarshal(b, c)
}
@@ -182,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
}
@@ -250,7 +282,9 @@ func LoadClientConfig(path string, strict bool) (
}
if cliCfg != nil {
cliCfg.Complete()
if err := cliCfg.Complete(); err != nil {
return nil, nil, nil, isLegacyFormat, err
}
}
for _, c := range proxyCfgs {
c.Complete(cliCfg.User)

View File

@@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock"
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.Error(err)
}
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
// even in strict mode by properly handling dot-prefixed fields
func TestYAMLMergeInStrictMode(t *testing.T) {
require := require.New(t)
yamlContent := `
serverAddr: "127.0.0.1"
serverPort: 7000
.common: &common
type: stcp
secretKey: "test-secret"
localIP: 127.0.0.1
transport:
useEncryption: true
useCompression: true
proxies:
- name: ssh
localPort: 22
<<: *common
- name: web
localPort: 80
<<: *common
`
clientCfg := v1.ClientConfig{}
// This should work in strict mode
err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
require.NoError(err)
// Verify the merge worked correctly
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
require.Len(clientCfg.Proxies, 2)
// Check first proxy
sshProxy := clientCfg.Proxies[0].ProxyConfigurer
require.Equal("ssh", sshProxy.GetBaseConfig().Name)
require.Equal("stcp", sshProxy.GetBaseConfig().Type)
// Check second proxy
webProxy := clientCfg.Proxies[1].ProxyConfigurer
require.Equal("web", webProxy.GetBaseConfig().Name)
require.Equal("stcp", webProxy.GetBaseConfig().Type)
}
// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
func TestOptimizedYAMLProcessing(t *testing.T) {
require := require.New(t)
yamlWithDotFields := []byte(`
serverAddr: "127.0.0.1"
.common: &common
type: stcp
proxies:
- name: test
<<: *common
`)
yamlWithoutDotFields := []byte(`
serverAddr: "127.0.0.1"
proxies:
- name: test
type: tcp
localPort: 22
`)
// Test that YAML without dot fields works in strict mode
clientCfg := v1.ClientConfig{}
err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
// Test that YAML with dot fields still works in strict mode
err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
}
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
func TestYAMLEdgeCases(t *testing.T) {
require := require.New(t)
// Test array at root (should fail for frp config)
arrayYAML := []byte(`
- item1
- item2
`)
clientCfg := v1.ClientConfig{}
err := LoadConfigure(arrayYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test scalar at root (should fail for frp config)
scalarYAML := []byte(`"just a string"`)
err = LoadConfigure(scalarYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test empty object (should work)
emptyYAML := []byte(`{}`)
err = LoadConfigure(emptyYAML, &clientCfg, true)
require.NoError(err)
// Test nested structure without dots (should work)
nestedYAML := []byte(`
serverAddr: "127.0.0.1"
serverPort: 7000
`)
err = LoadConfigure(nestedYAML, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
}

View File

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

View File

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

View File

@@ -129,7 +129,7 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
if c.Plugin.ClientPluginOptions != nil {
c.Plugin.ClientPluginOptions.Complete()
c.Plugin.Complete()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -109,7 +109,7 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) {
m.info.ProxyTypeCounts[proxyType] = counter
proxyStats, ok := m.info.ProxyStatistics[name]
if !(ok && proxyStats.ProxyType == proxyType) {
if !ok || proxyStats.ProxyType != proxyType {
proxyStats = &ProxyStatistics{
Name: name,
ProxyType: proxyType,

View File

@@ -24,6 +24,7 @@ import (
"github.com/fatedier/golib/pool"
"github.com/fatedier/frp/pkg/msg"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
@@ -69,7 +70,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
}
}
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) {
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) {
var mu sync.RWMutex
udpConnMap := make(map[string]*net.UDPConn)
@@ -110,6 +111,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
if err != nil {
continue
}
mu.Lock()
udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()]
if !ok {
@@ -122,6 +124,18 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
}
mu.Unlock()
// Add proxy protocol header if configured
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
if err == nil {
// Prepend proxy protocol header to the UDP payload
finalBuf := make([]byte, len(ppBuf)+len(buf))
copy(finalBuf, ppBuf)
copy(finalBuf[len(ppBuf):], buf)
buf = finalBuf
}
}
_, err = udpConn.Write(buf)
if err != nil {
udpConn.Close()

View File

@@ -3,16 +3,16 @@ package udp
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUdpPacket(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
buf := []byte("hello world")
udpMsg := NewUDPPacket(buf, nil, nil)
newBuf, err := GetContent(udpMsg)
assert.NoError(err)
assert.EqualValues(buf, newBuf)
require.NoError(err)
require.EqualValues(buf, newBuf)
}

View File

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

View File

@@ -3,21 +3,21 @@ package metric
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCounter(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
c := NewCounter()
c.Inc(10)
assert.EqualValues(10, c.Count())
require.EqualValues(10, c.Count())
c.Dec(5)
assert.EqualValues(5, c.Count())
require.EqualValues(5, c.Count())
cTmp := c.Snapshot()
assert.EqualValues(5, cTmp.Count())
require.EqualValues(5, cTmp.Count())
c.Clear()
assert.EqualValues(0, c.Count())
require.EqualValues(0, c.Count())
}

View File

@@ -3,25 +3,25 @@ package metric
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDateCounter(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
dc := NewDateCounter(3)
dc.Inc(10)
assert.EqualValues(10, dc.TodayCount())
require.EqualValues(10, dc.TodayCount())
dc.Dec(5)
assert.EqualValues(5, dc.TodayCount())
require.EqualValues(5, dc.TodayCount())
counts := dc.GetLastDaysCount(3)
assert.EqualValues(3, len(counts))
assert.EqualValues(5, counts[0])
assert.EqualValues(0, counts[1])
assert.EqualValues(0, counts[2])
require.EqualValues(3, len(counts))
require.EqualValues(5, counts[0])
require.EqualValues(0, counts[1])
require.EqualValues(0, counts[2])
dcTmp := dc.Snapshot()
assert.EqualValues(5, dcTmp.TodayCount())
require.EqualValues(5, dcTmp.TodayCount())
}

View File

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

View File

@@ -0,0 +1,45 @@
// 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 net
import (
"bytes"
"fmt"
"net"
pp "github.com/pires/go-proxyproto"
)
func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header {
var versionByte byte
if version == "v1" {
versionByte = 1
} else {
versionByte = 2 // default to v2
}
return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr)
}
func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) {
h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version)
// Convert header to bytes using a buffer
var buf bytes.Buffer
_, err := h.WriteTo(&buf)
if err != nil {
return nil, fmt.Errorf("failed to write proxy protocol header: %v", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,178 @@
package net
import (
"net"
"testing"
pp "github.com/pires/go-proxyproto"
"github.com/stretchr/testify/require"
)
func TestBuildProxyProtocolHeader(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectError bool
}{
{
name: "UDP IPv4 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv4 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "UDP IPv6 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv6 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectError: false,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
}
for _, tt := range tests {
header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version)
if tt.expectError {
require.Error(err, "test case: %s", tt.name)
continue
}
require.NoError(err, "test case: %s", tt.name)
require.NotEmpty(header, "test case: %s", tt.name)
}
}
func TestBuildProxyProtocolHeaderStruct(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectedProtocol pp.AddressFamilyAndProtocol
expectedVersion byte
expectedCommand pp.ProtocolVersionAndCommand
expectedSourceAddr net.Addr
expectedDestAddr net.Addr
}{
{
name: "TCP IPv4 v2",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v2",
expectedProtocol: pp.TCPv4,
expectedVersion: 2,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
},
{
name: "UDP IPv6 v1",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v1",
expectedProtocol: pp.UDPv6,
expectedVersion: 1,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
},
{
name: "TCP IPv6 default version",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "",
expectedProtocol: pp.TCPv6,
expectedVersion: 2, // default to v2
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil
expectedDestAddr: nil,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil
expectedDestAddr: nil,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types
expectedDestAddr: nil,
},
}
for _, tt := range tests {
header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version)
require.NotNil(header, "test case: %s", tt.name)
require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name)
require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name)
require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name)
require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name)
require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name)
}
}

View File

@@ -3,45 +3,41 @@ package util
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRandId(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
id, err := RandID()
assert.NoError(err)
require.NoError(err)
t.Log(id)
assert.Equal(16, len(id))
require.Equal(16, len(id))
}
func TestGetAuthKey(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
key := GetAuthKey("1234", 1488720000)
assert.Equal("6df41a43725f0c770fd56379e12acf8c", key)
require.Equal("6df41a43725f0c770fd56379e12acf8c", key)
}
func TestParseRangeNumbers(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
numbers, err := ParseRangeNumbers("2-5")
if assert.NoError(err) {
assert.Equal([]int64{2, 3, 4, 5}, numbers)
}
require.NoError(err)
require.Equal([]int64{2, 3, 4, 5}, numbers)
numbers, err = ParseRangeNumbers("1")
if assert.NoError(err) {
assert.Equal([]int64{1}, numbers)
}
require.NoError(err)
require.Equal([]int64{1}, numbers)
numbers, err = ParseRangeNumbers("3-5,8")
if assert.NoError(err) {
assert.Equal([]int64{3, 4, 5, 8}, numbers)
}
require.NoError(err)
require.Equal([]int64{3, 4, 5, 8}, numbers)
numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ")
if assert.NoError(err) {
assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers)
}
require.NoError(err)
require.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers)
_, err = ParseRangeNumbers("3-a")
assert.Error(err)
require.Error(err)
}

View File

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

View File

@@ -225,11 +225,7 @@ func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (
// *.example.com
// *.com
domainSplit := strings.Split(domain, ".")
for {
if len(domainSplit) < 3 {
break
}
for len(domainSplit) >= 3 {
domainSplit[0] = "*"
domain = strings.Join(domainSplit, ".")
vr, ok = findRouter(domain, location, routeByHTTPUser)

View File

@@ -169,11 +169,7 @@ func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) {
}
domainSplit := strings.Split(name, ".")
for {
if len(domainSplit) < 3 {
break
}
for len(domainSplit) >= 3 {
domainSplit[0] = "*"
name = strings.Join(domainSplit, ".")

View File

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

View File

@@ -262,7 +262,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)
}
@@ -550,7 +550,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
return
}
// Start a new goroutine to handle connection.
go func(ctx context.Context, frpConn quic.Connection) {
go func(ctx context.Context, frpConn *quic.Conn) {
for {
stream, err := frpConn.AcceptStream(context.Background())
if err != nil {

View File

@@ -24,12 +24,14 @@ type generalTestConfigures struct {
}
func renderBindPortConfig(protocol string) string {
if protocol == "kcp" {
switch protocol {
case "kcp":
return fmt.Sprintf(`kcp_bind_port = {{ .%s }}`, consts.PortServerName)
} else if protocol == "quic" {
case "quic":
return fmt.Sprintf(`quic_bind_port = {{ .%s }}`, consts.PortServerName)
default:
return ""
}
return ""
}
func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {

View File

@@ -223,7 +223,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
handler := func(req *plugin.Request) *plugin.Response {
var ret plugin.Response
content := req.Content.(*plugin.PingContent)
record = content.Ping.PrivilegeKey
record = content.PrivilegeKey
ret.Unchange = true
return &ret
}
@@ -273,7 +273,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
handler := func(req *plugin.Request) *plugin.Response {
var ret plugin.Response
content := req.Content.(*plugin.NewWorkConnContent)
record = content.NewWorkConn.RunID
record = content.RunID
ret.Unchange = true
return &ret
}

View File

@@ -24,12 +24,14 @@ type generalTestConfigures struct {
}
func renderBindPortConfig(protocol string) string {
if protocol == "kcp" {
switch protocol {
case "kcp":
return fmt.Sprintf(`kcpBindPort = {{ .%s }}`, consts.PortServerName)
} else if protocol == "quic" {
case "quic":
return fmt.Sprintf(`quicBindPort = {{ .%s }}`, consts.PortServerName)
default:
return ""
}
return ""
}
func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {

View File

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

View File

@@ -227,6 +227,56 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
})
})
ginkgo.It("UDP", func() {
serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig
localPort := f.AllocPort()
localServer := streamserver.New(streamserver.UDP, streamserver.WithBindPort(localPort),
streamserver.WithCustomHandler(func(c net.Conn) {
defer c.Close()
rd := bufio.NewReader(c)
ppHeader, err := pp.Read(rd)
if err != nil {
log.Errorf("read proxy protocol error: %v", err)
return
}
// Read the actual UDP content after proxy protocol header
if _, err := rpc.ReadBytes(rd); err != nil {
return
}
buf := []byte(ppHeader.SourceAddr.String())
_, _ = rpc.WriteBytes(c, buf)
}))
f.RunServer("", localServer)
remotePort := f.AllocPort()
clientConf += fmt.Sprintf(`
[[proxies]]
name = "udp"
type = "udp"
localPort = %d
remotePort = %d
transport.proxyProtocolVersion = "v2"
`, localPort, remotePort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Protocol("udp").Port(remotePort).Ensure(func(resp *request.Response) bool {
log.Tracef("udp proxy protocol get SourceAddr: %s", string(resp.Content))
addr, err := net.ResolveUDPAddr("udp", string(resp.Content))
if err != nil {
return false
}
if addr.IP.String() != "127.0.0.1" {
return false
}
return true
})
})
ginkgo.It("HTTP", func() {
vhostHTTPPort := f.AllocPort()
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`

View File

@@ -232,7 +232,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
handler := func(req *plugin.Request) *plugin.Response {
var ret plugin.Response
content := req.Content.(*plugin.PingContent)
record = content.Ping.PrivilegeKey
record = content.PrivilegeKey
ret.Unchange = true
return &ret
}
@@ -284,7 +284,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
handler := func(req *plugin.Request) *plugin.Response {
var ret plugin.Response
content := req.Content.(*plugin.NewWorkConnContent)
record = content.NewWorkConn.RunID
record = content.RunID
ret.Unchange = true
return &ret
}