Compare commits

...

58 Commits

Author SHA1 Message Date
fatedier
2d3af8a108 Merge pull request #3651 from fatedier/dev
bump version to v0.52.0
2023-10-10 17:24:07 +08:00
fatedier
3ae1a4f45a update confugration examples and README (#3650) 2023-10-10 16:48:13 +08:00
ZeroVocabulary
21d8e674f0 mentioned antivirus detection in installation instructions, added recommendation to whitelist client binaries in antivirus (#3647) 2023-10-09 15:05:03 +08:00
fatedier
5e70d5bee0 code optimization (#3625) 2023-09-20 15:18:50 +08:00
Zeyu Dong
5c8ea51eb5 return ssl alert unrecognized_name when https domain not registered (#3620) 2023-09-18 14:28:05 +08:00
fatedier
bae0b4d7c0 optimize the code of the command line (#3614) 2023-09-15 10:33:32 +08:00
fatedier
74255f711e config: add some validations (#3610) 2023-09-13 18:59:51 +08:00
fatedier
7cd02f5bd8 add e2e tests for v1 config (#3608) 2023-09-13 16:32:39 +08:00
fatedier
c95311d1a0 support yaml/json/toml configuration format, make ini deprecated (#3599) 2023-09-06 10:18:02 +08:00
fatedier
885b029fcf remove arch 386 (#3593) 2023-08-31 11:16:20 +08:00
fatedier
466d69eae0 Merge pull request #3574 from fatedier/dev
release v0.51.3
2023-08-14 11:59:09 +08:00
fatedier
f1454e91f5 support go1.21 (#3573) 2023-08-14 11:10:38 +08:00
fatedier
7c8cbeb250 Merge pull request #3550 from fatedier/dev
release v0.51.2
2023-07-25 21:35:08 +08:00
fatedier
e9e12cf888 fix incorrect use of snappy pool (#3549) 2023-07-25 21:31:26 +08:00
fatedier
6430afcfa5 fix a goroutine leak issue caused by Login plugin timeout (#3547) 2023-07-25 15:12:40 +08:00
fatedier
3235addaaa update dependencies (#3539) 2023-07-21 14:34:44 +08:00
fatedier
46ff40543a update github actions (#3538) 2023-07-21 10:30:46 +08:00
fatedier
4fd6301577 Merge pull request #3537 from fatedier/dev
release v0.51.1
2023-07-20 22:38:48 +08:00
fatedier
efcc028a3d fix a race condition issue (#3536) 2023-07-20 22:32:32 +08:00
fatedier
90861b6821 update golib (#3532) 2023-07-17 17:27:43 +08:00
fatedier
8f105adbca update FUNDING.yml (#3520) 2023-07-06 19:58:50 +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
b1789afbab update version (#3516) 2023-07-05 20:35:08 +08:00
fatedier
88c7e8bf7c update doc (#3512) 2023-07-02 00:35:33 +08:00
fatedier
fc4e787fe2 frpc: support stop command (#3511) 2023-06-30 17:35:37 +08:00
fatedier
4c4d5f0d0d service.Run supports passing in context (#3504) 2023-06-29 18:04:20 +08:00
fatedier
801e8c6742 support wss between frpc and frps (#3503) 2023-06-29 11:20:45 +08:00
fatedier
4fd800bc48 Merge pull request #3499 from fatedier/dev
release v0.50.0
2023-06-26 17:03:56 +08:00
fatedier
b146989703 add release notes for v0.50.0 (#3498) 2023-06-26 16:48:14 +08:00
fatedier
685d7618f3 change default value of tls_enable and disable_custom_tls_first_byte (#3494) 2023-06-26 00:10:27 +08:00
fatedier
15a245766e fix the issue of duplicate xtcp proxies will cause the previous proxy to become ineffective (#3489) 2023-06-16 00:41:06 +08:00
fatedier
e1cef053be server/proxy: simplify the code (#3488) 2023-06-16 00:14:19 +08:00
fatedier
9ba6a06470 e2e: update 2023-06-04 20:11:07 +08:00
fatedier
ea08de668e yamux: change default MaxStreamWindowSize to 6MB (#3474) 2023-06-04 19:58:55 +08:00
fatedier
de85c9455a stcp, xtcp, sudp: support allow_users and specified server user (#3472) 2023-06-02 16:06:29 +08:00
fatedier
cceab7e1b1 client/proxy: simplify the code (#3465) 2023-05-30 22:18:56 +08:00
fatedier
9aef3b9944 adjust code style about config (#3464) 2023-05-30 20:25:22 +08:00
fatedier
341a5e3e3a frps dashboard: remove bind_udp_port (#3463) 2023-05-30 15:14:05 +08:00
fatedier
c7a0cfc66d xtcp: when connection timeout occurs, support fallback to STCP (#3460) 2023-05-30 10:55:00 +08:00
fatedier
555db9d272 format import package name (#3455) 2023-05-29 14:10:34 +08:00
fatedier
0d6d968fe8 Merge pull request #3454 from fatedier/dev
release v0.49.0
2023-05-29 01:12:26 +08:00
fatedier
98068402c8 add release notes (#3453) 2023-05-29 01:09:53 +08:00
fatedier
4915852b9c use constant time comparison (#3452) 2023-05-29 00:27:27 +08:00
fatedier
756dd1ad5e add e2e case for xtcp (#3451) 2023-05-28 23:06:35 +08:00
fatedier
c71efde303 refactor the code related to xtcp (#3449) 2023-05-28 16:50:43 +08:00
fatedier
9f029e3248 update Build Status 2023-05-17 15:24:42 +08:00
fatedier
8095075719 update sponsors 2023-05-17 15:22:18 +08:00
Haruka Abe
2225a1781f fix: typo tot => to (#3434)
Co-authored-by: Haruka ABE <abe_haruka@grenge.co.jp>
2023-05-08 21:28:44 +08:00
Aarni Koskela
0214b974dd Fix typo: dasboard -> dashboard (#3416) 2023-04-18 21:25:32 +08:00
fatedier
738c53ce47 web: fix el-popover bug (#3413)
* web: update

* web: fix el-popover bug
2023-04-16 16:43:44 +08:00
hxysayhi
db52f07d34 Update frpc_full.ini (#3399)
Fix typos.
2023-04-12 11:02:56 +08:00
fatedier
f6b8645f56 nathole: support discover without configuration file (#3395) 2023-04-10 21:45:00 +08:00
fatedier
2c2c4ecdbc feat(nathole): add flag for STUN server in nathole discovery command (#3383) 2023-03-30 22:02:00 +08:00
fatedier
3faae194d0 feat(nathole): use serverUDPPort in nathole discovery when available (#3382) 2023-03-30 21:49:12 +08:00
fatedier
a22d6c9504 frpc: support nathole discover (#3381) 2023-03-30 20:28:15 +08:00
fatedier
9800b4cfcf feat(pull request): add pull request template including summary and walkthrough (#3380) 2023-03-30 11:49:31 +08:00
fatedier
8f394dba27 trace log adds join errors (#3352) 2023-03-11 19:34:06 +08:00
fatedier
fccd518512 frpc: add parameter nat_hole_server_addr (#3351) 2023-03-10 15:51:14 +08:00
232 changed files with 14721 additions and 8972 deletions

View File

@@ -2,7 +2,7 @@ version: 2
jobs:
go-version-latest:
docker:
- image: cimg/go:1.20-node
- image: cimg/go:1.21-node
resource_class: large
steps:
- checkout
@@ -10,7 +10,7 @@ jobs:
- run: make alltest
go-version-last:
docker:
- image: cimg/go:1.19-node
- image: cimg/go:1.20-node
resource_class: large
steps:
- checkout

1
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,4 @@
# These are supported funding model platforms
github: [fatedier]
custom: ["https://afdian.net/a/fatedier"]

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
### Summary
copilot:summary
### WHY
<!-- author to complete -->

View File

@@ -61,7 +61,7 @@ jobs:
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
- name: Build and push frpc
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./dockerfiles/Dockerfile-for-frpc
@@ -72,7 +72,7 @@ jobs:
${{ env.TAG_FRPC_GPR }}
- name: Build and push frps
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./dockerfiles/Dockerfile-for-frps

View File

@@ -14,15 +14,15 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
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.51
version: v1.53
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0

View File

@@ -13,18 +13,18 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- name: Make All
run: |
./package.sh
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
uses: goreleaser/goreleaser-action@v4
with:
version: latest
args: release --rm-dist --release-notes=./Release.md
args: release --clean --release-notes=./Release.md
env:
GITHUB_TOKEN: ${{ secrets.GPR_TOKEN }}

View File

@@ -18,7 +18,7 @@ jobs:
pull-requests: write # for actions/stale to close stale PRs
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ packages/
release/
test/bin/
vendor/
lastversion/
dist/
.idea/
.vscode/

View File

@@ -120,16 +120,18 @@ issues:
# - composite literal uses unkeyed fields
exclude-rules:
# Exclude some linters from running on test files.
- path: _test\.go$|^tests/|^samples/
linters:
- errcheck
- maligned
# keep it until we only support go1.20
- linters:
- staticcheck
text: "SA1019: rand.Seed has been deprecated"
# 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"
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all

View File

@@ -19,6 +19,9 @@ fmt:
fmt-more:
gofumpt -l -w .
gci:
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
vet:
go vet ./...
@@ -43,8 +46,23 @@ e2e:
e2e-trace:
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
e2e-compatibility-last-frpc:
if [ ! -d "./lastversion" ]; then \
TARGET_DIRNAME=lastversion ./hack/download.sh; \
fi
FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh
rm -r ./lastversion
e2e-compatibility-last-frps:
if [ ! -d "./lastversion" ]; then \
TARGET_DIRNAME=lastversion ./hack/download.sh; \
fi
FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh
rm -r ./lastversion
alltest: vet gotest e2e
clean:
rm -f ./bin/frpc
rm -f ./bin/frps
rm -rf ./lastversion

View File

@@ -2,7 +2,7 @@ export PATH := $(GOPATH)/bin:$(PATH)
export GO111MODULE=on
LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
all: build
@@ -19,8 +19,6 @@ app:
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\
echo "Build $${os}-$${arch} done";\
)
@mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe
@mv ./release/frps_windows_386 ./release/frps_windows_386.exe
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe
@mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe

1005
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
# frp
[![Build Status](https://travis-ci.org/fatedier/frp.svg?branch=master)](https://travis-ci.org/fatedier/frp)
[![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/fatedier/frp)
[![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases)
[README](README.md) | [中文文档](README_zh.md)
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
<h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start-->
@@ -13,10 +13,6 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a>
<a>&nbsp</a>
<a href="https://asocks.com/c/vDu6Dk" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_asocks.jpg">
</a>
</p>
<!--gold sponsors end-->
@@ -24,12 +20,13 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
通过在具有公网 IP 的节点上部署 frp 服务端,可以轻松地将内网服务穿透到公网,同时提供诸多专业的功能特性,这包括:
* 客户端服务端通信支持 TCP、KCP 以及 Websocket 等多种协议。
* 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间。
* 客户端服务端通信支持 TCP、QUIC、KCP 以及 Websocket 等多种协议。
* 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间,降低请求延迟
* 代理组间的负载均衡。
* 端口复用,多个服务通过同一个服务端端口暴露。
* 多个原生支持的客户端插件静态文件查看HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作
* 高度扩展性的服务端插件系统,方便结合自身需求进行功能扩展
* 支持 P2P 通信,流量不经过服务器中转,充分利用带宽资源
* 多个原生支持的客户端插件静态文件查看HTTPS/HTTP 协议转换HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作
* 高度扩展性的服务端插件系统,易于结合自身需求进行功能扩展。
* 服务端和客户端 UI 页面。
## 开发状态
@@ -38,10 +35,24 @@ frp 目前已被很多公司广泛用于测试、生产环境。
master 分支用于发布稳定版本dev 分支用于开发,您可以尝试下载最新的 release 版本进行测试。
我们正在进行 v2 大版本的开发,将会尝试在各个方面进行重构和升级,且不会与 v1 版本进行兼容,预计会持续一段时间。
我们正在进行 v2 大版本的开发,将会尝试在各个方面进行重构和升级,且不会与 v1 版本进行兼容,预计会持续较长的一段时间。
现在的 v0 版本将会在合适的时间切换为 v1 版本并且保证兼容性,后续只做 bug 修复和优化,不再进行大的功能性更新。
### 关于 v2 的一些说明
当前整体形势不佳,面临的生活工作压力很大。
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构。
v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceMesh 方面的工作经验和思考。它的核心是一个现代化的四层和七层代理,类似于 envoy。这个代理本身高度可扩展不仅可以用于实现内网穿透的功能还可以应用于更多领域。在这个高度可扩展的内核基础上我们将实现 frp v1 中的所有功能,并且能够以一种更加优雅的方式实现原先架构中无法实现或不易实现的功能。同时,我们将保持高效的开发和迭代能力。
除此之外,我希望 frp 本身也成为一个高度可扩展的系统和平台,就像我们可以基于 K8s 提供一系列扩展能力一样。在 K8s 上,我们可以根据企业需求进行定制化开发,例如使用 CRD、controller 模式、webhook、CSI 和 CNI 等。在 frp v1 中,我们引入了服务端插件的概念,实现了一些简单的扩展性。但是,它实际上依赖于简单的 HTTP 协议,并且需要用户自己启动独立的进程和管理。这种方式远远不够灵活和方便,而且现实世界的需求千差万别,我们不能期望一个由少数人维护的非营利性开源项目能够满足所有人的需求。
最后,我们意识到像配置管理、权限验证、证书管理和管理 API 等模块的当前设计并不够现代化。尽管我们可能在 v1 版本中进行一些优化,但确保兼容性是一个令人头疼的问题,需要投入大量精力来解决。
非常感谢您对 frp 的支持。
## 文档
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org/docs)。
@@ -59,26 +70,26 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
##
##
如果您觉得 frp 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。
### GitHub Sponsors
### Sponsors
长期赞助可以帮助我们保持项目的持续发展。
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
### 知识星球
如果您想学习 frp 相关的知识和技术,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg)
### 支付宝扫码捐赠
![donate-alipay](/doc/pic/donate-alipay.png)
### 微信支付捐赠
![donate-wechatpay](/doc/pic/donate-wechatpay.png)

View File

@@ -1,8 +1,9 @@
### New
### Features
* The `httpconnect` type in `tcpmux` now supports authentication through the parameters `http_user` and `http_pwd`.
* Configuration: We now support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. #2521
### Improved
### Breaking Changes
* The web framework has been upgraded to vue3 + element-plus, and the dashboard has added some information display and supports dark mode.
* The e2e testing has been switched to ginkgo v2.
* Change the way to start the visitor through the command line from `frpc stcp --role=visitor xxx` to `frpc stcp visitor xxx`.
* Modified the semantics of the `server_addr` in the command line, no longer including the port. Added the `server_port` parameter to configure the port.
* No longer support range ports mapping in TOML/YAML/JSON.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<head>
<meta charset="utf-8">
<title>frp client admin UI</title>
<script type="module" crossorigin src="./index-7dd223da.js"></script>
<link rel="stylesheet" href="./index-aa3c7267.css">
<script type="module" crossorigin src="./index-1c7ed8b0.js"></script>
<link rel="stylesheet" href="./index-1e2a7ce0.css">
</head>
<body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-b8250b3f.js"></script>
<link rel="stylesheet" href="./index-7b4711f8.css">
<script type="module" crossorigin src="./index-ea3edf22.js"></script>
<link rel="stylesheet" href="./index-1e0c7400.css">
</head>
<body>

View File

@@ -23,7 +23,7 @@ import (
"github.com/gorilla/mux"
"github.com/fatedier/frp/assets"
frpNet "github.com/fatedier/frp/pkg/util/net"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
var (
@@ -38,7 +38,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {
router.HandleFunc("/healthz", svr.healthz)
// debug
if svr.cfg.PprofEnable {
if svr.cfg.WebServer.PprofEnable {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
@@ -47,18 +47,19 @@ func (svr *Service) RunAdminServer(address string) (err error) {
}
subRouter := router.NewRoute().Subrouter()
user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)
user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
// api, see admin_api.go
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
// view
subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
})

View File

@@ -24,11 +24,14 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/util"
)
type GeneralResponse struct {
@@ -37,12 +40,12 @@ type GeneralResponse struct {
}
// /healthz
func (svr *Service) healthz(w http.ResponseWriter, r *http.Request) {
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
}
// GET api/reload
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
// GET /api/reload
func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("api request [/api/reload]")
@@ -54,15 +57,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
}
}()
_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
return
}
if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
return
}
if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
res.Code = 500
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
@@ -71,6 +80,22 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
log.Info("success reload conf")
}
// POST /api/stop
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("api request [/api/stop]")
defer func() {
log.Info("api response [/api/stop], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
go svr.GracefulClose(100 * time.Millisecond)
}
type StatusResp map[string][]ProxyStatusResp
type ProxyStatusResp struct {
@@ -90,23 +115,23 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
Status: status.Phase,
Err: status.Err,
}
baseCfg := status.Cfg.GetBaseInfo()
baseCfg := status.Cfg.GetBaseConfig()
if baseCfg.LocalPort != 0 {
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
}
psr.Plugin = baseCfg.Plugin
psr.Plugin = baseCfg.Plugin.Type
if status.Err == "" {
psr.RemoteAddr = status.RemoteAddr
if util.InSlice(status.Type, []string{"tcp", "udp"}) {
if lo.Contains([]string{"tcp", "udp"}, status.Type) {
psr.RemoteAddr = serverAddr + psr.RemoteAddr
}
}
return psr
}
// GET api/status
func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
// GET /api/status
func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
var (
buf []byte
res StatusResp = make(map[string][]ProxyStatusResp)
@@ -134,8 +159,8 @@ func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
}
}
// GET api/config
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
// GET /api/config
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("Http get request [/api/config]")
@@ -154,27 +179,17 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
return
}
content, err := config.GetRenderedConfFromFile(svr.cfgFile)
content, err := os.ReadFile(svr.cfgFile)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("load frpc config file error: %s", res.Msg)
return
}
rows := strings.Split(string(content), "\n")
newRows := make([]string, 0, len(rows))
for _, row := range rows {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
continue
}
newRows = append(newRows, row)
}
res.Msg = strings.Join(newRows, "\n")
res.Msg = string(content)
}
// PUT api/config
// PUT /api/config
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
@@ -203,49 +218,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
return
}
// get token from origin content
token := ""
b, err := os.ReadFile(svr.cfgFile)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("load frpc config file error: %s", res.Msg)
return
}
content := string(b)
for _, row := range strings.Split(content, "\n") {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
token = row
break
}
}
tmpRows := make([]string, 0)
for _, row := range strings.Split(string(body), "\n") {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
continue
}
tmpRows = append(tmpRows, row)
}
newRows := make([]string, 0)
if token != "" {
for _, row := range tmpRows {
newRows = append(newRows, row)
if strings.HasPrefix(row, "[common]") {
newRows = append(newRows, token)
}
}
} else {
newRows = tmpRows
}
content = strings.Join(newRows, "\n")
err = os.WriteFile(svr.cfgFile, []byte(content), 0o644)
if err != nil {
if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil {
res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warn("%s", res.Msg)

View File

@@ -23,24 +23,32 @@ import (
"github.com/fatedier/golib/control/shutdown"
"github.com/fatedier/golib/crypto"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/client/visitor"
"github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
)
type Control struct {
// uniq id got from frps, attach it in loginMsg
// service context
ctx context.Context
xl *xlog.Logger
// Unique ID obtained from frps.
// It should be attached to the login message when reconnecting.
runID string
// manage all proxies
pxyCfgs map[string]config.ProxyConf
pxyCfgs []v1.ProxyConfigurer
pm *proxy.Manager
// manage all visitors
vm *VisitorManager
vm *visitor.Manager
// control connection
conn net.Conn
@@ -62,34 +70,29 @@ type Control struct {
lastPong time.Time
// The client configuration
clientCfg config.ClientCommonConf
clientCfg *v1.ClientCommonConfig
readerShutdown *shutdown.Shutdown
writerShutdown *shutdown.Shutdown
msgHandlerShutdown *shutdown.Shutdown
// The UDP port that the server is listening on
serverUDPPort int
xl *xlog.Logger
// service context
ctx context.Context
// sets authentication based on selected method
authSetter auth.Setter
msgTransporter transport.MessageTransporter
}
func NewControl(
ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
clientCfg config.ClientCommonConf,
pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf,
serverUDPPort int,
clientCfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
authSetter auth.Setter,
) *Control {
// new xlog instance
ctl := &Control{
ctx: ctx,
xl: xlog.FromContextSafe(ctx),
runID: runID,
conn: conn,
cm: cm,
@@ -102,14 +105,12 @@ func NewControl(
readerShutdown: shutdown.New(),
writerShutdown: shutdown.New(),
msgHandlerShutdown: shutdown.New(),
serverUDPPort: serverUDPPort,
xl: xlog.FromContextSafe(ctx),
ctx: ctx,
authSetter: authSetter,
}
ctl.pm = proxy.NewManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort)
ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
ctl.vm = NewVisitorManager(ctl.ctx, ctl)
ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
ctl.vm.Reload(visitorCfgs)
return ctl
}
@@ -124,7 +125,7 @@ func (ctl *Control) Run() {
go ctl.vm.Run()
}
func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
xl := ctl.xl
workConn, err := ctl.connectServer()
if err != nil {
@@ -173,6 +174,16 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
}
}
func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
xl := ctl.xl
// Dispatch the NatHoleResp message to the related proxy.
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
if !ok {
xl.Trace("dispatch NatHoleResp message to related proxy error")
}
}
func (ctl *Control) Close() error {
return ctl.GracefulClose(0)
}
@@ -188,7 +199,7 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
return nil
}
// ClosedDoneCh returns a channel which will be closed after all resources are released
// ClosedDoneCh returns a channel that will be closed after all resources are released
func (ctl *Control) ClosedDoneCh() <-chan struct{} {
return ctl.closedDoneCh
}
@@ -210,7 +221,7 @@ func (ctl *Control) reader() {
defer ctl.readerShutdown.Done()
defer close(ctl.closedCh)
encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token))
encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
for {
m, err := msg.ReadMsg(encReader)
if err != nil {
@@ -230,7 +241,7 @@ func (ctl *Control) reader() {
func (ctl *Control) writer() {
xl := ctl.xl
defer ctl.writerShutdown.Done()
encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Token))
encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
if err != nil {
xl.Error("crypto new writer error: %v", err)
ctl.conn.Close()
@@ -250,7 +261,7 @@ func (ctl *Control) writer() {
}
}
// msgHandler handles all channel events and do corresponding operations.
// msgHandler handles all channel events and performs corresponding operations.
func (ctl *Control) msgHandler() {
xl := ctl.xl
defer func() {
@@ -264,15 +275,16 @@ func (ctl *Control) msgHandler() {
var hbSendCh <-chan time.Time
// TODO(fatedier): disable heartbeat if TCPMux is enabled.
// Just keep it here to keep compatible with old version frps.
if ctl.clientCfg.HeartbeatInterval > 0 {
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second)
defer hbSend.Stop()
hbSendCh = hbSend.C
}
var hbCheckCh <-chan time.Time
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 &&
!lo.FromPtr(ctl.clientCfg.Transport.TCPMux) {
hbCheck := time.NewTicker(time.Second)
defer hbCheck.Stop()
hbCheckCh = hbCheck.C
@@ -291,7 +303,7 @@ func (ctl *Control) msgHandler() {
}
ctl.sendCh <- pingMsg
case <-hbCheckCh:
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
// let reader() stop
ctl.conn.Close()
@@ -307,6 +319,8 @@ func (ctl *Control) msgHandler() {
go ctl.HandleReqWorkConn(m)
case *msg.NewProxyResp:
ctl.HandleNewProxyResp(m)
case *msg.NatHoleResp:
ctl.HandleNatHoleResp(m)
case *msg.Pong:
if m.Error != "" {
xl.Error("Pong contains error: %s", m.Error)
@@ -342,7 +356,7 @@ func (ctl *Control) worker() {
ctl.cm.Close()
}
func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
ctl.vm.Reload(visitorCfgs)
ctl.pm.Reload(pxyCfgs)
return nil

View File

@@ -21,8 +21,10 @@ import (
"io"
"net"
"net/http"
"strings"
"time"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -49,26 +51,33 @@ type Monitor struct {
cancel context.CancelFunc
}
func NewMonitor(ctx context.Context, checkType string,
intervalS int, timeoutS int, maxFailedTimes int,
addr string, url string,
func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
statusNormalFn func(), statusFailedFn func(),
) *Monitor {
if intervalS <= 0 {
intervalS = 10
if cfg.IntervalSeconds <= 0 {
cfg.IntervalSeconds = 10
}
if timeoutS <= 0 {
timeoutS = 3
if cfg.TimeoutSeconds <= 0 {
cfg.TimeoutSeconds = 3
}
if maxFailedTimes <= 0 {
maxFailedTimes = 1
if cfg.MaxFailed <= 0 {
cfg.MaxFailed = 1
}
newctx, cancel := context.WithCancel(ctx)
var url string
if cfg.Type == "http" && cfg.Path != "" {
s := "http://" + addr
if !strings.HasPrefix(cfg.Path, "/") {
s += "/"
}
url = s + cfg.Path
}
return &Monitor{
checkType: checkType,
interval: time.Duration(intervalS) * time.Second,
timeout: time.Duration(timeoutS) * time.Second,
maxFailedTimes: maxFailedTimes,
checkType: cfg.Type,
interval: time.Duration(cfg.IntervalSeconds) * time.Second,
timeout: time.Duration(cfg.TimeoutSeconds) * time.Second,
maxFailedTimes: cfg.MaxFailed,
addr: addr,
url: url,
statusOK: false,

View File

@@ -0,0 +1,47 @@
// Copyright 2023 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 proxy
import (
"reflect"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
func init() {
pxyConfs := []v1.ProxyConfigurer{
&v1.TCPProxyConfig{},
&v1.HTTPProxyConfig{},
&v1.HTTPSProxyConfig{},
&v1.STCPProxyConfig{},
&v1.TCPMuxProxyConfig{},
}
for _, cfg := range pxyConfs {
RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
}
}
// GeneralTCPProxy is a general implementation of Proxy interface for TCP protocol.
// If the default GeneralTCPProxy cannot meet the requirements, you can customize
// the implementation of the Proxy interface.
type GeneralTCPProxy struct {
*BaseProxy
}
func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy {
return &GeneralTCPProxy{
BaseProxy: baseProxy,
}
}

View File

@@ -15,32 +15,35 @@
package proxy
import (
"bytes"
"context"
"io"
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/fatedier/golib/errors"
frpIo "github.com/fatedier/golib/io"
libio "github.com/fatedier/golib/io"
libdial "github.com/fatedier/golib/net/dial"
"github.com/fatedier/golib/pool"
fmux "github.com/hashicorp/yamux"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) {
proxyFactoryRegistry[proxyConfType] = factory
}
// Proxy defines how to handle work connections for different proxy type.
type Proxy interface {
Run() error
@@ -51,711 +54,102 @@ type Proxy interface {
Close()
}
func NewProxy(ctx context.Context, pxyConf config.ProxyConf, clientCfg config.ClientCommonConf, serverUDPPort int) (pxy Proxy) {
func NewProxy(
ctx context.Context,
pxyConf v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter,
) (pxy Proxy) {
var limiter *rate.Limiter
limitBytes := pxyConf.GetBaseInfo().BandwidthLimit.Bytes()
if limitBytes > 0 && pxyConf.GetBaseInfo().BandwidthLimitMode == config.BandwidthLimitModeClient {
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient {
limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
}
baseProxy := BaseProxy{
clientCfg: clientCfg,
serverUDPPort: serverUDPPort,
limiter: limiter,
xl: xlog.FromContextSafe(ctx),
ctx: ctx,
baseCfg: pxyConf.GetBaseConfig(),
clientCfg: clientCfg,
limiter: limiter,
msgTransporter: msgTransporter,
xl: xlog.FromContextSafe(ctx),
ctx: ctx,
}
switch cfg := pxyConf.(type) {
case *config.TCPProxyConf:
pxy = &TCPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.TCPMuxProxyConf:
pxy = &TCPMuxProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.UDPProxyConf:
pxy = &UDPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.HTTPProxyConf:
pxy = &HTTPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.HTTPSProxyConf:
pxy = &HTTPSProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.STCPProxyConf:
pxy = &STCPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.XTCPProxyConf:
pxy = &XTCPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.SUDPProxyConf:
pxy = &SUDPProxy{
BaseProxy: &baseProxy,
cfg: cfg,
closeCh: make(chan struct{}),
}
factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
if factory == nil {
return nil
}
return
return factory(&baseProxy, pxyConf)
}
type BaseProxy struct {
closed bool
clientCfg config.ClientCommonConf
serverUDPPort int
limiter *rate.Limiter
baseCfg *v1.ProxyBaseConfig
clientCfg *v1.ClientCommonConfig
msgTransporter transport.MessageTransporter
limiter *rate.Limiter
// proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now.
proxyPlugin plugin.Plugin
mu sync.RWMutex
xl *xlog.Logger
ctx context.Context
}
// TCP
type TCPProxy struct {
*BaseProxy
cfg *config.TCPProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *TCPProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
func (pxy *BaseProxy) Run() error {
if pxy.baseCfg.Plugin.Type != "" {
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions)
if err != nil {
return
return err
}
pxy.proxyPlugin = p
}
return
return nil
}
func (pxy *TCPProxy) Close() {
func (pxy *BaseProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *TCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
conn, []byte(pxy.clientCfg.Token), m)
}
// TCP Multiplexer
type TCPMuxProxy struct {
*BaseProxy
cfg *config.TCPMuxProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *TCPMuxProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
if err != nil {
return
}
}
return
}
func (pxy *TCPMuxProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *TCPMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
conn, []byte(pxy.clientCfg.Token), m)
}
// HTTP
type HTTPProxy struct {
*BaseProxy
cfg *config.HTTPProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *HTTPProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
if err != nil {
return
}
}
return
}
func (pxy *HTTPProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *HTTPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
conn, []byte(pxy.clientCfg.Token), m)
}
// HTTPS
type HTTPSProxy struct {
*BaseProxy
cfg *config.HTTPSProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *HTTPSProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
if err != nil {
return
}
}
return
}
func (pxy *HTTPSProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *HTTPSProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
conn, []byte(pxy.clientCfg.Token), m)
}
// STCP
type STCPProxy struct {
*BaseProxy
cfg *config.STCPProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *STCPProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
if err != nil {
return
}
}
return
}
func (pxy *STCPProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *STCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
conn, []byte(pxy.clientCfg.Token), m)
}
// XTCP
type XTCPProxy struct {
*BaseProxy
cfg *config.XTCPProxyConf
proxyPlugin plugin.Plugin
}
func (pxy *XTCPProxy) Run() (err error) {
if pxy.cfg.Plugin != "" {
pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
if err != nil {
return
}
}
return
}
func (pxy *XTCPProxy) Close() {
if pxy.proxyPlugin != nil {
pxy.proxyPlugin.Close()
}
}
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
xl := pxy.xl
defer conn.Close()
var natHoleSidMsg msg.NatHoleSid
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
if err != nil {
xl.Error("xtcp read from workConn error: %v", err)
return
}
natHoleClientMsg := &msg.NatHoleClient{
ProxyName: pxy.cfg.ProxyName,
Sid: natHoleSidMsg.Sid,
}
raddr, _ := net.ResolveUDPAddr("udp",
net.JoinHostPort(pxy.clientCfg.ServerAddr, strconv.Itoa(pxy.serverUDPPort)))
clientConn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
xl.Error("dial server udp addr error: %v", err)
return
}
defer clientConn.Close()
err = msg.WriteMsg(clientConn, natHoleClientMsg)
if err != nil {
xl.Error("send natHoleClientMsg to server error: %v", err)
return
}
// Wait for client address at most 5 seconds.
var natHoleRespMsg msg.NatHoleResp
_ = clientConn.SetReadDeadline(time.Now().Add(5 * time.Second))
buf := pool.GetBuf(1024)
n, err := clientConn.Read(buf)
if err != nil {
xl.Error("get natHoleRespMsg error: %v", err)
return
}
err = msg.ReadMsgInto(bytes.NewReader(buf[:n]), &natHoleRespMsg)
if err != nil {
xl.Error("get natHoleRespMsg error: %v", err)
return
}
_ = clientConn.SetReadDeadline(time.Time{})
_ = clientConn.Close()
if natHoleRespMsg.Error != "" {
xl.Error("natHoleRespMsg get error info: %s", natHoleRespMsg.Error)
return
}
xl.Trace("get natHoleRespMsg, sid [%s], client address [%s] visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr)
// Send detect message
host, portStr, err := net.SplitHostPort(natHoleRespMsg.VisitorAddr)
if err != nil {
xl.Error("get NatHoleResp visitor address [%s] error: %v", natHoleRespMsg.VisitorAddr, err)
}
laddr, _ := net.ResolveUDPAddr("udp", clientConn.LocalAddr().String())
port, err := strconv.ParseInt(portStr, 10, 64)
if err != nil {
xl.Error("get natHoleResp visitor address error: %v", natHoleRespMsg.VisitorAddr)
return
}
_ = pxy.sendDetectMsg(host, int(port), laddr, []byte(natHoleRespMsg.Sid))
xl.Trace("send all detect msg done")
if err := msg.WriteMsg(conn, &msg.NatHoleClientDetectOK{}); err != nil {
xl.Error("write message error: %v", err)
return
}
// Listen for clientConn's address and wait for visitor connection
lConn, err := net.ListenUDP("udp", laddr)
if err != nil {
xl.Error("listen on visitorConn's local address error: %v", err)
return
}
defer lConn.Close()
_ = lConn.SetReadDeadline(time.Now().Add(8 * time.Second))
sidBuf := pool.GetBuf(1024)
var uAddr *net.UDPAddr
n, uAddr, err = lConn.ReadFromUDP(sidBuf)
if err != nil {
xl.Warn("get sid from visitor error: %v", err)
return
}
_ = lConn.SetReadDeadline(time.Time{})
if string(sidBuf[:n]) != natHoleRespMsg.Sid {
xl.Warn("incorrect sid from visitor")
return
}
pool.PutBuf(sidBuf)
xl.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid)
if _, err := lConn.WriteToUDP(sidBuf[:n], uAddr); err != nil {
xl.Error("write uaddr error: %v", err)
return
}
kcpConn, err := frpNet.NewKCPConnFromUDP(lConn, false, uAddr.String())
if err != nil {
xl.Error("create kcp connection from udp connection error: %v", err)
return
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 5 * time.Second
fmuxCfg.LogOutput = io.Discard
sess, err := fmux.Server(kcpConn, fmuxCfg)
if err != nil {
xl.Error("create yamux server from kcp connection error: %v", err)
return
}
defer sess.Close()
muxConn, err := sess.Accept()
if err != nil {
xl.Error("accept for yamux connection error: %v", err)
return
}
HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
muxConn, []byte(pxy.cfg.Sk), m)
}
func (pxy *XTCPProxy) sendDetectMsg(addr string, port int, laddr *net.UDPAddr, content []byte) (err error) {
daddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(addr, strconv.Itoa(port)))
if err != nil {
return err
}
tConn, err := net.DialUDP("udp", laddr, daddr)
if err != nil {
return err
}
// uConn := ipv4.NewConn(tConn)
// uConn.SetTTL(3)
if _, err := tConn.Write(content); err != nil {
return err
}
return tConn.Close()
}
// UDP
type UDPProxy struct {
*BaseProxy
cfg *config.UDPProxyConf
localAddr *net.UDPAddr
readCh chan *msg.UDPPacket
// include msg.UDPPacket and msg.Ping
sendCh chan msg.Message
workConn net.Conn
}
func (pxy *UDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *UDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
if !pxy.closed {
pxy.closed = true
if pxy.workConn != nil {
pxy.workConn.Close()
}
if pxy.readCh != nil {
close(pxy.readCh)
}
if pxy.sendCh != nil {
close(pxy.sendCh)
}
}
}
func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
// close resources releated with old workConn
pxy.Close()
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.UseEncryption {
rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.UseCompression {
rwc = frpIo.WithCompression(rwc)
}
conn = frpNet.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock()
pxy.workConn = conn
pxy.readCh = make(chan *msg.UDPPacket, 1024)
pxy.sendCh = make(chan msg.Message, 1024)
pxy.closed = false
pxy.mu.Unlock()
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
for {
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for udp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
xl.Trace("get udp package from workConn: %s", udpMsg.Content)
readCh <- &udpMsg
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed: %v", errRet)
return
}
}
}
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
xl.Info("writer goroutine for udp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("send udp package to workConn: %s", m.Content)
case *msg.Ping:
xl.Trace("send ping message to udp workConn")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("udp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
var errRet error
for {
time.Sleep(time.Duration(30) * time.Second)
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Trace("heartbeat goroutine for udp work connection closed")
break
}
}
}
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))
}
type SUDPProxy struct {
*BaseProxy
cfg *config.SUDPProxyConf
localAddr *net.UDPAddr
closeCh chan struct{}
}
func (pxy *SUDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *SUDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
select {
case <-pxy.closeCh:
return
default:
close(pxy.closeCh)
}
}
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.UseEncryption {
rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.UseCompression {
rwc = frpIo.WithCompression(rwc)
}
conn = frpNet.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn
readCh := make(chan *msg.UDPPacket, 1024)
sendCh := make(chan msg.Message, 1024)
isClose := false
mu := &sync.Mutex{}
closeFn := func() {
mu.Lock()
defer mu.Unlock()
if isClose {
return
}
isClose = true
if workConn != nil {
workConn.Close()
}
close(readCh)
close(sendCh)
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
defer closeFn()
for {
// first to check sudp proxy is closed or not
select {
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
default:
}
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for sudp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
readCh <- &udpMsg
}); errRet != nil {
xl.Warn("reader goroutine for sudp work connection closed: %v", errRet)
return
}
}
}
// udp service -> frpc -> frps -> frpc visitor -> user
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
closeFn()
xl.Info("writer goroutine for sudp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]",
m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
case *msg.Ping:
xl.Trace("frpc send ping message to frpc visitor")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("sudp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
closeFn()
}()
var errRet error
for {
select {
case <-ticker.C:
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Warn("heartbeat goroutine for sudp work connection closed")
return
}
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
}
}
}
go workConnSenderFn(workConn, sendCh)
go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
}
// Common handler for tcp work connections.
func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf, proxyPlugin plugin.Plugin,
baseInfo *config.BaseProxyConf, limiter *rate.Limiter, workConn net.Conn, encKey []byte, m *msg.StartWorkConn,
) {
xl := xlog.FromContextSafe(ctx)
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
xl := pxy.xl
baseCfg := pxy.baseCfg
var (
remote io.ReadWriteCloser
err error
)
remote = workConn
if limiter != nil {
remote = frpIo.WrapReadWriteCloser(limit.NewReader(workConn, limiter), limit.NewWriter(workConn, limiter), func() error {
if pxy.limiter != nil {
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
return workConn.Close()
})
}
xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
baseInfo.UseEncryption, baseInfo.UseCompression)
if baseInfo.UseEncryption {
remote, err = frpIo.WithEncryption(remote, encKey)
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
if baseCfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, encKey)
if err != nil {
workConn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if baseInfo.UseCompression {
remote = frpIo.WithCompression(remote)
var compressionResourceRecycleFn func()
if baseCfg.Transport.UseCompression {
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
}
// check if we need to send proxy protocol info
var extraInfo []byte
if baseInfo.ProxyProtocolVersion != "" {
var extraInfo plugin.ExtraInfo
if baseCfg.Transport.ProxyProtocolVersion != "" {
if m.SrcAddr != "" && m.SrcPort != 0 {
if m.DstAddr == "" {
m.DstAddr = "127.0.0.1"
@@ -774,47 +168,51 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
h.TransportProtocol = pp.TCPv6
}
if baseInfo.ProxyProtocolVersion == "v1" {
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
h.Version = 1
} else if baseInfo.ProxyProtocolVersion == "v2" {
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
buf := bytes.NewBuffer(nil)
_, _ = h.WriteTo(buf)
extraInfo = buf.Bytes()
extraInfo.ProxyProtocolHeader = h
}
}
if proxyPlugin != nil {
// if plugin is set, let plugin handle connections first
xl.Debug("handle by plugin: %s", proxyPlugin.Name())
proxyPlugin.Handle(remote, workConn, extraInfo)
if pxy.proxyPlugin != nil {
// if plugin is set, let plugin handle connection first
xl.Debug("handle by plugin: %s", pxy.proxyPlugin.Name())
pxy.proxyPlugin.Handle(remote, workConn, &extraInfo)
xl.Debug("handle by plugin finished")
return
}
localConn, err := libdial.Dial(
net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)),
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
libdial.WithTimeout(10*time.Second),
)
if err != nil {
workConn.Close()
xl.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIP, localInfo.LocalPort, err)
xl.Error("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
return
}
xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
if len(extraInfo) > 0 {
if _, err := localConn.Write(extraInfo); err != nil {
if extraInfo.ProxyProtocolHeader != nil {
if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
workConn.Close()
xl.Error("write extraInfo to local conn error: %v", err)
xl.Error("write proxy protocol header to local conn error: %v", err)
return
}
}
frpIo.Join(localConn, remote)
_, _, errs := libio.Join(localConn, remote)
xl.Debug("join connections closed")
if len(errs) > 0 {
xl.Trace("join connections errors: %v", errs)
}
if compressionResourceRecycleFn != nil {
compressionResourceRecycleFn()
}
}

View File

@@ -1,42 +1,58 @@
// Copyright 2023 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 proxy
import (
"context"
"fmt"
"net"
"reflect"
"sync"
"github.com/fatedier/golib/errors"
"github.com/samber/lo"
"github.com/fatedier/frp/client/event"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
)
type Manager struct {
sendCh chan (msg.Message)
proxies map[string]*Wrapper
proxies map[string]*Wrapper
msgTransporter transport.MessageTransporter
closed bool
mu sync.RWMutex
clientCfg config.ClientCommonConf
// The UDP port that the server is listening on
serverUDPPort int
clientCfg *v1.ClientCommonConfig
ctx context.Context
}
func NewManager(ctx context.Context, msgSendCh chan (msg.Message), clientCfg config.ClientCommonConf, serverUDPPort int) *Manager {
func NewManager(
ctx context.Context,
clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter,
) *Manager {
return &Manager{
sendCh: msgSendCh,
proxies: make(map[string]*Wrapper),
closed: false,
clientCfg: clientCfg,
serverUDPPort: serverUDPPort,
ctx: ctx,
proxies: make(map[string]*Wrapper),
msgTransporter: msgTransporter,
closed: false,
clientCfg: clientCfg,
ctx: ctx,
}
}
@@ -86,10 +102,7 @@ func (pm *Manager) HandleEvent(payload interface{}) error {
return event.ErrPayloadType
}
err := errors.PanicToError(func() {
pm.sendCh <- m
})
return err
return pm.msgTransporter.Send(m)
}
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
@@ -102,36 +115,37 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
return ps
}
func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
xl := xlog.FromContextSafe(pm.ctx)
pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string {
return c.GetBaseConfig().Name
})
pm.mu.Lock()
defer pm.mu.Unlock()
delPxyNames := make([]string, 0)
for name, pxy := range pm.proxies {
del := false
cfg, ok := pxyCfgs[name]
if !ok {
del = true
} else if !pxy.Cfg.Compare(cfg) {
cfg, ok := pxyCfgsMap[name]
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
del = true
}
if del {
delPxyNames = append(delPxyNames, name)
delete(pm.proxies, name)
pxy.Stop()
}
}
if len(delPxyNames) > 0 {
xl.Info("proxy removed: %v", delPxyNames)
xl.Info("proxy removed: %s", delPxyNames)
}
addPxyNames := make([]string, 0)
for name, cfg := range pxyCfgs {
for _, cfg := range pxyCfgs {
name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.serverUDPPort)
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
pm.proxies[name] = pxy
addPxyNames = append(addPxyNames, name)
@@ -139,6 +153,6 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
}
}
if len(addPxyNames) > 0 {
xl.Info("proxy added: %v", addPxyNames)
xl.Info("proxy added: %s", addPxyNames)
}
}

View File

@@ -1,9 +1,24 @@
// Copyright 2023 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 proxy
import (
"context"
"fmt"
"net"
"strconv"
"sync"
"sync/atomic"
"time"
@@ -12,8 +27,9 @@ import (
"github.com/fatedier/frp/client/event"
"github.com/fatedier/frp/client/health"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -33,11 +49,11 @@ var (
)
type WorkingStatus struct {
Name string `json:"name"`
Type string `json:"type"`
Phase string `json:"status"`
Err string `json:"err"`
Cfg config.ProxyConf `json:"cfg"`
Name string `json:"name"`
Type string `json:"type"`
Phase string `json:"status"`
Err string `json:"err"`
Cfg v1.ProxyConfigurer `json:"cfg"`
// Got from server.
RemoteAddr string `json:"remote_addr"`
@@ -56,6 +72,8 @@ type Wrapper struct {
// event handler
handler event.Handler
msgTransporter transport.MessageTransporter
health uint32
lastSendStartMsg time.Time
lastStartErr time.Time
@@ -67,32 +85,39 @@ type Wrapper struct {
ctx context.Context
}
func NewWrapper(ctx context.Context, cfg config.ProxyConf, clientCfg config.ClientCommonConf, eventHandler event.Handler, serverUDPPort int) *Wrapper {
baseInfo := cfg.GetBaseInfo()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName)
func NewWrapper(
ctx context.Context,
cfg v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig,
eventHandler event.Handler,
msgTransporter transport.MessageTransporter,
) *Wrapper {
baseInfo := cfg.GetBaseConfig()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
pw := &Wrapper{
WorkingStatus: WorkingStatus{
Name: baseInfo.ProxyName,
Type: baseInfo.ProxyType,
Name: baseInfo.Name,
Type: baseInfo.Type,
Phase: ProxyPhaseNew,
Cfg: cfg,
},
closeCh: make(chan struct{}),
healthNotifyCh: make(chan struct{}),
handler: eventHandler,
msgTransporter: msgTransporter,
xl: xl,
ctx: xlog.NewContext(ctx, xl),
}
if baseInfo.HealthCheckType != "" {
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
pw.health = 1 // means failed
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheckType, baseInfo.HealthCheckIntervalS,
baseInfo.HealthCheckTimeoutS, baseInfo.HealthCheckMaxFailed, baseInfo.HealthCheckAddr,
baseInfo.HealthCheckURL, pw.statusNormalCallback, pw.statusFailedCallback)
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
pw.statusNormalCallback, pw.statusFailedCallback)
xl.Trace("enable health check monitor")
}
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, serverUDPPort)
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter)
return pw
}

207
client/proxy/sudp.go Normal file
View File

@@ -0,0 +1,207 @@
// Copyright 2023 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 proxy
import (
"io"
"net"
"reflect"
"strconv"
"sync"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
}
type SUDPProxy struct {
*BaseProxy
cfg *v1.SUDPProxyConfig
localAddr *net.UDPAddr
closeCh chan struct{}
}
func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
unwrapped, ok := cfg.(*v1.SUDPProxyConfig)
if !ok {
return nil
}
return &SUDPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
closeCh: make(chan struct{}),
}
}
func (pxy *SUDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *SUDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
select {
case <-pxy.closeCh:
return
default:
close(pxy.closeCh)
}
}
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.Transport.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc)
}
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn
readCh := make(chan *msg.UDPPacket, 1024)
sendCh := make(chan msg.Message, 1024)
isClose := false
mu := &sync.Mutex{}
closeFn := func() {
mu.Lock()
defer mu.Unlock()
if isClose {
return
}
isClose = true
if workConn != nil {
workConn.Close()
}
close(readCh)
close(sendCh)
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
defer closeFn()
for {
// first to check sudp proxy is closed or not
select {
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
default:
}
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for sudp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
readCh <- &udpMsg
}); errRet != nil {
xl.Warn("reader goroutine for sudp work connection closed: %v", errRet)
return
}
}
}
// udp service -> frpc -> frps -> frpc visitor -> user
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
closeFn()
xl.Info("writer goroutine for sudp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]",
m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
case *msg.Ping:
xl.Trace("frpc send ping message to frpc visitor")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("sudp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
closeFn()
}()
var errRet error
for {
select {
case <-ticker.C:
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Warn("heartbeat goroutine for sudp work connection closed")
return
}
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
}
}
}
go workConnSenderFn(workConn, sendCh)
go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
}

173
client/proxy/udp.go Normal file
View File

@@ -0,0 +1,173 @@
// Copyright 2023 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 proxy
import (
"io"
"net"
"reflect"
"strconv"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
}
type UDPProxy struct {
*BaseProxy
cfg *v1.UDPProxyConfig
localAddr *net.UDPAddr
readCh chan *msg.UDPPacket
// include msg.UDPPacket and msg.Ping
sendCh chan msg.Message
workConn net.Conn
closed bool
}
func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
unwrapped, ok := cfg.(*v1.UDPProxyConfig)
if !ok {
return nil
}
return &UDPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
}
}
func (pxy *UDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *UDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
if !pxy.closed {
pxy.closed = true
if pxy.workConn != nil {
pxy.workConn.Close()
}
if pxy.readCh != nil {
close(pxy.readCh)
}
if pxy.sendCh != nil {
close(pxy.sendCh)
}
}
}
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
// close resources releated with old workConn
pxy.Close()
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.Transport.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc)
}
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock()
pxy.workConn = conn
pxy.readCh = make(chan *msg.UDPPacket, 1024)
pxy.sendCh = make(chan msg.Message, 1024)
pxy.closed = false
pxy.mu.Unlock()
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
for {
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for udp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
xl.Trace("get udp package from workConn: %s", udpMsg.Content)
readCh <- &udpMsg
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed: %v", errRet)
return
}
}
}
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
xl.Info("writer goroutine for udp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("send udp package to workConn: %s", m.Content)
case *msg.Ping:
xl.Trace("send ping message to udp workConn")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("udp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
var errRet error
for {
time.Sleep(time.Duration(30) * time.Second)
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Trace("heartbeat goroutine for udp work connection closed")
break
}
}
}
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))
}

197
client/proxy/xtcp.go Normal file
View File

@@ -0,0 +1,197 @@
// Copyright 2023 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 proxy
import (
"io"
"net"
"reflect"
"time"
fmux "github.com/hashicorp/yamux"
"github.com/quic-go/quic-go"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
}
type XTCPProxy struct {
*BaseProxy
cfg *v1.XTCPProxyConfig
}
func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
unwrapped, ok := cfg.(*v1.XTCPProxyConfig)
if !ok {
return nil
}
return &XTCPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
}
}
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
defer conn.Close()
var natHoleSidMsg msg.NatHoleSid
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
if err != nil {
xl.Error("xtcp read from workConn error: %v", err)
return
}
xl.Trace("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
if err != nil {
xl.Warn("nathole prepare error: %v", err)
return
}
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
defer prepareResult.ListenConn.Close()
// send NatHoleClient msg to server
transactionID := nathole.NewTransactionID()
natHoleClientMsg := &msg.NatHoleClient{
TransactionID: transactionID,
ProxyName: pxy.cfg.Name,
Sid: natHoleSidMsg.Sid,
MappedAddrs: prepareResult.Addrs,
AssistedAddrs: prepareResult.AssistedAddrs,
}
xl.Trace("nathole exchange info start")
natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
if err != nil {
xl.Warn("nathole exchange info error: %v", err)
return
}
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
listenConn := prepareResult.ListenConn
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey))
if err != nil {
listenConn.Close()
xl.Warn("make hole error: %v", err)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid,
Success: false,
})
return
}
listenConn = newListenConn
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid,
Success: true,
})
if natHoleRespMsg.Protocol == "kcp" {
pxy.listenByKCP(listenConn, raddr, startWorkConnMsg)
return
}
// default is quic
pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg)
}
func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
listenConn.Close()
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
lConn, err := net.DialUDP("udp", laddr, raddr)
if err != nil {
xl.Warn("dial udp error: %v", err)
return
}
defer lConn.Close()
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil {
xl.Warn("create kcp connection from udp connection error: %v", err)
return
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 10 * time.Second
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
fmuxCfg.LogOutput = io.Discard
session, err := fmux.Server(remote, fmuxCfg)
if err != nil {
xl.Error("create mux session error: %v", err)
return
}
defer session.Close()
for {
muxConn, err := session.Accept()
if err != nil {
xl.Error("accept connection error: %v", err)
return
}
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey))
}
}
func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
defer listenConn.Close()
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
if err != nil {
xl.Warn("create tls config error: %v", err)
return
}
tlsConfig.NextProtos = []string{"frp"}
quicListener, err := quic.Listen(listenConn, tlsConfig,
&quic.Config{
MaxIdleTimeout: time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams),
KeepAlivePeriod: time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
},
)
if err != nil {
xl.Warn("dial quic error: %v", err)
return
}
// only accept one connection from raddr
c, err := quicListener.Accept(pxy.ctx)
if err != nil {
xl.Error("quic accept connection error: %v", err)
return
}
for {
stream, err := c.AcceptStream(pxy.ctx)
if err != nil {
xl.Debug("quic accept stream error: %v", err)
_ = c.CloseWithError(0, "")
return
}
go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
}
}

View File

@@ -19,7 +19,6 @@ import (
"crypto/tls"
"fmt"
"io"
"math/rand"
"net"
"runtime"
"strconv"
@@ -32,14 +31,15 @@ import (
libdial "github.com/fatedier/golib/net/dial"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
frpNet "github.com/fatedier/frp/pkg/util/net"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/xlog"
@@ -47,8 +47,6 @@ import (
func init() {
crypto.DefaultSalt = "frp"
// TODO: remove this when we drop support for go1.19
rand.Seed(time.Now().UnixNano())
}
// Service is a client service.
@@ -63,18 +61,15 @@ type Service struct {
// Sets authentication based on selected method
authSetter auth.Setter
cfg config.ClientCommonConf
pxyCfgs map[string]config.ProxyConf
visitorCfgs map[string]config.VisitorConf
cfg *v1.ClientCommonConfig
pxyCfgs []v1.ProxyConfigurer
visitorCfgs []v1.VisitorConfigurer
cfgMu sync.RWMutex
// The configuration file used to initialize this client, or an empty
// string if no configuration file was used.
cfgFile string
// This is configured by the login response from frps
serverUDPPort int
exit uint32 // 0 means not exit
// service context
@@ -84,21 +79,19 @@ type Service struct {
}
func NewService(
cfg config.ClientCommonConf,
pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf,
cfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
) (svr *Service, err error) {
ctx, cancel := context.WithCancel(context.Background())
svr = &Service{
authSetter: auth.NewAuthSetter(cfg.ClientConfig),
authSetter: auth.NewAuthSetter(cfg.Auth),
cfg: cfg,
cfgFile: cfgFile,
pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs,
ctx: context.Background(),
exit: 0,
ctx: xlog.NewContext(ctx, xlog.New()),
cancel: cancel,
}
return
}
@@ -109,7 +102,11 @@ func (svr *Service) GetController() *Control {
return svr.ctl
}
func (svr *Service) Run() error {
func (svr *Service) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
svr.ctx = xlog.NewContext(ctx, xlog.New())
svr.cancel = cancel
xl := xlog.FromContextSafe(svr.ctx)
// set custom DNSServer
@@ -135,13 +132,13 @@ func (svr *Service) Run() error {
// if login_fail_exit is true, just exit this program
// otherwise sleep a while and try again to connect to server
if svr.cfg.LoginFailExit {
if lo.FromPtr(svr.cfg.LoginFailExit) {
return err
}
util.RandomSleep(10*time.Second, 0.9, 1.1)
util.RandomSleep(5*time.Second, 0.9, 1.1)
} else {
// login success
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
ctl.Run()
svr.ctlMu.Lock()
svr.ctl = ctl
@@ -152,18 +149,22 @@ func (svr *Service) Run() error {
go svr.keepControllerWorking()
if svr.cfg.AdminPort != 0 {
if svr.cfg.WebServer.Port != 0 {
// Init admin server assets
assets.Load(svr.cfg.AssetsDir)
assets.Load(svr.cfg.WebServer.AssetsDir)
address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort))
address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port))
err := svr.RunAdminServer(address)
if err != nil {
log.Warn("run admin server error: %v", err)
}
log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort)
log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
}
<-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources
if atomic.LoadUint32(&svr.exit) == 0 {
svr.Close()
}
return nil
}
@@ -185,7 +186,7 @@ func (svr *Service) keepControllerWorking() {
return
}
// the first three retry with no delay
// the first three attempts with a low delay
if reconnectCounts > 3 {
util.RandomSleep(reconnectDelay, 0.9, 1.1)
xl.Info("wait %v to reconnect", reconnectDelay)
@@ -223,7 +224,7 @@ func (svr *Service) keepControllerWorking() {
// reconnect success, init delayTime
delayTime = time.Second
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
ctl.Run()
svr.ctlMu.Lock()
if svr.ctl != nil {
@@ -241,7 +242,7 @@ func (svr *Service) keepControllerWorking() {
// session: if it's not nil, using tcp mux
func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
xl := xlog.FromContextSafe(svr.ctx)
cm = NewConnectionManager(svr.ctx, &svr.cfg)
cm = NewConnectionManager(svr.ctx, svr.cfg)
if err = cm.OpenConnection(); err != nil {
return nil, nil, err
@@ -261,12 +262,12 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
loginMsg := &msg.Login{
Arch: runtime.GOARCH,
Os: runtime.GOOS,
PoolCount: svr.cfg.PoolCount,
PoolCount: svr.cfg.Transport.PoolCount,
User: svr.cfg.User,
Version: version.Full(),
Timestamp: time.Now().Unix(),
RunID: svr.runID,
Metas: svr.cfg.Metas,
Metas: svr.cfg.Metadatas,
}
// Add auth
@@ -295,12 +296,11 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
xl.ResetPrefixes()
xl.AppendPrefix(svr.runID)
svr.serverUDPPort = loginRespMsg.ServerUDPPort
xl.Info("login to server success, get run id [%s], server udp port [%d]", loginRespMsg.RunID, loginRespMsg.ServerUDPPort)
xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
return
}
func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
svr.cfgMu.Lock()
svr.pxyCfgs = pxyCfgs
svr.visitorCfgs = visitorCfgs
@@ -326,21 +326,24 @@ func (svr *Service) GracefulClose(d time.Duration) {
svr.ctlMu.RLock()
if svr.ctl != nil {
svr.ctl.GracefulClose(d)
svr.ctl = nil
}
svr.ctlMu.RUnlock()
svr.cancel()
if svr.cancel != nil {
svr.cancel()
}
}
type ConnectionManager struct {
ctx context.Context
cfg *config.ClientCommonConf
cfg *v1.ClientCommonConfig
muxSession *fmux.Session
quicConn quic.Connection
}
func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager {
func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager {
return &ConnectionManager{
ctx: ctx,
cfg: cfg,
@@ -351,18 +354,18 @@ func (cm *ConnectionManager) OpenConnection() error {
xl := xlog.FromContextSafe(cm.ctx)
// special for quic
if strings.EqualFold(cm.cfg.Protocol, "quic") {
if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") {
var tlsConfig *tls.Config
var err error
sn := cm.cfg.TLSServerName
sn := cm.cfg.Transport.TLS.ServerName
if sn == "" {
sn = cm.cfg.ServerAddr
}
if cm.cfg.TLSEnable {
if lo.FromPtr(cm.cfg.Transport.TLS.Enable) {
tlsConfig, err = transport.NewClientTLSConfig(
cm.cfg.TLSCertFile,
cm.cfg.TLSKeyFile,
cm.cfg.TLSTrustedCaFile,
cm.cfg.Transport.TLS.CertFile,
cm.cfg.Transport.TLS.KeyFile,
cm.cfg.Transport.TLS.TrustedCaFile,
sn)
} else {
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
@@ -374,11 +377,12 @@ func (cm *ConnectionManager) OpenConnection() error {
tlsConfig.NextProtos = []string{"frp"}
conn, err := quic.DialAddr(
cm.ctx,
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
tlsConfig, &quic.Config{
MaxIdleTimeout: time.Duration(cm.cfg.QUICMaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(cm.cfg.QUICMaxIncomingStreams),
KeepAlivePeriod: time.Duration(cm.cfg.QUICKeepalivePeriod) * time.Second,
MaxIdleTimeout: time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams),
KeepAlivePeriod: time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
})
if err != nil {
return err
@@ -387,7 +391,7 @@ func (cm *ConnectionManager) OpenConnection() error {
return nil
}
if !cm.cfg.TCPMux {
if !lo.FromPtr(cm.cfg.Transport.TCPMux) {
return nil
}
@@ -397,8 +401,9 @@ func (cm *ConnectionManager) OpenConnection() error {
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Client(conn, fmuxCfg)
if err != nil {
return err
@@ -413,7 +418,7 @@ func (cm *ConnectionManager) Connect() (net.Conn, error) {
if err != nil {
return nil, err
}
return frpNet.QuicStreamToNetConn(stream, cm.quicConn), nil
return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil
} else if cm.muxSession != nil {
stream, err := cm.muxSession.OpenStream()
if err != nil {
@@ -429,16 +434,20 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
xl := xlog.FromContextSafe(cm.ctx)
var tlsConfig *tls.Config
var err error
if cm.cfg.TLSEnable {
sn := cm.cfg.TLSServerName
tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable)
if cm.cfg.Transport.Protocol == "wss" {
tlsEnable = true
}
if tlsEnable {
sn := cm.cfg.Transport.TLS.ServerName
if sn == "" {
sn = cm.cfg.ServerAddr
}
tlsConfig, err = transport.NewClientTLSConfig(
cm.cfg.TLSCertFile,
cm.cfg.TLSKeyFile,
cm.cfg.TLSTrustedCaFile,
cm.cfg.Transport.TLS.CertFile,
cm.cfg.Transport.TLS.KeyFile,
cm.cfg.Transport.TLS.TrustedCaFile,
sn)
if err != nil {
xl.Warn("fail to build tls configuration, err: %v", err)
@@ -446,32 +455,42 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
}
}
proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.HTTPProxy)
proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL)
if err != nil {
xl.Error("fail to parse proxy url")
return nil, err
}
dialOptions := []libdial.DialOption{}
protocol := cm.cfg.Protocol
if protocol == "websocket" {
protocol := cm.cfg.Transport.Protocol
switch protocol {
case "websocket":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
case "wss":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
}
if cm.cfg.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
if cm.cfg.Transport.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(cm.cfg.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second),
libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libdial.WithTLSConfig(tlsConfig),
libdial.WithAfterHook(libdial.AfterHook{
Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
}),
)
conn, err := libdial.Dial(
conn, err := libdial.DialContext(
cm.ctx,
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
dialOptions...,
)

View File

@@ -1,568 +0,0 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// 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 client
import (
"bytes"
"context"
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/fatedier/golib/errors"
frpIo "github.com/fatedier/golib/io"
"github.com/fatedier/golib/pool"
fmux "github.com/hashicorp/yamux"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
// Visitor is used for forward traffics from local port tot remote service.
type Visitor interface {
Run() error
Close()
}
func NewVisitor(ctx context.Context, ctl *Control, cfg config.VisitorConf) (visitor Visitor) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseInfo().ProxyName)
baseVisitor := BaseVisitor{
ctl: ctl,
ctx: xlog.NewContext(ctx, xl),
}
switch cfg := cfg.(type) {
case *config.STCPVisitorConf:
visitor = &STCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *config.XTCPVisitorConf:
visitor = &XTCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *config.SUDPVisitorConf:
visitor = &SUDPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
checkCloseCh: make(chan struct{}),
}
}
return
}
type BaseVisitor struct {
ctl *Control
l net.Listener
mu sync.RWMutex
ctx context.Context
}
type STCPVisitor struct {
*BaseVisitor
cfg *config.STCPVisitorConf
}
func (sv *STCPVisitor) Run() (err error) {
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return
}
go sv.worker()
return
}
func (sv *STCPVisitor) Close() {
sv.l.Close()
}
func (sv *STCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("stcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new stcp user connection")
visitorConn, err := sv.ctl.connectServer()
if err != nil {
return
}
defer visitorConn.Close()
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
xl.Warn("send newVisitorConnMsg to server error: %v", err)
return
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
xl.Warn("get newVisitorConnRespMsg error: %v", err)
return
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
return
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.UseCompression {
remote = frpIo.WithCompression(remote)
}
frpIo.Join(userConn, remote)
}
type XTCPVisitor struct {
*BaseVisitor
cfg *config.XTCPVisitorConf
}
func (sv *XTCPVisitor) Run() (err error) {
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return
}
go sv.worker()
return
}
func (sv *XTCPVisitor) Close() {
sv.l.Close()
}
func (sv *XTCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("xtcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new xtcp user connection")
if sv.ctl.serverUDPPort == 0 {
xl.Error("xtcp is not supported by server")
return
}
raddr, err := net.ResolveUDPAddr("udp",
net.JoinHostPort(sv.ctl.clientCfg.ServerAddr, strconv.Itoa(sv.ctl.serverUDPPort)))
if err != nil {
xl.Error("resolve server UDP addr error")
return
}
visitorConn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
xl.Warn("dial server udp addr error: %v", err)
return
}
defer visitorConn.Close()
now := time.Now().Unix()
natHoleVisitorMsg := &msg.NatHoleVisitor{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
}
err = msg.WriteMsg(visitorConn, natHoleVisitorMsg)
if err != nil {
xl.Warn("send natHoleVisitorMsg to server error: %v", err)
return
}
// Wait for client address at most 10 seconds.
var natHoleRespMsg msg.NatHoleResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
buf := pool.GetBuf(1024)
n, err := visitorConn.Read(buf)
if err != nil {
xl.Warn("get natHoleRespMsg error: %v", err)
return
}
err = msg.ReadMsgInto(bytes.NewReader(buf[:n]), &natHoleRespMsg)
if err != nil {
xl.Warn("get natHoleRespMsg error: %v", err)
return
}
_ = visitorConn.SetReadDeadline(time.Time{})
pool.PutBuf(buf)
if natHoleRespMsg.Error != "" {
xl.Error("natHoleRespMsg get error info: %s", natHoleRespMsg.Error)
return
}
xl.Trace("get natHoleRespMsg, sid [%s], client address [%s], visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr)
// Close visitorConn, so we can use it's local address.
visitorConn.Close()
// send sid message to client
laddr, _ := net.ResolveUDPAddr("udp", visitorConn.LocalAddr().String())
daddr, err := net.ResolveUDPAddr("udp", natHoleRespMsg.ClientAddr)
if err != nil {
xl.Error("resolve client udp address error: %v", err)
return
}
lConn, err := net.DialUDP("udp", laddr, daddr)
if err != nil {
xl.Error("dial client udp address error: %v", err)
return
}
defer lConn.Close()
if _, err := lConn.Write([]byte(natHoleRespMsg.Sid)); err != nil {
xl.Error("write sid error: %v", err)
return
}
// read ack sid from client
sidBuf := pool.GetBuf(1024)
_ = lConn.SetReadDeadline(time.Now().Add(8 * time.Second))
n, err = lConn.Read(sidBuf)
if err != nil {
xl.Warn("get sid from client error: %v", err)
return
}
_ = lConn.SetReadDeadline(time.Time{})
if string(sidBuf[:n]) != natHoleRespMsg.Sid {
xl.Warn("incorrect sid from client")
return
}
pool.PutBuf(sidBuf)
xl.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid)
// wrap kcp connection
var remote io.ReadWriteCloser
remote, err = frpNet.NewKCPConnFromUDP(lConn, true, natHoleRespMsg.ClientAddr)
if err != nil {
xl.Error("create kcp connection from udp connection error: %v", err)
return
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 5 * time.Second
fmuxCfg.LogOutput = io.Discard
sess, err := fmux.Client(remote, fmuxCfg)
if err != nil {
xl.Error("create yamux session error: %v", err)
return
}
defer sess.Close()
muxConn, err := sess.Open()
if err != nil {
xl.Error("open yamux stream error: %v", err)
return
}
var muxConnRWCloser io.ReadWriteCloser = muxConn
if sv.cfg.UseEncryption {
muxConnRWCloser, err = frpIo.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.UseCompression {
muxConnRWCloser = frpIo.WithCompression(muxConnRWCloser)
}
frpIo.Join(userConn, muxConnRWCloser)
xl.Debug("join connections closed")
}
type SUDPVisitor struct {
*BaseVisitor
checkCloseCh chan struct{}
// udpConn is the listener of udp packet
udpConn *net.UDPConn
readCh chan *msg.UDPPacket
sendCh chan *msg.UDPPacket
cfg *config.SUDPVisitorConf
}
// SUDP Run start listen a udp port
func (sv *SUDPVisitor) Run() (err error) {
xl := xlog.FromContextSafe(sv.ctx)
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return fmt.Errorf("sudp ResolveUDPAddr error: %v", err)
}
sv.udpConn, err = net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("listen udp port %s error: %v", addr.String(), err)
}
sv.sendCh = make(chan *msg.UDPPacket, 1024)
sv.readCh = make(chan *msg.UDPPacket, 1024)
xl.Info("sudp start to work, listen on %s", addr)
go sv.dispatcher()
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.ctl.clientCfg.UDPPacketSize))
return
}
func (sv *SUDPVisitor) dispatcher() {
xl := xlog.FromContextSafe(sv.ctx)
var (
visitorConn net.Conn
err error
firstPacket *msg.UDPPacket
)
for {
select {
case firstPacket = <-sv.sendCh:
if firstPacket == nil {
xl.Info("frpc sudp visitor proxy is closed")
return
}
case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed")
return
}
visitorConn, err = sv.getNewVisitorConn()
if err != nil {
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err)
continue
}
// visitorConn always be closed when worker done.
sv.worker(visitorConn, firstPacket)
select {
case <-sv.checkCloseCh:
return
default:
}
}
}
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker")
wg := &sync.WaitGroup{}
wg.Add(2)
closeCh := make(chan struct{})
// udp service -> frpc -> frps -> frpc visitor -> user
workConnReaderFn := func(conn net.Conn) {
defer func() {
conn.Close()
close(closeCh)
wg.Done()
}()
for {
var (
rawMsg msg.Message
errRet error
)
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
xl.Warn("read from workconn for user udp conn error: %v", errRet)
return
}
_ = conn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Ping:
xl.Debug("frpc visitor get ping message from frpc")
continue
case *msg.UDPPacket:
if errRet := errors.PanicToError(func() {
sv.readCh <- m
xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content)
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed")
return
}
}
}
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnSenderFn := func(conn net.Conn) {
defer func() {
conn.Close()
wg.Done()
}()
var errRet error
if firstPacket != nil {
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", firstPacket.Content)
}
for {
select {
case udpMsg, ok := <-sv.sendCh:
if !ok {
xl.Info("sender goroutine for udp work connection closed")
return
}
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", udpMsg.Content)
case <-closeCh:
return
}
}
}
go workConnReaderFn(workConn)
go workConnSenderFn(workConn)
wg.Wait()
xl.Info("sudp worker is closed")
}
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
xl := xlog.FromContextSafe(sv.ctx)
visitorConn, err := sv.ctl.connectServer()
if err != nil {
return nil, fmt.Errorf("frpc connect frps error: %v", err)
}
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return nil, err
}
}
if sv.cfg.UseCompression {
remote = frpIo.WithCompression(remote)
}
return frpNet.WrapReadWriteCloserToConn(remote, visitorConn), nil
}
func (sv *SUDPVisitor) Close() {
sv.mu.Lock()
defer sv.mu.Unlock()
select {
case <-sv.checkCloseCh:
return
default:
close(sv.checkCloseCh)
}
if sv.udpConn != nil {
sv.udpConn.Close()
}
close(sv.readCh)
close(sv.sendCh)
}

135
client/visitor/stcp.go Normal file
View File

@@ -0,0 +1,135 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// 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 visitor
import (
"io"
"net"
"strconv"
"time"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
type STCPVisitor struct {
*BaseVisitor
cfg *v1.STCPVisitorConfig
}
func (sv *STCPVisitor) Run() (err error) {
if sv.cfg.BindPort > 0 {
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return
}
go sv.worker()
}
go sv.internalConnWorker()
return
}
func (sv *STCPVisitor) Close() {
sv.BaseVisitor.Close()
}
func (sv *STCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("stcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) internalConnWorker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.internalLn.Accept()
if err != nil {
xl.Warn("stcp internal listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new stcp user connection")
visitorConn, err := sv.helper.ConnectServer()
if err != nil {
return
}
defer visitorConn.Close()
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
RunID: sv.helper.RunID(),
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
Timestamp: now,
UseEncryption: sv.cfg.Transport.UseEncryption,
UseCompression: sv.cfg.Transport.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
xl.Warn("send newVisitorConnMsg to server error: %v", err)
return
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
xl.Warn("get newVisitorConnRespMsg error: %v", err)
return
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
return
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.Transport.UseCompression {
var recycleFn func()
remote, recycleFn = libio.WithCompressionFromPool(remote)
defer recycleFn()
}
libio.Join(userConn, remote)
}

264
client/visitor/sudp.go Normal file
View File

@@ -0,0 +1,264 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// 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 visitor
import (
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
type SUDPVisitor struct {
*BaseVisitor
checkCloseCh chan struct{}
// udpConn is the listener of udp packet
udpConn *net.UDPConn
readCh chan *msg.UDPPacket
sendCh chan *msg.UDPPacket
cfg *v1.SUDPVisitorConfig
}
// SUDP Run start listen a udp port
func (sv *SUDPVisitor) Run() (err error) {
xl := xlog.FromContextSafe(sv.ctx)
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return fmt.Errorf("sudp ResolveUDPAddr error: %v", err)
}
sv.udpConn, err = net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("listen udp port %s error: %v", addr.String(), err)
}
sv.sendCh = make(chan *msg.UDPPacket, 1024)
sv.readCh = make(chan *msg.UDPPacket, 1024)
xl.Info("sudp start to work, listen on %s", addr)
go sv.dispatcher()
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize))
return
}
func (sv *SUDPVisitor) dispatcher() {
xl := xlog.FromContextSafe(sv.ctx)
var (
visitorConn net.Conn
err error
firstPacket *msg.UDPPacket
)
for {
select {
case firstPacket = <-sv.sendCh:
if firstPacket == nil {
xl.Info("frpc sudp visitor proxy is closed")
return
}
case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed")
return
}
visitorConn, err = sv.getNewVisitorConn()
if err != nil {
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err)
continue
}
// visitorConn always be closed when worker done.
sv.worker(visitorConn, firstPacket)
select {
case <-sv.checkCloseCh:
return
default:
}
}
}
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker")
wg := &sync.WaitGroup{}
wg.Add(2)
closeCh := make(chan struct{})
// udp service -> frpc -> frps -> frpc visitor -> user
workConnReaderFn := func(conn net.Conn) {
defer func() {
conn.Close()
close(closeCh)
wg.Done()
}()
for {
var (
rawMsg msg.Message
errRet error
)
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
xl.Warn("read from workconn for user udp conn error: %v", errRet)
return
}
_ = conn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Ping:
xl.Debug("frpc visitor get ping message from frpc")
continue
case *msg.UDPPacket:
if errRet := errors.PanicToError(func() {
sv.readCh <- m
xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content)
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed")
return
}
}
}
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnSenderFn := func(conn net.Conn) {
defer func() {
conn.Close()
wg.Done()
}()
var errRet error
if firstPacket != nil {
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", firstPacket.Content)
}
for {
select {
case udpMsg, ok := <-sv.sendCh:
if !ok {
xl.Info("sender goroutine for udp work connection closed")
return
}
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", udpMsg.Content)
case <-closeCh:
return
}
}
}
go workConnReaderFn(workConn)
go workConnSenderFn(workConn)
wg.Wait()
xl.Info("sudp worker is closed")
}
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
xl := xlog.FromContextSafe(sv.ctx)
visitorConn, err := sv.helper.ConnectServer()
if err != nil {
return nil, fmt.Errorf("frpc connect frps error: %v", err)
}
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
RunID: sv.helper.RunID(),
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
Timestamp: now,
UseEncryption: sv.cfg.Transport.UseEncryption,
UseCompression: sv.cfg.Transport.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return nil, err
}
}
if sv.cfg.Transport.UseCompression {
remote = libio.WithCompression(remote)
}
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
}
func (sv *SUDPVisitor) Close() {
sv.mu.Lock()
defer sv.mu.Unlock()
select {
case <-sv.checkCloseCh:
return
default:
close(sv.checkCloseCh)
}
sv.BaseVisitor.Close()
if sv.udpConn != nil {
sv.udpConn.Close()
}
close(sv.readCh)
close(sv.sendCh)
}

104
client/visitor/visitor.go Normal file
View File

@@ -0,0 +1,104 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// 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 visitor
import (
"context"
"net"
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
// Helper wrapps some functions for visitor to use.
type Helper interface {
// ConnectServer directly connects to the frp server.
ConnectServer() (net.Conn, error)
// TransferConn transfers the connection to another visitor.
TransferConn(string, net.Conn) error
// MsgTransporter returns the message transporter that is used to send and receive messages
// to the frp server through the controller.
MsgTransporter() transport.MessageTransporter
// RunID returns the run id of current controller.
RunID() string
}
// Visitor is used for forward traffics from local port tot remote service.
type Visitor interface {
Run() error
AcceptConn(conn net.Conn) error
Close()
}
func NewVisitor(
ctx context.Context,
cfg v1.VisitorConfigurer,
clientCfg *v1.ClientCommonConfig,
helper Helper,
) (visitor Visitor) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
baseVisitor := BaseVisitor{
clientCfg: clientCfg,
helper: helper,
ctx: xlog.NewContext(ctx, xl),
internalLn: utilnet.NewInternalListener(),
}
switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig:
visitor = &STCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *v1.XTCPVisitorConfig:
visitor = &XTCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
startTunnelCh: make(chan struct{}),
}
case *v1.SUDPVisitorConfig:
visitor = &SUDPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
checkCloseCh: make(chan struct{}),
}
}
return
}
type BaseVisitor struct {
clientCfg *v1.ClientCommonConfig
helper Helper
l net.Listener
internalLn *utilnet.InternalListener
mu sync.RWMutex
ctx context.Context
}
func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
return v.internalLn.PutConn(conn)
}
func (v *BaseVisitor) Close() {
if v.l != nil {
v.l.Close()
}
if v.internalLn != nil {
v.internalLn.Close()
}
}

View File

@@ -12,43 +12,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package client
package visitor
import (
"context"
"fmt"
"net"
"reflect"
"sync"
"time"
"github.com/fatedier/frp/pkg/config"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
)
type VisitorManager struct {
ctl *Control
cfgs map[string]config.VisitorConf
visitors map[string]Visitor
type Manager struct {
clientCfg *v1.ClientCommonConfig
cfgs map[string]v1.VisitorConfigurer
visitors map[string]Visitor
helper Helper
checkInterval time.Duration
mu sync.Mutex
mu sync.RWMutex
ctx context.Context
stopCh chan struct{}
}
func NewVisitorManager(ctx context.Context, ctl *Control) *VisitorManager {
return &VisitorManager{
ctl: ctl,
cfgs: make(map[string]config.VisitorConf),
func NewManager(
ctx context.Context,
runID string,
clientCfg *v1.ClientCommonConfig,
connectServer func() (net.Conn, error),
msgTransporter transport.MessageTransporter,
) *Manager {
m := &Manager{
clientCfg: clientCfg,
cfgs: make(map[string]v1.VisitorConfigurer),
visitors: make(map[string]Visitor),
checkInterval: 10 * time.Second,
ctx: ctx,
stopCh: make(chan struct{}),
}
m.helper = &visitorHelperImpl{
connectServerFn: connectServer,
msgTransporter: msgTransporter,
transferConnFn: m.TransferConn,
runID: runID,
}
return m
}
func (vm *VisitorManager) Run() {
func (vm *Manager) Run() {
xl := xlog.FromContextSafe(vm.ctx)
ticker := time.NewTicker(vm.checkInterval)
@@ -62,7 +81,7 @@ func (vm *VisitorManager) Run() {
case <-ticker.C:
vm.mu.Lock()
for _, cfg := range vm.cfgs {
name := cfg.GetBaseInfo().ProxyName
name := cfg.GetBaseConfig().Name
if _, exist := vm.visitors[name]; !exist {
xl.Info("try to start visitor [%s]", name)
_ = vm.startVisitor(cfg)
@@ -73,11 +92,24 @@ func (vm *VisitorManager) Run() {
}
}
func (vm *Manager) Close() {
vm.mu.Lock()
defer vm.mu.Unlock()
for _, v := range vm.visitors {
v.Close()
}
select {
case <-vm.stopCh:
default:
close(vm.stopCh)
}
}
// Hold lock before calling this function.
func (vm *VisitorManager) startVisitor(cfg config.VisitorConf) (err error) {
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
xl := xlog.FromContextSafe(vm.ctx)
name := cfg.GetBaseInfo().ProxyName
visitor := NewVisitor(vm.ctx, vm.ctl, cfg)
name := cfg.GetBaseConfig().Name
visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
err = visitor.Run()
if err != nil {
xl.Warn("start error: %v", err)
@@ -88,18 +120,19 @@ func (vm *VisitorManager) startVisitor(cfg config.VisitorConf) (err error) {
return
}
func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) {
func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
xl := xlog.FromContextSafe(vm.ctx)
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
return c.GetBaseConfig().Name
})
vm.mu.Lock()
defer vm.mu.Unlock()
delNames := make([]string, 0)
for name, oldCfg := range vm.cfgs {
del := false
cfg, ok := cfgs[name]
if !ok {
del = true
} else if !oldCfg.Compare(cfg) {
cfg, ok := cfgsMap[name]
if !ok || !reflect.DeepEqual(oldCfg, cfg) {
del = true
}
@@ -117,7 +150,8 @@ func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) {
}
addNames := make([]string, 0)
for name, cfg := range cfgs {
for _, cfg := range cfgs {
name := cfg.GetBaseConfig().Name
if _, ok := vm.cfgs[name]; !ok {
vm.cfgs[name] = cfg
addNames = append(addNames, name)
@@ -129,15 +163,36 @@ func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) {
}
}
func (vm *VisitorManager) Close() {
vm.mu.Lock()
defer vm.mu.Unlock()
for _, v := range vm.visitors {
v.Close()
}
select {
case <-vm.stopCh:
default:
close(vm.stopCh)
// TransferConn transfers a connection to a visitor.
func (vm *Manager) TransferConn(name string, conn net.Conn) error {
vm.mu.RLock()
defer vm.mu.RUnlock()
v, ok := vm.visitors[name]
if !ok {
return fmt.Errorf("visitor [%s] not found", name)
}
return v.AcceptConn(conn)
}
type visitorHelperImpl struct {
connectServerFn func() (net.Conn, error)
msgTransporter transport.MessageTransporter
transferConnFn func(name string, conn net.Conn) error
runID string
}
func (v *visitorHelperImpl) ConnectServer() (net.Conn, error) {
return v.connectServerFn()
}
func (v *visitorHelperImpl) TransferConn(name string, conn net.Conn) error {
return v.transferConnFn(name, conn)
}
func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
return v.msgTransporter
}
func (v *visitorHelperImpl) RunID() string {
return v.runID
}

457
client/visitor/xtcp.go Normal file
View File

@@ -0,0 +1,457 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// 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 visitor
import (
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
libio "github.com/fatedier/golib/io"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"golang.org/x/time/rate"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
var ErrNoTunnelSession = errors.New("no tunnel session")
type XTCPVisitor struct {
*BaseVisitor
session TunnelSession
startTunnelCh chan struct{}
retryLimiter *rate.Limiter
cancel context.CancelFunc
cfg *v1.XTCPVisitorConfig
}
func (sv *XTCPVisitor) Run() (err error) {
sv.ctx, sv.cancel = context.WithCancel(sv.ctx)
if sv.cfg.Protocol == "kcp" {
sv.session = NewKCPTunnelSession()
} else {
sv.session = NewQUICTunnelSession(sv.clientCfg)
}
if sv.cfg.BindPort > 0 {
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return
}
go sv.worker()
}
go sv.internalConnWorker()
go sv.processTunnelStartEvents()
if sv.cfg.KeepTunnelOpen {
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
go sv.keepTunnelOpenWorker()
}
return
}
func (sv *XTCPVisitor) Close() {
sv.mu.Lock()
defer sv.mu.Unlock()
sv.BaseVisitor.Close()
if sv.cancel != nil {
sv.cancel()
}
if sv.session != nil {
sv.session.Close()
}
}
func (sv *XTCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("xtcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *XTCPVisitor) internalConnWorker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.internalLn.Accept()
if err != nil {
xl.Warn("xtcp internal listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *XTCPVisitor) processTunnelStartEvents() {
for {
select {
case <-sv.ctx.Done():
return
case <-sv.startTunnelCh:
start := time.Now()
sv.makeNatHole()
duration := time.Since(start)
// avoid too frequently
if duration < 10*time.Second {
time.Sleep(10*time.Second - duration)
}
}
}
}
func (sv *XTCPVisitor) keepTunnelOpenWorker() {
xl := xlog.FromContextSafe(sv.ctx)
ticker := time.NewTicker(time.Duration(sv.cfg.MinRetryInterval) * time.Second)
defer ticker.Stop()
sv.startTunnelCh <- struct{}{}
for {
select {
case <-sv.ctx.Done():
return
case <-ticker.C:
xl.Debug("keepTunnelOpenWorker try to check tunnel...")
conn, err := sv.getTunnelConn()
if err != nil {
xl.Warn("keepTunnelOpenWorker get tunnel connection error: %v", err)
_ = sv.retryLimiter.Wait(sv.ctx)
continue
}
xl.Debug("keepTunnelOpenWorker check success")
if conn != nil {
conn.Close()
}
}
}
}
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
isConnTrasfered := false
defer func() {
if !isConnTrasfered {
userConn.Close()
}
}()
xl.Debug("get a new xtcp user connection")
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
ctx := context.Background()
if sv.cfg.FallbackTo != "" {
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
defer cancel()
ctx = timeoutCtx
}
tunnelConn, err := sv.openTunnel(ctx)
if err != nil {
xl.Error("open tunnel error: %v", err)
// no fallback, just return
if sv.cfg.FallbackTo == "" {
return
}
xl.Debug("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil {
xl.Error("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return
}
isConnTrasfered = true
return
}
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
if sv.cfg.Transport.UseEncryption {
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.Transport.UseCompression {
var recycleFn func()
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
defer recycleFn()
}
_, _, errs := libio.Join(userConn, muxConnRWCloser)
xl.Debug("join connections closed")
if len(errs) > 0 {
xl.Trace("join connections errors: %v", errs)
}
}
// 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()
timeoutC := time.After(20 * time.Second)
immediateTrigger := make(chan struct{}, 1)
defer close(immediateTrigger)
immediateTrigger <- struct{}{}
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.Warn("get tunnel connection error: %v", err)
}
continue
}
return conn, nil
}
}
func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
conn, err := sv.session.OpenConn(sv.ctx)
if err == nil {
return conn, nil
}
sv.session.Close()
select {
case sv.startTunnelCh <- struct{}{}:
default:
}
return nil, err
}
// 0. PreCheck
// 1. Prepare
// 2. ExchangeInfo
// 3. MakeNATHole
// 4. Create a tunnel session using an underlying UDP connection.
func (sv *XTCPVisitor) makeNatHole() {
xl := xlog.FromContextSafe(sv.ctx)
xl.Trace("makeNatHole start")
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
xl.Warn("nathole precheck error: %v", err)
return
}
xl.Trace("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
if err != nil {
xl.Warn("nathole prepare error: %v", err)
return
}
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
listenConn := prepareResult.ListenConn
// send NatHoleVisitor to server
now := time.Now().Unix()
transactionID := nathole.NewTransactionID()
natHoleVisitorMsg := &msg.NatHoleVisitor{
TransactionID: transactionID,
ProxyName: sv.cfg.ServerName,
Protocol: sv.cfg.Protocol,
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
Timestamp: now,
MappedAddrs: prepareResult.Addrs,
AssistedAddrs: prepareResult.AssistedAddrs,
}
xl.Trace("nathole exchange info start")
natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second)
if err != nil {
listenConn.Close()
xl.Warn("nathole exchange info error: %v", err)
return
}
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey))
if err != nil {
listenConn.Close()
xl.Warn("make hole error: %v", err)
return
}
listenConn = newListenConn
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
if err := sv.session.Init(listenConn, raddr); err != nil {
listenConn.Close()
xl.Warn("init tunnel session error: %v", err)
return
}
}
type TunnelSession interface {
Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error
OpenConn(context.Context) (net.Conn, error)
Close()
}
type KCPTunnelSession struct {
session *fmux.Session
lConn *net.UDPConn
mu sync.RWMutex
}
func NewKCPTunnelSession() TunnelSession {
return &KCPTunnelSession{}
}
func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {
listenConn.Close()
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
lConn, err := net.DialUDP("udp", laddr, raddr)
if err != nil {
return fmt.Errorf("dial udp error: %v", err)
}
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil {
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 10 * time.Second
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
fmuxCfg.LogOutput = io.Discard
session, err := fmux.Client(remote, fmuxCfg)
if err != nil {
remote.Close()
return fmt.Errorf("initial client session error: %v", err)
}
ks.mu.Lock()
ks.session = session
ks.lConn = lConn
ks.mu.Unlock()
return nil
}
func (ks *KCPTunnelSession) OpenConn(_ context.Context) (net.Conn, error) {
ks.mu.RLock()
defer ks.mu.RUnlock()
session := ks.session
if session == nil {
return nil, ErrNoTunnelSession
}
return session.Open()
}
func (ks *KCPTunnelSession) Close() {
ks.mu.Lock()
defer ks.mu.Unlock()
if ks.session != nil {
_ = ks.session.Close()
ks.session = nil
}
if ks.lConn != nil {
_ = ks.lConn.Close()
ks.lConn = nil
}
}
type QUICTunnelSession struct {
session quic.Connection
listenConn *net.UDPConn
mu sync.RWMutex
clientCfg *v1.ClientCommonConfig
}
func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession {
return &QUICTunnelSession{
clientCfg: clientCfg,
}
}
func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {
tlsConfig, err := transport.NewClientTLSConfig("", "", "", raddr.String())
if err != nil {
return fmt.Errorf("create tls config error: %v", err)
}
tlsConfig.NextProtos = []string{"frp"}
quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,
&quic.Config{
MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),
KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
})
if err != nil {
return fmt.Errorf("dial quic error: %v", err)
}
qs.mu.Lock()
qs.session = quicConn
qs.listenConn = listenConn
qs.mu.Unlock()
return nil
}
func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
qs.mu.RLock()
defer qs.mu.RUnlock()
session := qs.session
if session == nil {
return nil, ErrNoTunnelSession
}
stream, err := session.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
return utilnet.QuicStreamToNetConn(stream, session), nil
}
func (qs *QUICTunnelSession) Close() {
qs.mu.Lock()
defer qs.mu.Unlock()
if qs.session != nil {
_ = qs.session.CloseWithError(0, "")
qs.session = nil
}
if qs.listenConn != nil {
_ = qs.listenConn.Close()
qs.listenConn = nil
}
}

117
cmd/frpc/sub/admin.go Normal file
View File

@@ -0,0 +1,117 @@
// Copyright 2023 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 sub
import (
"fmt"
"os"
"strings"
"github.com/rodaine/table"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
)
func init() {
rootCmd.AddCommand(NewAdminCommand(
"reload",
"Hot-Reload frpc configuration",
ReloadHandler,
))
rootCmd.AddCommand(NewAdminCommand(
"status",
"Overview of all proxies status",
StatusHandler,
))
rootCmd.AddCommand(NewAdminCommand(
"stop",
"Stop the running frpc",
StopHandler,
))
}
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
return &cobra.Command{
Use: name,
Short: short,
Run: func(cmd *cobra.Command, args []string) {
cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if cfg.WebServer.Port <= 0 {
fmt.Println("web server port should be set if you want to use this feature")
os.Exit(1)
}
if err := handler(cfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
}
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Reload(); err != nil {
return err
}
fmt.Println("reload success")
return nil
}
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
res, err := client.GetAllProxyStatus()
if err != nil {
return err
}
fmt.Printf("Proxy Status...\n\n")
for _, typ := range proxyTypes {
arrs := res[string(typ)]
if len(arrs) == 0 {
continue
}
fmt.Println(strings.ToUpper(string(typ)))
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range arrs {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
return nil
}
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Stop(); err != nil {
return err
}
fmt.Println("stop success")
return nil
}

125
cmd/frpc/sub/flags.go Normal file
View File

@@ -0,0 +1,125 @@
// Copyright 2023 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 sub
import (
"fmt"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
type BandwidthQuantityFlag struct {
V *types.BandwidthQuantity
}
func (f *BandwidthQuantityFlag) Set(s string) error {
return f.V.UnmarshalString(s)
}
func (f *BandwidthQuantityFlag) String() string {
return f.V.String()
}
func (f *BandwidthQuantityFlag) Type() string {
return "string"
}
func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
registerProxyBaseConfigFlags(cmd, c.GetBaseConfig())
switch cc := c.(type) {
case *v1.TCPProxyConfig:
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
case *v1.UDPProxyConfig:
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
case *v1.HTTPProxyConfig:
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations")
cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
case *v1.HTTPSProxyConfig:
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
case *v1.TCPMuxProxyConfig:
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer")
case *v1.STCPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
case *v1.SUDPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
case *v1.XTCPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
}
}
func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) {
if c == nil {
return
}
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port")
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)")
}
func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
if c == nil {
return
}
cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains")
cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
}
func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) {
registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig())
// add visitor flags if exist
}
func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig) {
if c == nil {
return
}
cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name")
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
}
func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig) {
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
}

