Compare commits

..

139 Commits

Author SHA1 Message Date
fatedier
4fd6301577 Merge pull request #3537 from fatedier/dev
release v0.51.1
2023-07-20 22:38:48 +08:00
fatedier
efcc028a3d fix a race condition issue (#3536) 2023-07-20 22:32:32 +08:00
fatedier
90861b6821 update golib (#3532) 2023-07-17 17:27:43 +08:00
fatedier
8f105adbca update FUNDING.yml (#3520) 2023-07-06 19:58:50 +08:00
fatedier
53626b370c Merge pull request #3517 from fatedier/dev
bump version to v0.51.0
2023-07-05 20:39:25 +08:00
fatedier
b1789afbab update version (#3516) 2023-07-05 20:35:08 +08:00
fatedier
88c7e8bf7c update doc (#3512) 2023-07-02 00:35:33 +08:00
fatedier
fc4e787fe2 frpc: support stop command (#3511) 2023-06-30 17:35:37 +08:00
fatedier
4c4d5f0d0d service.Run supports passing in context (#3504) 2023-06-29 18:04:20 +08:00
fatedier
801e8c6742 support wss between frpc and frps (#3503) 2023-06-29 11:20:45 +08:00
fatedier
4fd800bc48 Merge pull request #3499 from fatedier/dev
release v0.50.0
2023-06-26 17:03:56 +08:00
fatedier
b146989703 add release notes for v0.50.0 (#3498) 2023-06-26 16:48:14 +08:00
fatedier
685d7618f3 change default value of tls_enable and disable_custom_tls_first_byte (#3494) 2023-06-26 00:10:27 +08:00
fatedier
15a245766e fix the issue of duplicate xtcp proxies will cause the previous proxy to become ineffective (#3489) 2023-06-16 00:41:06 +08:00
fatedier
e1cef053be server/proxy: simplify the code (#3488) 2023-06-16 00:14:19 +08:00
fatedier
9ba6a06470 e2e: update 2023-06-04 20:11:07 +08:00
fatedier
ea08de668e yamux: change default MaxStreamWindowSize to 6MB (#3474) 2023-06-04 19:58:55 +08:00
fatedier
de85c9455a stcp, xtcp, sudp: support allow_users and specified server user (#3472) 2023-06-02 16:06:29 +08:00
fatedier
cceab7e1b1 client/proxy: simplify the code (#3465) 2023-05-30 22:18:56 +08:00
fatedier
9aef3b9944 adjust code style about config (#3464) 2023-05-30 20:25:22 +08:00
fatedier
341a5e3e3a frps dashboard: remove bind_udp_port (#3463) 2023-05-30 15:14:05 +08:00
fatedier
c7a0cfc66d xtcp: when connection timeout occurs, support fallback to STCP (#3460) 2023-05-30 10:55:00 +08:00
fatedier
555db9d272 format import package name (#3455) 2023-05-29 14:10:34 +08:00
fatedier
0d6d968fe8 Merge pull request #3454 from fatedier/dev
release v0.49.0
2023-05-29 01:12:26 +08:00
fatedier
98068402c8 add release notes (#3453) 2023-05-29 01:09:53 +08:00
fatedier
4915852b9c use constant time comparison (#3452) 2023-05-29 00:27:27 +08:00
fatedier
756dd1ad5e add e2e case for xtcp (#3451) 2023-05-28 23:06:35 +08:00
fatedier
c71efde303 refactor the code related to xtcp (#3449) 2023-05-28 16:50:43 +08:00
fatedier
9f029e3248 update Build Status 2023-05-17 15:24:42 +08:00
fatedier
8095075719 update sponsors 2023-05-17 15:22:18 +08:00
Haruka Abe
2225a1781f fix: typo tot => to (#3434)
Co-authored-by: Haruka ABE <abe_haruka@grenge.co.jp>
2023-05-08 21:28:44 +08:00
Aarni Koskela
0214b974dd Fix typo: dasboard -> dashboard (#3416) 2023-04-18 21:25:32 +08:00
fatedier
738c53ce47 web: fix el-popover bug (#3413)
* web: update

* web: fix el-popover bug
2023-04-16 16:43:44 +08:00
hxysayhi
db52f07d34 Update frpc_full.ini (#3399)
Fix typos.
2023-04-12 11:02:56 +08:00
fatedier
f6b8645f56 nathole: support discover without configuration file (#3395) 2023-04-10 21:45:00 +08:00
fatedier
2c2c4ecdbc feat(nathole): add flag for STUN server in nathole discovery command (#3383) 2023-03-30 22:02:00 +08:00
fatedier
3faae194d0 feat(nathole): use serverUDPPort in nathole discovery when available (#3382) 2023-03-30 21:49:12 +08:00
fatedier
a22d6c9504 frpc: support nathole discover (#3381) 2023-03-30 20:28:15 +08:00
fatedier
9800b4cfcf feat(pull request): add pull request template including summary and walkthrough (#3380) 2023-03-30 11:49:31 +08:00
fatedier
8f394dba27 trace log adds join errors (#3352) 2023-03-11 19:34:06 +08:00
fatedier
fccd518512 frpc: add parameter nat_hole_server_addr (#3351) 2023-03-10 15:51:14 +08:00
fatedier
8fb99ef7a9 Merge pull request #3348 from fatedier/dev
bump version
2023-03-08 11:40:31 +08:00
fatedier
968ba4d3a1 update release notes (#3347) 2023-03-08 11:19:52 +08:00
fatedier
862b1642ba tcpmux: support authentication (#3345) 2023-03-07 19:53:32 +08:00
fatedier
54eb704650 e2e: upgrade to ginkgo v2 (#3335) 2023-02-27 14:44:16 +08:00
fatedier
8c6303c1e5 web/frpc: support more info (#3334) 2023-02-26 02:54:53 +08:00
fatedier
871511ba52 sponsor: update (#3330) 2023-02-23 13:22:05 +08:00
fatedier
cb6d7ba7f9 sponsor: add pic 2023-02-23 12:05:47 +08:00
fatedier
31f40aa913 web/frpc&frps: support dark mode (#3327) 2023-02-22 14:15:21 +08:00
fatedier
2f59e967a0 web/frps: more info (#3326) 2023-02-22 00:39:56 +08:00
fatedier
fe8374e99b web/frpc: upgrade vue and element-plus (#3322) 2023-02-20 23:52:55 +08:00
fatedier
24f0b3afa5 web/frps: upgrade vue and element-plus (#3310) 2023-02-16 02:45:48 +08:00
fatedier
39941117b6 support windows arm64 2023-02-10 13:25:04 +08:00
fatedier
6a1f9ad893 update README.md 2023-02-10 01:15:28 +08:00
fatedier
88e74ff24d Merge pull request #3300 from fatedier/dev
sync
2023-02-10 01:12:00 +08:00
fatedier
18ab58eb25 fix incompatible problem for bandwidth_limit_mode 2023-02-10 00:56:20 +08:00
fatedier
534dc99d55 Merge pull request #3299 from fatedier/dev
sync
2023-02-09 23:06:14 +08:00
fatedier
fa0593ae2c add release notes 2023-02-09 22:52:48 +08:00
fatedier
89fff7d11d e2e: add test case for bandwidth_limit_mode server (#3295) 2023-02-09 01:11:00 +08:00
fatedier
38d42dbe4b release: prepare for 0.47.0 (#3296) 2023-02-09 00:59:40 +08:00
Craig O'Donnell
aa31d7ad0b support bandwidth_limit set by server plugin (#3271)
* support bandwidth_limit set by server plugin

* limiter at proxy level

* bandwidth_limit_mode

* updates tests for bandwidth_limit_mode default

* bandwidth_limit_mode as string

* add checkForSrv for bandwidth_limit_mode

* bandwidth_limit flags for sub cmds

* gci write
2023-02-09 00:38:36 +08:00
fatedier
113e3b0b0d revert random change (#3292) 2023-02-06 15:55:33 +08:00
fatedier
100148d925 support go1.20 (#3287) 2023-02-05 20:52:36 +08:00
fatedier
6b3daffaf0 upgrade quic-go and change import path (#3286) 2023-02-02 20:20:17 +08:00
fatedier
5e17bc7bf1 remove sponsors 2023-02-02 12:03:20 +08:00
Gerhard Tan
b1b8d9a82b frpc: do a graceful close for quic protocol at exit (#3282) 2023-02-01 13:09:31 +08:00
fatedier
24c7d1d9e2 clean Release.md 2023-02-01 11:19:31 +08:00
Gerhard Tan
d205c26480 Bugfix: add ipv6 parsing for custom DNS server (#3266) 2023-01-29 23:54:40 +08:00
Richard Kovacs
0eecab06c1 Fix basic example server to join in readme (#3209) 2023-01-15 19:15:48 +08:00
bobo liu
ad3548d332 optimize: GetAuthKey (#3254) 2023-01-14 21:43:26 +08:00
fatedier
595aba5a9b Merge pull request #3248 from fatedier/dev
bump version
2023-01-10 10:26:56 +08:00
fatedier
679992db25 update version (#3247) 2023-01-10 10:24:44 +08:00
fatedier
5cfbb976f4 pkg/util/net: fix quic streams leak (#3246) 2023-01-10 10:19:37 +08:00
Craig O'Donnell
b03f0ad1e6 fix: incorrect op for newWorkConn (#3244)
(cherry picked from commit 53b6bbf0b14ab632dc89416d7fffa9b1d0887c0a)
2023-01-09 10:34:09 +08:00
0x7fff
804f2910fd refactor: ifelse (#3224) 2022-12-22 17:55:06 +08:00
fatedier
a4189ba474 Merge branch 'dev' 2022-12-18 19:27:22 +08:00
fatedier
e2d28d9929 update goreleaser.yaml 2022-12-18 19:18:09 +08:00
fatedier
9ec84f8143 Merge pull request #3218 from fatedier/dev
release v0.46.0
2022-12-18 18:46:52 +08:00
fatedier
7678938c08 support quic options (#3211) 2022-12-18 18:43:42 +08:00
0x7fff
b2e3946800 feat: release (#3205)
Co-authored-by: blizard863 <760076784@qq.com>
2022-12-13 22:37:06 +08:00
0x7fff
af0b7939a7 feat: odic refine (#3202)
Co-authored-by: Matt Feury <mattfeury@gmail.com>
2022-12-12 15:10:38 +08:00
fatedier
2f66dc3e99 support protocol quic between frpc and frps (#3198) 2022-12-12 11:04:10 +08:00
Matt Feury
649df8827c feat: Support OIDC scope parameter (#3192) 2022-12-09 11:46:34 +08:00
Ashish Kurmi
da51adc276 Set least privileged token permission for GitHub Actions (#3155)
Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>
2022-10-31 15:46:46 +08:00
fatedier
e5af37bc8c upgrade github actions version (#3150) 2022-10-27 14:22:14 +08:00
fatedier
8ab474cc97 remove unsupported platform (#3148) 2022-10-27 10:22:47 +08:00
fatedier
e8c8d5903a remove unsupported platform 2022-10-27 10:19:30 +08:00
fatedier
a301046f3d Merge pull request #3147 from fatedier/dev
bump version
2022-10-26 23:18:40 +08:00
fatedier
34ab6b0e74 add release notes 2022-10-26 23:09:17 +08:00
fatedier
cf66ca10b4 improve http group load balancing (#3131) 2022-10-19 12:14:35 +08:00
fatedier
3fbe6b659e adjust sponsors (#3136) 2022-10-19 12:14:09 +08:00
fatedier
6a71d71e58 improve not found response (#3121) 2022-10-09 12:13:27 +08:00
fatedier
6ecc97c857 update deps (#3094) 2022-09-08 17:16:45 +08:00
Abirdcfly
ba492f07c3 chore: remove duplicate word in comments (#3081) 2022-08-29 12:25:36 +08:00
fatedier
9d077b02cf lint by golangci-lint (#3080) 2022-08-29 01:02:53 +08:00
Dingli Zhang
f4e4fbea62 Add support for riscv64 (#3071) 2022-08-23 10:10:04 +08:00
Yonatan Koren
3e721d122b Update README.md (#3062) 2022-08-16 20:47:56 +08:00
chenjiayao
1bc899ec12 [client] Remove redundant function parameters (#3016) 2022-07-14 15:31:32 +08:00
ChristLZS
6f2571980c [client] Fixed a bug where service loops reconnection after disconnection.service is shut down and can not exit goroutine (#3012)
Co-authored-by: lizhisheng <zhishengli@deepglint.com>
2022-07-12 18:43:58 +08:00
fatedier
8888610d83 Merge pull request #3010 from fatedier/dev
release v0.44.0
2022-07-11 00:10:43 +08:00
fatedier
fa7c05c617 release note for v0.44.0 2022-07-11 00:06:57 +08:00
EMRE ÇELİK
218b354f82 Server Dashboard SSL Support (#2982) 2022-06-27 10:08:02 +08:00
fatedier
c652b8ef07 fix ipv6 address parsing (#2978) 2022-06-14 14:24:34 +08:00
fatedier
5b8b145577 Use auto generated certificates if plugin_key_path and plugin_crt_path are empty for plugin https2https and https2http. (#2968) 2022-06-05 17:15:28 +08:00
fatedier
fe5fb0326b Merge pull request #2955 from fatedier/dev
bump version to v0.43.0
2022-05-27 16:27:19 +08:00
fatedier
0711295b0a release note for v0.43.0 (#2954) 2022-05-27 16:02:36 +08:00
fatedier
4af85da0c2 type http/tcpmux proxy support route_by_http_user, tcpmux support passthourgh mode (#2932) 2022-05-26 23:57:30 +08:00
fatedier
bd89eaba2f remove systemd files 2022-04-29 21:31:48 +08:00
fatedier
a72259c604 docker build&push: some adjustments 2022-04-29 01:15:42 +08:00
蓝云Reyes
44eb513f05 Update docker image build file (#2892)
* update docker image building
2022-04-29 01:12:07 +08:00
fatedier
eb1e19a821 Merge pull request #2906 from fatedier/dev
bump version
2022-04-22 11:32:27 +08:00
fatedier
6c658586f6 bump version to v0.42.0 2022-04-22 11:15:23 +08:00
fatedier
888ed25314 dependency: update github.com/pires/go-proxyproto to v0.6.2 (#2894) 2022-04-15 11:36:00 +08:00
fatedier
21240ed962 some improvements 2022-04-14 11:24:36 +08:00
Colin Adler
6481870d03 fix: data races when accessing github.com/fatedier/frp/client.(*Service).ctl (#2891)
* fix: data race in client/service.go

* review fixes
2022-04-14 11:14:19 +08:00
fatedier
a7a4ba270d fix error parsing env values (#2886) 2022-04-05 12:48:57 +08:00
cui fliter
915d9f4c09 fix some typos (#2882)
Signed-off-by: cuishuang <imcusg@gmail.com>
2022-04-02 17:35:51 +08:00
fatedier
18a2af4703 frpc: support multiple confs (#2873) 2022-03-28 12:12:35 +08:00
fatedier
305e40fa8a update .goreleaser.yml 2022-03-23 21:47:43 +08:00
fatedier
10f2620131 Merge pull request #2869 from fatedier/dev
bump version to v0.41.0
2022-03-23 21:19:59 +08:00
fatedier
4acae540c8 support go1.18 and remove go1.16 (#2868) 2022-03-23 21:15:01 +08:00
fatedier
11b13533a0 add release note (#2867) 2022-03-23 20:14:55 +08:00
fatedier
100d556336 support tcp keepalive params (#2863) 2022-03-22 19:29:30 +08:00
Blizard
452fe25cc6 feat: SUDP alway reconnect and print too much log when no data ready (#2844)
* feat: random sleep duration before reconnecting

* fix: bug
2022-03-17 12:03:20 +08:00
fatedier
63efa6b776 support pprof (#2849) 2022-03-17 11:42:59 +08:00
fatedier
37c27169ac workflows: update stale action (#2846) 2022-03-15 11:53:14 +08:00
fatedier
ce677820c6 Merge pull request #2834 from fatedier/dev
bump version
2022-03-11 19:51:32 +08:00
fatedier
1f88a7a0b8 bump version to v0.40.0 (#2833) 2022-03-11 19:45:34 +08:00
Johan Hernefeldt
eeea7602d9 bugfix: Issue #2831 - Cant connect to frps behind ingress with tls (#2832)
Co-authored-by: Johan Hernefeldt <johan.hernefeldt@moralis.io>
2022-03-11 14:51:47 +08:00
Harry Cheng
bf635c0e90 Notify server plugins when a proxy is closed (#2823)
* add close proxy op

* Move to actual closing routine

* Fix e2e tests for CloseProxy

* Add warning on resource exhaustion

* Add CloseProxy to manual close

* retuen errors to `CloseProxy` callers
2022-03-08 15:08:09 +08:00
Blizard
cd31359a27 feat: support add additional params for OIDC (#2814)
* feat: support add additional params and test access by auth0

* fix: config name

Co-authored-by: blizard863 <760076784@qq.com>
2022-03-07 14:23:49 +08:00
fatedier
19739ed31a random sleep duration before reconnecting (#2816) 2022-02-24 11:59:36 +08:00
fatedier
10100c28d9 client: add dial_server_timeout (#2805) 2022-02-19 16:49:21 +08:00
fatedier
88fcc079e8 Merge pull request #2792 from fatedier/dev
bump version
2022-02-09 16:11:20 +08:00
fatedier
ddc1e163c4 update README 2022-02-09 15:42:34 +08:00
fatedier
d20a6d3d75 update release note 2022-02-09 15:23:01 +08:00
fatedier
6194273615 use net.JoinHostPort instead of fmt.Sprintf (#2791) 2022-02-09 15:19:35 +08:00
fatedier
b2311e55e7 add new sponsor logo (#2785) 2022-01-28 15:29:43 +08:00
fatedier
07873d471f doc: update donation section (#2783) 2022-01-26 20:56:00 +08:00
278 changed files with 16057 additions and 18324 deletions

View File

@@ -2,14 +2,16 @@ version: 2
jobs: jobs:
go-version-latest: go-version-latest:
docker: docker:
- image: cimg/go:1.17-node - image: cimg/go:1.20-node
resource_class: large
steps: steps:
- checkout - checkout
- run: make - run: make
- run: make alltest - run: make alltest
go-version-last: go-version-last:
docker: docker:
- image: cimg/go:1.16-node - image: cimg/go:1.19-node
resource_class: large
steps: steps:
- checkout - checkout
- run: make - run: make

1
.github/FUNDING.yml vendored
View File

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

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

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

View File

@@ -9,69 +9,25 @@ on:
description: 'Image tag' description: 'Image tag'
required: true required: true
default: 'test' default: 'test'
permissions:
contents: read
jobs: jobs:
binary:
name: Build Golang project
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: 1.17
- run: |
# https://github.com/actions/setup-go/issues/107
cp -f `which go` /usr/bin/go
- run: go version
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build
run: make build
- name: Archive artifacts for frpc
uses: actions/upload-artifact@v1
with:
name: frpc
path: bin/frpc
- name: Archive artifacts for frps
uses: actions/upload-artifact@v1
with:
name: frps
path: bin/frps
image: image:
name: Build Image from Dockerfile and binaries name: Build Image from Dockerfile and binaries
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: binary
steps: steps:
# environment # environment
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: '0' fetch-depth: '0'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
# download binaries of frpc and frps
- name: Download binary of frpc
uses: actions/download-artifact@v2
with:
name: frpc
path: bin/frpc
- name: Download binary of frps
uses: actions/download-artifact@v2
with:
name: frps
path: bin/frps
# get image tag name # get image tag name
- name: Get Image Tag Name - name: Get Image Tag Name
@@ -81,6 +37,18 @@ jobs:
else else
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi fi
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to the GPR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GPR_TOKEN }}
# prepare image tags # prepare image tags
- name: Prepare Image Tags - name: Prepare Image Tags
@@ -92,26 +60,24 @@ jobs:
echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
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
# build images - name: Build and push frpc
- name: Build Images uses: docker/build-push-action@v3
run: | with:
# for Docker hub context: .
docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC }} . file: ./dockerfiles/Dockerfile-for-frpc
docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS }} . platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
# for GPR push: true
docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC_GPR }} . tags: |
docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS_GPR }} . ${{ env.TAG_FRPC }}
${{ env.TAG_FRPC_GPR }}
# push to dockerhub - name: Build and push frps
- name: Publish to Dockerhub uses: docker/build-push-action@v3
run: | with:
echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin context: .
docker push ${{ env.TAG_FRPC }} file: ./dockerfiles/Dockerfile-for-frps
docker push ${{ env.TAG_FRPS }} platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
push: true
# push to gpr tags: |
- name: Publish to GPR ${{ env.TAG_FRPS }}
run: | ${{ env.TAG_FRPS_GPR }}
echo ${{ secrets.GPR_TOKEN }} | docker login ghcr.io --username ${{ github.repository_owner }} --password-stdin
docker push ${{ env.TAG_FRPC_GPR }}
docker push ${{ env.TAG_FRPS_GPR }}

41
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: golangci-lint
on:
push:
branches:
- master
- dev
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: '1.20'
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.51
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
# skip-cache: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true

View File

@@ -8,25 +8,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: 1.17 go-version: '1.20'
- run: |
# https://github.com/actions/setup-go/issues/107
cp -f `which go` /usr/bin/go
- name: Make All - name: Make All
run: | run: |
./package.sh ./package.sh
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v3
with: with:
version: latest version: latest
args: release --rm-dist --release-notes=./Release.md args: release --rm-dist --release-notes=./Release.md

View File

@@ -8,15 +8,21 @@ on:
description: 'In debug mod' description: 'In debug mod'
required: false required: false
default: 'false' default: 'false'
permissions:
contents: read
jobs: jobs:
stale: stale:
permissions:
issues: write # for actions/stale to close stale issues
pull-requests: write # for actions/stale to close stale PRs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v6
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.' 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: '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 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'
@@ -24,3 +30,5 @@ jobs:
days-before-stale: 30 days-before-stale: 30
days-before-close: 7 days-before-close: 7
debug-only: ${{ github.event.inputs.debug-only }} debug-only: ${{ github.event.inputs.debug-only }}
exempt-all-pr-milestones: true
exempt-all-pr-assignees: true

2
.gitignore vendored
View File

@@ -29,8 +29,10 @@ packages/
release/ release/
test/bin/ test/bin/
vendor/ vendor/
lastversion/
dist/ dist/
.idea/ .idea/
.vscode/
# Cache # Cache
*.swp *.swp

144
.golangci.yml Normal file
View File

@@ -0,0 +1,144 @@
service:
golangci-lint-version: 1.51.x # use the fixed version to not introduce new linters unexpectedly
run:
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 20m
build-tags:
- integ
- 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:
disable-all: true
enable:
- unused
- errcheck
- exportloopref
- gocritic
- gofumpt
- goimports
- revive
- gosimple
- govet
- ineffassign
- lll
- misspell
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- gci
- gosec
- asciicheck
- prealloc
- predeclared
- makezero
fast: false
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
check-shadowing: false
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- cancelled
- marshalled
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 160
# tab width in spaces. Default to 1.
tab-width: 1
gocritic:
disabled-checks:
- exitAfterDefer
unused:
check-exported: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
gci:
sections:
- standard
- default
- prefix(github.com/fatedier/frp/)
gosec:
severity: "low"
confidence: "low"
excludes:
- G102
- G112
- G306
- G401
- G402
- G404
- G501
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
# exclude:
# - composite literal uses unkeyed fields
exclude-rules:
# Exclude some linters from running on test files.
- path: _test\.go$|^tests/|^samples/
linters:
- errcheck
- maligned
# keep it until we only support go1.20
- linters:
- staticcheck
text: "SA1019: rand.Seed has been deprecated"
# 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

View File

@@ -1,7 +1,10 @@
builds: builds:
- skip: true - skip: true
checksum: checksum:
name_template: 'checksums.txt' name_template: '{{ .ProjectName }}_sha256_checksums.txt'
algorithm: sha256
extra_files:
- glob: ./release/packages/*
release: release:
# Same as for github # Same as for github
# Note: it can only be one: either github, gitlab or gitea # Note: it can only be one: either github, gitlab or gitea

View File

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

View File

@@ -2,7 +2,7 @@ export PATH := $(GOPATH)/bin:$(PATH)
export GO111MODULE=on export GO111MODULE=on
LDFLAGS := -s -w LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
all: build all: build
@@ -23,3 +23,5 @@ app:
@mv ./release/frps_windows_386 ./release/frps_windows_386.exe @mv ./release/frps_windows_386 ./release/frps_windows_386.exe
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe @mv ./release/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
@mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe
@mv ./release/frps_windows_arm64 ./release/frps_windows_arm64.exe

262
README.md
View File

@@ -1,4 +1,3 @@
# frp # frp
[![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)
@@ -6,40 +5,36 @@
[README](README.md) | [中文文档](README_zh.md) [README](README.md) | [中文文档](README_zh.md)
## What is frp? <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start-->
frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the Internet. As of now, it supports **TCP** and **UDP**, as well as **HTTP** and **HTTPS** protocols, where requests can be forwarded to internal services by domain name.
frp also has a P2P connect mode.
<h3 align="center">Platinum Sponsors</h3>
<!--platinum sponsors start-->
<p align="center"> <p align="center">
<a href="https://www.doppler.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank"> <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_doppler.png"> <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a> </a>
</p> </p>
<!--gold sponsors end-->
<!--platinum sponsors end--> ## What is frp?
<h3 align="center">Silver Sponsors</h3> frp is a fast reverse proxy that allows you to expose a local server located behind a NAT or firewall to the Internet. It currently supports **TCP** and **UDP**, as well as **HTTP** and **HTTPS** protocols, enabling requests to be forwarded to internal services via domain name.
* Sakura Frp - 欢迎点击 "加入我们" frp also offers a P2P connect mode.
## Table of Contents ## Table of Contents
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Development Status](#development-status) * [Development Status](#development-status)
* [About V2](#about-v2)
* [Architecture](#architecture) * [Architecture](#architecture)
* [Example Usage](#example-usage) * [Example Usage](#example-usage)
* [Access your computer in LAN by SSH](#access-your-computer-in-lan-by-ssh) * [Access your computer in a LAN network via SSH](#access-your-computer-in-a-lan-network-via-ssh)
* [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains) * [Multiple SSH services sharing the same port](#multiple-ssh-services-sharing-the-same-port)
* [Forward DNS query request](#forward-dns-query-request) * [Accessing Internal Web Services with Custom Domains in LAN](#accessing-internal-web-services-with-custom-domains-in-lan)
* [Forward Unix domain socket](#forward-unix-domain-socket) * [Forward DNS query requests](#forward-dns-query-requests)
* [Forward Unix Domain Socket](#forward-unix-domain-socket)
* [Expose a simple HTTP file server](#expose-a-simple-http-file-server) * [Expose a simple HTTP file server](#expose-a-simple-http-file-server)
* [Enable HTTPS for local HTTP(S) service](#enable-https-for-local-https-service) * [Enable HTTPS for a local HTTP(S) service](#enable-https-for-a-local-https-service)
* [Expose your service privately](#expose-your-service-privately) * [Expose your service privately](#expose-your-service-privately)
* [P2P Mode](#p2p-mode) * [P2P Mode](#p2p-mode)
* [Features](#features) * [Features](#features)
@@ -63,6 +58,7 @@ frp also has a P2P connect mode.
* [For Each Proxy](#for-each-proxy) * [For Each Proxy](#for-each-proxy)
* [TCP Stream Multiplexing](#tcp-stream-multiplexing) * [TCP Stream Multiplexing](#tcp-stream-multiplexing)
* [Support KCP Protocol](#support-kcp-protocol) * [Support KCP Protocol](#support-kcp-protocol)
* [Support QUIC Protocol](#support-quic-protocol)
* [Connection Pooling](#connection-pooling) * [Connection Pooling](#connection-pooling)
* [Load balancing](#load-balancing) * [Load balancing](#load-balancing)
* [Service Health Check](#service-health-check) * [Service Health Check](#service-health-check)
@@ -82,19 +78,32 @@ frp also has a P2P connect mode.
* [Development Plan](#development-plan) * [Development Plan](#development-plan)
* [Contributing](#contributing) * [Contributing](#contributing)
* [Donation](#donation) * [Donation](#donation)
* [AliPay](#alipay) * [GitHub Sponsors](#github-sponsors)
* [Wechat Pay](#wechat-pay)
* [PayPal](#paypal) * [PayPal](#paypal)
<!-- vim-markdown-toc --> <!-- vim-markdown-toc -->
## Development Status ## Development Status
frp is under development. Try the latest release version in the `master` branch, or use the `dev` branch for the version in development. frp is currently under development. You can try the latest release version in the `master` branch, or use the `dev` branch to access the version currently in development.
We are working on v2 version and trying to do some code refactor and improvements. It won't be compatible with v1. We are currently working on version 2 and attempting to perform some code refactoring and improvements. However, please note that it will not be compatible with version 1.
We will switch v0 to v1 at the right time and only accept bug fixes and improvements instead of big feature requirements. We will transition from version 0 to version 1 at the appropriate time and will only accept bug fixes and improvements, rather than big feature requests.
### About V2
The overall situation is currently unfavorable, and there is significant pressure in both personal and professional aspects.
The complexity and difficulty of the v2 version are much higher than anticipated. I can only work on its development during fragmented time periods, and the constant interruptions disrupt productivity significantly. Given this situation, we will continue to optimize and iterate on the current version until we have more free time to proceed with the major version overhaul.
The concept behind v2 is based on my years of experience and reflection in the cloud-native domain, particularly in K8s and ServiceMesh. Its core is a modernized four-layer and seven-layer proxy, similar to envoy. This proxy itself is highly scalable, not only capable of implementing the functionality of intranet penetration but also applicable to various other domains. Building upon this highly scalable core, we aim to implement all the capabilities of frp v1 while also addressing the functionalities that were previously unachievable or difficult to implement in an elegant manner. Furthermore, we will maintain efficient development and iteration capabilities.
In addition, I envision frp itself becoming a highly extensible system and platform, similar to how we can provide a range of extension capabilities based on K8s. In K8s, we can customize development according to enterprise needs, utilizing features such as CRD, controller mode, webhook, CSI, and CNI. In frp v1, we introduced the concept of server plugins, which implemented some basic extensibility. However, it relies on a simple HTTP protocol and requires users to start independent processes and manage them on their own. This approach is far from flexible and convenient, and real-world demands vary greatly. It is unrealistic to expect a non-profit open-source project maintained by a few individuals to meet everyone's needs.
Finally, we acknowledge that the current design of modules such as configuration management, permission verification, certificate management, and API management is not modern enough. While we may carry out some optimizations in the v1 version, ensuring compatibility remains a challenging issue that requires a considerable amount of effort to address.
We sincerely appreciate your support for frp.
## Architecture ## Architecture
@@ -102,15 +111,15 @@ We will switch v0 to v1 at the right time and only accept bug fixes and improvem
## Example Usage ## Example Usage
Firstly, download the latest programs from [Release](https://github.com/fatedier/frp/releases) page according to your operating system and architecture. To begin, download the latest program for your operating system and architecture from the [Release](https://github.com/fatedier/frp/releases) page.
Put `frps` and `frps.ini` onto your server A with public IP. Next, place the `frps` binary and `frps.ini` configuration file on Server A, which has a public IP address.
Put `frpc` and `frpc.ini` onto your server B in LAN (that can't be connected from public Internet). Finally, place the `frpc` binary and `frpc.ini` configuration file on Server B, which is located on a LAN that cannot be directly accessed from the public internet.
### Access your computer in LAN by SSH ### Access your computer in a LAN network via SSH
1. Modify `frps.ini` on server A and set the `bind_port` to be connected to frp clients: 1. Modify `frps.ini` on server A by setting the `bind_port` for frp clients to connect to:
```ini ```ini
# frps.ini # frps.ini
@@ -122,7 +131,7 @@ Put `frpc` and `frpc.ini` onto your server B in LAN (that can't be connected fro
`./frps -c ./frps.ini` `./frps -c ./frps.ini`
3. On server B, modify `frpc.ini` to put in your `frps` server public IP as `server_addr` field: 3. Modify `frpc.ini` on server B and set the `server_addr` field to the public IP address of your frps server:
```ini ```ini
# frpc.ini # frpc.ini
@@ -137,23 +146,73 @@ Put `frpc` and `frpc.ini` onto your server B in LAN (that can't be connected fro
remote_port = 6000 remote_port = 6000
``` ```
Note that `local_port` (listened on client) and `remote_port` (exposed on server) are for traffic goes in/out the frp system, whereas `server_port` is used between frps. Note that the `local_port` (listened on the client) and `remote_port` (exposed on the server) are used for traffic going in and out of the frp system, while the `server_port` is used for communication between frps and frpc.
4. Start `frpc` on server B: 4. Start `frpc` on server B:
`./frpc -c ./frpc.ini` `./frpc -c ./frpc.ini`
5. From another machine, SSH to server B like this (assuming that username is `test`): 5. To access server B from another machine through server A via SSH (assuming the username is `test`), use the following command:
`ssh -oPort=6000 test@x.x.x.x` `ssh -oPort=6000 test@x.x.x.x`
### Visit your web service in LAN by custom domains ### Multiple SSH services sharing the same port
Sometimes we want to expose a local web service behind a NAT network to others for testing with your own domain name and unfortunately we can't resolve a domain name to a local IP. This example implements multiple SSH services exposed through the same port using a proxy of type tcpmux. Similarly, as long as the client supports the HTTP Connect proxy connection method, port reuse can be achieved in this way.
However, we can expose an HTTP(S) service using frp. 1. Deploy frps on a machine with a public IP and modify the frps.ini file. Here is a simplified configuration:
1. Modify `frps.ini`, set the vhost HTTP port to 8080: ```ini
[common]
bind_port = 7000
tcpmux_httpconnect_port = 5002
```
2. Deploy frpc on the internal machine A with the following configuration:
```ini
[common]
server_addr = x.x.x.x
server_port = 7000
[ssh1]
type = tcpmux
multiplexer = httpconnect
custom_domains = machine-a.example.com
local_ip = 127.0.0.1
local_port = 22
```
3. Deploy another frpc on the internal machine B with the following configuration:
```ini
[common]
server_addr = x.x.x.x
server_port = 7000
[ssh2]
type = tcpmux
multiplexer = httpconnect
custom_domains = machine-b.example.com
local_ip = 127.0.0.1
local_port = 22
```
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`
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`
### Accessing Internal Web Services with Custom Domains in LAN
Sometimes we need to expose a local web service behind a NAT network to others for testing purposes with our own domain name.
Unfortunately, we cannot resolve a domain name to a local IP. However, we can use frp to expose an HTTP(S) service.
1. Modify `frps.ini` and set the HTTP port for vhost to 8080:
```ini ```ini
# frps.ini # frps.ini
@@ -162,11 +221,13 @@ However, we can expose an HTTP(S) service using frp.
vhost_http_port = 8080 vhost_http_port = 8080
``` ```
If you want to configure an https proxy, you need to set up the `vhost_https_port`.
2. Start `frps`: 2. Start `frps`:
`./frps -c ./frps.ini` `./frps -c ./frps.ini`
3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. The `local_port` is the port of your web service: 3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. Specify the `local_port` of your web service:
```ini ```ini
# frpc.ini # frpc.ini
@@ -184,11 +245,11 @@ However, we can expose an HTTP(S) service using frp.
`./frpc -c ./frpc.ini` `./frpc -c ./frpc.ini`
5. Resolve A record of `www.example.com` to the public IP of the remote frps server or CNAME record to your origin domain. 5. Map the A record of `www.example.com` to either the public IP of the remote frps server or a CNAME record pointing to your original domain.
6. Now visit your local web service using url `http://www.example.com:8080`. 6. Visit your local web service using url `http://www.example.com:8080`.
### Forward DNS query request ### Forward DNS query requests
1. Modify `frps.ini`: 1. Modify `frps.ini`:
@@ -202,7 +263,7 @@ However, we can expose an HTTP(S) service using frp.
`./frps -c ./frps.ini` `./frps -c ./frps.ini`
3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server, forward DNS query request to Google Public DNS server `8.8.8.8:53`: 3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. Forward DNS query requests to the Google Public DNS server `8.8.8.8:53`:
```ini ```ini
# frpc.ini # frpc.ini
@@ -221,17 +282,17 @@ However, we can expose an HTTP(S) service using frp.
`./frpc -c ./frpc.ini` `./frpc -c ./frpc.ini`
5. Test DNS resolution using `dig` command: 5. Test DNS resolution using the `dig` command:
`dig @x.x.x.x -p 6000 www.google.com` `dig @x.x.x.x -p 6000 www.google.com`
### Forward Unix domain socket ### Forward Unix Domain Socket
Expose a Unix domain socket (e.g. the Docker daemon socket) as TCP. Expose a Unix domain socket (e.g. the Docker daemon socket) as TCP.
Configure `frps` same as above. Configure `frps` as above.
1. Start `frpc` with configuration: 1. Start `frpc` with the following configuration:
```ini ```ini
# frpc.ini # frpc.ini
@@ -246,17 +307,17 @@ Configure `frps` same as above.
plugin_unix_path = /var/run/docker.sock plugin_unix_path = /var/run/docker.sock
``` ```
2. Test: Get Docker version using `curl`: 2. Test the configuration by getting the docker version using `curl`:
`curl http://x.x.x.x:6000/version` `curl http://x.x.x.x:6000/version`
### Expose a simple HTTP file server ### Expose a simple HTTP file server
Browser your files stored in the LAN, from public Internet. Expose a simple HTTP file server to access files stored in the LAN from the public Internet.
Configure `frps` same as above. Configure `frps` as described above, then:
1. Start `frpc` with configuration: 1. Start `frpc` with the following configuration:
```ini ```ini
# frpc.ini # frpc.ini
@@ -274,19 +335,20 @@ Configure `frps` same as above.
plugin_http_passwd = abc plugin_http_passwd = abc
``` ```
2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct user and password to view files in `/tmp/files` on the `frpc` machine. 2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct username and password to view files in `/tmp/files` on the `frpc` machine.
### Enable HTTPS for local HTTP(S) service ### Enable HTTPS for a local HTTP(S) service
You may substitute `https2https` for the plugin, and point the `plugin_local_addr` to a HTTPS endpoint. You may substitute `https2https` for the plugin, and point the `plugin_local_addr` to a HTTPS endpoint.
1. Start `frpc` with configuration: 1. Start `frpc` with the following configuration:
```ini ```ini
# frpc.ini # frpc.ini
[common] [common]
server_addr = x.x.x.x server_addr = x.x.x.x
server_port = 7000 server_port = 7000
vhost_https_port = 443
[test_https2http] [test_https2http]
type = https type = https
@@ -304,7 +366,7 @@ You may substitute `https2https` for the plugin, and point the `plugin_local_add
### Expose your service privately ### Expose your service privately
Some services will be at risk if exposed directly to the public network. With **STCP** (secret TCP) mode, a preshared key is needed to access the service from another client. To mitigate risks associated with exposing certain services directly to the public network, STCP (Secret TCP) mode requires a preshared key to be used for access to the service from other clients.
Configure `frps` same as above. Configure `frps` same as above.
@@ -346,24 +408,19 @@ Configure `frps` same as above.
### P2P Mode ### P2P Mode
**xtcp** is designed for transmitting large amounts of data directly between clients. A frps server is still needed, as P2P here only refers the actual data transmission. **xtcp** is designed to transmit large amounts of data directly between clients. A frps server is still needed, as P2P here only refers to the actual data transmission.
Note it can't penetrate all types of NAT devices. You might want to fallback to **stcp** if **xtcp** doesn't work. Note that it may not work with all types of NAT devices. You might want to fallback to stcp if xtcp doesn't work.
1. In `frps.ini` configure a UDP port for xtcp: 1. Start `frpc` on machine B, and expose the SSH port. Note that the `remote_port` field is removed:
```ini
# frps.ini
bind_udp_port = 7001
```
2. Start `frpc` on machine B, expose the SSH port. Note that `remote_port` field is removed:
```ini ```ini
# frpc.ini # frpc.ini
[common] [common]
server_addr = x.x.x.x server_addr = x.x.x.x
server_port = 7000 server_port = 7000
# set up a new stun server if the default one is not available.
# nat_hole_stun_server = xxx
[p2p_ssh] [p2p_ssh]
type = xtcp type = xtcp
@@ -372,13 +429,15 @@ Note it can't penetrate all types of NAT devices. You might want to fallback to
local_port = 22 local_port = 22
``` ```
3. Start another `frpc` (typically on another machine C) with the config to connect to SSH using P2P mode: 2. Start another `frpc` (typically on another machine C) with the configuration to connect to SSH using P2P mode:
```ini ```ini
# frpc.ini # frpc.ini
[common] [common]
server_addr = x.x.x.x server_addr = x.x.x.x
server_port = 7000 server_port = 7000
# set up a new stun server if the default one is not available.
# nat_hole_stun_server = xxx
[p2p_ssh_visitor] [p2p_ssh_visitor]
type = xtcp type = xtcp
@@ -387,9 +446,11 @@ Note it can't penetrate all types of NAT devices. You might want to fallback to
sk = abcdefg sk = abcdefg
bind_addr = 127.0.0.1 bind_addr = 127.0.0.1
bind_port = 6000 bind_port = 6000
# when automatic tunnel persistence is required, set it to true
keep_tunnel_open = false
``` ```
4. On machine C, connect to SSH on machine B, using this command: 3. On machine C, connect to SSH on machine B, using this command:
`ssh -oPort=6000 127.0.0.1` `ssh -oPort=6000 127.0.0.1`
@@ -467,6 +528,21 @@ dashboard_pwd = admin
Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`. Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`.
Additionally, you can use HTTPS port by using your domains wildcard or normal SSL certificate:
```ini
[common]
dashboard_port = 7500
# dashboard's username and password are both optional
dashboard_user = admin
dashboard_pwd = admin
dashboard_tls_mode = true
dashboard_tls_cert_file = server.crt
dashboard_tls_key_file = server.key
```
Then visit `https://[server_addr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`.
![dashboard](/doc/pic/dashboard.png) ![dashboard](/doc/pic/dashboard.png)
### Admin UI ### Admin UI
@@ -555,11 +631,9 @@ use_compression = true
#### TLS #### TLS
frp supports the TLS protocol between `frpc` and `frps` since v0.25.0. Since v0.50.0, the default value of `tls_enable` and `disable_custom_tls_first_byte` has been changed to true, and tls is enabled by default.
For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. This only takes effect when you set `disable_custom_tls_first_byte` to false.
Configure `tls_enable = true` in the `[common]` section to `frpc.ini` to enable this feature.
To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.** To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.**
@@ -574,7 +648,6 @@ tls_trusted_ca_file = ca.crt
**`frps` TLS settings (under the `[common]` section):** **`frps` TLS settings (under the `[common]` section):**
```ini ```ini
tls_only = true tls_only = true
tls_enable = true
tls_cert_file = certificate.crt tls_cert_file = certificate.crt
tls_key_file = certificate.key tls_key_file = certificate.key
tls_trusted_ca_file = ca.crt tls_trusted_ca_file = ca.crt
@@ -631,7 +704,7 @@ openssl req -new -sha256 -key server.key \
-config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com")) \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com")) \
-out server.csr -out server.csr
openssl x509 -req -days 365 \ openssl x509 -req -days 365 -sha256 \
-in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \ -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \
-out server.crt -out server.crt
@@ -646,7 +719,7 @@ openssl req -new -sha256 -key client.key \
-config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:client.com,DNS:example.client.com")) \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:client.com,DNS:example.client.com")) \
-out client.csr -out client.csr
openssl x509 -req -days 365 \ openssl x509 -req -days 365 -sha256 \
-in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=DNS:client.com,DNS:example.client.com") \ -extfile <(printf "subjectAltName=DNS:client.com,DNS:example.client.com") \
-out client.crt -out client.crt
@@ -706,6 +779,8 @@ bandwidth_limit = 1MB
Set `bandwidth_limit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`. Set `bandwidth_limit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`.
Set `bandwidth_limit_mode` to `client` or `server` to limit bandwidth on the client or server side. Default is `client`.
### TCP Stream Multiplexing ### TCP Stream Multiplexing
frp supports tcp stream multiplexing since v0.10.0 like HTTP2 Multiplexing, in which case all logic connections to the same frpc are multiplexed into the same TCP connection. frp supports tcp stream multiplexing since v0.10.0 like HTTP2 Multiplexing, in which case all logic connections to the same frpc are multiplexed into the same TCP connection.
@@ -747,6 +822,35 @@ KCP mode uses UDP as the underlying transport. Using KCP in frp:
protocol = kcp protocol = kcp
``` ```
### Support QUIC Protocol
QUIC is a new multiplexed transport built on top of UDP.
Using QUIC in frp:
1. Enable QUIC in frps:
```ini
# frps.ini
[common]
bind_port = 7000
# Specify a UDP port for QUIC.
quic_bind_port = 7000
```
The `quic_bind_port` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port.
2. Configure `frpc.ini` to use QUIC to connect to frps:
```ini
# frpc.ini
[common]
server_addr = x.x.x.x
# Same as the 'quic_bind_port' in frps.ini
server_port = 7000
protocol = quic
```
### Connection Pooling ### Connection Pooling
By default, frps creates a new frpc connection to the backend service upon a user request. With connection pooling, frps keeps a certain number of pre-established connections, reducing the time needed to establish a connection. By default, frps creates a new frpc connection to the backend service upon a user request. With connection pooling, frps keeps a certain number of pre-established connections, reducing the time needed to establish a connection.
@@ -858,7 +962,7 @@ custom_domains = test.example.com
host_header_rewrite = dev.example.com host_header_rewrite = dev.example.com
``` ```
The HTTP request will have the the `Host` header rewritten to `Host: dev.example.com` when it reaches the actual web server, although the request from the browser probably has `Host: test.example.com`. The HTTP request will have the `Host` header rewritten to `Host: dev.example.com` when it reaches the actual web server, although the request from the browser probably has `Host: test.example.com`.
### Setting other HTTP Headers ### Setting other HTTP Headers
@@ -1092,15 +1196,11 @@ Interested in getting involved? We would like to help you!
If frp helps you a lot, you can support us by: If frp helps you a lot, you can support us by:
frp QQ group: 606194980 ### GitHub Sponsors
### AliPay Support us by [Github Sponsors](https://github.com/sponsors/fatedier).
![donation-alipay](/doc/pic/donate-alipay.png) You can have your company's logo placed on README file of this project.
### Wechat Pay
![donation-wechatpay](/doc/pic/donate-wechatpay.png)
### PayPal ### PayPal

View File

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

View File

@@ -1,12 +1,4 @@
### New ### Fixes
* Added `connect_server_local_ip` in frpc to specify local IP connected to frps. * Fix the issue of not disabling tcp keepalive when configuring `tcp_keepalive` = -1 in frps.
* Added `tcp_mux_keepalive_interval` both in frpc and frps to set `tcp_mux` keepalive interval seconds if `tcp_mux` is enabled. After using this params, you can set `heartbeat_interval` to `-1` to disable application layer heartbeat to reduce traffic usage(Make sure frps is in the latest version). * Fix a race condition error.
### Improve
* Server Plugin: Added `client_address` in Login Operation.
### Fix
* Remove authentication for healthz api.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,16 @@
<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frp client admin UI</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5d5774096cf5c1b4d5af"></script><script type="text/javascript" src="vendor.js?dc42700731a508d39009"></script></body> </html> <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>frp client admin UI</title>
<script type="module" crossorigin src="./index-1c7ed8b0.js"></script>
<link rel="stylesheet" href="./index-1e2a7ce0.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1 +0,0 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(r&&r(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=n(n.s=u[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"dc42700731a508d39009"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,u.appendChild(i),c},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

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

@@ -1 +1,16 @@
<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5d154ba4c6b342d8c0c3"></script><script type="text/javascript" src="vendor.js?ddbd1f69fb6e67be4b78"></script></body> </html> <!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-ea3edf22.js"></script>
<link rel="stylesheet" href="./index-1e0c7400.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1 +0,0 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in u)Object.prototype.hasOwnProperty.call(u,i)&&(e[i]=u[i]);for(r&&r(t,u,c);s.length;)s.shift()();if(c)for(l=0;l<c.length;l++)f=n(n.s=c[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var u=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=u;var c=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"ddbd1f69fb6e67be4b78"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,c.appendChild(i),u},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

File diff suppressed because one or more lines are too long

View File

@@ -17,17 +17,18 @@ package client
import ( import (
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"time" "time"
"github.com/fatedier/frp/assets"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/fatedier/frp/assets"
utilnet "github.com/fatedier/frp/pkg/util/net"
) )
var ( var (
httpServerReadTimeout = 10 * time.Second httpServerReadTimeout = 60 * time.Second
httpServerWriteTimeout = 10 * time.Second httpServerWriteTimeout = 60 * time.Second
) )
func (svr *Service) RunAdminServer(address string) (err error) { func (svr *Service) RunAdminServer(address string) (err error) {
@@ -36,19 +37,29 @@ func (svr *Service) RunAdminServer(address string) (err error) {
router.HandleFunc("/healthz", svr.healthz) router.HandleFunc("/healthz", svr.healthz)
// debug
if svr.cfg.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() subRouter := router.NewRoute().Subrouter()
user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware) subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
// api, see admin_api.go // api, see admin_api.go
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET") 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/status", svr.apiStatus).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET") subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT") subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
// view // view
subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET") subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET") subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/static/", http.StatusMovedPermanently) http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
}) })
@@ -67,6 +78,8 @@ func (svr *Service) RunAdminServer(address string) (err error) {
return err return err
} }
go server.Serve(ln) go func() {
_ = server.Serve(ln)
}()
return return
} }

View File

@@ -18,10 +18,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"os" "os"
"sort" "sort"
"strconv"
"strings" "strings"
"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"
@@ -38,7 +43,7 @@ func (svr *Service) healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
} }
// GET api/reload // GET /api/reload
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
@@ -47,7 +52,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
log.Info("api response [/api/reload], code [%d]", res.Code) log.Info("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))
} }
}() }()
@@ -66,19 +71,26 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Info("success reload conf") log.Info("success reload conf")
return
} }
type StatusResp struct { // POST /api/stop
TCP []ProxyStatusResp `json:"tcp"` func (svr *Service) apiStop(w http.ResponseWriter, r *http.Request) {
UDP []ProxyStatusResp `json:"udp"` res := GeneralResponse{Code: 200}
HTTP []ProxyStatusResp `json:"http"`
HTTPS []ProxyStatusResp `json:"https"` log.Info("api request [/api/stop]")
STCP []ProxyStatusResp `json:"stcp"` defer func() {
XTCP []ProxyStatusResp `json:"xtcp"` log.Info("api response [/api/stop], code [%d]", res.Code)
SUDP []ProxyStatusResp `json:"sudp"` w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
go svr.GracefulClose(100 * time.Millisecond)
} }
type StatusResp map[string][]ProxyStatusResp
type ProxyStatusResp struct { type ProxyStatusResp struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
@@ -89,12 +101,6 @@ type ProxyStatusResp struct {
RemoteAddr string `json:"remote_addr"` RemoteAddr string `json:"remote_addr"`
} }
type ByProxyStatusResp []ProxyStatusResp
func (a ByProxyStatusResp) Len() int { return len(a) }
func (a ByProxyStatusResp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByProxyStatusResp) Less(i, j int) bool { return strings.Compare(a[i].Name, a[j].Name) < 0 }
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp { func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
psr := ProxyStatusResp{ psr := ProxyStatusResp{
Name: status.Name, Name: status.Name,
@@ -102,108 +108,51 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
Status: status.Phase, Status: status.Phase,
Err: status.Err, Err: status.Err,
} }
switch cfg := status.Cfg.(type) { baseCfg := status.Cfg.GetBaseConfig()
case *config.TCPProxyConf: if baseCfg.LocalPort != 0 {
if cfg.LocalPort != 0 { psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort) }
} psr.Plugin = baseCfg.Plugin
psr.Plugin = cfg.Plugin
if status.Err != "" { if status.Err == "" {
psr.RemoteAddr = fmt.Sprintf("%s:%d", serverAddr, cfg.RemotePort)
} else {
psr.RemoteAddr = serverAddr + status.RemoteAddr
}
case *config.UDPProxyConf:
if cfg.LocalPort != 0 {
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
}
if status.Err != "" {
psr.RemoteAddr = fmt.Sprintf("%s:%d", serverAddr, cfg.RemotePort)
} else {
psr.RemoteAddr = serverAddr + status.RemoteAddr
}
case *config.HTTPProxyConf:
if cfg.LocalPort != 0 {
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
}
psr.Plugin = cfg.Plugin
psr.RemoteAddr = status.RemoteAddr psr.RemoteAddr = status.RemoteAddr
case *config.HTTPSProxyConf: if lo.Contains([]string{"tcp", "udp"}, status.Type) {
if cfg.LocalPort != 0 { psr.RemoteAddr = serverAddr + psr.RemoteAddr
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
} }
psr.Plugin = cfg.Plugin
psr.RemoteAddr = status.RemoteAddr
case *config.STCPProxyConf:
if cfg.LocalPort != 0 {
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
}
psr.Plugin = cfg.Plugin
case *config.XTCPProxyConf:
if cfg.LocalPort != 0 {
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
}
psr.Plugin = cfg.Plugin
case *config.SUDPProxyConf:
if cfg.LocalPort != 0 {
psr.LocalAddr = fmt.Sprintf("%s:%d", cfg.LocalIP, cfg.LocalPort)
}
psr.Plugin = cfg.Plugin
} }
return psr return psr
} }
// GET api/status // GET /api/status
func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
var ( var (
buf []byte buf []byte
res StatusResp res StatusResp = make(map[string][]ProxyStatusResp)
) )
res.TCP = make([]ProxyStatusResp, 0)
res.UDP = make([]ProxyStatusResp, 0)
res.HTTP = make([]ProxyStatusResp, 0)
res.HTTPS = make([]ProxyStatusResp, 0)
res.STCP = make([]ProxyStatusResp, 0)
res.XTCP = make([]ProxyStatusResp, 0)
res.SUDP = make([]ProxyStatusResp, 0)
log.Info("Http request [/api/status]") log.Info("Http request [/api/status]")
defer func() { defer func() {
log.Info("Http response [/api/status]") log.Info("Http response [/api/status]")
buf, _ = json.Marshal(&res) buf, _ = json.Marshal(&res)
w.Write(buf) _, _ = w.Write(buf)
}() }()
ps := svr.ctl.pm.GetAllProxyStatus() ps := svr.ctl.pm.GetAllProxyStatus()
for _, status := range ps { for _, status := range ps {
switch status.Type { res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr))
case "tcp": }
res.TCP = append(res.TCP, NewProxyStatusResp(status, svr.cfg.ServerAddr))
case "udp": for _, arrs := range res {
res.UDP = append(res.UDP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) if len(arrs) <= 1 {
case "http": continue
res.HTTP = append(res.HTTP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) }
case "https": sort.Slice(arrs, func(i, j int) bool {
res.HTTPS = append(res.HTTPS, NewProxyStatusResp(status, svr.cfg.ServerAddr)) return strings.Compare(arrs[i].Name, arrs[j].Name) < 0
case "stcp": })
res.STCP = append(res.STCP, NewProxyStatusResp(status, svr.cfg.ServerAddr))
case "xtcp":
res.XTCP = append(res.XTCP, NewProxyStatusResp(status, svr.cfg.ServerAddr))
case "sudp":
res.SUDP = append(res.SUDP, NewProxyStatusResp(status, svr.cfg.ServerAddr))
}
} }
sort.Sort(ByProxyStatusResp(res.TCP))
sort.Sort(ByProxyStatusResp(res.UDP))
sort.Sort(ByProxyStatusResp(res.HTTP))
sort.Sort(ByProxyStatusResp(res.HTTPS))
sort.Sort(ByProxyStatusResp(res.STCP))
sort.Sort(ByProxyStatusResp(res.XTCP))
sort.Sort(ByProxyStatusResp(res.SUDP))
return
} }
// GET api/config // GET /api/config
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
@@ -212,7 +161,7 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
log.Info("Http get response [/api/config], code [%d]", res.Code) log.Info("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))
} }
}() }()
@@ -243,7 +192,7 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
res.Msg = strings.Join(newRows, "\n") res.Msg = strings.Join(newRows, "\n")
} }
// PUT api/config // PUT /api/config
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}
@@ -252,7 +201,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
log.Info("Http put response [/api/config], code [%d]", res.Code) log.Info("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))
} }
}() }()
@@ -313,7 +262,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
} }
content = strings.Join(newRows, "\n") content = strings.Join(newRows, "\n")
err = os.WriteFile(svr.cfgFile, []byte(content), 0644) err = os.WriteFile(svr.cfgFile, []byte(content), 0o644)
if err != nil { if 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)

View File

@@ -16,30 +16,30 @@ package client
import ( import (
"context" "context"
"crypto/tls"
"io" "io"
"net" "net"
"runtime/debug" "runtime/debug"
"strconv"
"sync"
"time" "time"
"github.com/fatedier/golib/control/shutdown"
"github.com/fatedier/golib/crypto"
"github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/client/visitor"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/golib/control/shutdown"
"github.com/fatedier/golib/crypto"
libdial "github.com/fatedier/golib/net/dial"
fmux "github.com/hashicorp/yamux"
) )
type Control struct { type Control struct {
// uniq id got from frps, attach it in loginMsg // service context
ctx context.Context
xl *xlog.Logger
// Unique ID obtained from frps.
// It should be attached to the login message when reconnecting.
runID string runID string
// manage all proxies // manage all proxies
@@ -47,13 +47,12 @@ type Control struct {
pm *proxy.Manager pm *proxy.Manager
// manage all visitors // manage all visitors
vm *VisitorManager vm *visitor.Manager
// control connection // control connection
conn net.Conn conn net.Conn
// tcp stream multiplexing, if enabled cm *ConnectionManager
session *fmux.Session
// put a message in this channel to send it over control connection to server // put a message in this channel to send it over control connection to server
sendCh chan (msg.Message) sendCh chan (msg.Message)
@@ -76,32 +75,26 @@ type Control struct {
writerShutdown *shutdown.Shutdown writerShutdown *shutdown.Shutdown
msgHandlerShutdown *shutdown.Shutdown msgHandlerShutdown *shutdown.Shutdown
// The UDP port that the server is listening on
serverUDPPort int
mu sync.RWMutex
xl *xlog.Logger
// service context
ctx context.Context
// sets authentication based on selected method // sets authentication based on selected method
authSetter auth.Setter authSetter auth.Setter
msgTransporter transport.MessageTransporter
} }
func NewControl(ctx context.Context, runID string, conn net.Conn, session *fmux.Session, func NewControl(
ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
clientCfg config.ClientCommonConf, clientCfg config.ClientCommonConf,
pxyCfgs map[string]config.ProxyConf, pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf, visitorCfgs map[string]config.VisitorConf,
serverUDPPort int, authSetter auth.Setter,
authSetter auth.Setter) *Control { ) *Control {
// new xlog instance // new xlog instance
ctl := &Control{ ctl := &Control{
ctx: ctx,
xl: xlog.FromContextSafe(ctx),
runID: runID, runID: runID,
conn: conn, conn: conn,
session: session, cm: cm,
pxyCfgs: pxyCfgs, pxyCfgs: pxyCfgs,
sendCh: make(chan msg.Message, 100), sendCh: make(chan msg.Message, 100),
readCh: make(chan msg.Message, 100), readCh: make(chan msg.Message, 100),
@@ -111,14 +104,12 @@ func NewControl(ctx context.Context, runID string, conn net.Conn, session *fmux.
readerShutdown: shutdown.New(), readerShutdown: shutdown.New(),
writerShutdown: shutdown.New(), writerShutdown: shutdown.New(),
msgHandlerShutdown: shutdown.New(), msgHandlerShutdown: shutdown.New(),
serverUDPPort: serverUDPPort,
xl: xlog.FromContextSafe(ctx),
ctx: ctx,
authSetter: authSetter, authSetter: authSetter,
} }
ctl.pm = proxy.NewManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort) ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
ctl.vm = NewVisitorManager(ctl.ctx, ctl) ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
ctl.vm.Reload(visitorCfgs) ctl.vm.Reload(visitorCfgs)
return ctl return ctl
} }
@@ -131,13 +122,13 @@ func (ctl *Control) Run() {
// start all visitors // start all visitors
go ctl.vm.Run() go ctl.vm.Run()
return
} }
func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) { func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
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)
return return
} }
@@ -156,7 +147,7 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
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.Error("work connection closed before response StartWorkConn message: %v", err) xl.Trace("work connection closed before response StartWorkConn message: %v", err)
workConn.Close() workConn.Close()
return return
} }
@@ -182,6 +173,16 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
} }
} }
func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
xl := ctl.xl
// Dispatch the NatHoleResp message to the related proxy.
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
if !ok {
xl.Trace("dispatch NatHoleResp message to related proxy error")
}
}
func (ctl *Control) Close() error { func (ctl *Control) Close() error {
return ctl.GracefulClose(0) return ctl.GracefulClose(0)
} }
@@ -193,81 +194,18 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
time.Sleep(d) time.Sleep(d)
ctl.conn.Close() ctl.conn.Close()
if ctl.session != nil { ctl.cm.Close()
ctl.session.Close()
}
return nil return nil
} }
// ClosedDoneCh returns a channel which will be closed after all resources are released // ClosedDoneCh returns a channel that will be closed after all resources are released
func (ctl *Control) ClosedDoneCh() <-chan struct{} { func (ctl *Control) ClosedDoneCh() <-chan struct{} {
return ctl.closedDoneCh return ctl.closedDoneCh
} }
// 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() (conn net.Conn, err error) {
xl := ctl.xl return ctl.cm.Connect()
if ctl.clientCfg.TCPMux {
stream, errRet := ctl.session.OpenStream()
if errRet != nil {
err = errRet
xl.Warn("start new connection to server error: %v", err)
return
}
conn = stream
} else {
var tlsConfig *tls.Config
sn := ctl.clientCfg.TLSServerName
if sn == "" {
sn = ctl.clientCfg.ServerAddr
}
if ctl.clientCfg.TLSEnable {
tlsConfig, err = transport.NewClientTLSConfig(
ctl.clientCfg.TLSCertFile,
ctl.clientCfg.TLSKeyFile,
ctl.clientCfg.TLSTrustedCaFile,
sn)
if err != nil {
xl.Warn("fail to build tls configuration when connecting to server, err: %v", err)
return
}
}
proxyType, addr, auth, err := libdial.ParseProxyURL(ctl.clientCfg.HTTPProxy)
if err != nil {
xl.Error("fail to parse proxy url")
return nil, err
}
dialOptions := []libdial.DialOption{}
protocol := ctl.clientCfg.Protocol
if protocol == "websocket" {
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
}
if ctl.clientCfg.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(ctl.clientCfg.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libdial.WithTLSConfig(tlsConfig),
libdial.WithAfterHook(libdial.AfterHook{
Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, ctl.clientCfg.DisableCustomTLSFirstByte),
}),
)
conn, err = libdial.Dial(
net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort)),
dialOptions...,
)
if err != nil {
xl.Warn("start new connection to server error: %v", err)
return nil, err
}
}
return
} }
// reader read all messages from frps and send to readCh // reader read all messages from frps and send to readCh
@@ -322,7 +260,7 @@ func (ctl *Control) writer() {
} }
} }
// msgHandler handles all channel events and do corresponding operations. // msgHandler handles all channel events and performs corresponding operations.
func (ctl *Control) msgHandler() { func (ctl *Control) msgHandler() {
xl := ctl.xl xl := ctl.xl
defer func() { defer func() {
@@ -379,6 +317,8 @@ func (ctl *Control) msgHandler() {
go ctl.HandleReqWorkConn(m) go ctl.HandleReqWorkConn(m)
case *msg.NewProxyResp: case *msg.NewProxyResp:
ctl.HandleNewProxyResp(m) ctl.HandleNewProxyResp(m)
case *msg.NatHoleResp:
ctl.HandleNatHoleResp(m)
case *msg.Pong: case *msg.Pong:
if m.Error != "" { if m.Error != "" {
xl.Error("Pong contains error: %s", m.Error) xl.Error("Pong contains error: %s", m.Error)
@@ -398,25 +338,20 @@ func (ctl *Control) worker() {
go ctl.reader() go ctl.reader()
go ctl.writer() go ctl.writer()
select { <-ctl.closedCh
case <-ctl.closedCh: // close related channels and wait until other goroutines done
// close related channels and wait until other goroutines done close(ctl.readCh)
close(ctl.readCh) ctl.readerShutdown.WaitDone()
ctl.readerShutdown.WaitDone() ctl.msgHandlerShutdown.WaitDone()
ctl.msgHandlerShutdown.WaitDone()
close(ctl.sendCh) close(ctl.sendCh)
ctl.writerShutdown.WaitDone() ctl.writerShutdown.WaitDone()
ctl.pm.Close() ctl.pm.Close()
ctl.vm.Close() ctl.vm.Close()
close(ctl.closedDoneCh) close(ctl.closedDoneCh)
if ctl.session != nil { ctl.cm.Close()
ctl.session.Close()
}
return
}
} }
func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error { func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {

View File

@@ -6,18 +6,9 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
) )
type Type int var ErrPayloadType = errors.New("error payload type")
const ( type Handler func(payload interface{}) error
EvStartProxy Type = iota
EvCloseProxy
)
var (
ErrPayloadType = errors.New("error payload type")
)
type Handler func(evType Type, payload interface{}) error
type StartProxyPayload struct { type StartProxyPayload struct {
NewProxyMsg *msg.NewProxy NewProxyMsg *msg.NewProxy

View File

@@ -26,9 +26,7 @@ import (
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
var ( var ErrHealthCheckType = errors.New("error health check type")
ErrHealthCheckType = errors.New("error health check type")
)
type Monitor struct { type Monitor struct {
checkType string checkType string
@@ -54,8 +52,8 @@ type Monitor struct {
func NewMonitor(ctx context.Context, checkType string, func NewMonitor(ctx context.Context, checkType string,
intervalS int, timeoutS int, maxFailedTimes int, intervalS int, timeoutS int, maxFailedTimes int,
addr string, url string, addr string, url string,
statusNormalFn func(), statusFailedFn func()) *Monitor { statusNormalFn func(), statusFailedFn func(),
) *Monitor {
if intervalS <= 0 { if intervalS <= 0 {
intervalS = 10 intervalS = 10
} }
@@ -152,7 +150,7 @@ func (monitor *Monitor) doTCPCheck(ctx context.Context) error {
} }
func (monitor *Monitor) doHTTPCheck(ctx context.Context) error { func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
req, err := http.NewRequest("GET", monitor.url, nil) req, err := http.NewRequestWithContext(ctx, "GET", monitor.url, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -161,7 +159,7 @@ func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
io.Copy(io.Discard, resp.Body) _, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode/100 != 2 { if resp.StatusCode/100 != 2 {
return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode) return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode)

View File

@@ -0,0 +1,47 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"reflect"
"github.com/fatedier/frp/pkg/config"
)
func init() {
pxyConfs := []config.ProxyConf{
&config.TCPProxyConf{},
&config.HTTPProxyConf{},
&config.HTTPSProxyConf{},
&config.STCPProxyConf{},
&config.TCPMuxProxyConf{},
}
for _, cfg := range pxyConfs {
RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
}
}
// GeneralTCPProxy is a general implementation of Proxy interface for TCP protocol.
// If the default GeneralTCPProxy cannot meet the requirements, you can customize
// the implementation of the Proxy interface.
type GeneralTCPProxy struct {
*BaseProxy
}
func NewGeneralTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
return &GeneralTCPProxy{
BaseProxy: baseProxy,
}
}

View File

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

View File

@@ -1,42 +1,56 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy package proxy
import ( import (
"context" "context"
"fmt" "fmt"
"net" "net"
"reflect"
"sync" "sync"
"github.com/fatedier/frp/client/event" "github.com/fatedier/frp/client/event"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/golib/errors"
) )
type Manager struct { type Manager struct {
sendCh chan (msg.Message) proxies map[string]*Wrapper
proxies map[string]*Wrapper msgTransporter transport.MessageTransporter
closed bool closed bool
mu sync.RWMutex mu sync.RWMutex
clientCfg config.ClientCommonConf clientCfg config.ClientCommonConf
// The UDP port that the server is listening on
serverUDPPort int
ctx context.Context ctx context.Context
} }
func NewManager(ctx context.Context, msgSendCh chan (msg.Message), clientCfg config.ClientCommonConf, serverUDPPort int) *Manager { func NewManager(
ctx context.Context,
clientCfg config.ClientCommonConf,
msgTransporter transport.MessageTransporter,
) *Manager {
return &Manager{ return &Manager{
sendCh: msgSendCh, proxies: make(map[string]*Wrapper),
proxies: make(map[string]*Wrapper), msgTransporter: msgTransporter,
closed: false, closed: false,
clientCfg: clientCfg, clientCfg: clientCfg,
serverUDPPort: serverUDPPort, ctx: ctx,
ctx: ctx,
} }
} }
@@ -75,7 +89,7 @@ func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWo
} }
} }
func (pm *Manager) HandleEvent(evType event.Type, payload interface{}) error { func (pm *Manager) HandleEvent(payload interface{}) error {
var m msg.Message var m msg.Message
switch e := payload.(type) { switch e := payload.(type) {
case *event.StartProxyPayload: case *event.StartProxyPayload:
@@ -86,10 +100,7 @@ func (pm *Manager) HandleEvent(evType event.Type, payload interface{}) error {
return event.ErrPayloadType return event.ErrPayloadType
} }
err := errors.PanicToError(func() { return pm.msgTransporter.Send(m)
pm.sendCh <- m
})
return err
} }
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus { func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
@@ -111,29 +122,24 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
for name, pxy := range pm.proxies { for name, pxy := range pm.proxies {
del := false del := false
cfg, ok := pxyCfgs[name] cfg, ok := pxyCfgs[name]
if !ok { if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
del = true del = true
} else {
if !pxy.Cfg.Compare(cfg) {
del = true
}
} }
if del { if del {
delPxyNames = append(delPxyNames, name) delPxyNames = append(delPxyNames, name)
delete(pm.proxies, name) delete(pm.proxies, name)
pxy.Stop() pxy.Stop()
} }
} }
if len(delPxyNames) > 0 { if len(delPxyNames) > 0 {
xl.Info("proxy removed: %v", delPxyNames) xl.Info("proxy removed: %s", delPxyNames)
} }
addPxyNames := make([]string, 0) addPxyNames := make([]string, 0)
for name, cfg := range pxyCfgs { for name, cfg := range pxyCfgs {
if _, ok := pm.proxies[name]; !ok { if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.serverUDPPort) pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
pm.proxies[name] = pxy pm.proxies[name] = pxy
addPxyNames = append(addPxyNames, name) addPxyNames = append(addPxyNames, name)
@@ -141,6 +147,6 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
} }
} }
if len(addPxyNames) > 0 { if len(addPxyNames) > 0 {
xl.Info("proxy added: %v", addPxyNames) xl.Info("proxy added: %s", addPxyNames)
} }
} }

View File

@@ -1,3 +1,17 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy package proxy
import ( import (
@@ -8,13 +22,14 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/fatedier/golib/errors"
"github.com/fatedier/frp/client/event" "github.com/fatedier/frp/client/event"
"github.com/fatedier/frp/client/health" "github.com/fatedier/frp/client/health"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/golib/errors"
) )
const ( const (
@@ -27,9 +42,9 @@ const (
) )
var ( var (
statusCheckInterval time.Duration = 3 * time.Second statusCheckInterval = 3 * time.Second
waitResponseTimeout = 20 * time.Second waitResponseTimeout = 20 * time.Second
startErrTimeout = 30 * time.Second startErrTimeout = 30 * time.Second
) )
type WorkingStatus struct { type WorkingStatus struct {
@@ -56,6 +71,8 @@ type Wrapper struct {
// event handler // event handler
handler event.Handler handler event.Handler
msgTransporter transport.MessageTransporter
health uint32 health uint32
lastSendStartMsg time.Time lastSendStartMsg time.Time
lastStartErr time.Time lastStartErr time.Time
@@ -67,8 +84,14 @@ type Wrapper struct {
ctx context.Context ctx context.Context
} }
func NewWrapper(ctx context.Context, cfg config.ProxyConf, clientCfg config.ClientCommonConf, eventHandler event.Handler, serverUDPPort int) *Wrapper { func NewWrapper(
baseInfo := cfg.GetBaseInfo() ctx context.Context,
cfg config.ProxyConf,
clientCfg config.ClientCommonConf,
eventHandler event.Handler,
msgTransporter transport.MessageTransporter,
) *Wrapper {
baseInfo := cfg.GetBaseConfig()
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName) xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName)
pw := &Wrapper{ pw := &Wrapper{
WorkingStatus: WorkingStatus{ WorkingStatus: WorkingStatus{
@@ -80,6 +103,7 @@ func NewWrapper(ctx context.Context, cfg config.ProxyConf, clientCfg config.Clie
closeCh: make(chan struct{}), closeCh: make(chan struct{}),
healthNotifyCh: make(chan struct{}), healthNotifyCh: make(chan struct{}),
handler: eventHandler, handler: eventHandler,
msgTransporter: msgTransporter,
xl: xl, xl: xl,
ctx: xlog.NewContext(ctx, xl), ctx: xlog.NewContext(ctx, xl),
} }
@@ -92,7 +116,7 @@ func NewWrapper(ctx context.Context, cfg config.ProxyConf, clientCfg config.Clie
xl.Trace("enable health check monitor") xl.Trace("enable health check monitor")
} }
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, serverUDPPort) pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter)
return pw return pw
} }
@@ -145,7 +169,7 @@ func (pw *Wrapper) Stop() {
} }
func (pw *Wrapper) close() { func (pw *Wrapper) close() {
pw.handler(event.EvCloseProxy, &event.CloseProxyPayload{ _ = pw.handler(&event.CloseProxyPayload{
CloseProxyMsg: &msg.CloseProxy{ CloseProxyMsg: &msg.CloseProxy{
ProxyName: pw.Name, ProxyName: pw.Name,
}, },
@@ -174,7 +198,7 @@ func (pw *Wrapper) checkWorker() {
var newProxyMsg msg.NewProxy var newProxyMsg msg.NewProxy
pw.Cfg.MarshalToMsg(&newProxyMsg) pw.Cfg.MarshalToMsg(&newProxyMsg)
pw.lastSendStartMsg = now pw.lastSendStartMsg = now
pw.handler(event.EvStartProxy, &event.StartProxyPayload{ _ = pw.handler(&event.StartProxyPayload{
NewProxyMsg: &newProxyMsg, NewProxyMsg: &newProxyMsg,
}) })
} }
@@ -201,7 +225,7 @@ func (pw *Wrapper) checkWorker() {
func (pw *Wrapper) statusNormalCallback() { func (pw *Wrapper) statusNormalCallback() {
xl := pw.xl xl := pw.xl
atomic.StoreUint32(&pw.health, 0) atomic.StoreUint32(&pw.health, 0)
errors.PanicToError(func() { _ = errors.PanicToError(func() {
select { select {
case pw.healthNotifyCh <- struct{}{}: case pw.healthNotifyCh <- struct{}{}:
default: default:
@@ -213,7 +237,7 @@ func (pw *Wrapper) statusNormalCallback() {
func (pw *Wrapper) statusFailedCallback() { func (pw *Wrapper) statusFailedCallback() {
xl := pw.xl xl := pw.xl
atomic.StoreUint32(&pw.health, 1) atomic.StoreUint32(&pw.health, 1)
errors.PanicToError(func() { _ = errors.PanicToError(func() {
select { select {
case pw.healthNotifyCh <- struct{}{}: case pw.healthNotifyCh <- struct{}{}:
default: default:

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

@@ -0,0 +1,209 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"io"
"net"
"reflect"
"strconv"
"sync"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
}
type SUDPProxy struct {
*BaseProxy
cfg *config.SUDPProxyConf
localAddr *net.UDPAddr
closeCh chan struct{}
}
func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
unwrapped, ok := cfg.(*config.SUDPProxyConf)
if !ok {
return nil
}
return &SUDPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
closeCh: make(chan struct{}),
}
}
func (pxy *SUDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *SUDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
select {
case <-pxy.closeCh:
return
default:
close(pxy.closeCh)
}
}
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.UseCompression {
var releaseFn func()
rwc, releaseFn = libio.WithCompressionFromPool(rwc)
defer releaseFn()
}
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn
readCh := make(chan *msg.UDPPacket, 1024)
sendCh := make(chan msg.Message, 1024)
isClose := false
mu := &sync.Mutex{}
closeFn := func() {
mu.Lock()
defer mu.Unlock()
if isClose {
return
}
isClose = true
if workConn != nil {
workConn.Close()
}
close(readCh)
close(sendCh)
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
defer closeFn()
for {
// first to check sudp proxy is closed or not
select {
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
default:
}
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for sudp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
readCh <- &udpMsg
}); errRet != nil {
xl.Warn("reader goroutine for sudp work connection closed: %v", errRet)
return
}
}
}
// udp service -> frpc -> frps -> frpc visitor -> user
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
closeFn()
xl.Info("writer goroutine for sudp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]",
m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
case *msg.Ping:
xl.Trace("frpc send ping message to frpc visitor")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("sudp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
closeFn()
}()
var errRet error
for {
select {
case <-ticker.C:
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Warn("heartbeat goroutine for sudp work connection closed")
return
}
case <-pxy.closeCh:
xl.Trace("frpc sudp proxy is closed")
return
}
}
}
go workConnSenderFn(workConn, sendCh)
go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
}

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

@@ -0,0 +1,175 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"io"
"net"
"reflect"
"strconv"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy)
}
type UDPProxy struct {
*BaseProxy
cfg *config.UDPProxyConf
localAddr *net.UDPAddr
readCh chan *msg.UDPPacket
// include msg.UDPPacket and msg.Ping
sendCh chan msg.Message
workConn net.Conn
closed bool
}
func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
unwrapped, ok := cfg.(*config.UDPProxyConf)
if !ok {
return nil
}
return &UDPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
}
}
func (pxy *UDPProxy) Run() (err error) {
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
if err != nil {
return
}
return
}
func (pxy *UDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
if !pxy.closed {
pxy.closed = true
if pxy.workConn != nil {
pxy.workConn.Close()
}
if pxy.readCh != nil {
close(pxy.readCh)
}
if pxy.sendCh != nil {
close(pxy.sendCh)
}
}
}
func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
// close resources releated with old workConn
pxy.Close()
var rwc io.ReadWriteCloser = conn
var err error
if pxy.limiter != nil {
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
return conn.Close()
})
}
if pxy.cfg.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
if err != nil {
conn.Close()
xl.Error("create encryption stream error: %v", err)
return
}
}
if pxy.cfg.UseCompression {
var releaseFn func()
rwc, releaseFn = libio.WithCompressionFromPool(rwc)
defer releaseFn()
}
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock()
pxy.workConn = conn
pxy.readCh = make(chan *msg.UDPPacket, 1024)
pxy.sendCh = make(chan msg.Message, 1024)
pxy.closed = false
pxy.mu.Unlock()
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
for {
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
xl.Warn("read from workConn for udp error: %v", errRet)
return
}
if errRet := errors.PanicToError(func() {
xl.Trace("get udp package from workConn: %s", udpMsg.Content)
readCh <- &udpMsg
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed: %v", errRet)
return
}
}
}
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
defer func() {
xl.Info("writer goroutine for udp work connection closed")
}()
var errRet error
for rawMsg := range sendCh {
switch m := rawMsg.(type) {
case *msg.UDPPacket:
xl.Trace("send udp package to workConn: %s", m.Content)
case *msg.Ping:
xl.Trace("send ping message to udp workConn")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
xl.Error("udp work write error: %v", errRet)
return
}
}
}
heartbeatFn := func(sendCh chan msg.Message) {
var errRet error
for {
time.Sleep(time.Duration(30) * time.Second)
if errRet = errors.PanicToError(func() {
sendCh <- &msg.Ping{}
}); errRet != nil {
xl.Trace("heartbeat goroutine for udp work connection closed")
break
}
}
}
go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh)
go heartbeatFn(pxy.sendCh)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
}

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

@@ -0,0 +1,197 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"io"
"net"
"reflect"
"time"
fmux "github.com/hashicorp/yamux"
"github.com/quic-go/quic-go"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
func init() {
RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
}
type XTCPProxy struct {
*BaseProxy
cfg *config.XTCPProxyConf
}
func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
unwrapped, ok := cfg.(*config.XTCPProxyConf)
if !ok {
return nil
}
return &XTCPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
}
}
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
defer conn.Close()
var natHoleSidMsg msg.NatHoleSid
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
if err != nil {
xl.Error("xtcp read from workConn error: %v", err)
return
}
xl.Trace("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
if err != nil {
xl.Warn("nathole prepare error: %v", err)
return
}
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
defer prepareResult.ListenConn.Close()
// send NatHoleClient msg to server
transactionID := nathole.NewTransactionID()
natHoleClientMsg := &msg.NatHoleClient{
TransactionID: transactionID,
ProxyName: pxy.cfg.ProxyName,
Sid: natHoleSidMsg.Sid,
MappedAddrs: prepareResult.Addrs,
AssistedAddrs: prepareResult.AssistedAddrs,
}
xl.Trace("nathole exchange info start")
natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
if err != nil {
xl.Warn("nathole exchange info error: %v", err)
return
}
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
listenConn := prepareResult.ListenConn
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Sk))
if err != nil {
listenConn.Close()
xl.Warn("make hole error: %v", err)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid,
Success: false,
})
return
}
listenConn = newListenConn
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
Sid: natHoleRespMsg.Sid,
Success: true,
})
if natHoleRespMsg.Protocol == "kcp" {
pxy.listenByKCP(listenConn, raddr, startWorkConnMsg)
return
}
// default is quic
pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg)
}
func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
listenConn.Close()
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
lConn, err := net.DialUDP("udp", laddr, raddr)
if err != nil {
xl.Warn("dial udp error: %v", err)
return
}
defer lConn.Close()
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil {
xl.Warn("create kcp connection from udp connection error: %v", err)
return
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 10 * time.Second
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
fmuxCfg.LogOutput = io.Discard
session, err := fmux.Server(remote, fmuxCfg)
if err != nil {
xl.Error("create mux session error: %v", err)
return
}
defer session.Close()
for {
muxConn, err := session.Accept()
if err != nil {
xl.Error("accept connection error: %v", err)
return
}
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Sk))
}
}
func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
defer listenConn.Close()
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
if err != nil {
xl.Warn("create tls config error: %v", err)
return
}
tlsConfig.NextProtos = []string{"frp"}
quicListener, err := quic.Listen(listenConn, tlsConfig,
&quic.Config{
MaxIdleTimeout: time.Duration(pxy.clientCfg.QUICMaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(pxy.clientCfg.QUICMaxIncomingStreams),
KeepAlivePeriod: time.Duration(pxy.clientCfg.QUICKeepalivePeriod) * time.Second,
},
)
if err != nil {
xl.Warn("dial quic error: %v", err)
return
}
// only accept one connection from raddr
c, err := quicListener.Accept(pxy.ctx)
if err != nil {
xl.Error("quic accept connection error: %v", err)
return
}
for {
stream, err := c.AcceptStream(pxy.ctx)
if err != nil {
xl.Debug("quic accept stream error: %v", err)
_ = c.CloseWithError(0, "")
return
}
go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Sk))
}
}

View File

@@ -17,30 +17,40 @@ package client
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io" "io"
"math/rand"
"net" "net"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"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/fatedier/frp/assets" "github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"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/log" "github.com/fatedier/frp/pkg/util/log"
frpNet "github.com/fatedier/frp/pkg/util/net" utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
libdial "github.com/fatedier/golib/net/dial"
fmux "github.com/hashicorp/yamux"
) )
func init() {
crypto.DefaultSalt = "frp"
// TODO: remove this when we drop support for go1.19
rand.Seed(time.Now().UnixNano())
}
// Service is a client service. // Service is a client service.
type Service struct { type Service struct {
// uniq id got from frps, attach it in loginMsg // uniq id got from frps, attach it in loginMsg
@@ -62,9 +72,6 @@ type Service struct {
// string if no configuration file was used. // string if no configuration file was used.
cfgFile string cfgFile string
// This is configured by the login response from frps
serverUDPPort int
exit uint32 // 0 means not exit exit uint32 // 0 means not exit
// service context // service context
@@ -73,18 +80,20 @@ type Service struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
func NewService(cfg config.ClientCommonConf, pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf, cfgFile string) (svr *Service, err error) { func NewService(
cfg config.ClientCommonConf,
ctx, cancel := context.WithCancel(context.Background()) pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf,
cfgFile string,
) (svr *Service, err error) {
svr = &Service{ svr = &Service{
authSetter: auth.NewAuthSetter(cfg.ClientConfig), authSetter: auth.NewAuthSetter(cfg.ClientConfig),
cfg: cfg, cfg: cfg,
cfgFile: cfgFile, cfgFile: cfgFile,
pxyCfgs: pxyCfgs, pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs, visitorCfgs: visitorCfgs,
ctx: context.Background(),
exit: 0, exit: 0,
ctx: xlog.NewContext(ctx, xlog.New()),
cancel: cancel,
} }
return return
} }
@@ -95,12 +104,31 @@ func (svr *Service) GetController() *Control {
return svr.ctl return svr.ctl
} }
func (svr *Service) Run() error { func (svr *Service) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
svr.ctx = xlog.NewContext(ctx, xlog.New())
svr.cancel = cancel
xl := xlog.FromContextSafe(svr.ctx) xl := xlog.FromContextSafe(svr.ctx)
// set custom DNSServer
if svr.cfg.DNSServer != "" {
dnsAddr := svr.cfg.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 // login to frps
for { for {
conn, session, err := svr.login() conn, cm, err := svr.login()
if err != nil { if err != nil {
xl.Warn("login to server failed: %v", err) xl.Warn("login to server failed: %v", err)
@@ -109,10 +137,10 @@ func (svr *Service) Run() error {
if svr.cfg.LoginFailExit { if svr.cfg.LoginFailExit {
return err return err
} }
time.Sleep(10 * time.Second) util.RandomSleep(5*time.Second, 0.9, 1.1)
} else { } else {
// login success // login success
ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
ctl.Run() ctl.Run()
svr.ctlMu.Lock() svr.ctlMu.Lock()
svr.ctl = ctl svr.ctl = ctl
@@ -135,6 +163,10 @@ func (svr *Service) Run() error {
log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort) log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort)
} }
<-svr.ctx.Done() <-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources
if atomic.LoadUint32(&svr.exit) == 0 {
svr.Close()
}
return nil return nil
} }
@@ -156,10 +188,13 @@ func (svr *Service) keepControllerWorking() {
return return
} }
// the first three retry with no delay // the first three attempts with a low delay
if reconnectCounts > 3 { if reconnectCounts > 3 {
time.Sleep(reconnectDelay) util.RandomSleep(reconnectDelay, 0.9, 1.1)
xl.Info("wait %v to reconnect", reconnectDelay)
reconnectDelay *= 2 reconnectDelay *= 2
} else {
util.RandomSleep(time.Second, 0, 0.5)
} }
reconnectCounts++ reconnectCounts++
@@ -172,28 +207,26 @@ func (svr *Service) keepControllerWorking() {
} }
for { for {
xl.Info("try to reconnect to server...") if atomic.LoadUint32(&svr.exit) != 0 {
conn, session, err := svr.login() return
if err != nil { }
xl.Warn("reconnect to server error: %v", err)
time.Sleep(delayTime)
opErr := &net.OpError{} xl.Info("try to reconnect to server...")
// quick retry for dial error conn, cm, err := svr.login()
if errors.As(err, &opErr) && opErr.Op == "dial" { if err != nil {
delayTime = 2 * time.Second xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
} else { util.RandomSleep(delayTime, 0.9, 1.1)
delayTime = delayTime * 2
if delayTime > maxDelayTime { delayTime *= 2
delayTime = maxDelayTime if delayTime > maxDelayTime {
} delayTime = maxDelayTime
} }
continue continue
} }
// reconnect success, init delayTime // reconnect success, init delayTime
delayTime = time.Second delayTime = time.Second
ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
ctl.Run() ctl.Run()
svr.ctlMu.Lock() svr.ctlMu.Lock()
if svr.ctl != nil { if svr.ctl != nil {
@@ -209,81 +242,23 @@ func (svr *Service) keepControllerWorking() {
// 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, session *fmux.Session, err error) { func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
xl := xlog.FromContextSafe(svr.ctx) xl := xlog.FromContextSafe(svr.ctx)
var tlsConfig *tls.Config cm = NewConnectionManager(svr.ctx, &svr.cfg)
if svr.cfg.TLSEnable {
sn := svr.cfg.TLSServerName
if sn == "" {
sn = svr.cfg.ServerAddr
}
tlsConfig, err = transport.NewClientTLSConfig( if err = cm.OpenConnection(); err != nil {
svr.cfg.TLSCertFile, return nil, nil, err
svr.cfg.TLSKeyFile,
svr.cfg.TLSTrustedCaFile,
sn)
if err != nil {
xl.Warn("fail to build tls configuration when service login, err: %v", err)
return
}
}
proxyType, addr, auth, err := libdial.ParseProxyURL(svr.cfg.HTTPProxy)
if err != nil {
xl.Error("fail to parse proxy url")
return
}
dialOptions := []libdial.DialOption{}
protocol := svr.cfg.Protocol
if protocol == "websocket" {
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
}
if svr.cfg.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(svr.cfg.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libdial.WithTLSConfig(tlsConfig),
libdial.WithAfterHook(libdial.AfterHook{
Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, svr.cfg.DisableCustomTLSFirstByte),
}),
)
conn, err = libdial.Dial(
net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort)),
dialOptions...,
)
if err != nil {
return
} }
defer func() { defer func() {
if err != nil { if err != nil {
conn.Close() cm.Close()
if session != nil {
session.Close()
}
} }
}() }()
if svr.cfg.TCPMux { conn, err = cm.Connect()
fmuxCfg := fmux.DefaultConfig() if err != nil {
fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second return
fmuxCfg.LogOutput = io.Discard
session, err = fmux.Client(conn, fmuxCfg)
if err != nil {
return
}
stream, errRet := session.OpenStream()
if errRet != nil {
session.Close()
err = errRet
return
}
conn = stream
} }
loginMsg := &msg.Login{ loginMsg := &msg.Login{
@@ -307,11 +282,11 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
} }
var loginRespMsg msg.LoginResp var loginRespMsg msg.LoginResp
conn.SetReadDeadline(time.Now().Add(10 * time.Second)) _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
if err = msg.ReadMsgInto(conn, &loginRespMsg); err != nil { if err = msg.ReadMsgInto(conn, &loginRespMsg); err != nil {
return return
} }
conn.SetReadDeadline(time.Time{}) _ = conn.SetReadDeadline(time.Time{})
if loginRespMsg.Error != "" { if loginRespMsg.Error != "" {
err = fmt.Errorf("%s", loginRespMsg.Error) err = fmt.Errorf("%s", loginRespMsg.Error)
@@ -323,8 +298,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
xl.ResetPrefixes() xl.ResetPrefixes()
xl.AppendPrefix(svr.runID) xl.AppendPrefix(svr.runID)
svr.serverUDPPort = loginRespMsg.ServerUDPPort xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
xl.Info("login to server success, get run id [%s], server udp port [%d]", loginRespMsg.RunID, loginRespMsg.ServerUDPPort)
return return
} }
@@ -334,7 +308,14 @@ func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs
svr.visitorCfgs = visitorCfgs svr.visitorCfgs = visitorCfgs
svr.cfgMu.Unlock() svr.cfgMu.Unlock()
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs) svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock()
if ctl != nil {
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
}
return nil
} }
func (svr *Service) Close() { func (svr *Service) Close() {
@@ -343,8 +324,187 @@ func (svr *Service) Close() {
func (svr *Service) GracefulClose(d time.Duration) { func (svr *Service) GracefulClose(d time.Duration) {
atomic.StoreUint32(&svr.exit, 1) atomic.StoreUint32(&svr.exit, 1)
svr.ctlMu.RLock()
if svr.ctl != nil { if svr.ctl != nil {
svr.ctl.GracefulClose(d) svr.ctl.GracefulClose(d)
svr.ctl = nil
}
svr.ctlMu.RUnlock()
if svr.cancel != nil {
svr.cancel()
} }
svr.cancel() }
type ConnectionManager struct {
ctx context.Context
cfg *config.ClientCommonConf
muxSession *fmux.Session
quicConn quic.Connection
}
func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager {
return &ConnectionManager{
ctx: ctx,
cfg: cfg,
}
}
func (cm *ConnectionManager) OpenConnection() error {
xl := xlog.FromContextSafe(cm.ctx)
// special for quic
if strings.EqualFold(cm.cfg.Protocol, "quic") {
var tlsConfig *tls.Config
var err error
sn := cm.cfg.TLSServerName
if sn == "" {
sn = cm.cfg.ServerAddr
}
if cm.cfg.TLSEnable {
tlsConfig, err = transport.NewClientTLSConfig(
cm.cfg.TLSCertFile,
cm.cfg.TLSKeyFile,
cm.cfg.TLSTrustedCaFile,
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.DialAddrContext(
cm.ctx,
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
tlsConfig, &quic.Config{
MaxIdleTimeout: time.Duration(cm.cfg.QUICMaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(cm.cfg.QUICMaxIncomingStreams),
KeepAlivePeriod: time.Duration(cm.cfg.QUICKeepalivePeriod) * time.Second,
})
if err != nil {
return err
}
cm.quicConn = conn
return nil
}
if !cm.cfg.TCPMux {
return nil
}
conn, err := cm.realConnect()
if err != nil {
return err
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.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 := cm.cfg.TLSEnable
if cm.cfg.Protocol == "wss" {
tlsEnable = true
}
if tlsEnable {
sn := cm.cfg.TLSServerName
if sn == "" {
sn = cm.cfg.ServerAddr
}
tlsConfig, err = transport.NewClientTLSConfig(
cm.cfg.TLSCertFile,
cm.cfg.TLSKeyFile,
cm.cfg.TLSTrustedCaFile,
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.HTTPProxy)
if err != nil {
xl.Error("fail to parse proxy url")
return nil, err
}
dialOptions := []libdial.DialOption{}
protocol := cm.cfg.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, cm.cfg.DisableCustomTLSFirstByte),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
case "wss":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
}
if cm.cfg.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(cm.cfg.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second),
libdial.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

@@ -1,553 +0,0 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"bytes"
"context"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/golib/errors"
frpIo "github.com/fatedier/golib/io"
"github.com/fatedier/golib/pool"
fmux "github.com/hashicorp/yamux"
)
// Visitor is used for forward traffics from local port tot remote service.
type Visitor interface {
Run() error
Close()
}
func NewVisitor(ctx context.Context, ctl *Control, cfg config.VisitorConf) (visitor Visitor) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseInfo().ProxyName)
baseVisitor := BaseVisitor{
ctl: ctl,
ctx: xlog.NewContext(ctx, xl),
}
switch cfg := cfg.(type) {
case *config.STCPVisitorConf:
visitor = &STCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *config.XTCPVisitorConf:
visitor = &XTCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *config.SUDPVisitorConf:
visitor = &SUDPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
checkCloseCh: make(chan struct{}),
}
}
return
}
type BaseVisitor struct {
ctl *Control
l net.Listener
closed bool
mu sync.RWMutex
ctx context.Context
}
type STCPVisitor struct {
*BaseVisitor
cfg *config.STCPVisitorConf
}
func (sv *STCPVisitor) Run() (err error) {
sv.l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", sv.cfg.BindAddr, sv.cfg.BindPort))
if err != nil {
return
}
go sv.worker()
return
}
func (sv *STCPVisitor) Close() {
sv.l.Close()
}
func (sv *STCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("stcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new stcp user connection")
visitorConn, err := sv.ctl.connectServer()
if err != nil {
return
}
defer visitorConn.Close()
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
xl.Warn("send newVisitorConnMsg to server error: %v", err)
return
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
xl.Warn("get newVisitorConnRespMsg error: %v", err)
return
}
visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
return
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.UseCompression {
remote = frpIo.WithCompression(remote)
}
frpIo.Join(userConn, remote)
}
type XTCPVisitor struct {
*BaseVisitor
cfg *config.XTCPVisitorConf
}
func (sv *XTCPVisitor) Run() (err error) {
sv.l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", sv.cfg.BindAddr, sv.cfg.BindPort))
if err != nil {
return
}
go sv.worker()
return
}
func (sv *XTCPVisitor) Close() {
sv.l.Close()
}
func (sv *XTCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("xtcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new xtcp user connection")
if sv.ctl.serverUDPPort == 0 {
xl.Error("xtcp is not supported by server")
return
}
raddr, err := net.ResolveUDPAddr("udp",
fmt.Sprintf("%s:%d", sv.ctl.clientCfg.ServerAddr, sv.ctl.serverUDPPort))
if err != nil {
xl.Error("resolve server UDP addr error")
return
}
visitorConn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
xl.Warn("dial server udp addr error: %v", err)
return
}
defer visitorConn.Close()
now := time.Now().Unix()
natHoleVisitorMsg := &msg.NatHoleVisitor{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
}
err = msg.WriteMsg(visitorConn, natHoleVisitorMsg)
if err != nil {
xl.Warn("send natHoleVisitorMsg to server error: %v", err)
return
}
// Wait for client address at most 10 seconds.
var natHoleRespMsg msg.NatHoleResp
visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
buf := pool.GetBuf(1024)
n, err := visitorConn.Read(buf)
if err != nil {
xl.Warn("get natHoleRespMsg error: %v", err)
return
}
err = msg.ReadMsgInto(bytes.NewReader(buf[:n]), &natHoleRespMsg)
if err != nil {
xl.Warn("get natHoleRespMsg error: %v", err)
return
}
visitorConn.SetReadDeadline(time.Time{})
pool.PutBuf(buf)
if natHoleRespMsg.Error != "" {
xl.Error("natHoleRespMsg get error info: %s", natHoleRespMsg.Error)
return
}
xl.Trace("get natHoleRespMsg, sid [%s], client address [%s], visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr)
// Close visitorConn, so we can use it's local address.
visitorConn.Close()
// send sid message to client
laddr, _ := net.ResolveUDPAddr("udp", visitorConn.LocalAddr().String())
daddr, err := net.ResolveUDPAddr("udp", natHoleRespMsg.ClientAddr)
if err != nil {
xl.Error("resolve client udp address error: %v", err)
return
}
lConn, err := net.DialUDP("udp", laddr, daddr)
if err != nil {
xl.Error("dial client udp address error: %v", err)
return
}
defer lConn.Close()
lConn.Write([]byte(natHoleRespMsg.Sid))
// read ack sid from client
sidBuf := pool.GetBuf(1024)
lConn.SetReadDeadline(time.Now().Add(8 * time.Second))
n, err = lConn.Read(sidBuf)
if err != nil {
xl.Warn("get sid from client error: %v", err)
return
}
lConn.SetReadDeadline(time.Time{})
if string(sidBuf[:n]) != natHoleRespMsg.Sid {
xl.Warn("incorrect sid from client")
return
}
pool.PutBuf(sidBuf)
xl.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid)
// wrap kcp connection
var remote io.ReadWriteCloser
remote, err = frpNet.NewKCPConnFromUDP(lConn, true, natHoleRespMsg.ClientAddr)
if err != nil {
xl.Error("create kcp connection from udp connection error: %v", err)
return
}
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 5 * time.Second
fmuxCfg.LogOutput = io.Discard
sess, err := fmux.Client(remote, fmuxCfg)
if err != nil {
xl.Error("create yamux session error: %v", err)
return
}
defer sess.Close()
muxConn, err := sess.Open()
if err != nil {
xl.Error("open yamux stream error: %v", err)
return
}
var muxConnRWCloser io.ReadWriteCloser = muxConn
if sv.cfg.UseEncryption {
muxConnRWCloser, err = frpIo.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.UseCompression {
muxConnRWCloser = frpIo.WithCompression(muxConnRWCloser)
}
frpIo.Join(userConn, muxConnRWCloser)
xl.Debug("join connections closed")
}
type SUDPVisitor struct {
*BaseVisitor
checkCloseCh chan struct{}
// udpConn is the listener of udp packet
udpConn *net.UDPConn
readCh chan *msg.UDPPacket
sendCh chan *msg.UDPPacket
cfg *config.SUDPVisitorConf
}
// SUDP Run start listen a udp port
func (sv *SUDPVisitor) Run() (err error) {
xl := xlog.FromContextSafe(sv.ctx)
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", sv.cfg.BindAddr, sv.cfg.BindPort))
if err != nil {
return fmt.Errorf("sudp ResolveUDPAddr error: %v", err)
}
sv.udpConn, err = net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("listen udp port %s error: %v", addr.String(), err)
}
sv.sendCh = make(chan *msg.UDPPacket, 1024)
sv.readCh = make(chan *msg.UDPPacket, 1024)
xl.Info("sudp start to work, listen on %s", addr)
go sv.dispatcher()
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.ctl.clientCfg.UDPPacketSize))
return
}
func (sv *SUDPVisitor) dispatcher() {
xl := xlog.FromContextSafe(sv.ctx)
for {
// loop for get frpc to frps tcp conn
// setup worker
// wait worker to finished
// retry or exit
visitorConn, err := sv.getNewVisitorConn()
if err != nil {
// check if proxy is closed
// if checkCloseCh is close, we will return, other case we will continue to reconnect
select {
case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed")
return
default:
}
time.Sleep(3 * time.Second)
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err)
continue
}
sv.worker(visitorConn)
select {
case <-sv.checkCloseCh:
return
default:
}
}
}
func (sv *SUDPVisitor) worker(workConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker")
wg := &sync.WaitGroup{}
wg.Add(2)
closeCh := make(chan struct{})
// udp service -> frpc -> frps -> frpc visitor -> user
workConnReaderFn := func(conn net.Conn) {
defer func() {
conn.Close()
close(closeCh)
wg.Done()
}()
for {
var (
rawMsg msg.Message
errRet error
)
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
xl.Warn("read from workconn for user udp conn error: %v", errRet)
return
}
conn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Ping:
xl.Debug("frpc visitor get ping message from frpc")
continue
case *msg.UDPPacket:
if errRet := errors.PanicToError(func() {
sv.readCh <- m
xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content)
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed")
return
}
}
}
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnSenderFn := func(conn net.Conn) {
defer func() {
conn.Close()
wg.Done()
}()
var errRet error
for {
select {
case udpMsg, ok := <-sv.sendCh:
if !ok {
xl.Info("sender goroutine for udp work connection closed")
return
}
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", udpMsg.Content)
case <-closeCh:
return
}
}
}
go workConnReaderFn(workConn)
go workConnSenderFn(workConn)
wg.Wait()
xl.Info("sudp worker is closed")
}
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
xl := xlog.FromContextSafe(sv.ctx)
visitorConn, err := sv.ctl.connectServer()
if err != nil {
return nil, fmt.Errorf("frpc connect frps error: %v", err)
}
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
}
visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return nil, err
}
}
if sv.cfg.UseCompression {
remote = frpIo.WithCompression(remote)
}
return frpNet.WrapReadWriteCloserToConn(remote, visitorConn), nil
}
func (sv *SUDPVisitor) Close() {
sv.mu.Lock()
defer sv.mu.Unlock()
select {
case <-sv.checkCloseCh:
return
default:
close(sv.checkCloseCh)
}
if sv.udpConn != nil {
sv.udpConn.Close()
}
close(sv.readCh)
close(sv.sendCh)
}

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

@@ -0,0 +1,135 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package visitor
import (
"io"
"net"
"strconv"
"time"
libio "github.com/fatedier/golib/io"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
type STCPVisitor struct {
*BaseVisitor
cfg *config.STCPVisitorConf
}
func (sv *STCPVisitor) Run() (err error) {
if sv.cfg.BindPort > 0 {
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return
}
go sv.worker()
}
go sv.internalConnWorker()
return
}
func (sv *STCPVisitor) Close() {
sv.BaseVisitor.Close()
}
func (sv *STCPVisitor) worker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.l.Accept()
if err != nil {
xl.Warn("stcp local listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) internalConnWorker() {
xl := xlog.FromContextSafe(sv.ctx)
for {
conn, err := sv.internalLn.Accept()
if err != nil {
xl.Warn("stcp internal listener closed")
return
}
go sv.handleConn(conn)
}
}
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close()
xl.Debug("get a new stcp user connection")
visitorConn, err := sv.helper.ConnectServer()
if err != nil {
return
}
defer visitorConn.Close()
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
RunID: sv.helper.RunID(),
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
xl.Warn("send newVisitorConnMsg to server error: %v", err)
return
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
xl.Warn("get newVisitorConnRespMsg error: %v", err)
return
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
return
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return
}
}
if sv.cfg.UseCompression {
var releaseFn func()
remote, releaseFn = libio.WithCompressionFromPool(remote)
defer releaseFn()
}
libio.Join(userConn, remote)
}

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

@@ -0,0 +1,264 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package visitor
import (
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
type SUDPVisitor struct {
*BaseVisitor
checkCloseCh chan struct{}
// udpConn is the listener of udp packet
udpConn *net.UDPConn
readCh chan *msg.UDPPacket
sendCh chan *msg.UDPPacket
cfg *config.SUDPVisitorConf
}
// SUDP Run start listen a udp port
func (sv *SUDPVisitor) Run() (err error) {
xl := xlog.FromContextSafe(sv.ctx)
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
if err != nil {
return fmt.Errorf("sudp ResolveUDPAddr error: %v", err)
}
sv.udpConn, err = net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("listen udp port %s error: %v", addr.String(), err)
}
sv.sendCh = make(chan *msg.UDPPacket, 1024)
sv.readCh = make(chan *msg.UDPPacket, 1024)
xl.Info("sudp start to work, listen on %s", addr)
go sv.dispatcher()
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize))
return
}
func (sv *SUDPVisitor) dispatcher() {
xl := xlog.FromContextSafe(sv.ctx)
var (
visitorConn net.Conn
err error
firstPacket *msg.UDPPacket
)
for {
select {
case firstPacket = <-sv.sendCh:
if firstPacket == nil {
xl.Info("frpc sudp visitor proxy is closed")
return
}
case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed")
return
}
visitorConn, err = sv.getNewVisitorConn()
if err != nil {
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err)
continue
}
// visitorConn always be closed when worker done.
sv.worker(visitorConn, firstPacket)
select {
case <-sv.checkCloseCh:
return
default:
}
}
}
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker")
wg := &sync.WaitGroup{}
wg.Add(2)
closeCh := make(chan struct{})
// udp service -> frpc -> frps -> frpc visitor -> user
workConnReaderFn := func(conn net.Conn) {
defer func() {
conn.Close()
close(closeCh)
wg.Done()
}()
for {
var (
rawMsg msg.Message
errRet error
)
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
xl.Warn("read from workconn for user udp conn error: %v", errRet)
return
}
_ = conn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Ping:
xl.Debug("frpc visitor get ping message from frpc")
continue
case *msg.UDPPacket:
if errRet := errors.PanicToError(func() {
sv.readCh <- m
xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content)
}); errRet != nil {
xl.Info("reader goroutine for udp work connection closed")
return
}
}
}
}
// udp service <- frpc <- frps <- frpc visitor <- user
workConnSenderFn := func(conn net.Conn) {
defer func() {
conn.Close()
wg.Done()
}()
var errRet error
if firstPacket != nil {
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", firstPacket.Content)
}
for {
select {
case udpMsg, ok := <-sv.sendCh:
if !ok {
xl.Info("sender goroutine for udp work connection closed")
return
}
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", udpMsg.Content)
case <-closeCh:
return
}
}
}
go workConnReaderFn(workConn)
go workConnSenderFn(workConn)
wg.Wait()
xl.Info("sudp worker is closed")
}
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
xl := xlog.FromContextSafe(sv.ctx)
visitorConn, err := sv.helper.ConnectServer()
if err != nil {
return nil, fmt.Errorf("frpc connect frps error: %v", err)
}
now := time.Now().Unix()
newVisitorConnMsg := &msg.NewVisitorConn{
RunID: sv.helper.RunID(),
ProxyName: sv.cfg.ServerName,
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
Timestamp: now,
UseEncryption: sv.cfg.UseEncryption,
UseCompression: sv.cfg.UseCompression,
}
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil {
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
}
var newVisitorConnRespMsg msg.NewVisitorConnResp
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil {
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
}
_ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" {
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
}
var remote io.ReadWriteCloser
remote = visitorConn
if sv.cfg.UseEncryption {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
if err != nil {
xl.Error("create encryption stream error: %v", err)
return nil, err
}
}
if sv.cfg.UseCompression {
remote = libio.WithCompression(remote)
}
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
}
func (sv *SUDPVisitor) Close() {
sv.mu.Lock()
defer sv.mu.Unlock()
select {
case <-sv.checkCloseCh:
return
default:
close(sv.checkCloseCh)
}
sv.BaseVisitor.Close()
if sv.udpConn != nil {
sv.udpConn.Close()
}
close(sv.readCh)
close(sv.sendCh)
}

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

@@ -0,0 +1,104 @@
// Copyright 2017 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package visitor
import (
"context"
"net"
"sync"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
// Helper wrapps some functions for visitor to use.
type Helper interface {
// ConnectServer directly connects to the frp server.
ConnectServer() (net.Conn, error)
// TransferConn transfers the connection to another visitor.
TransferConn(string, net.Conn) error
// MsgTransporter returns the message transporter that is used to send and receive messages
// to the frp server through the controller.
MsgTransporter() transport.MessageTransporter
// RunID returns the run id of current controller.
RunID() string
}
// Visitor is used for forward traffics from local port tot remote service.
type Visitor interface {
Run() error
AcceptConn(conn net.Conn) error
Close()
}
func NewVisitor(
ctx context.Context,
cfg config.VisitorConf,
clientCfg config.ClientCommonConf,
helper Helper,
) (visitor Visitor) {
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().ProxyName)
baseVisitor := BaseVisitor{
clientCfg: clientCfg,
helper: helper,
ctx: xlog.NewContext(ctx, xl),
internalLn: utilnet.NewInternalListener(),
}
switch cfg := cfg.(type) {
case *config.STCPVisitorConf:
visitor = &STCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
}
case *config.XTCPVisitorConf:
visitor = &XTCPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
startTunnelCh: make(chan struct{}),
}
case *config.SUDPVisitorConf:
visitor = &SUDPVisitor{
BaseVisitor: &baseVisitor,
cfg: cfg,
checkCloseCh: make(chan struct{}),
}
}
return
}
type BaseVisitor struct {
clientCfg config.ClientCommonConf
helper Helper
l net.Listener
internalLn *utilnet.InternalListener
mu sync.RWMutex
ctx context.Context
}
func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
return v.internalLn.PutConn(conn)
}
func (v *BaseVisitor) Close() {
if v.l != nil {
v.l.Close()
}
if v.internalLn != nil {
v.internalLn.Close()
}
}

View File

@@ -12,43 +12,60 @@
// 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.
package client package visitor
import ( import (
"context" "context"
"fmt"
"net"
"reflect"
"sync" "sync"
"time" "time"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
type VisitorManager struct { type Manager struct {
ctl *Control clientCfg config.ClientCommonConf
cfgs map[string]config.VisitorConf
cfgs map[string]config.VisitorConf visitors map[string]Visitor
visitors map[string]Visitor helper Helper
checkInterval time.Duration checkInterval time.Duration
mu sync.Mutex mu sync.RWMutex
ctx context.Context ctx context.Context
stopCh chan struct{} stopCh chan struct{}
} }
func NewVisitorManager(ctx context.Context, ctl *Control) *VisitorManager { func NewManager(
return &VisitorManager{ ctx context.Context,
ctl: ctl, runID string,
clientCfg config.ClientCommonConf,
connectServer func() (net.Conn, error),
msgTransporter transport.MessageTransporter,
) *Manager {
m := &Manager{
clientCfg: clientCfg,
cfgs: make(map[string]config.VisitorConf), cfgs: make(map[string]config.VisitorConf),
visitors: make(map[string]Visitor), visitors: make(map[string]Visitor),
checkInterval: 10 * time.Second, checkInterval: 10 * time.Second,
ctx: ctx, ctx: ctx,
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
} }
m.helper = &visitorHelperImpl{
connectServerFn: connectServer,
msgTransporter: msgTransporter,
transferConnFn: m.TransferConn,
runID: runID,
}
return m
} }
func (vm *VisitorManager) Run() { func (vm *Manager) Run() {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
ticker := time.NewTicker(vm.checkInterval) ticker := time.NewTicker(vm.checkInterval)
@@ -62,10 +79,10 @@ func (vm *VisitorManager) Run() {
case <-ticker.C: case <-ticker.C:
vm.mu.Lock() vm.mu.Lock()
for _, cfg := range vm.cfgs { for _, cfg := range vm.cfgs {
name := cfg.GetBaseInfo().ProxyName name := cfg.GetBaseConfig().ProxyName
if _, exist := vm.visitors[name]; !exist { if _, exist := vm.visitors[name]; !exist {
xl.Info("try to start visitor [%s]", name) xl.Info("try to start visitor [%s]", name)
vm.startVisitor(cfg) _ = vm.startVisitor(cfg)
} }
} }
vm.mu.Unlock() vm.mu.Unlock()
@@ -73,11 +90,24 @@ func (vm *VisitorManager) Run() {
} }
} }
func (vm *Manager) Close() {
vm.mu.Lock()
defer vm.mu.Unlock()
for _, v := range vm.visitors {
v.Close()
}
select {
case <-vm.stopCh:
default:
close(vm.stopCh)
}
}
// Hold lock before calling this function. // Hold lock before calling this function.
func (vm *VisitorManager) startVisitor(cfg config.VisitorConf) (err error) { func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
name := cfg.GetBaseInfo().ProxyName name := cfg.GetBaseConfig().ProxyName
visitor := NewVisitor(vm.ctx, vm.ctl, cfg) visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
err = visitor.Run() err = visitor.Run()
if err != nil { if err != nil {
xl.Warn("start error: %v", err) xl.Warn("start error: %v", err)
@@ -88,7 +118,7 @@ func (vm *VisitorManager) startVisitor(cfg config.VisitorConf) (err error) {
return return
} }
func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) { func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
xl := xlog.FromContextSafe(vm.ctx) xl := xlog.FromContextSafe(vm.ctx)
vm.mu.Lock() vm.mu.Lock()
defer vm.mu.Unlock() defer vm.mu.Unlock()
@@ -97,12 +127,8 @@ func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) {
for name, oldCfg := range vm.cfgs { for name, oldCfg := range vm.cfgs {
del := false del := false
cfg, ok := cfgs[name] cfg, ok := cfgs[name]
if !ok { if !ok || !reflect.DeepEqual(oldCfg, cfg) {
del = true del = true
} else {
if !oldCfg.Compare(cfg) {
del = true
}
} }
if del { if del {
@@ -123,24 +149,44 @@ func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) {
if _, ok := vm.cfgs[name]; !ok { if _, ok := vm.cfgs[name]; !ok {
vm.cfgs[name] = cfg vm.cfgs[name] = cfg
addNames = append(addNames, name) addNames = append(addNames, name)
vm.startVisitor(cfg) _ = vm.startVisitor(cfg)
} }
} }
if len(addNames) > 0 { if len(addNames) > 0 {
xl.Info("visitor added: %v", addNames) xl.Info("visitor added: %v", addNames)
} }
return
} }
func (vm *VisitorManager) Close() { // TransferConn transfers a connection to a visitor.
vm.mu.Lock() func (vm *Manager) TransferConn(name string, conn net.Conn) error {
defer vm.mu.Unlock() vm.mu.RLock()
for _, v := range vm.visitors { defer vm.mu.RUnlock()
v.Close() v, ok := vm.visitors[name]
} if !ok {
select { return fmt.Errorf("visitor [%s] not found", name)
case <-vm.stopCh:
default:
close(vm.stopCh)
} }
return v.AcceptConn(conn)
}
type visitorHelperImpl struct {
connectServerFn func() (net.Conn, error)
msgTransporter transport.MessageTransporter
transferConnFn func(name string, conn net.Conn) error
runID string
}
func (v *visitorHelperImpl) ConnectServer() (net.Conn, error) {
return v.connectServerFn()
}
func (v *visitorHelperImpl) TransferConn(name string, conn net.Conn) error {
return v.transferConnFn(name, conn)
}
func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
return v.msgTransporter
}
func (v *visitorHelperImpl) RunID() string {
return v.runID
} }

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

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

View File

@@ -15,18 +15,10 @@
package main package main
import ( import (
"math/rand"
"time"
_ "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/golib/crypto"
) )
func main() { func main() {
crypto.DefaultSalt = "frp"
rand.Seed(time.Now().UnixNano())
sub.Execute() sub.Execute()
} }

View File

@@ -19,10 +19,10 @@ import (
"os" "os"
"strings" "strings"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -39,6 +39,8 @@ func init() {
httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
httpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(httpCmd) rootCmd.AddCommand(httpCmd)
} }
@@ -70,8 +72,14 @@ var httpCmd = &cobra.Command{
cfg.HostHeaderRewrite = hostHeaderRewrite cfg.HostHeaderRewrite = hostHeaderRewrite
cfg.UseEncryption = useEncryption cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli() err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -35,6 +35,8 @@ func init() {
httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(httpsCmd) rootCmd.AddCommand(httpsCmd)
} }
@@ -62,8 +64,14 @@ var httpsCmd = &cobra.Command{
cfg.SubDomain = subDomain cfg.SubDomain = subDomain
cfg.UseEncryption = useEncryption cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli() err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

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

@@ -0,0 +1,97 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sub
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/nathole"
)
var (
natHoleSTUNServer string
natHoleLocalAddr string
)
func init() {
RegisterCommonFlags(natholeCmd)
rootCmd.AddCommand(natholeCmd)
natholeCmd.AddCommand(natholeDiscoveryCmd)
natholeCmd.PersistentFlags().StringVarP(&natHoleSTUNServer, "nat_hole_stun_server", "", "", "STUN server address for nathole")
natholeCmd.PersistentFlags().StringVarP(&natHoleLocalAddr, "nat_hole_local_addr", "l", "", "local address to connect STUN server")
}
var natholeCmd = &cobra.Command{
Use: "nathole",
Short: "Actions about nathole",
}
var natholeDiscoveryCmd = &cobra.Command{
Use: "discover",
Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters
cfg, _, _, err := config.ParseClientConfig(cfgFile)
if err != nil {
cfg = config.GetDefaultClientConf()
}
if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer
}
if err := validateForNatHoleDiscovery(cfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
addrs, localAddr, err := nathole.Discover([]string{cfg.NatHoleSTUNServer}, natHoleLocalAddr)
if err != nil {
fmt.Println("discover error:", err)
os.Exit(1)
}
if len(addrs) < 2 {
fmt.Printf("discover error: can not get enough addresses, need 2, got: %v\n", addrs)
os.Exit(1)
}
localIPs, _ := nathole.ListLocalIPsForNatHole(10)
natFeature, err := nathole.ClassifyNATFeature(addrs, localIPs)
if err != nil {
fmt.Println("classify nat feature error:", err)
os.Exit(1)
}
fmt.Println("STUN server:", cfg.NatHoleSTUNServer)
fmt.Println("Your NAT type is:", natFeature.NatType)
fmt.Println("Behavior is:", natFeature.Behavior)
fmt.Println("External address is:", addrs)
fmt.Println("Local address is:", localAddr.String())
fmt.Println("Public Network:", natFeature.PublicNetwork)
return nil
},
}
func validateForNatHoleDiscovery(cfg config.ClientCommonConf) error {
if cfg.NatHoleSTUNServer == "" {
return fmt.Errorf("nat_hole_stun_server can not be empty")
}
return nil
}

View File

@@ -22,9 +22,9 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatedier/frp/pkg/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
) )
func init() { func init() {

View File

@@ -17,21 +17,23 @@ package sub
import ( import (
"context" "context"
"fmt" "fmt"
"io/fs"
"net" "net"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strconv" "strconv"
"strings" "sync"
"syscall" "syscall"
"time" "time"
"github.com/spf13/cobra"
"github.com/fatedier/frp/client" "github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"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"
"github.com/spf13/cobra"
) )
const ( const (
@@ -41,6 +43,7 @@ const (
var ( var (
cfgFile string cfgFile string
cfgDir string
showVersion bool showVersion bool
serverAddr string serverAddr string
@@ -51,48 +54,51 @@ var (
logFile string logFile string
logMaxDays int logMaxDays int
disableLogColor bool disableLogColor bool
dnsServer string
proxyName string proxyName string
localIP string localIP string
localPort int localPort int
remotePort int remotePort int
useEncryption bool useEncryption bool
useCompression bool useCompression bool
customDomains string bandwidthLimit string
subDomain string bandwidthLimitMode string
httpUser string customDomains string
httpPwd string subDomain string
locations string httpUser string
hostHeaderRewrite string httpPwd string
role string locations string
sk string hostHeaderRewrite string
multiplexer string role string
serverName string sk string
bindAddr string multiplexer string
bindPort int serverName string
bindAddr string
bindPort int
tlsEnable bool tlsEnable bool
tlsServerName string
kcpDoneCh chan struct{}
) )
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().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
kcpDoneCh = make(chan struct{})
} }
func RegisterCommonFlags(cmd *cobra.Command) { func RegisterCommonFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address") cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address")
cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user") cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user")
cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket") cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp, kcp, quic, websocket, wss")
cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path") cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path")
cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days") cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days")
cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", false, "enable frpc tls") cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", true, "enable frpc tls")
cmd.PersistentFlags().StringVarP(&tlsServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
cmd.PersistentFlags().StringVarP(&dnsServer, "dns_server", "", "", "specify dns server instead of using system default one")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -104,28 +110,54 @@ var rootCmd = &cobra.Command{
return nil return nil
} }
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
// Note that it's only designed for testing. It's not guaranteed to be stable.
if cfgDir != "" {
_ = runMultipleClients(cfgDir)
return nil
}
// Do not show command usage here. // Do not show command usage here.
err := runClient(cfgFile) err := runClient(cfgFile)
if err != nil { if err != nil {
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
return nil return nil
}, },
} }
func runMultipleClients(cfgDir string) error {
var wg sync.WaitGroup
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
wg.Add(1)
time.Sleep(time.Millisecond)
go func() {
defer wg.Done()
err := runClient(path)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
}
}()
return nil
})
wg.Wait()
return err
}
func Execute() { func Execute() {
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }
} }
func handleSignal(svr *client.Service) { func handleTermSignal(svr *client.Service) {
ch := make(chan os.Signal) ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch <-ch
svr.GracefulClose(500 * time.Millisecond) svr.GracefulClose(500 * time.Millisecond)
close(kcpDoneCh)
} }
func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
@@ -150,15 +182,17 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
cfg.LogFile = logFile cfg.LogFile = logFile
cfg.LogMaxDays = int64(logMaxDays) cfg.LogMaxDays = int64(logMaxDays)
cfg.DisableLogColor = disableLogColor cfg.DisableLogColor = disableLogColor
cfg.DNSServer = dnsServer
// Only token authentication is supported in cmd mode // Only token authentication is supported in cmd mode
cfg.ClientConfig = auth.GetDefaultClientConf() cfg.ClientConfig = auth.GetDefaultClientConf()
cfg.Token = token cfg.Token = token
cfg.TLSEnable = tlsEnable cfg.TLSEnable = tlsEnable
cfg.TLSServerName = tlsServerName
cfg.Complete() cfg.Complete()
if err = cfg.Validate(); err != nil { if err = cfg.Validate(); err != nil {
err = fmt.Errorf("Parse config error: %v", err) err = fmt.Errorf("parse config error: %v", err)
return return
} }
return return
@@ -167,6 +201,7 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
func runClient(cfgFilePath string) error { func runClient(cfgFilePath string) error {
cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath) cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
if err != nil { if err != nil {
fmt.Println(err)
return err return err
} }
return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
@@ -178,22 +213,12 @@ func startService(
visitorCfgs map[string]config.VisitorConf, visitorCfgs map[string]config.VisitorConf,
cfgFile string, cfgFile string,
) (err error) { ) (err error) {
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
cfg.LogMaxDays, cfg.DisableLogColor) cfg.LogMaxDays, cfg.DisableLogColor)
if cfg.DNSServer != "" { if cfgFile != "" {
s := cfg.DNSServer log.Info("start frpc service for config file [%s]", cfgFile)
if !strings.Contains(s, ":") { defer log.Info("frpc service for config file [%s] stopped", cfgFile)
s += ":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", s)
},
}
} }
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
if errRet != nil { if errRet != nil {
@@ -201,14 +226,12 @@ func startService(
return return
} }
// Capture the exit signal if we use kcp. shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"
if cfg.Protocol == "kcp" { // Capture the exit signal if we use kcp or quic.
go handleSignal(svr) if shouldGracefulClose {
go handleTermSignal(svr)
} }
err = svr.Run() _ = svr.Run(context.Background())
if err == nil && cfg.Protocol == "kcp" {
<-kcpDoneCh
}
return return
} }

View File

@@ -23,11 +23,11 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
"github.com/rodaine/table" "github.com/rodaine/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
) )
func init() { func init() {
@@ -81,67 +81,27 @@ func status(clientCfg config.ClientCommonConf) error {
if err != nil { if err != nil {
return err return err
} }
res := &client.StatusResp{} res := make(client.StatusResp)
err = json.Unmarshal(body, &res) err = json.Unmarshal(body, &res)
if err != nil { if err != nil {
return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body))) return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body)))
} }
fmt.Println("Proxy Status...") fmt.Println("Proxy Status...")
if len(res.TCP) > 0 { types := []string{"tcp", "udp", "tcpmux", "http", "https", "stcp", "sudp", "xtcp"}
fmt.Println("TCP") for _, pxyType := range types {
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") arrs := res[pxyType]
for _, ps := range res.TCP { if len(arrs) == 0 {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) continue
} }
tbl.Print()
fmt.Println("")
}
if len(res.UDP) > 0 {
fmt.Println("UDP")
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range res.UDP {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
if len(res.HTTP) > 0 {
fmt.Println("HTTP")
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range res.HTTP {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
if len(res.HTTPS) > 0 {
fmt.Println("HTTPS")
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range res.HTTPS {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
if len(res.STCP) > 0 {
fmt.Println("STCP")
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range res.STCP {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
if len(res.XTCP) > 0 {
fmt.Println("XTCP")
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range res.XTCP {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
fmt.Println(strings.ToUpper(pxyType))
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
for _, ps := range arrs {
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
}
tbl.Print()
fmt.Println("")
}
return nil return nil
} }

View File

@@ -18,10 +18,10 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -37,6 +37,8 @@ func init() {
stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(stcpCmd) rootCmd.AddCommand(stcpCmd)
} }
@@ -59,7 +61,8 @@ var stcpCmd = &cobra.Command{
prefix = user + "." prefix = user + "."
} }
if role == "server" { switch role {
case "server":
cfg := &config.STCPProxyConf{} cfg := &config.STCPProxyConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.STCPProxy cfg.ProxyType = consts.STCPProxy
@@ -69,13 +72,19 @@ var stcpCmd = &cobra.Command{
cfg.Sk = sk cfg.Sk = sk
cfg.LocalIP = localIP cfg.LocalIP = localIP
cfg.LocalPort = localPort cfg.LocalPort = localPort
err = cfg.CheckForCli() cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
proxyConfs[cfg.ProxyName] = cfg proxyConfs[cfg.ProxyName] = cfg
} else if role == "visitor" { case "visitor":
cfg := &config.STCPVisitorConf{} cfg := &config.STCPVisitorConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.STCPProxy cfg.ProxyType = consts.STCPProxy
@@ -86,13 +95,13 @@ var stcpCmd = &cobra.Command{
cfg.ServerName = serverName cfg.ServerName = serverName
cfg.BindAddr = bindAddr cfg.BindAddr = bindAddr
cfg.BindPort = bindPort cfg.BindPort = bindPort
err = cfg.Check() err = cfg.Validate()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
visitorConfs[cfg.ProxyName] = cfg visitorConfs[cfg.ProxyName] = cfg
} else { default:
fmt.Println("invalid role") fmt.Println("invalid role")
os.Exit(1) os.Exit(1)
} }

84
cmd/frpc/sub/stop.go Normal file
View File

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

View File

@@ -18,10 +18,10 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -37,6 +37,8 @@ func init() {
sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(sudpCmd) rootCmd.AddCommand(sudpCmd)
} }
@@ -59,7 +61,8 @@ var sudpCmd = &cobra.Command{
prefix = user + "." prefix = user + "."
} }
if role == "server" { switch role {
case "server":
cfg := &config.SUDPProxyConf{} cfg := &config.SUDPProxyConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.SUDPProxy cfg.ProxyType = consts.SUDPProxy
@@ -69,13 +72,19 @@ var sudpCmd = &cobra.Command{
cfg.Sk = sk cfg.Sk = sk
cfg.LocalIP = localIP cfg.LocalIP = localIP
cfg.LocalPort = localPort cfg.LocalPort = localPort
err = cfg.CheckForCli() cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
proxyConfs[cfg.ProxyName] = cfg proxyConfs[cfg.ProxyName] = cfg
} else if role == "visitor" { case "visitor":
cfg := &config.SUDPVisitorConf{} cfg := &config.SUDPVisitorConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.SUDPProxy cfg.ProxyType = consts.SUDPProxy
@@ -86,13 +95,13 @@ var sudpCmd = &cobra.Command{
cfg.ServerName = serverName cfg.ServerName = serverName
cfg.BindAddr = bindAddr cfg.BindAddr = bindAddr
cfg.BindPort = bindPort cfg.BindPort = bindPort
err = cfg.Check() err = cfg.Validate()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
visitorConfs[cfg.ProxyName] = cfg visitorConfs[cfg.ProxyName] = cfg
} else { default:
fmt.Println("invalid role") fmt.Println("invalid role")
os.Exit(1) os.Exit(1)
} }

View File

@@ -33,6 +33,8 @@ func init() {
tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(tcpCmd) rootCmd.AddCommand(tcpCmd)
} }
@@ -59,8 +61,14 @@ var tcpCmd = &cobra.Command{
cfg.RemotePort = remotePort cfg.RemotePort = remotePort
cfg.UseEncryption = useEncryption cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli() err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -36,6 +36,8 @@ func init() {
tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer") tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer")
tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(tcpMuxCmd) rootCmd.AddCommand(tcpMuxCmd)
} }
@@ -64,8 +66,14 @@ var tcpMuxCmd = &cobra.Command{
cfg.Multiplexer = multiplexer cfg.Multiplexer = multiplexer
cfg.UseEncryption = useEncryption cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli() err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -18,10 +18,10 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -33,6 +33,8 @@ func init() {
udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
udpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(udpCmd) rootCmd.AddCommand(udpCmd)
} }
@@ -59,8 +61,14 @@ var udpCmd = &cobra.Command{
cfg.RemotePort = remotePort cfg.RemotePort = remotePort
cfg.UseEncryption = useEncryption cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression cfg.UseCompression = useCompression
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.CheckForCli() err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -18,9 +18,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatedier/frp/pkg/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
) )
func init() { func init() {

View File

@@ -18,10 +18,10 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -37,6 +37,8 @@ func init() {
xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
rootCmd.AddCommand(xtcpCmd) rootCmd.AddCommand(xtcpCmd)
} }
@@ -59,7 +61,8 @@ var xtcpCmd = &cobra.Command{
prefix = user + "." prefix = user + "."
} }
if role == "server" { switch role {
case "server":
cfg := &config.XTCPProxyConf{} cfg := &config.XTCPProxyConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.XTCPProxy cfg.ProxyType = consts.XTCPProxy
@@ -69,13 +72,19 @@ var xtcpCmd = &cobra.Command{
cfg.Sk = sk cfg.Sk = sk
cfg.LocalIP = localIP cfg.LocalIP = localIP
cfg.LocalPort = localPort cfg.LocalPort = localPort
err = cfg.CheckForCli() cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg.BandwidthLimitMode = bandwidthLimitMode
err = cfg.ValidateForClient()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
proxyConfs[cfg.ProxyName] = cfg proxyConfs[cfg.ProxyName] = cfg
} else if role == "visitor" { case "visitor":
cfg := &config.XTCPVisitorConf{} cfg := &config.XTCPVisitorConf{}
cfg.ProxyName = prefix + proxyName cfg.ProxyName = prefix + proxyName
cfg.ProxyType = consts.XTCPProxy cfg.ProxyType = consts.XTCPProxy
@@ -86,13 +95,13 @@ var xtcpCmd = &cobra.Command{
cfg.ServerName = serverName cfg.ServerName = serverName
cfg.BindAddr = bindAddr cfg.BindAddr = bindAddr
cfg.BindPort = bindPort cfg.BindPort = bindPort
err = cfg.Check() err = cfg.Validate()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
visitorConfs[cfg.ProxyName] = cfg visitorConfs[cfg.ProxyName] = cfg
} else { default:
fmt.Println("invalid role") fmt.Println("invalid role")
os.Exit(1) os.Exit(1)
} }

View File

@@ -26,6 +26,7 @@ import (
func main() { func main() {
crypto.DefaultSalt = "frp" crypto.DefaultSalt = "frp"
// TODO: remove this when we drop support for go1.19
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
Execute() Execute()

View File

@@ -15,17 +15,18 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/util" "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/server" "github.com/fatedier/frp/server"
"github.com/spf13/cobra"
) )
const ( const (
@@ -37,31 +38,30 @@ var (
cfgFile string cfgFile string
showVersion bool showVersion bool
bindAddr string bindAddr string
bindPort int bindPort int
bindUDPPort int kcpBindPort int
kcpBindPort int proxyBindAddr string
proxyBindAddr string vhostHTTPPort int
vhostHTTPPort int vhostHTTPSPort int
vhostHTTPSPort int vhostHTTPTimeout int64
vhostHTTPTimeout int64 dashboardAddr string
dashboardAddr string dashboardPort int
dashboardPort int dashboardUser string
dashboardUser string dashboardPwd string
dashboardPwd string enablePrometheus bool
enablePrometheus bool logFile string
assetsDir string logLevel string
logFile string logMaxDays int64
logLevel string disableLogColor bool
logMaxDays int64 token string
disableLogColor bool subDomainHost string
token string allowPorts string
subDomainHost string maxPortsPerClient int64
tcpMux bool tlsOnly bool
allowPorts string dashboardTLSMode bool
maxPoolCount int64 dashboardTLSCertFile string
maxPortsPerClient int64 dashboardTLSKeyFile string
tlsOnly bool
) )
func init() { func init() {
@@ -70,13 +70,12 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address") rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address")
rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port") rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port")
rootCmd.PersistentFlags().IntVarP(&bindUDPPort, "bind_udp_port", "", 0, "bind udp port")
rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dasboard address") rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port") rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port")
rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user") rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user")
rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password") rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password")
@@ -91,6 +90,9 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports") rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports")
rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only") rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only")
rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode")
rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -145,7 +147,7 @@ func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonC
cfg.Complete() cfg.Complete()
err = cfg.Validate() err = cfg.Validate()
if err != nil { if err != nil {
err = fmt.Errorf("Parse config error: %v", err) err = fmt.Errorf("parse config error: %v", err)
return return
} }
return return
@@ -156,7 +158,6 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
cfg.BindAddr = bindAddr cfg.BindAddr = bindAddr
cfg.BindPort = bindPort cfg.BindPort = bindPort
cfg.BindUDPPort = bindUDPPort
cfg.KCPBindPort = kcpBindPort cfg.KCPBindPort = kcpBindPort
cfg.ProxyBindAddr = proxyBindAddr cfg.ProxyBindAddr = proxyBindAddr
cfg.VhostHTTPPort = vhostHTTPPort cfg.VhostHTTPPort = vhostHTTPPort
@@ -167,6 +168,9 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
cfg.DashboardUser = dashboardUser cfg.DashboardUser = dashboardUser
cfg.DashboardPwd = dashboardPwd cfg.DashboardPwd = dashboardPwd
cfg.EnablePrometheus = enablePrometheus cfg.EnablePrometheus = enablePrometheus
cfg.DashboardTLSCertFile = dashboardTLSCertFile
cfg.DashboardTLSKeyFile = dashboardTLSKeyFile
cfg.DashboardTLSMode = dashboardTLSMode
cfg.LogFile = logFile cfg.LogFile = logFile
cfg.LogLevel = logLevel cfg.LogLevel = logLevel
cfg.LogMaxDays = logMaxDays cfg.LogMaxDays = logMaxDays
@@ -180,7 +184,7 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
// e.g. 1000-2000,2001,2002,3000-4000 // e.g. 1000-2000,2001,2002,3000-4000
ports, errRet := util.ParseRangeNumbers(allowPorts) ports, errRet := util.ParseRangeNumbers(allowPorts)
if errRet != nil { if errRet != nil {
err = fmt.Errorf("Parse conf error: allow_ports: %v", errRet) err = fmt.Errorf("parse conf error: allow_ports: %v", errRet)
return return
} }
@@ -207,6 +211,6 @@ func runServer(cfg config.ServerCommonConf) (err error) {
return err return err
} }
log.Info("frps started successfully") log.Info("frps started successfully")
svr.Run() svr.Run(context.Background())
return return
} }

View File

@@ -18,9 +18,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/fatedier/frp/pkg/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
) )
func init() { func init() {

View File

@@ -6,6 +6,16 @@
server_addr = 0.0.0.0 server_addr = 0.0.0.0
server_port = 7000 server_port = 7000
# STUN server to help penetrate NAT hole.
# nat_hole_stun_server = stun.easyvoip.com:3478
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
# dial_server_timeout = 10
# dial_server_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# dial_server_keepalive = 7200
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set http_proxy here or in global environment variables # if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set http_proxy here or in global environment variables
# it only works when protocol is tcp # it only works when protocol is tcp
# http_proxy = http://user:passwd@192.168.1.128:8080 # http_proxy = http://user:passwd@192.168.1.128:8080
@@ -33,6 +43,8 @@ authenticate_new_work_conns = false
# auth token # auth token
token = 12345678 token = 12345678
authentication_method =
# oidc_client_id specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". # oidc_client_id specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc".
# By default, this value is "". # By default, this value is "".
oidc_client_id = oidc_client_id =
@@ -44,10 +56,19 @@ 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 =
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint. # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "". # It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "".
oidc_token_endpoint_url = oidc_token_endpoint_url =
# oidc_additional_xxx specifies additional parameters to be sent to the OIDC Token Endpoint.
# For example, if you want to specify the "audience" parameter, you can set as follow.
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
# oidc_additional_audience = https://dev.auth.com/api/v2/
# oidc_additional_var1 = foobar
# set admin address for control frpc's action by http api such as reload # set admin address for control frpc's action by http api such as reload
admin_addr = 127.0.0.1 admin_addr = 127.0.0.1
admin_port = 7400 admin_port = 7400
@@ -60,7 +81,8 @@ admin_pwd = admin
pool_count = 5 pool_count = 5
# if tcp stream multiplexing is used, default is true, it must be same with frps # if tcp stream multiplexing is used, default is true, it must be same with frps
tcp_mux = true # tcp_mux = true
# specify keep alive interval for tcp mux. # specify keep alive interval for tcp mux.
# only valid if tcp_mux is true. # only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60 # tcp_mux_keepalive_interval = 60
@@ -73,14 +95,20 @@ user = your_name
login_fail_exit = true login_fail_exit = true
# communication protocol used to connect to server # communication protocol used to connect to server
# now it supports tcp, kcp and websocket, default is tcp # supports tcp, kcp, quic, websocket and wss now, default is tcp
protocol = tcp protocol = tcp
# set client binding ip when connect server, default is empty. # set client binding ip when connect server, default is empty.
# only when protocol = tcp or websocket, the value will be used. # only when protocol = tcp or websocket, the value will be used.
connect_server_local_ip = 0.0.0.0 connect_server_local_ip = 0.0.0.0
# if tls_enable is true, frpc will connect frps by tls # quic protocol options
# quic_keepalive_period = 10
# quic_max_idle_timeout = 30
# quic_max_incoming_streams = 100000
# If tls_enable is true, frpc will connect frps by tls.
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
tls_enable = true tls_enable = true
# tls_cert_file = client.crt # tls_cert_file = client.crt
@@ -91,7 +119,7 @@ tls_enable = true
# specify a dns server, so frpc will use this instead of default one # specify a dns server, so frpc will use this instead of default one
# dns_server = 8.8.8.8 # dns_server = 8.8.8.8
# proxy names you want to start seperated by ',' # proxy names you want to start separated by ','
# default is empty, means all proxies # default is empty, means all proxies
# start = ssh,dns # start = ssh,dns
@@ -113,9 +141,14 @@ udp_packet_size = 1500
# include other config files for proxies. # include other config files for proxies.
# includes = ./confd/*.ini # includes = ./confd/*.ini
# By default, frpc will connect frps with first custom byte if tls is enabled. # If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
# If DisableCustomTLSFirstByte is true, frpc will not send that custom byte. # first custom byte when tls is enabled.
disable_custom_tls_first_byte = false # Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
disable_custom_tls_first_byte = true
# Enable golang pprof handlers in admin listener.
# Admin port must be set first.
pprof_enable = false
# 'ssh' is the unique proxy name # 'ssh' is the unique proxy name
# if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' # if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
@@ -126,6 +159,8 @@ local_ip = 127.0.0.1
local_port = 22 local_port = 22
# limit bandwidth for this proxy, unit is KB and MB # limit bandwidth for this proxy, unit is KB and MB
bandwidth_limit = 1MB bandwidth_limit = 1MB
# where to limit bandwidth, can be 'client' or 'server', default is 'client'
bandwidth_limit_mode = client
# true or false, if true, messages between frps and frpc will be encrypted, default is false # true or false, if true, messages between frps and frpc will be encrypted, default is false
use_encryption = false use_encryption = false
# if true, message will be compressed # if true, message will be compressed
@@ -198,6 +233,8 @@ subdomain = web01
custom_domains = web01.yourdomain.com custom_domains = web01.yourdomain.com
# locations is only available for http type # locations is only available for http type
locations = /,/pic locations = /,/pic
# route requests to this service if http basic auto user is abc
# route_by_http_user = abc
host_header_rewrite = example.com host_header_rewrite = example.com
# params with prefix "header_" will be used to update http request headers # params with prefix "header_" will be used to update http request headers
header_X-From-Where = frp header_X-From-Where = frp
@@ -215,7 +252,7 @@ local_ip = 127.0.0.1
local_port = 8000 local_port = 8000
use_encryption = false use_encryption = false
use_compression = false use_compression = false
subdomain = web01 subdomain = web02
custom_domains = web02.yourdomain.com custom_domains = web02.yourdomain.com
# if not empty, frpc will use proxy protocol to transfer connection info to your local service # if not empty, frpc will use proxy protocol to transfer connection info to your local service
# v1 or v2 or empty # v1 or v2 or empty
@@ -291,6 +328,9 @@ local_ip = 127.0.0.1
local_port = 22 local_port = 22
use_encryption = false use_encryption = false
use_compression = false use_compression = false
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allow_users = *
# user of frpc should be same in both stcp server and stcp visitor # user of frpc should be same in both stcp server and stcp visitor
[secret_tcp_visitor] [secret_tcp_visitor]
@@ -302,6 +342,8 @@ server_name = secret_tcp
sk = abcdefg sk = abcdefg
# connect this address to visitor stcp server # connect this address to visitor stcp server
bind_addr = 127.0.0.1 bind_addr = 127.0.0.1
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bind_port = 9000 bind_port = 9000
use_encryption = false use_encryption = false
use_compression = false use_compression = false
@@ -313,16 +355,30 @@ local_ip = 127.0.0.1
local_port = 22 local_port = 22
use_encryption = false use_encryption = false
use_compression = false use_compression = false
# If not empty, only visitors from specified users can connect.
# Otherwise, visitors from same user can connect. '*' means allow all users.
allow_users = user1, user2
[p2p_tcp_visitor] [p2p_tcp_visitor]
role = visitor role = visitor
type = xtcp type = xtcp
# if the server user is not set, it defaults to the current user
server_user = user1
server_name = p2p_tcp server_name = p2p_tcp
sk = abcdefg sk = abcdefg
bind_addr = 127.0.0.1 bind_addr = 127.0.0.1
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
# other visitors. (This is not supported for SUDP now)
bind_port = 9001 bind_port = 9001
use_encryption = false use_encryption = false
use_compression = false use_compression = false
# when automatic tunnel persistence is required, set it to true
keep_tunnel_open = false
# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
max_retries_an_hour = 8
min_retry_interval = 90
# fallback_to = stcp_visitor
# fallback_timeout_ms = 500
[tcpmuxhttpconnect] [tcpmuxhttpconnect]
type = tcpmux type = tcpmux
@@ -330,3 +386,4 @@ multiplexer = httpconnect
local_ip = 127.0.0.1 local_ip = 127.0.0.1
local_port = 10701 local_port = 10701
custom_domains = tunnel1 custom_domains = tunnel1
# route_by_http_user = user1

View File

@@ -6,13 +6,18 @@
bind_addr = 0.0.0.0 bind_addr = 0.0.0.0
bind_port = 7000 bind_port = 7000
# udp port to help make udp hole to penetrate nat # udp port used for kcp protocol, it can be same with 'bind_port'.
bind_udp_port = 7001 # if not set, kcp is disabled in frps.
# udp port used for kcp protocol, it can be same with 'bind_port'
# if not set, kcp is disabled in frps
kcp_bind_port = 7000 kcp_bind_port = 7000
# udp port used for quic protocol.
# if not set, quic is disabled in frps.
# quic_bind_port = 7002
# quic protocol options
# quic_keepalive_period = 10
# quic_max_idle_timeout = 30
# quic_max_incoming_streams = 100000
# specify which address proxy will listen for, default value is same with bind_addr # specify which address proxy will listen for, default value is same with bind_addr
# proxy_bind_addr = 127.0.0.1 # proxy_bind_addr = 127.0.0.1
@@ -30,6 +35,9 @@ vhost_https_port = 443
# HTTP CONNECT requests. By default, this value is 0. # HTTP CONNECT requests. By default, this value is 0.
# tcpmux_httpconnect_port = 1337 # tcpmux_httpconnect_port = 1337
# If tcpmux_passthrough is true, frps won't do any update on traffic.
# tcpmux_passthrough = false
# set dashboard_addr and dashboard_port to view dashboard of frps # set dashboard_addr and dashboard_port to view dashboard of frps
# dashboard_addr's default value is same with bind_addr # dashboard_addr's default value is same with bind_addr
# dashboard is available only if dashboard_port is set # dashboard is available only if dashboard_port is set
@@ -40,6 +48,11 @@ dashboard_port = 7500
dashboard_user = admin dashboard_user = admin
dashboard_pwd = admin dashboard_pwd = admin
# dashboard TLS mode
dashboard_tls_mode = false
# dashboard_tls_cert_file = server.crt
# dashboard_tls_key_file = server.key
# enable_prometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} in /metrics api. # enable_prometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} in /metrics api.
enable_prometheus = true enable_prometheus = true
@@ -86,7 +99,6 @@ oidc_audience =
# By default, this value is false. # By default, this value is false.
oidc_skip_expiry_check = false oidc_skip_expiry_check = false
# oidc_skip_issuer_check specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer. # oidc_skip_issuer_check specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
# By default, this value is false. # By default, this value is false.
oidc_skip_issuer_check = false oidc_skip_issuer_check = false
@@ -120,11 +132,16 @@ tls_only = false
subdomain_host = frps.com subdomain_host = frps.com
# if tcp stream multiplexing is used, default is true # if tcp stream multiplexing is used, default is true
tcp_mux = true # tcp_mux = true
# specify keep alive interval for tcp mux. # specify keep alive interval for tcp mux.
# only valid if tcp_mux is true. # only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60 # tcp_mux_keepalive_interval = 60
# tcp_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# tcp_keepalive = 7200
# custom 404 page for HTTP requests # custom 404 page for HTTP requests
# custom_404_page = /path/to/404.html # custom_404_page = /path/to/404.html
@@ -133,6 +150,13 @@ tcp_mux = true
# It affects the udp and sudp proxy. # It affects the udp and sudp proxy.
udp_packet_size = 1500 udp_packet_size = 1500
# Enable golang pprof handlers in dashboard listener.
# Dashboard port must be set first
pprof_enable = false
# Retention time for NAT hole punching strategy data.
nat_hole_analysis_data_reserve_hours = 168
[plugin.user-manager] [plugin.user-manager]
addr = 127.0.0.1:9000 addr = 127.0.0.1:9000
path = /handler path = /handler

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Frp Client Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frpc -c /etc/frp/frpc.ini
ExecReload=/usr/bin/frpc reload -c /etc/frp/frpc.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Frp Client Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frpc -c /etc/frp/%i.ini
ExecReload=/usr/bin/frpc reload -c /etc/frp/%i.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Frp Server Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frps -c /etc/frp/frps.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Frp Server Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frps -c /etc/frp/%i.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

BIN
doc/pic/sponsor_asocks.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 41 KiB

BIN
doc/pic/sponsor_workos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -70,7 +70,7 @@ The response can look like any of the following:
### Operation ### Operation
Currently `Login`, `NewProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported. Currently `Login`, `NewProxy`, `CloseProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported.
#### Login #### Login
@@ -110,6 +110,8 @@ Create new proxy
"proxy_type": <string>, "proxy_type": <string>,
"use_encryption": <bool>, "use_encryption": <bool>,
"use_compression": <bool>, "use_compression": <bool>,
"bandwidth_limit": <string>,
"bandwidth_limit_mode": <string>,
"group": <string>, "group": <string>,
"group_key": <string>, "group_key": <string>,
@@ -136,6 +138,26 @@ Create new proxy
} }
``` ```
#### CloseProxy
A previously created proxy is closed.
Please note that one request will be sent for every proxy that is closed, do **NOT** use this
if you have too many proxies bound to a single client, as this may exhaust the server's resources.
```
{
"content": {
"user": {
"user": <string>,
"metas": map<string>string
"run_id": <string>
},
"proxy_name": <string>
}
}
```
#### Ping #### Ping
Heartbeat from frpc Heartbeat from frpc

View File

@@ -1,14 +1,12 @@
FROM alpine:3 AS temp FROM golang:1.20 AS building
COPY bin/frpc /tmp COPY . /building
WORKDIR /building
RUN chmod -R 777 /tmp/frpc
RUN make frpc
FROM alpine:3 FROM alpine:3
WORKDIR /app COPY --from=building /building/bin/frpc /usr/bin/frpc
COPY --from=temp /tmp/frpc /usr/bin
ENTRYPOINT ["/usr/bin/frpc"] ENTRYPOINT ["/usr/bin/frpc"]

View File

@@ -1,14 +1,12 @@
FROM alpine:3 AS temp FROM golang:1.20 AS building
COPY bin/frps /tmp COPY . /building
WORKDIR /building
RUN chmod -R 777 /tmp/frps
RUN make frps
FROM alpine:3 FROM alpine:3
WORKDIR /app COPY --from=building /building/bin/frps /usr/bin/frps
COPY --from=temp /tmp/frps /usr/bin
ENTRYPOINT ["/usr/bin/frps"] ENTRYPOINT ["/usr/bin/frps"]

91
go.mod
View File

@@ -1,33 +1,80 @@
module github.com/fatedier/frp module github.com/fatedier/frp
go 1.16 go 1.20
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 v2.2.1+incompatible github.com/coreos/go-oidc/v3 v3.4.0
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee github.com/fatedier/golib v0.1.1-0.20230720124328-204db2e322f8
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
github.com/go-playground/validator/v10 v10.6.1 github.com/go-playground/validator/v10 v10.11.0
github.com/google/uuid v1.2.0 github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c github.com/hashicorp/yamux v0.1.1
github.com/leodido/go-urn v1.2.1 // indirect github.com/onsi/ginkgo/v2 v2.8.3
github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.27.0
github.com/onsi/gomega v1.13.0 github.com/pion/stun v0.4.0
github.com/pires/go-proxyproto v0.5.0 github.com/pires/go-proxyproto v0.6.2
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/prometheus/client_golang v1.13.0
github.com/prometheus/client_golang v1.11.0 github.com/quic-go/quic-go v0.34.0
github.com/rodaine/table v1.0.1 github.com/rodaine/table v1.0.1
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.8.1
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/oauth2 v0.3.0
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect golang.org/x/sync v0.1.0
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
gopkg.in/ini.v1 v1.62.0 gopkg.in/ini.v1 v1.67.0
gopkg.in/square/go-jose.v2 v2.4.1 // indirect k8s.io/apimachinery v0.26.1
k8s.io/apimachinery v0.21.2 k8s.io/client-go v0.26.1
k8s.io/client-go v0.21.2
) )
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/klauspost/reedsolomon v1.9.15 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pion/transport/v2 v2.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
)
// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d

602
go.sum

File diff suppressed because it is too large Load Diff

63
hack/download.sh Executable file
View File

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

View File

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

View File

@@ -15,7 +15,7 @@ rm -rf ./release/packages
mkdir -p ./release/packages mkdir -p ./release/packages
os_all='linux windows darwin freebsd' os_all='linux windows darwin freebsd'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle' arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
cd ./release cd ./release

View File

@@ -18,10 +18,10 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/fatedier/frp/pkg/msg" "github.com/coreos/go-oidc/v3/oidc"
"github.com/coreos/go-oidc"
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
"github.com/fatedier/frp/pkg/msg"
) )
type OidcClientConfig struct { type OidcClientConfig struct {
@@ -34,20 +34,30 @@ type OidcClientConfig struct {
// is "". // is "".
OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
// OidcAudience specifies the audience of the token in OIDC authentication // OidcAudience specifies the audience of the token in OIDC authentication
//if AuthenticationMethod == "oidc". By default, this value is "". // if AuthenticationMethod == "oidc". By default, this value is "".
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
// OidcScope specifies the scope of the token in OIDC authentication
// if AuthenticationMethod == "oidc". By default, this value is "".
OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
// It will be used to get an OIDC token if AuthenticationMethod == "oidc". // It will be used to get an OIDC token if AuthenticationMethod == "oidc".
// By default, this value is "". // By default, this value is "".
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
// OidcAdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator
// The field will be set by prefix "oidc_additional_"
OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
} }
func getDefaultOidcClientConf() OidcClientConfig { func getDefaultOidcClientConf() OidcClientConfig {
return OidcClientConfig{ return OidcClientConfig{
OidcClientID: "", OidcClientID: "",
OidcClientSecret: "", OidcClientSecret: "",
OidcAudience: "", OidcAudience: "",
OidcTokenEndpointURL: "", OidcScope: "",
OidcTokenEndpointURL: "",
OidcAdditionalEndpointParams: make(map[string]string),
} }
} }
@@ -88,11 +98,21 @@ type OidcAuthProvider struct {
} }
func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider { func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider {
eps := make(map[string][]string)
for k, v := range cfg.OidcAdditionalEndpointParams {
eps[k] = []string{v}
}
if cfg.OidcAudience != "" {
eps["audience"] = []string{cfg.OidcAudience}
}
tokenGenerator := &clientcredentials.Config{ tokenGenerator := &clientcredentials.Config{
ClientID: cfg.OidcClientID, ClientID: cfg.OidcClientID,
ClientSecret: cfg.OidcClientSecret, ClientSecret: cfg.OidcClientSecret,
Scopes: []string{cfg.OidcAudience}, Scopes: []string{cfg.OidcScope},
TokenURL: cfg.OidcTokenEndpointURL, TokenURL: cfg.OidcTokenEndpointURL,
EndpointParams: eps,
} }
return &OidcAuthProvider{ return &OidcAuthProvider{

View File

@@ -73,30 +73,30 @@ func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkC
return nil return nil
} }
func (auth *TokenAuthSetterVerifier) VerifyLogin(loginMsg *msg.Login) error { func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
if util.GetAuthKey(auth.token, loginMsg.Timestamp) != loginMsg.PrivilegeKey { if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in login doesn't match token from configuration") return fmt.Errorf("token in login doesn't match token from configuration")
} }
return nil return nil
} }
func (auth *TokenAuthSetterVerifier) VerifyPing(pingMsg *msg.Ping) error { func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
if !auth.AuthenticateHeartBeats { if !auth.AuthenticateHeartBeats {
return nil return nil
} }
if util.GetAuthKey(auth.token, pingMsg.Timestamp) != pingMsg.PrivilegeKey { if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in heartbeat doesn't match token from configuration") return fmt.Errorf("token in heartbeat doesn't match token from configuration")
} }
return nil return nil
} }
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
if !auth.AuthenticateNewWorkConns { if !auth.AuthenticateNewWorkConns {
return nil return nil
} }
if util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp) != newWorkConnMsg.PrivilegeKey { if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {
return fmt.Errorf("token in NewWorkConn doesn't match token from configuration") return fmt.Errorf("token in NewWorkConn doesn't match token from configuration")
} }
return nil return nil

View File

@@ -20,10 +20,11 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/samber/lo"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
"gopkg.in/ini.v1"
) )
// ClientCommonConf contains information for a client service. It is // ClientCommonConf contains information for a client service. It is
@@ -38,6 +39,13 @@ type ClientCommonConf struct {
// ServerPort specifies the port to connect to the server on. By default, // ServerPort specifies the port to connect to the server on. By default,
// this value is 7000. // this value is 7000.
ServerPort int `ini:"server_port" json:"server_port"` ServerPort int `ini:"server_port" json:"server_port"`
// STUN server to help penetrate NAT hole.
NatHoleSTUNServer string `ini:"nat_hole_stun_server" json:"nat_hole_stun_server"`
// The maximum amount of time a dial to server will wait for a connect to complete.
DialServerTimeout int64 `ini:"dial_server_timeout" json:"dial_server_timeout"`
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
// If negative, keep-alive probes are disabled.
DialServerKeepAlive int64 `ini:"dial_server_keepalive" json:"dial_server_keepalive"`
// ConnectServerLocalIP specifies the address of the client bind when it connect to server. // ConnectServerLocalIP specifies the address of the client bind when it connect to server.
// By default, this value is empty. // By default, this value is empty.
// this value only use in TCP/Websocket protocol. Not support in KCP protocol. // this value only use in TCP/Websocket protocol. Not support in KCP protocol.
@@ -108,14 +116,19 @@ type ClientCommonConf struct {
// all supplied proxies are enabled. By default, this value is an empty // all supplied proxies are enabled. By default, this value is an empty
// set. // set.
Start []string `ini:"start" json:"start"` Start []string `ini:"start" json:"start"`
//Start map[string]struct{} `json:"start"` // Start map[string]struct{} `json:"start"`
// Protocol specifies the protocol to use when interacting with the server. // Protocol specifies the protocol to use when interacting with the server.
// Valid values are "tcp", "kcp" and "websocket". By default, this value // Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
// is "tcp". // is "tcp".
Protocol string `ini:"protocol" json:"protocol"` Protocol string `ini:"protocol" json:"protocol"`
// QUIC protocol options
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
// TLSEnable specifies whether or not TLS should be used when communicating // TLSEnable specifies whether or not TLS should be used when communicating
// with the server. If "tls_cert_file" and "tls_key_file" are valid, // with the server. If "tls_cert_file" and "tls_key_file" are valid,
// client will load the supplied tls configuration. // client will load the supplied tls configuration.
// Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
TLSEnable bool `ini:"tls_enable" json:"tls_enable"` TLSEnable bool `ini:"tls_enable" json:"tls_enable"`
// TLSCertPath specifies the path of the cert file that client will // TLSCertPath specifies the path of the cert file that client will
// load. It only works when "tls_enable" is true and "tls_key_file" is valid. // load. It only works when "tls_enable" is true and "tls_key_file" is valid.
@@ -128,11 +141,12 @@ type ClientCommonConf struct {
// It only works when "tls_enable" is valid and tls configuration of server // It only works when "tls_enable" is valid and tls configuration of server
// has been specified. // has been specified.
TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"` TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"`
// TLSServerName specifices the custom server name of tls certificate. By // TLSServerName specifies the custom server name of tls certificate. By
// default, server name if same to ServerAddr. // default, server name if same to ServerAddr.
TLSServerName string `ini:"tls_server_name" json:"tls_server_name"` TLSServerName string `ini:"tls_server_name" json:"tls_server_name"`
// By default, frpc will connect frps with first custom byte if tls is enabled. // If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
// If DisableCustomTLSFirstByte is true, frpc will not send that custom byte. // first custom byte when tls is enabled.
// Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"` DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
// HeartBeatInterval specifies at what interval heartbeats are sent to the // HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By // server, in seconds. It is not recommended to change this value. By
@@ -149,42 +163,42 @@ type ClientCommonConf struct {
UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
// Include other config files for proxies. // Include other config files for proxies.
IncludeConfigFiles []string `ini:"includes" json:"includes"` IncludeConfigFiles []string `ini:"includes" json:"includes"`
// Enable golang pprof handlers in admin listener.
// Admin port must be set first.
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
} }
// GetDefaultClientConf returns a client configuration with default values. // GetDefaultClientConf returns a client configuration with default values.
func GetDefaultClientConf() ClientCommonConf { func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{ return ClientCommonConf{
ClientConfig: auth.GetDefaultClientConf(), ClientConfig: auth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0", ServerAddr: "0.0.0.0",
ServerPort: 7000, ServerPort: 7000,
HTTPProxy: os.Getenv("http_proxy"), NatHoleSTUNServer: "stun.easyvoip.com:3478",
LogFile: "console", DialServerTimeout: 10,
LogWay: "console", DialServerKeepAlive: 7200,
LogLevel: "info", HTTPProxy: os.Getenv("http_proxy"),
LogMaxDays: 3, LogFile: "console",
DisableLogColor: false, LogWay: "console",
AdminAddr: "127.0.0.1", LogLevel: "info",
AdminPort: 0, LogMaxDays: 3,
AdminUser: "", AdminAddr: "127.0.0.1",
AdminPwd: "", PoolCount: 1,
AssetsDir: "", TCPMux: true,
PoolCount: 1, TCPMuxKeepaliveInterval: 60,
TCPMux: true, LoginFailExit: true,
TCPMuxKeepaliveInterval: 60, Start: make([]string, 0),
User: "", Protocol: "tcp",
DNSServer: "", QUICKeepalivePeriod: 10,
LoginFailExit: true, QUICMaxIdleTimeout: 30,
Start: make([]string, 0), QUICMaxIncomingStreams: 100000,
Protocol: "tcp", TLSEnable: true,
TLSEnable: false, DisableCustomTLSFirstByte: true,
TLSCertFile: "", HeartbeatInterval: 30,
TLSKeyFile: "", HeartbeatTimeout: 90,
TLSTrustedCaFile: "", Metas: make(map[string]string),
HeartbeatInterval: 30, UDPPacketSize: 1500,
HeartbeatTimeout: 90, IncludeConfigFiles: make([]string, 0),
Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
} }
} }
@@ -203,7 +217,7 @@ func (cfg *ClientCommonConf) Validate() error {
} }
} }
if cfg.TLSEnable == false { if !cfg.TLSEnable {
if cfg.TLSCertFile != "" { if cfg.TLSCertFile != "" {
fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false") fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
} }
@@ -217,7 +231,7 @@ func (cfg *ClientCommonConf) Validate() error {
} }
} }
if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" { if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
return fmt.Errorf("invalid protocol") return fmt.Errorf("invalid protocol")
} }
@@ -258,6 +272,8 @@ 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_")
return common, nil return common, nil
} }
@@ -268,7 +284,6 @@ func LoadAllProxyConfsFromIni(
source interface{}, source interface{},
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{
Insensitive: false, Insensitive: false,
InsensitiveSections: false, InsensitiveSections: false,
@@ -342,7 +357,7 @@ func LoadAllProxyConfsFromIni(
case "visitor": case "visitor":
newConf, newErr := NewVisitorConfFromIni(prefix, name, section) newConf, newErr := NewVisitorConfFromIni(prefix, name, section)
if newErr != nil { if newErr != nil {
return nil, nil, newErr return nil, nil, fmt.Errorf("failed to parse visitor %s, err: %v", name, newErr)
} }
visitorConfs[prefix+name] = newConf visitorConfs[prefix+name] = newConf
default: default:
@@ -353,7 +368,6 @@ func LoadAllProxyConfsFromIni(
} }
func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error { func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error {
// Validation // Validation
localPortStr := section.Key("local_port").String() localPortStr := section.Key("local_port").String()
remotePortStr := section.Key("remote_port").String() remotePortStr := section.Key("remote_port").String()
@@ -391,8 +405,12 @@ func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error {
} }
copySection(section, tmpsection) copySection(section, tmpsection)
tmpsection.NewKey("local_port", fmt.Sprintf("%d", localPorts[i])) if _, err := tmpsection.NewKey("local_port", fmt.Sprintf("%d", localPorts[i])); err != nil {
tmpsection.NewKey("remote_port", fmt.Sprintf("%d", remotePorts[i])) return fmt.Errorf("local_port new key in section error: %v", err)
}
if _, err := tmpsection.NewKey("remote_port", fmt.Sprintf("%d", remotePorts[i])); err != nil {
return fmt.Errorf("remote_port new key in section error: %v", err)
}
} }
return nil return nil
@@ -400,6 +418,6 @@ func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error {
func copySection(source, target *ini.Section) { func copySection(source, target *ini.Section) {
for key, value := range source.KeysHash() { for key, value := range source.KeysHash() {
target.NewKey(key, value) _, _ = target.NewKey(key, value)
} }
} }

View File

@@ -17,18 +17,17 @@ package config
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/consts"
"github.com/stretchr/testify/assert"
) )
const ( const (
testUser = "test" testUser = "test"
) )
var ( var testClientBytesWithFull = []byte(`
testClientBytesWithFull = []byte(`
# [common] is integral section # [common] is integral section
[common] [common]
server_addr = 0.0.0.9 server_addr = 0.0.0.9
@@ -75,6 +74,7 @@ var (
local_ip = 127.0.0.9 local_ip = 127.0.0.9
local_port = 29 local_port = 29
bandwidth_limit = 19MB bandwidth_limit = 19MB
bandwidth_limit_mode = server
use_encryption use_encryption
use_compression use_compression
remote_port = 6009 remote_port = 6009
@@ -237,7 +237,6 @@ var (
use_encryption = false use_encryption = false
use_compression = false use_compression = false
`) `)
)
func Test_LoadClientCommonConf(t *testing.T) { func Test_LoadClientCommonConf(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
@@ -259,34 +258,41 @@ func Test_LoadClientCommonConf(t *testing.T) {
OidcTokenEndpointURL: "endpoint_url", OidcTokenEndpointURL: "endpoint_url",
}, },
}, },
ServerAddr: "0.0.0.9", ServerAddr: "0.0.0.9",
ServerPort: 7009, ServerPort: 7009,
HTTPProxy: "http://user:passwd@192.168.1.128:8080", NatHoleSTUNServer: "stun.easyvoip.com:3478",
LogFile: "./frpc.log9", DialServerTimeout: 10,
LogWay: "file", DialServerKeepAlive: 7200,
LogLevel: "info9", HTTPProxy: "http://user:passwd@192.168.1.128:8080",
LogMaxDays: 39, LogFile: "./frpc.log9",
DisableLogColor: false, LogWay: "file",
AdminAddr: "127.0.0.9", LogLevel: "info9",
AdminPort: 7409, LogMaxDays: 39,
AdminUser: "admin9", DisableLogColor: false,
AdminPwd: "admin9", AdminAddr: "127.0.0.9",
AssetsDir: "./static9", AdminPort: 7409,
PoolCount: 59, AdminUser: "admin9",
TCPMux: true, AdminPwd: "admin9",
TCPMuxKeepaliveInterval: 60, AssetsDir: "./static9",
User: "your_name", PoolCount: 59,
LoginFailExit: true, TCPMux: true,
Protocol: "tcp", TCPMuxKeepaliveInterval: 60,
TLSEnable: true, User: "your_name",
TLSCertFile: "client.crt", LoginFailExit: true,
TLSKeyFile: "client.key", Protocol: "tcp",
TLSTrustedCaFile: "ca.crt", QUICKeepalivePeriod: 10,
TLSServerName: "example.com", QUICMaxIdleTimeout: 30,
DNSServer: "8.8.8.9", QUICMaxIncomingStreams: 100000,
Start: []string{"ssh", "dns"}, TLSEnable: true,
HeartbeatInterval: 39, TLSCertFile: "client.crt",
HeartbeatTimeout: 99, TLSKeyFile: "client.key",
TLSTrustedCaFile: "ca.crt",
TLSServerName: "example.com",
DisableCustomTLSFirstByte: true,
DNSServer: "8.8.8.9",
Start: []string{"ssh", "dns"},
HeartbeatInterval: 39,
HeartbeatTimeout: 99,
Metas: map[string]string{ Metas: map[string]string{
"var1": "123", "var1": "123",
"var2": "234", "var2": "234",
@@ -306,13 +312,14 @@ func Test_LoadClientBasicConf(t *testing.T) {
proxyExpected := map[string]ProxyConf{ proxyExpected := map[string]ProxyConf{
testUser + ".ssh": &TCPProxyConf{ testUser + ".ssh": &TCPProxyConf{
BaseProxyConf: BaseProxyConf{ BaseProxyConf: BaseProxyConf{
ProxyName: testUser + ".ssh", ProxyName: testUser + ".ssh",
ProxyType: consts.TCPProxy, ProxyType: consts.TCPProxy,
UseCompression: true, UseCompression: true,
UseEncryption: true, UseEncryption: true,
Group: "test_group", Group: "test_group",
GroupKey: "123456", GroupKey: "123456",
BandwidthLimit: MustBandwidthQuantity("19MB"), BandwidthLimit: MustBandwidthQuantity("19MB"),
BandwidthLimitMode: BandwidthLimitModeServer,
Metas: map[string]string{ Metas: map[string]string{
"var1": "123", "var1": "123",
"var2": "234", "var2": "234",
@@ -339,6 +346,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 29, LocalPort: 29,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 9, RemotePort: 9,
}, },
@@ -350,6 +358,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6010, LocalPort: 6010,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6010, RemotePort: 6010,
}, },
@@ -361,6 +370,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6011, LocalPort: 6011,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6011, RemotePort: 6011,
}, },
@@ -372,6 +382,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6019, LocalPort: 6019,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6019, RemotePort: 6019,
}, },
@@ -385,6 +396,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 59, LocalPort: 59,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6009, RemotePort: 6009,
}, },
@@ -398,6 +410,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6000, LocalPort: 6000,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6000, RemotePort: 6000,
}, },
@@ -411,6 +424,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6010, LocalPort: 6010,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6010, RemotePort: 6010,
}, },
@@ -424,6 +438,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6011, LocalPort: 6011,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6011, RemotePort: 6011,
}, },
@@ -444,6 +459,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
HealthCheckIntervalS: 19, HealthCheckIntervalS: 19,
HealthCheckURL: "http://127.0.0.9:89/status", HealthCheckURL: "http://127.0.0.9:89/status",
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"}, CustomDomains: []string{"web02.yourdomain.com"},
@@ -468,6 +484,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalPort: 8009, LocalPort: 8009,
}, },
ProxyProtocolVersion: "v2", ProxyProtocolVersion: "v2",
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"}, CustomDomains: []string{"web02.yourdomain.com"},
@@ -482,9 +499,12 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 22, LocalPort: 22,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
},
RoleServerCommonConf: RoleServerCommonConf{
Role: "server",
Sk: "abcdefg",
}, },
Role: "server",
Sk: "abcdefg",
}, },
testUser + ".p2p_tcp": &XTCPProxyConf{ testUser + ".p2p_tcp": &XTCPProxyConf{
BaseProxyConf: BaseProxyConf{ BaseProxyConf: BaseProxyConf{
@@ -494,9 +514,12 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 22, LocalPort: 22,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
},
RoleServerCommonConf: RoleServerCommonConf{
Role: "server",
Sk: "abcdefg",
}, },
Role: "server",
Sk: "abcdefg",
}, },
testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{ testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{
BaseProxyConf: BaseProxyConf{ BaseProxyConf: BaseProxyConf{
@@ -506,6 +529,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 10701, LocalPort: 10701,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"tunnel1"}, CustomDomains: []string{"tunnel1"},
@@ -524,6 +548,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_unix_path": "/var/run/docker.sock", "plugin_unix_path": "/var/run/docker.sock",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6003, RemotePort: 6003,
}, },
@@ -539,6 +564,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_http_passwd": "abc", "plugin_http_passwd": "abc",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6004, RemotePort: 6004,
}, },
@@ -554,6 +580,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_passwd": "abc", "plugin_passwd": "abc",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6005, RemotePort: 6005,
}, },
@@ -571,6 +598,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_http_passwd": "abc", "plugin_http_passwd": "abc",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6006, RemotePort: 6006,
}, },
@@ -589,6 +617,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_header_X-From-Where": "frp", "plugin_header_X-From-Where": "frp",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"test.yourdomain.com"}, CustomDomains: []string{"test.yourdomain.com"},
@@ -607,6 +636,7 @@ func Test_LoadClientBasicConf(t *testing.T) {
"plugin_header_X-From-Where": "frp", "plugin_header_X-From-Where": "frp",
}, },
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"test.yourdomain.com"}, CustomDomains: []string{"test.yourdomain.com"},
@@ -636,6 +666,10 @@ func Test_LoadClientBasicConf(t *testing.T) {
BindAddr: "127.0.0.1", BindAddr: "127.0.0.1",
BindPort: 9001, BindPort: 9001,
}, },
Protocol: "quic",
MaxRetriesAnHour: 8,
MinRetryInterval: 90,
FallbackTimeoutMs: 1000,
}, },
} }

View File

@@ -42,7 +42,7 @@ func ParseClientConfig(filePath string) (
} }
cfg.Complete() cfg.Complete()
if err = cfg.Validate(); err != nil { if err = cfg.Validate(); err != nil {
err = fmt.Errorf("Parse config error: %v", err) err = fmt.Errorf("parse config error: %v", err)
return return
} }

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,10 @@ package config
import ( import (
"testing" "testing"
"github.com/fatedier/frp/pkg/consts"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/consts"
) )
var ( var (
@@ -49,7 +49,6 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
source []byte source []byte
expected ProxyConf expected ProxyConf
}{ }{
{ {
sname: "ssh", sname: "ssh",
source: []byte(` source: []byte(`
@@ -59,6 +58,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
local_ip = 127.0.0.9 local_ip = 127.0.0.9
local_port = 29 local_port = 29
bandwidth_limit = 19MB bandwidth_limit = 19MB
bandwidth_limit_mode = server
use_encryption use_encryption
use_compression use_compression
remote_port = 6009 remote_port = 6009
@@ -72,13 +72,14 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
meta_var2 = 234`), meta_var2 = 234`),
expected: &TCPProxyConf{ expected: &TCPProxyConf{
BaseProxyConf: BaseProxyConf{ BaseProxyConf: BaseProxyConf{
ProxyName: testProxyPrefix + "ssh", ProxyName: testProxyPrefix + "ssh",
ProxyType: consts.TCPProxy, ProxyType: consts.TCPProxy,
UseCompression: true, UseCompression: true,
UseEncryption: true, UseEncryption: true,
Group: "test_group", Group: "test_group",
GroupKey: "123456", GroupKey: "123456",
BandwidthLimit: MustBandwidthQuantity("19MB"), BandwidthLimit: MustBandwidthQuantity("19MB"),
BandwidthLimitMode: BandwidthLimitModeServer,
Metas: map[string]string{ Metas: map[string]string{
"var1": "123", "var1": "123",
"var2": "234", "var2": "234",
@@ -115,6 +116,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 29, LocalPort: 29,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 9, RemotePort: 9,
}, },
@@ -140,6 +142,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 59, LocalPort: 59,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6009, RemotePort: 6009,
}, },
@@ -183,6 +186,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
HealthCheckIntervalS: 19, HealthCheckIntervalS: 19,
HealthCheckURL: "http://127.0.0.9:89/status", HealthCheckURL: "http://127.0.0.9:89/status",
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"}, CustomDomains: []string{"web02.yourdomain.com"},
@@ -221,6 +225,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalPort: 8009, LocalPort: 8009,
}, },
ProxyProtocolVersion: "v2", ProxyProtocolVersion: "v2",
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"web02.yourdomain.com"}, CustomDomains: []string{"web02.yourdomain.com"},
@@ -247,9 +252,12 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 22, LocalPort: 22,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
},
RoleServerCommonConf: RoleServerCommonConf{
Role: "server",
Sk: "abcdefg",
}, },
Role: "server",
Sk: "abcdefg",
}, },
}, },
{ {
@@ -271,9 +279,12 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 22, LocalPort: 22,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
},
RoleServerCommonConf: RoleServerCommonConf{
Role: "server",
Sk: "abcdefg",
}, },
Role: "server",
Sk: "abcdefg",
}, },
}, },
{ {
@@ -294,6 +305,7 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.1", LocalIP: "127.0.0.1",
LocalPort: 10701, LocalPort: 10701,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
DomainConf: DomainConf{ DomainConf: DomainConf{
CustomDomains: []string{"tunnel1"}, CustomDomains: []string{"tunnel1"},
@@ -348,6 +360,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6010, LocalPort: 6010,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6010, RemotePort: 6010,
}, },
@@ -359,6 +372,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6011, LocalPort: 6011,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6011, RemotePort: 6011,
}, },
@@ -370,6 +384,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "127.0.0.9", LocalIP: "127.0.0.9",
LocalPort: 6019, LocalPort: 6019,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6019, RemotePort: 6019,
}, },
@@ -397,6 +412,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6000, LocalPort: 6000,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6000, RemotePort: 6000,
}, },
@@ -410,6 +426,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6010, LocalPort: 6010,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6010, RemotePort: 6010,
}, },
@@ -423,6 +440,7 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
LocalIP: "114.114.114.114", LocalIP: "114.114.114.114",
LocalPort: 6011, LocalPort: 6011,
}, },
BandwidthLimitMode: BandwidthLimitModeClient,
}, },
RemotePort: 6011, RemotePort: 6011,
}, },
@@ -457,5 +475,4 @@ func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
assert.Equal(c.expected, actual) assert.Equal(c.expected, actual)
} }
} }

View File

@@ -18,12 +18,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/go-playground/validator/v10"
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
plugin "github.com/fatedier/frp/pkg/plugin/server" plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
"github.com/go-playground/validator/v10"
"gopkg.in/ini.v1"
) )
// ServerCommonConf contains information for a server service. It is // ServerCommonConf contains information for a server service. It is
@@ -38,14 +38,18 @@ type ServerCommonConf struct {
// BindPort specifies the port that the server listens on. By default, this // BindPort specifies the port that the server listens on. By default, this
// value is 7000. // value is 7000.
BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"` BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"`
// BindUDPPort specifies the UDP port that the server listens on. If this
// value is 0, the server will not listen for UDP connections. By default,
// this value is 0
BindUDPPort int `ini:"bind_udp_port" json:"bind_udp_port" validate:"gte=0,lte=65535"`
// KCPBindPort specifies the KCP port that the server listens on. If this // KCPBindPort specifies the KCP port that the server listens on. If this
// value is 0, the server will not listen for KCP connections. By default, // value is 0, the server will not listen for KCP connections. By default,
// this value is 0. // this value is 0.
KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"` KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"`
// QUICBindPort specifies the QUIC port that the server listens on.
// Set this value to 0 will disable this feature.
// By default, the value is 0.
QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port" validate:"gte=0,lte=65535"`
// QUIC protocol options
QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
// ProxyBindAddr specifies the address that the proxy binds to. This value // ProxyBindAddr specifies the address that the proxy binds to. This value
// may be the same as BindAddr. // may be the same as BindAddr.
ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"` ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"`
@@ -62,6 +66,8 @@ type ServerCommonConf struct {
// requests on one single port. If it's not - it will listen on this value for // requests on one single port. If it's not - it will listen on this value for
// HTTP CONNECT requests. By default, this value is 0. // HTTP CONNECT requests. By default, this value is 0.
TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"` TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"`
// If TCPMuxPassthrough is true, frps won't do any update on traffic.
TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"`
// VhostHTTPTimeout specifies the response header timeout for the Vhost // VhostHTTPTimeout specifies the response header timeout for the Vhost
// HTTP server, in seconds. By default, this value is 60. // HTTP server, in seconds. By default, this value is 60.
VhostHTTPTimeout int64 `ini:"vhost_http_timeout" json:"vhost_http_timeout"` VhostHTTPTimeout int64 `ini:"vhost_http_timeout" json:"vhost_http_timeout"`
@@ -72,6 +78,17 @@ type ServerCommonConf struct {
// value is 0, the dashboard will not be started. By default, this value is // value is 0, the dashboard will not be started. By default, this value is
// 0. // 0.
DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"` DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"`
// DashboardTLSCertFile specifies the path of the cert file that the server will
// load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this
// supplied tls configuration.
DashboardTLSCertFile string `ini:"dashboard_tls_cert_file" json:"dashboard_tls_cert_file"`
// DashboardTLSKeyFile specifies the path of the secret key that the server will
// load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this
// supplied tls configuration.
DashboardTLSKeyFile string `ini:"dashboard_tls_key_file" json:"dashboard_tls_key_file"`
// DashboardTLSMode specifies the mode of the dashboard between HTTP or HTTPS modes. By
// default, this value is false, which is HTTP mode.
DashboardTLSMode bool `ini:"dashboard_tls_mode" json:"dashboard_tls_mode"`
// DashboardUser specifies the username that the dashboard will use for // DashboardUser specifies the username that the dashboard will use for
// login. // login.
DashboardUser string `ini:"dashboard_user" json:"dashboard_user"` DashboardUser string `ini:"dashboard_user" json:"dashboard_user"`
@@ -121,6 +138,9 @@ type ServerCommonConf struct {
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. // 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.
// If negative, keep-alive probes are disabled.
TCPKeepAlive int64 `ini:"tcp_keepalive" json:"tcp_keepalive"`
// Custom404Page specifies a path to a custom 404 page to display. If this // Custom404Page specifies a path to a custom 404 page to display. If this
// value is "", a default page will be displayed. By default, this value is // value is "", a default page will be displayed. By default, this value is
// "". // "".
@@ -130,6 +150,8 @@ type ServerCommonConf struct {
// If the length of this value is 0, all ports are allowed. By default, // If the length of this value is 0, all ports are allowed. By default,
// this value is an empty set. // this value is an empty set.
AllowPorts map[int]struct{} `ini:"-" json:"-"` AllowPorts map[int]struct{} `ini:"-" json:"-"`
// Original string.
AllowPortsStr string `ini:"-" json:"-"`
// MaxPoolCount specifies the maximum pool size for each proxy. By default, // MaxPoolCount specifies the maximum pool size for each proxy. By default,
// this value is 5. // this value is 5.
MaxPoolCount int64 `ini:"max_pool_count" json:"max_pool_count"` MaxPoolCount int64 `ini:"max_pool_count" json:"max_pool_count"`
@@ -167,54 +189,45 @@ type ServerCommonConf struct {
// UDPPacketSize specifies the UDP packet size // UDPPacketSize specifies the UDP packet size
// By default, this value is 1500 // By default, this value is 1500
UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
// Enable golang pprof handlers in dashboard listener.
// Dashboard port must be set first.
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
// NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data.
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
} }
// GetDefaultServerConf returns a server configuration with reasonable // GetDefaultServerConf returns a server configuration with reasonable
// defaults. // defaults.
func GetDefaultServerConf() ServerCommonConf { func GetDefaultServerConf() ServerCommonConf {
return ServerCommonConf{ return ServerCommonConf{
ServerConfig: auth.GetDefaultServerConf(), ServerConfig: auth.GetDefaultServerConf(),
BindAddr: "0.0.0.0", BindAddr: "0.0.0.0",
BindPort: 7000, BindPort: 7000,
BindUDPPort: 0, QUICKeepalivePeriod: 10,
KCPBindPort: 0, QUICMaxIdleTimeout: 30,
ProxyBindAddr: "", QUICMaxIncomingStreams: 100000,
VhostHTTPPort: 0, VhostHTTPTimeout: 60,
VhostHTTPSPort: 0, DashboardAddr: "0.0.0.0",
TCPMuxHTTPConnectPort: 0, LogFile: "console",
VhostHTTPTimeout: 60, LogWay: "console",
DashboardAddr: "0.0.0.0", LogLevel: "info",
DashboardPort: 0, LogMaxDays: 3,
DashboardUser: "", DetailedErrorsToClient: true,
DashboardPwd: "", TCPMux: true,
EnablePrometheus: false, TCPMuxKeepaliveInterval: 60,
AssetsDir: "", TCPKeepAlive: 7200,
LogFile: "console", AllowPorts: make(map[int]struct{}),
LogWay: "console", MaxPoolCount: 5,
LogLevel: "info", MaxPortsPerClient: 0,
LogMaxDays: 3, HeartbeatTimeout: 90,
DisableLogColor: false, UserConnTimeout: 10,
DetailedErrorsToClient: true, HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
SubDomainHost: "", UDPPacketSize: 1500,
TCPMux: true, NatHoleAnalysisDataReserveHours: 7 * 24,
TCPMuxKeepaliveInterval: 60,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
MaxPortsPerClient: 0,
TLSOnly: false,
TLSCertFile: "",
TLSKeyFile: "",
TLSTrustedCaFile: "",
HeartbeatTimeout: 90,
UserConnTimeout: 10,
Custom404Page: "",
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
} }
} }
func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
f, err := ini.LoadSources(ini.LoadOptions{ f, err := ini.LoadSources(ini.LoadOptions{
Insensitive: false, Insensitive: false,
InsensitiveSections: false, InsensitiveSections: false,
@@ -247,6 +260,7 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
for _, port := range allowPorts { for _, port := range allowPorts {
common.AllowPorts[int(port)] = struct{}{} common.AllowPorts[int(port)] = struct{}{}
} }
common.AllowPortsStr = allowPortStr
} }
// plugin.xxx // plugin.xxx
@@ -286,6 +300,23 @@ func (cfg *ServerCommonConf) Complete() {
} }
func (cfg *ServerCommonConf) Validate() error { func (cfg *ServerCommonConf) Validate() error {
if !cfg.DashboardTLSMode {
if cfg.DashboardTLSCertFile != "" {
fmt.Println("WARNING! dashboard_tls_cert_file is invalid when dashboard_tls_mode is false")
}
if cfg.DashboardTLSKeyFile != "" {
fmt.Println("WARNING! dashboard_tls_key_file is invalid when dashboard_tls_mode is false")
}
} else {
if cfg.DashboardTLSCertFile == "" {
return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
}
if cfg.DashboardTLSKeyFile == "" {
return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
}
}
return validator.New().Struct(cfg) return validator.New().Struct(cfg)
} }

View File

@@ -17,10 +17,10 @@ package config
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
plugin "github.com/fatedier/frp/pkg/plugin/server" plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/stretchr/testify/assert"
) )
func Test_LoadServerCommonConf(t *testing.T) { func Test_LoadServerCommonConf(t *testing.T) {
@@ -36,7 +36,6 @@ func Test_LoadServerCommonConf(t *testing.T) {
[common] [common]
bind_addr = 0.0.0.9 bind_addr = 0.0.0.9
bind_port = 7009 bind_port = 7009
bind_udp_port = 7008
kcp_bind_port = 7007 kcp_bind_port = 7007
proxy_bind_addr = 127.0.0.9 proxy_bind_addr = 127.0.0.9
vhost_http_port = 89 vhost_http_port = 89
@@ -104,8 +103,10 @@ func Test_LoadServerCommonConf(t *testing.T) {
}, },
BindAddr: "0.0.0.9", BindAddr: "0.0.0.9",
BindPort: 7009, BindPort: 7009,
BindUDPPort: 7008,
KCPBindPort: 7007, KCPBindPort: 7007,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
ProxyBindAddr: "127.0.0.9", ProxyBindAddr: "127.0.0.9",
VhostHTTPPort: 89, VhostHTTPPort: 89,
VhostHTTPSPort: 449, VhostHTTPSPort: 449,
@@ -126,21 +127,24 @@ func Test_LoadServerCommonConf(t *testing.T) {
HeartbeatTimeout: 99, HeartbeatTimeout: 99,
UserConnTimeout: 9, UserConnTimeout: 9,
AllowPorts: map[int]struct{}{ AllowPorts: map[int]struct{}{
10: struct{}{}, 10: {},
11: struct{}{}, 11: {},
12: struct{}{}, 12: {},
99: struct{}{}, 99: {},
}, },
MaxPoolCount: 59, AllowPortsStr: "10-12,99",
MaxPortsPerClient: 9, MaxPoolCount: 59,
TLSOnly: true, MaxPortsPerClient: 9,
TLSCertFile: "server.crt", TLSOnly: true,
TLSKeyFile: "server.key", TLSCertFile: "server.crt",
TLSTrustedCaFile: "ca.crt", TLSKeyFile: "server.key",
SubDomainHost: "frps.com", TLSTrustedCaFile: "ca.crt",
TCPMux: true, SubDomainHost: "frps.com",
TCPMuxKeepaliveInterval: 60, TCPMux: true,
UDPPacketSize: 1509, TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
UDPPacketSize: 1509,
NatHoleAnalysisDataReserveHours: 7 * 24,
HTTPPlugins: map[string]plugin.HTTPPluginOptions{ HTTPPlugins: map[string]plugin.HTTPPluginOptions{
"user-manager": { "user-manager": {
@@ -165,7 +169,6 @@ func Test_LoadServerCommonConf(t *testing.T) {
[common] [common]
bind_addr = 0.0.0.9 bind_addr = 0.0.0.9
bind_port = 7009 bind_port = 7009
bind_udp_port = 7008
`), `),
expected: ServerCommonConf{ expected: ServerCommonConf{
ServerConfig: auth.ServerConfig{ ServerConfig: auth.ServerConfig{
@@ -175,28 +178,32 @@ func Test_LoadServerCommonConf(t *testing.T) {
AuthenticateNewWorkConns: false, AuthenticateNewWorkConns: false,
}, },
}, },
BindAddr: "0.0.0.9", BindAddr: "0.0.0.9",
BindPort: 7009, BindPort: 7009,
BindUDPPort: 7008, QUICKeepalivePeriod: 10,
ProxyBindAddr: "0.0.0.9", QUICMaxIdleTimeout: 30,
VhostHTTPTimeout: 60, QUICMaxIncomingStreams: 100000,
DashboardAddr: "0.0.0.0", ProxyBindAddr: "0.0.0.9",
DashboardUser: "", VhostHTTPTimeout: 60,
DashboardPwd: "", DashboardAddr: "0.0.0.0",
EnablePrometheus: false, DashboardUser: "",
LogFile: "console", DashboardPwd: "",
LogWay: "console", EnablePrometheus: false,
LogLevel: "info", LogFile: "console",
LogMaxDays: 3, LogWay: "console",
DetailedErrorsToClient: true, LogLevel: "info",
TCPMux: true, LogMaxDays: 3,
TCPMuxKeepaliveInterval: 60, DetailedErrorsToClient: true,
AllowPorts: make(map[int]struct{}), TCPMux: true,
MaxPoolCount: 5, TCPMuxKeepaliveInterval: 60,
HeartbeatTimeout: 90, TCPKeepAlive: 7200,
UserConnTimeout: 10, AllowPorts: make(map[int]struct{}),
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), MaxPoolCount: 5,
UDPPacketSize: 1500, HeartbeatTimeout: 90,
UserConnTimeout: 10,
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
NatHoleAnalysisDataReserveHours: 7 * 24,
}, },
}, },
} }

View File

@@ -24,6 +24,9 @@ import (
const ( const (
MB = 1024 * 1024 MB = 1024 * 1024
KB = 1024 KB = 1024
BandwidthLimitModeClient = "client"
BandwidthLimitModeServer = "server"
) )
type BandwidthQuantity struct { type BandwidthQuantity struct {
@@ -75,21 +78,22 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error {
f float64 f float64
err error err error
) )
if strings.HasSuffix(s, "MB") { switch {
case strings.HasSuffix(s, "MB"):
base = MB base = MB
fstr := strings.TrimSuffix(s, "MB") fstr := strings.TrimSuffix(s, "MB")
f, err = strconv.ParseFloat(fstr, 64) f, err = strconv.ParseFloat(fstr, 64)
if err != nil { if err != nil {
return err return err
} }
} else if strings.HasSuffix(s, "KB") { case strings.HasSuffix(s, "KB"):
base = KB base = KB
fstr := strings.TrimSuffix(s, "KB") fstr := strings.TrimSuffix(s, "KB")
f, err = strconv.ParseFloat(fstr, 64) f, err = strconv.ParseFloat(fstr, 64)
if err != nil { if err != nil {
return err return err
} }
} else { default:
return errors.New("unit not support") return errors.New("unit not support")
} }

View File

@@ -21,19 +21,17 @@ import (
"text/template" "text/template"
) )
var ( var glbEnvs map[string]string
glbEnvs map[string]string
)
func init() { func init() {
glbEnvs = make(map[string]string) glbEnvs = make(map[string]string)
envs := os.Environ() envs := os.Environ()
for _, env := range envs { for _, env := range envs {
kv := strings.Split(env, "=") pair := strings.SplitN(env, "=", 2)
if len(kv) != 2 { if len(pair) != 2 {
continue continue
} }
glbEnvs[kv[0]] = kv[1] glbEnvs[pair[0]] = pair[1]
} }
} }

View File

@@ -18,9 +18,10 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/fatedier/frp/pkg/consts" "github.com/samber/lo"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/consts"
) )
// Visitor // Visitor
@@ -33,10 +34,12 @@ var (
) )
type VisitorConf interface { type VisitorConf interface {
GetBaseInfo() *BaseVisitorConf // GetBaseConfig returns the base config of visitor.
Compare(cmp VisitorConf) bool GetBaseConfig() *BaseVisitorConf
// UnmarshalFromIni unmarshals config from ini.
UnmarshalFromIni(prefix string, name string, section *ini.Section) error UnmarshalFromIni(prefix string, name string, section *ini.Section) error
Check() error // Validate validates config.
Validate() error
} }
type BaseVisitorConf struct { type BaseVisitorConf struct {
@@ -46,9 +49,14 @@ type BaseVisitorConf struct {
UseCompression bool `ini:"use_compression" json:"use_compression"` UseCompression bool `ini:"use_compression" json:"use_compression"`
Role string `ini:"role" json:"role"` Role string `ini:"role" json:"role"`
Sk string `ini:"sk" json:"sk"` Sk string `ini:"sk" json:"sk"`
ServerName string `ini:"server_name" json:"server_name"` // if the server user is not set, it defaults to the current user
BindAddr string `ini:"bind_addr" json:"bind_addr"` ServerUser string `ini:"server_user" json:"server_user"`
BindPort int `ini:"bind_port" json:"bind_port"` ServerName string `ini:"server_name" json:"server_name"`
BindAddr string `ini:"bind_addr" json:"bind_addr"`
// BindPort is the port that visitor listens on.
// It can be less than 0, it means don't bind to the port and only receive connections redirected from
// other visitors. (This is not supported for SUDP now)
BindPort int `ini:"bind_port" json:"bind_port"`
} }
type SUDPVisitorConf struct { type SUDPVisitorConf struct {
@@ -61,6 +69,13 @@ type STCPVisitorConf struct {
type XTCPVisitorConf struct { type XTCPVisitorConf struct {
BaseVisitorConf `ini:",extends"` BaseVisitorConf `ini:",extends"`
Protocol string `ini:"protocol" json:"protocol,omitempty"`
KeepTunnelOpen bool `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"`
MaxRetriesAnHour int `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"`
MinRetryInterval int `ini:"min_retry_interval" json:"min_retry_interval,omitempty"`
FallbackTo string `ini:"fallback_to" json:"fallback_to,omitempty"`
FallbackTimeoutMs int `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"`
} }
// DefaultVisitorConf creates a empty VisitorConf object by visitorType. // DefaultVisitorConf creates a empty VisitorConf object by visitorType.
@@ -70,7 +85,6 @@ func DefaultVisitorConf(visitorType string) VisitorConf {
if !ok { if !ok {
return nil return nil
} }
return reflect.New(v).Interface().(VisitorConf) return reflect.New(v).Interface().(VisitorConf)
} }
@@ -80,19 +94,19 @@ func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (Vi
visitorType := section.Key("type").String() visitorType := section.Key("type").String()
if visitorType == "" { if visitorType == "" {
return nil, fmt.Errorf("visitor [%s] type shouldn't be empty", name) return nil, fmt.Errorf("type shouldn't be empty")
} }
conf := DefaultVisitorConf(visitorType) conf := DefaultVisitorConf(visitorType)
if conf == nil { if conf == nil {
return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) return nil, fmt.Errorf("type [%s] error", visitorType)
} }
if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) return nil, fmt.Errorf("type [%s] error", visitorType)
} }
if err := conf.Check(); err != nil { if err := conf.Validate(); err != nil {
return nil, err return nil, err
} }
@@ -100,26 +114,11 @@ func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (Vi
} }
// Base // Base
func (cfg *BaseVisitorConf) GetBaseInfo() *BaseVisitorConf { func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf {
return cfg return cfg
} }
func (cfg *BaseVisitorConf) compare(cmp *BaseVisitorConf) bool { func (cfg *BaseVisitorConf) validate() (err error) {
if cfg.ProxyName != cmp.ProxyName ||
cfg.ProxyType != cmp.ProxyType ||
cfg.UseEncryption != cmp.UseEncryption ||
cfg.UseCompression != cmp.UseCompression ||
cfg.Role != cmp.Role ||
cfg.Sk != cmp.Sk ||
cfg.ServerName != cmp.ServerName ||
cfg.BindAddr != cmp.BindAddr ||
cfg.BindPort != cmp.BindPort {
return false
}
return true
}
func (cfg *BaseVisitorConf) check() (err error) {
if cfg.Role != "visitor" { if cfg.Role != "visitor" {
err = fmt.Errorf("invalid role") err = fmt.Errorf("invalid role")
return return
@@ -128,7 +127,9 @@ func (cfg *BaseVisitorConf) check() (err error) {
err = fmt.Errorf("bind_addr shouldn't be empty") err = fmt.Errorf("bind_addr shouldn't be empty")
return return
} }
if cfg.BindPort <= 0 { // BindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
// other visitors
if cfg.BindPort == 0 {
err = fmt.Errorf("bind_port is required") err = fmt.Errorf("bind_port is required")
return return
} }
@@ -136,19 +137,23 @@ func (cfg *BaseVisitorConf) check() (err error) {
} }
func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error { func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error {
_ = section
// Custom decoration after basic unmarshal: // Custom decoration after basic unmarshal:
// proxy name // proxy name
cfg.ProxyName = prefix + name cfg.ProxyName = prefix + name
// server_name // server_name
cfg.ServerName = prefix + cfg.ServerName if cfg.ServerUser == "" {
cfg.ServerName = prefix + cfg.ServerName
} else {
cfg.ServerName = cfg.ServerUser + "." + cfg.ServerName
}
// bind_addr // bind_addr
if cfg.BindAddr == "" { if cfg.BindAddr == "" {
cfg.BindAddr = "127.0.0.1" cfg.BindAddr = "127.0.0.1"
} }
return nil return nil
} }
@@ -158,32 +163,16 @@ func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, sec
return err return err
} }
err = cfg.GetBaseInfo().unmarshalFromIni(prefix, name, section) err = cfg.GetBaseConfig().unmarshalFromIni(prefix, name, section)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// SUDP // SUDP
var _ VisitorConf = &SUDPVisitorConf{} var _ VisitorConf = &SUDPVisitorConf{}
func (cfg *SUDPVisitorConf) Compare(cmp VisitorConf) bool {
cmpConf, ok := cmp.(*SUDPVisitorConf)
if !ok {
return false
}
if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
return false
}
// Add custom login equal, if exists
return true
}
func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil { if err != nil {
@@ -195,8 +184,8 @@ func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section
return return
} }
func (cfg *SUDPVisitorConf) Check() (err error) { func (cfg *SUDPVisitorConf) Validate() (err error) {
if err = cfg.BaseVisitorConf.check(); err != nil { if err = cfg.BaseVisitorConf.validate(); err != nil {
return return
} }
@@ -208,21 +197,6 @@ func (cfg *SUDPVisitorConf) Check() (err error) {
// STCP // STCP
var _ VisitorConf = &STCPVisitorConf{} var _ VisitorConf = &STCPVisitorConf{}
func (cfg *STCPVisitorConf) Compare(cmp VisitorConf) bool {
cmpConf, ok := cmp.(*STCPVisitorConf)
if !ok {
return false
}
if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
return false
}
// Add custom login equal, if exists
return true
}
func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil { if err != nil {
@@ -234,8 +208,8 @@ func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
return return
} }
func (cfg *STCPVisitorConf) Check() (err error) { func (cfg *STCPVisitorConf) Validate() (err error) {
if err = cfg.BaseVisitorConf.check(); err != nil { if err = cfg.BaseVisitorConf.validate(); err != nil {
return return
} }
@@ -247,21 +221,6 @@ func (cfg *STCPVisitorConf) Check() (err error) {
// XTCP // XTCP
var _ VisitorConf = &XTCPVisitorConf{} var _ VisitorConf = &XTCPVisitorConf{}
func (cfg *XTCPVisitorConf) Compare(cmp VisitorConf) bool {
cmpConf, ok := cmp.(*XTCPVisitorConf)
if !ok {
return false
}
if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
return false
}
// Add custom login equal, if exists
return true
}
func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
if err != nil { if err != nil {
@@ -269,16 +228,29 @@ func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
} }
// Add custom logic unmarshal, if exists // Add custom logic unmarshal, if exists
if cfg.Protocol == "" {
cfg.Protocol = "quic"
}
if cfg.MaxRetriesAnHour <= 0 {
cfg.MaxRetriesAnHour = 8
}
if cfg.MinRetryInterval <= 0 {
cfg.MinRetryInterval = 90
}
if cfg.FallbackTimeoutMs <= 0 {
cfg.FallbackTimeoutMs = 1000
}
return return
} }
func (cfg *XTCPVisitorConf) Check() (err error) { func (cfg *XTCPVisitorConf) Validate() (err error) {
if err = cfg.BaseVisitorConf.check(); err != nil { if err = cfg.BaseVisitorConf.validate(); err != nil {
return return
} }
// Add custom logic validate, if exists // Add custom logic validate, if exists
if !lo.Contains([]string{"", "kcp", "quic"}, cfg.Protocol) {
return fmt.Errorf("protocol should be 'kcp' or 'quic'")
}
return return
} }

View File

@@ -17,10 +17,10 @@ package config
import ( import (
"testing" "testing"
"github.com/fatedier/frp/pkg/consts"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/consts"
) )
const testVisitorPrefix = "test." const testVisitorPrefix = "test."
@@ -87,6 +87,10 @@ func Test_Visitor_UnmarshalFromIni(t *testing.T) {
BindAddr: "127.0.0.1", BindAddr: "127.0.0.1",
BindPort: 9001, BindPort: 9001,
}, },
Protocol: "quic",
MaxRetriesAnHour: 8,
MinRetryInterval: 90,
FallbackTimeoutMs: 1000,
}, },
}, },
} }

View File

@@ -16,26 +16,26 @@ package consts
var ( var (
// proxy status // proxy status
Idle string = "idle" Idle = "idle"
Working string = "working" Working = "working"
Closed string = "closed" Closed = "closed"
Online string = "online" Online = "online"
Offline string = "offline" Offline = "offline"
// proxy type // proxy type
TCPProxy string = "tcp" TCPProxy = "tcp"
UDPProxy string = "udp" UDPProxy = "udp"
TCPMuxProxy string = "tcpmux" TCPMuxProxy = "tcpmux"
HTTPProxy string = "http" HTTPProxy = "http"
HTTPSProxy string = "https" HTTPSProxy = "https"
STCPProxy string = "stcp" STCPProxy = "stcp"
XTCPProxy string = "xtcp" XTCPProxy = "xtcp"
SUDPProxy string = "sudp" SUDPProxy = "sudp"
// authentication method // authentication method
TokenAuthMethod string = "token" TokenAuthMethod = "token"
OidcAuthMethod string = "oidc" OidcAuthMethod = "oidc"
// TCP multiplexer // TCP multiplexer
HTTPConnectTCPMultiplexer string = "httpconnect" HTTPConnectTCPMultiplexer = "httpconnect"
) )

View File

@@ -30,7 +30,7 @@ func EnablePrometheus() {
sm.Add(prometheus.ServerMetrics) sm.Add(prometheus.ServerMetrics)
} }
var sm *serverMetrics = &serverMetrics{} var sm = &serverMetrics{}
func init() { func init() {
metrics.Register(sm) metrics.Register(sm)

View File

@@ -23,9 +23,12 @@ import (
server "github.com/fatedier/frp/server/metrics" server "github.com/fatedier/frp/server/metrics"
) )
var sm *serverMetrics = newServerMetrics() var (
var ServerMetrics server.ServerMetrics sm = newServerMetrics()
var StatsCollector Collector
ServerMetrics server.ServerMetrics
StatsCollector Collector
)
func init() { func init() {
ServerMetrics = sm ServerMetrics = sm
@@ -57,25 +60,30 @@ func (m *serverMetrics) run() {
go func() { go func() {
for { for {
time.Sleep(12 * time.Hour) time.Sleep(12 * time.Hour)
log.Debug("start to clear useless proxy statistics data...") start := time.Now()
m.clearUselessInfo() count, total := m.clearUselessInfo()
log.Debug("finish to clear useless proxy statistics data") log.Debug("clear useless proxy statistics data count %d/%d, cost %v", count, total, time.Since(start))
} }
}() }()
} }
func (m *serverMetrics) clearUselessInfo() { func (m *serverMetrics) clearUselessInfo() (int, int) {
count := 0
total := 0
// To check if there are proxies that closed than 7 days and drop them. // To check if there are proxies that closed than 7 days and drop them.
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
total = len(m.info.ProxyStatistics)
for name, data := range m.info.ProxyStatistics { for name, data := range m.info.ProxyStatistics {
if !data.LastCloseTime.IsZero() && if !data.LastCloseTime.IsZero() &&
data.LastStartTime.Before(data.LastCloseTime) && data.LastStartTime.Before(data.LastCloseTime) &&
time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour { time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour {
delete(m.info.ProxyStatistics, name) delete(m.info.ProxyStatistics, name)
count++
log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String())
} }
} }
return count, total
} }
func (m *serverMetrics) NewClient() { func (m *serverMetrics) NewClient() {

View File

@@ -4,5 +4,7 @@ import (
"github.com/fatedier/frp/pkg/metrics/aggregate" "github.com/fatedier/frp/pkg/metrics/aggregate"
) )
var EnableMem = aggregate.EnableMem var (
var EnablePrometheus = aggregate.EnablePrometheus EnableMem = aggregate.EnableMem
EnablePrometheus = aggregate.EnablePrometheus
)

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