Compare commits

...

146 Commits
v0.52.3 ... dev

Author SHA1 Message Date
fatedier
de690a55c8
update golangci-lint version (#4817) 2025-05-27 16:46:15 +08:00
fatedier
1ced733340
update test package (#4814) 2025-05-26 14:54:03 +08:00
fatedier
ce366ee17f
add proxy protocol support for UDP proxies (#4810) 2025-05-23 21:39:47 +08:00
fatedier
3128350dd6
feat: support YAML merge in strict configuration mode (#4809) 2025-05-23 19:25:34 +08:00
scientificworld
3be6efdd28
fix: type error in server_plugin doc (#4799) 2025-05-19 11:39:35 +08:00
CrynTox
6cbb26283c
build: add x64 openbsd (#4780)
Co-authored-by: CrynTox <>
2025-05-06 12:11:20 +08:00
fatedier
75edea3370
update release notes (#4772) 2025-04-27 15:41:13 +08:00
fatedier
e687aef37e
vnet: fix issues (#4771) 2025-04-27 15:22:28 +08:00
fatedier
a23455a737
update feature gates doc (#4755) 2025-04-17 01:34:14 +08:00
fatedier
e208043323
vnet: update tun_unsupported function (#4752) 2025-04-16 16:17:26 +08:00
fatedier
a78814a2e9
virtual-net: initial (#4751) 2025-04-16 16:05:54 +08:00
fatedier
773169e0c4
update version (#4699) 2025-03-07 17:22:51 +08:00
fatedier
9757a351c6
fix golangci lint config (#4698) 2025-03-07 16:56:08 +08:00
fatedier
1e8db66743
update Release.md (#4668) 2025-02-12 12:30:37 +08:00
fatedier
e0dd947e6a
frps: release resources in service.Close() (#4667) 2025-02-12 12:22:57 +08:00
ubergeek77
8b86e1473c
Fix ports not being released on Service.Close() (#4666) 2025-02-12 11:28:30 +08:00
hansmi
b8d3ace113
Use text/template instead of html/template for config pre-processing (#4656) 2025-02-07 11:33:11 +08:00
Jeb.Wang
450b8393bc
Fix goroutine leaks
* Fix goroutine leaks
2025-01-16 10:50:57 +08:00
fatedier
27db6217ec
frpc: support metadatas and annotations in frpc proxy commands (#4623) 2025-01-06 14:22:57 +08:00
Andreas Deininger
6542dcd4ed
Fix typos (#4615) 2025-01-02 11:33:56 +08:00
Gabriel Marin
092e5d3f94
client, pkg, server, test: replaced 'interface{}' with 'any' (#4611) 2025-01-02 11:24:08 +08:00
fatedier
01fed8d1a9
Update stale workflow (#4600) 2024-12-19 18:13:25 +08:00
fatedier
f47d8ab97f
update Release.md (#4589) 2024-12-16 19:33:58 +08:00
Sword
bb912d6c37
enable h2c for vhost server (#4582) 2024-12-13 14:37:07 +08:00
Roy Reznik
c73096f2bf
Upgrade packages to resolve CVE-2024-53259 (#4577) 2024-12-10 20:07:38 +08:00
Guiwoo Park
0358113948
samber lo version up (#4569) 2024-12-02 11:56:52 +08:00
fatedier
8593eff752
update sponsor info (#4545) 2024-11-20 15:14:51 +08:00
fatedier
dff56cb0ca
update .golangci.yml (#4527) 2024-11-07 17:14:40 +08:00
fatedier
4383756fd4
frps: add support for quic-bind-port parameter in frps (#4519) 2024-10-30 15:12:31 +08:00
fatedier
6ba849fc75
readme: update sponsor (#4504) 2024-10-22 10:48:24 +08:00
fatedier
9d5638cae6
update Release.md (#4500) 2024-10-18 12:31:55 +08:00
fatedier
62352c7ba5
dockerfiles: add tzdata (#4499) 2024-10-18 12:03:17 +08:00
fatedier
f7a06cbe61
use go1.23 (#4495) 2024-10-17 17:22:41 +08:00
fatedier
3a08c2aeb0
conf: fix example for tls2raw (#4494) 2024-10-17 16:27:41 +08:00
0x7fff
b14192a8d3
feat: bump (#4490)
Co-authored-by: Coder123 <coder123@example.com>
2024-10-15 10:55:56 +08:00
RobKenis
2466e65f43
support multiple subjects in oidc ping (#4475)
Resolves: #4466
2024-10-12 18:52:47 +08:00
fatedier
2855ac71e3
frpc visitor: add --server-user option to specify server proxy username (#4477) 2024-10-09 14:04:30 +08:00
拾光,
fe4ca1b54e
Update README_zh.md (#4421)
修复爱发电链接无法访问问题
2024-09-06 11:41:11 +08:00
crystalstall
edd7cf8967
chore: fix function name (#4416)
Signed-off-by: crystalstall <crystalruby@qq.com>
2024-09-06 11:39:22 +08:00
Wang Xiang
03c8d7bf96
bump kcp-go to add linux/loong64 support (#4384) 2024-08-16 22:24:27 +08:00
fatedier
2dcdb24cc4
replace github.com/templexxx/xorsimd to the new version (#4373) 2024-08-07 11:18:17 +08:00
Wang Xiang
d47e138bc9
bump templexxx/cpu version and add support for linux/loong64 (#4367)
* support linux/loong64

* bump cpu version
2024-08-06 17:33:14 +08:00
fatedier
f1fb2d721a
update .github/FUNDING.yml (#4365) 2024-08-02 17:04:59 +08:00
fatedier
ae73ec2fed
added a 30s timeout for frpc subcommands to avoid long delays (#4359) 2024-07-30 18:12:22 +08:00
Yurun
e8045194cd
Fix loginFailExit = false bug (#4354)
* Fixed the issue that when loginFailExit = false, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup

* Update Release.md
2024-07-30 11:19:26 +08:00
fatedier
69cc422edf
client plugin: added plugin tls2raw (#4341) 2024-07-25 14:28:17 +08:00
fatedier
b4d5d8c756
plugin https2http&https2https: return 421 if host not match sni (#4323) 2024-07-09 10:50:16 +08:00
fatedier
c6f9d8d403
update sponsors (#4303) 2024-06-26 14:51:34 +08:00
fatedier
939c490768
Add http2http client plugin with hostHeaderRewrite and requestHeaders support (#4275) 2024-06-12 17:30:10 +08:00
fatedier
f390e4a401
add sponsor (#4265) 2024-06-05 16:53:29 +08:00
fatedier
77990c31ef
fix ini configuration default values (#4250) 2024-05-30 10:36:30 +08:00
fatedier
e680acf42d
android: only use google dns server when the default dns server cannot be obtained (#4236) 2024-05-23 16:09:58 +08:00
fatedier
522e2c94c1
config: return error if plugin type is empty (#4235) 2024-05-23 14:52:12 +08:00
fatedier
301515d2e8
update the default value of transport.tcpMuxKeepaliveInterval (#4231) 2024-05-21 12:00:35 +08:00
fatedier
f0442d0cd5
plugin: fix http2 not enabled for https2http and https2https plugin (#4230) 2024-05-21 11:26:52 +08:00
fatedier
9ced717d69
update build-and-push-image.yml (#4206) 2024-05-07 19:14:09 +08:00
fatedier
92cb0b30c2
update version (#4204) 2024-05-07 18:05:36 +08:00
fatedier
e81b36c5ba
support responseHeaders.set for proxy type http (#4192) 2024-04-29 15:53:45 +08:00
Weltolk
d0d396becb
Update README.md (#4190) 2024-04-29 11:50:56 +08:00
fatedier
ee3892798d
change default value of heartbeat interval and timeout when tcpmux enabled (#4186) 2024-04-28 20:48:44 +08:00
fatedier
405969085f
client: add StatusExporter in service (#4182) 2024-04-25 20:20:39 +08:00
fatedier
c1893ee1b4
adjust arm compilation configuration (#4181) 2024-04-25 13:08:41 +08:00
ddscentral
eaae212d2d
Makefile.cross-compiles: Fix softfloat flag not being honored for mipsle. (#4176) 2024-04-22 19:50:58 +08:00
fatedier
885278c045
README add releated projects (#4167) 2024-04-17 20:41:14 +08:00
fatedier
2626d6ed92
support linux/arm v6 (#4154) 2024-04-12 21:21:28 +08:00
fatedier
f3a71bc08f
show tcpmux proxies on the frps dashboard (#4152) 2024-04-11 22:40:42 +08:00
fatedier
dd7e2e8473
return 504 instead of 404 for proxy type http request timeout (#4151) 2024-04-11 20:19:08 +08:00
gopherfarm
07946e9752
fix: revert gorilla/websocket from 1.5.1 to 1.5.0 (#4149) 2024-04-11 11:01:02 +08:00
fatedier
e52727e01c
update golib (#4142) 2024-04-10 12:05:22 +08:00
fatedier
ba937e9fbf
update README (#4139) 2024-04-09 15:03:32 +08:00
fatedier
d2d03a8fd9
bump deps version (#4136) 2024-04-09 11:39:21 +08:00
fatedier
590ccda677
fix x-forwarded-for header (#4111) 2024-03-28 16:47:27 +08:00
fatedier
86f90f4d27
package.sh add android (#4094) 2024-03-21 19:38:18 +08:00
fatedier
f16ef00975
set CompatibilityMode for android (#4091) 2024-03-21 17:34:09 +08:00
fatedier
b36f3834eb
use math/rand/v2 (#4020) 2024-03-20 15:48:31 +08:00
fatedier
c08be0fd92
update release notes (#4086) 2024-03-20 15:16:01 +08:00
Kaive Young
bc5fb91c05
add header for http healthcheck (#4085) 2024-03-20 14:58:03 +08:00
fatedier
002831ea82
add doc for port range mapping (#4081) 2024-03-19 13:22:29 +08:00
fatedier
acf33db4e4
update release notes (#4074) 2024-03-15 17:50:58 +08:00
fatedier
3585f5c0c0
support range ports mapping by go template (#4073) 2024-03-15 17:23:16 +08:00
fatedier
8383d528d9
disable quic-go's ECN support by default (#4069) 2024-03-15 14:22:03 +08:00
fatedier
fa977c839f
fix daily log rotate (#4066) 2024-03-14 19:49:58 +08:00
fatedier
86c2ad78c8
disable quic-go's receive buffer warning (#4063) 2024-03-13 21:53:09 +08:00
fatedier
d5589213c5
add set -e in package.sh (#4059) 2024-03-12 18:07:55 +08:00
fatedier
e0c979e98e
fix release scripts (#4057) 2024-03-12 17:37:14 +08:00
fatedier
e6ec5a509b
update release notes (#4055) 2024-03-12 15:14:13 +08:00
fatedier
43ba7bd338
use new log package (#4054) 2024-03-12 13:58:53 +08:00
fatedier
49443cb2c6
update sponsor (#4045)
* update sponsors
2024-03-07 14:45:55 +08:00
fatedier
32f09c4b60
regen web assets (#4040) 2024-03-03 23:02:38 +08:00
Haotian Zou
f63b4d5c29
fix: change absolute path to relative path (#4038) 2024-03-03 22:57:17 +08:00
fatedier
b3946489dd
add new badges (#4028) 2024-02-27 17:43:37 +08:00
fatedier
7ae3719b82
cleanup code (#4019) 2024-02-22 21:04:21 +08:00
fatedier
80cfd0938e
use go1.22 (#4018) 2024-02-22 20:24:33 +08:00
fatedier
52f66b05e6
update golangci-lint version to 1.56 (#4013) 2024-02-21 17:20:40 +08:00
fatedier
b2b580be22
update kcp-go package (#4009) 2024-02-20 16:11:37 +08:00
fatedier
3e0c78233a
use std slices package (#4008) 2024-02-20 12:01:41 +08:00
fatedier
b6361fb143
use go built-in min & max functions to replace lo.Min and lo.Max (#4007) 2024-02-19 21:27:56 +08:00
fatedier
adb04e81e7
bump pion/stun to v2 (#4006) 2024-02-19 21:13:20 +08:00
fatedier
518ca2ceb2
ReverseProxy: use Rewrite to replace Director (#4005)
* display go version in make

* ReverseProxy: use Rewrite to replace Director
2024-02-19 21:04:29 +08:00
fatedier
4957fd23ee
display go version in make (#4003) 2024-02-19 17:46:47 +08:00
fatedier
2f958c2095
bump quic-go to 0.41.0 (#4002) 2024-02-19 17:17:02 +08:00
fatedier
dc34a68542
support go1.22 and drop support for go1.20 (#4001) 2024-02-19 16:45:25 +08:00
fatedier
3529158f31
proxy supports configuring annotations, which will be displayed in the frps dashboard (#4000) 2024-02-19 16:28:27 +08:00
Gerhard Tan
9152c59570
fix nil map error when using plugin headers in legacy format (#3996)
* fix nil map error when using plugin headers in legacy format

* create map on demand

* better initialization
2024-02-19 13:03:37 +08:00
Apflkuacha
2af2cf7dbd
Updated readme for the HTTP Proxy (#3999)
Improved the http proxy command by using %h and %p to avoid typing the host name twice
2024-02-19 11:43:09 +08:00
Innokenty Ivanov
cf025d6320
Update README.md - add note for public purpose (#3971)
Some people set servers in purpose to be accessed though public internet. This line removes some misconfiguration if they want publicly accessed server.
2024-02-04 15:41:09 +08:00
fatedier
e8ace492a5
bump version to 0.54.0 (#3967) 2024-02-01 12:52:25 +08:00
fatedier
1c8bc0bfa8
make the host/domain matching case-insensitive (#3966) 2024-02-01 11:53:52 +08:00
fatedier
b31c67d7c0
web: support to clear offline proxies data on dashboard (#3963) 2024-02-01 10:54:57 +08:00
fatedier
8023d147b0 update stale workflow 2024-01-25 14:22:15 +08:00
fatedier
6a488cc081
update stale workflow (#3952) 2024-01-25 14:10:03 +08:00
fatedier
7418ae098d
increase operations-per-run in stale workflow (#3950) 2024-01-25 12:25:29 +08:00
fatedier
7999791708
update stale workflow (#3949) 2024-01-25 11:38:19 +08:00
Remember
f7efbfeec5
fix: typo (#3938) 2024-01-16 18:38:54 +08:00
ThrRip
1e8806d26b
Fix missing prefix for transport.tls.force in the example (#3921) 2024-01-09 18:50:50 +08:00
fatedier
d01f4a3ec1
cmd: use hyphen instead of underscore (#3898) 2023-12-27 10:44:13 +08:00
Remember
596262d5e0
upgrade go-jose and crypto version (#3895) 2023-12-26 10:49:46 +08:00
Remember
cdfa8fa66f
fix(client): close workConn when authentication err (#3885) 2023-12-22 15:47:59 +08:00
fatedier
256b87321d
improve e2e port allocator (#3882) 2023-12-21 22:46:08 +08:00
fatedier
5b7b81a117
let e2e concurrency configurable (#3881) 2023-12-21 21:58:56 +08:00
fatedier
2a9a7a0e4a
fix login retry interval (#3879) 2023-12-21 21:38:32 +08:00
fatedier
5e77c8e2d3
fix lint (#3877) 2023-12-21 21:19:49 +08:00
im_zhou
3bf6605e1a
fix: duplicate call loginFunc (#3860) (#3875)
modify ext func, specify whether exit immediately
2023-12-21 20:51:10 +08:00
Remember
3540910879
fix(backoff): close of closed out channel (#3871)
* fix: close of closed channel

* feat: replace Try0 to std
2023-12-21 11:43:42 +08:00
fatedier
2d67e2e0c6
remove copilot for pr (#3857) 2023-12-18 10:53:02 +08:00
fatedier
cc2076970f
update doc (#3844) 2023-12-14 20:54:03 +08:00
0x7fff
e7652f4ccc
feat: ssh doc (#3841)
* feat: add example

* feat: add ssh doc

---------

Co-authored-by: int7 <int7@gmail.com>
2023-12-14 20:32:40 +08:00
0x7fff
e66e77cb8f
add error (#3833)
Co-authored-by: int7 <int7@gmail.com>
2023-12-07 17:25:22 +08:00
fatedier
1bc9d1a28e update sponsor doc 2023-12-02 16:39:35 +08:00
fatedier
7ad62818bd
update sponsor doc (#3823) 2023-12-02 16:12:37 +08:00
fatedier
9ecafeab40
bump version to v0.53.0 (#3822) 2023-12-01 20:44:50 +08:00
fatedier
95cf418963
ssh: return informations to client (#3821) 2023-12-01 20:18:13 +08:00
im_zhou
6d9e0c20f6
fix static assets (#3816) 2023-11-30 10:59:08 +08:00
fatedier
97d3cf1a3b
call config complete in nathole discover (#3813) 2023-11-28 19:02:51 +08:00
fatedier
38f297a395
Improve the strict configuration validation (#3809) 2023-11-28 18:43:33 +08:00
fatedier
7c799ee921
add e2e tests for ssh tunnel (#3805) 2023-11-28 13:48:32 +08:00
fatedier
69ae2b0b69
optimize some code (#3801) 2023-11-27 15:47:49 +08:00
fatedier
d5b41f1e14 sshTunnelGateway refactor (#3784) 2023-11-22 14:35:37 +08:00
0x7fff
8b432e179d feat: ssh client implement (#3671)
* feat: frps support ssh

* fix: comments

* fix: update pkg

* fix: remove useless change

---------

Co-authored-by: int7 <int7@gmail.com>
2023-11-22 14:35:37 +08:00
Aarni Koskela
f5d5a00eef
Fix various typos (#3783) 2023-11-22 14:30:22 +08:00
fatedier
526e809bd5
update for strict config (#3779) 2023-11-16 21:03:36 +08:00
Aarni Koskela
e8deb65c4b
Strict configuration parsing (#3773)
* Test configuration loading more precisely

* Add strict configuration parsing
2023-11-16 15:42:49 +08:00
fatedier
184223cb2f
Code refactoring related to message handling and retry logic. (#3745) 2023-11-06 10:51:48 +08:00
fatedier
5760c1cf92
frpc: exit with code 1 if first login failed (#3740) 2023-11-01 17:06:55 +08:00
Rene Leonhardt
5c4d820eb4
chore: Update dependencies (#3738)
* chore: Update dependencies

* Removed all foolish updates
2023-10-31 19:40:48 +08:00
0x7fff
46266e4d30
fix: set ping (#3734)
Co-authored-by: int7 <int7@gmail.com>
2023-10-30 20:24:57 +08:00
253 changed files with 10433 additions and 6195 deletions

View File

@ -2,24 +2,15 @@ version: 2
jobs: jobs:
go-version-latest: go-version-latest:
docker: docker:
- image: cimg/go:1.21-node - image: cimg/go:1.23-node
resource_class: large resource_class: large
steps: steps:
- checkout - checkout
- run: make - run: make
- run: make alltest - run: make alltest
go-version-last:
docker:
- image: cimg/go:1.20-node
resource_class: large
steps:
- checkout
- run: make
- run: make alltest
workflows: workflows:
version: 2 version: 2
build_and_test: build_and_test:
jobs: jobs:
- go-version-latest - go-version-latest
- go-version-last

2
.github/FUNDING.yml vendored
View File

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

View File

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

View File

@ -2,7 +2,7 @@ name: Build Image and Publish to Dockerhub & GPR
on: on:
release: release:
types: [ created ] types: [ published ]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
@ -19,15 +19,15 @@ jobs:
steps: steps:
# environment # environment
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: '0' fetch-depth: '0'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
# get image tag name # get image tag name
- name: Get Image Tag Name - name: Get Image Tag Name
@ -38,13 +38,13 @@ jobs:
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi fi
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to the GPR - name: Login to the GPR
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -61,7 +61,7 @@ jobs:
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
- name: Build and push frpc - name: Build and push frpc
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./dockerfiles/Dockerfile-for-frpc file: ./dockerfiles/Dockerfile-for-frpc
@ -72,7 +72,7 @@ jobs:
${{ env.TAG_FRPC_GPR }} ${{ env.TAG_FRPC_GPR }}
- name: Build and push frps - name: Build and push frps
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./dockerfiles/Dockerfile-for-frps file: ./dockerfiles/Dockerfile-for-frps

View File

@ -14,28 +14,23 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v4 - uses: actions/checkout@v4
with: - uses: actions/setup-go@v5
go-version: '1.21' with:
- uses: actions/checkout@v3 go-version: '1.23'
- name: golangci-lint cache: false
uses: golangci/golangci-lint-action@v3 - name: golangci-lint
with: uses: golangci/golangci-lint-action@v8
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version with:
version: v1.53 # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v2.1
# Optional: golangci-lint command line arguments. # Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0 # args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled, # Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options. # takes precedence over all other caching options.
# skip-cache: true # skip-cache: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true

View File

@ -8,21 +8,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.23'
- name: Make All - name: Make All
run: | run: |
./package.sh ./package.sh
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4 uses: goreleaser/goreleaser-action@v5
with: with:
version: latest version: latest
args: release --clean --release-notes=./Release.md args: release --clean --release-notes=./Release.md

View File

@ -1,4 +1,4 @@
name: "Close stale issues" name: "Close stale issues and PRs"
on: on:
schedule: schedule:
- cron: "20 0 * * *" - cron: "20 0 * * *"
@ -16,19 +16,20 @@ jobs:
permissions: permissions:
issues: write # for actions/stale to close stale issues issues: write # for actions/stale to close stale issues
pull-requests: write # for actions/stale to close stale PRs pull-requests: write # for actions/stale to close stale PRs
actions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v9
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.' stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
stale-pr-message: "PRs go stale after 30d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
stale-issue-label: 'lifecycle/stale' stale-issue-label: 'lifecycle/stale'
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
stale-pr-label: 'lifecycle/stale' stale-pr-label: 'lifecycle/stale'
exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
days-before-stale: 30 days-before-stale: 14
days-before-close: 7 days-before-close: 3
debug-only: ${{ github.event.inputs.debug-only }} debug-only: ${{ github.event.inputs.debug-only }}
exempt-all-pr-milestones: true exempt-all-pr-milestones: true
exempt-all-pr-assignees: true exempt-all-pr-assignees: true
operations-per-run: 200

3
.gitignore vendored
View File

@ -33,6 +33,9 @@ lastversion/
dist/ dist/
.idea/ .idea/
.vscode/ .vscode/
.autogen_ssh_key
client.crt
client.key
# Cache # Cache
*.swp *.swp

View File

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

View File

@ -1,11 +1,14 @@
export PATH := $(GOPATH)/bin:$(PATH) export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on export GO111MODULE=on
LDFLAGS := -s -w LDFLAGS := -s -w
all: fmt build all: env fmt build
build: frps frpc build: frps frpc
env:
@go version
# compile assets into binary file # compile assets into binary file
file: file:
rm -rf ./assets/frps/static/* rm -rf ./assets/frps/static/*
@ -26,10 +29,10 @@ vet:
go vet ./... go vet ./...
frps: frps:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
frpc: frpc:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
test: gotest test: gotest

View File

@ -1,23 +1,35 @@
export PATH := $(GOPATH)/bin:$(PATH) export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on export GO111MODULE=on
LDFLAGS := -s -w LDFLAGS := -s -w
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 os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
all: build all: build
build: app build: app
app: app:
@$(foreach n, $(os-archs),\ @$(foreach n, $(os-archs), \
os=$(shell echo "$(n)" | cut -d : -f 1);\ os=$(shell echo "$(n)" | cut -d : -f 1); \
arch=$(shell echo "$(n)" | cut -d : -f 2);\ arch=$(shell echo "$(n)" | cut -d : -f 2); \
gomips=$(shell echo "$(n)" | cut -d : -f 3);\ extra=$(shell echo "$(n)" | cut -d : -f 3); \
target_suffix=$${os}_$${arch};\ flags=''; \
echo "Build $${os}-$${arch}...";\ target_suffix=$${os}_$${arch}; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_$${target_suffix} ./cmd/frpc;\ if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\ if [ "$${extra}" = "7" ]; then \
echo "Build $${os}-$${arch} done";\ flags=GOARM=7; \
target_suffix=$${os}_arm_hf; \
elif [ "$${extra}" = "5" ]; then \
flags=GOARM=5; \
target_suffix=$${os}_arm; \
fi; \
elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
flags=GOMIPS=$${extra}; \
fi; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
) )
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe @mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe @mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe

157
README.md
View File

@ -2,14 +2,30 @@
[![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/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) [![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases)
[![Go Report Card](https://goreportcard.com/badge/github.com/fatedier/frp)](https://goreportcard.com/report/github.com/fatedier/frp)
[![GitHub Releases Stats](https://img.shields.io/github/downloads/fatedier/frp/total.svg?logo=github)](https://somsubhra.github.io/github-release-stats/?username=fatedier&repository=frp)
[README](README.md) | [中文文档](README_zh.md) [README](README.md) | [中文文档](README_zh.md)
## Sponsors
frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors. If you'd like to join them, please consider [sponsoring frp's development](https://github.com/sponsors/fatedier).
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center"> <p align="center">
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
</a>
</p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
</a> </a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@ -42,7 +58,7 @@ frp also offers a P2P connect mode.
* [Using Environment Variables](#using-environment-variables) * [Using Environment Variables](#using-environment-variables)
* [Split Configures Into Different Files](#split-configures-into-different-files) * [Split Configures Into Different Files](#split-configures-into-different-files)
* [Server Dashboard](#server-dashboard) * [Server Dashboard](#server-dashboard)
* [Admin UI](#admin-ui) * [Client Admin UI](#client-admin-ui)
* [Monitor](#monitor) * [Monitor](#monitor)
* [Prometheus](#prometheus) * [Prometheus](#prometheus)
* [Authenticating the Client](#authenticating-the-client) * [Authenticating the Client](#authenticating-the-client)
@ -71,9 +87,17 @@ frp also offers a P2P connect mode.
* [Custom Subdomain Names](#custom-subdomain-names) * [Custom Subdomain Names](#custom-subdomain-names)
* [URL Routing](#url-routing) * [URL Routing](#url-routing)
* [TCP Port Multiplexing](#tcp-port-multiplexing) * [TCP Port Multiplexing](#tcp-port-multiplexing)
* [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) * [Connecting to frps via PROXY](#connecting-to-frps-via-proxy)
* [Port range mapping](#port-range-mapping)
* [Client Plugins](#client-plugins) * [Client Plugins](#client-plugins)
* [Server Manage Plugins](#server-manage-plugins) * [Server Manage Plugins](#server-manage-plugins)
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
* [Virtual Network (VirtualNet)](#virtual-network-virtualnet)
* [Feature Gates](#feature-gates)
* [Available Feature Gates](#available-feature-gates)
* [Enabling Feature Gates](#enabling-feature-gates)
* [Feature Lifecycle](#feature-lifecycle)
* [Related Projects](#related-projects)
* [Contributing](#contributing) * [Contributing](#contributing)
* [Donation](#donation) * [Donation](#donation)
* [GitHub Sponsors](#github-sponsors) * [GitHub Sponsors](#github-sponsors)
@ -196,11 +220,11 @@ This example implements multiple SSH services exposed through the same port usin
4. To access internal machine A using SSH ProxyCommand, assuming the username is "test": 4. To access internal machine A using SSH ProxyCommand, assuming the username is "test":
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-a.example.com:22,proxyport=5002' test@machine-a` `ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-a.example.com`
5. To access internal machine B, the only difference is the domain name, assuming the username is "test": 5. To access internal machine B, the only difference is the domain name, assuming the username is "test":
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-b.example.com:22,proxyport=5002' test@machine-b` `ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-b.example.com`
### Accessing Internal Web Services with Custom Domains in LAN ### Accessing Internal Web Services with Custom Domains in LAN
@ -343,7 +367,6 @@ You may substitute `https2https` for the plugin, and point the `localAddr` to a
# frpc.toml # frpc.toml
serverAddr = "x.x.x.x" serverAddr = "x.x.x.x"
serverPort = 7000 serverPort = 7000
vhostHTTPSPort = 443
[[proxies]] [[proxies]]
name = "test_https2http" name = "test_https2http"
@ -505,6 +528,7 @@ includes = ["./confd/*.toml"]
```toml ```toml
# ./confd/test.toml # ./confd/test.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -520,6 +544,8 @@ Check frp's status and proxies' statistics information by Dashboard.
Configure a port for dashboard to enable this feature: Configure a port for dashboard to enable this feature:
```toml ```toml
# The default value is 127.0.0.1. Change it to 0.0.0.0 when you want to access it from a public network.
webServer.addr = "0.0.0.0"
webServer.port = 7500 webServer.port = 7500
# dashboard's username and password are both optional # dashboard's username and password are both optional
webServer.user = "admin" webServer.user = "admin"
@ -616,6 +642,7 @@ The features are off by default. You can turn on encryption and/or compression:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -771,6 +798,7 @@ We would like to try to allow multiple proxies bind a same remote port with diff
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -791,7 +819,7 @@ You can disable this feature by modify `frps.toml` and `frpc.toml`:
```toml ```toml
# frps.toml and frpc.toml, must be same # frps.toml and frpc.toml, must be same
tcpMux = false transport.tcpMux = false
``` ```
### Support KCP Protocol ### Support KCP Protocol
@ -876,6 +904,7 @@ This feature is only available for types `tcp`, `http`, `tcpmux` now.
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "test1" name = "test1"
type = "tcp" type = "tcp"
@ -911,6 +940,7 @@ With health check type **tcp**, the service port will be pinged (TCPing):
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "test1" name = "test1"
type = "tcp" type = "tcp"
@ -930,6 +960,7 @@ With health check type **http**, an HTTP request will be sent to the service and
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -954,6 +985,7 @@ However, speaking of web servers and HTTP requests, your web server might rely o
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -966,10 +998,11 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com
### Setting other HTTP Headers ### Setting other HTTP Headers
Similar to `Host`, You can override other HTTP request headers with proxy type `http`. Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`.
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -977,26 +1010,28 @@ localPort = 80
customDomains = ["test.example.com"] customDomains = ["test.example.com"]
hostHeaderRewrite = "dev.example.com" hostHeaderRewrite = "dev.example.com"
requestHeaders.set.x-from-where = "frp" requestHeaders.set.x-from-where = "frp"
responseHeaders.set.foo = "bar"
``` ```
In this example, it will set header `x-from-where: frp` in the HTTP request. In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response.
### Get Real IP ### Get Real IP
#### HTTP X-Forwarded-For #### HTTP X-Forwarded-For
This feature is for http proxy only. This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled.
You can get user's real IP from HTTP request headers `X-Forwarded-For`. You can get user's real IP from HTTP request headers `X-Forwarded-For`.
#### Proxy Protocol #### Proxy Protocol
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP. frp supports Proxy Protocol to send user's real IP to local services.
Here is an example for https service: Here is an example for https service:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "https" type = "https"
@ -1019,6 +1054,7 @@ It can only be enabled when proxy type is http.
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -1043,6 +1079,7 @@ Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -1062,6 +1099,7 @@ frp supports forwarding HTTP requests to different backend web services by url r
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web01" name = "web01"
type = "http" type = "http"
@ -1137,6 +1175,24 @@ serverPort = 7000
transport.proxyURL = "http://user:pwd@192.168.1.128:8080" transport.proxyURL = "http://user:pwd@192.168.1.128:8080"
``` ```
### Port range mapping
*Added in v0.56.0*
We can use the range syntax of Go template combined with the built-in `parseNumberRangePair` function to achieve port range mapping.
The following example, when run, will create 8 proxies named `test-6000, test-6001 ... test-6007`, each mapping the remote port to the local port.
```
{{- range $_, $v := parseNumberRangePair "6000-6006,6007" "6000-6006,6007" }}
[[proxies]]
name = "tcp-{{ $v.First }}"
type = "tcp"
localPort = {{ $v.First }}
remotePort = {{ $v.Second }}
{{- end }}
```
### Client Plugins ### Client Plugins
frpc only forwards requests to local TCP or UDP ports by default. frpc only forwards requests to local TCP or UDP ports by default.
@ -1147,6 +1203,7 @@ Using plugin **http_proxy**:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "http_proxy" name = "http_proxy"
type = "tcp" type = "tcp"
@ -1165,6 +1222,82 @@ Read the [document](/doc/server_plugin.md).
Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin). Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin).
### SSH Tunnel Gateway
*added in v0.53.0*
frp supports listening to an SSH port on the frps side and achieves TCP protocol proxying through the SSH -R protocol, without relying on frpc.
```toml
# frps.toml
sshTunnelGateway.bindPort = 2200
```
When running `./frps -c frps.toml`, a private key file named `.autogen_ssh_key` will be automatically created in the current working directory. This generated private key file will be used by the SSH server in frps.
Executing the command
```bash
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
```
sets up a proxy on frps that forwards the local 8080 service to the port 9090.
```bash
frp (via SSH) (Ctrl+C to quit)
User:
ProxyName: test-tcp
Type: tcp
RemoteAddress: :9090
```
This is equivalent to:
```bash
frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
```
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
### Virtual Network (VirtualNet)
*Alpha feature added in v0.62.0*
The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
For detailed information about configuration and usage, please refer to the [VirtualNet documentation](/doc/virtual_net.md).
## Feature Gates
frp supports feature gates to enable or disable experimental features. This allows users to try out new features before they're considered stable.
### Available Feature Gates
| Name | Stage | Default | Description |
|------|-------|---------|-------------|
| VirtualNet | ALPHA | false | Virtual network capabilities for frp |
### Enabling Feature Gates
To enable an experimental feature, add the feature gate to your configuration:
```toml
featureGates = { VirtualNet = true }
```
### Feature Lifecycle
Features typically go through three stages:
1. **ALPHA**: Disabled by default, may be unstable
2. **BETA**: May be enabled by default, more stable but still evolving
3. **GA (Generally Available)**: Enabled by default, ready for production use
## Related Projects
* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources.
## Contributing ## Contributing
Interested in getting involved? We would like to help you! Interested in getting involved? We would like to help you!

View File

@ -2,16 +2,32 @@
[![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/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) [![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases)
[![Go Report Card](https://goreportcard.com/badge/github.com/fatedier/frp)](https://goreportcard.com/report/github.com/fatedier/frp)
[![GitHub Releases Stats](https://img.shields.io/github/downloads/fatedier/frp/total.svg?logo=github)](https://somsubhra.github.io/github-release-stats/?username=fatedier&repository=frp)
[README](README.md) | [中文文档](README_zh.md) [README](README.md) | [中文文档](README_zh.md)
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
## Sponsors
frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center"> <p align="center">
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
</a>
</p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
</a> </a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@ -66,7 +82,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
* 贡献代码请提交 PR 至 dev 分支master 分支仅用于发布稳定可用版本。 * 贡献代码请提交 PR 至 dev 分支master 分支仅用于发布稳定可用版本。
* 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。 * 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。** **提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
## 关联项目
* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
## 赞助 ## 赞助
@ -78,16 +99,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。 您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。 国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
### 知识星球 ### 知识星球
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: 如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg) ![zsxq](/doc/pic/zsxq.jpg)
### 微信支付捐赠
![donate-wechatpay](/doc/pic/donate-wechatpay.png)

View File

@ -1,3 +1,4 @@
### Fixes ## Features
* `admin_user` is not effective in the INI configuration. * Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter.
* Support for proxy protocol in UDP proxies to preserve real client IP addresses.

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,13 +4,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frp client admin UI</title> <title>frp client admin UI</title>
<script type="module" crossorigin src="./index-1c7ed8b0.js"></script> <script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
<link rel="stylesheet" href="./index-1e2a7ce0.css"> <link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

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,13 +4,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frps dashboard</title> <title>frps dashboard</title>
<script type="module" crossorigin src="./index-c322b7dd.js"></script> <script type="module" crossorigin src="./index-82-40HIG.js"></script>
<link rel="stylesheet" href="./index-1e0c7400.css"> <link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

View File

@ -1,85 +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 (
"net"
"net/http"
"net/http/pprof"
"time"
"github.com/gorilla/mux"
"github.com/fatedier/frp/assets"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
var (
httpServerReadTimeout = 60 * time.Second
httpServerWriteTimeout = 60 * time.Second
)
func (svr *Service) RunAdminServer(address string) (err error) {
// url router
router := mux.NewRouter()
router.HandleFunc("/healthz", svr.healthz)
// debug
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)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
subRouter := router.NewRoute().Subrouter()
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(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)
})
server := &http.Server{
Addr: address,
Handler: router,
ReadTimeout: httpServerReadTimeout,
WriteTimeout: httpServerWriteTimeout,
}
if address == "" {
address = ":http"
}
ln, err := net.Listen("tcp", address)
if err != nil {
return err
}
go func() {
_ = server.Serve(ln)
}()
return
}

View File

@ -15,23 +15,23 @@
package client package client
import ( import (
"cmp"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"os" "os"
"sort" "slices"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
) )
type GeneralResponse struct { type GeneralResponse struct {
@ -39,54 +39,82 @@ type GeneralResponse struct {
Msg string Msg string
} }
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
helper.Router.HandleFunc("/healthz", svr.healthz)
subRouter := helper.Router.NewRoute().Subrouter()
subRouter.Use(helper.AuthMiddleware.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(helper.AssetsFS)).Methods("GET")
subRouter.PathPrefix("/static/").Handler(
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
).Methods("GET")
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
})
}
// /healthz // /healthz
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) { func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
} }
// GET /api/reload // GET /api/reload
func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
strictConfigMode := false
strictStr := r.URL.Query().Get("strictConfig")
if strictStr != "" {
strictConfigMode, _ = strconv.ParseBool(strictStr)
}
log.Info("api request [/api/reload]") log.Infof("api request [/api/reload]")
defer func() { defer func() {
log.Info("api response [/api/reload], code [%d]", res.Code) log.Infof("api response [/api/reload], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))
} }
}() }()
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile) cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
if err != nil { if err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) log.Warnf("reload frpc proxy config error: %s", res.Msg)
return return
} }
if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil { if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) log.Warnf("reload frpc proxy config error: %s", res.Msg)
return return
} }
if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
res.Code = 500 res.Code = 500
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) log.Warnf("reload frpc proxy config error: %s", res.Msg)
return return
} }
log.Info("success reload conf") log.Infof("success reload conf")
} }
// POST /api/stop // POST /api/stop
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
log.Info("api request [/api/stop]") log.Infof("api request [/api/stop]")
defer func() { defer func() {
log.Info("api response [/api/stop], code [%d]", res.Code) log.Infof("api response [/api/stop], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))
@ -123,7 +151,7 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
if status.Err == "" { if status.Err == "" {
psr.RemoteAddr = status.RemoteAddr psr.RemoteAddr = status.RemoteAddr
if lo.Contains([]string{"tcp", "udp"}, status.Type) { if slices.Contains([]string{"tcp", "udp"}, status.Type) {
psr.RemoteAddr = serverAddr + psr.RemoteAddr psr.RemoteAddr = serverAddr + psr.RemoteAddr
} }
} }
@ -137,24 +165,31 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
res StatusResp = make(map[string][]ProxyStatusResp) res StatusResp = make(map[string][]ProxyStatusResp)
) )
log.Info("Http request [/api/status]") log.Infof("http request [/api/status]")
defer func() { defer func() {
log.Info("Http response [/api/status]") log.Infof("http response [/api/status]")
buf, _ = json.Marshal(&res) buf, _ = json.Marshal(&res)
_, _ = w.Write(buf) _, _ = w.Write(buf)
}() }()
ps := svr.ctl.pm.GetAllProxyStatus() svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock()
if ctl == nil {
return
}
ps := ctl.pm.GetAllProxyStatus()
for _, status := range ps { for _, status := range ps {
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr)) res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
} }
for _, arrs := range res { for _, arrs := range res {
if len(arrs) <= 1 { if len(arrs) <= 1 {
continue continue
} }
sort.Slice(arrs, func(i, j int) bool { slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
return strings.Compare(arrs[i].Name, arrs[j].Name) < 0 return cmp.Compare(a.Name, b.Name)
}) })
} }
} }
@ -163,27 +198,27 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
log.Info("Http get request [/api/config]") log.Infof("http get request [/api/config]")
defer func() { defer func() {
log.Info("Http get response [/api/config], code [%d]", res.Code) log.Infof("http get response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))
} }
}() }()
if svr.cfgFile == "" { if svr.configFilePath == "" {
res.Code = 400 res.Code = 400
res.Msg = "frpc has no config file path" res.Msg = "frpc has no config file path"
log.Warn("%s", res.Msg) log.Warnf("%s", res.Msg)
return return
} }
content, err := os.ReadFile(svr.cfgFile) content, err := os.ReadFile(svr.configFilePath)
if err != nil { if err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("load frpc config file error: %s", res.Msg) log.Warnf("load frpc config file error: %s", res.Msg)
return return
} }
res.Msg = string(content) res.Msg = string(content)
@ -193,9 +228,9 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
log.Info("Http put request [/api/config]") log.Infof("http put request [/api/config]")
defer func() { defer func() {
log.Info("Http put response [/api/config], code [%d]", res.Code) log.Infof("http put response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))
@ -207,21 +242,21 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
res.Code = 400 res.Code = 400
res.Msg = fmt.Sprintf("read request body error: %v", err) res.Msg = fmt.Sprintf("read request body error: %v", err)
log.Warn("%s", res.Msg) log.Warnf("%s", res.Msg)
return return
} }
if len(body) == 0 { if len(body) == 0 {
res.Code = 400 res.Code = 400
res.Msg = "body can't be empty" res.Msg = "body can't be empty"
log.Warn("%s", res.Msg) log.Warnf("%s", res.Msg)
return return
} }
if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil { if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
res.Code = 500 res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err) res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warn("%s", res.Msg) log.Warnf("%s", res.Msg)
return return
} }
} }

227
client/connector.go Normal file
View File

@ -0,0 +1,227 @@
// 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 client
import (
"context"
"crypto/tls"
"io"
"net"
"strconv"
"strings"
"sync"
"time"
libnet "github.com/fatedier/golib/net"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
// Connector is an interface for establishing connections to the server.
type Connector interface {
Open() error
Connect() (net.Conn, error)
Close() error
}
// defaultConnectorImpl is the default implementation of Connector for normal frpc.
type defaultConnectorImpl struct {
ctx context.Context
cfg *v1.ClientCommonConfig
muxSession *fmux.Session
quicConn quic.Connection
closeOnce sync.Once
}
func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
return &defaultConnectorImpl{
ctx: ctx,
cfg: cfg,
}
}
// Open opens an underlying connection to the server.
// The underlying connection is either a TCP connection or a QUIC connection.
// After the underlying connection is established, you can call Connect() to get a stream.
// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect().
func (c *defaultConnectorImpl) Open() error {
xl := xlog.FromContextSafe(c.ctx)
// special for quic
if strings.EqualFold(c.cfg.Transport.Protocol, "quic") {
var tlsConfig *tls.Config
var err error
sn := c.cfg.Transport.TLS.ServerName
if sn == "" {
sn = c.cfg.ServerAddr
}
if lo.FromPtr(c.cfg.Transport.TLS.Enable) {
tlsConfig, err = transport.NewClientTLSConfig(
c.cfg.Transport.TLS.CertFile,
c.cfg.Transport.TLS.KeyFile,
c.cfg.Transport.TLS.TrustedCaFile,
sn)
} else {
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
}
if err != nil {
xl.Warnf("fail to build tls configuration, err: %v", err)
return err
}
tlsConfig.NextProtos = []string{"frp"}
conn, err := quic.DialAddr(
c.ctx,
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
tlsConfig, &quic.Config{
MaxIdleTimeout: time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams),
KeepAlivePeriod: time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
})
if err != nil {
return err
}
c.quicConn = conn
return nil
}
if !lo.FromPtr(c.cfg.Transport.TCPMux) {
return nil
}
conn, err := c.realConnect()
if err != nil {
return err
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(c.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
}
c.muxSession = session
return nil
}
// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
func (c *defaultConnectorImpl) Connect() (net.Conn, error) {
if c.quicConn != nil {
stream, err := c.quicConn.OpenStreamSync(context.Background())
if err != nil {
return nil, err
}
return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil
} else if c.muxSession != nil {
stream, err := c.muxSession.OpenStream()
if err != nil {
return nil, err
}
return stream, nil
}
return c.realConnect()
}
func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
xl := xlog.FromContextSafe(c.ctx)
var tlsConfig *tls.Config
var err error
tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable)
if c.cfg.Transport.Protocol == "wss" {
tlsEnable = true
}
if tlsEnable {
sn := c.cfg.Transport.TLS.ServerName
if sn == "" {
sn = c.cfg.ServerAddr
}
tlsConfig, err = transport.NewClientTLSConfig(
c.cfg.Transport.TLS.CertFile,
c.cfg.Transport.TLS.KeyFile,
c.cfg.Transport.TLS.TrustedCaFile,
sn)
if err != nil {
xl.Warnf("fail to build tls configuration, err: %v", err)
return nil, err
}
}
proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
if err != nil {
xl.Errorf("fail to parse proxy url")
return nil, err
}
dialOptions := []libnet.DialOption{}
protocol := c.cfg.Transport.Protocol
switch protocol {
case "websocket":
protocol = "tcp"
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
case "wss":
protocol = "tcp"
dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig))
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
}
if c.cfg.Transport.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libnet.WithProtocol(protocol),
libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
libnet.WithProxy(proxyType, addr),
libnet.WithProxyAuth(auth),
)
conn, err := libnet.DialContext(
c.ctx,
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
dialOptions...,
)
return conn, err
}
func (c *defaultConnectorImpl) Close() error {
c.closeOnce.Do(func() {
if c.quicConn != nil {
_ = c.quicConn.CloseWithError(0, "")
}
if c.muxSession != nil {
_ = c.muxSession.Close()
}
})
return nil
}

View File

@ -16,144 +16,142 @@ package client
import ( import (
"context" "context"
"io"
"net" "net"
"runtime/debug" "sync/atomic"
"time" "time"
"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/proxy"
"github.com/fatedier/frp/client/visitor" "github.com/fatedier/frp/client/visitor"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type SessionContext struct {
// The client common configuration.
Common *v1.ClientCommonConfig
// Unique ID obtained from frps.
// It should be attached to the login message when reconnecting.
RunID string
// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
Conn net.Conn
// Indicates whether the connection is encrypted.
ConnEncrypted bool
// Sets authentication based on selected method
AuthSetter auth.Setter
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
Connector Connector
// Virtual net controller
VnetController *vnet.Controller
}
type Control struct { type Control struct {
// service context // service context
ctx context.Context ctx context.Context
xl *xlog.Logger xl *xlog.Logger
// Unique ID obtained from frps. // session context
// It should be attached to the login message when reconnecting. sessionCtx *SessionContext
runID string
// manage all proxies // manage all proxies
pxyCfgs []v1.ProxyConfigurer pm *proxy.Manager
pm *proxy.Manager
// manage all visitors // manage all visitors
vm *visitor.Manager vm *visitor.Manager
// control connection doneCh chan struct{}
conn net.Conn
cm *ConnectionManager // of time.Time, last time got the Pong message
lastPong atomic.Value
// put a message in this channel to send it over control connection to server
sendCh chan (msg.Message)
// read from this channel to get the next message sent by server
readCh chan (msg.Message)
// goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed
closedCh chan struct{}
closedDoneCh chan struct{}
// last time got the Pong message
lastPong time.Time
// The client configuration
clientCfg *v1.ClientCommonConfig
readerShutdown *shutdown.Shutdown
writerShutdown *shutdown.Shutdown
msgHandlerShutdown *shutdown.Shutdown
// sets authentication based on selected method
authSetter auth.Setter
// The role of msgTransporter is similar to HTTP2.
// It allows multiple messages to be sent simultaneously on the same control connection.
// The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type.
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
// msgDispatcher is a wrapper for control connection.
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
msgDispatcher *msg.Dispatcher
} }
func NewControl( func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
clientCfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
authSetter auth.Setter,
) *Control {
// new xlog instance // new xlog instance
ctl := &Control{ ctl := &Control{
ctx: ctx, ctx: ctx,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
runID: runID, sessionCtx: sessionCtx,
conn: conn, doneCh: make(chan struct{}),
cm: cm,
pxyCfgs: pxyCfgs,
sendCh: make(chan msg.Message, 100),
readCh: make(chan msg.Message, 100),
closedCh: make(chan struct{}),
closedDoneCh: make(chan struct{}),
clientCfg: clientCfg,
readerShutdown: shutdown.New(),
writerShutdown: shutdown.New(),
msgHandlerShutdown: shutdown.New(),
authSetter: authSetter,
} }
ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) ctl.lastPong.Store(time.Now())
ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter) if sessionCtx.ConnEncrypted {
ctl.vm.Reload(visitorCfgs) cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
return ctl if err != nil {
return nil, err
}
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
} else {
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
}
ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
return ctl, nil
} }
func (ctl *Control) Run() { func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) {
go ctl.worker() go ctl.worker()
// start all proxies // start all proxies
ctl.pm.Reload(ctl.pxyCfgs) ctl.pm.UpdateAll(proxyCfgs)
// start all visitors // start all visitors
go ctl.vm.Run() ctl.vm.UpdateAll(visitorCfgs)
} }
func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) { func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
ctl.pm.SetInWorkConnCallback(cb)
}
func (ctl *Control) handleReqWorkConn(_ msg.Message) {
xl := ctl.xl xl := ctl.xl
workConn, err := ctl.connectServer() workConn, err := ctl.connectServer()
if err != nil { if err != nil {
xl.Warn("start new connection to server error: %v", err) xl.Warnf("start new connection to server error: %v", err)
return return
} }
m := &msg.NewWorkConn{ m := &msg.NewWorkConn{
RunID: ctl.runID, RunID: ctl.sessionCtx.RunID,
} }
if err = ctl.authSetter.SetNewWorkConn(m); err != nil { if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
xl.Warn("error during NewWorkConn authentication: %v", err) xl.Warnf("error during NewWorkConn authentication: %v", err)
workConn.Close()
return return
} }
if err = msg.WriteMsg(workConn, m); err != nil { if err = msg.WriteMsg(workConn, m); err != nil {
xl.Warn("work connection write to server error: %v", err) xl.Warnf("work connection write to server error: %v", err)
workConn.Close() workConn.Close()
return return
} }
var startMsg msg.StartWorkConn var startMsg msg.StartWorkConn
if err = msg.ReadMsgInto(workConn, &startMsg); err != nil { if err = msg.ReadMsgInto(workConn, &startMsg); err != nil {
xl.Trace("work connection closed before response StartWorkConn message: %v", err) xl.Tracef("work connection closed before response StartWorkConn message: %v", err)
workConn.Close() workConn.Close()
return return
} }
if startMsg.Error != "" { if startMsg.Error != "" {
xl.Error("StartWorkConn contains error: %s", startMsg.Error) xl.Errorf("StartWorkConn contains error: %s", startMsg.Error)
workConn.Close() workConn.Close()
return return
} }
@ -162,28 +160,49 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
} }
func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { func (ctl *Control) handleNewProxyResp(m msg.Message) {
xl := ctl.xl xl := ctl.xl
inMsg := m.(*msg.NewProxyResp)
// Server will return NewProxyResp message to each NewProxy message. // Server will return NewProxyResp message to each NewProxy message.
// Start a new proxy handler if no error got // Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
if err != nil { if err != nil {
xl.Warn("[%s] start error: %v", inMsg.ProxyName, err) xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
} else { } else {
xl.Info("[%s] start proxy success", inMsg.ProxyName) xl.Infof("[%s] start proxy success", inMsg.ProxyName)
} }
} }
func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) { func (ctl *Control) handleNatHoleResp(m msg.Message) {
xl := ctl.xl xl := ctl.xl
inMsg := m.(*msg.NatHoleResp)
// Dispatch the NatHoleResp message to the related proxy. // Dispatch the NatHoleResp message to the related proxy.
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID) ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
if !ok { if !ok {
xl.Trace("dispatch NatHoleResp message to related proxy error") xl.Tracef("dispatch NatHoleResp message to related proxy error")
} }
} }
func (ctl *Control) handlePong(m msg.Message) {
xl := ctl.xl
inMsg := m.(*msg.Pong)
if inMsg.Error != "" {
xl.Errorf("pong message contains error: %s", inMsg.Error)
ctl.closeSession()
return
}
ctl.lastPong.Store(time.Now())
xl.Debugf("receive heartbeat from server")
}
// closeSession closes the control connection.
func (ctl *Control) closeSession() {
ctl.sessionCtx.Conn.Close()
ctl.sessionCtx.Connector.Close()
}
func (ctl *Control) Close() error { func (ctl *Control) Close() error {
return ctl.GracefulClose(0) return ctl.GracefulClose(0)
} }
@ -194,170 +213,82 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
time.Sleep(d) time.Sleep(d)
ctl.conn.Close() ctl.closeSession()
ctl.cm.Close()
return nil return nil
} }
// ClosedDoneCh returns a channel that will be closed after all resources are released // Done returns a channel that will be closed after all resources are released
func (ctl *Control) ClosedDoneCh() <-chan struct{} { func (ctl *Control) Done() <-chan struct{} {
return ctl.closedDoneCh return ctl.doneCh
} }
// connectServer return a new connection to frps // connectServer return a new connection to frps
func (ctl *Control) connectServer() (conn net.Conn, err error) { func (ctl *Control) connectServer() (net.Conn, error) {
return ctl.cm.Connect() return ctl.sessionCtx.Connector.Connect()
} }
// reader read all messages from frps and send to readCh func (ctl *Control) registerMsgHandlers() {
func (ctl *Control) reader() { ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
xl := ctl.xl ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
defer func() { ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
if err := recover(); err != nil { ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
xl.Error("panic error: %v", err)
xl.Error(string(debug.Stack()))
}
}()
defer ctl.readerShutdown.Done()
defer close(ctl.closedCh)
encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
for {
m, err := msg.ReadMsg(encReader)
if err != nil {
if err == io.EOF {
xl.Debug("read from control connection EOF")
return
}
xl.Warn("read error: %v", err)
ctl.conn.Close()
return
}
ctl.readCh <- m
}
} }
// writer writes messages got from sendCh to frps // heartbeatWorker sends heartbeat to server and check heartbeat timeout.
func (ctl *Control) writer() { func (ctl *Control) heartbeatWorker() {
xl := ctl.xl xl := ctl.xl
defer ctl.writerShutdown.Done()
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()
return
}
for {
m, ok := <-ctl.sendCh
if !ok {
xl.Info("control writer is closing")
return
}
if err := msg.WriteMsg(encWriter, m); err != nil { if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
xl.Warn("write message to control connection error: %v", err) // Send heartbeat to server.
return sendHeartBeat := func() (bool, error) {
} xl.Debugf("send heartbeat to server")
}
}
// msgHandler handles all channel events and performs corresponding operations.
func (ctl *Control) msgHandler() {
xl := ctl.xl
defer func() {
if err := recover(); err != nil {
xl.Error("panic error: %v", err)
xl.Error(string(debug.Stack()))
}
}()
defer ctl.msgHandlerShutdown.Done()
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.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.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
}
ctl.lastPong = time.Now()
for {
select {
case <-hbSendCh:
// send heartbeat to server
xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{} pingMsg := &msg.Ping{}
if err := ctl.authSetter.SetPing(pingMsg); err != nil { if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
xl.Warn("error during ping authentication: %v", err) xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
return return false, err
}
ctl.sendCh <- pingMsg
case <-hbCheckCh:
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
// let reader() stop
ctl.conn.Close()
return
}
case rawMsg, ok := <-ctl.readCh:
if !ok {
return
}
switch m := rawMsg.(type) {
case *msg.ReqWorkConn:
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)
ctl.conn.Close()
return
}
ctl.lastPong = time.Now()
xl.Debug("receive heartbeat from server")
} }
_ = ctl.msgDispatcher.Send(pingMsg)
return false, nil
} }
go wait.BackoffUntil(sendHeartBeat,
wait.NewFastBackoffManager(wait.FastBackoffOptions{
Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
InitDurationIfFail: time.Second,
Factor: 2.0,
Jitter: 0.1,
MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
}),
true, ctl.doneCh,
)
}
// Check heartbeat timeout.
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
go wait.Until(func() {
if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
xl.Warnf("heartbeat timeout")
ctl.closeSession()
return
}
}, time.Second, ctl.doneCh)
} }
} }
// If controler is notified by closedCh, reader and writer and handler will exit
func (ctl *Control) worker() { func (ctl *Control) worker() {
go ctl.msgHandler() go ctl.heartbeatWorker()
go ctl.reader() go ctl.msgDispatcher.Run()
go ctl.writer()
<-ctl.closedCh <-ctl.msgDispatcher.Done()
// close related channels and wait until other goroutines done ctl.closeSession()
close(ctl.readCh)
ctl.readerShutdown.WaitDone()
ctl.msgHandlerShutdown.WaitDone()
close(ctl.sendCh)
ctl.writerShutdown.WaitDone()
ctl.pm.Close() ctl.pm.Close()
ctl.vm.Close() ctl.vm.Close()
close(ctl.doneCh)
close(ctl.closedDoneCh)
ctl.cm.Close()
} }
func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
ctl.vm.Reload(visitorCfgs) ctl.vm.UpdateAll(visitorCfgs)
ctl.pm.Reload(pxyCfgs) ctl.pm.UpdateAll(proxyCfgs)
return nil return nil
} }

View File

@ -8,7 +8,7 @@ import (
var ErrPayloadType = errors.New("error payload type") var ErrPayloadType = errors.New("error payload type")
type Handler func(payload interface{}) error type Handler func(payload any) error
type StartProxyPayload struct { type StartProxyPayload struct {
NewProxyMsg *msg.NewProxy NewProxyMsg *msg.NewProxy

View File

@ -40,8 +40,8 @@ type Monitor struct {
addr string addr string
// For http // For http
url string url string
header http.Header
failedTimes uint64 failedTimes uint64
statusOK bool statusOK bool
statusNormalFn func() statusNormalFn func()
@ -73,6 +73,11 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
} }
url = s + cfg.Path url = s + cfg.Path
} }
header := make(http.Header)
for _, h := range cfg.HTTPHeaders {
header.Set(h.Name, h.Value)
}
return &Monitor{ return &Monitor{
checkType: cfg.Type, checkType: cfg.Type,
interval: time.Duration(cfg.IntervalSeconds) * time.Second, interval: time.Duration(cfg.IntervalSeconds) * time.Second,
@ -80,6 +85,7 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
maxFailedTimes: cfg.MaxFailed, maxFailedTimes: cfg.MaxFailed,
addr: addr, addr: addr,
url: url, url: url,
header: header,
statusOK: false, statusOK: false,
statusNormalFn: statusNormalFn, statusNormalFn: statusNormalFn,
statusFailedFn: statusFailedFn, statusFailedFn: statusFailedFn,
@ -112,17 +118,17 @@ func (monitor *Monitor) checkWorker() {
} }
if err == nil { if err == nil {
xl.Trace("do one health check success") xl.Tracef("do one health check success")
if !monitor.statusOK && monitor.statusNormalFn != nil { if !monitor.statusOK && monitor.statusNormalFn != nil {
xl.Info("health check status change to success") xl.Infof("health check status change to success")
monitor.statusOK = true monitor.statusOK = true
monitor.statusNormalFn() monitor.statusNormalFn()
} }
} else { } else {
xl.Warn("do one health check failed: %v", err) xl.Warnf("do one health check failed: %v", err)
monitor.failedTimes++ monitor.failedTimes++
if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil { if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil {
xl.Warn("health check status change to failed") xl.Warnf("health check status change to failed")
monitor.statusOK = false monitor.statusOK = false
monitor.statusFailedFn() monitor.statusFailedFn()
} }
@ -163,6 +169,8 @@ func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
req.Header = monitor.header
req.Host = monitor.header.Get("Host")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err

View File

@ -20,13 +20,11 @@ import (
"net" "net"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
libdial "github.com/fatedier/golib/net/dial" libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/config/types"
@ -35,7 +33,9 @@ import (
plugin "github.com/fatedier/frp/pkg/plugin/client" plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{} var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
@ -47,10 +47,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v
// Proxy defines how to handle work connections for different proxy type. // Proxy defines how to handle work connections for different proxy type.
type Proxy interface { type Proxy interface {
Run() error Run() error
// InWorkConn accept work connections registered to server. // InWorkConn accept work connections registered to server.
InWorkConn(net.Conn, *msg.StartWorkConn) InWorkConn(net.Conn, *msg.StartWorkConn)
SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
Close() Close()
} }
@ -59,6 +58,7 @@ func NewProxy(
pxyConf v1.ProxyConfigurer, pxyConf v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) (pxy Proxy) { ) (pxy Proxy) {
var limiter *rate.Limiter var limiter *rate.Limiter
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
@ -71,6 +71,7 @@ func NewProxy(
clientCfg: clientCfg, clientCfg: clientCfg,
limiter: limiter, limiter: limiter,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
ctx: ctx, ctx: ctx,
} }
@ -86,10 +87,12 @@ type BaseProxy struct {
baseCfg *v1.ProxyBaseConfig baseCfg *v1.ProxyBaseConfig
clientCfg *v1.ClientCommonConfig clientCfg *v1.ClientCommonConfig
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
vnetController *vnet.Controller
limiter *rate.Limiter limiter *rate.Limiter
// proxyPlugin is used to handle connections instead of dialing to local service. // proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now. // It's only validate for TCP protocol now.
proxyPlugin plugin.Plugin proxyPlugin plugin.Plugin
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool
mu sync.RWMutex mu sync.RWMutex
xl *xlog.Logger xl *xlog.Logger
@ -98,7 +101,10 @@ type BaseProxy struct {
func (pxy *BaseProxy) Run() error { func (pxy *BaseProxy) Run() error {
if pxy.baseCfg.Plugin.Type != "" { if pxy.baseCfg.Plugin.Type != "" {
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions) p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
Name: pxy.baseCfg.Name,
VnetController: pxy.vnetController,
}, pxy.baseCfg.Plugin.ClientPluginOptions)
if err != nil { if err != nil {
return err return err
} }
@ -113,7 +119,16 @@ func (pxy *BaseProxy) Close() {
} }
} }
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
pxy.inWorkConnCallback = cb
}
func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
if pxy.inWorkConnCallback != nil {
if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) {
return
}
}
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token)) pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
} }
@ -132,13 +147,13 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
}) })
} }
xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t", xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
if baseCfg.Transport.UseEncryption { if baseCfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, encKey) remote, err = libio.WithEncryption(remote, encKey)
if err != nil { if err != nil {
workConn.Close() workConn.Close()
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return return
} }
} }
@ -148,69 +163,58 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
} }
// check if we need to send proxy protocol info // check if we need to send proxy protocol info
var extraInfo plugin.ExtraInfo var connInfo plugin.ConnectionInfo
if baseCfg.Transport.ProxyProtocolVersion != "" { if m.SrcAddr != "" && m.SrcPort != 0 {
if m.SrcAddr != "" && m.SrcPort != 0 { if m.DstAddr == "" {
if m.DstAddr == "" { m.DstAddr = "127.0.0.1"
m.DstAddr = "127.0.0.1"
}
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
h := &pp.Header{
Command: pp.PROXY,
SourceAddr: srcAddr,
DestinationAddr: dstAddr,
}
if strings.Contains(m.SrcAddr, ".") {
h.TransportProtocol = pp.TCPv4
} else {
h.TransportProtocol = pp.TCPv6
}
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
h.Version = 1
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
extraInfo.ProxyProtocolHeader = h
} }
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
connInfo.SrcAddr = srcAddr
connInfo.DstAddr = dstAddr
} }
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
// Use the common proxy protocol builder function
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
connInfo.ProxyProtocolHeader = header
}
connInfo.Conn = remote
connInfo.UnderlyingConn = workConn
if pxy.proxyPlugin != nil { if pxy.proxyPlugin != nil {
// if plugin is set, let plugin handle connection first // if plugin is set, let plugin handle connection first
xl.Debug("handle by plugin: %s", pxy.proxyPlugin.Name()) xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
pxy.proxyPlugin.Handle(remote, workConn, &extraInfo) pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
xl.Debug("handle by plugin finished") xl.Debugf("handle by plugin finished")
return return
} }
localConn, err := libdial.Dial( localConn, err := libnet.Dial(
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)), net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
libdial.WithTimeout(10*time.Second), libnet.WithTimeout(10*time.Second),
) )
if err != nil { if err != nil {
workConn.Close() workConn.Close()
xl.Error("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err) xl.Errorf("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
return return
} }
xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
if extraInfo.ProxyProtocolHeader != nil { if connInfo.ProxyProtocolHeader != nil {
if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
workConn.Close() workConn.Close()
xl.Error("write proxy protocol header to local conn error: %v", err) xl.Errorf("write proxy protocol header to local conn error: %v", err)
return return
} }
} }
_, _, errs := libio.Join(localConn, remote) _, _, errs := libio.Join(localConn, remote)
xl.Debug("join connections closed") xl.Debugf("join connections closed")
if len(errs) > 0 { if len(errs) > 0 {
xl.Trace("join connections errors: %v", errs) xl.Tracef("join connections errors: %v", errs)
} }
if compressionResourceRecycleFn != nil { if compressionResourceRecycleFn != nil {
compressionResourceRecycleFn() compressionResourceRecycleFn()

View File

@ -28,11 +28,14 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type Manager struct { type Manager struct {
proxies map[string]*Wrapper proxies map[string]*Wrapper
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
vnetController *vnet.Controller
closed bool closed bool
mu sync.RWMutex mu sync.RWMutex
@ -46,10 +49,12 @@ func NewManager(
ctx context.Context, ctx context.Context,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Manager { ) *Manager {
return &Manager{ return &Manager{
proxies: make(map[string]*Wrapper), proxies: make(map[string]*Wrapper),
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
closed: false, closed: false,
clientCfg: clientCfg, clientCfg: clientCfg,
ctx: ctx, ctx: ctx,
@ -71,6 +76,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri
return nil return nil
} }
func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
pm.inWorkConnCallback = cb
}
func (pm *Manager) Close() { func (pm *Manager) Close() {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
@ -91,7 +100,7 @@ func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWo
} }
} }
func (pm *Manager) HandleEvent(payload interface{}) error { func (pm *Manager) HandleEvent(payload any) error {
var m msg.Message var m msg.Message
switch e := payload.(type) { switch e := payload.(type) {
case *event.StartProxyPayload: case *event.StartProxyPayload:
@ -115,9 +124,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
return ps return ps
} }
func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) { func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
pm.mu.RLock()
defer pm.mu.RUnlock()
if pxy, ok := pm.proxies[name]; ok {
return pxy.GetStatus(), true
}
return nil, false
}
func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
xl := xlog.FromContextSafe(pm.ctx) xl := xlog.FromContextSafe(pm.ctx)
pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string { proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
return c.GetBaseConfig().Name return c.GetBaseConfig().Name
}) })
pm.mu.Lock() pm.mu.Lock()
@ -126,7 +144,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
delPxyNames := make([]string, 0) delPxyNames := make([]string, 0)
for name, pxy := range pm.proxies { for name, pxy := range pm.proxies {
del := false del := false
cfg, ok := pxyCfgsMap[name] cfg, ok := proxyCfgsMap[name]
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) { if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
del = true del = true
} }
@ -138,14 +156,17 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
} }
} }
if len(delPxyNames) > 0 { if len(delPxyNames) > 0 {
xl.Info("proxy removed: %s", delPxyNames) xl.Infof("proxy removed: %s", delPxyNames)
} }
addPxyNames := make([]string, 0) addPxyNames := make([]string, 0)
for _, cfg := range pxyCfgs { for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok { if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter) pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
if pm.inWorkConnCallback != nil {
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
}
pm.proxies[name] = pxy pm.proxies[name] = pxy
addPxyNames = append(addPxyNames, name) addPxyNames = append(addPxyNames, name)
@ -153,6 +174,6 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
} }
} }
if len(addPxyNames) > 0 { if len(addPxyNames) > 0 {
xl.Info("proxy added: %s", addPxyNames) xl.Infof("proxy added: %s", addPxyNames)
} }
} }

View File

@ -31,6 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
const ( const (
@ -73,6 +74,8 @@ type Wrapper struct {
handler event.Handler handler event.Handler
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
// vnet controller
vnetController *vnet.Controller
health uint32 health uint32
lastSendStartMsg time.Time lastSendStartMsg time.Time
@ -91,6 +94,7 @@ func NewWrapper(
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
eventHandler event.Handler, eventHandler event.Handler,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Wrapper { ) *Wrapper {
baseInfo := cfg.GetBaseConfig() baseInfo := cfg.GetBaseConfig()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
@ -105,6 +109,7 @@ func NewWrapper(
healthNotifyCh: make(chan struct{}), healthNotifyCh: make(chan struct{}),
handler: eventHandler, handler: eventHandler,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
xl: xl, xl: xl,
ctx: xlog.NewContext(ctx, xl), ctx: xlog.NewContext(ctx, xl),
} }
@ -114,13 +119,17 @@ func NewWrapper(
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort)) addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr, pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
pw.statusNormalCallback, pw.statusFailedCallback) pw.statusNormalCallback, pw.statusFailedCallback)
xl.Trace("enable health check monitor") xl.Tracef("enable health check monitor")
} }
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter) pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
return pw return pw
} }
func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
pw.pxy.SetInWorkConnCallback(cb)
}
func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error { func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
pw.mu.Lock() pw.mu.Lock()
defer pw.mu.Unlock() defer pw.mu.Unlock()
@ -133,7 +142,7 @@ func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
pw.Phase = ProxyPhaseStartErr pw.Phase = ProxyPhaseStartErr
pw.Err = respErr pw.Err = respErr
pw.lastStartErr = time.Now() pw.lastStartErr = time.Now()
return fmt.Errorf(pw.Err) return fmt.Errorf("%s", pw.Err)
} }
if err := pw.pxy.Run(); err != nil { if err := pw.pxy.Run(); err != nil {
@ -193,7 +202,7 @@ func (pw *Wrapper) checkWorker() {
(pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) || (pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) ||
(pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) { (pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) {
xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart) xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart)
pw.Phase = ProxyPhaseWaitStart pw.Phase = ProxyPhaseWaitStart
var newProxyMsg msg.NewProxy var newProxyMsg msg.NewProxy
@ -208,7 +217,7 @@ func (pw *Wrapper) checkWorker() {
pw.mu.Lock() pw.mu.Lock()
if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart { if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart {
pw.close() pw.close()
xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed) xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed)
pw.Phase = ProxyPhaseCheckFailed pw.Phase = ProxyPhaseCheckFailed
} }
pw.mu.Unlock() pw.mu.Unlock()
@ -232,7 +241,7 @@ func (pw *Wrapper) statusNormalCallback() {
default: default:
} }
}) })
xl.Info("health check success") xl.Infof("health check success")
} }
func (pw *Wrapper) statusFailedCallback() { func (pw *Wrapper) statusFailedCallback() {
@ -244,7 +253,7 @@ func (pw *Wrapper) statusFailedCallback() {
default: default:
} }
}) })
xl.Info("health check failed") xl.Infof("health check failed")
} }
func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) { func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
@ -253,7 +262,7 @@ func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
pxy := pw.pxy pxy := pw.pxy
pw.mu.RUnlock() pw.mu.RUnlock()
if pxy != nil && pw.Phase == ProxyPhaseRunning { if pxy != nil && pw.Phase == ProxyPhaseRunning {
xl.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String()) xl.Debugf("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
go pxy.InWorkConn(workConn, m) go pxy.InWorkConn(workConn, m)
} else { } else {
workConn.Close() workConn.Close()

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -29,7 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -79,7 +81,7 @@ func (pxy *SUDPProxy) Close() {
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl xl := pxy.xl
xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String()) xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
var rwc io.ReadWriteCloser = conn var rwc io.ReadWriteCloser = conn
var err error var err error
@ -92,14 +94,14 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
if err != nil { if err != nil {
conn.Close() conn.Close()
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return return
} }
} }
if pxy.cfg.Transport.UseCompression { if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc) rwc = libio.WithCompression(rwc)
} }
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn workConn := conn
readCh := make(chan *msg.UDPPacket, 1024) readCh := make(chan *msg.UDPPacket, 1024)
@ -131,21 +133,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
// first to check sudp proxy is closed or not // first to check sudp proxy is closed or not
select { select {
case <-pxy.closeCh: case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed") xl.Tracef("frpc sudp proxy is closed")
return return
default: default:
} }
var udpMsg msg.UDPPacket var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil { if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for sudp error: %v", errRet) xl.Warnf("read from workConn for sudp error: %v", errRet)
return return
} }
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
readCh <- &udpMsg readCh <- &udpMsg
}); errRet != nil { }); errRet != nil {
xl.Warn("reader goroutine for sudp work connection closed: %v", errRet) xl.Warnf("reader goroutine for sudp work connection closed: %v", errRet)
return return
} }
} }
@ -155,21 +157,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) { workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() { defer func() {
closeFn() closeFn()
xl.Info("writer goroutine for sudp work connection closed") xl.Infof("writer goroutine for sudp work connection closed")
}() }()
var errRet error var errRet error
for rawMsg := range sendCh { for rawMsg := range sendCh {
switch m := rawMsg.(type) { switch m := rawMsg.(type) {
case *msg.UDPPacket: case *msg.UDPPacket:
xl.Trace("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]", xl.Tracef("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()) m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
case *msg.Ping: case *msg.Ping:
xl.Trace("frpc send ping message to frpc visitor") xl.Tracef("frpc send ping message to frpc visitor")
} }
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil { if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("sudp work write error: %v", errRet) xl.Errorf("sudp work write error: %v", errRet)
return return
} }
} }
@ -189,11 +191,11 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if errRet = errors.PanicToError(func() { if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{} sendCh <- &msg.Ping{}
}); errRet != nil { }); errRet != nil {
xl.Warn("heartbeat goroutine for sudp work connection closed") xl.Warnf("heartbeat goroutine for sudp work connection closed")
return return
} }
case <-pxy.closeCh: case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed") xl.Tracef("frpc sudp proxy is closed")
return return
} }
} }
@ -203,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnReaderFn(workConn, readCh) go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh) go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize)) udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -28,7 +30,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -88,8 +90,8 @@ func (pxy *UDPProxy) Close() {
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
// close resources releated with old workConn // close resources related with old workConn
pxy.Close() pxy.Close()
var rwc io.ReadWriteCloser = conn var rwc io.ReadWriteCloser = conn
@ -103,14 +105,14 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
if err != nil { if err != nil {
conn.Close() conn.Close()
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return return
} }
} }
if pxy.cfg.Transport.UseCompression { if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc) rwc = libio.WithCompression(rwc)
} }
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock() pxy.mu.Lock()
pxy.workConn = conn pxy.workConn = conn
@ -123,32 +125,32 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
for { for {
var udpMsg msg.UDPPacket var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil { if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for udp error: %v", errRet) xl.Warnf("read from workConn for udp error: %v", errRet)
return return
} }
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
xl.Trace("get udp package from workConn: %s", udpMsg.Content) xl.Tracef("get udp package from workConn: %s", udpMsg.Content)
readCh <- &udpMsg readCh <- &udpMsg
}); errRet != nil { }); errRet != nil {
xl.Info("reader goroutine for udp work connection closed: %v", errRet) xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
return return
} }
} }
} }
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) { workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() { defer func() {
xl.Info("writer goroutine for udp work connection closed") xl.Infof("writer goroutine for udp work connection closed")
}() }()
var errRet error var errRet error
for rawMsg := range sendCh { for rawMsg := range sendCh {
switch m := rawMsg.(type) { switch m := rawMsg.(type) {
case *msg.UDPPacket: case *msg.UDPPacket:
xl.Trace("send udp package to workConn: %s", m.Content) xl.Tracef("send udp package to workConn: %s", m.Content)
case *msg.Ping: case *msg.Ping:
xl.Trace("send ping message to udp workConn") xl.Tracef("send ping message to udp workConn")
} }
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil { if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("udp work write error: %v", errRet) xl.Errorf("udp work write error: %v", errRet)
return return
} }
} }
@ -160,7 +162,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if errRet = errors.PanicToError(func() { if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{} sendCh <- &msg.Ping{}
}); errRet != nil { }); errRet != nil {
xl.Trace("heartbeat goroutine for udp work connection closed") xl.Tracef("heartbeat goroutine for udp work connection closed")
break break
} }
} }
@ -169,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnSenderFn(pxy.workConn, pxy.sendCh) go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh) go workConnReaderFn(pxy.workConn, pxy.readCh)
go heartbeatFn(pxy.sendCh) go heartbeatFn(pxy.sendCh)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -27,7 +29,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -57,17 +59,17 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
var natHoleSidMsg msg.NatHoleSid var natHoleSidMsg msg.NatHoleSid
err := msg.ReadMsgInto(conn, &natHoleSidMsg) err := msg.ReadMsgInto(conn, &natHoleSidMsg)
if err != nil { if err != nil {
xl.Error("xtcp read from workConn error: %v", err) xl.Errorf("xtcp read from workConn error: %v", err)
return return
} }
xl.Trace("nathole prepare start") xl.Tracef("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}) prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
if err != nil { if err != nil {
xl.Warn("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
defer prepareResult.ListenConn.Close() defer prepareResult.ListenConn.Close()
@ -81,14 +83,14 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
AssistedAddrs: prepareResult.AssistedAddrs, AssistedAddrs: prepareResult.AssistedAddrs,
} }
xl.Trace("nathole exchange info start") xl.Tracef("nathole exchange info start")
natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second) natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
if err != nil { if err != nil {
xl.Warn("nathole exchange info error: %v", err) xl.Warnf("nathole exchange info error: %v", err)
return return
} }
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v", xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs, natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
@ -96,7 +98,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey)) newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey))
if err != nil { if err != nil {
listenConn.Close() listenConn.Close()
xl.Warn("make hole error: %v", err) xl.Warnf("make hole error: %v", err)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{ _ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid, Sid: natHoleRespMsg.Sid,
Success: false, Success: false,
@ -104,7 +106,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
return return
} }
listenConn = newListenConn listenConn = newListenConn
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr) xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{ _ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid, Sid: natHoleRespMsg.Sid,
@ -126,14 +128,14 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String()) laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
lConn, err := net.DialUDP("udp", laddr, raddr) lConn, err := net.DialUDP("udp", laddr, raddr)
if err != nil { if err != nil {
xl.Warn("dial udp error: %v", err) xl.Warnf("dial udp error: %v", err)
return return
} }
defer lConn.Close() defer lConn.Close()
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String()) remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil { if err != nil {
xl.Warn("create kcp connection from udp connection error: %v", err) xl.Warnf("create kcp connection from udp connection error: %v", err)
return return
} }
@ -143,7 +145,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
session, err := fmux.Server(remote, fmuxCfg) session, err := fmux.Server(remote, fmuxCfg)
if err != nil { if err != nil {
xl.Error("create mux session error: %v", err) xl.Errorf("create mux session error: %v", err)
return return
} }
defer session.Close() defer session.Close()
@ -151,7 +153,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
for { for {
muxConn, err := session.Accept() muxConn, err := session.Accept()
if err != nil { if err != nil {
xl.Error("accept connection error: %v", err) xl.Errorf("accept connection error: %v", err)
return return
} }
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey)) go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey))
@ -164,7 +166,7 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
tlsConfig, err := transport.NewServerTLSConfig("", "", "") tlsConfig, err := transport.NewServerTLSConfig("", "", "")
if err != nil { if err != nil {
xl.Warn("create tls config error: %v", err) xl.Warnf("create tls config error: %v", err)
return return
} }
tlsConfig.NextProtos = []string{"frp"} tlsConfig.NextProtos = []string{"frp"}
@ -176,22 +178,22 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
}, },
) )
if err != nil { if err != nil {
xl.Warn("dial quic error: %v", err) xl.Warnf("dial quic error: %v", err)
return return
} }
// only accept one connection from raddr // only accept one connection from raddr
c, err := quicListener.Accept(pxy.ctx) c, err := quicListener.Accept(pxy.ctx)
if err != nil { if err != nil {
xl.Error("quic accept connection error: %v", err) xl.Errorf("quic accept connection error: %v", err)
return return
} }
for { for {
stream, err := c.AcceptStream(pxy.ctx) stream, err := c.AcceptStream(pxy.ctx)
if err != nil { if err != nil {
xl.Debug("quic accept stream error: %v", err) xl.Debugf("quic accept stream error: %v", err)
_ = c.CloseWithError(0, "") _ = c.CloseWithError(0, "")
return return
} }
go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey)) go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
} }
} }

View File

@ -16,245 +16,250 @@ package client
import ( import (
"context" "context"
"crypto/tls" "errors"
"fmt" "fmt"
"io"
"net" "net"
"os"
"runtime" "runtime"
"strconv"
"strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
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/samber/lo"
"github.com/fatedier/frp/assets" "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "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/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
func init() { func init() {
crypto.DefaultSalt = "frp" crypto.DefaultSalt = "frp"
// Disable quic-go's receive buffer warning.
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
os.Setenv("QUIC_GO_DISABLE_ECN", "true")
}
} }
// Service is a client service. type cancelErr struct {
type Service struct { Err error
// uniq id got from frps, attach it in loginMsg }
runID string
// manager control connection with server func (e cancelErr) Error() string {
ctl *Control return e.Err.Error()
}
// ServiceOptions contains options for creating a new client service.
type ServiceOptions struct {
Common *v1.ClientCommonConfig
ProxyCfgs []v1.ProxyConfigurer
VisitorCfgs []v1.VisitorConfigurer
// ConfigFilePath is the path to the configuration file used to initialize.
// If it is empty, it means that the configuration file is not used for initialization.
// It may be initialized using command line parameters or called directly.
ConfigFilePath string
// ClientSpec is the client specification that control the client behavior.
ClientSpec *msg.ClientSpec
// ConnectorCreator is a function that creates a new connector to make connections to the server.
// The Connector shields the underlying connection details, whether it is through TCP or QUIC connection,
// and regardless of whether multiplexing is used.
//
// If it is not set, the default frpc connector will be used.
// By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps
// through a pipe instead of a real physical connection.
ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
// HandleWorkConnCb is a callback function that is called when a new work connection is created.
//
// If it is not set, the default frpc implementation will be used.
HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
}
// setServiceOptionsDefault sets the default values for ServiceOptions.
func setServiceOptionsDefault(options *ServiceOptions) {
if options.Common != nil {
options.Common.Complete()
}
if options.ConnectorCreator == nil {
options.ConnectorCreator = NewConnector
}
}
// Service is the client service that connects to frps and provides proxy services.
type Service struct {
ctlMu sync.RWMutex ctlMu sync.RWMutex
// manager control connection with server
ctl *Control
// Uniq id got from frps, it will be attached to loginMsg.
runID string
// Sets authentication based on selected method // Sets authentication based on selected method
authSetter auth.Setter authSetter auth.Setter
cfg *v1.ClientCommonConfig // web server for admin UI and apis
pxyCfgs []v1.ProxyConfigurer webServer *httppkg.Server
visitorCfgs []v1.VisitorConfigurer
vnetController *vnet.Controller
cfgMu sync.RWMutex cfgMu sync.RWMutex
common *v1.ClientCommonConfig
proxyCfgs []v1.ProxyConfigurer
visitorCfgs []v1.VisitorConfigurer
clientSpec *msg.ClientSpec
// The configuration file used to initialize this client, or an empty // The configuration file used to initialize this client, or an empty
// string if no configuration file was used. // string if no configuration file was used.
cfgFile string configFilePath string
exit uint32 // 0 means not exit
// service context // service context
ctx context.Context ctx context.Context
// call cancel to stop service // call cancel to stop service
cancel context.CancelFunc cancel context.CancelCauseFunc
gracefulShutdownDuration time.Duration
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
} }
func NewService( func NewService(options ServiceOptions) (*Service, error) {
cfg *v1.ClientCommonConfig, setServiceOptionsDefault(&options)
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer, var webServer *httppkg.Server
cfgFile string, if options.Common.WebServer.Port > 0 {
) (svr *Service, err error) { ws, err := httppkg.NewServer(options.Common.WebServer)
svr = &Service{ if err != nil {
authSetter: auth.NewAuthSetter(cfg.Auth), return nil, err
cfg: cfg, }
cfgFile: cfgFile, webServer = ws
pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs,
ctx: context.Background(),
exit: 0,
} }
return s := &Service{
} ctx: context.Background(),
authSetter: auth.NewAuthSetter(options.Common.Auth),
func (svr *Service) GetController() *Control { webServer: webServer,
svr.ctlMu.RLock() common: options.Common,
defer svr.ctlMu.RUnlock() configFilePath: options.ConfigFilePath,
return svr.ctl proxyCfgs: options.ProxyCfgs,
visitorCfgs: options.VisitorCfgs,
clientSpec: options.ClientSpec,
connectorCreator: options.ConnectorCreator,
handleWorkConnCb: options.HandleWorkConnCb,
}
if webServer != nil {
webServer.RouteRegister(s.registerRouteHandlers)
}
if options.Common.VirtualNet.Address != "" {
s.vnetController = vnet.NewController(options.Common.VirtualNet)
}
return s, nil
} }
func (svr *Service) Run(ctx context.Context) error { func (svr *Service) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancelCause(ctx)
svr.ctx = xlog.NewContext(ctx, xlog.New()) svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
svr.cancel = cancel svr.cancel = cancel
xl := xlog.FromContextSafe(svr.ctx)
// set custom DNSServer // set custom DNSServer
if svr.cfg.DNSServer != "" { if svr.common.DNSServer != "" {
dnsAddr := svr.cfg.DNSServer netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
if _, _, err := net.SplitHostPort(dnsAddr); err != nil {
dnsAddr = net.JoinHostPort(dnsAddr, "53")
}
// Change default dns server for frpc
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("udp", dnsAddr)
},
}
} }
// login to frps if svr.vnetController != nil {
for { if err := svr.vnetController.Init(); err != nil {
conn, cm, err := svr.login() log.Errorf("init virtual network controller error: %v", err)
if err != nil { return err
xl.Warn("login to server failed: %v", err)
// if login_fail_exit is true, just exit this program
// otherwise sleep a while and try again to connect to server
if lo.FromPtr(svr.cfg.LoginFailExit) {
return err
}
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.authSetter)
ctl.Run()
svr.ctlMu.Lock()
svr.ctl = ctl
svr.ctlMu.Unlock()
break
} }
go func() {
log.Infof("virtual network controller start...")
if err := svr.vnetController.Run(); err != nil {
log.Warnf("virtual network controller exit with error: %v", err)
}
}()
}
if svr.webServer != nil {
go func() {
log.Infof("admin server listen on %s", svr.webServer.Address())
if err := svr.webServer.Run(); err != nil {
log.Warnf("admin server exit with error: %v", err)
}
}()
}
// first login to frps
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
if svr.ctl == nil {
cancelCause := cancelErr{}
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
} }
go svr.keepControllerWorking() go svr.keepControllerWorking()
if svr.cfg.WebServer.Port != 0 {
// Init admin server assets
assets.Load(svr.cfg.WebServer.AssetsDir)
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.WebServer.Addr, svr.cfg.WebServer.Port)
}
<-svr.ctx.Done() <-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources svr.stop()
if atomic.LoadUint32(&svr.exit) == 0 {
svr.Close()
}
return nil return nil
} }
func (svr *Service) keepControllerWorking() { func (svr *Service) keepControllerWorking() {
xl := xlog.FromContextSafe(svr.ctx) <-svr.ctl.Done()
maxDelayTime := 20 * time.Second
delayTime := time.Second
// if frpc reconnect frps, we need to limit retry times in 1min // There is a situation where the login is successful but due to certain reasons,
// current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ... // the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
// when exceed 1min, we will reset delay and counts // The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
cutoffTime := time.Now().Add(time.Minute) // The maximum interval is 20 seconds.
reconnectDelay := time.Second wait.BackoffUntil(func() (bool, error) {
reconnectCounts := 1 // loopLoginUntilSuccess is another layer of loop that will continuously attempt to
// login to the server until successful.
for { svr.loopLoginUntilSuccess(20*time.Second, false)
<-svr.ctl.ClosedDoneCh() if svr.ctl != nil {
if atomic.LoadUint32(&svr.exit) != 0 { <-svr.ctl.Done()
return return false, errors.New("control is closed and try another loop")
} }
// If the control is nil, it means that the login failed and the service is also closed.
// the first three attempts with a low delay return false, nil
if reconnectCounts > 3 { }, wait.NewFastBackoffManager(
util.RandomSleep(reconnectDelay, 0.9, 1.1) wait.FastBackoffOptions{
xl.Info("wait %v to reconnect", reconnectDelay) Duration: time.Second,
reconnectDelay *= 2 Factor: 2,
} else { Jitter: 0.1,
util.RandomSleep(time.Second, 0, 0.5) MaxDuration: 20 * time.Second,
} FastRetryCount: 3,
reconnectCounts++ FastRetryDelay: 200 * time.Millisecond,
FastRetryWindow: time.Minute,
now := time.Now() FastRetryJitter: 0.5,
if now.After(cutoffTime) { },
// reset ), true, svr.ctx.Done())
cutoffTime = now.Add(time.Minute)
reconnectDelay = time.Second
reconnectCounts = 1
}
for {
if atomic.LoadUint32(&svr.exit) != 0 {
return
}
xl.Info("try to reconnect to server...")
conn, cm, err := svr.login()
if err != nil {
xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
util.RandomSleep(delayTime, 0.9, 1.1)
delayTime *= 2
if delayTime > maxDelayTime {
delayTime = maxDelayTime
}
continue
}
// reconnect success, init delayTime
delayTime = time.Second
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 {
svr.ctl.Close()
}
svr.ctl = ctl
svr.ctlMu.Unlock()
break
}
}
} }
// login creates a connection to frps and registers it self as a client // login creates a connection to frps and registers it self as a client
// conn: control connection // conn: control connection
// session: if it's not nil, using tcp mux // session: if it's not nil, using tcp mux
func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
xl := xlog.FromContextSafe(svr.ctx) xl := xlog.FromContextSafe(svr.ctx)
cm = NewConnectionManager(svr.ctx, svr.cfg) connector = svr.connectorCreator(svr.ctx, svr.common)
if err = connector.Open(); err != nil {
if err = cm.OpenConnection(); err != nil {
return nil, nil, err return nil, nil, err
} }
defer func() { defer func() {
if err != nil { if err != nil {
cm.Close() connector.Close()
} }
}() }()
conn, err = cm.Connect() conn, err = connector.Connect()
if err != nil { if err != nil {
return return
} }
@ -262,12 +267,15 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
loginMsg := &msg.Login{ loginMsg := &msg.Login{
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Os: runtime.GOOS, Os: runtime.GOOS,
PoolCount: svr.cfg.Transport.PoolCount, PoolCount: svr.common.Transport.PoolCount,
User: svr.cfg.User, User: svr.common.User,
Version: version.Full(), Version: version.Full(),
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
RunID: svr.runID, RunID: svr.runID,
Metas: svr.cfg.Metadatas, Metas: svr.common.Metadatas,
}
if svr.clientSpec != nil {
loginMsg.ClientSpec = *svr.clientSpec
} }
// Add auth // Add auth
@ -288,21 +296,79 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
if loginRespMsg.Error != "" { if loginRespMsg.Error != "" {
err = fmt.Errorf("%s", loginRespMsg.Error) err = fmt.Errorf("%s", loginRespMsg.Error)
xl.Error("%s", loginRespMsg.Error) xl.Errorf("%s", loginRespMsg.Error)
return return
} }
svr.runID = loginRespMsg.RunID svr.runID = loginRespMsg.RunID
xl.ResetPrefixes() xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
xl.AppendPrefix(svr.runID)
xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID) xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
return return
} }
func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
xl := xlog.FromContextSafe(svr.ctx)
loginFunc := func() (bool, error) {
xl.Infof("try to connect to server...")
conn, connector, err := svr.login()
if err != nil {
xl.Warnf("connect to server error: %v", err)
if firstLoginExit {
svr.cancel(cancelErr{Err: err})
}
return false, err
}
svr.cfgMu.RLock()
proxyCfgs := svr.proxyCfgs
visitorCfgs := svr.visitorCfgs
svr.cfgMu.RUnlock()
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
sessionCtx := &SessionContext{
Common: svr.common,
RunID: svr.runID,
Conn: conn,
ConnEncrypted: connEncrypted,
AuthSetter: svr.authSetter,
Connector: connector,
VnetController: svr.vnetController,
}
ctl, err := NewControl(svr.ctx, sessionCtx)
if err != nil {
conn.Close()
xl.Errorf("new control error: %v", err)
return false, err
}
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
ctl.Run(proxyCfgs, visitorCfgs)
// close and replace previous control
svr.ctlMu.Lock()
if svr.ctl != nil {
svr.ctl.Close()
}
svr.ctl = ctl
svr.ctlMu.Unlock()
return true, nil
}
// try to reconnect to server until success
wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
wait.FastBackoffOptions{
Duration: time.Second,
Factor: 2,
Jitter: 0.1,
MaxDuration: maxInterval,
}), true, svr.ctx.Done())
}
func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
svr.cfgMu.Lock() svr.cfgMu.Lock()
svr.pxyCfgs = pxyCfgs svr.proxyCfgs = proxyCfgs
svr.visitorCfgs = visitorCfgs svr.visitorCfgs = visitorCfgs
svr.cfgMu.Unlock() svr.cfgMu.Unlock()
@ -311,7 +377,7 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi
svr.ctlMu.RUnlock() svr.ctlMu.RUnlock()
if ctl != nil { if ctl != nil {
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs) return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
} }
return nil return nil
} }
@ -321,191 +387,44 @@ func (svr *Service) Close() {
} }
func (svr *Service) GracefulClose(d time.Duration) { func (svr *Service) GracefulClose(d time.Duration) {
atomic.StoreUint32(&svr.exit, 1) svr.gracefulShutdownDuration = d
svr.cancel(nil)
}
svr.ctlMu.RLock() func (svr *Service) stop() {
svr.ctlMu.Lock()
defer svr.ctlMu.Unlock()
if svr.ctl != nil { if svr.ctl != nil {
svr.ctl.GracefulClose(d) svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
svr.ctl = nil svr.ctl = nil
} }
}
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock() svr.ctlMu.RUnlock()
if svr.cancel != nil { if ctl == nil {
svr.cancel() return nil, false
}
return ctl.pm.GetProxyStatus(name)
}
func (svr *Service) StatusExporter() StatusExporter {
return &statusExporterImpl{
getProxyStatusFunc: svr.getProxyStatus,
} }
} }
type ConnectionManager struct { type StatusExporter interface {
ctx context.Context GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
cfg *v1.ClientCommonConfig
muxSession *fmux.Session
quicConn quic.Connection
} }
func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager { type statusExporterImpl struct {
return &ConnectionManager{ getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
ctx: ctx,
cfg: cfg,
}
} }
func (cm *ConnectionManager) OpenConnection() error { func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
xl := xlog.FromContextSafe(cm.ctx) return s.getProxyStatusFunc(name)
// special for quic
if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") {
var tlsConfig *tls.Config
var err error
sn := cm.cfg.Transport.TLS.ServerName
if sn == "" {
sn = cm.cfg.ServerAddr
}
if lo.FromPtr(cm.cfg.Transport.TLS.Enable) {
tlsConfig, err = transport.NewClientTLSConfig(
cm.cfg.Transport.TLS.CertFile,
cm.cfg.Transport.TLS.KeyFile,
cm.cfg.Transport.TLS.TrustedCaFile,
sn)
} else {
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
}
if err != nil {
xl.Warn("fail to build tls configuration, err: %v", err)
return err
}
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.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
}
cm.quicConn = conn
return nil
}
if !lo.FromPtr(cm.cfg.Transport.TCPMux) {
return nil
}
conn, err := cm.realConnect()
if err != nil {
return err
}
fmuxCfg := fmux.DefaultConfig()
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
}
cm.muxSession = session
return nil
}
func (cm *ConnectionManager) Connect() (net.Conn, error) {
if cm.quicConn != nil {
stream, err := cm.quicConn.OpenStreamSync(context.Background())
if err != nil {
return nil, err
}
return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil
} else if cm.muxSession != nil {
stream, err := cm.muxSession.OpenStream()
if err != nil {
return nil, err
}
return stream, nil
}
return cm.realConnect()
}
func (cm *ConnectionManager) realConnect() (net.Conn, error) {
xl := xlog.FromContextSafe(cm.ctx)
var tlsConfig *tls.Config
var err error
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.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)
return nil, err
}
}
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.Transport.Protocol
switch protocol {
case "websocket":
protocol = "tcp"
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.WithAfterHook(libdial.AfterHook{
Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
}
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.Transport.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
)
conn, err := libdial.DialContext(
cm.ctx,
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
dialOptions...,
)
return conn, err
}
func (cm *ConnectionManager) Close() error {
if cm.quicConn != nil {
_ = cm.quicConn.CloseWithError(0, "")
}
if cm.muxSession != nil {
_ = cm.muxSession.Close()
}
return nil
} }

View File

@ -44,6 +44,10 @@ func (sv *STCPVisitor) Run() (err error) {
} }
go sv.internalConnWorker() go sv.internalConnWorker()
if sv.plugin != nil {
sv.plugin.Start()
}
return return
} }
@ -56,7 +60,7 @@ func (sv *STCPVisitor) worker() {
for { for {
conn, err := sv.l.Accept() conn, err := sv.l.Accept()
if err != nil { if err != nil {
xl.Warn("stcp local listener closed") xl.Warnf("stcp local listener closed")
return return
} }
go sv.handleConn(conn) go sv.handleConn(conn)
@ -68,7 +72,7 @@ func (sv *STCPVisitor) internalConnWorker() {
for { for {
conn, err := sv.internalLn.Accept() conn, err := sv.internalLn.Accept()
if err != nil { if err != nil {
xl.Warn("stcp internal listener closed") xl.Warnf("stcp internal listener closed")
return return
} }
go sv.handleConn(conn) go sv.handleConn(conn)
@ -79,7 +83,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close() defer userConn.Close()
xl.Debug("get a new stcp user connection") xl.Debugf("get a new stcp user connection")
visitorConn, err := sv.helper.ConnectServer() visitorConn, err := sv.helper.ConnectServer()
if err != nil { if err != nil {
return return
@ -97,7 +101,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
} }
err = msg.WriteMsg(visitorConn, newVisitorConnMsg) err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil { if err != nil {
xl.Warn("send newVisitorConnMsg to server error: %v", err) xl.Warnf("send newVisitorConnMsg to server error: %v", err)
return return
} }
@ -105,13 +109,13 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil { if err != nil {
xl.Warn("get newVisitorConnRespMsg error: %v", err) xl.Warnf("get newVisitorConnRespMsg error: %v", err)
return return
} }
_ = visitorConn.SetReadDeadline(time.Time{}) _ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" { if newVisitorConnRespMsg.Error != "" {
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error) xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
return return
} }
@ -120,7 +124,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
if sv.cfg.Transport.UseEncryption { if sv.cfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return return
} }
} }

View File

@ -28,7 +28,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
@ -62,7 +62,7 @@ func (sv *SUDPVisitor) Run() (err error) {
sv.sendCh = make(chan *msg.UDPPacket, 1024) sv.sendCh = make(chan *msg.UDPPacket, 1024)
sv.readCh = make(chan *msg.UDPPacket, 1024) sv.readCh = make(chan *msg.UDPPacket, 1024)
xl.Info("sudp start to work, listen on %s", addr) xl.Infof("sudp start to work, listen on %s", addr)
go sv.dispatcher() go sv.dispatcher()
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize)) go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize))
@ -84,17 +84,17 @@ func (sv *SUDPVisitor) dispatcher() {
select { select {
case firstPacket = <-sv.sendCh: case firstPacket = <-sv.sendCh:
if firstPacket == nil { if firstPacket == nil {
xl.Info("frpc sudp visitor proxy is closed") xl.Infof("frpc sudp visitor proxy is closed")
return return
} }
case <-sv.checkCloseCh: case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed") xl.Infof("frpc sudp visitor proxy is closed")
return return
} }
visitorConn, err = sv.getNewVisitorConn() visitorConn, err = sv.getNewVisitorConn()
if err != nil { if err != nil {
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err) xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
continue continue
} }
@ -111,7 +111,7 @@ func (sv *SUDPVisitor) dispatcher() {
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker") xl.Debugf("starting sudp proxy worker")
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(2) wg.Add(2)
@ -134,21 +134,21 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
// frpc will send heartbeat in workConn to frpc visitor for keeping alive // frpc will send heartbeat in workConn to frpc visitor for keeping alive
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second)) _ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil { if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
xl.Warn("read from workconn for user udp conn error: %v", errRet) xl.Warnf("read from workconn for user udp conn error: %v", errRet)
return return
} }
_ = conn.SetReadDeadline(time.Time{}) _ = conn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) { switch m := rawMsg.(type) {
case *msg.Ping: case *msg.Ping:
xl.Debug("frpc visitor get ping message from frpc") xl.Debugf("frpc visitor get ping message from frpc")
continue continue
case *msg.UDPPacket: case *msg.UDPPacket:
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
sv.readCh <- m sv.readCh <- m
xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content) xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content)
}); errRet != nil { }); errRet != nil {
xl.Info("reader goroutine for udp work connection closed") xl.Infof("reader goroutine for udp work connection closed")
return return
} }
} }
@ -165,25 +165,25 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
var errRet error var errRet error
if firstPacket != nil { if firstPacket != nil {
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil { if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet) xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
return return
} }
xl.Trace("send udp package to workConn: %s", firstPacket.Content) xl.Tracef("send udp package to workConn: %s", firstPacket.Content)
} }
for { for {
select { select {
case udpMsg, ok := <-sv.sendCh: case udpMsg, ok := <-sv.sendCh:
if !ok { if !ok {
xl.Info("sender goroutine for udp work connection closed") xl.Infof("sender goroutine for udp work connection closed")
return return
} }
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil { if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet) xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
return return
} }
xl.Trace("send udp package to workConn: %s", udpMsg.Content) xl.Tracef("send udp package to workConn: %s", udpMsg.Content)
case <-closeCh: case <-closeCh:
return return
} }
@ -194,7 +194,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
go workConnSenderFn(workConn) go workConnSenderFn(workConn)
wg.Wait() wg.Wait()
xl.Info("sudp worker is closed") xl.Infof("sudp worker is closed")
} }
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
@ -235,14 +235,14 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
if sv.cfg.Transport.UseEncryption { if sv.cfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return nil, err return nil, err
} }
} }
if sv.cfg.Transport.UseCompression { if sv.cfg.Transport.UseCompression {
remote = libio.WithCompression(remote) remote = libio.WithCompression(remote)
} }
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
} }
func (sv *SUDPVisitor) Close() { func (sv *SUDPVisitor) Close() {

View File

@ -20,12 +20,14 @@ import (
"sync" "sync"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
// Helper wrapps some functions for visitor to use. // Helper wraps some functions for visitor to use.
type Helper interface { type Helper interface {
// ConnectServer directly connects to the frp server. // ConnectServer directly connects to the frp server.
ConnectServer() (net.Conn, error) ConnectServer() (net.Conn, error)
@ -34,6 +36,8 @@ type Helper interface {
// MsgTransporter returns the message transporter that is used to send and receive messages // MsgTransporter returns the message transporter that is used to send and receive messages
// to the frp server through the controller. // to the frp server through the controller.
MsgTransporter() transport.MessageTransporter MsgTransporter() transport.MessageTransporter
// VNetController returns the vnet controller that is used to manage the virtual network.
VNetController() *vnet.Controller
// RunID returns the run id of current controller. // RunID returns the run id of current controller.
RunID() string RunID() string
} }
@ -50,13 +54,33 @@ func NewVisitor(
cfg v1.VisitorConfigurer, cfg v1.VisitorConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
helper Helper, helper Helper,
) (visitor Visitor) { ) (Visitor, error) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name) xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
ctx = xlog.NewContext(ctx, xl)
var visitor Visitor
baseVisitor := BaseVisitor{ baseVisitor := BaseVisitor{
clientCfg: clientCfg, clientCfg: clientCfg,
helper: helper, helper: helper,
ctx: xlog.NewContext(ctx, xl), ctx: ctx,
internalLn: utilnet.NewInternalListener(), internalLn: netpkg.NewInternalListener(),
}
if cfg.GetBaseConfig().Plugin.Type != "" {
p, err := plugin.Create(
cfg.GetBaseConfig().Plugin.Type,
plugin.PluginContext{
Name: cfg.GetBaseConfig().Name,
Ctx: ctx,
VnetController: helper.VNetController(),
HandleConn: func(conn net.Conn) {
_ = baseVisitor.AcceptConn(conn)
},
},
cfg.GetBaseConfig().Plugin.VisitorPluginOptions,
)
if err != nil {
return nil, err
}
baseVisitor.plugin = p
} }
switch cfg := cfg.(type) { switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig: case *v1.STCPVisitorConfig:
@ -77,14 +101,15 @@ func NewVisitor(
checkCloseCh: make(chan struct{}), checkCloseCh: make(chan struct{}),
} }
} }
return return visitor, nil
} }
type BaseVisitor struct { type BaseVisitor struct {
clientCfg *v1.ClientCommonConfig clientCfg *v1.ClientCommonConfig
helper Helper helper Helper
l net.Listener l net.Listener
internalLn *utilnet.InternalListener internalLn *netpkg.InternalListener
plugin plugin.Plugin
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() {
if v.internalLn != nil { if v.internalLn != nil {
v.internalLn.Close() v.internalLn.Close()
} }
if v.plugin != nil {
v.plugin.Close()
}
} }

View File

@ -27,6 +27,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
) )
type Manager struct { type Manager struct {
@ -35,7 +36,8 @@ type Manager struct {
visitors map[string]Visitor visitors map[string]Visitor
helper Helper helper Helper
checkInterval time.Duration checkInterval time.Duration
keepVisitorsRunningOnce sync.Once
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@ -49,6 +51,7 @@ func NewManager(
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
connectServer func() (net.Conn, error), connectServer func() (net.Conn, error),
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
) *Manager { ) *Manager {
m := &Manager{ m := &Manager{
clientCfg: clientCfg, clientCfg: clientCfg,
@ -61,13 +64,16 @@ func NewManager(
m.helper = &visitorHelperImpl{ m.helper = &visitorHelperImpl{
connectServerFn: connectServer, connectServerFn: connectServer,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController,
transferConnFn: m.TransferConn, transferConnFn: m.TransferConn,
runID: runID, runID: runID,
} }
return m return m
} }
func (vm *Manager) Run() { // keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it.
// It will only start after Reload is called and a new visitor is added.
func (vm *Manager) keepVisitorsRunning() {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
ticker := time.NewTicker(vm.checkInterval) ticker := time.NewTicker(vm.checkInterval)
@ -76,14 +82,14 @@ func (vm *Manager) Run() {
for { for {
select { select {
case <-vm.stopCh: case <-vm.stopCh:
xl.Info("gracefully shutdown visitor manager") xl.Tracef("gracefully shutdown visitor manager")
return return
case <-ticker.C: case <-ticker.C:
vm.mu.Lock() vm.mu.Lock()
for _, cfg := range vm.cfgs { for _, cfg := range vm.cfgs {
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
if _, exist := vm.visitors[name]; !exist { if _, exist := vm.visitors[name]; !exist {
xl.Info("try to start visitor [%s]", name) xl.Infof("try to start visitor [%s]", name)
_ = vm.startVisitor(cfg) _ = vm.startVisitor(cfg)
} }
} }
@ -109,18 +115,29 @@ func (vm *Manager) Close() {
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper) visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
if err != nil {
xl.Warnf("new visitor error: %v", err)
return
}
err = visitor.Run() err = visitor.Run()
if err != nil { if err != nil {
xl.Warn("start error: %v", err) xl.Warnf("start error: %v", err)
} else { } else {
vm.visitors[name] = visitor vm.visitors[name] = visitor
xl.Info("start visitor success") xl.Infof("start visitor success")
} }
return return
} }
func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) { func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
if len(cfgs) > 0 {
// Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
vm.keepVisitorsRunningOnce.Do(func() {
go vm.keepVisitorsRunning()
})
}
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string { cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
return c.GetBaseConfig().Name return c.GetBaseConfig().Name
@ -146,7 +163,7 @@ func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
} }
} }
if len(delNames) > 0 { if len(delNames) > 0 {
xl.Info("visitor removed: %v", delNames) xl.Infof("visitor removed: %v", delNames)
} }
addNames := make([]string, 0) addNames := make([]string, 0)
@ -159,7 +176,7 @@ func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
} }
} }
if len(addNames) > 0 { if len(addNames) > 0 {
xl.Info("visitor added: %v", addNames) xl.Infof("visitor added: %v", addNames)
} }
} }
@ -177,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
type visitorHelperImpl struct { type visitorHelperImpl struct {
connectServerFn func() (net.Conn, error) connectServerFn func() (net.Conn, error)
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
vnetController *vnet.Controller
transferConnFn func(name string, conn net.Conn) error transferConnFn func(name string, conn net.Conn) error
runID string runID string
} }
@ -193,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
return v.msgTransporter return v.msgTransporter
} }
func (v *visitorHelperImpl) VNetController() *vnet.Controller {
return v.vnetController
}
func (v *visitorHelperImpl) RunID() string { func (v *visitorHelperImpl) RunID() string {
return v.runID return v.runID
} }

View File

@ -33,7 +33,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
@ -73,6 +73,10 @@ func (sv *XTCPVisitor) Run() (err error) {
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour) sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
go sv.keepTunnelOpenWorker() go sv.keepTunnelOpenWorker()
} }
if sv.plugin != nil {
sv.plugin.Start()
}
return return
} }
@ -93,7 +97,7 @@ func (sv *XTCPVisitor) worker() {
for { for {
conn, err := sv.l.Accept() conn, err := sv.l.Accept()
if err != nil { if err != nil {
xl.Warn("xtcp local listener closed") xl.Warnf("xtcp local listener closed")
return return
} }
go sv.handleConn(conn) go sv.handleConn(conn)
@ -105,7 +109,7 @@ func (sv *XTCPVisitor) internalConnWorker() {
for { for {
conn, err := sv.internalLn.Accept() conn, err := sv.internalLn.Accept()
if err != nil { if err != nil {
xl.Warn("xtcp internal listener closed") xl.Warnf("xtcp internal listener closed")
return return
} }
go sv.handleConn(conn) go sv.handleConn(conn)
@ -140,14 +144,14 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
case <-sv.ctx.Done(): case <-sv.ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
xl.Debug("keepTunnelOpenWorker try to check tunnel...") xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
conn, err := sv.getTunnelConn() conn, err := sv.getTunnelConn()
if err != nil { if err != nil {
xl.Warn("keepTunnelOpenWorker get tunnel connection error: %v", err) xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
_ = sv.retryLimiter.Wait(sv.ctx) _ = sv.retryLimiter.Wait(sv.ctx)
continue continue
} }
xl.Debug("keepTunnelOpenWorker check success") xl.Debugf("keepTunnelOpenWorker check success")
if conn != nil { if conn != nil {
conn.Close() conn.Close()
} }
@ -157,14 +161,14 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
func (sv *XTCPVisitor) handleConn(userConn net.Conn) { func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
isConnTrasfered := false isConnTransfered := false
defer func() { defer func() {
if !isConnTrasfered { if !isConnTransfered {
userConn.Close() userConn.Close()
} }
}() }()
xl.Debug("get a new xtcp user connection") xl.Debugf("get a new xtcp user connection")
// Open a tunnel connection to the server. If there is already a successful hole-punching connection, // Open a tunnel connection to the server. If there is already a successful hole-punching connection,
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout. // it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
@ -176,18 +180,18 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
} }
tunnelConn, err := sv.openTunnel(ctx) tunnelConn, err := sv.openTunnel(ctx)
if err != nil { if err != nil {
xl.Error("open tunnel error: %v", err) xl.Errorf("open tunnel error: %v", err)
// no fallback, just return // no fallback, just return
if sv.cfg.FallbackTo == "" { if sv.cfg.FallbackTo == "" {
return return
} }
xl.Debug("try to transfer connection to visitor: %s", sv.cfg.FallbackTo) xl.Debugf("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil { if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil {
xl.Error("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return return
} }
isConnTrasfered = true isConnTransfered = true
return return
} }
@ -195,7 +199,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
if sv.cfg.Transport.UseEncryption { if sv.cfg.Transport.UseEncryption {
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Error("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
return return
} }
} }
@ -206,9 +210,9 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
} }
_, _, errs := libio.Join(userConn, muxConnRWCloser) _, _, errs := libio.Join(userConn, muxConnRWCloser)
xl.Debug("join connections closed") xl.Debugf("join connections closed")
if len(errs) > 0 { if len(errs) > 0 {
xl.Trace("join connections errors: %v", errs) xl.Tracef("join connections errors: %v", errs)
} }
} }
@ -239,7 +243,7 @@ func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error
if err != nil { if err != nil {
if err != ErrNoTunnelSession { if err != ErrNoTunnelSession {
xl.Warn("get tunnel connection error: %v", err) xl.Warnf("get tunnel connection error: %v", err)
} }
continue continue
} }
@ -268,19 +272,19 @@ func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
// 4. Create a tunnel session using an underlying UDP connection. // 4. Create a tunnel session using an underlying UDP connection.
func (sv *XTCPVisitor) makeNatHole() { func (sv *XTCPVisitor) makeNatHole() {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
xl.Trace("makeNatHole start") xl.Tracef("makeNatHole start")
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil { if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
xl.Warn("nathole precheck error: %v", err) xl.Warnf("nathole precheck error: %v", err)
return return
} }
xl.Trace("nathole prepare start") xl.Tracef("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}) prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
if err != nil { if err != nil {
xl.Warn("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
listenConn := prepareResult.ListenConn listenConn := prepareResult.ListenConn
@ -298,30 +302,30 @@ func (sv *XTCPVisitor) makeNatHole() {
AssistedAddrs: prepareResult.AssistedAddrs, AssistedAddrs: prepareResult.AssistedAddrs,
} }
xl.Trace("nathole exchange info start") xl.Tracef("nathole exchange info start")
natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second) natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second)
if err != nil { if err != nil {
listenConn.Close() listenConn.Close()
xl.Warn("nathole exchange info error: %v", err) xl.Warnf("nathole exchange info error: %v", err)
return return
} }
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v", xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs, natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey)) newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
listenConn.Close() listenConn.Close()
xl.Warn("make hole error: %v", err) xl.Warnf("make hole error: %v", err)
return return
} }
listenConn = newListenConn listenConn = newListenConn
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr) xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
if err := sv.session.Init(listenConn, raddr); err != nil { if err := sv.session.Init(listenConn, raddr); err != nil {
listenConn.Close() listenConn.Close()
xl.Warn("init tunnel session error: %v", err) xl.Warnf("init tunnel session error: %v", err)
return return
} }
} }
@ -349,7 +353,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
if err != nil { if err != nil {
return fmt.Errorf("dial udp error: %v", err) return fmt.Errorf("dial udp error: %v", err)
} }
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String()) remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil { if err != nil {
return fmt.Errorf("create kcp connection from udp connection error: %v", err) return fmt.Errorf("create kcp connection from udp connection error: %v", err)
} }
@ -440,7 +444,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return utilnet.QuicStreamToNetConn(stream, session), nil return netpkg.QuicStreamToNetConn(stream, session), nil
} }
func (qs *QUICTunnelSession) Close() { func (qs *QUICTunnelSession) Close() {

View File

@ -17,8 +17,10 @@ package main
import ( import (
_ "github.com/fatedier/frp/assets/frpc" _ "github.com/fatedier/frp/assets/frpc"
"github.com/fatedier/frp/cmd/frpc/sub" "github.com/fatedier/frp/cmd/frpc/sub"
"github.com/fatedier/frp/pkg/util/system"
) )
func main() { func main() {
system.EnableCompatibilityMode()
sub.Execute() sub.Execute()
} }

View File

@ -15,9 +15,11 @@
package sub package sub
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
"github.com/rodaine/table" "github.com/rodaine/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -27,24 +29,24 @@ import (
clientsdk "github.com/fatedier/frp/pkg/sdk/client" clientsdk "github.com/fatedier/frp/pkg/sdk/client"
) )
var adminAPITimeout = 30 * time.Second
func init() { func init() {
rootCmd.AddCommand(NewAdminCommand( commands := []struct {
"reload", name string
"Hot-Reload frpc configuration", description string
ReloadHandler, handler func(*v1.ClientCommonConfig) error
)) }{
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
{"status", "Overview of all proxies status", StatusHandler},
{"stop", "Stop the running frpc", StopHandler},
}
rootCmd.AddCommand(NewAdminCommand( for _, cmdConfig := range commands {
"status", cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler)
"Overview of all proxies status", cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls")
StatusHandler, rootCmd.AddCommand(cmd)
)) }
rootCmd.AddCommand(NewAdminCommand(
"stop",
"Stop the running frpc",
StopHandler,
))
} }
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command { func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
@ -52,7 +54,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
Use: name, Use: name,
Short: short, Short: short,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cfg, _, _, _, err := config.LoadClientConfig(cfgFile) cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -73,7 +75,9 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Reload(); err != nil { ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
if err := client.Reload(ctx, strictConfigMode); err != nil {
return err return err
} }
fmt.Println("reload success") fmt.Println("reload success")
@ -83,7 +87,9 @@ func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
func StatusHandler(clientCfg *v1.ClientCommonConfig) error { func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
res, err := client.GetAllProxyStatus() ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
res, err := client.GetAllProxyStatus(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -109,7 +115,9 @@ func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
func StopHandler(clientCfg *v1.ClientCommonConfig) error { func StopHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Stop(); err != nil { ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
if err := client.Stop(ctx); err != nil {
return err return err
} }
fmt.Println("stop success") fmt.Println("stop success")

View File

@ -1,125 +0,0 @@
// 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

@ -48,9 +48,10 @@ var natholeDiscoveryCmd = &cobra.Command{
Short: "Discover nathole information from stun server", Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters // ignore error here, because we can use command line pameters
cfg, _, _, _, err := config.LoadClientConfig(cfgFile) cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
cfg = &v1.ClientCommonConfig{} cfg = &v1.ClientCommonConfig{}
cfg.Complete()
} }
if natHoleSTUNServer != "" { if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer cfg.NatHoleSTUNServer = natHoleSTUNServer

View File

@ -17,10 +17,11 @@ package sub
import ( import (
"fmt" "fmt"
"os" "os"
"slices"
"github.com/samber/lo"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
) )
@ -50,17 +51,17 @@ func init() {
} }
clientCfg := v1.ClientCommonConfig{} clientCfg := v1.ClientCommonConfig{}
cmd := NewProxyCommand(string(typ), c, &clientCfg) cmd := NewProxyCommand(string(typ), c, &clientCfg)
RegisterClientCommonConfigFlags(cmd, &clientCfg) config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
RegisterProxyFlags(cmd, c) config.RegisterProxyFlags(cmd, c)
// add sub command for visitor // add sub command for visitor
if lo.Contains(visitorTypes, v1.VisitorType(typ)) { if slices.Contains(visitorTypes, v1.VisitorType(typ)) {
vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ)) vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
if vc == nil { if vc == nil {
panic("visitor type: " + typ + " not support") panic("visitor type: " + typ + " not support")
} }
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg) visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
RegisterVisitorFlags(visitorCmd, vc) config.RegisterVisitorFlags(visitorCmd, vc)
cmd.AddCommand(visitorCmd) cmd.AddCommand(visitorCmd)
} }
rootCmd.AddCommand(cmd) rootCmd.AddCommand(cmd)

View File

@ -31,20 +31,23 @@ import (
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/featuregate"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
) )
var ( var (
cfgFile string cfgFile string
cfgDir string cfgDir string
showVersion bool showVersion bool
strictConfigMode bool
) )
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@ -95,6 +98,7 @@ func runMultipleClients(cfgDir string) error {
} }
func Execute() { func Execute() {
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }
@ -108,7 +112,7 @@ func handleTermSignal(svr *client.Service) {
} }
func runClient(cfgFilePath string) error { func runClient(cfgFilePath string) error {
cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath) cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
if err != nil { if err != nil {
return err return err
} }
@ -117,29 +121,40 @@ func runClient(cfgFilePath string) error {
"please use yaml/json/toml format instead!\n") "please use yaml/json/toml format instead!\n")
} }
warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs) if len(cfg.FeatureGates) > 0 {
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
return err
}
}
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }
if err != nil { if err != nil {
return err return err
} }
return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
} }
func startService( func startService(
cfg *v1.ClientCommonConfig, cfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer, proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer, visitorCfgs []v1.VisitorConfigurer,
cfgFile string, cfgFile string,
) error { ) error {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor) log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
if cfgFile != "" { if cfgFile != "" {
log.Info("start frpc service for config file [%s]", cfgFile) log.Infof("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile) defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
} }
svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) svr, err := client.NewService(client.ServiceOptions{
Common: cfg,
ProxyCfgs: proxyCfgs,
VisitorCfgs: visitorCfgs,
ConfigFilePath: cfgFile,
})
if err != nil { if err != nil {
return err return err
} }
@ -149,7 +164,5 @@ func startService(
if shouldGracefulClose { if shouldGracefulClose {
go handleTermSignal(svr) go handleTermSignal(svr)
} }
return svr.Run(context.Background())
_ = svr.Run(context.Background())
return nil
} }

View File

@ -37,12 +37,12 @@ var verifyCmd = &cobra.Command{
return nil return nil
} }
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile) cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs) warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }

View File

@ -1,110 +0,0 @@
// 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,13 +15,12 @@
package main package main
import ( import (
"github.com/fatedier/golib/crypto"
_ "github.com/fatedier/frp/assets/frps" _ "github.com/fatedier/frp/assets/frps"
_ "github.com/fatedier/frp/pkg/metrics" _ "github.com/fatedier/frp/pkg/metrics"
"github.com/fatedier/frp/pkg/util/system"
) )
func main() { func main() {
crypto.DefaultSalt = "frp" system.EnableCompatibilityMode()
Execute() Execute()
} }

View File

@ -30,8 +30,9 @@ import (
) )
var ( var (
cfgFile string cfgFile string
showVersion bool showVersion bool
strictConfigMode bool
serverCfg v1.ServerConfig serverCfg v1.ServerConfig
) )
@ -39,8 +40,9 @@ var (
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
RegisterServerConfigFlags(rootCmd, &serverCfg) config.RegisterServerConfigFlags(rootCmd, &serverCfg)
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@ -58,7 +60,7 @@ var rootCmd = &cobra.Command{
err error err error
) )
if cfgFile != "" { if cfgFile != "" {
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile) svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -90,25 +92,26 @@ var rootCmd = &cobra.Command{
} }
func Execute() { func Execute() {
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }
} }
func runServer(cfg *v1.ServerConfig) (err error) { func runServer(cfg *v1.ServerConfig) (err error) {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor) log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
if cfgFile != "" { if cfgFile != "" {
log.Info("frps uses config file: %s", cfgFile) log.Infof("frps uses config file: %s", cfgFile)
} else { } else {
log.Info("frps uses command line arguments for config") log.Infof("frps uses command line arguments for config")
} }
svr, err := server.NewService(cfg) svr, err := server.NewService(cfg)
if err != nil { if err != nil {
return err return err
} }
log.Info("frps started successfully") log.Infof("frps started successfully")
svr.Run(context.Background()) svr.Run(context.Background())
return return
} }

View File

@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{
fmt.Println("frps: the configuration file is not specified") fmt.Println("frps: the configuration file is not specified")
return nil return nil
} }
svrCfg, _, err := config.LoadServerConfig(cfgFile) svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@ -38,7 +38,7 @@ auth.token = "12345678"
# auth.oidc.clientSecret = "" # auth.oidc.clientSecret = ""
# oidc.audience specifies the audience of the token in OIDC authentication. # oidc.audience specifies the audience of the token in OIDC authentication.
# auth.oidc.audience = "" # auth.oidc.audience = ""
# oidc.scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". # oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
# auth.oidc.scope = "" # auth.oidc.scope = ""
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint. # oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token. # It will be used to get an OIDC token.
@ -76,7 +76,7 @@ transport.poolCount = 5
# Specify keep alive interval for tcp mux. # Specify keep alive interval for tcp mux.
# only valid if tcpMux is enabled. # only valid if tcpMux is enabled.
# transport.tcpMuxKeepaliveInterval = 60 # transport.tcpMuxKeepaliveInterval = 30
# Communication protocol used to connect to server # Communication protocol used to connect to server
# supports tcp, kcp, quic, websocket and wss now, default is tcp # supports tcp, kcp, quic, websocket and wss now, default is tcp
@ -129,6 +129,15 @@ transport.tls.enable = true
# It affects the udp and sudp proxy. # It affects the udp and sudp proxy.
udpPacketSize = 1500 udpPacketSize = 1500
# Feature gates allows you to enable or disable experimental features
# Format is a map of feature names to boolean values
# You can enable specific features:
#featureGates = { VirtualNet = true }
# VirtualNet settings for experimental virtual network capabilities
# The virtual network feature requires enabling the VirtualNet feature gate above
# virtualNet.address = "100.86.1.1/24"
# Additional metadatas for client. # Additional metadatas for client.
metadatas.var1 = "abc" metadatas.var1 = "abc"
metadatas.var2 = "123" metadatas.var2 = "123"
@ -164,11 +173,16 @@ healthCheck.type = "tcp"
healthCheck.timeoutSeconds = 3 healthCheck.timeoutSeconds = 3
# If continuous failed in 3 times, the proxy will be removed from frps # If continuous failed in 3 times, the proxy will be removed from frps
healthCheck.maxFailed = 3 healthCheck.maxFailed = 3
# every 10 seconds will do a health check # Every 10 seconds will do a health check
healthCheck.intervalSeconds = 10 healthCheck.intervalSeconds = 10
# additional meta info for each proxy # Additional meta info for each proxy. It will be passed to the server-side plugin for use.
metadatas.var1 = "abc" metadatas.var1 = "abc"
metadatas.var2 = "123" metadatas.var2 = "123"
# You can add some extra information to the proxy through annotations.
# These annotations will be displayed on the frps dashboard.
[proxies.annotations]
key1 = "value1"
"prefix/key2" = "value2"
[[proxies]] [[proxies]]
name = "ssh_random" name = "ssh_random"
@ -204,6 +218,7 @@ locations = ["/", "/pic"]
# routeByHTTPUser = abc # routeByHTTPUser = abc
hostHeaderRewrite = "example.com" hostHeaderRewrite = "example.com"
requestHeaders.set.x-from-where = "frp" requestHeaders.set.x-from-where = "frp"
responseHeaders.set.foo = "bar"
healthCheck.type = "http" healthCheck.type = "http"
# frpc will send a GET http request '/status' to local http service # frpc will send a GET http request '/status' to local http service
# http service is alive when it return 2xx http response code # http service is alive when it return 2xx http response code
@ -211,6 +226,10 @@ healthCheck.path = "/status"
healthCheck.intervalSeconds = 10 healthCheck.intervalSeconds = 10
healthCheck.maxFailed = 3 healthCheck.maxFailed = 3
healthCheck.timeoutSeconds = 3 healthCheck.timeoutSeconds = 3
# set health check headers
healthCheck.httpHeaders=[
{ name = "x-from-where", value = "frp" }
]
[[proxies]] [[proxies]]
name = "web02" name = "web02"
@ -305,6 +324,26 @@ localAddr = "127.0.0.1:443"
hostHeaderRewrite = "127.0.0.1" hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp" requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_http2http"
type = "tcp"
remotePort = 6007
[proxies.plugin]
type = "http2http"
localAddr = "127.0.0.1:80"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_tls2raw"
type = "tcp"
remotePort = 6008
[proxies.plugin]
type = "tls2raw"
localAddr = "127.0.0.1:80"
crtPath = "./server.crt"
keyPath = "./server.key"
[[proxies]] [[proxies]]
name = "secret_tcp" name = "secret_tcp"
# If the type is secret tcp, remotePort is useless # If the type is secret tcp, remotePort is useless
@ -328,6 +367,13 @@ localPort = 22
# Otherwise, visitors from same user can connect. '*' means allow all users. # Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["user1", "user2"] allowUsers = ["user1", "user2"]
[[proxies]]
name = "vnet-server"
type = "stcp"
secretKey = "your-secret-key"
[proxies.plugin]
type = "virtual_net"
# frpc role visitor -> frps -> frpc role server # frpc role visitor -> frps -> frpc role server
[[visitors]] [[visitors]]
name = "secret_tcp_visitor" name = "secret_tcp_visitor"
@ -359,3 +405,13 @@ maxRetriesAnHour = 8
minRetryInterval = 90 minRetryInterval = 90
# fallbackTo = "stcp_visitor" # fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500 # fallbackTimeoutMs = 500
[[visitors]]
name = "vnet-visitor"
type = "stcp"
serverName = "vnet-server"
secretKey = "your-secret-key"
bindPort = -1
[visitors.plugin]
type = "virtual_net"
destinationIP = "100.86.0.1"

View File

@ -34,14 +34,14 @@ transport.maxPoolCount = 5
# Specify keep alive interval for tcp mux. # Specify keep alive interval for tcp mux.
# only valid if tcpMux is true. # only valid if tcpMux is true.
# transport.tcpMuxKeepaliveInterval = 60 # transport.tcpMuxKeepaliveInterval = 30
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps. # tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled. # If negative, keep-alive probes are disabled.
# transport.tcpKeepalive = 7200 # transport.tcpKeepalive = 7200
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false. # transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
tls.force = false transport.tls.force = false
# transport.tls.certFile = "server.crt" # transport.tls.certFile = "server.crt"
# transport.tls.keyFile = "server.key" # transport.tls.keyFile = "server.key"
@ -129,7 +129,7 @@ allowPorts = [
maxPortsPerClient = 0 maxPortsPerClient = 0
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file # 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 # When subdomain is test, the host used by routing is test.frps.com
subDomainHost = "frps.com" subDomainHost = "frps.com"
# custom 404 page for HTTP requests # custom 404 page for HTTP requests
@ -143,6 +143,14 @@ udpPacketSize = 1500
# Retention time for NAT hole punching strategy data. # Retention time for NAT hole punching strategy data.
natholeAnalysisDataReserveHours = 168 natholeAnalysisDataReserveHours = 168
# ssh tunnel gateway
# If you want to enable this feature, the bindPort parameter is required, while others are optional.
# By default, this feature is disabled. It will be enabled if bindPort is greater than 0.
# sshTunnelGateway.bindPort = 2200
# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa"
# sshTunnelGateway.autoGenPrivateKeyPath = ""
# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys"
[[httpPlugins]] [[httpPlugins]]
name = "user-manager" name = "user-manager"
addr = "127.0.0.1:9000" addr = "127.0.0.1:9000"

View File

@ -56,7 +56,7 @@ oidc_client_secret =
# oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". # oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_audience = oidc_audience =
# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". # oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_scope = oidc_scope =
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint. # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
doc/pic/sponsor_daytona.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
doc/pic/sponsor_lokal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
doc/pic/sponsor_olares.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -121,7 +121,7 @@ Create new proxy
// http and https only // http and https only
"custom_domains": []<string>, "custom_domains": []<string>,
"subdomain": <string>, "subdomain": <string>,
"locations": <string>, "locations": []<string>,
"http_user": <string>, "http_user": <string>,
"http_pwd": <string>, "http_pwd": <string>,
"host_header_rewrite": <string>, "host_header_rewrite": <string>,

160
doc/ssh_tunnel_gateway.md Normal file
View File

@ -0,0 +1,160 @@
### SSH Tunnel Gateway
*Added in v0.53.0*
### Concept
SSH supports reverse proxy capabilities [rfc](https://www.rfc-editor.org/rfc/rfc4254#page-16).
frp supports listening on an SSH port on the frps side to achieve TCP protocol proxying using the SSH -R protocol. This mode does not rely on frpc.
SSH reverse tunneling proxying and proxying SSH ports through frp are two different concepts. SSH reverse tunneling proxying is essentially a basic reverse proxying accomplished by connecting to frps via an SSH client when you don't want to use frpc.
```toml
# frps.toml
sshTunnelGateway.bindPort = 0
sshTunnelGateway.privateKeyFile = ""
sshTunnelGateway.autoGenPrivateKeyPath = ""
sshTunnelGateway.authorizedKeysFile = ""
```
| Field | Type | Description | Required |
| :--- | :--- | :--- | :--- |
| bindPort| int | The ssh server port that frps listens on.| Yes |
| privateKeyFile | string | Default value is empty. The private key file used by the ssh server. If it is empty, frps will read the private key file under the autoGenPrivateKeyPath path. It can reuse the /home/user/.ssh/id_rsa file on the local machine, or a custom path can be specified.| No |
| autoGenPrivateKeyPath | string |Default value is ./.autogen_ssh_key. If the file does not exist or its content is empty, frps will automatically generate RSA private key file content and store it in this file.|No|
| authorizedKeysFile | string |Default value is empty. If it is empty, ssh client authentication is not authenticated. If it is not empty, it can implement ssh password-free login authentication. It can reuse the local /home/user/.ssh/authorized_keys file or a custom path can be specified.| No |
### Basic Usage
#### Server-side frps
Minimal configuration:
```toml
sshTunnelGateway.bindPort = 2200
```
Place the above configuration in frps.toml and run `./frps -c frps.toml`. It will listen on port 2200 and accept SSH reverse proxy requests.
Note:
1. When using the minimal configuration, a `.autogen_ssh_key` private key file will be automatically created in the current working directory. The SSH server of frps will use this private key file for encryption and decryption. Alternatively, you can reuse an existing private key file on your local machine, such as `/home/user/.ssh/id_rsa`.
2. When running frps in the minimal configuration mode, connecting to frps via SSH does not require authentication. It is strongly recommended to configure a token in frps and specify the token in the SSH command line.
#### Client-side SSH
The command format is:
```bash
ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token}
```
1. `--proxy_name` is optional, and if left empty, a random one will be generated.
2. The username for logging in to frps is always "v0" and currently has no significance, i.e., `v0@{frps_address}`.
3. The server-side proxy listens on the port determined by `--remote_port`.
4. `{tcp|http|https|stcp|tcpmux}` supports the complete command parameters, which can be obtained by using `--help`. For example: `ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help`.
5. The token is optional, but for security reasons, it is strongly recommended to configure the token in frps.
#### TCP Proxy
```bash
ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
```
This sets up a proxy on frps that listens on port 9090 and proxies local service on port 8080.
```bash
frp (via SSH) (Ctrl+C to quit)
User:
ProxyName: test-tcp
Type: tcp
RemoteAddress: :9090
```
Equivalent to:
```bash
frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
```
More parameters can be obtained by executing `--help`.
#### HTTP Proxy
```bash
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 http --proxy_name "test-http" --custom_domain test-http.frps.com
```
Equivalent to:
```bash
frpc http --proxy_name "test-http" --custom_domain test-http.frps.com
```
You can access the HTTP service using the following command:
curl 'http://test-http.frps.com'
More parameters can be obtained by executing --help.
#### HTTPS/STCP/TCPMUX Proxy
To obtain the usage instructions, use the following command:
```bash
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 {https|stcp|tcpmux} --help
```
### Advanced Usage
#### Reusing the id_rsa File on the Local Machine
```toml
# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa"
```
During the SSH protocol handshake, public keys are exchanged for data encryption. Therefore, the SSH server on the frps side needs to specify a private key file, which can be reused from an existing file on the local machine. If the privateKeyFile field is empty, frps will automatically create an RSA private key file.
#### Specifying the Auto-Generated Private Key File Path
```toml
# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file"
```
frps will automatically create a private key file and store it at the specified path.
Note: Changing the private key file in frps can cause SSH client login failures. If you need to log in successfully, you can delete the old records from the `/home/user/.ssh/known_hosts` file.
#### Using an Existing authorized_keys File for SSH Public Key Authentication
```toml
# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys"
```
The authorizedKeysFile is the file used for SSH public key authentication, which contains the public key information for users, with one key per line.
If authorizedKeysFile is empty, frps won't perform any authentication for SSH clients. Frps does not support SSH username and password authentication.
You can reuse an existing `authorized_keys` file on your local machine for client authentication.
Note: authorizedKeysFile is for user authentication during the SSH login phase, while the token is for frps authentication. These two authentication methods are independent. SSH authentication comes first, followed by frps token authentication. It is strongly recommended to enable at least one of them. If authorizedKeysFile is empty, it is highly recommended to enable token authentication in frps to avoid security risks.
#### Using a Custom authorized_keys File for SSH Public Key Authentication
```toml
# frps.toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file"
```
Specify the path to a custom `authorized_keys` file.
Note that changes to the authorizedKeysFile file may result in SSH authentication failures. You may need to re-add the public key information to the authorizedKeysFile.

73
doc/virtual_net.md Normal file
View File

@ -0,0 +1,73 @@
# Virtual Network (VirtualNet)
*Alpha feature added in v0.62.0*
The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
> **Note**: VirtualNet is an Alpha stage feature and is currently unstable. Its configuration methods and functionality may be adjusted and changed at any time in subsequent versions. Do not use this feature in production environments; it is only recommended for testing and evaluation purposes.
## Enabling VirtualNet
Since VirtualNet is currently an alpha feature, you need to enable it with feature gates in your configuration:
```toml
# frpc.toml
featureGates = { VirtualNet = true }
```
## Basic Configuration
To use the virtual network capabilities:
1. First, configure your frpc with a virtual network address:
```toml
# frpc.toml
serverAddr = "x.x.x.x"
serverPort = 7000
featureGates = { VirtualNet = true }
# Configure the virtual network interface
virtualNet.address = "100.86.0.1/24"
```
2. For client proxies, use the `virtual_net` plugin:
```toml
# frpc.toml (server side)
[[proxies]]
name = "vnet-server"
type = "stcp"
secretKey = "your-secret-key"
[proxies.plugin]
type = "virtual_net"
```
3. For visitor connections, configure the `virtual_net` visitor plugin:
```toml
# frpc.toml (client side)
serverAddr = "x.x.x.x"
serverPort = 7000
featureGates = { VirtualNet = true }
# Configure the virtual network interface
virtualNet.address = "100.86.0.2/24"
[[visitors]]
name = "vnet-visitor"
type = "stcp"
serverName = "vnet-server"
secretKey = "your-secret-key"
bindPort = -1
[visitors.plugin]
type = "virtual_net"
destinationIP = "100.86.0.1"
```
## Requirements and Limitations
- **Permissions**: Creating a TUN interface requires elevated permissions (root/admin)
- **Platform Support**: Currently supported on Linux and macOS
- **Default Status**: As an alpha feature, VirtualNet is disabled by default
- **Configuration**: A valid IP/CIDR must be provided for each endpoint in the virtual network

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS building FROM golang:1.23 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building
@ -7,6 +7,8 @@ RUN make frpc
FROM alpine:3 FROM alpine:3
RUN apk add --no-cache tzdata
COPY --from=building /building/bin/frpc /usr/bin/frpc COPY --from=building /building/bin/frpc /usr/bin/frpc
ENTRYPOINT ["/usr/bin/frpc"] ENTRYPOINT ["/usr/bin/frpc"]

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS building FROM golang:1.23 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building
@ -7,6 +7,8 @@ RUN make frps
FROM alpine:3 FROM alpine:3
RUN apk add --no-cache tzdata
COPY --from=building /building/bin/frps /usr/bin/frps COPY --from=building /building/bin/frps /usr/bin/frps
ENTRYPOINT ["/usr/bin/frps"] ENTRYPOINT ["/usr/bin/frps"]

107
go.mod
View File

@ -1,80 +1,83 @@
module github.com/fatedier/frp module github.com/fatedier/frp
go 1.20 go 1.23.0
require ( require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.6.0 github.com/coreos/go-oidc/v3 v3.14.1
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb github.com/fatedier/golib v0.5.1
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40 github.com/google/uuid v1.6.0
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible github.com/gorilla/mux v1.8.1
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1 github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.27.8 github.com/onsi/gomega v1.36.3
github.com/pelletier/go-toml/v2 v2.1.0 github.com/pelletier/go-toml/v2 v2.2.0
github.com/pion/stun v0.6.1 github.com/pion/stun/v2 v2.0.0
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.37.4 github.com/quic-go/quic-go v0.48.2
github.com/rodaine/table v1.1.0 github.com/rodaine/table v1.2.0
github.com/samber/lo v1.38.1 github.com/samber/lo v1.47.0
github.com/spf13/cobra v1.7.0 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/stretchr/testify v1.8.4 github.com/spf13/cobra v1.8.0
golang.org/x/net v0.12.0 github.com/spf13/pflag v1.0.5
golang.org/x/oauth2 v0.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/sync v0.3.0 github.com/tidwall/gjson v1.17.1
golang.org/x/time v0.3.0 github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13
golang.org/x/crypto v0.37.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.13.0
golang.org/x/time v0.5.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.27.4 k8s.io/apimachinery v0.28.8
k8s.io/client-go v0.27.4 k8s.io/client-go v0.28.8
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.9.15 // indirect github.com/klauspost/reedsolomon v1.12.0 // 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/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/transport/v3 v3.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect github.com/templexxx/cpu v0.1.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/crypto v0.11.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/mod v0.10.0 // indirect go.uber.org/mock v0.5.0 // indirect
golang.org/x/sys v0.10.0 // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/text v0.11.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/tools v0.9.3 // indirect golang.org/x/sys v0.32.0 // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/tools v0.31.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/yaml v1.3.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect
) )

263
go.sum
View File

@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@ -9,242 +9,250 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw= github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40 h1:BVdpWT6viE/mpuRa6txNyRNjtHa1Efrii9Du6/gHfJ0=
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40/go.mod h1:Lmi9U4VfvdRvonSMh1FgXVy1hCXycVyJk4E9ktokknE=
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74=
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo= github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo= github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAKAP/qs14y4GCBOk= github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg= github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4= github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g= github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -257,12 +265,11 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@ -270,14 +277,16 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= k8s.io/apimachinery v0.28.8/go.mod h1:cBnwIM3fXoRo28SqbV/Ihxf/iviw85KyXOrzxvZQ83U=
k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= k8s.io/client-go v0.28.8 h1:TE59Tjd87WKvS2FPBTfIKLFX0nQJ4SSHsnDo5IHjgOw=
k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= k8s.io/client-go v0.28.8/go.mod h1:uDVQ/rPzWpWIy40c6lZ4mUwaEvRWGnpoqSO4FM65P3o=
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=

View File

@ -3,10 +3,10 @@
SCRIPT=$(readlink -f "$0") SCRIPT=$(readlink -f "$0")
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
ginkgo_command=$(which ginkgo 2>/dev/null) # Check if ginkgo is available
if [ -z "$ginkgo_command" ]; then if ! command -v ginkgo >/dev/null 2>&1; then
echo "ginkgo not found, try to install..." echo "ginkgo not found, try to install..."
go install github.com/onsi/ginkgo/v2/ginkgo@v2.11.0 go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
fi fi
debug=false debug=false
@ -26,5 +26,9 @@ frpsPath=${ROOT}/bin/frps
if [ "${FRPS_PATH}" ]; then if [ "${FRPS_PATH}" ]; then
frpsPath="${FRPS_PATH}" frpsPath="${FRPS_PATH}"
fi fi
concurrency="16"
if [ "${CONCURRENCY}" ]; then
concurrency="${CONCURRENCY}"
fi
ginkgo -nodes=8 --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug} ginkgo -nodes=${concurrency} --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}

View File

@ -1,3 +1,6 @@
#!/bin/sh
set -e
# compile for version # compile for version
make make
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@ -14,50 +17,57 @@ make -f ./Makefile.cross-compiles
rm -rf ./release/packages rm -rf ./release/packages
mkdir -p ./release/packages mkdir -p ./release/packages
os_all='linux windows darwin freebsd' os_all='linux windows darwin freebsd openbsd android'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64' arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
extra_all='_ hf'
cd ./release cd ./release
for os in $os_all; do for os in $os_all; do
for arch in $arch_all; do for arch in $arch_all; do
frp_dir_name="frp_${frp_version}_${os}_${arch}" for extra in $extra_all; do
frp_path="./packages/frp_${frp_version}_${os}_${arch}" suffix="${os}_${arch}"
if [ "x${extra}" != x"_" ]; then
suffix="${os}_${arch}_${extra}"
fi
frp_dir_name="frp_${frp_version}_${suffix}"
frp_path="./packages/frp_${frp_version}_${suffix}"
if [ "x${os}" = x"windows" ]; then if [ "x${os}" = x"windows" ]; then
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
continue continue
fi fi
if [ ! -f "./frps_${os}_${arch}.exe" ]; then if [ ! -f "./frps_${os}_${arch}.exe" ]; then
continue continue
fi fi
mkdir ${frp_path} mkdir ${frp_path}
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
else else
if [ ! -f "./frpc_${os}_${arch}" ]; then if [ ! -f "./frpc_${suffix}" ]; then
continue continue
fi fi
if [ ! -f "./frps_${os}_${arch}" ]; then if [ ! -f "./frps_${suffix}" ]; then
continue continue
fi fi
mkdir ${frp_path} mkdir ${frp_path}
mv ./frpc_${os}_${arch} ${frp_path}/frpc mv ./frpc_${suffix} ${frp_path}/frpc
mv ./frps_${os}_${arch} ${frp_path}/frps mv ./frps_${suffix} ${frp_path}/frps
fi fi
cp ../LICENSE ${frp_path} cp ../LICENSE ${frp_path}
cp -f ../conf/frpc.toml ${frp_path} cp -f ../conf/frpc.toml ${frp_path}
cp -f ../conf/frps.toml ${frp_path} cp -f ../conf/frps.toml ${frp_path}
# packages # packages
cd ./packages cd ./packages
if [ "x${os}" = x"windows" ]; then if [ "x${os}" = x"windows" ]; then
zip -rq ${frp_dir_name}.zip ${frp_dir_name} zip -rq ${frp_dir_name}.zip ${frp_dir_name}
else else
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name} tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
fi fi
cd .. cd ..
rm -rf ${frp_path} rm -rf ${frp_path}
done
done done
done done

View File

@ -50,7 +50,8 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
case v1.AuthMethodToken: case v1.AuthMethodToken:
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC: case v1.AuthMethodOIDC:
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) tokenVerifier := NewTokenVerifier(cfg.OIDC)
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
} }
return authVerifier return authVerifier
} }

View File

@ -17,9 +17,9 @@ package auth
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/samber/lo"
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
@ -70,7 +70,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
} }
func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil return nil
} }
@ -79,7 +79,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
} }
func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil return nil
} }
@ -87,14 +87,18 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
return err return err
} }
type TokenVerifier interface {
Verify(context.Context, string) (*oidc.IDToken, error)
}
type OidcAuthConsumer struct { type OidcAuthConsumer struct {
additionalAuthScopes []v1.AuthScope additionalAuthScopes []v1.AuthScope
verifier *oidc.IDTokenVerifier verifier TokenVerifier
subjectFromLogin string subjectsFromLogin []string
} }
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer { func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
if err != nil { if err != nil {
panic(err) panic(err)
@ -105,9 +109,14 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCSer
SkipExpiryCheck: cfg.SkipExpiryCheck, SkipExpiryCheck: cfg.SkipExpiryCheck,
SkipIssuerCheck: cfg.SkipIssuerCheck, SkipIssuerCheck: cfg.SkipIssuerCheck,
} }
return provider.Verifier(&verifierConf)
}
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
return &OidcAuthConsumer{ return &OidcAuthConsumer{
additionalAuthScopes: additionalAuthScopes, additionalAuthScopes: additionalAuthScopes,
verifier: provider.Verifier(&verifierConf), verifier: verifier,
subjectsFromLogin: []string{},
} }
} }
@ -116,7 +125,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
if err != nil { if err != nil {
return fmt.Errorf("invalid OIDC token in login: %v", err) return fmt.Errorf("invalid OIDC token in login: %v", err)
} }
auth.subjectFromLogin = token.Subject if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
}
return nil return nil
} }
@ -125,17 +136,17 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
if err != nil { if err != nil {
return fmt.Errorf("invalid OIDC token in ping: %v", err) return fmt.Errorf("invalid OIDC token in ping: %v", err)
} }
if token.Subject != auth.subjectFromLogin { if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
return fmt.Errorf("received different OIDC subject in login and ping. "+ return fmt.Errorf("received different OIDC subject in login and ping. "+
"original subject: %s, "+ "original subjects: %s, "+
"new subject: %s", "new subject: %s",
auth.subjectFromLogin, token.Subject) auth.subjectsFromLogin, token.Subject)
} }
return nil return nil
} }
func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil return nil
} }
@ -143,7 +154,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
} }
func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil return nil
} }

64
pkg/auth/oidc_test.go Normal file
View File

@ -0,0 +1,64 @@
package auth_test
import (
"context"
"testing"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/stretchr/testify/require"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
)
type mockTokenVerifier struct{}
func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.IDToken, error) {
return &oidc.IDToken{
Subject: subject,
}, nil
}
func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
err := consumer.VerifyPing(&msg.Ping{
PrivilegeKey: "ping-without-login",
Timestamp: time.Now().UnixMilli(),
})
r.Error(err)
r.Contains(err.Error(), "received different OIDC subject in login and ping")
}
func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
err := consumer.VerifyLogin(&msg.Login{
PrivilegeKey: "ping-after-login",
})
r.NoError(err)
err = consumer.VerifyPing(&msg.Ping{
PrivilegeKey: "ping-after-login",
Timestamp: time.Now().UnixMilli(),
})
r.NoError(err)
}
func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
err := consumer.VerifyLogin(&msg.Login{
PrivilegeKey: "login-with-first-subject",
})
r.NoError(err)
err = consumer.VerifyPing(&msg.Ping{
PrivilegeKey: "ping-with-different-subject",
Timestamp: time.Now().UnixMilli(),
})
r.Error(err)
r.Contains(err.Error(), "received different OIDC subject in login and ping")
}

31
pkg/auth/pass.go Normal file
View File

@ -0,0 +1,31 @@
// 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 auth
import (
"github.com/fatedier/frp/pkg/msg"
)
var AlwaysPassVerifier = &alwaysPass{}
var _ Verifier = &alwaysPass{}
type alwaysPass struct{}
func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil }
func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil }
func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil }

View File

@ -16,10 +16,9 @@ package auth
import ( import (
"fmt" "fmt"
"slices"
"time" "time"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
@ -43,7 +42,7 @@ func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error {
} }
func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil return nil
} }
@ -53,7 +52,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
} }
func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil return nil
} }
@ -70,7 +69,7 @@ func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
} }
func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error { func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil return nil
} }
@ -81,7 +80,7 @@ func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
} }
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error { func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil return nil
} }

258
pkg/config/flags.go Normal file
View File

@ -0,0 +1,258 @@
// 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 (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
// WordSepNormalizeFunc changes all flags that contain "_" separators
func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
if strings.Contains(name, "_") {
return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-"))
}
return pflag.NormalizedName(name)
}
type RegisterFlagOption func(*registerFlagOptions)
type registerFlagOptions struct {
sshMode bool
}
func WithSSHMode() RegisterFlagOption {
return func(o *registerFlagOptions) {
o.sshMode = true
}
}
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, opts ...RegisterFlagOption) {
registerProxyBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
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")
cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
case *v1.STCPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
case *v1.SUDPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
case *v1.XTCPProxyConfig:
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
}
}
func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) {
if c == nil {
return
}
options := &registerFlagOptions{}
for _, opt := range opts {
opt(options)
}
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
cmd.Flags().StringToStringVarP(&c.Metadatas, "metadatas", "", nil, "metadata key-value pairs (e.g., key1=value1,key2=value2)")
cmd.Flags().StringToStringVarP(&c.Annotations, "annotations", "", nil, "annotation key-value pairs (e.g., key1=value1,key2=value2)")
if !options.sshMode {
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, opts ...RegisterFlagOption) {
registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
// add visitor flags if exist
}
func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) {
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.ServerUser, "server-user", "", "", "server user")
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, opts ...RegisterFlagOption) {
options := &registerFlagOptions{}
for _, opt := range opts {
opt(options)
}
if !options.sshMode {
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.Transport.Protocol, "protocol", "p", "tcp",
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
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")
}
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
}
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, opts ...RegisterFlagOption) {
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().IntVarP(&c.QUICBindPort, "quic_bind_port", "", 0, "quic 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

@ -18,9 +18,9 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/samber/lo"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
legacyauth "github.com/fatedier/frp/pkg/auth/legacy" legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
@ -99,7 +99,7 @@ type ClientCommonConf struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// User specifies a prefix for proxy names to distinguish them from other // User specifies a prefix for proxy names to distinguish them from other
@ -170,7 +170,7 @@ type ClientCommonConf struct {
} }
// Supported sources including: string(file path), []byte, Reader interface. // Supported sources including: string(file path), []byte, Reader interface.
func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) {
f, err := ini.LoadSources(ini.LoadOptions{ f, err := ini.LoadSources(ini.LoadOptions{
Insensitive: false, Insensitive: false,
InsensitiveSections: false, InsensitiveSections: false,
@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
} }
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_") common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
return common, nil return common, nil
} }
@ -203,7 +203,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
// otherwise just start proxies in startProxy map // otherwise just start proxies in startProxy map
func LoadAllProxyConfsFromIni( func LoadAllProxyConfsFromIni(
prefix string, prefix string,
source interface{}, source any,
start []string, start []string,
) (map[string]ProxyConf, map[string]VisitorConf, error) { ) (map[string]ProxyConf, map[string]VisitorConf, error) {
f, err := ini.LoadSources(ini.LoadOptions{ f, err := ini.LoadSources(ini.LoadOptions{
@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni(
startProxy[s] = struct{}{} startProxy[s] = struct{}{}
} }
startAll := true startAll := len(startProxy) == 0
if len(startProxy) > 0 {
startAll = false
}
// Build template sections from range section And append to ini.File. // Build template sections from range section And append to ini.File.
rangeSections := make([]*ini.Section, 0) rangeSections := make([]*ini.Section, 0)
@ -345,35 +342,19 @@ func copySection(source, target *ini.Section) {
} }
// GetDefaultClientConf returns a client configuration with default values. // GetDefaultClientConf returns a client configuration with default values.
// Note: Some default values here will be set to empty and will be converted to them
// new configuration through the 'Complete' function to set them as the default
// values of the new configuration.
func GetDefaultClientConf() ClientCommonConf { func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{ return ClientCommonConf{
ClientConfig: legacyauth.GetDefaultClientConf(), 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, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
LoginFailExit: true, LoginFailExit: true,
Start: make([]string, 0),
Protocol: "tcp", Protocol: "tcp",
QUICKeepalivePeriod: 10, Start: make([]string, 0),
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
TLSEnable: true, TLSEnable: true,
DisableCustomTLSFirstByte: true, DisableCustomTLSFirstByte: true,
HeartbeatInterval: 30,
HeartbeatTimeout: 90,
Metas: make(map[string]string), Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0), IncludeConfigFiles: make([]string, 0),
} }
} }
@ -399,7 +380,7 @@ func (cfg *ClientCommonConf) Validate() error {
} }
} }
if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) { if !slices.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
return fmt.Errorf("invalid protocol") return fmt.Errorf("invalid protocol")
} }

View File

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

View File

@ -23,7 +23,7 @@ import (
func ParseClientConfig(filePath string) ( func ParseClientConfig(filePath string) (
cfg ClientCommonConf, cfg ClientCommonConf,
pxyCfgs map[string]ProxyConf, proxyCfgs map[string]ProxyConf,
visitorCfgs map[string]VisitorConf, visitorCfgs map[string]VisitorConf,
err error, err error,
) { ) {
@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) (
configBuffer.Write(buf) configBuffer.Write(buf)
// Parse all proxy and visitor configs. // Parse all proxy and visitor configs.
pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
if err != nil { if err != nil {
return return
} }

View File

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

View File

@ -139,7 +139,7 @@ type ServerCommonConf struct {
// from a client to share a single TCP connection. By default, this value // from a client to share a single TCP connection. By default, this value
// is true. // is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
@ -200,38 +200,24 @@ type ServerCommonConf struct {
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"` NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
} }
// GetDefaultServerConf returns a server configuration with reasonable // GetDefaultServerConf returns a server configuration with reasonable defaults.
// defaults. // Note: Some default values here will be set to empty and will be converted to them
// new configuration through the 'Complete' function to set them as the default
// values of the new configuration.
func GetDefaultServerConf() ServerCommonConf { func GetDefaultServerConf() ServerCommonConf {
return ServerCommonConf{ return ServerCommonConf{
ServerConfig: legacyauth.GetDefaultServerConf(), ServerConfig: legacyauth.GetDefaultServerConf(),
BindAddr: "0.0.0.0", DashboardAddr: "0.0.0.0",
BindPort: 7000, LogFile: "console",
QUICKeepalivePeriod: 10, LogWay: "console",
QUICMaxIdleTimeout: 30, DetailedErrorsToClient: true,
QUICMaxIncomingStreams: 100000, TCPMux: true,
VhostHTTPTimeout: 60, AllowPorts: make(map[int]struct{}),
DashboardAddr: "0.0.0.0", HTTPPlugins: make(map[string]HTTPPluginOptions),
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,
} }
} }
func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { func UnmarshalServerConfFromIni(source any) (ServerCommonConf, error) {
f, err := ini.LoadSources(ini.LoadOptions{ f, err := ini.LoadSources(ini.LoadOptions{
Insensitive: false, Insensitive: false,
InsensitiveSections: false, InsensitiveSections: false,

View File

@ -18,10 +18,10 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"text/template"
toml "github.com/pelletier/go-toml/v2" toml "github.com/pelletier/go-toml/v2"
"github.com/samber/lo" "github.com/samber/lo"
@ -80,7 +80,10 @@ func DetectLegacyINIFormatFromFile(path string) bool {
} }
func RenderWithTemplate(in []byte, values *Values) ([]byte, error) { func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
tmpl, err := template.New("frp").Parse(string(in)) tmpl, err := template.New("frp").Funcs(template.FuncMap{
"parseNumberRange": parseNumberRange,
"parseNumberRangePair": parseNumberRangePair,
}).Parse(string(in))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -100,26 +103,72 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
return RenderWithTemplate(b, values) return RenderWithTemplate(b, values)
} }
func LoadConfigureFromFile(path string, c any) error { func LoadConfigureFromFile(path string, c any, strict bool) error {
content, err := LoadFileContentWithTemplate(path, GetValues()) content, err := LoadFileContentWithTemplate(path, GetValues())
if err != nil { if err != nil {
return err return err
} }
return LoadConfigure(content, c) return LoadConfigure(content, c, strict)
}
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
// This function handles both cases efficiently: with or without dot fields
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
var temp any
if err := yaml.Unmarshal(content, &temp); err != nil {
return err
}
// Remove dot fields if it's a map
if tempMap, ok := temp.(map[string]any); ok {
for key := range tempMap {
if strings.HasPrefix(key, ".") {
delete(tempMap, key)
}
}
}
// Convert to JSON and decode with strict validation
jsonBytes, err := json.Marshal(temp)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
} }
// LoadConfigure loads configuration from bytes and unmarshal into c. // LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format. // Now it supports json, yaml and toml format.
func LoadConfigure(b []byte, c any) error { func LoadConfigure(b []byte, c any, strict bool) error {
var tomlObj interface{} v1.DisallowUnknownFieldsMu.Lock()
defer v1.DisallowUnknownFieldsMu.Unlock()
v1.DisallowUnknownFields = strict
var tomlObj any
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
if err := toml.Unmarshal(b, &tomlObj); err == nil { if err := toml.Unmarshal(b, &tomlObj); err == nil {
b, err = json.Marshal(&tomlObj) b, err = json.Marshal(&tomlObj)
if err != nil { if err != nil {
return err return err
} }
} }
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096) // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
return decoder.Decode(c) if yaml.IsJSONBuffer(b) {
decoder := json.NewDecoder(bytes.NewBuffer(b))
if strict {
decoder.DisallowUnknownFields()
}
return decoder.Decode(c)
}
// Handle YAML content
if strict {
// In strict mode, always use our custom handler to support YAML merge
return parseYAMLWithDotFieldsHandling(b, c)
}
// Non-strict mode, parse normally
return yaml.Unmarshal(b, c)
} }
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
@ -139,7 +188,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
return configurer, nil return configurer, nil
} }
func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) {
var ( var (
svrCfg *v1.ServerConfig svrCfg *v1.ServerConfig
isLegacyFormat bool isLegacyFormat bool
@ -158,7 +207,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
isLegacyFormat = true isLegacyFormat = true
} else { } else {
svrCfg = &v1.ServerConfig{} svrCfg = &v1.ServerConfig{}
if err := LoadConfigureFromFile(path, svrCfg); err != nil { if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil {
return nil, false, err return nil, false, err
} }
} }
@ -168,7 +217,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
return svrCfg, isLegacyFormat, nil return svrCfg, isLegacyFormat, nil
} }
func LoadClientConfig(path string) ( func LoadClientConfig(path string, strict bool) (
*v1.ClientCommonConfig, *v1.ClientCommonConfig,
[]v1.ProxyConfigurer, []v1.ProxyConfigurer,
[]v1.VisitorConfigurer, []v1.VisitorConfigurer,
@ -176,19 +225,19 @@ func LoadClientConfig(path string) (
) { ) {
var ( var (
cliCfg *v1.ClientCommonConfig cliCfg *v1.ClientCommonConfig
pxyCfgs = make([]v1.ProxyConfigurer, 0) proxyCfgs = make([]v1.ProxyConfigurer, 0)
visitorCfgs = make([]v1.VisitorConfigurer, 0) visitorCfgs = make([]v1.VisitorConfigurer, 0)
isLegacyFormat bool isLegacyFormat bool
) )
if DetectLegacyINIFormatFromFile(path) { if DetectLegacyINIFormatFromFile(path) {
legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
if err != nil { if err != nil {
return nil, nil, nil, true, err return nil, nil, nil, true, err
} }
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
for _, c := range legacyPxyCfgs { for _, c := range legacyProxyCfgs {
pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
} }
for _, c := range legacyVisitorCfgs { for _, c := range legacyVisitorCfgs {
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c)) visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
@ -196,12 +245,12 @@ func LoadClientConfig(path string) (
isLegacyFormat = true isLegacyFormat = true
} else { } else {
allCfg := v1.ClientConfig{} allCfg := v1.ClientConfig{}
if err := LoadConfigureFromFile(path, &allCfg); err != nil { if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
return nil, nil, nil, false, err return nil, nil, nil, false, err
} }
cliCfg = &allCfg.ClientCommonConfig cliCfg = &allCfg.ClientCommonConfig
for _, c := range allCfg.Proxies { for _, c := range allCfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
} }
for _, c := range allCfg.Visitors { for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@ -209,20 +258,20 @@ func LoadClientConfig(path string) (
} }
// Load additional config from includes. // Load additional config from includes.
// legacy ini format alredy handle this in ParseClientConfig. // legacy ini format already handle this in ParseClientConfig.
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat { if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat) extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
if err != nil { if err != nil {
return nil, nil, nil, isLegacyFormat, err return nil, nil, nil, isLegacyFormat, err
} }
pxyCfgs = append(pxyCfgs, extPxyCfgs...) proxyCfgs = append(proxyCfgs, extProxyCfgs...)
visitorCfgs = append(visitorCfgs, extVisitorCfgs...) visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
} }
// Filter by start // Filter by start
if len(cliCfg.Start) > 0 { if len(cliCfg.Start) > 0 {
startSet := sets.New(cliCfg.Start...) startSet := sets.New(cliCfg.Start...)
pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
return startSet.Has(c.GetBaseConfig().Name) return startSet.Has(c.GetBaseConfig().Name)
}) })
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
@ -233,17 +282,17 @@ func LoadClientConfig(path string) (
if cliCfg != nil { if cliCfg != nil {
cliCfg.Complete() cliCfg.Complete()
} }
for _, c := range pxyCfgs { for _, c := range proxyCfgs {
c.Complete(cliCfg.User) c.Complete(cliCfg.User)
} }
for _, c := range visitorCfgs { for _, c := range visitorCfgs {
c.Complete(cliCfg) c.Complete(cliCfg)
} }
return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
} }
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
pxyCfgs := make([]v1.ProxyConfigurer, 0) proxyCfgs := make([]v1.ProxyConfigurer, 0)
visitorCfgs := make([]v1.VisitorConfigurer, 0) visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, path := range paths { for _, path := range paths {
absDir, err := filepath.Abs(filepath.Dir(path)) absDir, err := filepath.Abs(filepath.Dir(path))
@ -265,11 +314,11 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched { if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
// support yaml/json/toml // support yaml/json/toml
cfg := v1.ClientConfig{} cfg := v1.ClientConfig{}
if err := LoadConfigureFromFile(absFile, &cfg); err != nil { if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil {
return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err) return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
} }
for _, c := range cfg.Proxies { for _, c := range cfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
} }
for _, c := range cfg.Visitors { for _, c := range cfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@ -277,5 +326,5 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
} }
} }
} }
return pxyCfgs, visitorCfgs, nil return proxyCfgs, visitorCfgs, nil
} }

View File

@ -15,6 +15,8 @@
package config package config
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -22,9 +24,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
) )
func TestLoadConfigure(t *testing.T) { const tomlServerContent = `
require := require.New(t)
content := `
bindAddr = "127.0.0.1" bindAddr = "127.0.0.1"
kcpBindPort = 7000 kcpBindPort = 7000
quicBindPort = 7001 quicBindPort = 7001
@ -33,13 +33,276 @@ custom404Page = "/abc.html"
transport.tcpKeepalive = 10 transport.tcpKeepalive = 10
` `
svrCfg := v1.ServerConfig{} const yamlServerContent = `
err := LoadConfigure([]byte(content), &svrCfg) bindAddr: 127.0.0.1
require.NoError(err) kcpBindPort: 7000
require.EqualValues("127.0.0.1", svrCfg.BindAddr) quicBindPort: 7001
require.EqualValues(7000, svrCfg.KCPBindPort) tcpmuxHTTPConnectPort: 7005
require.EqualValues(7001, svrCfg.QUICBindPort) custom404Page: /abc.html
require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) transport:
require.EqualValues("/abc.html", svrCfg.Custom404Page) tcpKeepalive: 10
require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) `
const jsonServerContent = `
{
"bindAddr": "127.0.0.1",
"kcpBindPort": 7000,
"quicBindPort": 7001,
"tcpmuxHTTPConnectPort": 7005,
"custom404Page": "/abc.html",
"transport": {
"tcpKeepalive": 10
}
}
`
func TestLoadServerConfig(t *testing.T) {
tests := []struct {
name string
content string
}{
{"toml", tomlServerContent},
{"yaml", yamlServerContent},
{"json", jsonServerContent},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
svrCfg := v1.ServerConfig{}
err := LoadConfigure([]byte(test.content), &svrCfg, true)
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)
})
}
}
// Test that loading in strict mode fails when the config is invalid.
func TestLoadServerConfigStrictMode(t *testing.T) {
tests := []struct {
name string
content string
}{
{"toml", tomlServerContent},
{"yaml", yamlServerContent},
{"json", jsonServerContent},
}
for _, strict := range []bool{false, true} {
for _, test := range tests {
t.Run(fmt.Sprintf("%s-strict-%t", test.name, strict), func(t *testing.T) {
require := require.New(t)
// Break the content with an innocent typo
brokenContent := strings.Replace(test.content, "bindAddr", "bindAdur", 1)
svrCfg := v1.ServerConfig{}
err := LoadConfigure([]byte(brokenContent), &svrCfg, strict)
if strict {
require.ErrorContains(err, "bindAdur")
} else {
require.NoError(err)
// BindAddr didn't get parsed because of the typo.
require.EqualValues("", svrCfg.BindAddr)
}
})
}
}
}
func TestRenderWithTemplate(t *testing.T) {
tests := []struct {
name string
content string
want string
}{
{"toml", tomlServerContent, tomlServerContent},
{"yaml", yamlServerContent, yamlServerContent},
{"json", jsonServerContent, jsonServerContent},
{"template numeric", `key = {{ 123 }}`, "key = 123"},
{"template string", `key = {{ "xyz" }}`, "key = xyz"},
{"template quote", `key = {{ printf "%q" "with space" }}`, `key = "with space"`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
got, err := RenderWithTemplate([]byte(test.content), nil)
require.NoError(err)
require.EqualValues(test.want, string(got))
})
}
}
func TestCustomStructStrictMode(t *testing.T) {
require := require.New(t)
proxyStr := `
serverPort = 7000
[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
`
clientCfg := v1.ClientConfig{}
err := LoadConfigure([]byte(proxyStr), &clientCfg, true)
require.NoError(err)
proxyStr += `unknown = "unknown"`
err = LoadConfigure([]byte(proxyStr), &clientCfg, true)
require.Error(err)
visitorStr := `
serverPort = 7000
[[visitors]]
name = "test"
type = "stcp"
bindPort = 6000
serverName = "server"
`
err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
require.NoError(err)
visitorStr += `unknown = "unknown"`
err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
require.Error(err)
pluginStr := `
serverPort = 7000
[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/tmp/uds.sock"
`
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.NoError(err)
pluginStr += `unknown = "unknown"`
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.Error(err)
}
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
// even in strict mode by properly handling dot-prefixed fields
func TestYAMLMergeInStrictMode(t *testing.T) {
require := require.New(t)
yamlContent := `
serverAddr: "127.0.0.1"
serverPort: 7000
.common: &common
type: stcp
secretKey: "test-secret"
localIP: 127.0.0.1
transport:
useEncryption: true
useCompression: true
proxies:
- name: ssh
localPort: 22
<<: *common
- name: web
localPort: 80
<<: *common
`
clientCfg := v1.ClientConfig{}
// This should work in strict mode
err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
require.NoError(err)
// Verify the merge worked correctly
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
require.Len(clientCfg.Proxies, 2)
// Check first proxy
sshProxy := clientCfg.Proxies[0].ProxyConfigurer
require.Equal("ssh", sshProxy.GetBaseConfig().Name)
require.Equal("stcp", sshProxy.GetBaseConfig().Type)
// Check second proxy
webProxy := clientCfg.Proxies[1].ProxyConfigurer
require.Equal("web", webProxy.GetBaseConfig().Name)
require.Equal("stcp", webProxy.GetBaseConfig().Type)
}
// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
func TestOptimizedYAMLProcessing(t *testing.T) {
require := require.New(t)
yamlWithDotFields := []byte(`
serverAddr: "127.0.0.1"
.common: &common
type: stcp
proxies:
- name: test
<<: *common
`)
yamlWithoutDotFields := []byte(`
serverAddr: "127.0.0.1"
proxies:
- name: test
type: tcp
localPort: 22
`)
// Test that YAML without dot fields works in strict mode
clientCfg := v1.ClientConfig{}
err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
// Test that YAML with dot fields still works in strict mode
err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
}
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
func TestYAMLEdgeCases(t *testing.T) {
require := require.New(t)
// Test array at root (should fail for frp config)
arrayYAML := []byte(`
- item1
- item2
`)
clientCfg := v1.ClientConfig{}
err := LoadConfigure(arrayYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test scalar at root (should fail for frp config)
scalarYAML := []byte(`"just a string"`)
err = LoadConfigure(scalarYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test empty object (should work)
emptyYAML := []byte(`{}`)
err = LoadConfigure(emptyYAML, &clientCfg, true)
require.NoError(err)
// Test nested structure without dots (should work)
nestedYAML := []byte(`
serverAddr: "127.0.0.1"
serverPort: 7000
`)
err = LoadConfigure(nestedYAML, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
} }

52
pkg/config/template.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2024 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 (
"fmt"
"github.com/fatedier/frp/pkg/util/util"
)
type NumberPair struct {
First int64
Second int64
}
func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, error) {
firstRangeNumbers, err := util.ParseRangeNumbers(firstRangeStr)
if err != nil {
return nil, err
}
secondRangeNumbers, err := util.ParseRangeNumbers(secondRangeStr)
if err != nil {
return nil, err
}
if len(firstRangeNumbers) != len(secondRangeNumbers) {
return nil, fmt.Errorf("first and second range numbers are not in pairs")
}
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
for i := 0; i < len(firstRangeNumbers); i++ {
pairs = append(pairs, NumberPair{
First: firstRangeNumbers[i],
Second: secondRangeNumbers[i],
})
}
return pairs, nil
}
func parseNumberRange(firstRangeStr string) ([]int64, error) {
return util.ParseRangeNumbers(firstRangeStr)
}

View File

@ -45,15 +45,6 @@ func NewBandwidthQuantity(s string) (BandwidthQuantity, error) {
return q, nil return q, nil
} }
func MustBandwidthQuantity(s string) BandwidthQuantity {
q := BandwidthQuantity{}
err := q.UnmarshalString(s)
if err != nil {
panic(err)
}
return q
}
func (q *BandwidthQuantity) Equal(u *BandwidthQuantity) bool { func (q *BandwidthQuantity) Equal(u *BandwidthQuantity) bool {
if q == nil && u == nil { if q == nil && u == nil {
return true return true
@ -168,18 +159,18 @@ func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
out = append(out, PortsRange{Single: int(singleNum)}) out = append(out, PortsRange{Single: int(singleNum)})
case 2: case 2:
// range numbers // range numbers
min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) minNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("range number is invalid, %v", err) return nil, fmt.Errorf("range number is invalid, %v", err)
} }
max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) maxNum, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("range number is invalid, %v", err) return nil, fmt.Errorf("range number is invalid, %v", err)
} }
if max < min { if maxNum < minNum {
return nil, fmt.Errorf("range number is invalid") return nil, fmt.Errorf("range number is invalid")
} }
out = append(out, PortsRange{Start: int(min), End: int(max)}) out = append(out, PortsRange{Start: int(minNum), End: int(maxNum)})
default: default:
return nil, fmt.Errorf("range number is invalid") return nil, fmt.Errorf("range number is invalid")
} }

View File

@ -58,9 +58,14 @@ type ClientCommonConfig struct {
// set. // set.
Start []string `json:"start,omitempty"` Start []string `json:"start,omitempty"`
Log LogConfig `json:"log,omitempty"` Log LogConfig `json:"log,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"` WebServer WebServerConfig `json:"webServer,omitempty"`
Transport ClientTransportConfig `json:"transport,omitempty"` Transport ClientTransportConfig `json:"transport,omitempty"`
VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
// FeatureGates specifies a set of feature gates to enable or disable.
// This can be used to enable alpha/beta features or disable default features.
FeatureGates map[string]bool `json:"featureGates,omitempty"`
// UDPPacketSize specifies the udp packet size // UDPPacketSize specifies the udp packet size
// By default, this value is 1500 // By default, this value is 1500
@ -111,7 +116,7 @@ type ClientTransportConfig struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux *bool `json:"tcpMux,omitempty"` TCPMux *bool `json:"tcpMux,omitempty"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// QUIC protocol options. // QUIC protocol options.
@ -135,9 +140,15 @@ func (c *ClientTransportConfig) Complete() {
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy")) c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
c.PoolCount = util.EmptyOr(c.PoolCount, 1) c.PoolCount = util.EmptyOr(c.PoolCount, 1)
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60) c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30) if lo.FromPtr(c.TCPMux) {
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) // If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
} else {
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
}
c.QUIC.Complete() c.QUIC.Complete()
c.TLS.Complete() c.TLS.Complete()
} }
@ -198,3 +209,7 @@ type AuthOIDCClientConfig struct {
// this field will be transfer to map[string][]string in OIDC token generator. // this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
} }
type VirtualNetConfig struct {
Address string `json:"address,omitempty"`
}

View File

@ -15,9 +15,23 @@
package v1 package v1
import ( import (
"sync"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
) )
// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method
// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder.
// Here, a global variable is temporarily used to control whether unknown fields are allowed.
// Once the v2 version is implemented by the community, we can switch to a standardized approach.
//
// https://github.com/golang/go/issues/41144
// https://github.com/golang/go/discussions/63397
var (
DisallowUnknownFields = false
DisallowUnknownFieldsMu sync.Mutex
)
type AuthScope string type AuthScope string
const ( const (
@ -83,7 +97,7 @@ type TLSConfig struct {
} }
type LogConfig struct { type LogConfig struct {
// This is destination where frp should wirte the logs. // This is destination where frp should write the logs.
// If "console" is used, logs will be printed to stdout, otherwise, // If "console" is used, logs will be printed to stdout, otherwise,
// logs will be written to the specified file. // logs will be written to the specified file.
// By default, this value is "console". // By default, this value is "console".
@ -115,3 +129,8 @@ type HTTPPluginOptions struct {
type HeaderOperations struct { type HeaderOperations struct {
Set map[string]string `json:"set,omitempty"` Set map[string]string `json:"set,omitempty"`
} }
type HTTPHeader struct {
Name string `json:"name"`
Value string `json:"value"`
}

View File

@ -15,6 +15,7 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -96,6 +97,9 @@ type HealthCheckConfig struct {
// Path specifies the path to send health checks to if the // Path specifies the path to send health checks to if the
// health check type is "http". // health check type is "http".
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
// HTTPHeaders specifies the headers to send with the health request, if
// the health check type is "http".
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
} }
type DomainConfig struct { type DomainConfig struct {
@ -104,9 +108,10 @@ type DomainConfig struct {
} }
type ProxyBaseConfig struct { type ProxyBaseConfig struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Transport ProxyTransport `json:"transport,omitempty"` Annotations map[string]string `json:"annotations,omitempty"`
Transport ProxyTransport `json:"transport,omitempty"`
// metadata info for each proxy // metadata info for each proxy
Metadatas map[string]string `json:"metadatas,omitempty"` Metadatas map[string]string `json:"metadatas,omitempty"`
LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"` LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"`
@ -122,6 +127,10 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1") c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
if c.Plugin.ClientPluginOptions != nil {
c.Plugin.Complete()
}
} }
func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) { func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
@ -137,6 +146,7 @@ func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
m.Group = c.LoadBalancer.Group m.Group = c.LoadBalancer.Group
m.GroupKey = c.LoadBalancer.GroupKey m.GroupKey = c.LoadBalancer.GroupKey
m.Metas = c.Metadatas m.Metas = c.Metadatas
m.Annotations = c.Annotations
} }
func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) { func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) {
@ -153,6 +163,7 @@ func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) {
c.LoadBalancer.Group = m.Group c.LoadBalancer.Group = m.Group
c.LoadBalancer.GroupKey = m.GroupKey c.LoadBalancer.GroupKey = m.GroupKey
c.Metadatas = m.Metas c.Metadatas = m.Metas
c.Annotations = m.Annotations
} }
type TypedProxyConfig struct { type TypedProxyConfig struct {
@ -177,20 +188,28 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
if configurer == nil { if configurer == nil {
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type) return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
} }
if err := json.Unmarshal(b, configurer); err != nil { decoder := json.NewDecoder(bytes.NewBuffer(b))
return err if DisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(configurer); err != nil {
return fmt.Errorf("unmarshal ProxyConfig error: %v", err)
} }
c.ProxyConfigurer = configurer c.ProxyConfigurer = configurer
return nil return nil
} }
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ProxyConfigurer)
}
type ProxyConfigurer interface { type ProxyConfigurer interface {
Complete(namePrefix string) Complete(namePrefix string)
GetBaseConfig() *ProxyBaseConfig GetBaseConfig() *ProxyBaseConfig
// MarshalToMsg marshals this config into a msg.NewProxy message. This // MarshalToMsg marshals this config into a msg.NewProxy message. This
// function will be called on the frpc side. // function will be called on the frpc side.
MarshalToMsg(*msg.NewProxy) MarshalToMsg(*msg.NewProxy)
// UnmarshalFromMsg unmarshals a msg.NewProxy message into this config. // UnmarshalFromMsg unmarshal a msg.NewProxy message into this config.
// This function will be called on the frps side. // This function will be called on the frps side.
UnmarshalFromMsg(*msg.NewProxy) UnmarshalFromMsg(*msg.NewProxy)
} }
@ -224,7 +243,9 @@ func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
if !ok { if !ok {
return nil return nil
} }
return reflect.New(v).Interface().(ProxyConfigurer) pc := reflect.New(v).Interface().(ProxyConfigurer)
pc.GetBaseConfig().Type = string(proxyType)
return pc
} }
var _ ProxyConfigurer = &TCPProxyConfig{} var _ ProxyConfigurer = &TCPProxyConfig{}
@ -278,6 +299,7 @@ type HTTPProxyConfig struct {
HTTPPassword string `json:"httpPassword,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"`
RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"`
} }
@ -291,6 +313,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
m.HTTPUser = c.HTTPUser m.HTTPUser = c.HTTPUser
m.HTTPPwd = c.HTTPPassword m.HTTPPwd = c.HTTPPassword
m.Headers = c.RequestHeaders.Set m.Headers = c.RequestHeaders.Set
m.ResponseHeaders = c.ResponseHeaders.Set
m.RouteByHTTPUser = c.RouteByHTTPUser m.RouteByHTTPUser = c.RouteByHTTPUser
} }
@ -304,6 +327,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
c.HTTPUser = m.HTTPUser c.HTTPUser = m.HTTPUser
c.HTTPPassword = m.HTTPPwd c.HTTPPassword = m.HTTPPwd
c.RequestHeaders.Set = m.Headers c.RequestHeaders.Set = m.Headers
c.ResponseHeaders.Set = m.ResponseHeaders
c.RouteByHTTPUser = m.RouteByHTTPUser c.RouteByHTTPUser = m.RouteByHTTPUser
} }

View File

@ -15,12 +15,46 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/util/util"
) )
type ClientPluginOptions interface{} const (
PluginHTTP2HTTPS = "http2https"
PluginHTTPProxy = "http_proxy"
PluginHTTPS2HTTP = "https2http"
PluginHTTPS2HTTPS = "https2https"
PluginHTTP2HTTP = "http2http"
PluginSocks5 = "socks5"
PluginStaticFile = "static_file"
PluginUnixDomainSocket = "unix_domain_socket"
PluginTLS2Raw = "tls2raw"
PluginVirtualNet = "virtual_net"
)
var clientPluginOptionsTypeMap = map[string]reflect.Type{
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
}
type ClientPluginOptions interface {
Complete()
}
type TypedClientPluginOptions struct { type TypedClientPluginOptions struct {
Type string `json:"type"` Type string `json:"type"`
@ -41,7 +75,7 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
c.Type = typeStruct.Type c.Type = typeStruct.Type
if c.Type == "" { if c.Type == "" {
return nil return errors.New("plugin type is empty")
} }
v, ok := clientPluginOptionsTypeMap[typeStruct.Type] v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
@ -49,72 +83,113 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type) return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
} }
options := reflect.New(v).Interface().(ClientPluginOptions) options := reflect.New(v).Interface().(ClientPluginOptions)
if err := json.Unmarshal(b, options); err != nil {
return err decoder := json.NewDecoder(bytes.NewBuffer(b))
if DisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(options); err != nil {
return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
} }
c.ClientPluginOptions = options c.ClientPluginOptions = options
return nil return nil
} }
const ( func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
PluginHTTP2HTTPS = "http2https" return json.Marshal(c.ClientPluginOptions)
PluginHTTPProxy = "http_proxy"
PluginHTTPS2HTTP = "https2http"
PluginHTTPS2HTTPS = "https2https"
PluginSocks5 = "socks5"
PluginStaticFile = "static_file"
PluginUnixDomainSocket = "unix_domain_socket"
)
var clientPluginOptionsTypeMap = map[string]reflect.Type{
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
} }
type HTTP2HTTPSPluginOptions struct { type HTTP2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
} }
func (o *HTTP2HTTPSPluginOptions) Complete() {}
type HTTPProxyPluginOptions struct { type HTTPProxyPluginOptions struct {
Type string `json:"type,omitempty"`
HTTPUser string `json:"httpUser,omitempty"` HTTPUser string `json:"httpUser,omitempty"`
HTTPPassword string `json:"httpPassword,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"`
} }
func (o *HTTPProxyPluginOptions) Complete() {}
type HTTPS2HTTPPluginOptions struct { type HTTPS2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"` CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"` KeyPath string `json:"keyPath,omitempty"`
} }
func (o *HTTPS2HTTPPluginOptions) Complete() {
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
}
type HTTPS2HTTPSPluginOptions struct { type HTTPS2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"` CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"` KeyPath string `json:"keyPath,omitempty"`
} }
func (o *HTTPS2HTTPSPluginOptions) Complete() {
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
}
type HTTP2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
}
func (o *HTTP2HTTPPluginOptions) Complete() {}
type Socks5PluginOptions struct { type Socks5PluginOptions struct {
Type string `json:"type,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
} }
func (o *Socks5PluginOptions) Complete() {}
type StaticFilePluginOptions struct { type StaticFilePluginOptions struct {
Type string `json:"type,omitempty"`
LocalPath string `json:"localPath,omitempty"` LocalPath string `json:"localPath,omitempty"`
StripPrefix string `json:"stripPrefix,omitempty"` StripPrefix string `json:"stripPrefix,omitempty"`
HTTPUser string `json:"httpUser,omitempty"` HTTPUser string `json:"httpUser,omitempty"`
HTTPPassword string `json:"httpPassword,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"`
} }
func (o *StaticFilePluginOptions) Complete() {}
type UnixDomainSocketPluginOptions struct { type UnixDomainSocketPluginOptions struct {
Type string `json:"type,omitempty"`
UnixPath string `json:"unixPath,omitempty"` UnixPath string `json:"unixPath,omitempty"`
} }
func (o *UnixDomainSocketPluginOptions) Complete() {}
type TLS2RawPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
}
func (o *TLS2RawPluginOptions) Complete() {}
type VirtualNetPluginOptions struct {
Type string `json:"type,omitempty"`
}
func (o *VirtualNetPluginOptions) Complete() {}

View File

@ -67,6 +67,8 @@ type ServerConfig struct {
// value is "", a default page will be displayed. // value is "", a default page will be displayed.
Custom404Page string `json:"custom404Page,omitempty"` Custom404Page string `json:"custom404Page,omitempty"`
SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"` WebServer WebServerConfig `json:"webServer,omitempty"`
// EnablePrometheus will export prometheus metrics on webserver address // EnablePrometheus will export prometheus metrics on webserver address
// in /metrics api. // in /metrics api.
@ -101,6 +103,7 @@ func (c *ServerConfig) Complete() {
c.Log.Complete() c.Log.Complete()
c.Transport.Complete() c.Transport.Complete()
c.WebServer.Complete() c.WebServer.Complete()
c.SSHTunnelGateway.Complete()
c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0") c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
c.BindPort = util.EmptyOr(c.BindPort, 7000) c.BindPort = util.EmptyOr(c.BindPort, 7000)
@ -152,7 +155,7 @@ type ServerTransportConfig struct {
// is true. // is true.
// $HideFromDoc // $HideFromDoc
TCPMux *bool `json:"tcpMux,omitempty"` TCPMux *bool `json:"tcpMux,omitempty"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
@ -173,10 +176,15 @@ type ServerTransportConfig struct {
func (c *ServerTransportConfig) Complete() { func (c *ServerTransportConfig) Complete() {
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60) c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200) c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5) c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) if lo.FromPtr(c.TCPMux) {
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
} else {
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
}
c.QUIC.Complete() c.QUIC.Complete()
if c.TLS.TrustedCaFile != "" { if c.TLS.TrustedCaFile != "" {
c.TLS.Force = true c.TLS.Force = true
@ -189,3 +197,14 @@ type TLSServerConfig struct {
TLSConfig TLSConfig
} }
type SSHTunnelGateway struct {
BindPort int `json:"bindPort,omitempty"`
PrivateKeyFile string `json:"privateKeyFile,omitempty"`
AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"`
AuthorizedKeysFile string `json:"authorizedKeysFile,omitempty"`
}
func (c *SSHTunnelGateway) Complete() {
c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
}

View File

@ -18,10 +18,12 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"github.com/samber/lo" "github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/featuregate"
) )
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
@ -29,7 +31,14 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
warnings Warning warnings Warning
errs error errs error
) )
if !lo.Contains(SupportedAuthMethods, c.Auth.Method) { // validate feature gates
if c.VirtualNet.Address != "" {
if !featuregate.Enabled(featuregate.VirtualNet) {
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
}
}
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
} }
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
@ -63,7 +72,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
} }
if !lo.Contains(SupportedTransportProtocols, c.Transport.Protocol) { if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols)) errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
} }
@ -80,7 +89,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
return warnings, errs return warnings, errs
} }
func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
var warnings Warning var warnings Warning
if c != nil { if c != nil {
warning, err := ValidateClientCommonConfig(c) warning, err := ValidateClientCommonConfig(c)
@ -90,7 +99,7 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigu
} }
} }
for _, c := range pxyCfgs { for _, c := range proxyCfgs {
if err := ValidateProxyConfigurerForClient(c); err != nil { if err := ValidateProxyConfigurerForClient(c); err != nil {
return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err) return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err)
} }

View File

@ -16,8 +16,7 @@ package validation
import ( import (
"fmt" "fmt"
"slices"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
) )
@ -44,7 +43,7 @@ func ValidatePort(port int, fieldPath string) error {
} }
func validateLogConfig(c *v1.LogConfig) error { func validateLogConfig(c *v1.LogConfig) error {
if !lo.Contains(SupportedLogLevels, c.Level) { if !slices.Contains(SupportedLogLevels, c.Level) {
return fmt.Errorf("invalid log level, optional values are %v", SupportedLogLevels) return fmt.Errorf("invalid log level, optional values are %v", SupportedLogLevels)
} }
return nil return nil

View File

@ -32,6 +32,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
return validateStaticFilePluginOptions(v) return validateStaticFilePluginOptions(v)
case *v1.UnixDomainSocketPluginOptions: case *v1.UnixDomainSocketPluginOptions:
return validateUnixDomainSocketPluginOptions(v) return validateUnixDomainSocketPluginOptions(v)
case *v1.TLS2RawPluginOptions:
return validateTLS2RawPluginOptions(v)
} }
return nil return nil
} }
@ -70,3 +72,10 @@ func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions)
} }
return nil return nil
} }
func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error {
if c.LocalAddr == "" {
return errors.New("localAddr is required")
}
return nil
}

View File

@ -17,9 +17,10 @@ package validation
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"github.com/samber/lo" "k8s.io/apimachinery/pkg/util/validation"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
) )
@ -29,11 +30,13 @@ func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
return errors.New("name should not be empty") return errors.New("name should not be empty")
} }
if !lo.Contains([]string{"", "v1", "v2"}, c.Transport.ProxyProtocolVersion) { if err := ValidateAnnotations(c.Annotations); err != nil {
return err
}
if !slices.Contains([]string{"", "v1", "v2"}, c.Transport.ProxyProtocolVersion) {
return fmt.Errorf("not support proxy protocol version: %s", c.Transport.ProxyProtocolVersion) return fmt.Errorf("not support proxy protocol version: %s", c.Transport.ProxyProtocolVersion)
} }
if !slices.Contains([]string{"client", "server"}, c.Transport.BandwidthLimitMode) {
if !lo.Contains([]string{"client", "server"}, c.Transport.BandwidthLimitMode) {
return fmt.Errorf("bandwidth limit mode should be client or server") return fmt.Errorf("bandwidth limit mode should be client or server")
} }
@ -43,7 +46,7 @@ func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
} }
} }
if !lo.Contains([]string{"", "tcp", "http"}, c.HealthCheck.Type) { if !slices.Contains([]string{"", "tcp", "http"}, c.HealthCheck.Type) {
return fmt.Errorf("not support health check type: %s", c.HealthCheck.Type) return fmt.Errorf("not support health check type: %s", c.HealthCheck.Type)
} }
if c.HealthCheck.Type != "" { if c.HealthCheck.Type != "" {
@ -61,7 +64,10 @@ func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
return nil return nil
} }
func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig, s *v1.ServerConfig) error { func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig) error {
if err := ValidateAnnotations(c.Annotations); err != nil {
return err
}
return nil return nil
} }
@ -133,7 +139,7 @@ func validateTCPMuxProxyConfigForClient(c *v1.TCPMuxProxyConfig) error {
return err return err
} }
if !lo.Contains([]string{string(v1.TCPMultiplexerHTTPConnect)}, c.Multiplexer) { if !slices.Contains([]string{string(v1.TCPMultiplexerHTTPConnect)}, c.Multiplexer) {
return fmt.Errorf("not support multiplexer: %s", c.Multiplexer) return fmt.Errorf("not support multiplexer: %s", c.Multiplexer)
} }
return nil return nil
@ -161,7 +167,7 @@ func validateSUDPProxyConfigForClient(c *v1.SUDPProxyConfig) error {
func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error { func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error {
base := c.GetBaseConfig() base := c.GetBaseConfig()
if err := validateProxyBaseConfigForServer(base, s); err != nil { if err := validateProxyBaseConfigForServer(base); err != nil {
return err return err
} }
@ -231,3 +237,34 @@ func validateXTCPProxyConfigForServer(c *v1.XTCPProxyConfig, s *v1.ServerConfig)
func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error { func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error {
return nil return nil
} }
// ValidateAnnotations validates that a set of annotations are correctly defined.
func ValidateAnnotations(annotations map[string]string) error {
if len(annotations) == 0 {
return nil
}
var errs error
for k := range annotations {
for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) {
errs = AppendError(errs, fmt.Errorf("annotation key %s is invalid: %s", k, msg))
}
}
if err := ValidateAnnotationsSize(annotations); err != nil {
errs = AppendError(errs, err)
}
return errs
}
const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB
func ValidateAnnotationsSize(annotations map[string]string) error {
var totalSize int64
for k, v := range annotations {
totalSize += (int64)(len(k)) + (int64)(len(v))
}
if totalSize > (int64)(TotalAnnotationSizeLimitB) {
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB)
}
return nil
}

View File

@ -16,6 +16,7 @@ package validation
import ( import (
"fmt" "fmt"
"slices"
"github.com/samber/lo" "github.com/samber/lo"
@ -27,7 +28,7 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
warnings Warning warnings Warning
errs error errs error
) )
if !lo.Contains(SupportedAuthMethods, c.Auth.Method) { if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
} }
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {

View File

@ -17,8 +17,7 @@ package validation
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
) )
@ -56,7 +55,7 @@ func validateVisitorBaseConfig(c *v1.VisitorBaseConfig) error {
} }
func validateXTCPVisitorConfig(c *v1.XTCPVisitorConfig) error { func validateXTCPVisitorConfig(c *v1.XTCPVisitorConfig) error {
if !lo.Contains([]string{"kcp", "quic"}, c.Protocol) { if !slices.Contains([]string{"kcp", "quic"}, c.Protocol) {
return fmt.Errorf("protocol should be kcp or quic") return fmt.Errorf("protocol should be kcp or quic")
} }
return nil return nil

View File

@ -15,6 +15,7 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -43,6 +44,9 @@ type VisitorBaseConfig struct {
// It can be less than 0, it means don't bind to the port and only receive connections redirected from // 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) // other visitors. (This is not supported for SUDP now)
BindPort int `json:"bindPort,omitempty"` BindPort int `json:"bindPort,omitempty"`
// Plugin specifies what plugin should be used.
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
} }
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
@ -108,19 +112,29 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
if configurer == nil { if configurer == nil {
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type) return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
} }
if err := json.Unmarshal(b, configurer); err != nil { decoder := json.NewDecoder(bytes.NewBuffer(b))
return err if DisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(configurer); err != nil {
return fmt.Errorf("unmarshal VisitorConfig error: %v", err)
} }
c.VisitorConfigurer = configurer c.VisitorConfigurer = configurer
return nil return nil
} }
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(c.VisitorConfigurer)
}
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer { func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
v, ok := visitorConfigTypeMap[t] v, ok := visitorConfigTypeMap[t]
if !ok { if !ok {
return nil return nil
} }
return reflect.New(v).Interface().(VisitorConfigurer) vc := reflect.New(v).Interface().(VisitorConfigurer)
vc.GetBaseConfig().Type = string(t)
return vc
} }
var _ VisitorConfigurer = &STCPVisitorConfig{} var _ VisitorConfigurer = &STCPVisitorConfig{}

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