View File

@@ -1,98 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(httpCmd)
httpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
httpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
httpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
httpCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
httpCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations")
httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user")
httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password")
httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
httpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(httpCmd)
}
var httpCmd = &cobra.Command{
Use: "http",
Short: "Run frpc with a single http proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg := &config.HTTPProxyConf{}
var prefix string
if user != "" {
prefix = user + "."
}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.HTTPProxy
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.CustomDomains = strings.Split(customDomains, ",")
cfg.SubDomain = subDomain
cfg.Locations = strings.Split(locations, ",")
cfg.HTTPUser = httpUser
cfg.HTTPPwd = httpPwd
cfg.HostHeaderRewrite = hostHeaderRewrite
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := map[string]config.ProxyConf{
cfg.ProxyName: cfg,
}
err = startService(clientCfg, proxyConfs, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

View File

@@ -1,90 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(httpsCmd)
httpsCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
httpsCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
httpsCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
httpsCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(httpsCmd)
}
var httpsCmd = &cobra.Command{
Use: "https",
Short: "Run frpc with a single https proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg := &config.HTTPSProxyConf{}
var prefix string
if user != "" {
prefix = user + "."
}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.HTTPSProxy
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.CustomDomains = strings.Split(customDomains, ",")
cfg.SubDomain = subDomain
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := map[string]config.ProxyConf{
cfg.ProxyName: cfg,
}
err = startService(clientCfg, proxyConfs, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

96
cmd/frpc/sub/nathole.go Normal file
View File

@@ -0,0 +1,96 @@
// Copyright 2023 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/nathole"
)
var (
natHoleSTUNServer string
natHoleLocalAddr string
)
func init() {
rootCmd.AddCommand(natholeCmd)
natholeCmd.AddCommand(natholeDiscoveryCmd)
natholeCmd.PersistentFlags().StringVarP(&natHoleSTUNServer, "nat_hole_stun_server", "", "", "STUN server address for nathole")
natholeCmd.PersistentFlags().StringVarP(&natHoleLocalAddr, "nat_hole_local_addr", "l", "", "local address to connect STUN server")
}
var natholeCmd = &cobra.Command{
Use: "nathole",
Short: "Actions about nathole",
}
var natholeDiscoveryCmd = &cobra.Command{
Use: "discover",
Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters
cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
if err != nil {
cfg = &v1.ClientCommonConfig{}
}
if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer
}
if err := validateForNatHoleDiscovery(cfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
addrs, localAddr, err := nathole.Discover([]string{cfg.NatHoleSTUNServer}, natHoleLocalAddr)
if err != nil {
fmt.Println("discover error:", err)
os.Exit(1)
}
if len(addrs) < 2 {
fmt.Printf("discover error: can not get enough addresses, need 2, got: %v\n", addrs)
os.Exit(1)
}
localIPs, _ := nathole.ListLocalIPsForNatHole(10)
natFeature, err := nathole.ClassifyNATFeature(addrs, localIPs)
if err != nil {
fmt.Println("classify nat feature error:", err)
os.Exit(1)
}
fmt.Println("STUN server:", cfg.NatHoleSTUNServer)
fmt.Println("Your NAT type is:", natFeature.NatType)
fmt.Println("Behavior is:", natFeature.Behavior)
fmt.Println("External address is:", addrs)
fmt.Println("Local address is:", localAddr.String())
fmt.Println("Public Network:", natFeature.PublicNetwork)
return nil
},
}
func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error {
if cfg.NatHoleSTUNServer == "" {
return fmt.Errorf("nat_hole_stun_server can not be empty")
}
return nil
}

120
cmd/frpc/sub/proxy.go Normal file
View File

@@ -0,0 +1,120 @@
// Copyright 2023 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 sub
import (
"fmt"
"os"
"github.com/samber/lo"
"github.com/spf13/cobra"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
var proxyTypes = []v1.ProxyType{
v1.ProxyTypeTCP,
v1.ProxyTypeUDP,
v1.ProxyTypeTCPMUX,
v1.ProxyTypeHTTP,
v1.ProxyTypeHTTPS,
v1.ProxyTypeSTCP,
v1.ProxyTypeSUDP,
v1.ProxyTypeXTCP,
}
var visitorTypes = []v1.VisitorType{
v1.VisitorTypeSTCP,
v1.VisitorTypeSUDP,
v1.VisitorTypeXTCP,
}
func init() {
for _, typ := range proxyTypes {
c := v1.NewProxyConfigurerByType(typ)
if c == nil {
panic("proxy type: " + typ + " not support")
}
clientCfg := v1.ClientCommonConfig{}
cmd := NewProxyCommand(string(typ), c, &clientCfg)
RegisterClientCommonConfigFlags(cmd, &clientCfg)
RegisterProxyFlags(cmd, c)
// add sub command for visitor
if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
if vc == nil {
panic("visitor type: " + typ + " not support")
}
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
RegisterVisitorFlags(visitorCmd, vc)
cmd.AddCommand(visitorCmd)
}
rootCmd.AddCommand(cmd)
}
}
func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
return &cobra.Command{
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 {
fmt.Println(err)
os.Exit(1)
}
c.Complete(clientCfg.User)
c.GetBaseConfig().Type = name
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
fmt.Println(err)
os.Exit(1)
}
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
}
func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
return &cobra.Command{
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 {
fmt.Println(err)
os.Exit(1)
}
c.Complete(clientCfg)
c.GetBaseConfig().Type = name
if err := validation.ValidateVisitorConfigurer(c); err != nil {
fmt.Println(err)
os.Exit(1)
}
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
}

View File

@@ -1,84 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
)
func init() {
rootCmd.AddCommand(reloadCmd)
}
var reloadCmd = &cobra.Command{
Use: "reload",
Short: "Hot-Reload frpc configuration",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _, _, err := config.ParseClientConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = reload(cfg)
if err != nil {
fmt.Printf("frpc reload error: %v\n", err)
os.Exit(1)
}
fmt.Printf("reload success\n")
return nil
},
}
func reload(clientCfg config.ClientCommonConf) error {
if clientCfg.AdminPort == 0 {
return fmt.Errorf("admin_port shoud be set if you want to use reload feature")
}
req, err := http.NewRequest("GET", "http://"+
clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/reload", nil)
if err != nil {
return err
}
authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
clientCfg.AdminPwd))
req.Header.Add("Authorization", authStr)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
}

View File

@@ -15,13 +15,12 @@
package sub
import (
"context"
"fmt"
"io/fs"
"net"
"os"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
@@ -29,53 +28,17 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/auth"
"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/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
const (
CfgFileTypeIni = iota
CfgFileTypeCmd
)
var (
cfgFile string
cfgDir string
showVersion bool
serverAddr string
user string
protocol string
token string
logLevel string
logFile string
logMaxDays int
disableLogColor bool
proxyName string
localIP string
localPort int
remotePort int
useEncryption bool
useCompression bool
bandwidthLimit string
bandwidthLimitMode string
customDomains string
subDomain string
httpUser string
httpPwd string
locations string
hostHeaderRewrite string
role string
sk string
multiplexer string
serverName string
bindAddr string
bindPort int
tlsEnable bool
)
func init() {
@@ -84,18 +47,6 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
}
func RegisterCommonFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address")
cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user")
cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket")
cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path")
cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days")
cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", false, "enable frpc tls")
}
var rootCmd = &cobra.Command{
Use: "frpc",
Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
@@ -108,26 +59,7 @@ var rootCmd = &cobra.Command{
// 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 != "" {
var wg sync.WaitGroup
_ = filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
return nil
}
wg.Add(1)
time.Sleep(time.Millisecond)
go func() {
defer wg.Done()
err := runClient(path)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
}
}()
return nil
})
wg.Wait()
_ = runMultipleClients(cfgDir)
return nil
}
@@ -141,58 +73,54 @@ var rootCmd = &cobra.Command{
},
}
func runMultipleClients(cfgDir string) error {
var wg sync.WaitGroup
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
wg.Add(1)
time.Sleep(time.Millisecond)
go func() {
defer wg.Done()
err := runClient(path)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
}
}()
return nil
})
wg.Wait()
return err
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func handleSignal(svr *client.Service, doneCh chan struct{}) {
func handleTermSignal(svr *client.Service) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
svr.GracefulClose(500 * time.Millisecond)
close(doneCh)
}
func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
cfg = config.GetDefaultClientConf()
ipStr, portStr, err := net.SplitHostPort(serverAddr)
if err != nil {
err = fmt.Errorf("invalid server_addr: %v", err)
return
}
cfg.ServerAddr = ipStr
cfg.ServerPort, err = strconv.Atoi(portStr)
if err != nil {
err = fmt.Errorf("invalid server_addr: %v", err)
return
}
cfg.User = user
cfg.Protocol = protocol
cfg.LogLevel = logLevel
cfg.LogFile = logFile
cfg.LogMaxDays = int64(logMaxDays)
cfg.DisableLogColor = disableLogColor
// Only token authentication is supported in cmd mode
cfg.ClientConfig = auth.GetDefaultClientConf()
cfg.Token = token
cfg.TLSEnable = tlsEnable
cfg.Complete()
if err = cfg.Validate(); err != nil {
err = fmt.Errorf("parse config error: %v", err)
return
}
return
}
func runClient(cfgFilePath string) error {
cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
if err != nil {
return err
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
"please use yaml/json/toml format instead!\n")
}
warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
return err
}
@@ -200,34 +128,28 @@ func runClient(cfgFilePath string) error {
}
func startService(
cfg config.ClientCommonConf,
pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf,
cfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
) (err error) {
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
cfg.LogMaxDays, cfg.DisableLogColor)
) error {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
if cfgFile != "" {
log.Trace("start frpc service for config file [%s]", cfgFile)
defer log.Trace("frpc service for config file [%s] stopped", cfgFile)
log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
}
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
if errRet != nil {
err = errRet
return
svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
if err != nil {
return err
}
closedDoneCh := make(chan struct{})
shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
if shouldGracefulClose {
go handleSignal(svr, closedDoneCh)
go handleTermSignal(svr)
}
err = svr.Run()
if err == nil && shouldGracefulClose {
<-closedDoneCh
}
return
_ = svr.Run(context.Background())
return nil
}

View File

@@ -1,107 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/rodaine/table"
"github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
)
func init() {
rootCmd.AddCommand(statusCmd)
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "Overview of all proxies status",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _, _, err := config.ParseClientConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if err = status(cfg); err != nil {
fmt.Printf("frpc get status error: %v\n", err)
os.Exit(1)
}
return nil
},
}
func status(clientCfg config.ClientCommonConf) error {
if clientCfg.AdminPort == 0 {
return fmt.Errorf("admin_port shoud be set if you want to get proxy status")
}
req, err := http.NewRequest("GET", "http://"+
clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/status", nil)
if err != nil {
return err
}
authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
clientCfg.AdminPwd))
req.Header.Add("Authorization", authStr)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("admin api status code [%d]", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
res := make(client.StatusResp)
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body)))
}
fmt.Println("Proxy Status...")
types := []string{"tcp", "udp", "tcpmux", "http", "https", "stcp", "sudp", "xtcp"}
for _, pxyType := range types {
arrs := res[pxyType]
if len(arrs) == 0 {
continue
}
fmt.Println(strings.ToUpper(pxyType))
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range arrs {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
return nil
}

View File

@@ -1,116 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(stcpCmd)
stcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
stcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
stcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
stcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
stcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
stcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
stcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(stcpCmd)
}
var stcpCmd = &cobra.Command{
Use: "stcp",
Short: "Run frpc with a single stcp proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := make(map[string]config.ProxyConf)
visitorConfs := make(map[string]config.VisitorConf)
var prefix string
if user != "" {
prefix = user + "."
}
switch role {
case "server":
cfg := &config.STCPProxyConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.STCPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs[cfg.ProxyName] = cfg
case "visitor":
cfg := &config.STCPVisitorConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.STCPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.ServerName = serverName
cfg.BindAddr = bindAddr
cfg.BindPort = bindPort
err = cfg.Check()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
visitorConfs[cfg.ProxyName] = cfg
default:
fmt.Println("invalid role")
os.Exit(1)
}
err = startService(clientCfg, proxyConfs, visitorConfs, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

View File

@@ -1,115 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(sudpCmd)
sudpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
sudpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
sudpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
sudpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
sudpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
sudpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
sudpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(sudpCmd)
}
var sudpCmd = &cobra.Command{
Use: "sudp",
Short: "Run frpc with a single sudp proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := make(map[string]config.ProxyConf)
visitorConfs := make(map[string]config.VisitorConf)
var prefix string
if user != "" {
prefix = user + "."
}
switch role {
case "server":
cfg := &config.SUDPProxyConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.SUDPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs[cfg.ProxyName] = cfg
case "visitor":
cfg := &config.SUDPVisitorConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.SUDPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.ServerName = serverName
cfg.BindAddr = bindAddr
cfg.BindPort = bindPort
err = cfg.Check()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
visitorConfs[cfg.ProxyName] = cfg
default:
fmt.Println("invalid role")
os.Exit(1)
}
err = startService(clientCfg, proxyConfs, visitorConfs, "")
if err != nil {
os.Exit(1)
}
return nil
},
}

View File

@@ -1,87 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(tcpCmd)
tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(tcpCmd)
}
var tcpCmd = &cobra.Command{
Use: "tcp",
Short: "Run frpc with a single tcp proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg := &config.TCPProxyConf{}
var prefix string
if user != "" {
prefix = user + "."
}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.TCPProxy
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.RemotePort = remotePort
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := map[string]config.ProxyConf{
cfg.ProxyName: cfg,
}
err = startService(clientCfg, proxyConfs, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

View File

@@ -1,92 +0,0 @@
// Copyright 2020 guylewin, guy@lewin.co.il
//
// 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 sub
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(tcpMuxCmd)
tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
tcpMuxCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer")
tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(tcpMuxCmd)
}
var tcpMuxCmd = &cobra.Command{
Use: "tcpmux",
Short: "Run frpc with a single tcpmux proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg := &config.TCPMuxProxyConf{}
var prefix string
if user != "" {
prefix = user + "."
}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.TCPMuxProxy
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.CustomDomains = strings.Split(customDomains, ",")
cfg.SubDomain = subDomain
cfg.Multiplexer = multiplexer
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := map[string]config.ProxyConf{
cfg.ProxyName: cfg,
}
err = startService(clientCfg, proxyConfs, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

View File

@@ -1,87 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(udpCmd)
udpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
udpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
udpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
udpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(udpCmd)
}
var udpCmd = &cobra.Command{
Use: "udp",
Short: "Run frpc with a single udp proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg := &config.UDPProxyConf{}
var prefix string
if user != "" {
prefix = user + "."
}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.UDPProxy
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.RemotePort = remotePort
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := map[string]config.ProxyConf{
cfg.ProxyName: cfg,
}
err = startService(clientCfg, proxyConfs, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
func init() {
@@ -31,7 +32,20 @@ var verifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify that the configures is valid",
RunE: func(cmd *cobra.Command, args []string) error {
_, _, _, err := config.ParseClientConfig(cfgFile)
if cfgFile == "" {
fmt.Println("frpc: the configuration file is not specified")
return nil
}
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
fmt.Println(err)
os.Exit(1)

View File

@@ -1,116 +0,0 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// 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 sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts"
)
func init() {
RegisterCommonFlags(xtcpCmd)
xtcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
xtcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
xtcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
xtcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
xtcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
xtcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
xtcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(xtcpCmd)
}
var xtcpCmd = &cobra.Command{
Use: "xtcp",
Short: "Run frpc with a single xtcp proxy",
RunE: func(cmd *cobra.Command, args []string) error {
clientCfg, err := parseClientCommonCfgFromCmd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs := make(map[string]config.ProxyConf)
visitorConfs := make(map[string]config.VisitorConf)
var prefix string
if user != "" {
prefix = user + "."
}
switch role {
case "server":
cfg := &config.XTCPProxyConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.XTCPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.LocalIP = localIP
cfg.LocalPort = localPort
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proxyConfs[cfg.ProxyName] = cfg
case "visitor":
cfg := &config.XTCPVisitorConf{}
cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.XTCPProxy
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Role = role
cfg.Sk = sk
cfg.ServerName = serverName
cfg.BindAddr = bindAddr
cfg.BindPort = bindPort
err = cfg.Check()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
visitorConfs[cfg.ProxyName] = cfg
default:
fmt.Println("invalid role")
os.Exit(1)
}
err = startService(clientCfg, proxyConfs, visitorConfs, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}

110
cmd/frps/flags.go Normal file
View File

@@ -0,0 +1,110 @@
// Copyright 2023 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 main
import (
"strconv"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
type PortsRangeSliceFlag struct {
V *[]types.PortsRange
}
func (f *PortsRangeSliceFlag) String() string {
if f.V == nil {
return ""
}
return types.PortsRangeSlice(*f.V).String()
}
func (f *PortsRangeSliceFlag) Set(s string) error {
slice, err := types.NewPortsRangeSliceFromString(s)
if err != nil {
return err
}
*f.V = slice
return nil
}
func (f *PortsRangeSliceFlag) Type() string {
return "string"
}
type BoolFuncFlag struct {
TrueFunc func()
FalseFunc func()
v bool
}
func (f *BoolFuncFlag) String() string {
return strconv.FormatBool(f.v)
}
func (f *BoolFuncFlag) Set(s string) error {
f.v = strconv.FormatBool(f.v) == "true"
if !f.v {
if f.FalseFunc != nil {
f.FalseFunc()
}
return nil
}
if f.TrueFunc != nil {
f.TrueFunc()
}
return nil
}
func (f *BoolFuncFlag) Type() string {
return "bool"
}
func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
webServerTLS := v1.TLSConfig{}
cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
cmd.PersistentFlags().VarP(&BoolFuncFlag{
TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
}, "dashboard_tls_mode", "", "if enable dashboard tls mode")
}

View File

@@ -15,9 +15,6 @@
package main
import (
"math/rand"
"time"
"github.com/fatedier/golib/crypto"
_ "github.com/fatedier/frp/assets/frps"
@@ -26,8 +23,5 @@ import (
func main() {
crypto.DefaultSalt = "frp"
// TODO: remove this when we drop support for go1.19
rand.Seed(time.Now().UnixNano())
Execute()
}

View File

@@ -15,85 +15,32 @@
package main
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/auth"
"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/util/log"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/server"
)
const (
CfgFileTypeIni = iota
CfgFileTypeCmd
)
var (
cfgFile string
showVersion bool
bindAddr string
bindPort int
bindUDPPort int
kcpBindPort int
proxyBindAddr string
vhostHTTPPort int
vhostHTTPSPort int
vhostHTTPTimeout int64
dashboardAddr string
dashboardPort int
dashboardUser string
dashboardPwd string
enablePrometheus bool
logFile string
logLevel string
logMaxDays int64
disableLogColor bool
token string
subDomainHost string
allowPorts string
maxPortsPerClient int64
tlsOnly bool
dashboardTLSMode bool
dashboardTLSCertFile string
dashboardTLSKeyFile string
serverCfg v1.ServerConfig
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address")
rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port")
rootCmd.PersistentFlags().IntVarP(&bindUDPPort, "bind_udp_port", "", 0, "bind udp port")
rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dasboard address")
rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port")
rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user")
rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password")
rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days")
rootCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
rootCmd.PersistentFlags().StringVarP(&subDomainHost, "subdomain_host", "", "", "subdomain host")
rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports")
rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only")
rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode")
rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
RegisterServerConfigFlags(rootCmd, &serverCfg)
}
var rootCmd = &cobra.Command{
@@ -105,27 +52,39 @@ var rootCmd = &cobra.Command{
return nil
}
var cfg config.ServerCommonConf
var err error
var (
svrCfg *v1.ServerConfig
isLegacyFormat bool
err error
)
if cfgFile != "" {
var content []byte
content, err = config.GetRenderedConfFromFile(cfgFile)
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
if err != nil {
return err
fmt.Println(err)
os.Exit(1)
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
"please use yaml/json/toml format instead!\n")
}
cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
} else {
cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil)
}
if err != nil {
return err
serverCfg.Complete()
svrCfg = &serverCfg
}
err = runServer(cfg)
warning, err := validation.ValidateServerConfig(svrCfg)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if err := runServer(svrCfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}
@@ -136,71 +95,8 @@ func Execute() {
}
}
func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) {
if fileType == CfgFileTypeIni {
cfg, err = config.UnmarshalServerConfFromIni(source)
} else if fileType == CfgFileTypeCmd {
cfg, err = parseServerCommonCfgFromCmd()
}
if err != nil {
return
}
cfg.Complete()
err = cfg.Validate()
if err != nil {
err = fmt.Errorf("parse config error: %v", err)
return
}
return
}
func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
cfg = config.GetDefaultServerConf()
cfg.BindAddr = bindAddr
cfg.BindPort = bindPort
cfg.BindUDPPort = bindUDPPort
cfg.KCPBindPort = kcpBindPort
cfg.ProxyBindAddr = proxyBindAddr
cfg.VhostHTTPPort = vhostHTTPPort
cfg.VhostHTTPSPort = vhostHTTPSPort
cfg.VhostHTTPTimeout = vhostHTTPTimeout
cfg.DashboardAddr = dashboardAddr
cfg.DashboardPort = dashboardPort
cfg.DashboardUser = dashboardUser
cfg.DashboardPwd = dashboardPwd
cfg.EnablePrometheus = enablePrometheus
cfg.DashboardTLSCertFile = dashboardTLSCertFile
cfg.DashboardTLSKeyFile = dashboardTLSKeyFile
cfg.DashboardTLSMode = dashboardTLSMode
cfg.LogFile = logFile
cfg.LogLevel = logLevel
cfg.LogMaxDays = logMaxDays
cfg.SubDomainHost = subDomainHost
cfg.TLSOnly = tlsOnly
// Only token authentication is supported in cmd mode
cfg.ServerConfig = auth.GetDefaultServerConf()
cfg.Token = token
if len(allowPorts) > 0 {
// e.g. 1000-2000,2001,2002,3000-4000
ports, errRet := util.ParseRangeNumbers(allowPorts)
if errRet != nil {
err = fmt.Errorf("parse conf error: allow_ports: %v", errRet)
return
}
for _, port := range ports {
cfg.AllowPorts[int(port)] = struct{}{}
}
}
cfg.MaxPortsPerClient = maxPortsPerClient
cfg.DisableLogColor = disableLogColor
return
}
func runServer(cfg config.ServerCommonConf) (err error) {
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor)
func runServer(cfg *v1.ServerConfig) (err error) {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
if cfgFile != "" {
log.Info("frps uses config file: %s", cfgFile)
@@ -213,6 +109,6 @@ func runServer(cfg config.ServerCommonConf) (err error) {
return err
}
log.Info("frps started successfully")
svr.Run()
svr.Run(context.Background())
return
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
func init() {
@@ -32,21 +33,23 @@ var verifyCmd = &cobra.Command{
Short: "Verify that the configures is valid",
RunE: func(cmd *cobra.Command, args []string) error {
if cfgFile == "" {
fmt.Println("no config file is specified")
fmt.Println("frps: the configuration file is not specified")
return nil
}
iniContent, err := config.GetRenderedConfFromFile(cfgFile)
svrCfg, _, err := config.LoadServerConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
_, err = parseServerCommonCfg(CfgFileTypeIni, iniContent)
warning, err := validation.ValidateServerConfig(svrCfg)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
return nil
},

View File

@@ -1,9 +0,0 @@
[common]
server_addr = 127.0.0.1
server_port = 7000
[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000

360
conf/frpc.toml Normal file
View File

@@ -0,0 +1,360 @@
# your proxy name will be changed to {user}.{proxy}
user = "your_name"
# A literal address or host name for IPv6 must be enclosed
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
# For single serverAddr field, no need square brackets, like serverAddr = "::".
serverAddr = "0.0.0.0"
serverPort = 7000
# STUN server to help penetrate NAT hole.
# natHoleStunServer = "stun.easyvoip.com:3478"
# Decide if exit program when first login failed, otherwise continuous relogin to frps
# default is true
loginFailExit = true
# console or real logFile path like ./frpc.log
log.to = "./frpc.log"
# trace, debug, info, warn, error
log.level = "info"
log.maxDays = 3
# disable log colors when log.to is console, default is false
log.disablePrintColor = false
auth.method = "token"
# auth.additionalScopes specifies additional scopes to include authentication information.
# Optional values are HeartBeats, NewWorkConns.
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
# auth token
auth.token = "12345678"
# 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.
# auth.oidc.clientSecret = ""
# oidc.audience specifies the audience of the token in OIDC authentication.
# auth.oidc.audience = ""
# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
# auth.oidc.scope = ""
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token.
# auth.oidc.tokenEndpointURL = ""
# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint.
# For example, if you want to specify the "audience" parameter, you can set as follow.
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
# auth.oidc.additionalEndpointParams.var1 = "foobar"
# Set admin address for control frpc's action by http api such as reload
webServer.addr = "127.0.0.1"
webServer.port = 7400
webServer.user = "admin"
webServer.password = "admin"
# Admin assets directory. By default, these assets are bundled with frpc.
# webServer.assetsDir = "./static"
# Enable golang pprof handlers in admin listener.
webServer.pprofEnable = false
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
# transport.dialServerTimeout = 10
# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# transport.dialServerKeepalive = 7200
# connections will be established in advance, default value is zero
transport.poolCount = 5
# If tcp stream multiplexing is used, default is true, it must be same with frps
# transport.tcpMux = true
# Specify keep alive interval for tcp mux.
# only valid if tcpMux is enabled.
# transport.tcpMuxKeepaliveInterval = 60
# Communication protocol used to connect to server
# supports tcp, kcp, quic, websocket and wss now, default is tcp
transport.protocol = "tcp"
# set client binding ip when connect server, default is empty.
# only when protocol = tcp or websocket, the value will be used.
transport.connectServerLocalIP = "0.0.0.0"
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
# it only works when protocol is tcp
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080"
# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080"
# quic protocol options
# transport.quic.keepalivePeriod = 10
# transport.quic.maxIdleTimeout = 30
# transport.quic.maxIncomingStreams = 100000
# If tls.enable is true, frpc will connect frps by tls.
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
transport.tls.enable = true
# transport.tls.certFile = "client.crt"
# transport.tls.keyFile = "client.key"
# transport.tls.trustedCaFile = "ca.crt"
# transport.tls.serverName = "example.com"
# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
# first custom byte when tls is enabled.
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
# transport.tls.disableCustomTLSFirstByte = true
# Heartbeat configure, it's not recommended to modify the default value.
# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
# to disable it.
# transport.heartbeatInterval = 30
# transport.heartbeatTimeout = 90
# Specify a dns server, so frpc will use this instead of default one
# dnsServer = "8.8.8.8"
# Proxy names you want to start.
# Default is empty, means all proxies.
# start = ["ssh", "dns"]
# 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.
udpPacketSize = 1500
# Additional metadatas for client.
metadatas.var1 = "abc"
metadatas.var2 = "123"
# Include other config files for proxies.
# includes = ["./confd/*.ini"]
[[proxies]]
# 'ssh' is the unique proxy name
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
# Limit bandwidth for this proxy, unit is KB and MB
transport.bandwidthLimit = "1MB"
# Where to limit bandwidth, can be 'client' or 'server', default is 'client'
transport.bandwidthLimitMode = "client"
# If true, traffic of this proxy will be encrypted, default is false
transport.useEncryption = false
# If true, traffic will be compressed
transport.useCompression = false
# Remote port listen by frps
remotePort = 6001
# frps will load balancing connections for proxies in same group
loadBalancer.group = "test_group"
# group should have same group key
loadBalancer.groupKey = "123456"
# Enable health check for the backend service, it supports 'tcp' and 'http' now.
# frpc will connect local service's port to detect it's healthy status
healthCheck.type = "tcp"
# Health check connection timeout
healthCheck.timeoutSeconds = 3
# If continuous failed in 3 times, the proxy will be removed from frps
healthCheck.maxFailed = 3
# every 10 seconds will do a health check
healthCheck.intervalSeconds = 10
# additional meta info for each proxy
metadatas.var1 = "abc"
metadatas.var2 = "123"
[[proxies]]
name = "ssh_random"
type = "tcp"
localIP = "192.168.31.100"
localPort = 22
# If remote_port is 0, frps will assign a random port for you
remotePort = 0
[[proxies]]
name = "dns"
type = "udp"
localIP = "114.114.114.114"
localPort = 53
remotePort = 6002
# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
[[proxies]]
name = "web01"
type = "http"
localIP = "127.0.0.1"
localPort = 80
# http username and password are safety certification for http protocol
# if not set, you can access this custom_domains without certification
httpUser = "admin"
httpPassword = "admin"
# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
subdomain = "web01"
customDomains = ["web01.yourdomain.com"]
# locations is only available for http type
locations = ["/", "/pic"]
# route requests to this service if http basic auto user is abc
# route_by_http_user = abc
hostHeaderRewrite = "example.com"
# params with prefix "header_" will be used to update http request headers
requestHeaders.set.x-from-where = "frp"
healthCheck.type = "http"
# frpc will send a GET http request '/status' to local http service
# http service is alive when it return 2xx http response code
healthCheck.path = "/status"
healthCheck.intervalSeconds = 10
healthCheck.maxFailed = 3
healthCheck.timeoutSeconds = 3
[[proxies]]
name = "web02"
type = "https"
localIP = "127.0.0.1"
localPort = 8000
subdomain = "web02"
customDomains = ["web02.yourdomain.com"]
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
# v1 or v2 or empty
transport.proxyProtocolVersion = "v2"
[[proxies]]
name = "tcpmuxhttpconnect"
type = "tcpmux"
multiplexer = "httpconnect"
localIP = "127.0.0.1"
localPort = 10701
customDomains = ["tunnel1"]
# routeByHTTPUser = "user1"
[[proxies]]
name = "plugin_unix_domain_socket"
type = "tcp"
remotePort = 6003
# if plugin is defined, local_ip and local_port is useless
# plugin will handle connections got from frps
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/var/run/docker.sock"
[[proxies]]
name = "plugin_http_proxy"
type = "tcp"
remotePort = 6004
[proxies.plugin]
type = "http_proxy"
httpUser = "abc"
httpPassword = "abc"
[[proxies]]
name = "plugin_socks5"
type = "tcp"
remotePort = 6005
[proxies.plugin]
type = "socks5"
username = "abc"
password = "abc"
[[proxies]]
name = "plugin_static_file"
type = "tcp"
remotePort = 6006
[proxies.plugin]
type = "static_file"
localPath = "/var/www/blog"
stripPrefix = "static"
httpUser = "abc"
httpPassword = "abc"
[[proxies]]
name = "plugin_https2http"
type = "https"
customDomains = ["test.yourdomain.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:80"
crtPath = "./server.crt"
keyPath = "./server.key"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_https2https"
type = "https"
customDomains = ["test.yourdomain.com"]
[proxies.plugin]
type = "https2https"
localAddr = "127.0.0.1:443"
crtPath = "./server.crt"
keyPath = "./server.key"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_http2https"
type = "http"
customDomains = ["test.yourdomain.com"]
[proxies.plugin]
type = "http2https"
localAddr = "127.0.0.1:443"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "secret_tcp"
# If the type is secret tcp, remote_port is useless
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
type = "stcp"
# secretKey is used for authentication for visitors
secretKey = "abcdefg"
localIP = "127.0.0.1"
localPort = 22
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["*"]
[[proxies]]
name = "p2p_tcp"
type = "xtcp"
secretKey = "abcdefg"
localIP = "127.0.0.1"
localPort = 22
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["user1", "user2"]
# frpc role visitor -> frps -> frpc role server
[[visitors]]
name = "secret_tcp_visitor"
type = "stcp"
# the server name you want to visitor
serverName = "secret_tcp"
secretKey = "abcdefg"
# connect this address to visitor stcp server
bindAddr = "127.0.0.1"
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bindPort = 9000
[[visitors]]
name = "p2p_tcp_visitor"
type = "xtcp"
# if the server user is not set, it defaults to the current user
serverUser = "user1"
serverName = "p2p_tcp"
secretKey = "abcdefg"
bindAddr = "127.0.0.1"
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bindPort = 9001
# when automatic tunnel persistence is required, set it to true
keepTunnelOpen = false
# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
maxRetriesAnHour = 8
minRetryInterval = 90
# fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500

View File

@@ -1,2 +0,0 @@
[common]
bind_port = 7000

154
conf/frps.toml Normal file
View File

@@ -0,0 +1,154 @@
# A literal address or host name for IPv6 must be enclosed
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
# For single "bind_addr" field, no need square brackets, like "bind_addr = ::".
bindAddr = "0.0.0.0"
bindPort = 7000
# udp port used for kcp protocol, it can be same with 'bind_port'.
# if not set, kcp is disabled in frps.
kcpBindPort = 7000
# udp port used for quic protocol.
# if not set, quic is disabled in frps.
# quicBindPort = 7002
# Specify which address proxy will listen for, default value is same with bind_addr
# proxy_bind_addr = "127.0.0.1"
# quic protocol options
# transport.quic.keepalivePeriod = 10
# transport.quic.maxIdleTimeout = 30
# transport.quic.maxIncomingStreams = 100000
# Heartbeat configure, it's not recommended to modify the default value
# The default value of heartbeat_timeout is 90. Set negative value to disable it.
# transport.heartbeatTimeout = 90
# Pool count in each proxy will keep no more than maxPoolCount.
transport.maxPoolCount = 5
# If tcp stream multiplexing is used, default is true
# transport.tcpMux = true
# Specify keep alive interval for tcp mux.
# only valid if tcpMux is true.
# transport.tcpMuxKeepaliveInterval = 60
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# transport.tcpKeepalive = 7200
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
tls.force = false
# transport.tls.certFile = "server.crt"
# transport.tls.keyFile = "server.key"
# transport.tls.trustedCaFile = "ca.crt"
# If you want to support virtual host, you must set the http port for listening (optional)
# Note: http port and https port can be same with bind_port
vhostHTTPPort = 80
vhostHTTPSPort = 443
# Response header timeout(seconds) for vhost http server, default is 60s
# vhostHTTPTimeout = 60
# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
# requests on one single port. If it's not - it will listen on this value for
# HTTP CONNECT requests. By default, this value is 0.
# tcpmuxHTTPConnectPort = 1337
# If tcpmux_passthrough is true, frps won't do any update on traffic.
# tcpmuxPassthrough = false
# Configure the web server to enable the dashboard for frps.
# dashboard is available only if webServer.port is set.
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "admin"
# webServer.tls.certFile = "server.crt"
# webServer.tls.keyFile = "server.key"
# dashboard assets directory(only for debug mode)
# webServer.assetsDir = "./static"
# Enable golang pprof handlers in dashboard listener.
# Dashboard port must be set first
webServer.pprofEnable = false
# enablePrometheus will export prometheus metrics on webServer in /metrics api.
enablePrometheus = true
# console or real logFile path like ./frps.log
log.to = "./frps.log"
# trace, debug, info, warn, error
log.level = info
log.maxDays = 3
# disable log colors when log.to is console, default is false
log.disablePrintColor = false
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
detailedErrorsToClient = true
# auth.method specifies what authentication method to use authenticate frpc with frps.
# If "token" is specified - token will be read into login message.
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
auth.method = "token"
# auth.additionalScopes specifies additional scopes to include authentication information.
# Optional values are HeartBeats, NewWorkConns.
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
# auth token
auth.token = "12345678"
# oidc issuer specifies the issuer to verify OIDC tokens with.
auth.oidc.issuer = ""
# oidc audience specifies the audience OIDC tokens should contain when validated.
auth.oidc.audience = ""
# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired.
auth.oidc.skipExpiryCheck = false
# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
auth.oidc.skipIssuerCheck = false
# userConnTimeout specifies the maximum time to wait for a work connection.
# userConnTimeout = 10
# Only allow frpc to bind ports you list. By default, there won't be any limit.
allowPorts = [
{ start = 2000, end = 3000 },
{ single = 3001 },
{ single = 3003 },
{ start = 4000, end = 50000 }
]
# Max ports can be used for each client, default value is 0 means no limit
maxPortsPerClient = 0
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
# When subdomain is est, the host used by routing is test.frps.com
subDomainHost = "frps.com"
# custom 404 page for HTTP requests
# custom404Page = "/path/to/404.html"
# 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.
udpPacketSize = 1500
# Retention time for NAT hole punching strategy data.
natholeAnalysisDataReserveHours = 168
[[httpPlugins]]
name = "user-manager"
addr = "127.0.0.1:9000"
path = "/handler"
ops = ["Login"]
[[httpPlugins]]
name = "port-manager"
addr = "127.0.0.1:9001"
path = "/handler"
ops = ["NewProxy"]

View File

@@ -6,6 +6,9 @@
server_addr = 0.0.0.0
server_port = 7000
# STUN server to help penetrate NAT hole.
# nat_hole_stun_server = stun.easyvoip.com:3478
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
# dial_server_timeout = 10
@@ -92,7 +95,7 @@ user = your_name
login_fail_exit = true
# communication protocol used to connect to server
# supports tcp, kcp, quic and websocket now, default is tcp
# supports tcp, kcp, quic, websocket and wss now, default is tcp
protocol = tcp
# set client binding ip when connect server, default is empty.
@@ -104,7 +107,8 @@ connect_server_local_ip = 0.0.0.0
# quic_max_idle_timeout = 30
# quic_max_incoming_streams = 100000
# if tls_enable is true, frpc will connect frps by tls
# If tls_enable is true, frpc will connect frps by tls.
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
tls_enable = true
# tls_cert_file = client.crt
@@ -137,9 +141,10 @@ udp_packet_size = 1500
# include other config files for proxies.
# includes = ./confd/*.ini
# By default, frpc will connect frps with first custom byte if tls is enabled.
# If DisableCustomTLSFirstByte is true, frpc will not send that custom byte.
disable_custom_tls_first_byte = false
# If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
# first custom byte when tls is enabled.
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
disable_custom_tls_first_byte = true
# Enable golang pprof handlers in admin listener.
# Admin port must be set first.
@@ -247,7 +252,7 @@ local_ip = 127.0.0.1
local_port = 8000
use_encryption = false
use_compression = false
subdomain = web01
subdomain = web02
custom_domains = web02.yourdomain.com
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
# v1 or v2 or empty
@@ -323,6 +328,9 @@ local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allow_users = *
# user of frpc should be same in both stcp server and stcp visitor
[secret_tcp_visitor]
@@ -334,6 +342,8 @@ server_name = secret_tcp
sk = abcdefg
# connect this address to visitor stcp server
bind_addr = 127.0.0.1
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bind_port = 9000
use_encryption = false
use_compression = false
@@ -345,16 +355,30 @@ local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allow_users = user1, user2
[p2p_tcp_visitor]
role = visitor
type = xtcp
# if the server user is not set, it defaults to the current user
server_user = user1
server_name = p2p_tcp
sk = abcdefg
bind_addr = 127.0.0.1
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bind_port = 9001
use_encryption = false
use_compression = false
# when automatic tunnel persistence is required, set it to true
keep_tunnel_open = false
# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
max_retries_an_hour = 8
min_retry_interval = 90
# fallback_to = stcp_visitor
# fallback_timeout_ms = 500
[tcpmuxhttpconnect]
type = tcpmux

View File

@@ -6,9 +6,6 @@
bind_addr = 0.0.0.0
bind_port = 7000
# udp port to help make udp hole to penetrate nat
bind_udp_port = 7001
# udp port used for kcp protocol, it can be same with 'bind_port'.
# if not set, kcp is disabled in frps.
kcp_bind_port = 7000
@@ -157,6 +154,9 @@ udp_packet_size = 1500
# Dashboard port must be set first
pprof_enable = false
# Retention time for NAT hole punching strategy data.
nat_hole_analysis_data_reserve_hours = 168
[plugin.user-manager]
addr = 127.0.0.1:9000
path = /handler

View File

@@ -216,49 +216,50 @@ New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcp
### Server Plugin Configuration
```ini
# frps.ini
[common]
bind_port = 7000
```toml
# frps.toml
bindPort = 7000
[plugin.user-manager]
addr = 127.0.0.1:9000
path = /handler
ops = Login
[[httpPlugins]]
name = "user-manager"
addr = "127.0.0.1:9000"
path = "/handler"
ops = ["Login"]
[plugin.port-manager]
addr = 127.0.0.1:9001
path = /handler
ops = NewProxy
[[httpPlugins]]
name = "port-manager"
addr = "127.0.0.1:9001"
path = "/handler"
ops = ["NewProxy"]
```
- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`.
- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`.
- path: http request url path for the POST request.
- ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...).
- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
### Metadata
Metadata will be sent to the server plugin in each RPC request.
There are 2 types of metadata entries - 1 under `[common]` and the other under each proxy configuration.
Metadata entries under `[common]` will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
There are 2 types of metadata entries - global one and the other under each proxy configuration.
Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`.
Metadata entries start with `meta_`. This is an example of metadata entries in `[common]` and under the proxy named `[ssh]`:
This is an example of metadata entries:
```
# frpc.ini
[common]
server_addr = 127.0.0.1
server_port = 7000
user = fake
meta_token = fake
meta_version = 1.0.0
```toml
# frpc.toml
serverAddr = "127.0.0.1"
serverPort = 7000
user = "fake"
metadatas.token = "fake"
metadatas.version = "1.0.0"
[ssh]
type = tcp
local_port = 22
remote_port = 6000
meta_id = 123
[[proxies]]
name = "ssh"
type = "tcp"
localPort = 22
remotePort = 6000
metadatas.id = "123"
```

View File

@@ -1,4 +1,4 @@
FROM golang:1.20 AS building
FROM golang:1.21 AS building
COPY . /building
WORKDIR /building

View File

@@ -1,4 +1,4 @@
FROM golang:1.20 AS building
FROM golang:1.21 AS building
COPY . /building
WORKDIR /building

89
go.mod
View File

@@ -4,71 +4,80 @@ go 1.20
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.4.0
github.com/coreos/go-oidc/v3 v3.6.0
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
github.com/go-playground/validator/v10 v10.11.0
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.8.3
github.com/onsi/gomega v1.27.0
github.com/pires/go-proxyproto v0.6.2
github.com/prometheus/client_golang v1.13.0
github.com/quic-go/quic-go v0.32.0
github.com/rodaine/table v1.0.1
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.8.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.3.0
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
github.com/onsi/ginkgo/v2 v2.11.0
github.com/onsi/gomega v1.27.8
github.com/pelletier/go-toml/v2 v2.1.0
github.com/pion/stun v0.6.1
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.16.0
github.com/quic-go/quic-go v0.37.4
github.com/rodaine/table v1.1.0
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.12.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
golang.org/x/time v0.3.0
gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
k8s.io/apimachinery v0.27.4
k8s.io/client-go v0.27.4
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/klauspost/reedsolomon v1.9.15 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.9.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
// 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

899
go.sum

File diff suppressed because it is too large Load Diff

63
hack/download.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/sh
OS="$(go env GOOS)"
ARCH="$(go env GOARCH)"
if [ "${TARGET_OS}" ]; then
OS="${TARGET_OS}"
fi
if [ "${TARGET_ARCH}" ]; then
ARCH="${TARGET_ARCH}"
fi
# Determine the latest version by version number ignoring alpha, beta, and rc versions.
if [ "${FRP_VERSION}" = "" ] ; then
FRP_VERSION="$(curl -sL https://github.com/fatedier/frp/releases | \
grep -o 'releases/tag/v[0-9]*.[0-9]*.[0-9]*"' | sort -V | \
tail -1 | awk -F'/' '{ print $3}')"
FRP_VERSION="${FRP_VERSION%?}"
FRP_VERSION="${FRP_VERSION#?}"
fi
if [ "${FRP_VERSION}" = "" ] ; then
printf "Unable to get latest frp version. Set FRP_VERSION env var and re-run. For example: export FRP_VERSION=1.0.0"
exit 1;
fi
SUFFIX=".tar.gz"
if [ "${OS}" = "windows" ] ; then
SUFFIX=".zip"
fi
NAME="frp_${FRP_VERSION}_${OS}_${ARCH}${SUFFIX}"
DIR_NAME="frp_${FRP_VERSION}_${OS}_${ARCH}"
URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${NAME}"
download_and_extract() {
printf "Downloading %s from %s ...\n" "$NAME" "${URL}"
if ! curl -o /dev/null -sIf "${URL}"; then
printf "\n%s is not found, please specify a valid FRP_VERSION\n" "${URL}"
exit 1
fi
curl -fsLO "${URL}"
filename=$NAME
if [ "${OS}" = "windows" ]; then
unzip "${filename}"
else
tar -xzf "${filename}"
fi
rm "${filename}"
if [ "${TARGET_DIRNAME}" ]; then
mv "${DIR_NAME}" "${TARGET_DIRNAME}"
DIR_NAME="${TARGET_DIRNAME}"
fi
}
download_and_extract
printf ""
printf "\nfrp %s Download Complete!\n" "$FRP_VERSION"
printf "\n"
printf "frp has been successfully downloaded into the %s folder on your system.\n" "$DIR_NAME"
printf "\n"

View File

@@ -1,20 +1,30 @@
#!/usr/bin/env bash
#!/bin/sh
ROOT=$(unset CDPATH && cd $(dirname "${BASH_SOURCE[0]}")/.. && pwd)
SCRIPT=$(readlink -f "$0")
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
which ginkgo &> /dev/null
if [ $? -ne 0 ]; then
ginkgo_command=$(which ginkgo 2>/dev/null)
if [ -z "$ginkgo_command" ]; then
echo "ginkgo not found, try to install..."
go install github.com/onsi/ginkgo/v2/ginkgo@v2.8.3
go install github.com/onsi/ginkgo/v2/ginkgo@v2.11.0
fi
debug=false
if [ x${DEBUG} == x"true" ]; then
if [ "x${DEBUG}" = "xtrue" ]; then
debug=true
fi
logLevel=debug
if [ x${LOG_LEVEL} != x"" ]; then
logLevel=${LOG_LEVEL}
if [ "${LOG_LEVEL}" ]; then
logLevel="${LOG_LEVEL}"
fi
ginkgo -nodes=8 --poll-progress-after=20s ${ROOT}/test/e2e -- -frpc-path=${ROOT}/bin/frpc -frps-path=${ROOT}/bin/frps -log-level=${logLevel} -debug=${debug}
frpcPath=${ROOT}/bin/frpc
if [ "${FRPC_PATH}" ]; then
frpcPath="${FRPC_PATH}"
fi
frpsPath=${ROOT}/bin/frps
if [ "${FRPS_PATH}" ]; then
frpsPath="${FRPS_PATH}"
fi
ginkgo -nodes=8 --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}

View File

@@ -47,6 +47,7 @@ for os in $os_all; do
fi
cp ../LICENSE ${frp_path}
cp -rf ../conf/* ${frp_path}
rm -rf ${frp_path}/legacy
# packages
cd ./packages

View File

@@ -17,76 +17,25 @@ package auth
import (
"fmt"
"github.com/fatedier/frp/pkg/consts"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
)
type BaseConfig struct {
// AuthenticationMethod specifies what authentication method to use to
// authenticate frpc with frps. If "token" is specified - token will be
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
// token will be issued using OIDC settings. By default, this value is "token".
AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
// AuthenticateHeartBeats specifies whether to include authentication token in
// heartbeats sent to frps. By default, this value is false.
AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
// AuthenticateNewWorkConns specifies whether to include authentication token in
// new work connections sent to frps. By default, this value is false.
AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
}
func getDefaultBaseConf() BaseConfig {
return BaseConfig{
AuthenticationMethod: "token",
AuthenticateHeartBeats: false,
AuthenticateNewWorkConns: false,
}
}
type ClientConfig struct {
BaseConfig `ini:",extends"`
OidcClientConfig `ini:",extends"`
TokenConfig `ini:",extends"`
}
func GetDefaultClientConf() ClientConfig {
return ClientConfig{
BaseConfig: getDefaultBaseConf(),
OidcClientConfig: getDefaultOidcClientConf(),
TokenConfig: getDefaultTokenConf(),
}
}
type ServerConfig struct {
BaseConfig `ini:",extends"`
OidcServerConfig `ini:",extends"`
TokenConfig `ini:",extends"`
}
func GetDefaultServerConf() ServerConfig {
return ServerConfig{
BaseConfig: getDefaultBaseConf(),
OidcServerConfig: getDefaultOidcServerConf(),
TokenConfig: getDefaultTokenConf(),
}
}
type Setter interface {
SetLogin(*msg.Login) error
SetPing(*msg.Ping) error
SetNewWorkConn(*msg.NewWorkConn) error
}
func NewAuthSetter(cfg ClientConfig) (authProvider Setter) {
switch cfg.AuthenticationMethod {
case consts.TokenAuthMethod:
authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
case consts.OidcAuthMethod:
authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig)
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
switch cfg.Method {
case v1.AuthMethodToken:
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC:
authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
default:
panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod))
panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
}
return authProvider
}
@@ -96,13 +45,12 @@ type Verifier interface {
VerifyNewWorkConn(*msg.NewWorkConn) error
}
func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) {
switch cfg.AuthenticationMethod {
case consts.TokenAuthMethod:
authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
case consts.OidcAuthMethod:
authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig)
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
switch cfg.Method {
case v1.AuthMethodToken:
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC:
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC)
}
return authVerifier
}

145
pkg/auth/legacy/legacy.go Normal file
View File

@@ -0,0 +1,145 @@
// Copyright 2023 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 legacy
type BaseConfig struct {
// AuthenticationMethod specifies what authentication method to use to
// authenticate frpc with frps. If "token" is specified - token will be
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
// token will be issued using OIDC settings. By default, this value is "token".
AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
// AuthenticateHeartBeats specifies whether to include authentication token in
// heartbeats sent to frps. By default, this value is false.
AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
// AuthenticateNewWorkConns specifies whether to include authentication token in
// new work connections sent to frps. By default, this value is false.
AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
}
func getDefaultBaseConf() BaseConfig {
return BaseConfig{
AuthenticationMethod: "token",
AuthenticateHeartBeats: false,
AuthenticateNewWorkConns: false,
}
}
type ClientConfig struct {
BaseConfig `ini:",extends"`
OidcClientConfig `ini:",extends"`
TokenConfig `ini:",extends"`
}
func GetDefaultClientConf() ClientConfig {
return ClientConfig{
BaseConfig: getDefaultBaseConf(),
OidcClientConfig: getDefaultOidcClientConf(),
TokenConfig: getDefaultTokenConf(),
}
}
type ServerConfig struct {
BaseConfig `ini:",extends"`
OidcServerConfig `ini:",extends"`
TokenConfig `ini:",extends"`
}
func GetDefaultServerConf() ServerConfig {
return ServerConfig{
BaseConfig: getDefaultBaseConf(),
OidcServerConfig: getDefaultOidcServerConf(),
TokenConfig: getDefaultTokenConf(),
}
}
type OidcClientConfig struct {
// OidcClientID specifies the client ID to use to get a token in OIDC
// authentication if AuthenticationMethod == "oidc". By default, this value
// is "".
OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
// OidcClientSecret specifies the client secret to use to get a token in OIDC
// authentication if AuthenticationMethod == "oidc". By default, this value
// is "".
OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
// OidcAudience specifies the audience of the token in OIDC authentication
// if AuthenticationMethod == "oidc". By default, this value is "".
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
// OidcScope specifies the scope of the token in OIDC authentication
// if AuthenticationMethod == "oidc". By default, this value is "".
OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
// By default, this value is "".
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
// OidcAdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator
// The field will be set by prefix "oidc_additional_"
OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
}
func getDefaultOidcClientConf() OidcClientConfig {
return OidcClientConfig{
OidcClientID: "",
OidcClientSecret: "",
OidcAudience: "",
OidcScope: "",
OidcTokenEndpointURL: "",
OidcAdditionalEndpointParams: make(map[string]string),
}
}
type OidcServerConfig struct {
// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
// will be used to load public keys to verify signature and will be compared
// with the issuer claim in the OIDC token. It will be used if
// AuthenticationMethod == "oidc". By default, this value is "".
OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
// OidcAudience specifies the audience OIDC tokens should contain when validated.
// If this value is empty, audience ("client ID") verification will be skipped.
// It will be used when AuthenticationMethod == "oidc". By default, this
// value is "".
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
// expired. It will be used when AuthenticationMethod == "oidc". By default, this
// value is false.
OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
// issuer claim matches the issuer specified in OidcIssuer. It will be used when
// AuthenticationMethod == "oidc". By default, this value is false.
OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
}
func getDefaultOidcServerConf() OidcServerConfig {
return OidcServerConfig{
OidcIssuer: "",
OidcAudience: "",
OidcSkipExpiryCheck: false,
OidcSkipIssuerCheck: false,
}
}
type TokenConfig 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 `ini:"token" json:"token"`
}
func getDefaultTokenConf() TokenConfig {
return TokenConfig{
Token: "",
}
}

View File

@@ -19,105 +19,40 @@ import (
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/samber/lo"
"golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
)
type OidcClientConfig struct {
// OidcClientID specifies the client ID to use to get a token in OIDC
// authentication if AuthenticationMethod == "oidc". By default, this value
// is "".
OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
// OidcClientSecret specifies the client secret to use to get a token in OIDC
// authentication if AuthenticationMethod == "oidc". By default, this value
// is "".
OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
// OidcAudience specifies the audience of the token in OIDC authentication
// if AuthenticationMethod == "oidc". By default, this value is "".
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
// OidcScope specifies the scope of the token in OIDC authentication
// if AuthenticationMethod == "oidc". By default, this value is "".
OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
// By default, this value is "".
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
// OidcAdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator
// The field will be set by prefix "oidc_additional_"
OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
}
func getDefaultOidcClientConf() OidcClientConfig {
return OidcClientConfig{
OidcClientID: "",
OidcClientSecret: "",
OidcAudience: "",
OidcScope: "",
OidcTokenEndpointURL: "",
OidcAdditionalEndpointParams: make(map[string]string),
}
}
type OidcServerConfig struct {
// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
// will be used to load public keys to verify signature and will be compared
// with the issuer claim in the OIDC token. It will be used if
// AuthenticationMethod == "oidc". By default, this value is "".
OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
// OidcAudience specifies the audience OIDC tokens should contain when validated.
// If this value is empty, audience ("client ID") verification will be skipped.
// It will be used when AuthenticationMethod == "oidc". By default, this
// value is "".
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
// expired. It will be used when AuthenticationMethod == "oidc". By default, this
// value is false.
OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
// issuer claim matches the issuer specified in OidcIssuer. It will be used when
// AuthenticationMethod == "oidc". By default, this value is false.
OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
}
func getDefaultOidcServerConf() OidcServerConfig {
return OidcServerConfig{
OidcIssuer: "",
OidcAudience: "",
OidcSkipExpiryCheck: false,
OidcSkipIssuerCheck: false,
}
}
type OidcAuthProvider struct {
BaseConfig
additionalAuthScopes []v1.AuthScope
tokenGenerator *clientcredentials.Config
}
func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider {
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
eps := make(map[string][]string)
for k, v := range cfg.OidcAdditionalEndpointParams {
for k, v := range cfg.AdditionalEndpointParams {
eps[k] = []string{v}
}
if cfg.OidcAudience != "" {
eps["audience"] = []string{cfg.OidcAudience}
if cfg.Audience != "" {
eps["audience"] = []string{cfg.Audience}
}
tokenGenerator := &clientcredentials.Config{
ClientID: cfg.OidcClientID,
ClientSecret: cfg.OidcClientSecret,
Scopes: []string{cfg.OidcScope},
TokenURL: cfg.OidcTokenEndpointURL,
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Scopes: []string{cfg.Scope},
TokenURL: cfg.TokenEndpointURL,
EndpointParams: eps,
}
return &OidcAuthProvider{
BaseConfig: baseCfg,
tokenGenerator: tokenGenerator,
additionalAuthScopes: additionalAuthScopes,
tokenGenerator: tokenGenerator,
}
}
@@ -135,7 +70,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
}
func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
if !auth.AuthenticateHeartBeats {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil
}
@@ -144,7 +79,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
}
func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
if !auth.AuthenticateNewWorkConns {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil
}
@@ -153,26 +88,26 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
}
type OidcAuthConsumer struct {
BaseConfig
additionalAuthScopes []v1.AuthScope
verifier *oidc.IDTokenVerifier
subjectFromLogin string
}
func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer {
provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer)
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer {
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
if err != nil {
panic(err)
}
verifierConf := oidc.Config{
ClientID: cfg.OidcAudience,
SkipClientIDCheck: cfg.OidcAudience == "",
SkipExpiryCheck: cfg.OidcSkipExpiryCheck,
SkipIssuerCheck: cfg.OidcSkipIssuerCheck,
ClientID: cfg.Audience,
SkipClientIDCheck: cfg.Audience == "",
SkipExpiryCheck: cfg.SkipExpiryCheck,
SkipIssuerCheck: cfg.SkipIssuerCheck,
}
return &OidcAuthConsumer{
BaseConfig: baseCfg,
verifier: provider.Verifier(&verifierConf),
additionalAuthScopes: additionalAuthScopes,
verifier: provider.Verifier(&verifierConf),
}
}
@@ -200,7 +135,7 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
}
func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
if !auth.AuthenticateHeartBeats {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil
}
@@ -208,7 +143,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
}
func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
if !auth.AuthenticateNewWorkConns {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil
}

View File

@@ -18,43 +18,32 @@ import (
"fmt"
"time"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util"
)
type TokenConfig 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 `ini:"token" json:"token"`
}
func getDefaultTokenConf() TokenConfig {
return TokenConfig{
Token: "",
}
}
type TokenAuthSetterVerifier struct {
BaseConfig
token string
additionalAuthScopes []v1.AuthScope
token string
}
func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier {
func NewTokenAuth(additionalAuthScopes []v1.AuthScope, token string) *TokenAuthSetterVerifier {
return &TokenAuthSetterVerifier{
BaseConfig: baseCfg,
token: cfg.Token,
additionalAuthScopes: additionalAuthScopes,
token: token,
}
}
func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) {
func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error {
loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp)
return nil
}
func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
if !auth.AuthenticateHeartBeats {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil
}
@@ -64,7 +53,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
}
func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
if !auth.AuthenticateNewWorkConns {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil
}
@@ -73,30 +62,30 @@ func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkC
return nil
}
func (auth *TokenAuthSetterVerifier) VerifyLogin(loginMsg *msg.Login) error {
if util.GetAuthKey(auth.token, loginMsg.Timestamp) != loginMsg.PrivilegeKey {
func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in login doesn't match token from configuration")
}
return nil
}
func (auth *TokenAuthSetterVerifier) VerifyPing(pingMsg *msg.Ping) error {
if !auth.AuthenticateHeartBeats {
func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil
}
if util.GetAuthKey(auth.token, pingMsg.Timestamp) != pingMsg.PrivilegeKey {
if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in heartbeat doesn't match token from configuration")
}
return nil
}
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
if !auth.AuthenticateNewWorkConns {
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil
}
if util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp) != newWorkConnMsg.PrivilegeKey {
if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in NewWorkConn doesn't match token from configuration")
}
return nil

View File

@@ -1,670 +0,0 @@
// Copyright 2020 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 config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/consts"
)
const (
testUser = "test"
)
var testClientBytesWithFull = []byte(`
# [common] is integral section
[common]
server_addr = 0.0.0.9
server_port = 7009
http_proxy = http://user:passwd@192.168.1.128:8080
log_file = ./frpc.log9
log_way = file
log_level = info9
log_max_days = 39
disable_log_color = false
authenticate_heartbeats = false
authenticate_new_work_conns = false
token = 12345678
oidc_client_id = client-id
oidc_client_secret = client-secret
oidc_audience = audience
oidc_token_endpoint_url = endpoint_url
admin_addr = 127.0.0.9
admin_port = 7409
admin_user = admin9
admin_pwd = admin9
assets_dir = ./static9
pool_count = 59
tcp_mux
user = your_name
login_fail_exit
protocol = tcp
tls_enable = true
tls_cert_file = client.crt
tls_key_file = client.key
tls_trusted_ca_file = ca.crt
tls_server_name = example.com
dns_server = 8.8.8.9
start = ssh,dns
heartbeat_interval = 39
heartbeat_timeout = 99
meta_var1 = 123
meta_var2 = 234
udp_packet_size = 1509
# all proxy
[ssh]
type = tcp
local_ip = 127.0.0.9
local_port = 29
bandwidth_limit = 19MB
bandwidth_limit_mode = server
use_encryption
use_compression
remote_port = 6009
group = test_group
group_key = 123456
health_check_type = tcp
health_check_timeout_s = 3
health_check_max_failed = 3
health_check_interval_s = 19
meta_var1 = 123
meta_var2 = 234
[ssh_random]
type = tcp
local_ip = 127.0.0.9
local_port = 29
remote_port = 9
[range:tcp_port]
type = tcp
local_ip = 127.0.0.9
local_port = 6010-6011,6019
remote_port = 6010-6011,6019
use_encryption = false
use_compression = false
[dns]
type = udp
local_ip = 114.114.114.114
local_port = 59
remote_port = 6009
use_encryption
use_compression
[range:udp_port]
type = udp
local_ip = 114.114.114.114
local_port = 6000,6010-6011
remote_port = 6000,6010-6011
use_encryption
use_compression
[web01]
type = http
local_ip = 127.0.0.9
local_port = 89
use_encryption
use_compression
http_user = admin
http_pwd = admin
subdomain = web01
custom_domains = web02.yourdomain.com
locations = /,/pic
host_header_rewrite = example.com
header_X-From-Where = frp
health_check_type = http
health_check_url = /status
health_check_interval_s = 19
health_check_max_failed = 3
health_check_timeout_s = 3
[web02]
type = https
local_ip = 127.0.0.9
local_port = 8009
use_encryption
use_compression
subdomain = web01
custom_domains = web02.yourdomain.com
proxy_protocol_version = v2
[secret_tcp]
type = stcp
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
[p2p_tcp]
type = xtcp
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
[tcpmuxhttpconnect]
type = tcpmux
multiplexer = httpconnect
local_ip = 127.0.0.1
local_port = 10701
custom_domains = tunnel1
[plugin_unix_domain_socket]
type = tcp
remote_port = 6003
plugin = unix_domain_socket
plugin_unix_path = /var/run/docker.sock
[plugin_http_proxy]
type = tcp
remote_port = 6004
plugin = http_proxy
plugin_http_user = abc
plugin_http_passwd = abc
[plugin_socks5]
type = tcp
remote_port = 6005
plugin = socks5
plugin_user = abc
plugin_passwd = abc
[plugin_static_file]
type = tcp
remote_port = 6006
plugin = static_file
plugin_local_path = /var/www/blog
plugin_strip_prefix = static
plugin_http_user = abc
plugin_http_passwd = abc
[plugin_https2http]
type = https
custom_domains = test.yourdomain.com
plugin = https2http
plugin_local_addr = 127.0.0.1:80
plugin_crt_path = ./server.crt
plugin_key_path = ./server.key
plugin_host_header_rewrite = 127.0.0.1
plugin_header_X-From-Where = frp
[plugin_http2https]
type = http
custom_domains = test.yourdomain.com
plugin = http2https
plugin_local_addr = 127.0.0.1:443
plugin_host_header_rewrite = 127.0.0.1
plugin_header_X-From-Where = frp
# visitor
[secret_tcp_visitor]
role = visitor
type = stcp
server_name = secret_tcp
sk = abcdefg
bind_addr = 127.0.0.1
bind_port = 9000
use_encryption = false
use_compression = false
[p2p_tcp_visitor]
role = visitor
type = xtcp
server_name = p2p_tcp
sk = abcdefg
bind_addr = 127.0.0.1
bind_port = 9001
use_encryption = false
use_compression = false
`)
func Test_LoadClientCommonConf(t *testing.T) {
assert := assert.New(t)
expected := ClientCommonConf{
ClientConfig: auth.ClientConfig{
BaseConfig: auth.BaseConfig{
AuthenticationMethod: "token",
AuthenticateHeartBeats: false,
AuthenticateNewWorkConns: false,
},
TokenConfig: auth.TokenConfig{
Token: "12345678",
},
OidcClientConfig: auth.OidcClientConfig{
OidcClientID: "client-id",
OidcClientSecret: "client-secret",
OidcAudience: "audience",
OidcTokenEndpointURL: "endpoint_url",
},
},
ServerAddr: "0.0.0.9",
ServerPort: 7009,
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: "http://user:passwd@192.168.1.128:8080",
LogFile: "./frpc.log9",
LogWay: "file",
LogLevel: "info9",
LogMaxDays: 39,
DisableLogColor: false,
AdminAddr: "127.0.0.9",
AdminPort: 7409,
AdminUser: "admin9",
AdminPwd: "admin9",
AssetsDir: "./static9",
PoolCount: 59,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
User: "your_name",
LoginFailExit: true,
Protocol: "tcp",
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
TLSEnable: true,
TLSCertFile: "client.crt",
TLSKeyFile: "client.key",
TLSTrustedCaFile: "ca.crt",
TLSServerName: "example.com",
DNSServer: "8.8.8.9",
Start: []string{"ssh", "dns"},
HeartbeatInterval: 39,
HeartbeatTimeout: 99,
Metas: map[string]string{
"var1": "123",
"var2": "234",
},
UDPPacketSize: 1509,
IncludeConfigFiles: []string{},
}
common, err := UnmarshalClientConfFromIni(testClientBytesWithFull)
assert.NoError(err)
assert.EqualValues(expected, common)
}
func Test_LoadClientBasicConf(t *testing.T) {
assert := assert.New(t)
proxyExpected := map[string]ProxyConf{
testUser + ".ssh": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".ssh",
ProxyType: consts.TCPProxy,
UseCompression: true,
UseEncryption: true,
Group: "test_group",
GroupKey: "123456",
BandwidthLimit: MustBandwidthQuantity("19MB"),
BandwidthLimitMode: BandwidthLimitModeServer,
Metas: map[string]string{
"var1": "123",
"var2": "234",
},
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 29,
},
HealthCheckConf: HealthCheckConf{
HealthCheckType: consts.TCPProxy,
HealthCheckTimeoutS: 3,
HealthCheckMaxFailed: 3,
HealthCheckIntervalS: 19,
HealthCheckAddr: "127.0.0.9:29",
},
},
RemotePort: 6009,
},
testUser + ".ssh_random": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".ssh_random",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 29,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 9,
},
testUser + ".tcp_port_0": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".tcp_port_0",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6010,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6010,
},
testUser + ".tcp_port_1": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".tcp_port_1",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6011,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6011,
},
testUser + ".tcp_port_2": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".tcp_port_2",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6019,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6019,
},
testUser + ".dns": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".dns",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 59,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6009,
},
testUser + ".udp_port_0": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".udp_port_0",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6000,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6000,
},
testUser + ".udp_port_1": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".udp_port_1",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6010,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6010,
},
testUser + ".udp_port_2": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".udp_port_2",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6011,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6011,
},
testUser + ".web01": &HTTPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".web01",
ProxyType: consts.HTTPProxy,
UseCompression: true,
UseEncryption: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 89,
},
HealthCheckConf: HealthCheckConf{
HealthCheckType: consts.HTTPProxy,
HealthCheckTimeoutS: 3,
HealthCheckMaxFailed: 3,
HealthCheckIntervalS: 19,
HealthCheckURL: "http://127.0.0.9:89/status",
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"},
SubDomain: "web01",
},
Locations: []string{"/", "/pic"},
HTTPUser: "admin",
HTTPPwd: "admin",
HostHeaderRewrite: "example.com",
Headers: map[string]string{
"X-From-Where": "frp",
},
},
testUser + ".web02": &HTTPSProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".web02",
ProxyType: consts.HTTPSProxy,
UseCompression: true,
UseEncryption: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 8009,
},
ProxyProtocolVersion: "v2",
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"},
SubDomain: "web01",
},
},
testUser + ".secret_tcp": &STCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".secret_tcp",
ProxyType: consts.STCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 22,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
Role: "server",
Sk: "abcdefg",
},
testUser + ".p2p_tcp": &XTCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".p2p_tcp",
ProxyType: consts.XTCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 22,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
Role: "server",
Sk: "abcdefg",
},
testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".tcpmuxhttpconnect",
ProxyType: consts.TCPMuxProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 10701,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"tunnel1"},
SubDomain: "",
},
Multiplexer: "httpconnect",
},
testUser + ".plugin_unix_domain_socket": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_unix_domain_socket",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "unix_domain_socket",
PluginParams: map[string]string{
"plugin_unix_path": "/var/run/docker.sock",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6003,
},
testUser + ".plugin_http_proxy": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_http_proxy",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "http_proxy",
PluginParams: map[string]string{
"plugin_http_user": "abc",
"plugin_http_passwd": "abc",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6004,
},
testUser + ".plugin_socks5": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_socks5",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "socks5",
PluginParams: map[string]string{
"plugin_user": "abc",
"plugin_passwd": "abc",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6005,
},
testUser + ".plugin_static_file": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_static_file",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "static_file",
PluginParams: map[string]string{
"plugin_local_path": "/var/www/blog",
"plugin_strip_prefix": "static",
"plugin_http_user": "abc",
"plugin_http_passwd": "abc",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6006,
},
testUser + ".plugin_https2http": &HTTPSProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_https2http",
ProxyType: consts.HTTPSProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "https2http",
PluginParams: map[string]string{
"plugin_local_addr": "127.0.0.1:80",
"plugin_crt_path": "./server.crt",
"plugin_key_path": "./server.key",
"plugin_host_header_rewrite": "127.0.0.1",
"plugin_header_X-From-Where": "frp",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"test.yourdomain.com"},
},
},
testUser + ".plugin_http2https": &HTTPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".plugin_http2https",
ProxyType: consts.HTTPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
Plugin: "http2https",
PluginParams: map[string]string{
"plugin_local_addr": "127.0.0.1:443",
"plugin_host_header_rewrite": "127.0.0.1",
"plugin_header_X-From-Where": "frp",
},
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"test.yourdomain.com"},
},
},
}
visitorExpected := map[string]VisitorConf{
testUser + ".secret_tcp_visitor": &STCPVisitorConf{
BaseVisitorConf: BaseVisitorConf{
ProxyName: testUser + ".secret_tcp_visitor",
ProxyType: consts.STCPProxy,
Role: "visitor",
Sk: "abcdefg",
ServerName: testVisitorPrefix + "secret_tcp",
BindAddr: "127.0.0.1",
BindPort: 9000,
},
},
testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{
BaseVisitorConf: BaseVisitorConf{
ProxyName: testUser + ".p2p_tcp_visitor",
ProxyType: consts.XTCPProxy,
Role: "visitor",
Sk: "abcdefg",
ServerName: testProxyPrefix + "p2p_tcp",
BindAddr: "127.0.0.1",
BindPort: 9001,
},
},
}
proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil)
assert.NoError(err)
assert.Equal(proxyExpected, proxyActual)
assert.Equal(visitorExpected, visitorActual)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 The frp Authors
// Copyright 2023 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.
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package legacy
import (
"fmt"
@@ -20,17 +20,19 @@ import (
"path/filepath"
"strings"
"github.com/samber/lo"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/auth"
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
"github.com/fatedier/frp/pkg/util/util"
)
// ClientCommonConf contains information for a client service. It is
// ClientCommonConf is the configuration parsed from ini.
// It contains information for a client service. It is
// recommended to use GetDefaultClientConf instead of creating this object
// directly, so that all unspecified fields have reasonable default values.
type ClientCommonConf struct {
auth.ClientConfig `ini:",extends"`
legacyauth.ClientConfig `ini:",extends"`
// ServerAddr specifies the address of the server to connect to. By
// default, this value is "0.0.0.0".
@@ -38,6 +40,8 @@ type ClientCommonConf struct {
// ServerPort specifies the port to connect to the server on. By default,
// this value is 7000.
ServerPort int `ini:"server_port" json:"server_port"`
// STUN server to help penetrate NAT hole.
NatHoleSTUNServer string `ini:"nat_hole_stun_server" json:"nat_hole_stun_server"`
// The maximum amount of time a dial to server will wait for a connect to complete.
DialServerTimeout int64 `ini:"dial_server_timeout" json:"dial_server_timeout"`
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
@@ -115,16 +119,17 @@ type ClientCommonConf struct {
Start []string `ini:"start" json:"start"`
// Start map[string]struct{} `json:"start"`
// Protocol specifies the protocol to use when interacting with the server.
// Valid values are "tcp", "kcp", "quic" and "websocket". By default, this value
// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
// is "tcp".
Protocol string `ini:"protocol" json:"protocol"`
// QUIC protocol options
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"`
// TLSEnable specifies whether or not TLS should be used when communicating
// with the server. If "tls_cert_file" and "tls_key_file" are valid,
// client will load the supplied tls configuration.
// Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
TLSEnable bool `ini:"tls_enable" json:"tls_enable"`
// TLSCertPath specifies the path of the cert file that client will
// load. It only works when "tls_enable" is true and "tls_key_file" is valid.
@@ -140,8 +145,9 @@ type ClientCommonConf struct {
// TLSServerName specifies the custom server name of tls certificate. By
// default, server name if same to ServerAddr.
TLSServerName string `ini:"tls_server_name" json:"tls_server_name"`
// By default, frpc will connect frps with first custom byte if tls is enabled.
// If DisableCustomTLSFirstByte is true, frpc will not send that custom byte.
// If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
// first custom byte when tls is enabled.
// Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
// HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By
@@ -163,82 +169,6 @@ type ClientCommonConf struct {
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
}
// GetDefaultClientConf returns a client configuration with default values.
func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{
ClientConfig: auth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0",
ServerPort: 7000,
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: os.Getenv("http_proxy"),
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
AdminAddr: "127.0.0.1",
PoolCount: 1,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
LoginFailExit: true,
Start: make([]string, 0),
Protocol: "tcp",
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
HeartbeatInterval: 30,
HeartbeatTimeout: 90,
Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
}
}
func (cfg *ClientCommonConf) Complete() {
if cfg.LogFile == "console" {
cfg.LogWay = "console"
} else {
cfg.LogWay = "file"
}
}
func (cfg *ClientCommonConf) Validate() error {
if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
}
}
if !cfg.TLSEnable {
if cfg.TLSCertFile != "" {
fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
}
if cfg.TLSKeyFile != "" {
fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false")
}
if cfg.TLSTrustedCaFile != "" {
fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false")
}
}
if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" && cfg.Protocol != "quic" {
return fmt.Errorf("invalid protocol")
}
for _, f := range cfg.IncludeConfigFiles {
absDir, err := filepath.Abs(filepath.Dir(f))
if err != nil {
return fmt.Errorf("include: parse directory of %s failed: %v", f, absDir)
}
if _, err := os.Stat(absDir); os.IsNotExist(err) {
return fmt.Errorf("include: directory of %s not exist", f)
}
}
return nil
}
// Supported sources including: string(file path), []byte, Reader interface.
func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
f, err := ini.LoadSources(ini.LoadOptions{
@@ -349,7 +279,7 @@ func LoadAllProxyConfsFromIni(
case "visitor":
newConf, newErr := NewVisitorConfFromIni(prefix, name, section)
if newErr != nil {
return nil, nil, newErr
return nil, nil, fmt.Errorf("failed to parse visitor %s, err: %v", name, newErr)
}
visitorConfs[prefix+name] = newConf
default:
@@ -413,3 +343,74 @@ func copySection(source, target *ini.Section) {
_, _ = target.NewKey(key, value)
}
}
// GetDefaultClientConf returns a client configuration with default values.
func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{
ClientConfig: legacyauth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0",
ServerPort: 7000,
NatHoleSTUNServer: "stun.easyvoip.com:3478",
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: os.Getenv("http_proxy"),
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
AdminAddr: "127.0.0.1",
PoolCount: 1,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
LoginFailExit: true,
Start: make([]string, 0),
Protocol: "tcp",
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
TLSEnable: true,
DisableCustomTLSFirstByte: true,
HeartbeatInterval: 30,
HeartbeatTimeout: 90,
Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
}
}
func (cfg *ClientCommonConf) Validate() error {
if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
}
}
if !cfg.TLSEnable {
if cfg.TLSCertFile != "" {
fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
}
if cfg.TLSKeyFile != "" {
fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false")
}
if cfg.TLSTrustedCaFile != "" {
fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false")
}
}
if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
return fmt.Errorf("invalid protocol")
}
for _, f := range cfg.IncludeConfigFiles {
absDir, err := filepath.Abs(filepath.Dir(f))
if err != nil {
return fmt.Errorf("include: parse directory of %s failed: %v", f, err)
}
if _, err := os.Stat(absDir); os.IsNotExist(err) {
return fmt.Errorf("include: directory of %s not exist", f)
}
}
return nil
}

View File

@@ -0,0 +1,350 @@
// Copyright 2023 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 legacy
import (
"strings"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
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.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
}
if conf.ClientConfig.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.ServerAddr = conf.ServerAddr
out.ServerPort = conf.ServerPort
out.NatHoleSTUNServer = conf.NatHoleSTUNServer
out.Transport.DialServerTimeout = conf.DialServerTimeout
out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive
out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP
out.Transport.ProxyURL = conf.HTTPProxy
out.Transport.PoolCount = conf.PoolCount
out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval
out.Transport.Protocol = conf.Protocol
out.Transport.HeartbeatInterval = conf.HeartbeatInterval
out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod
out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout
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.Log.To = conf.LogFile
out.Log.Level = conf.LogLevel
out.Log.MaxDays = conf.LogMaxDays
out.Log.DisablePrintColor = conf.DisableLogColor
out.WebServer.Addr = conf.AdminAddr
out.WebServer.Port = conf.AdminPort
out.WebServer.Password = conf.AdminPwd
out.WebServer.AssetsDir = conf.AssetsDir
out.WebServer.PprofEnable = conf.PprofEnable
out.DNSServer = conf.DNSServer
out.LoginFailExit = lo.ToPtr(conf.LoginFailExit)
out.Start = conf.Start
out.UDPPacketSize = conf.UDPPacketSize
out.Metadatas = conf.Metas
out.IncludeConfigFiles = conf.IncludeConfigFiles
return out
}
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.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
}
if conf.ServerConfig.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.BindAddr = conf.BindAddr
out.BindPort = conf.BindPort
out.KCPBindPort = conf.KCPBindPort
out.QUICBindPort = conf.QUICBindPort
out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod
out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
out.ProxyBindAddr = conf.ProxyBindAddr
out.VhostHTTPPort = conf.VhostHTTPPort
out.VhostHTTPSPort = conf.VhostHTTPSPort
out.TCPMuxHTTPConnectPort = conf.TCPMuxHTTPConnectPort
out.TCPMuxPassthrough = conf.TCPMuxPassthrough
out.VhostHTTPTimeout = conf.VhostHTTPTimeout
out.WebServer.Addr = conf.DashboardAddr
out.WebServer.Port = conf.DashboardPort
out.WebServer.User = conf.DashboardUser
out.WebServer.Password = conf.DashboardPwd
out.WebServer.AssetsDir = conf.AssetsDir
if conf.DashboardTLSMode {
out.WebServer.TLS = &v1.TLSConfig{}
out.WebServer.TLS.CertFile = conf.DashboardTLSCertFile
out.WebServer.TLS.KeyFile = conf.DashboardTLSKeyFile
out.WebServer.PprofEnable = conf.PprofEnable
}
out.EnablePrometheus = conf.EnablePrometheus
out.Log.To = conf.LogFile
out.Log.Level = conf.LogLevel
out.Log.MaxDays = conf.LogMaxDays
out.Log.DisablePrintColor = conf.DisableLogColor
out.DetailedErrorsToClient = lo.ToPtr(conf.DetailedErrorsToClient)
out.SubDomainHost = conf.SubDomainHost
out.Custom404Page = conf.Custom404Page
out.UserConnTimeout = conf.UserConnTimeout
out.UDPPacketSize = conf.UDPPacketSize
out.NatHoleAnalysisDataReserveHours = conf.NatHoleAnalysisDataReserveHours
out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval
out.Transport.TCPKeepAlive = conf.TCPKeepAlive
out.Transport.MaxPoolCount = conf.MaxPoolCount
out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
out.Transport.TLS.Force = conf.TLSOnly
out.Transport.TLS.CertFile = conf.TLSCertFile
out.Transport.TLS.KeyFile = conf.TLSKeyFile
out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
out.MaxPortsPerClient = conf.MaxPortsPerClient
for _, v := range conf.HTTPPlugins {
out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{
Name: v.Name,
Addr: v.Addr,
Path: v.Path,
Ops: v.Ops,
TLSVerify: v.TLSVerify,
})
}
out.AllowPorts, _ = types.NewPortsRangeSliceFromString(conf.AllowPortsStr)
return out
}
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
out := v1.HeaderOperations{}
for k, v := range params {
if !strings.HasPrefix(k, "plugin_header_") {
continue
}
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
out.Set[k] = v
}
}
return out
}
func Convert_ProxyConf_To_v1_Base(conf ProxyConf) *v1.ProxyBaseConfig {
out := &v1.ProxyBaseConfig{}
base := conf.GetBaseConfig()
out.Name = base.ProxyName
out.Type = base.ProxyType
out.Metadatas = base.Metas
out.Transport.UseEncryption = base.UseEncryption
out.Transport.UseCompression = base.UseCompression
out.Transport.BandwidthLimit = base.BandwidthLimit
out.Transport.BandwidthLimitMode = base.BandwidthLimitMode
out.Transport.ProxyProtocolVersion = base.ProxyProtocolVersion
out.LoadBalancer.Group = base.Group
out.LoadBalancer.GroupKey = base.GroupKey
out.HealthCheck.Type = base.HealthCheckType
out.HealthCheck.TimeoutSeconds = base.HealthCheckTimeoutS
out.HealthCheck.MaxFailed = base.HealthCheckMaxFailed
out.HealthCheck.IntervalSeconds = base.HealthCheckIntervalS
out.HealthCheck.Path = base.HealthCheckURL
out.LocalIP = base.LocalIP
out.LocalPort = base.LocalPort
switch base.Plugin {
case "http2https":
out.Plugin.ClientPluginOptions = &v1.HTTP2HTTPSPluginOptions{
LocalAddr: base.PluginParams["plugin_local_addr"],
HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
RequestHeaders: transformHeadersFromPluginParams(base.PluginParams),
}
case "http_proxy":
out.Plugin.ClientPluginOptions = &v1.HTTPProxyPluginOptions{
HTTPUser: base.PluginParams["plugin_http_user"],
HTTPPassword: base.PluginParams["plugin_http_passwd"],
}
case "https2http":
out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPPluginOptions{
LocalAddr: base.PluginParams["plugin_local_addr"],
HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
RequestHeaders: transformHeadersFromPluginParams(base.PluginParams),
CrtPath: base.PluginParams["plugin_crt_path"],
KeyPath: base.PluginParams["plugin_key_path"],
}
case "https2https":
out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPSPluginOptions{
LocalAddr: base.PluginParams["plugin_local_addr"],
HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
RequestHeaders: transformHeadersFromPluginParams(base.PluginParams),
CrtPath: base.PluginParams["plugin_crt_path"],
KeyPath: base.PluginParams["plugin_key_path"],
}
case "socks5":
out.Plugin.ClientPluginOptions = &v1.Socks5PluginOptions{
Username: base.PluginParams["plugin_user"],
Password: base.PluginParams["plugin_passwd"],
}
case "static_file":
out.Plugin.ClientPluginOptions = &v1.StaticFilePluginOptions{
LocalPath: base.PluginParams["plugin_local_path"],
StripPrefix: base.PluginParams["plugin_strip_prefix"],
HTTPUser: base.PluginParams["plugin_http_user"],
HTTPPassword: base.PluginParams["plugin_http_passwd"],
}
case "unix_domain_socket":
out.Plugin.ClientPluginOptions = &v1.UnixDomainSocketPluginOptions{
UnixPath: base.PluginParams["plugin_unix_path"],
}
}
out.Plugin.Type = base.Plugin
return out
}
func Convert_ProxyConf_To_v1(conf ProxyConf) v1.ProxyConfigurer {
outBase := Convert_ProxyConf_To_v1_Base(conf)
var out v1.ProxyConfigurer
switch v := conf.(type) {
case *TCPProxyConf:
c := &v1.TCPProxyConfig{ProxyBaseConfig: *outBase}
c.RemotePort = v.RemotePort
out = c
case *UDPProxyConf:
c := &v1.UDPProxyConfig{ProxyBaseConfig: *outBase}
c.RemotePort = v.RemotePort
out = c
case *HTTPProxyConf:
c := &v1.HTTPProxyConfig{ProxyBaseConfig: *outBase}
c.CustomDomains = v.CustomDomains
c.SubDomain = v.SubDomain
c.Locations = v.Locations
c.HTTPUser = v.HTTPUser
c.HTTPPassword = v.HTTPPwd
c.HostHeaderRewrite = v.HostHeaderRewrite
c.RequestHeaders.Set = v.Headers
c.RouteByHTTPUser = v.RouteByHTTPUser
out = c
case *HTTPSProxyConf:
c := &v1.HTTPSProxyConfig{ProxyBaseConfig: *outBase}
c.CustomDomains = v.CustomDomains
c.SubDomain = v.SubDomain
out = c
case *TCPMuxProxyConf:
c := &v1.TCPMuxProxyConfig{ProxyBaseConfig: *outBase}
c.CustomDomains = v.CustomDomains
c.SubDomain = v.SubDomain
c.HTTPUser = v.HTTPUser
c.HTTPPassword = v.HTTPPwd
c.RouteByHTTPUser = v.RouteByHTTPUser
c.Multiplexer = v.Multiplexer
out = c
case *STCPProxyConf:
c := &v1.STCPProxyConfig{ProxyBaseConfig: *outBase}
c.Secretkey = v.Sk
c.AllowUsers = v.AllowUsers
out = c
case *SUDPProxyConf:
c := &v1.SUDPProxyConfig{ProxyBaseConfig: *outBase}
c.Secretkey = v.Sk
c.AllowUsers = v.AllowUsers
out = c
case *XTCPProxyConf:
c := &v1.XTCPProxyConfig{ProxyBaseConfig: *outBase}
c.Secretkey = v.Sk
c.AllowUsers = v.AllowUsers
}
return out
}
func Convert_VisitorConf_To_v1_Base(conf VisitorConf) *v1.VisitorBaseConfig {
out := &v1.VisitorBaseConfig{}
base := conf.GetBaseConfig()
out.Name = base.ProxyName
out.Type = base.ProxyType
out.Transport.UseEncryption = base.UseEncryption
out.Transport.UseCompression = base.UseCompression
out.SecretKey = base.Sk
out.ServerUser = base.ServerUser
out.ServerName = base.ServerName
out.BindAddr = base.BindAddr
out.BindPort = base.BindPort
return out
}
func Convert_VisitorConf_To_v1(conf VisitorConf) v1.VisitorConfigurer {
outBase := Convert_VisitorConf_To_v1_Base(conf)
var out v1.VisitorConfigurer
switch v := conf.(type) {
case *STCPVisitorConf:
c := &v1.STCPVisitorConfig{VisitorBaseConfig: *outBase}
out = c
case *SUDPVisitorConf:
c := &v1.SUDPVisitorConfig{VisitorBaseConfig: *outBase}
out = c
case *XTCPVisitorConf:
c := &v1.XTCPVisitorConfig{VisitorBaseConfig: *outBase}
c.Protocol = v.Protocol
c.KeepTunnelOpen = v.KeepTunnelOpen
c.MaxRetriesAnHour = v.MaxRetriesAnHour
c.MinRetryInterval = v.MinRetryInterval
c.FallbackTo = v.FallbackTo
c.FallbackTimeoutMs = v.FallbackTimeoutMs
out = c
}
return out
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package legacy
import (
"bytes"
@@ -40,7 +40,6 @@ func ParseClientConfig(filePath string) (
if err != nil {
return
}
cfg.Complete()
if err = cfg.Validate(); err != nil {
err = fmt.Errorf("parse config error: %v", err)
return

387
pkg/config/legacy/proxy.go Normal file
View File

@@ -0,0 +1,387 @@
// Copyright 2023 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 legacy
import (
"fmt"
"reflect"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/config/types"
)
type ProxyType string
const (
ProxyTypeTCP ProxyType = "tcp"
ProxyTypeUDP ProxyType = "udp"
ProxyTypeTCPMUX ProxyType = "tcpmux"
ProxyTypeHTTP ProxyType = "http"
ProxyTypeHTTPS ProxyType = "https"
ProxyTypeSTCP ProxyType = "stcp"
ProxyTypeXTCP ProxyType = "xtcp"
ProxyTypeSUDP ProxyType = "sudp"
)
// Proxy
var (
proxyConfTypeMap = map[ProxyType]reflect.Type{
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
}
)
type ProxyConf interface {
// GetBaseConfig returns the BaseProxyConf for this config.
GetBaseConfig() *BaseProxyConf
// UnmarshalFromIni unmarshals a ini.Section into this config. This function
// will be called on the frpc side.
UnmarshalFromIni(string, string, *ini.Section) error
}
func NewConfByType(proxyType ProxyType) ProxyConf {
v, ok := proxyConfTypeMap[proxyType]
if !ok {
return nil
}
cfg := reflect.New(v).Interface().(ProxyConf)
return cfg
}
// Proxy Conf Loader
// DefaultProxyConf creates a empty ProxyConf object by proxyType.
// If proxyType doesn't exist, return nil.
func DefaultProxyConf(proxyType ProxyType) ProxyConf {
return NewConfByType(proxyType)
}
// Proxy loaded from ini
func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) {
// section.Key: if key not exists, section will set it with default value.
proxyType := ProxyType(section.Key("type").String())
if proxyType == "" {
proxyType = ProxyTypeTCP
}
conf := DefaultProxyConf(proxyType)
if conf == nil {
return nil, fmt.Errorf("invalid type [%s]", proxyType)
}
if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
return nil, err
}
return conf, nil
}
// LocalSvrConf configures what location the client will to, or what
// plugin will be used.
type LocalSvrConf struct {
// LocalIP specifies the IP address or host name to to.
LocalIP string `ini:"local_ip" json:"local_ip"`
// LocalPort specifies the port to to.
LocalPort int `ini:"local_port" json:"local_port"`
// Plugin specifies what plugin should be used for ng. If this value
// is set, the LocalIp and LocalPort values will be ignored. By default,
// this value is "".
Plugin string `ini:"plugin" json:"plugin"`
// PluginParams specify parameters to be passed to the plugin, if one is
// being used. By default, this value is an empty map.
PluginParams map[string]string `ini:"-"`
}
// HealthCheckConf configures health checking. This can be useful for load
// balancing purposes to detect and remove proxies to failing services.
type HealthCheckConf struct {
// HealthCheckType specifies what protocol to use for health checking.
// Valid values include "tcp", "http", and "". If this value is "", health
// checking will not be performed. By default, this value is "".
//
// If the type is "tcp", a connection will be attempted to the target
// server. If a connection cannot be established, the health check fails.
//
// If the type is "http", a GET request will be made to the endpoint
// specified by HealthCheckURL. If the response is not a 200, the health
// check fails.
HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http
// HealthCheckTimeoutS specifies the number of seconds to wait for a health
// check attempt to connect. If the timeout is reached, this counts as a
// health check failure. By default, this value is 3.
HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"`
// HealthCheckMaxFailed specifies the number of allowed failures before the
// is stopped. By default, this value is 1.
HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"`
// HealthCheckIntervalS specifies the time in seconds between health
// checks. By default, this value is 10.
HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"`
// HealthCheckURL specifies the address to send health checks to if the
// health check type is "http".
HealthCheckURL string `ini:"health_check_url" json:"health_check_url"`
// HealthCheckAddr specifies the address to connect to if the health check
// type is "tcp".
HealthCheckAddr string `ini:"-"`
}
// BaseProxyConf provides configuration info that is common to all types.
type BaseProxyConf struct {
// ProxyName is the name of this
ProxyName string `ini:"name" json:"name"`
// ProxyType specifies the type of this Valid values include "tcp",
// "udp", "http", "https", "stcp", and "xtcp". By default, this value is
// "tcp".
ProxyType string `ini:"type" json:"type"`
// UseEncryption controls whether or not communication with the server will
// be encrypted. Encryption is done using the tokens supplied in the server
// and client configuration. By default, this value is false.
UseEncryption bool `ini:"use_encryption" json:"use_encryption"`
// UseCompression controls whether or not communication with the server
// will be compressed. By default, this value is false.
UseCompression bool `ini:"use_compression" json:"use_compression"`
// Group specifies which group the is a part of. The server will use
// this information to load balance proxies in the same group. If the value
// is "", this will not be in a group. By default, this value is "".
Group string `ini:"group" json:"group"`
// GroupKey specifies a group key, which should be the same among proxies
// of the same group. By default, this value is "".
GroupKey string `ini:"group_key" json:"group_key"`
// ProxyProtocolVersion specifies which protocol version to use. Valid
// values include "v1", "v2", and "". If the value is "", a protocol
// version will be automatically selected. By default, this value is "".
ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"`
// BandwidthLimit limit the bandwidth
// 0 means no limit
BandwidthLimit types.BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"`
// BandwidthLimitMode specifies whether to limit the bandwidth on the
// client or server side. Valid values include "client" and "server".
// By default, this value is "client".
BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"`
// meta info for each proxy
Metas map[string]string `ini:"-" json:"metas"`
LocalSvrConf `ini:",extends"`
HealthCheckConf `ini:",extends"`
}
// Base
func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf {
return cfg
}
// BaseProxyConf apply custom logic changes.
func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section) error {
cfg.ProxyName = name
// metas_xxx
cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_")
// bandwidth_limit
if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil {
cfg.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidth.String())
if err != nil {
return err
}
}
// plugin_xxx
cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
return nil
}
type DomainConf struct {
CustomDomains []string `ini:"custom_domains" json:"custom_domains"`
SubDomain string `ini:"subdomain" json:"subdomain"`
}
type RoleServerCommonConf struct {
Role string `ini:"role" json:"role"`
Sk string `ini:"sk" json:"sk"`
AllowUsers []string `ini:"allow_users" json:"allow_users"`
}
// HTTP
type HTTPProxyConf struct {
BaseProxyConf `ini:",extends"`
DomainConf `ini:",extends"`
Locations []string `ini:"locations" json:"locations"`
HTTPUser string `ini:"http_user" json:"http_user"`
HTTPPwd string `ini:"http_pwd" json:"http_pwd"`
HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"`
Headers map[string]string `ini:"-" json:"headers"`
RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
}
func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_")
return nil
}
// HTTPS
type HTTPSProxyConf struct {
BaseProxyConf `ini:",extends"`
DomainConf `ini:",extends"`
}
func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
return nil
}
// TCP
type TCPProxyConf struct {
BaseProxyConf `ini:",extends"`
RemotePort int `ini:"remote_port" json:"remote_port"`
}
func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
return nil
}
// UDP
type UDPProxyConf struct {
BaseProxyConf `ini:",extends"`
RemotePort int `ini:"remote_port" json:"remote_port"`
}
func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
return nil
}
// TCPMux
type TCPMuxProxyConf struct {
BaseProxyConf `ini:",extends"`
DomainConf `ini:",extends"`
HTTPUser string `ini:"http_user" json:"http_user,omitempty"`
HTTPPwd string `ini:"http_pwd" json:"http_pwd,omitempty"`
RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
Multiplexer string `ini:"multiplexer"`
}
func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
return nil
}
// STCP
type STCPProxyConf struct {
BaseProxyConf `ini:",extends"`
RoleServerCommonConf `ini:",extends"`
}
func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
if cfg.Role == "" {
cfg.Role = "server"
}
return nil
}
// XTCP
type XTCPProxyConf struct {
BaseProxyConf `ini:",extends"`
RoleServerCommonConf `ini:",extends"`
}
func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
if cfg.Role == "" {
cfg.Role = "server"
}
return nil
}
// SUDP
type SUDPProxyConf struct {
BaseProxyConf `ini:",extends"`
RoleServerCommonConf `ini:",extends"`
}
func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
err := preUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return err
}
// Add custom logic unmarshal if exists
return nil
}
func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error {
err := section.MapTo(cfg)
if err != nil {
return err
}
err = cfg.GetBaseConfig().decorate(prefix, name, section)
if err != nil {
return err
}
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 The frp Authors
// Copyright 2023 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.
@@ -12,64 +12,64 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package legacy
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/auth"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/util/util"
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
)
type HTTPPluginOptions struct {
Name string `ini:"name"`
Addr string `ini:"addr"`
Path string `ini:"path"`
Ops []string `ini:"ops"`
TLSVerify bool `ini:"tls_verify"`
}
// ServerCommonConf contains information for a server service. It is
// recommended to use GetDefaultServerConf instead of creating this object
// directly, so that all unspecified fields have reasonable default values.
type ServerCommonConf struct {
auth.ServerConfig `ini:",extends"`
legacyauth.ServerConfig `ini:",extends"`
// BindAddr specifies the address that the server binds to. By default,
// this value is "0.0.0.0".
BindAddr string `ini:"bind_addr" json:"bind_addr"`
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"`
// BindUDPPort specifies the UDP port that the server listens on. If this
// value is 0, the server will not listen for UDP connections. By default,
// this value is 0
BindUDPPort int `ini:"bind_udp_port" json:"bind_udp_port" validate:"gte=0,lte=65535"`
BindPort int `ini:"bind_port" json:"bind_port"`
// KCPBindPort specifies the KCP port that the server listens on. If this
// value is 0, the server will not listen for KCP connections. By default,
// this value is 0.
KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"`
KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port"`
// QUICBindPort specifies the QUIC port that the server listens on.
// Set this value to 0 will disable this feature.
// By default, the value is 0.
QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port" validate:"gte=0,lte=65535"`
QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port"`
// QUIC protocol options
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"`
// ProxyBindAddr specifies the address that the proxy binds to. This value
// may be the same as BindAddr.
ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"`
// VhostHTTPPort specifies the port that the server listens for HTTP Vhost
// requests. If this value is 0, the server will not listen for HTTP
// requests. By default, this value is 0.
VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port" validate:"gte=0,lte=65535"`
VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"`
// VhostHTTPSPort specifies the port that the server listens for HTTPS
// Vhost requests. If this value is 0, the server will not listen for HTTPS
// requests. By default, this value is 0.
VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port" validate:"gte=0,lte=65535"`
VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port"`
// TCPMuxHTTPConnectPort specifies the port that the server listens for TCP
// HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
// requests on one single port. If it's not - it will listen on this value for
// HTTP CONNECT requests. By default, this value is 0.
TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"`
TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port"`
// If TCPMuxPassthrough is true, frps won't do any update on traffic.
TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"`
// VhostHTTPTimeout specifies the response header timeout for the Vhost
@@ -81,7 +81,7 @@ type ServerCommonConf struct {
// DashboardPort specifies the port that the dashboard listens on. If this
// value is 0, the dashboard will not be started. By default, this value is
// 0.
DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"`
DashboardPort int `ini:"dashboard_port" json:"dashboard_port"`
// DashboardTLSCertFile specifies the path of the cert file that the server will
// load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this
// supplied tls configuration.
@@ -189,42 +189,45 @@ type ServerCommonConf struct {
// connection. By default, this value is 10.
UserConnTimeout int64 `ini:"user_conn_timeout" json:"user_conn_timeout"`
// HTTPPlugins specify the server plugins support HTTP protocol.
HTTPPlugins map[string]plugin.HTTPPluginOptions `ini:"-" json:"http_plugins"`
HTTPPlugins map[string]HTTPPluginOptions `ini:"-" json:"http_plugins"`
// UDPPacketSize specifies the UDP packet size
// By default, this value is 1500
UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
// Enable golang pprof handlers in dashboard listener.
// Dashboard port must be set first.
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
// NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data.
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
}
// GetDefaultServerConf returns a server configuration with reasonable
// defaults.
func GetDefaultServerConf() ServerCommonConf {
return ServerCommonConf{
ServerConfig: auth.GetDefaultServerConf(),
BindAddr: "0.0.0.0",
BindPort: 7000,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0",
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
DetailedErrorsToClient: true,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
MaxPortsPerClient: 0,
HeartbeatTimeout: 90,
UserConnTimeout: 10,
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
ServerConfig: legacyauth.GetDefaultServerConf(),
BindAddr: "0.0.0.0",
BindPort: 7000,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0",
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
DetailedErrorsToClient: true,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
MaxPortsPerClient: 0,
HeartbeatTimeout: 90,
UserConnTimeout: 10,
HTTPPlugins: make(map[string]HTTPPluginOptions),
UDPPacketSize: 1500,
NatHoleAnalysisDataReserveHours: 7 * 24,
}
}
@@ -254,18 +257,11 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
// allow_ports
allowPortStr := s.Key("allow_ports").String()
if allowPortStr != "" {
allowPorts, err := util.ParseRangeNumbers(allowPortStr)
if err != nil {
return ServerCommonConf{}, fmt.Errorf("invalid allow_ports: %v", err)
}
for _, port := range allowPorts {
common.AllowPorts[int(port)] = struct{}{}
}
common.AllowPortsStr = allowPortStr
}
// plugin.xxx
pluginOpts := make(map[string]plugin.HTTPPluginOptions)
pluginOpts := make(map[string]HTTPPluginOptions)
for _, section := range f.Sections() {
name := section.Name()
if !strings.HasPrefix(name, "plugin.") {
@@ -284,47 +280,10 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
return common, nil
}
func (cfg *ServerCommonConf) Complete() {
if cfg.LogFile == "console" {
cfg.LogWay = "console"
} else {
cfg.LogWay = "file"
}
if cfg.ProxyBindAddr == "" {
cfg.ProxyBindAddr = cfg.BindAddr
}
if cfg.TLSTrustedCaFile != "" {
cfg.TLSOnly = true
}
}
func (cfg *ServerCommonConf) Validate() error {
if !cfg.DashboardTLSMode {
if cfg.DashboardTLSCertFile != "" {
fmt.Println("WARNING! dashboard_tls_cert_file is invalid when dashboard_tls_mode is false")
}
if cfg.DashboardTLSKeyFile != "" {
fmt.Println("WARNING! dashboard_tls_key_file is invalid when dashboard_tls_mode is false")
}
} else {
if cfg.DashboardTLSCertFile == "" {
return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
}
if cfg.DashboardTLSKeyFile == "" {
return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
}
}
return validator.New().Struct(cfg)
}
func loadHTTPPluginOpt(section *ini.Section) (*plugin.HTTPPluginOptions, error) {
func loadHTTPPluginOpt(section *ini.Section) (*HTTPPluginOptions, error) {
name := strings.TrimSpace(strings.TrimPrefix(section.Name(), "plugin."))
opt := new(plugin.HTTPPluginOptions)
opt := &HTTPPluginOptions{}
err := section.MapTo(opt)
if err != nil {
return nil, err

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package legacy
import (
"strings"

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package legacy
import (
"bytes"

View File

@@ -0,0 +1,185 @@
// Copyright 2023 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 legacy
import (
"fmt"
"reflect"
"gopkg.in/ini.v1"
)
type VisitorType string
const (
VisitorTypeSTCP VisitorType = "stcp"
VisitorTypeXTCP VisitorType = "xtcp"
VisitorTypeSUDP VisitorType = "sudp"
)
// Visitor
var (
visitorConfTypeMap = map[VisitorType]reflect.Type{
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
}
)
type VisitorConf interface {
// GetBaseConfig returns the base config of visitor.
GetBaseConfig() *BaseVisitorConf
// UnmarshalFromIni unmarshals config from ini.
UnmarshalFromIni(prefix string, name string, section *ini.Section) error
}
// DefaultVisitorConf creates a empty VisitorConf object by visitorType.
// If visitorType doesn't exist, return nil.
func DefaultVisitorConf(visitorType VisitorType) VisitorConf {
v, ok := visitorConfTypeMap[visitorType]
if !ok {
return nil
}
return reflect.New(v).Interface().(VisitorConf)
}
type BaseVisitorConf struct {
ProxyName string `ini:"name" json:"name"`
ProxyType string `ini:"type" json:"type"`
UseEncryption bool `ini:"use_encryption" json:"use_encryption"`
UseCompression bool `ini:"use_compression" json:"use_compression"`
Role string `ini:"role" json:"role"`
Sk string `ini:"sk" json:"sk"`
// if the server user is not set, it defaults to the current user
ServerUser string `ini:"server_user" json:"server_user"`
ServerName string `ini:"server_name" json:"server_name"`
BindAddr string `ini:"bind_addr" json:"bind_addr"`
// BindPort is the port that visitor listens on.
// It can be less than 0, it means don't bind to the port and only receive connections redirected from
// other visitors. (This is not supported for SUDP now)
BindPort int `ini:"bind_port" json:"bind_port"`
}
// Base
func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf {
return cfg
}
func (cfg *BaseVisitorConf) unmarshalFromIni(_ string, name string, _ *ini.Section) error {
// Custom decoration after basic unmarshal:
cfg.ProxyName = name
// bind_addr
if cfg.BindAddr == "" {
cfg.BindAddr = "127.0.0.1"
}
return nil
}
func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, section *ini.Section) error {
err := section.MapTo(cfg)
if err != nil {
return err
}
err = cfg.GetBaseConfig().unmarshalFromIni(prefix, name, section)
if err != nil {
return err
}
return nil
}
type SUDPVisitorConf struct {
BaseVisitorConf `ini:",extends"`
}
func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return
}
// Add custom logic unmarshal, if exists
return
}
type STCPVisitorConf struct {
BaseVisitorConf `ini:",extends"`
}
func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return
}
// Add custom logic unmarshal, if exists
return
}
type XTCPVisitorConf struct {
BaseVisitorConf `ini:",extends"`
Protocol string `ini:"protocol" json:"protocol,omitempty"`
KeepTunnelOpen bool `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"`
MaxRetriesAnHour int `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"`
MinRetryInterval int `ini:"min_retry_interval" json:"min_retry_interval,omitempty"`
FallbackTo string `ini:"fallback_to" json:"fallback_to,omitempty"`
FallbackTimeoutMs int `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"`
}
func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil {
return
}
// Add custom logic unmarshal, if exists
if cfg.Protocol == "" {
cfg.Protocol = "quic"
}
if cfg.MaxRetriesAnHour <= 0 {
cfg.MaxRetriesAnHour = 8
}
if cfg.MinRetryInterval <= 0 {
cfg.MinRetryInterval = 90
}
if cfg.FallbackTimeoutMs <= 0 {
cfg.FallbackTimeoutMs = 1000
}
return
}
// Visitor loaded from ini
func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) {
// section.Key: if key not exists, section will set it with default value.
visitorType := VisitorType(section.Key("type").String())
if visitorType == "" {
return nil, fmt.Errorf("type shouldn't be empty")
}
conf := DefaultVisitorConf(visitorType)
if conf == nil {
return nil, fmt.Errorf("type [%s] error", visitorType)
}
if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
return nil, fmt.Errorf("type [%s] error", visitorType)
}
return conf, nil
}

281
pkg/config/load.go Normal file
View File

@@ -0,0 +1,281 @@
// Copyright 2023 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 config
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"os"
"path/filepath"
"strings"
toml "github.com/pelletier/go-toml/v2"
"github.com/samber/lo"
"gopkg.in/ini.v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/yaml"
"github.com/fatedier/frp/pkg/config/legacy"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util"
)
var glbEnvs map[string]string
func init() {
glbEnvs = make(map[string]string)
envs := os.Environ()
for _, env := range envs {
pair := strings.SplitN(env, "=", 2)
if len(pair) != 2 {
continue
}
glbEnvs[pair[0]] = pair[1]
}
}
type Values struct {
Envs map[string]string // environment vars
}
func GetValues() *Values {
return &Values{
Envs: glbEnvs,
}
}
func DetectLegacyINIFormat(content []byte) bool {
f, err := ini.Load(content)
if err != nil {
return false
}
if _, err := f.GetSection("common"); err == nil {
return true
}
return false
}
func DetectLegacyINIFormatFromFile(path string) bool {
b, err := os.ReadFile(path)
if err != nil {
return false
}
return DetectLegacyINIFormat(b)
}
func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
tmpl, err := template.New("frp").Parse(string(in))
if err != nil {
return nil, err
}
buffer := bytes.NewBufferString("")
if err := tmpl.Execute(buffer, values); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return RenderWithTemplate(b, values)
}
func LoadConfigureFromFile(path string, c any) error {
content, err := LoadFileContentWithTemplate(path, GetValues())
if err != nil {
return err
}
return LoadConfigure(content, c)
}
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
func LoadConfigure(b []byte, c any) error {
var tomlObj interface{}
if err := toml.Unmarshal(b, &tomlObj); err == nil {
b, err = json.Marshal(&tomlObj)
if err != nil {
return err
}
}
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
return decoder.Decode(c)
}
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
configurer := v1.NewProxyConfigurerByType(v1.ProxyType(m.ProxyType))
if configurer == nil {
return nil, fmt.Errorf("unknown proxy type: %s", m.ProxyType)
}
configurer.UnmarshalFromMsg(m)
configurer.Complete("")
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
return nil, err
}
return configurer, nil
}
func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
var (
svrCfg *v1.ServerConfig
isLegacyFormat bool
)
// detect legacy ini format
if DetectLegacyINIFormatFromFile(path) {
content, err := legacy.GetRenderedConfFromFile(path)
if err != nil {
return nil, true, err
}
legacyCfg, err := legacy.UnmarshalServerConfFromIni(content)
if err != nil {
return nil, true, err
}
svrCfg = legacy.Convert_ServerCommonConf_To_v1(&legacyCfg)
isLegacyFormat = true
} else {
svrCfg = &v1.ServerConfig{}
if err := LoadConfigureFromFile(path, svrCfg); err != nil {
return nil, false, err
}
}
if svrCfg != nil {
svrCfg.Complete()
}
return svrCfg, isLegacyFormat, nil
}
func LoadClientConfig(path string) (
*v1.ClientCommonConfig,
[]v1.ProxyConfigurer,
[]v1.VisitorConfigurer,
bool, error,
) {
var (
cliCfg *v1.ClientCommonConfig
pxyCfgs = make([]v1.ProxyConfigurer, 0)
visitorCfgs = make([]v1.VisitorConfigurer, 0)
isLegacyFormat bool
)
if DetectLegacyINIFormatFromFile(path) {
legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
if err != nil {
return nil, nil, nil, true, err
}
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
for _, c := range legacyPxyCfgs {
pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
}
for _, c := range legacyVisitorCfgs {
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
}
isLegacyFormat = true
} else {
allCfg := v1.ClientConfig{}
if err := LoadConfigureFromFile(path, &allCfg); err != nil {
return nil, nil, nil, false, err
}
cliCfg = &allCfg.ClientCommonConfig
for _, c := range allCfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
}
for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
}
}
// Load additional config from includes.
// legacy ini format alredy handle this in ParseClientConfig.
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat)
if err != nil {
return nil, nil, nil, isLegacyFormat, err
}
pxyCfgs = append(pxyCfgs, extPxyCfgs...)
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
}
// Filter by start
if len(cliCfg.Start) > 0 {
startSet := sets.New(cliCfg.Start...)
pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
return startSet.Has(c.GetBaseConfig().Name)
})
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
return startSet.Has(c.GetBaseConfig().Name)
})
}
if cliCfg != nil {
cliCfg.Complete()
}
for _, c := range pxyCfgs {
c.Complete(cliCfg.User)
}
for _, c := range visitorCfgs {
c.Complete(cliCfg)
}
return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil
}
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
pxyCfgs := make([]v1.ProxyConfigurer, 0)
visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, path := range paths {
absDir, err := filepath.Abs(filepath.Dir(path))
if err != nil {
return nil, nil, err
}
if _, err := os.Stat(absDir); os.IsNotExist(err) {
return nil, nil, err
}
files, err := os.ReadDir(absDir)
if err != nil {
return nil, nil, err
}
for _, fi := range files {
if fi.IsDir() {
continue
}
absFile := filepath.Join(absDir, fi.Name())
if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
// support yaml/json/toml
cfg := v1.ClientConfig{}
if err := LoadConfigureFromFile(absFile, &cfg); err != nil {
return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
}
for _, c := range cfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
}
for _, c := range cfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
}
}
}
}
return pxyCfgs, visitorCfgs, nil
}

45
pkg/config/load_test.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2023 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 config
import (
"testing"
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
func TestLoadConfigure(t *testing.T) {
require := require.New(t)
content := `
bindAddr = "127.0.0.1"
kcpBindPort = 7000
quicBindPort = 7001
tcpmuxHTTPConnectPort = 7005
custom404Page = "/abc.html"
transport.tcpKeepalive = 10
`
svrCfg := v1.ServerConfig{}
err := LoadConfigure([]byte(content), &svrCfg)
require.NoError(err)
require.EqualValues("127.0.0.1", svrCfg.BindAddr)
require.EqualValues(7000, svrCfg.KCPBindPort)
require.EqualValues(7001, svrCfg.QUICBindPort)
require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
require.EqualValues("/abc.html", svrCfg.Custom404Page)
require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,474 +0,0 @@
// Copyright 2020 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 config
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/consts"
)
var (
testLoadOptions = ini.LoadOptions{
Insensitive: false,
InsensitiveSections: false,
InsensitiveKeys: false,
IgnoreInlineComment: true,
AllowBooleanKeys: true,
}
testProxyPrefix = "test."
)
func Test_Proxy_Interface(t *testing.T) {
for name := range proxyConfTypeMap {
NewConfByType(name)
}
}
func Test_Proxy_UnmarshalFromIni(t *testing.T) {
assert := assert.New(t)
testcases := []struct {
sname string
source []byte
expected ProxyConf
}{
{
sname: "ssh",
source: []byte(`
[ssh]
# tcp | udp | http | https | stcp | xtcp, default is tcp
type = tcp
local_ip = 127.0.0.9
local_port = 29
bandwidth_limit = 19MB
bandwidth_limit_mode = server
use_encryption
use_compression
remote_port = 6009
group = test_group
group_key = 123456
health_check_type = tcp
health_check_timeout_s = 3
health_check_max_failed = 3
health_check_interval_s = 19
meta_var1 = 123
meta_var2 = 234`),
expected: &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "ssh",
ProxyType: consts.TCPProxy,
UseCompression: true,
UseEncryption: true,
Group: "test_group",
GroupKey: "123456",
BandwidthLimit: MustBandwidthQuantity("19MB"),
BandwidthLimitMode: BandwidthLimitModeServer,
Metas: map[string]string{
"var1": "123",
"var2": "234",
},
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 29,
},
HealthCheckConf: HealthCheckConf{
HealthCheckType: consts.TCPProxy,
HealthCheckTimeoutS: 3,
HealthCheckMaxFailed: 3,
HealthCheckIntervalS: 19,
HealthCheckAddr: "127.0.0.9:29",
},
},
RemotePort: 6009,
},
},
{
sname: "ssh_random",
source: []byte(`
[ssh_random]
type = tcp
local_ip = 127.0.0.9
local_port = 29
remote_port = 9
`),
expected: &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "ssh_random",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 29,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 9,
},
},
{
sname: "dns",
source: []byte(`
[dns]
type = udp
local_ip = 114.114.114.114
local_port = 59
remote_port = 6009
use_encryption
use_compression
`),
expected: &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "dns",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 59,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6009,
},
},
{
sname: "web01",
source: []byte(`
[web01]
type = http
local_ip = 127.0.0.9
local_port = 89
use_encryption
use_compression
http_user = admin
http_pwd = admin
subdomain = web01
custom_domains = web02.yourdomain.com
locations = /,/pic
host_header_rewrite = example.com
header_X-From-Where = frp
health_check_type = http
health_check_url = /status
health_check_interval_s = 19
health_check_max_failed = 3
health_check_timeout_s = 3
`),
expected: &HTTPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "web01",
ProxyType: consts.HTTPProxy,
UseCompression: true,
UseEncryption: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 89,
},
HealthCheckConf: HealthCheckConf{
HealthCheckType: consts.HTTPProxy,
HealthCheckTimeoutS: 3,
HealthCheckMaxFailed: 3,
HealthCheckIntervalS: 19,
HealthCheckURL: "http://127.0.0.9:89/status",
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"},
SubDomain: "web01",
},
Locations: []string{"/", "/pic"},
HTTPUser: "admin",
HTTPPwd: "admin",
HostHeaderRewrite: "example.com",
Headers: map[string]string{
"X-From-Where": "frp",
},
},
},
{
sname: "web02",
source: []byte(`
[web02]
type = https
local_ip = 127.0.0.9
local_port = 8009
use_encryption
use_compression
subdomain = web01
custom_domains = web02.yourdomain.com
proxy_protocol_version = v2
`),
expected: &HTTPSProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "web02",
ProxyType: consts.HTTPSProxy,
UseCompression: true,
UseEncryption: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 8009,
},
ProxyProtocolVersion: "v2",
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"},
SubDomain: "web01",
},
},
},
{
sname: "secret_tcp",
source: []byte(`
[secret_tcp]
type = stcp
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
`),
expected: &STCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "secret_tcp",
ProxyType: consts.STCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 22,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
Role: "server",
Sk: "abcdefg",
},
},
{
sname: "p2p_tcp",
source: []byte(`
[p2p_tcp]
type = xtcp
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22
use_encryption = false
use_compression = false
`),
expected: &XTCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "p2p_tcp",
ProxyType: consts.XTCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 22,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
Role: "server",
Sk: "abcdefg",
},
},
{
sname: "tcpmuxhttpconnect",
source: []byte(`
[tcpmuxhttpconnect]
type = tcpmux
multiplexer = httpconnect
local_ip = 127.0.0.1
local_port = 10701
custom_domains = tunnel1
`),
expected: &TCPMuxProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "tcpmuxhttpconnect",
ProxyType: consts.TCPMuxProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.1",
LocalPort: 10701,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
DomainConf: DomainConf{
CustomDomains: []string{"tunnel1"},
SubDomain: "",
},
Multiplexer: "httpconnect",
},
},
}
for _, c := range testcases {
f, err := ini.LoadSources(testLoadOptions, c.source)
assert.NoError(err)
proxyType := f.Section(c.sname).Key("type").String()
assert.NotEmpty(proxyType)
actual := DefaultProxyConf(proxyType)
assert.NotNil(actual)
err = actual.UnmarshalFromIni(testProxyPrefix, c.sname, f.Section(c.sname))
assert.NoError(err)
assert.Equal(c.expected, actual)
}
}
func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
assert := assert.New(t)
testcases := []struct {
sname string
source []byte
expected map[string]ProxyConf
}{
{
sname: "range:tcp_port",
source: []byte(`
[range:tcp_port]
type = tcp
local_ip = 127.0.0.9
local_port = 6010-6011,6019
remote_port = 6010-6011,6019
use_encryption = false
use_compression = false
`),
expected: map[string]ProxyConf{
"tcp_port_0": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "tcp_port_0",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6010,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6010,
},
"tcp_port_1": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "tcp_port_1",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6011,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6011,
},
"tcp_port_2": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "tcp_port_2",
ProxyType: consts.TCPProxy,
LocalSvrConf: LocalSvrConf{
LocalIP: "127.0.0.9",
LocalPort: 6019,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6019,
},
},
},
{
sname: "range:udp_port",
source: []byte(`
[range:udp_port]
type = udp
local_ip = 114.114.114.114
local_port = 6000,6010-6011
remote_port = 6000,6010-6011
use_encryption
use_compression
`),
expected: map[string]ProxyConf{
"udp_port_0": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "udp_port_0",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6000,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6000,
},
"udp_port_1": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "udp_port_1",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6010,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6010,
},
"udp_port_2": &UDPProxyConf{
BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "udp_port_2",
ProxyType: consts.UDPProxy,
UseEncryption: true,
UseCompression: true,
LocalSvrConf: LocalSvrConf{
LocalIP: "114.114.114.114",
LocalPort: 6011,
},
BandwidthLimitMode: BandwidthLimitModeClient,
},
RemotePort: 6011,
},
},
},
}
for _, c := range testcases {
f, err := ini.LoadSources(testLoadOptions, c.source)
assert.NoError(err)
actual := make(map[string]ProxyConf)
s := f.Section(c.sname)
err = renderRangeProxyTemplates(f, s)
assert.NoError(err)
f.DeleteSection(ini.DefaultSection)
f.DeleteSection(c.sname)
for _, section := range f.Sections() {
proxyType := section.Key("type").String()
newsname := section.Name()
tmp := DefaultProxyConf(proxyType)
err = tmp.UnmarshalFromIni(testProxyPrefix, newsname, section)
assert.NoError(err)
actual[newsname] = tmp
}
assert.Equal(c.expected, actual)
}
}

View File

@@ -1,219 +0,0 @@
// Copyright 2020 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 config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/fatedier/frp/pkg/auth"
plugin "github.com/fatedier/frp/pkg/plugin/server"
)
func Test_LoadServerCommonConf(t *testing.T) {
assert := assert.New(t)
testcases := []struct {
source []byte
expected ServerCommonConf
}{
{
source: []byte(`
# [common] is integral section
[common]
bind_addr = 0.0.0.9
bind_port = 7009
bind_udp_port = 7008
kcp_bind_port = 7007
proxy_bind_addr = 127.0.0.9
vhost_http_port = 89
vhost_https_port = 449
vhost_http_timeout = 69
tcpmux_httpconnect_port = 1339
dashboard_addr = 0.0.0.9
dashboard_port = 7509
dashboard_user = admin9
dashboard_pwd = admin9
enable_prometheus
assets_dir = ./static9
log_file = ./frps.log9
log_way = file
log_level = info9
log_max_days = 39
disable_log_color = false
detailed_errors_to_client
authentication_method = token
authenticate_heartbeats = false
authenticate_new_work_conns = false
token = 123456789
oidc_issuer = test9
oidc_audience = test9
oidc_skip_expiry_check
oidc_skip_issuer_check
heartbeat_timeout = 99
user_conn_timeout = 9
allow_ports = 10-12,99
max_pool_count = 59
max_ports_per_client = 9
tls_only = false
tls_cert_file = server.crt
tls_key_file = server.key
tls_trusted_ca_file = ca.crt
subdomain_host = frps.com
tcp_mux
udp_packet_size = 1509
[plugin.user-manager]
addr = 127.0.0.1:9009
path = /handler
ops = Login
[plugin.port-manager]
addr = 127.0.0.1:9009
path = /handler
ops = NewProxy
tls_verify
`),
expected: ServerCommonConf{
ServerConfig: auth.ServerConfig{
BaseConfig: auth.BaseConfig{
AuthenticationMethod: "token",
AuthenticateHeartBeats: false,
AuthenticateNewWorkConns: false,
},
TokenConfig: auth.TokenConfig{
Token: "123456789",
},
OidcServerConfig: auth.OidcServerConfig{
OidcIssuer: "test9",
OidcAudience: "test9",
OidcSkipExpiryCheck: true,
OidcSkipIssuerCheck: true,
},
},
BindAddr: "0.0.0.9",
BindPort: 7009,
BindUDPPort: 7008,
KCPBindPort: 7007,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
ProxyBindAddr: "127.0.0.9",
VhostHTTPPort: 89,
VhostHTTPSPort: 449,
VhostHTTPTimeout: 69,
TCPMuxHTTPConnectPort: 1339,
DashboardAddr: "0.0.0.9",
DashboardPort: 7509,
DashboardUser: "admin9",
DashboardPwd: "admin9",
EnablePrometheus: true,
AssetsDir: "./static9",
LogFile: "./frps.log9",
LogWay: "file",
LogLevel: "info9",
LogMaxDays: 39,
DisableLogColor: false,
DetailedErrorsToClient: true,
HeartbeatTimeout: 99,
UserConnTimeout: 9,
AllowPorts: map[int]struct{}{
10: {},
11: {},
12: {},
99: {},
},
AllowPortsStr: "10-12,99",
MaxPoolCount: 59,
MaxPortsPerClient: 9,
TLSOnly: true,
TLSCertFile: "server.crt",
TLSKeyFile: "server.key",
TLSTrustedCaFile: "ca.crt",
SubDomainHost: "frps.com",
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
UDPPacketSize: 1509,
HTTPPlugins: map[string]plugin.HTTPPluginOptions{
"user-manager": {
Name: "user-manager",
Addr: "127.0.0.1:9009",
Path: "/handler",
Ops: []string{"Login"},
},
"port-manager": {
Name: "port-manager",
Addr: "127.0.0.1:9009",
Path: "/handler",
Ops: []string{"NewProxy"},
TLSVerify: true,
},
},
},
},
{
source: []byte(`
# [common] is integral section
[common]
bind_addr = 0.0.0.9
bind_port = 7009
bind_udp_port = 7008
`),
expected: ServerCommonConf{
ServerConfig: auth.ServerConfig{
BaseConfig: auth.BaseConfig{
AuthenticationMethod: "token",
AuthenticateHeartBeats: false,
AuthenticateNewWorkConns: false,
},
},
BindAddr: "0.0.0.9",
BindPort: 7009,
BindUDPPort: 7008,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
ProxyBindAddr: "0.0.0.9",
VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0",
DashboardUser: "",
DashboardPwd: "",
EnablePrometheus: false,
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
DetailedErrorsToClient: true,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
HeartbeatTimeout: 90,
UserConnTimeout: 10,
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
},
},
}
for _, c := range testcases {
actual, err := UnmarshalServerConfFromIni(c.source)
assert.NoError(err)
actual.Complete()
assert.Equal(c.expected, actual)
}
}

View File

@@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package types
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)
@@ -123,3 +124,65 @@ func (q *BandwidthQuantity) MarshalJSON() ([]byte, error) {
func (q *BandwidthQuantity) Bytes() int64 {
return q.i
}
type PortsRange struct {
Start int `json:"start,omitempty"`
End int `json:"end,omitempty"`
Single int `json:"single,omitempty"`
}
type PortsRangeSlice []PortsRange
func (p PortsRangeSlice) String() string {
if len(p) == 0 {
return ""
}
strs := []string{}
for _, v := range p {
if v.Single > 0 {
strs = append(strs, strconv.Itoa(v.Single))
} else {
strs = append(strs, strconv.Itoa(v.Start)+"-"+strconv.Itoa(v.End))
}
}
return strings.Join(strs, ",")
}
// the format of str is like "1000-2000,3000,4000-5000"
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
str = strings.TrimSpace(str)
out := []PortsRange{}
numRanges := strings.Split(str, ",")
for _, numRangeStr := range numRanges {
// 1000-2000 or 2001
numArray := strings.Split(numRangeStr, "-")
// length: only 1 or 2 is correct
rangeType := len(numArray)
switch rangeType {
case 1:
// single number
singleNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
if err != nil {
return nil, fmt.Errorf("range number is invalid, %v", err)
}
out = append(out, PortsRange{Single: int(singleNum)})
case 2:
// range numbers
min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
if err != nil {
return nil, fmt.Errorf("range number is invalid, %v", err)
}
max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
if err != nil {
return nil, fmt.Errorf("range number is invalid, %v", err)
}
if max < min {
return nil, fmt.Errorf("range number is invalid")
}
out = append(out, PortsRange{Start: int(min), End: int(max)})
default:
return nil, fmt.Errorf("range number is invalid")
}
}
return out, nil
}

View File

@@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package types
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type Wrap struct {
@@ -27,14 +27,46 @@ type Wrap struct {
}
func TestBandwidthQuantity(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
var w Wrap
err := json.Unmarshal([]byte(`{"b":"1KB","int":5}`), &w)
assert.NoError(err)
assert.EqualValues(1*KB, w.B.Bytes())
require.NoError(err)
require.EqualValues(1*KB, w.B.Bytes())
buf, err := json.Marshal(&w)
assert.NoError(err)
assert.Equal(`{"b":"1KB","int":5}`, string(buf))
require.NoError(err)
require.Equal(`{"b":"1KB","int":5}`, string(buf))
}
func TestPortsRangeSlice2String(t *testing.T) {
require := require.New(t)
ports := []PortsRange{
{
Start: 1000,
End: 2000,
},
{
Single: 3000,
},
}
str := PortsRangeSlice(ports).String()
require.Equal("1000-2000,3000", str)
}
func TestNewPortsRangeSliceFromString(t *testing.T) {
require := require.New(t)
ports, err := NewPortsRangeSliceFromString("1000-2000,3000")
require.NoError(err)
require.Equal([]PortsRange{
{
Start: 1000,
End: 2000,
},
{
Single: 3000,
},
}, ports)
}

19
pkg/config/v1/api.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright 2023 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
type APIMetadata struct {
Version string `json:"version"`
}

199
pkg/config/v1/client.go Normal file
View File

@@ -0,0 +1,199 @@
// Copyright 2023 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 (
"os"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/util/util"
)
type ClientConfig struct {
ClientCommonConfig
Proxies []TypedProxyConfig `json:"proxies,omitempty"`
Visitors []TypedVisitorConfig `json:"visitors,omitempty"`
}
type ClientCommonConfig struct {
APIMetadata
Auth AuthClientConfig `json:"auth,omitempty"`
// User specifies a prefix for proxy names to distinguish them from other
// clients. If this value is not "", proxy names will automatically be
// changed to "{user}.{proxy_name}".
User string `json:"user,omitempty"`
// ServerAddr specifies the address of the server to connect to. By
// default, this value is "0.0.0.0".
ServerAddr string `json:"serverAddr,omitempty"`
// ServerPort specifies the port to connect to the server on. By default,
// this value is 7000.
ServerPort int `json:"serverPort,omitempty"`
// STUN server to help penetrate NAT hole.
NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"`
// DNSServer specifies a DNS server address for FRPC to use. If this value
// is "", the default DNS will be used.
DNSServer string `json:"dnsServer,omitempty"`
// LoginFailExit controls whether or not the client should exit after a
// failed login attempt. If false, the client will retry until a login
// attempt succeeds. By default, this value is true.
LoginFailExit *bool `json:"loginFailExit,omitempty"`
// Start specifies a set of enabled proxies by name. If this set is empty,
// all supplied proxies are enabled. By default, this value is an empty
// set.
Start []string `json:"start,omitempty"`
Log LogConfig `json:"log,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"`
Transport ClientTransportConfig `json:"transport,omitempty"`
// UDPPacketSize specifies the udp packet size
// By default, this value is 1500
UDPPacketSize int64 `json:"udpPacketSize,omitempty"`
// Client metadata info
Metadatas map[string]string `json:"metadatas,omitempty"`
// Include other config files for proxies.
IncludeConfigFiles []string `json:"includes,omitempty"`
}
func (c *ClientCommonConfig) Complete() {
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.Auth.Complete()
c.Log.Complete()
c.Transport.Complete()
c.WebServer.Complete()
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
}
type ClientTransportConfig struct {
// Protocol specifies the protocol to use when interacting with the server.
// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
// is "tcp".
Protocol string `json:"protocol,omitempty"`
// The maximum amount of time a dial to server will wait for a connect to complete.
DialServerTimeout int64 `json:"dialServerTimeout,omitempty"`
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
// If negative, keep-alive probes are disabled.
DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"`
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
// Note: This value only use in TCP/Websocket protocol. Not support in KCP protocol.
ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"`
// ProxyURL specifies a proxy address to connect to the server through. If
// this value is "", the server will be connected to directly. By default,
// this value is read from the "http_proxy" environment variable.
ProxyURL string `json:"proxyURL,omitempty"`
// PoolCount specifies the number of connections the client will make to
// the server in advance.
PoolCount int `json:"poolCount,omitempty"`
// TCPMux toggles TCP stream multiplexing. This allows multiple requests
// from a client to share a single TCP connection. If this value is true,
// the server must have TCP multiplexing enabled as well. By default, this
// value is true.
TCPMux *bool `json:"tcpMux,omitempty"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// QUIC protocol options.
QUIC QUICOptions `json:"quic,omitempty"`
// HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By
// default, this value is 30. Set negative value to disable it.
HeartbeatInterval int64 `json:"heartbeatInterval,omitempty"`
// HeartBeatTimeout specifies the maximum allowed heartbeat response delay
// before the connection is terminated, in seconds. It is not recommended
// to change this value. By default, this value is 90. Set negative value to disable it.
HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
// TLS specifies TLS settings for the connection to the server.
TLS TLSClientConfig `json:"tls,omitempty"`
}
func (c *ClientTransportConfig) Complete() {
c.Protocol = util.EmptyOr(c.Protocol, "tcp")
c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10)
c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200)
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
c.PoolCount = util.EmptyOr(c.PoolCount, 1)
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
c.QUIC.Complete()
c.TLS.Complete()
}
type TLSClientConfig struct {
// TLSEnable specifies whether or not TLS should be used when communicating
// with the server. If "tls.certFile" and "tls.keyFile" are valid,
// client will load the supplied tls configuration.
// Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
Enable *bool `json:"enable,omitempty"`
// If DisableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
// first custom byte when tls is enabled.
// Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
DisableCustomTLSFirstByte *bool `json:"disableCustomTLSFirstByte,omitempty"`
TLSConfig
}
func (c *TLSClientConfig) Complete() {
c.Enable = util.EmptyOr(c.Enable, lo.ToPtr(true))
c.DisableCustomTLSFirstByte = util.EmptyOr(c.DisableCustomTLSFirstByte, lo.ToPtr(true))
}
type AuthClientConfig struct {
// Method specifies what authentication method to use to
// authenticate frpc with frps. If "token" is specified - token will be
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
// token will be issued using OIDC settings. By default, this value is "token".
Method AuthMethod `json:"method,omitempty"`
// Specify whether to include auth info in additional scope.
// Current supported scopes are: "HeartBeats", "NewWorkConns".
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
// 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"`
}
func (c *AuthClientConfig) Complete() {
c.Method = util.EmptyOr(c.Method, "token")
}
type AuthOIDCClientConfig struct {
// ClientID specifies the client ID to use to get a token in OIDC authentication.
ClientID string `json:"clientID,omitempty"`
// ClientSecret specifies the client secret to use to get a token in OIDC
// authentication.
ClientSecret string `json:"clientSecret,omitempty"`
// Audience specifies the audience of the token in OIDC authentication.
Audience string `json:"audience,omitempty"`
// Scope specifies the scope of the token in OIDC authentication.
Scope string `json:"scope,omitempty"`
// TokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
// It will be used to get an OIDC token.
TokenEndpointURL string `json:"tokenEndpointURL,omitempty"`
// 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"`
}

View File

@@ -0,0 +1,34 @@
// Copyright 2023 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 (
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)
func TestClientConfigComplete(t *testing.T) {
require := require.New(t)
c := &ClientConfig{}
c.Complete()
require.EqualValues("token", c.Auth.Method)
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
require.Equal(true, lo.FromPtr(c.LoginFailExit))
require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable))
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
}

Some files were not shown because too many files have changed in this diff Show More