Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9634fd99d1 | ||
|
|
7a1c248b67 | ||
|
|
886c9c2fdb | ||
|
|
266c492b5d | ||
|
|
5dd70ace6b | ||
|
|
fb2c98e87b | ||
|
|
ed13141c56 | ||
|
|
3370bd53f5 | ||
|
|
1245f8804e | ||
|
|
479e9f50c2 | ||
|
|
a4175a2595 | ||
|
|
36718d88e4 | ||
|
|
bc378bcbec | ||
|
|
33428ab538 | ||
|
|
ef96481f58 | ||
|
|
7526d7a69a | ||
|
|
2bdf25bae6 | ||
|
|
0fe8f7a0b6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 | ||
|
|
b5e90c03a1 | ||
|
|
b642a6323c | ||
|
|
6561107945 | ||
|
|
abf4942e8a | ||
|
|
7cfa546b55 | ||
|
|
0a798a7a69 | ||
|
|
604700cea5 | ||
|
|
610e5ed479 | ||
|
|
80d3f332e1 | ||
|
|
14253afe2f | ||
|
|
024c334d9d | ||
|
|
f795950742 | ||
|
|
024e4f5f1d | ||
|
|
dc3bc9182c | ||
|
|
e6dacf3a67 | ||
|
|
7fe295f4f4 | ||
|
|
c3bf952d8f | ||
|
|
f9065a6a78 | ||
|
|
61330d4d79 | ||
|
|
c777891f75 | ||
|
|
43cf1688e4 | ||
|
|
720c09c06b | ||
|
|
3fa76b72f3 | ||
|
|
8eb525a648 | ||
|
|
077ba80ba3 | ||
|
|
c99986fa28 | ||
|
|
b41d8f8e40 | ||
|
|
3c8d648ddc | ||
|
|
27f66baf54 | ||
|
|
c5a8f6ef4a | ||
|
|
e208043323 | ||
|
|
a78814a2e9 | ||
|
|
31b44c1feb | ||
|
|
773169e0c4 | ||
|
|
9757a351c6 | ||
|
|
1e8db66743 | ||
|
|
e0dd947e6a | ||
|
|
8b86e1473c | ||
|
|
b8d3ace113 | ||
|
|
450b8393bc | ||
|
|
27db6217ec | ||
|
|
6542dcd4ed | ||
|
|
092e5d3f94 | ||
|
|
01fed8d1a9 | ||
|
|
2a7aa69890 | ||
|
|
f47d8ab97f | ||
|
|
bb912d6c37 | ||
|
|
c73096f2bf | ||
|
|
0358113948 | ||
|
|
8593eff752 | ||
|
|
dff56cb0ca | ||
|
|
4383756fd4 | ||
|
|
6ba849fc75 | ||
|
|
9d5638cae6 | ||
|
|
62352c7ba5 | ||
|
|
4bbec09d57 | ||
|
|
f7a06cbe61 | ||
|
|
3a08c2aeb0 | ||
|
|
b14192a8d3 | ||
|
|
2466e65f43 | ||
|
|
2855ac71e3 | ||
|
|
fe4ca1b54e | ||
|
|
edd7cf8967 | ||
|
|
ccfe8c97f4 | ||
|
|
03c8d7bf96 | ||
|
|
2dcdb24cc4 | ||
|
|
d47e138bc9 | ||
|
|
f1fb2d721a | ||
|
|
ae73ec2fed | ||
|
|
e8045194cd | ||
|
|
69cc422edf | ||
|
|
243ca994e0 | ||
|
|
b4d5d8c756 | ||
|
|
c6f9d8d403 | ||
|
|
939c490768 | ||
|
|
f390e4a401 | ||
|
|
e649692217 | ||
|
|
77990c31ef | ||
|
|
e680acf42d | ||
|
|
522e2c94c1 | ||
|
|
301515d2e8 | ||
|
|
f0442d0cd5 | ||
|
|
9ced717d69 | ||
|
|
4e8e9e1dec | ||
|
|
92cb0b30c2 | ||
|
|
e81b36c5ba | ||
|
|
d0d396becb | ||
|
|
ee3892798d | ||
|
|
405969085f | ||
|
|
c1893ee1b4 | ||
|
|
eaae212d2d | ||
|
|
885278c045 | ||
|
|
2626d6ed92 | ||
|
|
f3a71bc08f | ||
|
|
dd7e2e8473 | ||
|
|
07946e9752 | ||
|
|
e52727e01c | ||
|
|
ba937e9fbf | ||
|
|
8f23733f47 | ||
|
|
d2d03a8fd9 | ||
|
|
590ccda677 | ||
|
|
86f90f4d27 | ||
|
|
5a6d9f60c2 | ||
|
|
f16ef00975 | ||
|
|
b36f3834eb | ||
|
|
c08be0fd92 | ||
|
|
bc5fb91c05 | ||
|
|
002831ea82 | ||
|
|
acf33db4e4 | ||
|
|
3585f5c0c0 | ||
|
|
8383d528d9 | ||
|
|
fa977c839f | ||
|
|
86c2ad78c8 | ||
|
|
a5b7abfc8b | ||
|
|
d5589213c5 | ||
|
|
e0c979e98e | ||
|
|
1e650ea9a7 | ||
|
|
e6ec5a509b | ||
|
|
43ba7bd338 | ||
|
|
49443cb2c6 | ||
|
|
32f09c4b60 | ||
|
|
f63b4d5c29 | ||
|
|
b3946489dd | ||
|
|
7ae3719b82 | ||
|
|
80cfd0938e | ||
|
|
52f66b05e6 | ||
|
|
b2b580be22 | ||
|
|
3e0c78233a | ||
|
|
b6361fb143 | ||
|
|
adb04e81e7 | ||
|
|
518ca2ceb2 | ||
|
|
4957fd23ee | ||
|
|
2f958c2095 | ||
|
|
dc34a68542 | ||
|
|
3529158f31 | ||
|
|
9152c59570 | ||
|
|
2af2cf7dbd | ||
|
|
cf025d6320 | ||
|
|
d689f0fc53 | ||
|
|
e8ace492a5 | ||
|
|
1c8bc0bfa8 | ||
|
|
b31c67d7c0 | ||
|
|
8023d147b0 | ||
|
|
6a488cc081 | ||
|
|
7418ae098d | ||
|
|
7999791708 | ||
|
|
f7efbfeec5 | ||
|
|
1e8806d26b | ||
|
|
d01f4a3ec1 | ||
|
|
596262d5e0 | ||
|
|
cdfa8fa66f | ||
|
|
256b87321d | ||
|
|
5b7b81a117 | ||
|
|
d505ecb473 | ||
|
|
2a9a7a0e4a | ||
|
|
2b83436a97 | ||
|
|
5e77c8e2d3 | ||
|
|
3bf6605e1a | ||
|
|
3540910879 | ||
|
|
2d67e2e0c6 | ||
|
|
051299ec25 | ||
|
|
cc2076970f | ||
|
|
e7652f4ccc | ||
|
|
e66e77cb8f | ||
|
|
1bc9d1a28e | ||
|
|
7ad62818bd | ||
|
|
9ecafeab40 | ||
|
|
95cf418963 | ||
|
|
6d9e0c20f6 | ||
|
|
97d3cf1a3b | ||
|
|
38f297a395 | ||
|
|
7c799ee921 | ||
|
|
69ae2b0b69 | ||
|
|
d5b41f1e14 | ||
|
|
8b432e179d | ||
|
|
f5d5a00eef | ||
|
|
526e809bd5 | ||
|
|
e8deb65c4b | ||
|
|
184223cb2f | ||
|
|
5760c1cf92 | ||
|
|
5c4d820eb4 | ||
|
|
46266e4d30 | ||
|
|
44985f574d | ||
|
|
a6478aeac8 | ||
|
|
806b55c292 | ||
|
|
c9ca9353cf | ||
|
|
496b1f1078 | ||
|
|
9cb0726ebc | ||
|
|
31190c703d | ||
|
|
1452facf77 | ||
|
|
6d4d8e616d | ||
|
|
a7ad967231 | ||
|
|
31fa3f021a | ||
|
|
01a0d557ef | ||
|
|
b9c24e9b69 | ||
|
|
df12cc2b9d | ||
|
|
7cc67e852e | ||
|
|
307d1bfa3f | ||
|
|
5eb8f3db03 | ||
|
|
2d3af8a108 | ||
|
|
3ae1a4f45a | ||
|
|
21d8e674f0 | ||
|
|
5e70d5bee0 | ||
|
|
5c8ea51eb5 | ||
|
|
bae0b4d7c0 | ||
|
|
74255f711e | ||
|
|
7cd02f5bd8 | ||
|
|
c95311d1a0 | ||
|
|
885b029fcf | ||
|
|
466d69eae0 | ||
|
|
f1454e91f5 | ||
|
|
7c8cbeb250 | ||
|
|
e9e12cf888 | ||
|
|
6430afcfa5 | ||
|
|
3235addaaa | ||
|
|
46ff40543a |
@ -2,24 +2,23 @@ version: 2
|
||||
jobs:
|
||||
go-version-latest:
|
||||
docker:
|
||||
- image: cimg/go:1.20-node
|
||||
- image: cimg/go:1.24-node
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- run: make
|
||||
- run: make alltest
|
||||
go-version-last:
|
||||
docker:
|
||||
- image: cimg/go:1.19-node
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- run: make
|
||||
- run: make alltest
|
||||
- checkout
|
||||
- run:
|
||||
name: Build web assets (frps)
|
||||
command: make install build
|
||||
working_directory: web/frps
|
||||
- run:
|
||||
name: Build web assets (frpc)
|
||||
command: make install build
|
||||
working_directory: web/frpc
|
||||
- run: make
|
||||
- run: make alltest
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- go-version-latest
|
||||
- go-version-last
|
||||
- go-version-latest
|
||||
|
||||
2
.github/FUNDING.yml
vendored
@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [fatedier]
|
||||
custom: ["https://afdian.net/a/fatedier"]
|
||||
custom: ["https://afdian.com/a/fatedier"]
|
||||
|
||||
5
.github/pull_request_template.md
vendored
@ -1,6 +1,3 @@
|
||||
### Summary
|
||||
|
||||
copilot:summary
|
||||
|
||||
### WHY
|
||||
|
||||
<!-- author to complete -->
|
||||
|
||||
16
.github/workflows/build-and-push-image.yml
vendored
@ -2,7 +2,7 @@ name: Build Image and Publish to Dockerhub & GPR
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ created ]
|
||||
types: [ published ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
@ -19,15 +19,15 @@ jobs:
|
||||
steps:
|
||||
# environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# get image tag name
|
||||
- name: Get Image Tag Name
|
||||
@ -38,13 +38,13 @@ jobs:
|
||||
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to the GPR
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push frpc
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./dockerfiles/Dockerfile-for-frpc
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
${{ env.TAG_FRPC_GPR }}
|
||||
|
||||
- name: Build and push frps
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./dockerfiles/Dockerfile-for-frps
|
||||
|
||||
44
.github/workflows/golangci-lint.yml
vendored
@ -14,28 +14,22 @@ jobs:
|
||||
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
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
run: make build
|
||||
working-directory: web/frps
|
||||
- name: Build web assets (frpc)
|
||||
run: make build
|
||||
working-directory: web/frpc
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: v2.3
|
||||
|
||||
20
.github/workflows/goreleaser.yml
vendored
@ -8,23 +8,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
|
||||
go-version: '1.24'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
run: make build
|
||||
working-directory: web/frps
|
||||
- name: Build web assets (frpc)
|
||||
run: make build
|
||||
working-directory: web/frpc
|
||||
- name: Make All
|
||||
run: |
|
||||
./package.sh
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist --release-notes=./Release.md
|
||||
args: release --clean --release-notes=./Release.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GPR_TOKEN }}
|
||||
|
||||
15
.github/workflows/stale.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Close stale issues"
|
||||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "20 0 * * *"
|
||||
@ -16,19 +16,20 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions/stale to close stale issues
|
||||
pull-requests: write # for actions/stale to close stale PRs
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v6
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'
|
||||
stale-pr-message: "PRs go stale after 30d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
|
||||
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
||||
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
||||
stale-issue-label: 'lifecycle/stale'
|
||||
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||
stale-pr-label: 'lifecycle/stale'
|
||||
exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
days-before-stale: 14
|
||||
days-before-close: 3
|
||||
debug-only: ${{ github.event.inputs.debug-only }}
|
||||
exempt-all-pr-milestones: true
|
||||
exempt-all-pr-assignees: true
|
||||
operations-per-run: 200
|
||||
|
||||
6
.gitignore
vendored
@ -33,6 +33,12 @@ lastversion/
|
||||
dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
.autogen_ssh_key
|
||||
client.crt
|
||||
client.key
|
||||
|
||||
# Cache
|
||||
*.swp
|
||||
|
||||
# AI
|
||||
CLAUDE.md
|
||||
|
||||
226
.golangci.yml
@ -1,144 +1,116 @@
|
||||
service:
|
||||
golangci-lint-version: 1.51.x # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
version: "2"
|
||||
run:
|
||||
concurrency: 4
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 20m
|
||||
timeout: 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
|
||||
default: none
|
||||
enable:
|
||||
- unused
|
||||
- asciicheck
|
||||
- copyloopvar
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- goimports
|
||||
- revive
|
||||
- gosimple
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- makezero
|
||||
- misspell
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- gci
|
||||
- gosec
|
||||
- asciicheck
|
||||
- prealloc
|
||||
- predeclared
|
||||
- makezero
|
||||
fast: false
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: false
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: false
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
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
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
errcheck:
|
||||
check-type-assertions: false
|
||||
check-blank: false
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- exitAfterDefer
|
||||
gosec:
|
||||
excludes:
|
||||
- G401
|
||||
- G402
|
||||
- G404
|
||||
- G501
|
||||
- G115
|
||||
- G204
|
||||
severity: low
|
||||
confidence: low
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
lll:
|
||||
line-length: 160
|
||||
tab-width: 1
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-rules:
|
||||
- cancelled
|
||||
- marshalled
|
||||
unparam:
|
||||
check-exported: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- 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.
|
||||
- errcheck
|
||||
- maligned
|
||||
path: _test\.go$|^tests/|^samples/
|
||||
- linters:
|
||||
- revive
|
||||
- staticcheck
|
||||
text: use underscores in Go names
|
||||
- linters:
|
||||
- revive
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- revive
|
||||
text: "avoid meaningless package names"
|
||||
- linters:
|
||||
- unparam
|
||||
text: is always false
|
||||
paths:
|
||||
- .*\.pb\.go
|
||||
- .*\.gen\.go
|
||||
- genfiles$
|
||||
- vendor$
|
||||
- bin$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/fatedier/frp/)
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- .*\.pb\.go
|
||||
- .*\.gen\.go
|
||||
- genfiles$
|
||||
- vendor$
|
||||
- bin$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
30
Makefile
@ -1,17 +1,23 @@
|
||||
export PATH := $(GOPATH)/bin:$(PATH)
|
||||
export PATH := $(PATH):`go env GOPATH`/bin
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
|
||||
all: fmt build
|
||||
.PHONY: web frps-web frpc-web frps frpc
|
||||
|
||||
all: env fmt web build
|
||||
|
||||
build: frps frpc
|
||||
|
||||
# compile assets into binary file
|
||||
file:
|
||||
rm -rf ./assets/frps/static/*
|
||||
rm -rf ./assets/frpc/static/*
|
||||
cp -rf ./web/frps/dist/* ./assets/frps/static
|
||||
cp -rf ./web/frpc/dist/* ./assets/frpc/static
|
||||
env:
|
||||
@go version
|
||||
|
||||
web: frps-web frpc-web
|
||||
|
||||
frps-web:
|
||||
$(MAKE) -C web/frps build
|
||||
|
||||
frpc-web:
|
||||
$(MAKE) -C web/frpc build
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
@ -22,18 +28,18 @@ fmt-more:
|
||||
gci:
|
||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||
|
||||
vet:
|
||||
vet: web
|
||||
go vet ./...
|
||||
|
||||
frps:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
|
||||
|
||||
frpc:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
|
||||
|
||||
test: gotest
|
||||
|
||||
gotest:
|
||||
gotest: web
|
||||
go test -v --cover ./assets/...
|
||||
go test -v --cover ./cmd/...
|
||||
go test -v --cover ./client/...
|
||||
|
||||
@ -1,26 +1,36 @@
|
||||
export PATH := $(GOPATH)/bin:$(PATH)
|
||||
export PATH := $(PATH):`go env GOPATH`/bin
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
|
||||
os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
|
||||
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
|
||||
|
||||
all: build
|
||||
|
||||
build: app
|
||||
|
||||
app:
|
||||
@$(foreach n, $(os-archs),\
|
||||
os=$(shell echo "$(n)" | cut -d : -f 1);\
|
||||
arch=$(shell echo "$(n)" | cut -d : -f 2);\
|
||||
gomips=$(shell echo "$(n)" | cut -d : -f 3);\
|
||||
target_suffix=$${os}_$${arch};\
|
||||
echo "Build $${os}-$${arch}...";\
|
||||
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_$${target_suffix} ./cmd/frpc;\
|
||||
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\
|
||||
echo "Build $${os}-$${arch} done";\
|
||||
@$(foreach n, $(os-archs), \
|
||||
os=$(shell echo "$(n)" | cut -d : -f 1); \
|
||||
arch=$(shell echo "$(n)" | cut -d : -f 2); \
|
||||
extra=$(shell echo "$(n)" | cut -d : -f 3); \
|
||||
flags=''; \
|
||||
target_suffix=$${os}_$${arch}; \
|
||||
if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
|
||||
if [ "$${extra}" = "7" ]; then \
|
||||
flags=GOARM=7; \
|
||||
target_suffix=$${os}_arm_hf; \
|
||||
elif [ "$${extra}" = "5" ]; then \
|
||||
flags=GOARM=5; \
|
||||
target_suffix=$${os}_arm; \
|
||||
fi; \
|
||||
elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
|
||||
flags=GOMIPS=$${extra}; \
|
||||
fi; \
|
||||
echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
|
||||
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
|
||||
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
|
||||
echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
|
||||
)
|
||||
@mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe
|
||||
@mv ./release/frps_windows_386 ./release/frps_windows_386.exe
|
||||
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
|
||||
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe
|
||||
@mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe
|
||||
|
||||
70
README_zh.md
@ -2,18 +2,55 @@
|
||||
|
||||
[](https://circleci.com/gh/fatedier/frp)
|
||||
[](https://github.com/fatedier/frp/releases)
|
||||
[](https://goreportcard.com/report/github.com/fatedier/frp)
|
||||
[](https://somsubhra.github.io/github-release-stats/?username=fatedier&repository=frp)
|
||||
|
||||
[README](README.md) | [中文文档](README_zh.md)
|
||||
|
||||
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
|
||||
|
||||
## Sponsors
|
||||
|
||||
frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
|
||||
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
<br>
|
||||
<b>The sovereign cloud that puts you in control</b>
|
||||
<br>
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
|
||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## 为什么使用 frp ?
|
||||
@ -41,9 +78,7 @@ master 分支用于发布稳定版本,dev 分支用于开发,您可以尝试
|
||||
|
||||
### 关于 v2 的一些说明
|
||||
|
||||
当前整体形势不佳,面临的生活工作压力很大。
|
||||
|
||||
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构。
|
||||
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构,或者也有可能放弃一次性的重构,而是采用渐进的方式在当前版本上逐步做一些可能会导致不兼容的修改。
|
||||
|
||||
v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceMesh 方面的工作经验和思考。它的核心是一个现代化的四层和七层代理,类似于 envoy。这个代理本身高度可扩展,不仅可以用于实现内网穿透的功能,还可以应用于更多领域。在这个高度可扩展的内核基础上,我们将实现 frp v1 中的所有功能,并且能够以一种更加优雅的方式实现原先架构中无法实现或不易实现的功能。同时,我们将保持高效的开发和迭代能力。
|
||||
|
||||
@ -55,7 +90,7 @@ v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceM
|
||||
|
||||
## 文档
|
||||
|
||||
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org/docs)。
|
||||
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org)。
|
||||
|
||||
## 为 frp 做贡献
|
||||
|
||||
@ -68,7 +103,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
||||
* 贡献代码请提交 PR 至 dev 分支,master 分支仅用于发布稳定可用版本。
|
||||
* 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
|
||||
|
||||
**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
|
||||
**提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
|
||||
|
||||
## 关联项目
|
||||
|
||||
* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
|
||||
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
|
||||
|
||||
## 赞助
|
||||
|
||||
@ -80,20 +120,6 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
||||
|
||||
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
|
||||
|
||||
国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。
|
||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||
|
||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||
|
||||
### 知识星球
|
||||
|
||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
||||
|
||||

|
||||
|
||||
### 支付宝扫码捐赠
|
||||
|
||||

|
||||
|
||||
### 微信支付捐赠
|
||||
|
||||

|
||||
|
||||
10
Release.md
@ -1,4 +1,8 @@
|
||||
### Fixes
|
||||
## Features
|
||||
|
||||
* Fix the issue of not disabling tcp keepalive when configuring `tcp_keepalive` = -1 in frps.
|
||||
* Fix a race condition error.
|
||||
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
||||
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
||||
|
||||
## Fixes
|
||||
|
||||
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
||||
|
||||
@ -41,7 +41,7 @@ func Load(path string) {
|
||||
}
|
||||
|
||||
func Register(fileSystem fs.FS) {
|
||||
subFs, err := fs.Sub(fileSystem, "static")
|
||||
subFs, err := fs.Sub(fileSystem, "dist")
|
||||
if err == nil {
|
||||
content = subFs
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,16 +0,0 @@
|
||||
<!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>
|
||||
@ -1,14 +0,0 @@
|
||||
package frpc
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/fatedier/frp/assets"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var content embed.FS
|
||||
|
||||
func init() {
|
||||
assets.Register(content)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,16 +0,0 @@
|
||||
<!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>
|
||||
@ -1,85 +0,0 @@
|
||||
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/fatedier/frp/assets"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
var (
|
||||
httpServerReadTimeout = 60 * time.Second
|
||||
httpServerWriteTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
func (svr *Service) RunAdminServer(address string) (err error) {
|
||||
// url router
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/healthz", svr.healthz)
|
||||
|
||||
// debug
|
||||
if svr.cfg.PprofEnable {
|
||||
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
|
||||
}
|
||||
|
||||
subRouter := router.NewRoute().Subrouter()
|
||||
user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
|
||||
subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
|
||||
|
||||
// api, see admin_api.go
|
||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||
|
||||
// view
|
||||
subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
|
||||
subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
|
||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: router,
|
||||
ReadTimeout: httpServerReadTimeout,
|
||||
WriteTimeout: httpServerWriteTimeout,
|
||||
}
|
||||
if address == "" {
|
||||
address = ":http"
|
||||
}
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = server.Serve(ln)
|
||||
}()
|
||||
return
|
||||
}
|
||||
@ -15,258 +15,60 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/api"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
type GeneralResponse struct {
|
||||
Code int
|
||||
Msg string
|
||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||
apiController := newAPIController(svr)
|
||||
|
||||
// Healthz endpoint without auth
|
||||
helper.Router.HandleFunc("/healthz", healthz)
|
||||
|
||||
// API routes and static files with auth
|
||||
subRouter := helper.Router.NewRoute().Subrouter()
|
||||
subRouter.Use(helper.AuthMiddleware)
|
||||
subRouter.Use(httppkg.NewRequestLogger)
|
||||
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||
subRouter.PathPrefix("/static/").Handler(
|
||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||
).Methods("GET")
|
||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
|
||||
// /healthz
|
||||
func (svr *Service) healthz(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// GET /api/reload
|
||||
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Info("api request [/api/reload]")
|
||||
defer func() {
|
||||
log.Info("api response [/api/reload], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warn("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = err.Error()
|
||||
log.Warn("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
log.Info("success reload conf")
|
||||
func newAPIController(svr *Service) *api.Controller {
|
||||
return api.NewController(api.ControllerParams{
|
||||
GetProxyStatus: svr.getAllProxyStatus,
|
||||
ServerAddr: svr.common.ServerAddr,
|
||||
ConfigFilePath: svr.configFilePath,
|
||||
UnsafeFeatures: svr.unsafeFeatures,
|
||||
UpdateConfig: svr.UpdateAllConfigurer,
|
||||
GracefulClose: svr.GracefulClose,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/stop
|
||||
func (svr *Service) apiStop(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Info("api request [/api/stop]")
|
||||
defer func() {
|
||||
log.Info("api response [/api/stop], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
go svr.GracefulClose(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
type StatusResp map[string][]ProxyStatusResp
|
||||
|
||||
type ProxyStatusResp struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
Plugin string `json:"plugin"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
|
||||
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
|
||||
psr := ProxyStatusResp{
|
||||
Name: status.Name,
|
||||
Type: status.Type,
|
||||
Status: status.Phase,
|
||||
Err: status.Err,
|
||||
}
|
||||
baseCfg := status.Cfg.GetBaseConfig()
|
||||
if baseCfg.LocalPort != 0 {
|
||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||
}
|
||||
psr.Plugin = baseCfg.Plugin
|
||||
|
||||
if status.Err == "" {
|
||||
psr.RemoteAddr = status.RemoteAddr
|
||||
if lo.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||
psr.RemoteAddr = serverAddr + psr.RemoteAddr
|
||||
}
|
||||
}
|
||||
return psr
|
||||
}
|
||||
|
||||
// GET /api/status
|
||||
func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
buf []byte
|
||||
res StatusResp = make(map[string][]ProxyStatusResp)
|
||||
)
|
||||
|
||||
log.Info("Http request [/api/status]")
|
||||
defer func() {
|
||||
log.Info("Http response [/api/status]")
|
||||
buf, _ = json.Marshal(&res)
|
||||
_, _ = w.Write(buf)
|
||||
}()
|
||||
|
||||
ps := svr.ctl.pm.GetAllProxyStatus()
|
||||
for _, status := range ps {
|
||||
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr))
|
||||
}
|
||||
|
||||
for _, arrs := range res {
|
||||
if len(arrs) <= 1 {
|
||||
continue
|
||||
}
|
||||
sort.Slice(arrs, func(i, j int) bool {
|
||||
return strings.Compare(arrs[i].Name, arrs[j].Name) < 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/config
|
||||
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Info("Http get request [/api/config]")
|
||||
defer func() {
|
||||
log.Info("Http get response [/api/config], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
if svr.cfgFile == "" {
|
||||
res.Code = 400
|
||||
res.Msg = "frpc has no config file path"
|
||||
log.Warn("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := config.GetRenderedConfFromFile(svr.cfgFile)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warn("load frpc config file error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
rows := strings.Split(string(content), "\n")
|
||||
newRows := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
row = strings.TrimSpace(row)
|
||||
if strings.HasPrefix(row, "token") {
|
||||
continue
|
||||
}
|
||||
newRows = append(newRows, row)
|
||||
}
|
||||
res.Msg = strings.Join(newRows, "\n")
|
||||
}
|
||||
|
||||
// PUT /api/config
|
||||
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Info("Http put request [/api/config]")
|
||||
defer func() {
|
||||
log.Info("Http put response [/api/config], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
// get new config content
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = fmt.Sprintf("read request body error: %v", err)
|
||||
log.Warn("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
res.Code = 400
|
||||
res.Msg = "body can't be empty"
|
||||
log.Warn("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
// get token from origin content
|
||||
token := ""
|
||||
b, err := os.ReadFile(svr.cfgFile)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warn("load frpc config file error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
content := string(b)
|
||||
|
||||
for _, row := range strings.Split(content, "\n") {
|
||||
row = strings.TrimSpace(row)
|
||||
if strings.HasPrefix(row, "token") {
|
||||
token = row
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tmpRows := make([]string, 0)
|
||||
for _, row := range strings.Split(string(body), "\n") {
|
||||
row = strings.TrimSpace(row)
|
||||
if strings.HasPrefix(row, "token") {
|
||||
continue
|
||||
}
|
||||
tmpRows = append(tmpRows, row)
|
||||
}
|
||||
|
||||
newRows := make([]string, 0)
|
||||
if token != "" {
|
||||
for _, row := range tmpRows {
|
||||
newRows = append(newRows, row)
|
||||
if strings.HasPrefix(row, "[common]") {
|
||||
newRows = append(newRows, token)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRows = tmpRows
|
||||
}
|
||||
content = strings.Join(newRows, "\n")
|
||||
|
||||
err = os.WriteFile(svr.cfgFile, []byte(content), 0o644)
|
||||
if err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
||||
log.Warn("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
// getAllProxyStatus returns all proxy statuses.
|
||||
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||
svr.ctlMu.RLock()
|
||||
ctl := svr.ctl
|
||||
svr.ctlMu.RUnlock()
|
||||
if ctl == nil {
|
||||
return nil
|
||||
}
|
||||
return ctl.pm.GetAllProxyStatus()
|
||||
}
|
||||
|
||||
189
client/api/controller.go
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
// Controller handles HTTP API requests for frpc.
|
||||
type Controller struct {
|
||||
// getProxyStatus returns the current proxy status.
|
||||
// Returns nil if the control connection is not established.
|
||||
getProxyStatus func() []*proxy.WorkingStatus
|
||||
|
||||
// serverAddr is the frps server address for display.
|
||||
serverAddr string
|
||||
|
||||
// configFilePath is the path to the configuration file.
|
||||
configFilePath string
|
||||
|
||||
// unsafeFeatures is used for validation.
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// updateConfig updates proxy and visitor configurations.
|
||||
updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||
|
||||
// gracefulClose gracefully stops the service.
|
||||
gracefulClose func(d time.Duration)
|
||||
}
|
||||
|
||||
// ControllerParams contains parameters for creating an APIController.
|
||||
type ControllerParams struct {
|
||||
GetProxyStatus func() []*proxy.WorkingStatus
|
||||
ServerAddr string
|
||||
ConfigFilePath string
|
||||
UnsafeFeatures *security.UnsafeFeatures
|
||||
UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||
GracefulClose func(d time.Duration)
|
||||
}
|
||||
|
||||
// NewController creates a new Controller.
|
||||
func NewController(params ControllerParams) *Controller {
|
||||
return &Controller{
|
||||
getProxyStatus: params.GetProxyStatus,
|
||||
serverAddr: params.ServerAddr,
|
||||
configFilePath: params.ConfigFilePath,
|
||||
unsafeFeatures: params.UnsafeFeatures,
|
||||
updateConfig: params.UpdateConfig,
|
||||
gracefulClose: params.GracefulClose,
|
||||
}
|
||||
}
|
||||
|
||||
// Reload handles GET /api/reload
|
||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||
strictConfigMode := false
|
||||
strictStr := ctx.Query("strictConfig")
|
||||
if strictStr != "" {
|
||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||
}
|
||||
|
||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
log.Infof("success reload conf")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Stop handles POST /api/stop
|
||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||
go c.gracefulClose(100 * time.Millisecond)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Status handles GET /api/status
|
||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||
res := make(StatusResp)
|
||||
ps := c.getProxyStatus()
|
||||
if ps == nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
for _, status := range ps {
|
||||
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||
}
|
||||
|
||||
for _, arrs := range res {
|
||||
if len(arrs) <= 1 {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetConfig handles GET /api/config
|
||||
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||
if c.configFilePath == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(c.configFilePath)
|
||||
if err != nil {
|
||||
log.Warnf("load frpc config file error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// PutConfig handles PUT /api/config
|
||||
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus
|
||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
||||
psr := ProxyStatusResp{
|
||||
Name: status.Name,
|
||||
Type: status.Type,
|
||||
Status: status.Phase,
|
||||
Err: status.Err,
|
||||
}
|
||||
baseCfg := status.Cfg.GetBaseConfig()
|
||||
if baseCfg.LocalPort != 0 {
|
||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||
}
|
||||
psr.Plugin = baseCfg.Plugin.Type
|
||||
|
||||
if status.Err == "" {
|
||||
psr.RemoteAddr = status.RemoteAddr
|
||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||
}
|
||||
}
|
||||
return psr
|
||||
}
|
||||
29
client/api/types.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
// StatusResp is the response for GET /api/status
|
||||
type StatusResp map[string][]ProxyStatusResp
|
||||
|
||||
// ProxyStatusResp contains proxy status information
|
||||
type ProxyStatusResp struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
Plugin string `json:"plugin"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
227
client/connector.go
Normal file
@ -0,0 +1,227 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
libnet "github.com/fatedier/golib/net"
|
||||
fmux "github.com/hashicorp/yamux"
|
||||
quic "github.com/quic-go/quic-go"
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
|
||||
// Connector is an interface for establishing connections to the server.
|
||||
type Connector interface {
|
||||
Open() error
|
||||
Connect() (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// defaultConnectorImpl is the default implementation of Connector for normal frpc.
|
||||
type defaultConnectorImpl struct {
|
||||
ctx context.Context
|
||||
cfg *v1.ClientCommonConfig
|
||||
|
||||
muxSession *fmux.Session
|
||||
quicConn *quic.Conn
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
|
||||
return &defaultConnectorImpl{
|
||||
ctx: ctx,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens an underlying connection to the server.
|
||||
// The underlying connection is either a TCP connection or a QUIC connection.
|
||||
// After the underlying connection is established, you can call Connect() to get a stream.
|
||||
// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect().
|
||||
func (c *defaultConnectorImpl) Open() error {
|
||||
xl := xlog.FromContextSafe(c.ctx)
|
||||
|
||||
// special for quic
|
||||
if strings.EqualFold(c.cfg.Transport.Protocol, "quic") {
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
sn := c.cfg.Transport.TLS.ServerName
|
||||
if sn == "" {
|
||||
sn = c.cfg.ServerAddr
|
||||
}
|
||||
if lo.FromPtr(c.cfg.Transport.TLS.Enable) {
|
||||
tlsConfig, err = transport.NewClientTLSConfig(
|
||||
c.cfg.Transport.TLS.CertFile,
|
||||
c.cfg.Transport.TLS.KeyFile,
|
||||
c.cfg.Transport.TLS.TrustedCaFile,
|
||||
sn)
|
||||
} else {
|
||||
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
|
||||
}
|
||||
if err != nil {
|
||||
xl.Warnf("fail to build tls configuration, err: %v", err)
|
||||
return err
|
||||
}
|
||||
tlsConfig.NextProtos = []string{"frp"}
|
||||
|
||||
conn, err := quic.DialAddr(
|
||||
c.ctx,
|
||||
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
|
||||
tlsConfig, &quic.Config{
|
||||
MaxIdleTimeout: time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||
MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams),
|
||||
KeepAlivePeriod: time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.quicConn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
if !lo.FromPtr(c.cfg.Transport.TCPMux) {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := c.realConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmuxCfg := fmux.DefaultConfig()
|
||||
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
||||
// Use trace level for yamux logs
|
||||
fmuxCfg.LogOutput = xlog.NewTraceWriter(xl)
|
||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||
session, err := fmux.Client(conn, fmuxCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.muxSession = session
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
|
||||
func (c *defaultConnectorImpl) Connect() (net.Conn, error) {
|
||||
if c.quicConn != nil {
|
||||
stream, err := c.quicConn.OpenStreamSync(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil
|
||||
} else if c.muxSession != nil {
|
||||
stream, err := c.muxSession.OpenStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
return c.realConnect()
|
||||
}
|
||||
|
||||
func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
||||
xl := xlog.FromContextSafe(c.ctx)
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable)
|
||||
if c.cfg.Transport.Protocol == "wss" {
|
||||
tlsEnable = true
|
||||
}
|
||||
if tlsEnable {
|
||||
sn := c.cfg.Transport.TLS.ServerName
|
||||
if sn == "" {
|
||||
sn = c.cfg.ServerAddr
|
||||
}
|
||||
|
||||
tlsConfig, err = transport.NewClientTLSConfig(
|
||||
c.cfg.Transport.TLS.CertFile,
|
||||
c.cfg.Transport.TLS.KeyFile,
|
||||
c.cfg.Transport.TLS.TrustedCaFile,
|
||||
sn)
|
||||
if err != nil {
|
||||
xl.Warnf("fail to build tls configuration, err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
|
||||
if err != nil {
|
||||
xl.Errorf("fail to parse proxy url")
|
||||
return nil, err
|
||||
}
|
||||
dialOptions := []libnet.DialOption{}
|
||||
protocol := c.cfg.Transport.Protocol
|
||||
switch protocol {
|
||||
case "websocket":
|
||||
protocol = "tcp"
|
||||
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
|
||||
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
|
||||
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||
}))
|
||||
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||
case "wss":
|
||||
protocol = "tcp"
|
||||
dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig))
|
||||
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
|
||||
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
|
||||
default:
|
||||
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
|
||||
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||
}))
|
||||
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||
}
|
||||
|
||||
if c.cfg.Transport.ConnectServerLocalIP != "" {
|
||||
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
|
||||
}
|
||||
dialOptions = append(dialOptions,
|
||||
libnet.WithProtocol(protocol),
|
||||
libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
|
||||
libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
|
||||
libnet.WithProxy(proxyType, addr),
|
||||
libnet.WithProxyAuth(auth),
|
||||
)
|
||||
conn, err := libnet.DialContext(
|
||||
c.ctx,
|
||||
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
|
||||
dialOptions...,
|
||||
)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (c *defaultConnectorImpl) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
if c.quicConn != nil {
|
||||
_ = c.quicConn.CloseWithError(0, "")
|
||||
}
|
||||
if c.muxSession != nil {
|
||||
_ = c.muxSession.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@ -16,143 +16,142 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/control/shutdown"
|
||||
"github.com/fatedier/golib/crypto"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/client/visitor"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/wait"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
type SessionContext struct {
|
||||
// The client common configuration.
|
||||
Common *v1.ClientCommonConfig
|
||||
|
||||
// Unique ID obtained from frps.
|
||||
// It should be attached to the login message when reconnecting.
|
||||
RunID string
|
||||
// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
|
||||
Conn net.Conn
|
||||
// Indicates whether the connection is encrypted.
|
||||
ConnEncrypted bool
|
||||
// Auth runtime used for login, heartbeats, and encryption.
|
||||
Auth *auth.ClientAuth
|
||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||
Connector Connector
|
||||
// Virtual net controller
|
||||
VnetController *vnet.Controller
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
// 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
|
||||
// session context
|
||||
sessionCtx *SessionContext
|
||||
|
||||
// manage all proxies
|
||||
pxyCfgs map[string]config.ProxyConf
|
||||
pm *proxy.Manager
|
||||
pm *proxy.Manager
|
||||
|
||||
// manage all visitors
|
||||
vm *visitor.Manager
|
||||
|
||||
// control connection
|
||||
conn net.Conn
|
||||
doneCh chan struct{}
|
||||
|
||||
cm *ConnectionManager
|
||||
|
||||
// put a message in this channel to send it over control connection to server
|
||||
sendCh chan (msg.Message)
|
||||
|
||||
// read from this channel to get the next message sent by server
|
||||
readCh chan (msg.Message)
|
||||
|
||||
// goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed
|
||||
closedCh chan struct{}
|
||||
|
||||
closedDoneCh chan struct{}
|
||||
|
||||
// last time got the Pong message
|
||||
lastPong time.Time
|
||||
|
||||
// The client configuration
|
||||
clientCfg config.ClientCommonConf
|
||||
|
||||
readerShutdown *shutdown.Shutdown
|
||||
writerShutdown *shutdown.Shutdown
|
||||
msgHandlerShutdown *shutdown.Shutdown
|
||||
|
||||
// sets authentication based on selected method
|
||||
authSetter auth.Setter
|
||||
// of time.Time, last time got the Pong message
|
||||
lastPong atomic.Value
|
||||
|
||||
// The role of msgTransporter is similar to HTTP2.
|
||||
// It allows multiple messages to be sent simultaneously on the same control connection.
|
||||
// The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type.
|
||||
msgTransporter transport.MessageTransporter
|
||||
|
||||
// msgDispatcher is a wrapper for control connection.
|
||||
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
|
||||
msgDispatcher *msg.Dispatcher
|
||||
}
|
||||
|
||||
func NewControl(
|
||||
ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
|
||||
clientCfg config.ClientCommonConf,
|
||||
pxyCfgs map[string]config.ProxyConf,
|
||||
visitorCfgs map[string]config.VisitorConf,
|
||||
authSetter auth.Setter,
|
||||
) *Control {
|
||||
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
|
||||
// new xlog instance
|
||||
ctl := &Control{
|
||||
ctx: ctx,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
runID: runID,
|
||||
conn: conn,
|
||||
cm: cm,
|
||||
pxyCfgs: pxyCfgs,
|
||||
sendCh: make(chan msg.Message, 100),
|
||||
readCh: make(chan msg.Message, 100),
|
||||
closedCh: make(chan struct{}),
|
||||
closedDoneCh: make(chan struct{}),
|
||||
clientCfg: clientCfg,
|
||||
readerShutdown: shutdown.New(),
|
||||
writerShutdown: shutdown.New(),
|
||||
msgHandlerShutdown: shutdown.New(),
|
||||
authSetter: authSetter,
|
||||
ctx: ctx,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
sessionCtx: sessionCtx,
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
|
||||
ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
|
||||
ctl.lastPong.Store(time.Now())
|
||||
|
||||
ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
|
||||
ctl.vm.Reload(visitorCfgs)
|
||||
return ctl
|
||||
if sessionCtx.ConnEncrypted {
|
||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
||||
} else {
|
||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||
}
|
||||
ctl.registerMsgHandlers()
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||
|
||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||
return ctl, nil
|
||||
}
|
||||
|
||||
func (ctl *Control) Run() {
|
||||
func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) {
|
||||
go ctl.worker()
|
||||
|
||||
// start all proxies
|
||||
ctl.pm.Reload(ctl.pxyCfgs)
|
||||
ctl.pm.UpdateAll(proxyCfgs)
|
||||
|
||||
// start all visitors
|
||||
go ctl.vm.Run()
|
||||
ctl.vm.UpdateAll(visitorCfgs)
|
||||
}
|
||||
|
||||
func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
|
||||
func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||
ctl.pm.SetInWorkConnCallback(cb)
|
||||
}
|
||||
|
||||
func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
||||
xl := ctl.xl
|
||||
workConn, err := ctl.connectServer()
|
||||
if err != nil {
|
||||
xl.Warn("start new connection to server error: %v", err)
|
||||
xl.Warnf("start new connection to server error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
m := &msg.NewWorkConn{
|
||||
RunID: ctl.runID,
|
||||
RunID: ctl.sessionCtx.RunID,
|
||||
}
|
||||
if err = ctl.authSetter.SetNewWorkConn(m); err != nil {
|
||||
xl.Warn("error during NewWorkConn authentication: %v", err)
|
||||
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||
workConn.Close()
|
||||
return
|
||||
}
|
||||
if err = msg.WriteMsg(workConn, m); err != nil {
|
||||
xl.Warn("work connection write to server error: %v", err)
|
||||
xl.Warnf("work connection write to server error: %v", err)
|
||||
workConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var startMsg msg.StartWorkConn
|
||||
if err = msg.ReadMsgInto(workConn, &startMsg); err != nil {
|
||||
xl.Trace("work connection closed before response StartWorkConn message: %v", err)
|
||||
xl.Tracef("work connection closed before response StartWorkConn message: %v", err)
|
||||
workConn.Close()
|
||||
return
|
||||
}
|
||||
if startMsg.Error != "" {
|
||||
xl.Error("StartWorkConn contains error: %s", startMsg.Error)
|
||||
xl.Errorf("StartWorkConn contains error: %s", startMsg.Error)
|
||||
workConn.Close()
|
||||
return
|
||||
}
|
||||
@ -161,28 +160,49 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
|
||||
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
||||
}
|
||||
|
||||
func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
|
||||
func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
||||
xl := ctl.xl
|
||||
inMsg := m.(*msg.NewProxyResp)
|
||||
// Server will return NewProxyResp message to each NewProxy message.
|
||||
// Start a new proxy handler if no error got
|
||||
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||
if err != nil {
|
||||
xl.Warn("[%s] start error: %v", inMsg.ProxyName, err)
|
||||
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
|
||||
} else {
|
||||
xl.Info("[%s] start proxy success", inMsg.ProxyName)
|
||||
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
|
||||
func (ctl *Control) handleNatHoleResp(m msg.Message) {
|
||||
xl := ctl.xl
|
||||
inMsg := m.(*msg.NatHoleResp)
|
||||
|
||||
// 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")
|
||||
xl.Tracef("dispatch NatHoleResp message to related proxy error")
|
||||
}
|
||||
}
|
||||
|
||||
func (ctl *Control) handlePong(m msg.Message) {
|
||||
xl := ctl.xl
|
||||
inMsg := m.(*msg.Pong)
|
||||
|
||||
if inMsg.Error != "" {
|
||||
xl.Errorf("pong message contains error: %s", inMsg.Error)
|
||||
ctl.closeSession()
|
||||
return
|
||||
}
|
||||
ctl.lastPong.Store(time.Now())
|
||||
xl.Debugf("receive heartbeat from server")
|
||||
}
|
||||
|
||||
// closeSession closes the control connection.
|
||||
func (ctl *Control) closeSession() {
|
||||
ctl.sessionCtx.Conn.Close()
|
||||
ctl.sessionCtx.Connector.Close()
|
||||
}
|
||||
|
||||
func (ctl *Control) Close() error {
|
||||
return ctl.GracefulClose(0)
|
||||
}
|
||||
@ -193,169 +213,84 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
|
||||
|
||||
time.Sleep(d)
|
||||
|
||||
ctl.conn.Close()
|
||||
ctl.cm.Close()
|
||||
ctl.closeSession()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosedDoneCh returns a channel that will be closed after all resources are released
|
||||
func (ctl *Control) ClosedDoneCh() <-chan struct{} {
|
||||
return ctl.closedDoneCh
|
||||
// Done returns a channel that will be closed after all resources are released
|
||||
func (ctl *Control) Done() <-chan struct{} {
|
||||
return ctl.doneCh
|
||||
}
|
||||
|
||||
// connectServer return a new connection to frps
|
||||
func (ctl *Control) connectServer() (conn net.Conn, err error) {
|
||||
return ctl.cm.Connect()
|
||||
func (ctl *Control) connectServer() (net.Conn, error) {
|
||||
return ctl.sessionCtx.Connector.Connect()
|
||||
}
|
||||
|
||||
// reader read all messages from frps and send to readCh
|
||||
func (ctl *Control) reader() {
|
||||
xl := ctl.xl
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
xl.Error("panic error: %v", err)
|
||||
xl.Error(string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
defer ctl.readerShutdown.Done()
|
||||
defer close(ctl.closedCh)
|
||||
|
||||
encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token))
|
||||
for {
|
||||
m, err := msg.ReadMsg(encReader)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
xl.Debug("read from control connection EOF")
|
||||
return
|
||||
}
|
||||
xl.Warn("read error: %v", err)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
ctl.readCh <- m
|
||||
}
|
||||
func (ctl *Control) registerMsgHandlers() {
|
||||
ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
|
||||
ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
|
||||
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
|
||||
ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
|
||||
}
|
||||
|
||||
// writer writes messages got from sendCh to frps
|
||||
func (ctl *Control) writer() {
|
||||
// heartbeatWorker sends heartbeat to server and check heartbeat timeout.
|
||||
func (ctl *Control) heartbeatWorker() {
|
||||
xl := ctl.xl
|
||||
defer ctl.writerShutdown.Done()
|
||||
encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Token))
|
||||
if err != nil {
|
||||
xl.Error("crypto new writer error: %v", err)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
for {
|
||||
m, ok := <-ctl.sendCh
|
||||
if !ok {
|
||||
xl.Info("control writer is closing")
|
||||
return
|
||||
}
|
||||
|
||||
if err := msg.WriteMsg(encWriter, m); err != nil {
|
||||
xl.Warn("write message to control connection error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// msgHandler handles all channel events and performs corresponding operations.
|
||||
func (ctl *Control) msgHandler() {
|
||||
xl := ctl.xl
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
xl.Error("panic error: %v", err)
|
||||
xl.Error(string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
defer ctl.msgHandlerShutdown.Done()
|
||||
|
||||
var hbSendCh <-chan time.Time
|
||||
// TODO(fatedier): disable heartbeat if TCPMux is enabled.
|
||||
// Just keep it here to keep compatible with old version frps.
|
||||
if ctl.clientCfg.HeartbeatInterval > 0 {
|
||||
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
|
||||
defer hbSend.Stop()
|
||||
hbSendCh = hbSend.C
|
||||
}
|
||||
|
||||
var hbCheckCh <-chan time.Time
|
||||
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
|
||||
if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
|
||||
hbCheck := time.NewTicker(time.Second)
|
||||
defer hbCheck.Stop()
|
||||
hbCheckCh = hbCheck.C
|
||||
}
|
||||
|
||||
ctl.lastPong = time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-hbSendCh:
|
||||
// send heartbeat to server
|
||||
xl.Debug("send heartbeat to server")
|
||||
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
|
||||
// Send heartbeat to server.
|
||||
sendHeartBeat := func() (bool, error) {
|
||||
xl.Debugf("send heartbeat to server")
|
||||
pingMsg := &msg.Ping{}
|
||||
if err := ctl.authSetter.SetPing(pingMsg); err != nil {
|
||||
xl.Warn("error during ping authentication: %v", err)
|
||||
return
|
||||
}
|
||||
ctl.sendCh <- pingMsg
|
||||
case <-hbCheckCh:
|
||||
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
|
||||
xl.Warn("heartbeat timeout")
|
||||
// let reader() stop
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
case rawMsg, ok := <-ctl.readCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.ReqWorkConn:
|
||||
go ctl.HandleReqWorkConn(m)
|
||||
case *msg.NewProxyResp:
|
||||
ctl.HandleNewProxyResp(m)
|
||||
case *msg.NatHoleResp:
|
||||
ctl.HandleNatHoleResp(m)
|
||||
case *msg.Pong:
|
||||
if m.Error != "" {
|
||||
xl.Error("Pong contains error: %s", m.Error)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
ctl.lastPong = time.Now()
|
||||
xl.Debug("receive heartbeat from server")
|
||||
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||
return false, err
|
||||
}
|
||||
_ = ctl.msgDispatcher.Send(pingMsg)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
go wait.BackoffUntil(sendHeartBeat,
|
||||
wait.NewFastBackoffManager(wait.FastBackoffOptions{
|
||||
Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
|
||||
InitDurationIfFail: time.Second,
|
||||
Factor: 2.0,
|
||||
Jitter: 0.1,
|
||||
MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
|
||||
}),
|
||||
true, ctl.doneCh,
|
||||
)
|
||||
}
|
||||
|
||||
// Check heartbeat timeout.
|
||||
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
|
||||
go wait.Until(func() {
|
||||
if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warnf("heartbeat timeout")
|
||||
ctl.closeSession()
|
||||
return
|
||||
}
|
||||
}, time.Second, ctl.doneCh)
|
||||
}
|
||||
}
|
||||
|
||||
// If controler is notified by closedCh, reader and writer and handler will exit
|
||||
func (ctl *Control) worker() {
|
||||
go ctl.msgHandler()
|
||||
go ctl.reader()
|
||||
go ctl.writer()
|
||||
xl := ctl.xl
|
||||
go ctl.heartbeatWorker()
|
||||
go ctl.msgDispatcher.Run()
|
||||
|
||||
<-ctl.closedCh
|
||||
// close related channels and wait until other goroutines done
|
||||
close(ctl.readCh)
|
||||
ctl.readerShutdown.WaitDone()
|
||||
ctl.msgHandlerShutdown.WaitDone()
|
||||
|
||||
close(ctl.sendCh)
|
||||
ctl.writerShutdown.WaitDone()
|
||||
<-ctl.msgDispatcher.Done()
|
||||
xl.Debugf("control message dispatcher exited")
|
||||
ctl.closeSession()
|
||||
|
||||
ctl.pm.Close()
|
||||
ctl.vm.Close()
|
||||
|
||||
close(ctl.closedDoneCh)
|
||||
ctl.cm.Close()
|
||||
close(ctl.doneCh)
|
||||
}
|
||||
|
||||
func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
|
||||
ctl.vm.Reload(visitorCfgs)
|
||||
ctl.pm.Reload(pxyCfgs)
|
||||
func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
||||
ctl.vm.UpdateAll(visitorCfgs)
|
||||
ctl.pm.UpdateAll(proxyCfgs)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
|
||||
var ErrPayloadType = errors.New("error payload type")
|
||||
|
||||
type Handler func(payload interface{}) error
|
||||
type Handler func(payload any) error
|
||||
|
||||
type StartProxyPayload struct {
|
||||
NewProxyMsg *msg.NewProxy
|
||||
|
||||
@ -21,8 +21,10 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
|
||||
@ -38,8 +40,8 @@ type Monitor struct {
|
||||
addr string
|
||||
|
||||
// For http
|
||||
url string
|
||||
|
||||
url string
|
||||
header http.Header
|
||||
failedTimes uint64
|
||||
statusOK bool
|
||||
statusNormalFn func()
|
||||
@ -49,28 +51,41 @@ type Monitor struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewMonitor(ctx context.Context, checkType string,
|
||||
intervalS int, timeoutS int, maxFailedTimes int,
|
||||
addr string, url string,
|
||||
func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
|
||||
statusNormalFn func(), statusFailedFn func(),
|
||||
) *Monitor {
|
||||
if intervalS <= 0 {
|
||||
intervalS = 10
|
||||
if cfg.IntervalSeconds <= 0 {
|
||||
cfg.IntervalSeconds = 10
|
||||
}
|
||||
if timeoutS <= 0 {
|
||||
timeoutS = 3
|
||||
if cfg.TimeoutSeconds <= 0 {
|
||||
cfg.TimeoutSeconds = 3
|
||||
}
|
||||
if maxFailedTimes <= 0 {
|
||||
maxFailedTimes = 1
|
||||
if cfg.MaxFailed <= 0 {
|
||||
cfg.MaxFailed = 1
|
||||
}
|
||||
newctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
var url string
|
||||
if cfg.Type == "http" && cfg.Path != "" {
|
||||
s := "http://" + addr
|
||||
if !strings.HasPrefix(cfg.Path, "/") {
|
||||
s += "/"
|
||||
}
|
||||
url = s + cfg.Path
|
||||
}
|
||||
header := make(http.Header)
|
||||
for _, h := range cfg.HTTPHeaders {
|
||||
header.Set(h.Name, h.Value)
|
||||
}
|
||||
|
||||
return &Monitor{
|
||||
checkType: checkType,
|
||||
interval: time.Duration(intervalS) * time.Second,
|
||||
timeout: time.Duration(timeoutS) * time.Second,
|
||||
maxFailedTimes: maxFailedTimes,
|
||||
checkType: cfg.Type,
|
||||
interval: time.Duration(cfg.IntervalSeconds) * time.Second,
|
||||
timeout: time.Duration(cfg.TimeoutSeconds) * time.Second,
|
||||
maxFailedTimes: cfg.MaxFailed,
|
||||
addr: addr,
|
||||
url: url,
|
||||
header: header,
|
||||
statusOK: false,
|
||||
statusNormalFn: statusNormalFn,
|
||||
statusFailedFn: statusFailedFn,
|
||||
@ -103,17 +118,17 @@ func (monitor *Monitor) checkWorker() {
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
xl.Trace("do one health check success")
|
||||
xl.Tracef("do one health check success")
|
||||
if !monitor.statusOK && monitor.statusNormalFn != nil {
|
||||
xl.Info("health check status change to success")
|
||||
xl.Infof("health check status change to success")
|
||||
monitor.statusOK = true
|
||||
monitor.statusNormalFn()
|
||||
}
|
||||
} else {
|
||||
xl.Warn("do one health check failed: %v", err)
|
||||
xl.Warnf("do one health check failed: %v", err)
|
||||
monitor.failedTimes++
|
||||
if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil {
|
||||
xl.Warn("health check status change to failed")
|
||||
xl.Warnf("health check status change to failed")
|
||||
monitor.statusOK = false
|
||||
monitor.statusFailedFn()
|
||||
}
|
||||
@ -154,6 +169,8 @@ func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header = monitor.header
|
||||
req.Host = monitor.header.Get("Host")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -17,16 +17,16 @@ package proxy
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pxyConfs := []config.ProxyConf{
|
||||
&config.TCPProxyConf{},
|
||||
&config.HTTPProxyConf{},
|
||||
&config.HTTPSProxyConf{},
|
||||
&config.STCPProxyConf{},
|
||||
&config.TCPMuxProxyConf{},
|
||||
pxyConfs := []v1.ProxyConfigurer{
|
||||
&v1.TCPProxyConfig{},
|
||||
&v1.HTTPProxyConfig{},
|
||||
&v1.HTTPSProxyConfig{},
|
||||
&v1.STCPProxyConfig{},
|
||||
&v1.TCPMuxProxyConfig{},
|
||||
}
|
||||
for _, cfg := range pxyConfs {
|
||||
RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
|
||||
@ -40,7 +40,7 @@ type GeneralTCPProxy struct {
|
||||
*BaseProxy
|
||||
}
|
||||
|
||||
func NewGeneralTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
|
||||
func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy {
|
||||
return &GeneralTCPProxy{
|
||||
BaseProxy: baseProxy,
|
||||
}
|
||||
|
||||
@ -15,64 +15,67 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
libdial "github.com/fatedier/golib/net/dial"
|
||||
pp "github.com/pires/go-proxyproto"
|
||||
libnet "github.com/fatedier/golib/net"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{}
|
||||
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
||||
|
||||
func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) {
|
||||
func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) {
|
||||
proxyFactoryRegistry[proxyConfType] = factory
|
||||
}
|
||||
|
||||
// Proxy defines how to handle work connections for different proxy type.
|
||||
type Proxy interface {
|
||||
Run() error
|
||||
|
||||
// InWorkConn accept work connections registered to server.
|
||||
InWorkConn(net.Conn, *msg.StartWorkConn)
|
||||
|
||||
SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
|
||||
Close()
|
||||
}
|
||||
|
||||
func NewProxy(
|
||||
ctx context.Context,
|
||||
pxyConf config.ProxyConf,
|
||||
clientCfg config.ClientCommonConf,
|
||||
pxyConf v1.ProxyConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) (pxy Proxy) {
|
||||
var limiter *rate.Limiter
|
||||
limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes()
|
||||
if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeClient {
|
||||
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
|
||||
if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient {
|
||||
limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
|
||||
}
|
||||
|
||||
baseProxy := BaseProxy{
|
||||
baseProxyConfig: pxyConf.GetBaseConfig(),
|
||||
clientCfg: clientCfg,
|
||||
limiter: limiter,
|
||||
msgTransporter: msgTransporter,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
ctx: ctx,
|
||||
baseCfg: pxyConf.GetBaseConfig(),
|
||||
clientCfg: clientCfg,
|
||||
encryptionKey: encryptionKey,
|
||||
limiter: limiter,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
|
||||
@ -83,13 +86,16 @@ func NewProxy(
|
||||
}
|
||||
|
||||
type BaseProxy struct {
|
||||
baseProxyConfig *config.BaseProxyConf
|
||||
clientCfg config.ClientCommonConf
|
||||
msgTransporter transport.MessageTransporter
|
||||
limiter *rate.Limiter
|
||||
baseCfg *v1.ProxyBaseConfig
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
encryptionKey []byte
|
||||
msgTransporter transport.MessageTransporter
|
||||
vnetController *vnet.Controller
|
||||
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
|
||||
proxyPlugin plugin.Plugin
|
||||
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool
|
||||
|
||||
mu sync.RWMutex
|
||||
xl *xlog.Logger
|
||||
@ -97,8 +103,11 @@ type BaseProxy struct {
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) Run() error {
|
||||
if pxy.baseProxyConfig.Plugin != "" {
|
||||
p, err := plugin.Create(pxy.baseProxyConfig.Plugin, pxy.baseProxyConfig.PluginParams)
|
||||
if pxy.baseCfg.Plugin.Type != "" {
|
||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||
Name: pxy.baseCfg.Name,
|
||||
VnetController: pxy.vnetController,
|
||||
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -113,14 +122,23 @@ func (pxy *BaseProxy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||
pxy.inWorkConnCallback = cb
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Token))
|
||||
if pxy.inWorkConnCallback != nil {
|
||||
if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) {
|
||||
return
|
||||
}
|
||||
}
|
||||
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
||||
}
|
||||
|
||||
// Common handler for tcp work connections.
|
||||
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||
xl := pxy.xl
|
||||
baseConfig := pxy.baseProxyConfig
|
||||
baseCfg := pxy.baseCfg
|
||||
var (
|
||||
remote io.ReadWriteCloser
|
||||
err error
|
||||
@ -132,87 +150,76 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
||||
})
|
||||
}
|
||||
|
||||
xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
|
||||
baseConfig.UseEncryption, baseConfig.UseCompression)
|
||||
if baseConfig.UseEncryption {
|
||||
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||
if baseCfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, encKey)
|
||||
if err != nil {
|
||||
workConn.Close()
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if baseConfig.UseCompression {
|
||||
var releaseFn func()
|
||||
remote, releaseFn = libio.WithCompressionFromPool(remote)
|
||||
defer releaseFn()
|
||||
var compressionResourceRecycleFn func()
|
||||
if baseCfg.Transport.UseCompression {
|
||||
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
||||
}
|
||||
|
||||
// check if we need to send proxy protocol info
|
||||
var extraInfo []byte
|
||||
if baseConfig.ProxyProtocolVersion != "" {
|
||||
if m.SrcAddr != "" && m.SrcPort != 0 {
|
||||
if m.DstAddr == "" {
|
||||
m.DstAddr = "127.0.0.1"
|
||||
}
|
||||
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
|
||||
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
|
||||
h := &pp.Header{
|
||||
Command: pp.PROXY,
|
||||
SourceAddr: srcAddr,
|
||||
DestinationAddr: dstAddr,
|
||||
}
|
||||
|
||||
if strings.Contains(m.SrcAddr, ".") {
|
||||
h.TransportProtocol = pp.TCPv4
|
||||
} else {
|
||||
h.TransportProtocol = pp.TCPv6
|
||||
}
|
||||
|
||||
if baseConfig.ProxyProtocolVersion == "v1" {
|
||||
h.Version = 1
|
||||
} else if baseConfig.ProxyProtocolVersion == "v2" {
|
||||
h.Version = 2
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_, _ = h.WriteTo(buf)
|
||||
extraInfo = buf.Bytes()
|
||||
var connInfo plugin.ConnectionInfo
|
||||
if m.SrcAddr != "" && m.SrcPort != 0 {
|
||||
if m.DstAddr == "" {
|
||||
m.DstAddr = "127.0.0.1"
|
||||
}
|
||||
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
|
||||
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
|
||||
connInfo.SrcAddr = srcAddr
|
||||
connInfo.DstAddr = dstAddr
|
||||
}
|
||||
|
||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||
// Use the common proxy protocol builder function
|
||||
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||
connInfo.ProxyProtocolHeader = header
|
||||
}
|
||||
connInfo.Conn = remote
|
||||
connInfo.UnderlyingConn = workConn
|
||||
|
||||
if pxy.proxyPlugin != nil {
|
||||
// if plugin is set, let plugin handle connection first
|
||||
xl.Debug("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||
pxy.proxyPlugin.Handle(remote, workConn, extraInfo)
|
||||
xl.Debug("handle by plugin finished")
|
||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||
xl.Debugf("handle by plugin finished")
|
||||
return
|
||||
}
|
||||
|
||||
localConn, err := libdial.Dial(
|
||||
net.JoinHostPort(baseConfig.LocalIP, strconv.Itoa(baseConfig.LocalPort)),
|
||||
libdial.WithTimeout(10*time.Second),
|
||||
localConn, err := libnet.Dial(
|
||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||
libnet.WithTimeout(10*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
workConn.Close()
|
||||
xl.Error("connect to local service [%s:%d] error: %v", baseConfig.LocalIP, baseConfig.LocalPort, err)
|
||||
xl.Errorf("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
|
||||
xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
|
||||
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||
|
||||
if len(extraInfo) > 0 {
|
||||
if _, err := localConn.Write(extraInfo); err != nil {
|
||||
if connInfo.ProxyProtocolHeader != nil {
|
||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||
workConn.Close()
|
||||
xl.Error("write extraInfo to local conn error: %v", err)
|
||||
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, _, errs := libio.Join(localConn, remote)
|
||||
xl.Debug("join connections closed")
|
||||
xl.Debugf("join connections closed")
|
||||
if len(errs) > 0 {
|
||||
xl.Trace("join connections errors: %v", errs)
|
||||
xl.Tracef("join connections errors: %v", errs)
|
||||
}
|
||||
if compressionResourceRecycleFn != nil {
|
||||
compressionResourceRecycleFn()
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,34 +21,44 @@ import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/event"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
proxies map[string]*Wrapper
|
||||
msgTransporter transport.MessageTransporter
|
||||
proxies map[string]*Wrapper
|
||||
msgTransporter transport.MessageTransporter
|
||||
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||
vnetController *vnet.Controller
|
||||
|
||||
closed bool
|
||||
mu sync.RWMutex
|
||||
|
||||
clientCfg config.ClientCommonConf
|
||||
encryptionKey []byte
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewManager(
|
||||
ctx context.Context,
|
||||
clientCfg config.ClientCommonConf,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
proxies: make(map[string]*Wrapper),
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
closed: false,
|
||||
encryptionKey: encryptionKey,
|
||||
clientCfg: clientCfg,
|
||||
ctx: ctx,
|
||||
}
|
||||
@ -69,6 +79,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||
pm.inWorkConnCallback = cb
|
||||
}
|
||||
|
||||
func (pm *Manager) Close() {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
@ -89,7 +103,7 @@ func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWo
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *Manager) HandleEvent(payload interface{}) error {
|
||||
func (pm *Manager) HandleEvent(payload any) error {
|
||||
var m msg.Message
|
||||
switch e := payload.(type) {
|
||||
case *event.StartProxyPayload:
|
||||
@ -113,15 +127,27 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
||||
return ps
|
||||
}
|
||||
|
||||
func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
|
||||
func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
if pxy, ok := pm.proxies[name]; ok {
|
||||
return pxy.GetStatus(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
||||
xl := xlog.FromContextSafe(pm.ctx)
|
||||
proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
|
||||
return c.GetBaseConfig().Name
|
||||
})
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
delPxyNames := make([]string, 0)
|
||||
for name, pxy := range pm.proxies {
|
||||
del := false
|
||||
cfg, ok := pxyCfgs[name]
|
||||
cfg, ok := proxyCfgsMap[name]
|
||||
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
|
||||
del = true
|
||||
}
|
||||
@ -133,13 +159,17 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
|
||||
}
|
||||
}
|
||||
if len(delPxyNames) > 0 {
|
||||
xl.Info("proxy removed: %s", delPxyNames)
|
||||
xl.Infof("proxy removed: %s", delPxyNames)
|
||||
}
|
||||
|
||||
addPxyNames := make([]string, 0)
|
||||
for name, cfg := range pxyCfgs {
|
||||
for _, cfg := range proxyCfgs {
|
||||
name := cfg.GetBaseConfig().Name
|
||||
if _, ok := pm.proxies[name]; !ok {
|
||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
|
||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||
if pm.inWorkConnCallback != nil {
|
||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||
}
|
||||
pm.proxies[name] = pxy
|
||||
addPxyNames = append(addPxyNames, name)
|
||||
|
||||
@ -147,6 +177,6 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
|
||||
}
|
||||
}
|
||||
if len(addPxyNames) > 0 {
|
||||
xl.Info("proxy added: %s", addPxyNames)
|
||||
xl.Infof("proxy added: %s", addPxyNames)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -26,10 +27,11 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/client/event"
|
||||
"github.com/fatedier/frp/client/health"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -48,11 +50,11 @@ var (
|
||||
)
|
||||
|
||||
type WorkingStatus struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Phase string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
Cfg config.ProxyConf `json:"cfg"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Phase string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
Cfg v1.ProxyConfigurer `json:"cfg"`
|
||||
|
||||
// Got from server.
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
@ -72,6 +74,8 @@ type Wrapper struct {
|
||||
handler event.Handler
|
||||
|
||||
msgTransporter transport.MessageTransporter
|
||||
// vnet controller
|
||||
vnetController *vnet.Controller
|
||||
|
||||
health uint32
|
||||
lastSendStartMsg time.Time
|
||||
@ -86,17 +90,19 @@ type Wrapper struct {
|
||||
|
||||
func NewWrapper(
|
||||
ctx context.Context,
|
||||
cfg config.ProxyConf,
|
||||
clientCfg config.ClientCommonConf,
|
||||
cfg v1.ProxyConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
eventHandler event.Handler,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) *Wrapper {
|
||||
baseInfo := cfg.GetBaseConfig()
|
||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName)
|
||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
|
||||
pw := &Wrapper{
|
||||
WorkingStatus: WorkingStatus{
|
||||
Name: baseInfo.ProxyName,
|
||||
Type: baseInfo.ProxyType,
|
||||
Name: baseInfo.Name,
|
||||
Type: baseInfo.Type,
|
||||
Phase: ProxyPhaseNew,
|
||||
Cfg: cfg,
|
||||
},
|
||||
@ -104,22 +110,27 @@ func NewWrapper(
|
||||
healthNotifyCh: make(chan struct{}),
|
||||
handler: eventHandler,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
xl: xl,
|
||||
ctx: xlog.NewContext(ctx, xl),
|
||||
}
|
||||
|
||||
if baseInfo.HealthCheckType != "" {
|
||||
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||
pw.health = 1 // means failed
|
||||
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheckType, baseInfo.HealthCheckIntervalS,
|
||||
baseInfo.HealthCheckTimeoutS, baseInfo.HealthCheckMaxFailed, baseInfo.HealthCheckAddr,
|
||||
baseInfo.HealthCheckURL, pw.statusNormalCallback, pw.statusFailedCallback)
|
||||
xl.Trace("enable health check monitor")
|
||||
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
|
||||
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
|
||||
pw.statusNormalCallback, pw.statusFailedCallback)
|
||||
xl.Tracef("enable health check monitor")
|
||||
}
|
||||
|
||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter)
|
||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
||||
return pw
|
||||
}
|
||||
|
||||
func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||
pw.pxy.SetInWorkConnCallback(cb)
|
||||
}
|
||||
|
||||
func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
|
||||
pw.mu.Lock()
|
||||
defer pw.mu.Unlock()
|
||||
@ -132,7 +143,7 @@ func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
|
||||
pw.Phase = ProxyPhaseStartErr
|
||||
pw.Err = respErr
|
||||
pw.lastStartErr = time.Now()
|
||||
return fmt.Errorf(pw.Err)
|
||||
return fmt.Errorf("%s", pw.Err)
|
||||
}
|
||||
|
||||
if err := pw.pxy.Run(); err != nil {
|
||||
@ -192,7 +203,7 @@ func (pw *Wrapper) checkWorker() {
|
||||
(pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) ||
|
||||
(pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) {
|
||||
|
||||
xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart)
|
||||
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart)
|
||||
pw.Phase = ProxyPhaseWaitStart
|
||||
|
||||
var newProxyMsg msg.NewProxy
|
||||
@ -207,7 +218,7 @@ func (pw *Wrapper) checkWorker() {
|
||||
pw.mu.Lock()
|
||||
if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart {
|
||||
pw.close()
|
||||
xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed)
|
||||
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed)
|
||||
pw.Phase = ProxyPhaseCheckFailed
|
||||
}
|
||||
pw.mu.Unlock()
|
||||
@ -231,7 +242,7 @@ func (pw *Wrapper) statusNormalCallback() {
|
||||
default:
|
||||
}
|
||||
})
|
||||
xl.Info("health check success")
|
||||
xl.Infof("health check success")
|
||||
}
|
||||
|
||||
func (pw *Wrapper) statusFailedCallback() {
|
||||
@ -243,7 +254,7 @@ func (pw *Wrapper) statusFailedCallback() {
|
||||
default:
|
||||
}
|
||||
})
|
||||
xl.Info("health check failed")
|
||||
xl.Infof("health check failed")
|
||||
}
|
||||
|
||||
func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
|
||||
@ -252,7 +263,7 @@ func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
|
||||
pxy := pw.pxy
|
||||
pw.mu.RUnlock()
|
||||
if pxy != nil && pw.Phase == ProxyPhaseRunning {
|
||||
xl.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||
xl.Debugf("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||
go pxy.InWorkConn(workConn, m)
|
||||
} else {
|
||||
workConn.Close()
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
@ -25,29 +27,29 @@ import (
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
||||
}
|
||||
|
||||
type SUDPProxy struct {
|
||||
*BaseProxy
|
||||
|
||||
cfg *config.SUDPProxyConf
|
||||
cfg *v1.SUDPProxyConfig
|
||||
|
||||
localAddr *net.UDPAddr
|
||||
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
|
||||
unwrapped, ok := cfg.(*config.SUDPProxyConf)
|
||||
func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||
unwrapped, ok := cfg.(*v1.SUDPProxyConfig)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@ -77,9 +79,9 @@ func (pxy *SUDPProxy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
xl := pxy.xl
|
||||
xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
var err error
|
||||
@ -88,20 +90,18 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.cfg.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.UseCompression {
|
||||
var releaseFn func()
|
||||
rwc, releaseFn = libio.WithCompressionFromPool(rwc)
|
||||
defer releaseFn()
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
|
||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
workConn := conn
|
||||
readCh := make(chan *msg.UDPPacket, 1024)
|
||||
@ -133,21 +133,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
// first to check sudp proxy is closed or not
|
||||
select {
|
||||
case <-pxy.closeCh:
|
||||
xl.Trace("frpc sudp proxy is closed")
|
||||
xl.Tracef("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)
|
||||
xl.Warnf("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)
|
||||
xl.Warnf("reader goroutine for sudp work connection closed: %v", errRet)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -157,21 +157,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
|
||||
defer func() {
|
||||
closeFn()
|
||||
xl.Info("writer goroutine for sudp work connection closed")
|
||||
xl.Infof("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]",
|
||||
xl.Tracef("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]",
|
||||
m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
|
||||
case *msg.Ping:
|
||||
xl.Trace("frpc send ping message to frpc visitor")
|
||||
xl.Tracef("frpc send ping message to frpc visitor")
|
||||
}
|
||||
|
||||
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
|
||||
xl.Error("sudp work write error: %v", errRet)
|
||||
xl.Errorf("sudp work write error: %v", errRet)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -191,11 +191,11 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
if errRet = errors.PanicToError(func() {
|
||||
sendCh <- &msg.Ping{}
|
||||
}); errRet != nil {
|
||||
xl.Warn("heartbeat goroutine for sudp work connection closed")
|
||||
xl.Warnf("heartbeat goroutine for sudp work connection closed")
|
||||
return
|
||||
}
|
||||
case <-pxy.closeCh:
|
||||
xl.Trace("frpc sudp proxy is closed")
|
||||
xl.Tracef("frpc sudp proxy is closed")
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
go workConnReaderFn(workConn, readCh)
|
||||
go heartbeatFn(sendCh)
|
||||
|
||||
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
|
||||
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
@ -24,21 +26,21 @@ import (
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
||||
}
|
||||
|
||||
type UDPProxy struct {
|
||||
*BaseProxy
|
||||
|
||||
cfg *config.UDPProxyConf
|
||||
cfg *v1.UDPProxyConfig
|
||||
|
||||
localAddr *net.UDPAddr
|
||||
readCh chan *msg.UDPPacket
|
||||
@ -49,8 +51,8 @@ type UDPProxy struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
|
||||
unwrapped, ok := cfg.(*config.UDPProxyConf)
|
||||
func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||
unwrapped, ok := cfg.(*v1.UDPProxyConfig)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@ -86,10 +88,10 @@ func (pxy *UDPProxy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
xl := pxy.xl
|
||||
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
|
||||
// close resources releated with old workConn
|
||||
xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
|
||||
// close resources related with old workConn
|
||||
pxy.Close()
|
||||
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
@ -99,20 +101,18 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.cfg.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.UseCompression {
|
||||
var releaseFn func()
|
||||
rwc, releaseFn = libio.WithCompressionFromPool(rwc)
|
||||
defer releaseFn()
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
|
||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
pxy.mu.Lock()
|
||||
pxy.workConn = conn
|
||||
@ -125,32 +125,32 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
for {
|
||||
var udpMsg msg.UDPPacket
|
||||
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
|
||||
xl.Warn("read from workConn for udp error: %v", errRet)
|
||||
xl.Warnf("read from workConn for udp error: %v", errRet)
|
||||
return
|
||||
}
|
||||
if errRet := errors.PanicToError(func() {
|
||||
xl.Trace("get udp package from workConn: %s", udpMsg.Content)
|
||||
xl.Tracef("get udp package from workConn: %s", udpMsg.Content)
|
||||
readCh <- &udpMsg
|
||||
}); errRet != nil {
|
||||
xl.Info("reader goroutine for udp work connection closed: %v", errRet)
|
||||
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
|
||||
defer func() {
|
||||
xl.Info("writer goroutine for udp work connection closed")
|
||||
xl.Infof("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)
|
||||
xl.Tracef("send udp package to workConn: %s", m.Content)
|
||||
case *msg.Ping:
|
||||
xl.Trace("send ping message to udp workConn")
|
||||
xl.Tracef("send ping message to udp workConn")
|
||||
}
|
||||
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
|
||||
xl.Error("udp work write error: %v", errRet)
|
||||
xl.Errorf("udp work write error: %v", errRet)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -162,7 +162,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
if errRet = errors.PanicToError(func() {
|
||||
sendCh <- &msg.Ping{}
|
||||
}); errRet != nil {
|
||||
xl.Trace("heartbeat goroutine for udp work connection closed")
|
||||
xl.Tracef("heartbeat goroutine for udp work connection closed")
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
||||
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
||||
go heartbeatFn(pxy.sendCh)
|
||||
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
|
||||
|
||||
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
|
||||
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
@ -23,25 +25,25 @@ import (
|
||||
fmux "github.com/hashicorp/yamux"
|
||||
"github.com/quic-go/quic-go"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
||||
}
|
||||
|
||||
type XTCPProxy struct {
|
||||
*BaseProxy
|
||||
|
||||
cfg *config.XTCPProxyConf
|
||||
cfg *v1.XTCPProxyConfig
|
||||
}
|
||||
|
||||
func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
|
||||
unwrapped, ok := cfg.(*config.XTCPProxyConf)
|
||||
func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||
unwrapped, ok := cfg.(*v1.XTCPProxyConfig)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@ -57,17 +59,25 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
var natHoleSidMsg msg.NatHoleSid
|
||||
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
|
||||
if err != nil {
|
||||
xl.Error("xtcp read from workConn error: %v", err)
|
||||
xl.Errorf("xtcp read from workConn error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Trace("nathole prepare start")
|
||||
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
|
||||
xl.Tracef("nathole prepare start")
|
||||
|
||||
// Prepare NAT traversal options
|
||||
var opts nathole.PrepareOptions
|
||||
if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs {
|
||||
opts.DisableAssistedAddrs = true
|
||||
}
|
||||
|
||||
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts)
|
||||
if err != nil {
|
||||
xl.Warn("nathole prepare error: %v", err)
|
||||
xl.Warnf("nathole prepare error: %v", err)
|
||||
return
|
||||
}
|
||||
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
|
||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||
defer prepareResult.ListenConn.Close()
|
||||
|
||||
@ -75,28 +85,28 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
transactionID := nathole.NewTransactionID()
|
||||
natHoleClientMsg := &msg.NatHoleClient{
|
||||
TransactionID: transactionID,
|
||||
ProxyName: pxy.cfg.ProxyName,
|
||||
ProxyName: pxy.cfg.Name,
|
||||
Sid: natHoleSidMsg.Sid,
|
||||
MappedAddrs: prepareResult.Addrs,
|
||||
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||
}
|
||||
|
||||
xl.Trace("nathole exchange info start")
|
||||
xl.Tracef("nathole exchange info start")
|
||||
natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
|
||||
if err != nil {
|
||||
xl.Warn("nathole exchange info error: %v", err)
|
||||
xl.Warnf("nathole exchange info error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||
xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
|
||||
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
|
||||
|
||||
listenConn := prepareResult.ListenConn
|
||||
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Sk))
|
||||
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey))
|
||||
if err != nil {
|
||||
listenConn.Close()
|
||||
xl.Warn("make hole error: %v", err)
|
||||
xl.Warnf("make hole error: %v", err)
|
||||
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
|
||||
Sid: natHoleRespMsg.Sid,
|
||||
Success: false,
|
||||
@ -104,7 +114,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
return
|
||||
}
|
||||
listenConn = newListenConn
|
||||
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||
xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||
|
||||
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
|
||||
Sid: natHoleRespMsg.Sid,
|
||||
@ -126,14 +136,14 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
|
||||
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
|
||||
lConn, err := net.DialUDP("udp", laddr, raddr)
|
||||
if err != nil {
|
||||
xl.Warn("dial udp error: %v", err)
|
||||
xl.Warnf("dial udp error: %v", err)
|
||||
return
|
||||
}
|
||||
defer lConn.Close()
|
||||
|
||||
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||
if err != nil {
|
||||
xl.Warn("create kcp connection from udp connection error: %v", err)
|
||||
xl.Warnf("create kcp connection from udp connection error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -143,7 +153,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
|
||||
fmuxCfg.LogOutput = io.Discard
|
||||
session, err := fmux.Server(remote, fmuxCfg)
|
||||
if err != nil {
|
||||
xl.Error("create mux session error: %v", err)
|
||||
xl.Errorf("create mux session error: %v", err)
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
@ -151,10 +161,10 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
|
||||
for {
|
||||
muxConn, err := session.Accept()
|
||||
if err != nil {
|
||||
xl.Error("accept connection error: %v", err)
|
||||
xl.Errorf("accept connection error: %v", err)
|
||||
return
|
||||
}
|
||||
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Sk))
|
||||
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey))
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,34 +174,34 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
|
||||
|
||||
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||
if err != nil {
|
||||
xl.Warn("create tls config error: %v", err)
|
||||
xl.Warnf("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,
|
||||
MaxIdleTimeout: time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||
MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams),
|
||||
KeepAlivePeriod: time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
xl.Warn("dial quic error: %v", err)
|
||||
xl.Warnf("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)
|
||||
xl.Errorf("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)
|
||||
xl.Debugf("quic accept stream error: %v", err)
|
||||
_ = c.CloseWithError(0, "")
|
||||
return
|
||||
}
|
||||
go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Sk))
|
||||
go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,264 +16,291 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"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/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/assets"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/pkg/util/wait"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
crypto.DefaultSalt = "frp"
|
||||
// TODO: remove this when we drop support for go1.19
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// Disable quic-go's receive buffer warning.
|
||||
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
|
||||
// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
|
||||
if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
|
||||
os.Setenv("QUIC_GO_DISABLE_ECN", "true")
|
||||
}
|
||||
}
|
||||
|
||||
// Service is a client service.
|
||||
type Service struct {
|
||||
// uniq id got from frps, attach it in loginMsg
|
||||
runID string
|
||||
|
||||
// manager control connection with server
|
||||
ctl *Control
|
||||
ctlMu sync.RWMutex
|
||||
|
||||
// Sets authentication based on selected method
|
||||
authSetter auth.Setter
|
||||
|
||||
cfg config.ClientCommonConf
|
||||
pxyCfgs map[string]config.ProxyConf
|
||||
visitorCfgs map[string]config.VisitorConf
|
||||
cfgMu sync.RWMutex
|
||||
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
cfgFile string
|
||||
|
||||
exit uint32 // 0 means not exit
|
||||
|
||||
// service context
|
||||
ctx context.Context
|
||||
// call cancel to stop service
|
||||
cancel context.CancelFunc
|
||||
type cancelErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func NewService(
|
||||
cfg config.ClientCommonConf,
|
||||
pxyCfgs map[string]config.ProxyConf,
|
||||
visitorCfgs map[string]config.VisitorConf,
|
||||
cfgFile string,
|
||||
) (svr *Service, err error) {
|
||||
svr = &Service{
|
||||
authSetter: auth.NewAuthSetter(cfg.ClientConfig),
|
||||
cfg: cfg,
|
||||
cfgFile: cfgFile,
|
||||
pxyCfgs: pxyCfgs,
|
||||
visitorCfgs: visitorCfgs,
|
||||
ctx: context.Background(),
|
||||
exit: 0,
|
||||
}
|
||||
return
|
||||
func (e cancelErr) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (svr *Service) GetController() *Control {
|
||||
svr.ctlMu.RLock()
|
||||
defer svr.ctlMu.RUnlock()
|
||||
return svr.ctl
|
||||
// ServiceOptions contains options for creating a new client service.
|
||||
type ServiceOptions struct {
|
||||
Common *v1.ClientCommonConfig
|
||||
ProxyCfgs []v1.ProxyConfigurer
|
||||
VisitorCfgs []v1.VisitorConfigurer
|
||||
|
||||
UnsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||
// If it is empty, it means that the configuration file is not used for initialization.
|
||||
// It may be initialized using command line parameters or called directly.
|
||||
ConfigFilePath string
|
||||
|
||||
// ClientSpec is the client specification that control the client behavior.
|
||||
ClientSpec *msg.ClientSpec
|
||||
|
||||
// ConnectorCreator is a function that creates a new connector to make connections to the server.
|
||||
// The Connector shields the underlying connection details, whether it is through TCP or QUIC connection,
|
||||
// and regardless of whether multiplexing is used.
|
||||
//
|
||||
// If it is not set, the default frpc connector will be used.
|
||||
// By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps
|
||||
// through a pipe instead of a real physical connection.
|
||||
ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
||||
|
||||
// HandleWorkConnCb is a callback function that is called when a new work connection is created.
|
||||
//
|
||||
// If it is not set, the default frpc implementation will be used.
|
||||
HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||
}
|
||||
|
||||
func (svr *Service) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
svr.ctx = xlog.NewContext(ctx, xlog.New())
|
||||
svr.cancel = cancel
|
||||
|
||||
xl := xlog.FromContextSafe(svr.ctx)
|
||||
|
||||
// set custom DNSServer
|
||||
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)
|
||||
},
|
||||
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
||||
func setServiceOptionsDefault(options *ServiceOptions) error {
|
||||
if options.Common != nil {
|
||||
if err := options.Common.Complete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// login to frps
|
||||
for {
|
||||
conn, cm, err := svr.login()
|
||||
if err != nil {
|
||||
xl.Warn("login to server failed: %v", err)
|
||||
|
||||
// if login_fail_exit is true, just exit this program
|
||||
// otherwise sleep a while and try again to connect to server
|
||||
if svr.cfg.LoginFailExit {
|
||||
return err
|
||||
}
|
||||
util.RandomSleep(5*time.Second, 0.9, 1.1)
|
||||
} else {
|
||||
// login success
|
||||
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
|
||||
ctl.Run()
|
||||
svr.ctlMu.Lock()
|
||||
svr.ctl = ctl
|
||||
svr.ctlMu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
go svr.keepControllerWorking()
|
||||
|
||||
if svr.cfg.AdminPort != 0 {
|
||||
// Init admin server assets
|
||||
assets.Load(svr.cfg.AssetsDir)
|
||||
|
||||
address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort))
|
||||
err := svr.RunAdminServer(address)
|
||||
if err != nil {
|
||||
log.Warn("run admin server error: %v", err)
|
||||
}
|
||||
log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort)
|
||||
}
|
||||
<-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()
|
||||
if options.ConnectorCreator == nil {
|
||||
options.ConnectorCreator = NewConnector
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svr *Service) keepControllerWorking() {
|
||||
xl := xlog.FromContextSafe(svr.ctx)
|
||||
maxDelayTime := 20 * time.Second
|
||||
delayTime := time.Second
|
||||
// Service is the client service that connects to frps and provides proxy services.
|
||||
type Service struct {
|
||||
ctlMu sync.RWMutex
|
||||
// manager control connection with server
|
||||
ctl *Control
|
||||
// Uniq id got from frps, it will be attached to loginMsg.
|
||||
runID string
|
||||
|
||||
// if frpc reconnect frps, we need to limit retry times in 1min
|
||||
// current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ...
|
||||
// when exceed 1min, we will reset delay and counts
|
||||
cutoffTime := time.Now().Add(time.Minute)
|
||||
reconnectDelay := time.Second
|
||||
reconnectCounts := 1
|
||||
// Auth runtime and encryption materials
|
||||
auth *auth.ClientAuth
|
||||
|
||||
for {
|
||||
<-svr.ctl.ClosedDoneCh()
|
||||
if atomic.LoadUint32(&svr.exit) != 0 {
|
||||
return
|
||||
}
|
||||
// web server for admin UI and apis
|
||||
webServer *httppkg.Server
|
||||
|
||||
// the first three attempts with a low delay
|
||||
if reconnectCounts > 3 {
|
||||
util.RandomSleep(reconnectDelay, 0.9, 1.1)
|
||||
xl.Info("wait %v to reconnect", reconnectDelay)
|
||||
reconnectDelay *= 2
|
||||
} else {
|
||||
util.RandomSleep(time.Second, 0, 0.5)
|
||||
}
|
||||
reconnectCounts++
|
||||
vnetController *vnet.Controller
|
||||
|
||||
now := time.Now()
|
||||
if now.After(cutoffTime) {
|
||||
// reset
|
||||
cutoffTime = now.Add(time.Minute)
|
||||
reconnectDelay = time.Second
|
||||
reconnectCounts = 1
|
||||
}
|
||||
cfgMu sync.RWMutex
|
||||
common *v1.ClientCommonConfig
|
||||
proxyCfgs []v1.ProxyConfigurer
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
clientSpec *msg.ClientSpec
|
||||
|
||||
for {
|
||||
if atomic.LoadUint32(&svr.exit) != 0 {
|
||||
return
|
||||
}
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
xl.Info("try to reconnect to server...")
|
||||
conn, cm, err := svr.login()
|
||||
if err != nil {
|
||||
xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
|
||||
util.RandomSleep(delayTime, 0.9, 1.1)
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
configFilePath string
|
||||
|
||||
delayTime *= 2
|
||||
if delayTime > maxDelayTime {
|
||||
delayTime = maxDelayTime
|
||||
}
|
||||
continue
|
||||
}
|
||||
// reconnect success, init delayTime
|
||||
delayTime = time.Second
|
||||
// service context
|
||||
ctx context.Context
|
||||
// call cancel to stop service
|
||||
cancel context.CancelCauseFunc
|
||||
gracefulShutdownDuration time.Duration
|
||||
|
||||
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
|
||||
ctl.Run()
|
||||
svr.ctlMu.Lock()
|
||||
if svr.ctl != nil {
|
||||
svr.ctl.Close()
|
||||
}
|
||||
svr.ctl = ctl
|
||||
svr.ctlMu.Unlock()
|
||||
break
|
||||
}
|
||||
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
||||
handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||
}
|
||||
|
||||
func NewService(options ServiceOptions) (*Service, error) {
|
||||
if err := setServiceOptionsDefault(&options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var webServer *httppkg.Server
|
||||
if options.Common.WebServer.Port > 0 {
|
||||
ws, err := httppkg.NewServer(options.Common.WebServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webServer = ws
|
||||
}
|
||||
|
||||
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Service{
|
||||
ctx: context.Background(),
|
||||
auth: authRuntime,
|
||||
webServer: webServer,
|
||||
common: options.Common,
|
||||
configFilePath: options.ConfigFilePath,
|
||||
unsafeFeatures: options.UnsafeFeatures,
|
||||
proxyCfgs: options.ProxyCfgs,
|
||||
visitorCfgs: options.VisitorCfgs,
|
||||
clientSpec: options.ClientSpec,
|
||||
connectorCreator: options.ConnectorCreator,
|
||||
handleWorkConnCb: options.HandleWorkConnCb,
|
||||
}
|
||||
if webServer != nil {
|
||||
webServer.RouteRegister(s.registerRouteHandlers)
|
||||
}
|
||||
if options.Common.VirtualNet.Address != "" {
|
||||
s.vnetController = vnet.NewController(options.Common.VirtualNet)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (svr *Service) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
|
||||
svr.cancel = cancel
|
||||
|
||||
// set custom DNSServer
|
||||
if svr.common.DNSServer != "" {
|
||||
netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
|
||||
}
|
||||
|
||||
if svr.vnetController != nil {
|
||||
if err := svr.vnetController.Init(); err != nil {
|
||||
log.Errorf("init virtual network controller error: %v", err)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
log.Infof("virtual network controller start...")
|
||||
if err := svr.vnetController.Run(); err != nil {
|
||||
log.Warnf("virtual network controller exit with error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if svr.webServer != nil {
|
||||
go func() {
|
||||
log.Infof("admin server listen on %s", svr.webServer.Address())
|
||||
if err := svr.webServer.Run(); err != nil {
|
||||
log.Warnf("admin server exit with error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// first login to frps
|
||||
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
|
||||
if svr.ctl == nil {
|
||||
cancelCause := cancelErr{}
|
||||
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
||||
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
||||
}
|
||||
|
||||
go svr.keepControllerWorking()
|
||||
|
||||
<-svr.ctx.Done()
|
||||
svr.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svr *Service) keepControllerWorking() {
|
||||
<-svr.ctl.Done()
|
||||
|
||||
// There is a situation where the login is successful but due to certain reasons,
|
||||
// the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
|
||||
// The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
|
||||
// The maximum interval is 20 seconds.
|
||||
wait.BackoffUntil(func() (bool, error) {
|
||||
// loopLoginUntilSuccess is another layer of loop that will continuously attempt to
|
||||
// login to the server until successful.
|
||||
svr.loopLoginUntilSuccess(20*time.Second, false)
|
||||
if svr.ctl != nil {
|
||||
<-svr.ctl.Done()
|
||||
return false, errors.New("control is closed and try another loop")
|
||||
}
|
||||
// If the control is nil, it means that the login failed and the service is also closed.
|
||||
return false, nil
|
||||
}, wait.NewFastBackoffManager(
|
||||
wait.FastBackoffOptions{
|
||||
Duration: time.Second,
|
||||
Factor: 2,
|
||||
Jitter: 0.1,
|
||||
MaxDuration: 20 * time.Second,
|
||||
FastRetryCount: 3,
|
||||
FastRetryDelay: 200 * time.Millisecond,
|
||||
FastRetryWindow: time.Minute,
|
||||
FastRetryJitter: 0.5,
|
||||
},
|
||||
), true, svr.ctx.Done())
|
||||
}
|
||||
|
||||
// login creates a connection to frps and registers it self as a client
|
||||
// conn: control connection
|
||||
// session: if it's not nil, using tcp mux
|
||||
func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
|
||||
func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
||||
xl := xlog.FromContextSafe(svr.ctx)
|
||||
cm = NewConnectionManager(svr.ctx, &svr.cfg)
|
||||
|
||||
if err = cm.OpenConnection(); err != nil {
|
||||
connector = svr.connectorCreator(svr.ctx, svr.common)
|
||||
if err = connector.Open(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cm.Close()
|
||||
connector.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err = cm.Connect()
|
||||
conn, err = connector.Connect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
loginMsg := &msg.Login{
|
||||
Arch: runtime.GOARCH,
|
||||
Os: runtime.GOOS,
|
||||
PoolCount: svr.cfg.PoolCount,
|
||||
User: svr.cfg.User,
|
||||
Hostname: hostname,
|
||||
PoolCount: svr.common.Transport.PoolCount,
|
||||
User: svr.common.User,
|
||||
ClientID: svr.common.ClientID,
|
||||
Version: version.Full(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
RunID: svr.runID,
|
||||
Metas: svr.cfg.Metas,
|
||||
Metas: svr.common.Metadatas,
|
||||
}
|
||||
if svr.clientSpec != nil {
|
||||
loginMsg.ClientSpec = *svr.clientSpec
|
||||
}
|
||||
|
||||
// Add auth
|
||||
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
||||
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -290,21 +317,79 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
|
||||
|
||||
if loginRespMsg.Error != "" {
|
||||
err = fmt.Errorf("%s", loginRespMsg.Error)
|
||||
xl.Error("%s", loginRespMsg.Error)
|
||||
xl.Errorf("%s", loginRespMsg.Error)
|
||||
return
|
||||
}
|
||||
|
||||
svr.runID = loginRespMsg.RunID
|
||||
xl.ResetPrefixes()
|
||||
xl.AppendPrefix(svr.runID)
|
||||
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
||||
|
||||
xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
|
||||
xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
|
||||
return
|
||||
}
|
||||
|
||||
func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
|
||||
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
||||
xl := xlog.FromContextSafe(svr.ctx)
|
||||
|
||||
loginFunc := func() (bool, error) {
|
||||
xl.Infof("try to connect to server...")
|
||||
conn, connector, err := svr.login()
|
||||
if err != nil {
|
||||
xl.Warnf("connect to server error: %v", err)
|
||||
if firstLoginExit {
|
||||
svr.cancel(cancelErr{Err: err})
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
svr.cfgMu.RLock()
|
||||
proxyCfgs := svr.proxyCfgs
|
||||
visitorCfgs := svr.visitorCfgs
|
||||
svr.cfgMu.RUnlock()
|
||||
|
||||
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
|
||||
|
||||
sessionCtx := &SessionContext{
|
||||
Common: svr.common,
|
||||
RunID: svr.runID,
|
||||
Conn: conn,
|
||||
ConnEncrypted: connEncrypted,
|
||||
Auth: svr.auth,
|
||||
Connector: connector,
|
||||
VnetController: svr.vnetController,
|
||||
}
|
||||
ctl, err := NewControl(svr.ctx, sessionCtx)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Errorf("new control error: %v", err)
|
||||
return false, err
|
||||
}
|
||||
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
|
||||
|
||||
ctl.Run(proxyCfgs, visitorCfgs)
|
||||
// close and replace previous control
|
||||
svr.ctlMu.Lock()
|
||||
if svr.ctl != nil {
|
||||
svr.ctl.Close()
|
||||
}
|
||||
svr.ctl = ctl
|
||||
svr.ctlMu.Unlock()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// try to reconnect to server until success
|
||||
wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
|
||||
wait.FastBackoffOptions{
|
||||
Duration: time.Second,
|
||||
Factor: 2,
|
||||
Jitter: 0.1,
|
||||
MaxDuration: maxInterval,
|
||||
}), true, svr.ctx.Done())
|
||||
}
|
||||
|
||||
func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
||||
svr.cfgMu.Lock()
|
||||
svr.pxyCfgs = pxyCfgs
|
||||
svr.proxyCfgs = proxyCfgs
|
||||
svr.visitorCfgs = visitorCfgs
|
||||
svr.cfgMu.Unlock()
|
||||
|
||||
@ -313,7 +398,7 @@ func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs
|
||||
svr.ctlMu.RUnlock()
|
||||
|
||||
if ctl != nil {
|
||||
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
|
||||
return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -323,188 +408,48 @@ func (svr *Service) Close() {
|
||||
}
|
||||
|
||||
func (svr *Service) GracefulClose(d time.Duration) {
|
||||
atomic.StoreUint32(&svr.exit, 1)
|
||||
svr.gracefulShutdownDuration = d
|
||||
svr.cancel(nil)
|
||||
}
|
||||
|
||||
svr.ctlMu.RLock()
|
||||
func (svr *Service) stop() {
|
||||
svr.ctlMu.Lock()
|
||||
defer svr.ctlMu.Unlock()
|
||||
if svr.ctl != nil {
|
||||
svr.ctl.GracefulClose(d)
|
||||
svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
|
||||
svr.ctl = nil
|
||||
}
|
||||
if svr.webServer != nil {
|
||||
svr.webServer.Close()
|
||||
svr.webServer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||
svr.ctlMu.RLock()
|
||||
ctl := svr.ctl
|
||||
svr.ctlMu.RUnlock()
|
||||
|
||||
if svr.cancel != nil {
|
||||
svr.cancel()
|
||||
if ctl == nil {
|
||||
return nil, false
|
||||
}
|
||||
return ctl.pm.GetProxyStatus(name)
|
||||
}
|
||||
|
||||
func (svr *Service) StatusExporter() StatusExporter {
|
||||
return &statusExporterImpl{
|
||||
getProxyStatusFunc: svr.getProxyStatus,
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectionManager struct {
|
||||
ctx context.Context
|
||||
cfg *config.ClientCommonConf
|
||||
|
||||
muxSession *fmux.Session
|
||||
quicConn quic.Connection
|
||||
type StatusExporter interface {
|
||||
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
|
||||
}
|
||||
|
||||
func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager {
|
||||
return &ConnectionManager{
|
||||
ctx: ctx,
|
||||
cfg: cfg,
|
||||
}
|
||||
type statusExporterImpl struct {
|
||||
getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
|
||||
}
|
||||
|
||||
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
|
||||
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||
return s.getProxyStatusFunc(name)
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
@ -22,7 +23,7 @@ import (
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
@ -31,7 +32,7 @@ import (
|
||||
type STCPVisitor struct {
|
||||
*BaseVisitor
|
||||
|
||||
cfg *config.STCPVisitorConf
|
||||
cfg *v1.STCPVisitorConfig
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) Run() (err error) {
|
||||
@ -44,6 +45,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
||||
}
|
||||
|
||||
go sv.internalConnWorker()
|
||||
|
||||
if sv.plugin != nil {
|
||||
sv.plugin.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -56,7 +61,7 @@ func (sv *STCPVisitor) worker() {
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("stcp local listener closed")
|
||||
xl.Warnf("stcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
@ -68,7 +73,7 @@ func (sv *STCPVisitor) internalConnWorker() {
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("stcp internal listener closed")
|
||||
xl.Warnf("stcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
@ -77,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() {
|
||||
|
||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
defer userConn.Close()
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
// If there was an error and connection supports CloseWithError, use it
|
||||
if tunnelErr != nil {
|
||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||
_ = eConn.CloseWithError(tunnelErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}()
|
||||
|
||||
xl.Debug("get a new stcp user connection")
|
||||
xl.Debugf("get a new stcp user connection")
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
if err != nil {
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
defer visitorConn.Close()
|
||||
@ -90,14 +106,15 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.UseEncryption,
|
||||
UseCompression: sv.cfg.UseCompression,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
UseCompression: sv.cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
xl.Warn("send newVisitorConnMsg to server error: %v", err)
|
||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
|
||||
@ -105,30 +122,33 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
xl.Warn("get newVisitorConnRespMsg error: %v", err)
|
||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
||||
return
|
||||
}
|
||||
|
||||
var remote io.ReadWriteCloser
|
||||
remote = visitorConn
|
||||
if sv.cfg.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if sv.cfg.UseCompression {
|
||||
var releaseFn func()
|
||||
remote, releaseFn = libio.WithCompressionFromPool(remote)
|
||||
defer releaseFn()
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
var recycleFn func()
|
||||
remote, recycleFn = libio.WithCompressionFromPool(remote)
|
||||
defer recycleFn()
|
||||
}
|
||||
|
||||
libio.Join(userConn, remote)
|
||||
|
||||
@ -25,10 +25,10 @@ import (
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
@ -42,7 +42,7 @@ type SUDPVisitor struct {
|
||||
readCh chan *msg.UDPPacket
|
||||
sendCh chan *msg.UDPPacket
|
||||
|
||||
cfg *config.SUDPVisitorConf
|
||||
cfg *v1.SUDPVisitorConfig
|
||||
}
|
||||
|
||||
// SUDP Run start listen a udp port
|
||||
@ -62,7 +62,7 @@ func (sv *SUDPVisitor) Run() (err error) {
|
||||
sv.sendCh = make(chan *msg.UDPPacket, 1024)
|
||||
sv.readCh = make(chan *msg.UDPPacket, 1024)
|
||||
|
||||
xl.Info("sudp start to work, listen on %s", addr)
|
||||
xl.Infof("sudp start to work, listen on %s", addr)
|
||||
|
||||
go sv.dispatcher()
|
||||
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize))
|
||||
@ -84,17 +84,17 @@ func (sv *SUDPVisitor) dispatcher() {
|
||||
select {
|
||||
case firstPacket = <-sv.sendCh:
|
||||
if firstPacket == nil {
|
||||
xl.Info("frpc sudp visitor proxy is closed")
|
||||
xl.Infof("frpc sudp visitor proxy is closed")
|
||||
return
|
||||
}
|
||||
case <-sv.checkCloseCh:
|
||||
xl.Info("frpc sudp visitor proxy is closed")
|
||||
xl.Infof("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)
|
||||
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ func (sv *SUDPVisitor) dispatcher() {
|
||||
|
||||
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
xl.Debug("starting sudp proxy worker")
|
||||
xl.Debugf("starting sudp proxy worker")
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
@ -134,21 +134,21 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
|
||||
_ = 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)
|
||||
xl.Warnf("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")
|
||||
xl.Debugf("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)
|
||||
xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content)
|
||||
}); errRet != nil {
|
||||
xl.Info("reader goroutine for udp work connection closed")
|
||||
xl.Infof("reader goroutine for udp work connection closed")
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -165,25 +165,25 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||
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)
|
||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||
return
|
||||
}
|
||||
xl.Trace("send udp package to workConn: %s", firstPacket.Content)
|
||||
xl.Tracef("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")
|
||||
xl.Infof("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)
|
||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||
return
|
||||
}
|
||||
xl.Trace("send udp package to workConn: %s", udpMsg.Content)
|
||||
xl.Tracef("send udp package to workConn: %s", udpMsg.Content)
|
||||
case <-closeCh:
|
||||
return
|
||||
}
|
||||
@ -194,7 +194,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||
go workConnSenderFn(workConn)
|
||||
|
||||
wg.Wait()
|
||||
xl.Info("sudp worker is closed")
|
||||
xl.Infof("sudp worker is closed")
|
||||
}
|
||||
|
||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||
@ -208,10 +208,10 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.UseEncryption,
|
||||
UseCompression: sv.cfg.UseCompression,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
UseCompression: sv.cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
@ -232,17 +232,17 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||
|
||||
var remote io.ReadWriteCloser
|
||||
remote = visitorConn
|
||||
if sv.cfg.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if sv.cfg.UseCompression {
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
remote = libio.WithCompression(remote)
|
||||
}
|
||||
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
||||
return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
||||
}
|
||||
|
||||
func (sv *SUDPVisitor) Close() {
|
||||
|
||||
@ -19,13 +19,15 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
// Helper wrapps some functions for visitor to use.
|
||||
// Helper wraps some functions for visitor to use.
|
||||
type Helper interface {
|
||||
// ConnectServer directly connects to the frp server.
|
||||
ConnectServer() (net.Conn, error)
|
||||
@ -34,6 +36,8 @@ type Helper interface {
|
||||
// MsgTransporter returns the message transporter that is used to send and receive messages
|
||||
// to the frp server through the controller.
|
||||
MsgTransporter() transport.MessageTransporter
|
||||
// VNetController returns the vnet controller that is used to manage the virtual network.
|
||||
VNetController() *vnet.Controller
|
||||
// RunID returns the run id of current controller.
|
||||
RunID() string
|
||||
}
|
||||
@ -47,44 +51,65 @@ type Visitor interface {
|
||||
|
||||
func NewVisitor(
|
||||
ctx context.Context,
|
||||
cfg config.VisitorConf,
|
||||
clientCfg config.ClientCommonConf,
|
||||
cfg v1.VisitorConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
helper Helper,
|
||||
) (visitor Visitor) {
|
||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().ProxyName)
|
||||
) (Visitor, error) {
|
||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
|
||||
ctx = xlog.NewContext(ctx, xl)
|
||||
var visitor Visitor
|
||||
baseVisitor := BaseVisitor{
|
||||
clientCfg: clientCfg,
|
||||
helper: helper,
|
||||
ctx: xlog.NewContext(ctx, xl),
|
||||
internalLn: utilnet.NewInternalListener(),
|
||||
ctx: ctx,
|
||||
internalLn: netpkg.NewInternalListener(),
|
||||
}
|
||||
if cfg.GetBaseConfig().Plugin.Type != "" {
|
||||
p, err := plugin.Create(
|
||||
cfg.GetBaseConfig().Plugin.Type,
|
||||
plugin.PluginContext{
|
||||
Name: cfg.GetBaseConfig().Name,
|
||||
Ctx: ctx,
|
||||
VnetController: helper.VNetController(),
|
||||
SendConnToVisitor: func(conn net.Conn) {
|
||||
_ = baseVisitor.AcceptConn(conn)
|
||||
},
|
||||
},
|
||||
cfg.GetBaseConfig().Plugin.VisitorPluginOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseVisitor.plugin = p
|
||||
}
|
||||
switch cfg := cfg.(type) {
|
||||
case *config.STCPVisitorConf:
|
||||
case *v1.STCPVisitorConfig:
|
||||
visitor = &STCPVisitor{
|
||||
BaseVisitor: &baseVisitor,
|
||||
cfg: cfg,
|
||||
}
|
||||
case *config.XTCPVisitorConf:
|
||||
case *v1.XTCPVisitorConfig:
|
||||
visitor = &XTCPVisitor{
|
||||
BaseVisitor: &baseVisitor,
|
||||
cfg: cfg,
|
||||
startTunnelCh: make(chan struct{}),
|
||||
}
|
||||
case *config.SUDPVisitorConf:
|
||||
case *v1.SUDPVisitorConfig:
|
||||
visitor = &SUDPVisitor{
|
||||
BaseVisitor: &baseVisitor,
|
||||
cfg: cfg,
|
||||
checkCloseCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
return
|
||||
return visitor, nil
|
||||
}
|
||||
|
||||
type BaseVisitor struct {
|
||||
clientCfg config.ClientCommonConf
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
helper Helper
|
||||
l net.Listener
|
||||
internalLn *utilnet.InternalListener
|
||||
internalLn *netpkg.InternalListener
|
||||
plugin plugin.Plugin
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() {
|
||||
if v.internalLn != nil {
|
||||
v.internalLn.Close()
|
||||
}
|
||||
if v.plugin != nil {
|
||||
v.plugin.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,18 +22,22 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
clientCfg config.ClientCommonConf
|
||||
cfgs map[string]config.VisitorConf
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
cfgs map[string]v1.VisitorConfigurer
|
||||
visitors map[string]Visitor
|
||||
helper Helper
|
||||
|
||||
checkInterval time.Duration
|
||||
checkInterval time.Duration
|
||||
keepVisitorsRunningOnce sync.Once
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@ -44,13 +48,14 @@ type Manager struct {
|
||||
func NewManager(
|
||||
ctx context.Context,
|
||||
runID string,
|
||||
clientCfg config.ClientCommonConf,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
connectServer func() (net.Conn, error),
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) *Manager {
|
||||
m := &Manager{
|
||||
clientCfg: clientCfg,
|
||||
cfgs: make(map[string]config.VisitorConf),
|
||||
cfgs: make(map[string]v1.VisitorConfigurer),
|
||||
visitors: make(map[string]Visitor),
|
||||
checkInterval: 10 * time.Second,
|
||||
ctx: ctx,
|
||||
@ -59,13 +64,16 @@ func NewManager(
|
||||
m.helper = &visitorHelperImpl{
|
||||
connectServerFn: connectServer,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
transferConnFn: m.TransferConn,
|
||||
runID: runID,
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (vm *Manager) Run() {
|
||||
// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it.
|
||||
// It will only start after Reload is called and a new visitor is added.
|
||||
func (vm *Manager) keepVisitorsRunning() {
|
||||
xl := xlog.FromContextSafe(vm.ctx)
|
||||
|
||||
ticker := time.NewTicker(vm.checkInterval)
|
||||
@ -74,14 +82,14 @@ func (vm *Manager) Run() {
|
||||
for {
|
||||
select {
|
||||
case <-vm.stopCh:
|
||||
xl.Info("gracefully shutdown visitor manager")
|
||||
xl.Tracef("gracefully shutdown visitor manager")
|
||||
return
|
||||
case <-ticker.C:
|
||||
vm.mu.Lock()
|
||||
for _, cfg := range vm.cfgs {
|
||||
name := cfg.GetBaseConfig().ProxyName
|
||||
name := cfg.GetBaseConfig().Name
|
||||
if _, exist := vm.visitors[name]; !exist {
|
||||
xl.Info("try to start visitor [%s]", name)
|
||||
xl.Infof("try to start visitor [%s]", name)
|
||||
_ = vm.startVisitor(cfg)
|
||||
}
|
||||
}
|
||||
@ -104,29 +112,43 @@ func (vm *Manager) Close() {
|
||||
}
|
||||
|
||||
// Hold lock before calling this function.
|
||||
func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) {
|
||||
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
|
||||
xl := xlog.FromContextSafe(vm.ctx)
|
||||
name := cfg.GetBaseConfig().ProxyName
|
||||
visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||
name := cfg.GetBaseConfig().Name
|
||||
visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||
if err != nil {
|
||||
xl.Warnf("new visitor error: %v", err)
|
||||
return
|
||||
}
|
||||
err = visitor.Run()
|
||||
if err != nil {
|
||||
xl.Warn("start error: %v", err)
|
||||
xl.Warnf("start error: %v", err)
|
||||
} else {
|
||||
vm.visitors[name] = visitor
|
||||
xl.Info("start visitor success")
|
||||
xl.Infof("start visitor success")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
|
||||
func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
|
||||
if len(cfgs) > 0 {
|
||||
// Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
|
||||
vm.keepVisitorsRunningOnce.Do(func() {
|
||||
go vm.keepVisitorsRunning()
|
||||
})
|
||||
}
|
||||
|
||||
xl := xlog.FromContextSafe(vm.ctx)
|
||||
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
|
||||
return c.GetBaseConfig().Name
|
||||
})
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
|
||||
delNames := make([]string, 0)
|
||||
for name, oldCfg := range vm.cfgs {
|
||||
del := false
|
||||
cfg, ok := cfgs[name]
|
||||
cfg, ok := cfgsMap[name]
|
||||
if !ok || !reflect.DeepEqual(oldCfg, cfg) {
|
||||
del = true
|
||||
}
|
||||
@ -141,11 +163,12 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
|
||||
}
|
||||
}
|
||||
if len(delNames) > 0 {
|
||||
xl.Info("visitor removed: %v", delNames)
|
||||
xl.Infof("visitor removed: %v", delNames)
|
||||
}
|
||||
|
||||
addNames := make([]string, 0)
|
||||
for name, cfg := range cfgs {
|
||||
for _, cfg := range cfgs {
|
||||
name := cfg.GetBaseConfig().Name
|
||||
if _, ok := vm.cfgs[name]; !ok {
|
||||
vm.cfgs[name] = cfg
|
||||
addNames = append(addNames, name)
|
||||
@ -153,7 +176,7 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
|
||||
}
|
||||
}
|
||||
if len(addNames) > 0 {
|
||||
xl.Info("visitor added: %v", addNames)
|
||||
xl.Infof("visitor added: %v", addNames)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
||||
type visitorHelperImpl struct {
|
||||
connectServerFn func() (net.Conn, error)
|
||||
msgTransporter transport.MessageTransporter
|
||||
vnetController *vnet.Controller
|
||||
transferConnFn func(name string, conn net.Conn) error
|
||||
runID string
|
||||
}
|
||||
@ -187,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
|
||||
return v.msgTransporter
|
||||
}
|
||||
|
||||
func (v *visitorHelperImpl) VNetController() *vnet.Controller {
|
||||
return v.vnetController
|
||||
}
|
||||
|
||||
func (v *visitorHelperImpl) RunID() string {
|
||||
return v.runID
|
||||
}
|
||||
|
||||
@ -29,11 +29,11 @@ import (
|
||||
quic "github.com/quic-go/quic-go"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
@ -47,7 +47,7 @@ type XTCPVisitor struct {
|
||||
retryLimiter *rate.Limiter
|
||||
cancel context.CancelFunc
|
||||
|
||||
cfg *config.XTCPVisitorConf
|
||||
cfg *v1.XTCPVisitorConfig
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) Run() (err error) {
|
||||
@ -56,7 +56,7 @@ func (sv *XTCPVisitor) Run() (err error) {
|
||||
if sv.cfg.Protocol == "kcp" {
|
||||
sv.session = NewKCPTunnelSession()
|
||||
} else {
|
||||
sv.session = NewQUICTunnelSession(&sv.clientCfg)
|
||||
sv.session = NewQUICTunnelSession(sv.clientCfg)
|
||||
}
|
||||
|
||||
if sv.cfg.BindPort > 0 {
|
||||
@ -73,6 +73,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||
go sv.keepTunnelOpenWorker()
|
||||
}
|
||||
|
||||
if sv.plugin != nil {
|
||||
sv.plugin.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -93,7 +97,7 @@ func (sv *XTCPVisitor) worker() {
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("xtcp local listener closed")
|
||||
xl.Warnf("xtcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
@ -105,7 +109,7 @@ func (sv *XTCPVisitor) internalConnWorker() {
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("xtcp internal listener closed")
|
||||
xl.Warnf("xtcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
@ -140,14 +144,14 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||
case <-sv.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
xl.Debug("keepTunnelOpenWorker try to check tunnel...")
|
||||
conn, err := sv.getTunnelConn()
|
||||
xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
|
||||
conn, err := sv.getTunnelConn(sv.ctx)
|
||||
if err != nil {
|
||||
xl.Warn("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
||||
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
||||
_ = sv.retryLimiter.Wait(sv.ctx)
|
||||
continue
|
||||
}
|
||||
xl.Debug("keepTunnelOpenWorker check success")
|
||||
xl.Debugf("keepTunnelOpenWorker check success")
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
@ -157,18 +161,26 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||
|
||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
isConnTrasfered := false
|
||||
isConnTransferred := false
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
if !isConnTrasfered {
|
||||
if !isConnTransferred {
|
||||
// If there was an error and connection supports CloseWithError, use it
|
||||
if tunnelErr != nil {
|
||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||
_ = eConn.CloseWithError(tunnelErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
xl.Debug("get a new xtcp user connection")
|
||||
xl.Debugf("get a new xtcp user connection")
|
||||
|
||||
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
|
||||
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
|
||||
ctx := context.Background()
|
||||
ctx := sv.ctx
|
||||
if sv.cfg.FallbackTo != "" {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
@ -176,79 +188,79 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
}
|
||||
tunnelConn, err := sv.openTunnel(ctx)
|
||||
if err != nil {
|
||||
xl.Error("open tunnel error: %v", err)
|
||||
xl.Errorf("open tunnel error: %v", err)
|
||||
tunnelErr = err
|
||||
|
||||
// no fallback, just return
|
||||
if sv.cfg.FallbackTo == "" {
|
||||
return
|
||||
}
|
||||
|
||||
xl.Debug("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
|
||||
xl.Debugf("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
|
||||
if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil {
|
||||
xl.Error("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
||||
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
||||
return
|
||||
}
|
||||
isConnTrasfered = true
|
||||
isConnTransferred = true
|
||||
return
|
||||
}
|
||||
|
||||
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
|
||||
if sv.cfg.UseEncryption {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
if sv.cfg.UseCompression {
|
||||
var releaseFn func()
|
||||
muxConnRWCloser, releaseFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
||||
defer releaseFn()
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
var recycleFn func()
|
||||
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
||||
defer recycleFn()
|
||||
}
|
||||
|
||||
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||
xl.Debug("join connections closed")
|
||||
xl.Debugf("join connections closed")
|
||||
if len(errs) > 0 {
|
||||
xl.Trace("join connections errors: %v", errs)
|
||||
xl.Tracef("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()
|
||||
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
timeoutC := time.After(20 * time.Second)
|
||||
immediateTrigger := make(chan struct{}, 1)
|
||||
defer close(immediateTrigger)
|
||||
immediateTrigger <- struct{}{}
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sv.ctx.Done():
|
||||
return nil, sv.ctx.Err()
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-immediateTrigger:
|
||||
conn, err = sv.getTunnelConn()
|
||||
case <-ticker.C:
|
||||
conn, err = sv.getTunnelConn()
|
||||
case <-timeoutC:
|
||||
return nil, fmt.Errorf("open tunnel timeout")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err != ErrNoTunnelSession {
|
||||
xl.Warn("get tunnel connection error: %v", err)
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, fmt.Errorf("open tunnel timeout")
|
||||
}
|
||||
continue
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
conn, err = sv.getTunnelConn(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrNoTunnelSession) {
|
||||
xl.Warnf("get tunnel connection error: %v", err)
|
||||
}
|
||||
timer.Reset(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
|
||||
conn, err := sv.session.OpenConn(sv.ctx)
|
||||
func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
||||
conn, err := sv.session.OpenConn(ctx)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
@ -268,19 +280,27 @@ func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
|
||||
// 4. Create a tunnel session using an underlying UDP connection.
|
||||
func (sv *XTCPVisitor) makeNatHole() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
xl.Trace("makeNatHole start")
|
||||
xl.Tracef("makeNatHole start")
|
||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
|
||||
xl.Warn("nathole precheck error: %v", err)
|
||||
xl.Warnf("nathole precheck error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Trace("nathole prepare start")
|
||||
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
|
||||
xl.Tracef("nathole prepare start")
|
||||
|
||||
// Prepare NAT traversal options
|
||||
var opts nathole.PrepareOptions
|
||||
if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs {
|
||||
opts.DisableAssistedAddrs = true
|
||||
}
|
||||
|
||||
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts)
|
||||
if err != nil {
|
||||
xl.Warn("nathole prepare error: %v", err)
|
||||
xl.Warnf("nathole prepare error: %v", err)
|
||||
return
|
||||
}
|
||||
xl.Info("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
|
||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||
|
||||
listenConn := prepareResult.ListenConn
|
||||
@ -292,36 +312,36 @@ func (sv *XTCPVisitor) makeNatHole() {
|
||||
TransactionID: transactionID,
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
Protocol: sv.cfg.Protocol,
|
||||
SignKey: util.GetAuthKey(sv.cfg.Sk, now),
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
MappedAddrs: prepareResult.Addrs,
|
||||
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||
}
|
||||
|
||||
xl.Trace("nathole exchange info start")
|
||||
xl.Tracef("nathole exchange info start")
|
||||
natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second)
|
||||
if err != nil {
|
||||
listenConn.Close()
|
||||
xl.Warn("nathole exchange info error: %v", err)
|
||||
xl.Warnf("nathole exchange info error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Info("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||
xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
|
||||
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
|
||||
|
||||
newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.Sk))
|
||||
newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
listenConn.Close()
|
||||
xl.Warn("make hole error: %v", err)
|
||||
xl.Warnf("make hole error: %v", err)
|
||||
return
|
||||
}
|
||||
listenConn = newListenConn
|
||||
xl.Info("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||
xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||
|
||||
if err := sv.session.Init(listenConn, raddr); err != nil {
|
||||
listenConn.Close()
|
||||
xl.Warn("init tunnel session error: %v", err)
|
||||
xl.Warnf("init tunnel session error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -349,7 +369,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial udp error: %v", err)
|
||||
}
|
||||
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||
}
|
||||
@ -370,7 +390,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ks *KCPTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
|
||||
func (ks *KCPTunnelSession) OpenConn(_ context.Context) (net.Conn, error) {
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
session := ks.session
|
||||
@ -394,14 +414,14 @@ func (ks *KCPTunnelSession) Close() {
|
||||
}
|
||||
|
||||
type QUICTunnelSession struct {
|
||||
session quic.Connection
|
||||
session *quic.Conn
|
||||
listenConn *net.UDPConn
|
||||
mu sync.RWMutex
|
||||
|
||||
clientCfg *config.ClientCommonConf
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
}
|
||||
|
||||
func NewQUICTunnelSession(clientCfg *config.ClientCommonConf) TunnelSession {
|
||||
func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession {
|
||||
return &QUICTunnelSession{
|
||||
clientCfg: clientCfg,
|
||||
}
|
||||
@ -413,11 +433,11 @@ func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) e
|
||||
return fmt.Errorf("create tls config error: %v", err)
|
||||
}
|
||||
tlsConfig.NextProtos = []string{"frp"}
|
||||
quicConn, err := quic.Dial(listenConn, raddr, raddr.String(), tlsConfig,
|
||||
quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,
|
||||
&quic.Config{
|
||||
MaxIdleTimeout: time.Duration(qs.clientCfg.QUICMaxIdleTimeout) * time.Second,
|
||||
MaxIncomingStreams: int64(qs.clientCfg.QUICMaxIncomingStreams),
|
||||
KeepAlivePeriod: time.Duration(qs.clientCfg.QUICKeepalivePeriod) * time.Second,
|
||||
MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||
MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),
|
||||
KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial quic error: %v", err)
|
||||
@ -440,7 +460,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utilnet.QuicStreamToNetConn(stream, session), nil
|
||||
return netpkg.QuicStreamToNetConn(stream, session), nil
|
||||
}
|
||||
|
||||
func (qs *QUICTunnelSession) Close() {
|
||||
|
||||
@ -15,10 +15,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/fatedier/frp/assets/frpc"
|
||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
system.EnableCompatibilityMode()
|
||||
sub.Execute()
|
||||
}
|
||||
|
||||
125
cmd/frpc/sub/admin.go
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
||||
)
|
||||
|
||||
var adminAPITimeout = 30 * time.Second
|
||||
|
||||
func init() {
|
||||
commands := []struct {
|
||||
name string
|
||||
description string
|
||||
handler func(*v1.ClientCommonConfig) error
|
||||
}{
|
||||
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
|
||||
{"status", "Overview of all proxies status", StatusHandler},
|
||||
{"stop", "Stop the running frpc", StopHandler},
|
||||
}
|
||||
|
||||
for _, cmdConfig := range commands {
|
||||
cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler)
|
||||
cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls")
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: name,
|
||||
Short: short,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.WebServer.Port <= 0 {
|
||||
fmt.Println("web server port should be set if you want to use this feature")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := handler(cfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
if err := client.Reload(ctx, strictConfigMode); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("reload success")
|
||||
return nil
|
||||
}
|
||||
|
||||
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
res, err := client.GetAllProxyStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Proxy Status...\n\n")
|
||||
for _, typ := range proxyTypes {
|
||||
arrs := res[string(typ)]
|
||||
if len(arrs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(strings.ToUpper(string(typ)))
|
||||
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
|
||||
for _, ps := range arrs {
|
||||
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
|
||||
}
|
||||
tbl.Print()
|
||||
fmt.Println("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
if err := client.Stop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("stop success")
|
||||
return nil
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(httpCmd)
|
||||
|
||||
httpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
httpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
httpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
httpCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
|
||||
httpCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
|
||||
httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations")
|
||||
httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user")
|
||||
httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password")
|
||||
httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
|
||||
httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
httpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(httpCmd)
|
||||
}
|
||||
|
||||
var httpCmd = &cobra.Command{
|
||||
Use: "http",
|
||||
Short: "Run frpc with a single http proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &config.HTTPProxyConf{}
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.HTTPProxy
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.CustomDomains = strings.Split(customDomains, ",")
|
||||
cfg.SubDomain = subDomain
|
||||
cfg.Locations = strings.Split(locations, ",")
|
||||
cfg.HTTPUser = httpUser
|
||||
cfg.HTTPPwd = httpPwd
|
||||
cfg.HostHeaderRewrite = hostHeaderRewrite
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := map[string]config.ProxyConf{
|
||||
cfg.ProxyName: cfg,
|
||||
}
|
||||
err = startService(clientCfg, proxyConfs, nil, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(httpsCmd)
|
||||
|
||||
httpsCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
httpsCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
httpsCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
httpsCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
|
||||
httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
|
||||
httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(httpsCmd)
|
||||
}
|
||||
|
||||
var httpsCmd = &cobra.Command{
|
||||
Use: "https",
|
||||
Short: "Run frpc with a single https proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &config.HTTPSProxyConf{}
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.HTTPSProxy
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.CustomDomains = strings.Split(customDomains, ",")
|
||||
cfg.SubDomain = subDomain
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := map[string]config.ProxyConf{
|
||||
cfg.ProxyName: cfg,
|
||||
}
|
||||
err = startService(clientCfg, proxyConfs, nil, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
)
|
||||
|
||||
@ -30,8 +31,6 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(natholeCmd)
|
||||
|
||||
rootCmd.AddCommand(natholeCmd)
|
||||
natholeCmd.AddCommand(natholeDiscoveryCmd)
|
||||
|
||||
@ -49,9 +48,13 @@ var natholeDiscoveryCmd = &cobra.Command{
|
||||
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)
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
cfg = config.GetDefaultClientConf()
|
||||
cfg = &v1.ClientCommonConfig{}
|
||||
if err := cfg.Complete(); err != nil {
|
||||
fmt.Printf("failed to complete config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if natHoleSTUNServer != "" {
|
||||
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
||||
@ -89,7 +92,7 @@ var natholeDiscoveryCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func validateForNatHoleDiscovery(cfg config.ClientCommonConf) error {
|
||||
func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error {
|
||||
if cfg.NatHoleSTUNServer == "" {
|
||||
return fmt.Errorf("nat_hole_stun_server can not be empty")
|
||||
}
|
||||
|
||||
133
cmd/frpc/sub/proxy.go
Normal file
@ -0,0 +1,133 @@
|
||||
// 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"
|
||||
"slices"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
var proxyTypes = []v1.ProxyType{
|
||||
v1.ProxyTypeTCP,
|
||||
v1.ProxyTypeUDP,
|
||||
v1.ProxyTypeTCPMUX,
|
||||
v1.ProxyTypeHTTP,
|
||||
v1.ProxyTypeHTTPS,
|
||||
v1.ProxyTypeSTCP,
|
||||
v1.ProxyTypeSUDP,
|
||||
v1.ProxyTypeXTCP,
|
||||
}
|
||||
|
||||
var visitorTypes = []v1.VisitorType{
|
||||
v1.VisitorTypeSTCP,
|
||||
v1.VisitorTypeSUDP,
|
||||
v1.VisitorTypeXTCP,
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, typ := range proxyTypes {
|
||||
c := v1.NewProxyConfigurerByType(typ)
|
||||
if c == nil {
|
||||
panic("proxy type: " + typ + " not support")
|
||||
}
|
||||
clientCfg := v1.ClientCommonConfig{}
|
||||
cmd := NewProxyCommand(string(typ), c, &clientCfg)
|
||||
config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
|
||||
config.RegisterProxyFlags(cmd, c)
|
||||
|
||||
// add sub command for visitor
|
||||
if slices.Contains(visitorTypes, v1.VisitorType(typ)) {
|
||||
vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
|
||||
if vc == nil {
|
||||
panic("visitor type: " + typ + " not support")
|
||||
}
|
||||
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
|
||||
config.RegisterVisitorFlags(visitorCmd, vc)
|
||||
cmd.AddCommand(visitorCmd)
|
||||
}
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: name,
|
||||
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Complete(clientCfg.User)
|
||||
c.GetBaseConfig().Type = name
|
||||
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "visitor",
|
||||
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Complete(clientCfg)
|
||||
c.GetBaseConfig().Type = name
|
||||
if err := validation.ValidateVisitorConfigurer(c); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(reloadCmd)
|
||||
}
|
||||
|
||||
var reloadCmd = &cobra.Command{
|
||||
Use: "reload",
|
||||
Short: "Hot-Reload frpc configuration",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _, _, err := config.ParseClientConfig(cfgFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = reload(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("frpc reload error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("reload success\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func reload(clientCfg config.ClientCommonConf) error {
|
||||
if clientCfg.AdminPort == 0 {
|
||||
return fmt.Errorf("admin_port shoud be set if you want to use reload feature")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://"+
|
||||
clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/reload", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
|
||||
clientCfg.AdminPwd))
|
||||
|
||||
req.Header.Add("Authorization", authStr)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
@ -18,11 +18,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@ -30,75 +29,31 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
)
|
||||
|
||||
const (
|
||||
CfgFileTypeIni = iota
|
||||
CfgFileTypeCmd
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
showVersion bool
|
||||
|
||||
serverAddr string
|
||||
user string
|
||||
protocol string
|
||||
token string
|
||||
logLevel string
|
||||
logFile string
|
||||
logMaxDays int
|
||||
disableLogColor bool
|
||||
dnsServer string
|
||||
|
||||
proxyName string
|
||||
localIP string
|
||||
localPort int
|
||||
remotePort int
|
||||
useEncryption bool
|
||||
useCompression bool
|
||||
bandwidthLimit string
|
||||
bandwidthLimitMode string
|
||||
customDomains string
|
||||
subDomain string
|
||||
httpUser string
|
||||
httpPwd string
|
||||
locations string
|
||||
hostHeaderRewrite string
|
||||
role string
|
||||
sk string
|
||||
multiplexer string
|
||||
serverName string
|
||||
bindAddr string
|
||||
bindPort int
|
||||
|
||||
tlsEnable bool
|
||||
tlsServerName string
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
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(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||
|
||||
func RegisterCommonFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address")
|
||||
cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user")
|
||||
cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp, kcp, quic, websocket, wss")
|
||||
cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
|
||||
cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
|
||||
cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path")
|
||||
cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days")
|
||||
cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", 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")
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@ -110,23 +65,26 @@ var rootCmd = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
|
||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||
if cfgDir != "" {
|
||||
_ = runMultipleClients(cfgDir)
|
||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not show command usage here.
|
||||
err := runClient(cfgFile)
|
||||
err := runClient(cfgFile, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func runMultipleClients(cfgDir string) error {
|
||||
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
var wg sync.WaitGroup
|
||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
@ -136,7 +94,7 @@ func runMultipleClients(cfgDir string) error {
|
||||
time.Sleep(time.Millisecond)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := runClient(path)
|
||||
err := runClient(path, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||
}
|
||||
@ -148,6 +106,7 @@ func runMultipleClients(cfgDir string) error {
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -160,78 +119,61 @@ func handleTermSignal(svr *client.Service) {
|
||||
svr.GracefulClose(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
|
||||
cfg = config.GetDefaultClientConf()
|
||||
|
||||
ipStr, portStr, err := net.SplitHostPort(serverAddr)
|
||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid server_addr: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg.ServerAddr = ipStr
|
||||
cfg.ServerPort, err = strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid server_addr: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg.User = user
|
||||
cfg.Protocol = protocol
|
||||
cfg.LogLevel = logLevel
|
||||
cfg.LogFile = logFile
|
||||
cfg.LogMaxDays = int64(logMaxDays)
|
||||
cfg.DisableLogColor = disableLogColor
|
||||
cfg.DNSServer = dnsServer
|
||||
|
||||
// Only token authentication is supported in cmd mode
|
||||
cfg.ClientConfig = auth.GetDefaultClientConf()
|
||||
cfg.Token = token
|
||||
cfg.TLSEnable = tlsEnable
|
||||
cfg.TLSServerName = tlsServerName
|
||||
|
||||
cfg.Complete()
|
||||
if err = cfg.Validate(); err != nil {
|
||||
err = fmt.Errorf("parse config error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func runClient(cfgFilePath string) error {
|
||||
cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
|
||||
if isLegacyFormat {
|
||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
}
|
||||
|
||||
if len(cfg.FeatureGates) > 0 {
|
||||
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
func startService(
|
||||
cfg config.ClientCommonConf,
|
||||
pxyCfgs map[string]config.ProxyConf,
|
||||
visitorCfgs map[string]config.VisitorConf,
|
||||
cfg *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) (err error) {
|
||||
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
|
||||
cfg.LogMaxDays, cfg.DisableLogColor)
|
||||
) error {
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
log.Info("start frpc service for config file [%s]", cfgFile)
|
||||
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
|
||||
log.Infof("start frpc service for config file [%s]", cfgFile)
|
||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||
}
|
||||
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
svr, err := client.NewService(client.ServiceOptions{
|
||||
Common: cfg,
|
||||
ProxyCfgs: proxyCfgs,
|
||||
VisitorCfgs: visitorCfgs,
|
||||
UnsafeFeatures: unsafeFeatures,
|
||||
ConfigFilePath: cfgFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"
|
||||
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
||||
// Capture the exit signal if we use kcp or quic.
|
||||
if shouldGracefulClose {
|
||||
go handleTermSignal(svr)
|
||||
}
|
||||
|
||||
_ = svr.Run(context.Background())
|
||||
return
|
||||
return svr.Run(context.Background())
|
||||
}
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Overview of all proxies status",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _, _, err := config.ParseClientConfig(cfgFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = status(cfg); err != nil {
|
||||
fmt.Printf("frpc get status error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func status(clientCfg config.ClientCommonConf) error {
|
||||
if clientCfg.AdminPort == 0 {
|
||||
return fmt.Errorf("admin_port shoud be set if you want to get proxy status")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://"+
|
||||
clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/status", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
|
||||
clientCfg.AdminPwd))
|
||||
|
||||
req.Header.Add("Authorization", authStr)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("admin api status code [%d]", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res := make(client.StatusResp)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
fmt.Println("Proxy Status...")
|
||||
types := []string{"tcp", "udp", "tcpmux", "http", "https", "stcp", "sudp", "xtcp"}
|
||||
for _, pxyType := range types {
|
||||
arrs := res[pxyType]
|
||||
if len(arrs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(strings.ToUpper(pxyType))
|
||||
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
|
||||
for _, ps := range arrs {
|
||||
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
|
||||
}
|
||||
tbl.Print()
|
||||
fmt.Println("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(stcpCmd)
|
||||
|
||||
stcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
stcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
|
||||
stcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
|
||||
stcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
|
||||
stcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
stcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
stcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
|
||||
stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
|
||||
stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(stcpCmd)
|
||||
}
|
||||
|
||||
var stcpCmd = &cobra.Command{
|
||||
Use: "stcp",
|
||||
Short: "Run frpc with a single stcp proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := make(map[string]config.ProxyConf)
|
||||
visitorConfs := make(map[string]config.VisitorConf)
|
||||
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "server":
|
||||
cfg := &config.STCPProxyConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.STCPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
proxyConfs[cfg.ProxyName] = cfg
|
||||
case "visitor":
|
||||
cfg := &config.STCPVisitorConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.STCPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.ServerName = serverName
|
||||
cfg.BindAddr = bindAddr
|
||||
cfg.BindPort = bindPort
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
visitorConfs[cfg.ProxyName] = cfg
|
||||
default:
|
||||
fmt.Println("invalid role")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = startService(clientCfg, proxyConfs, visitorConfs, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"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)))
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(sudpCmd)
|
||||
|
||||
sudpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
sudpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
|
||||
sudpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
|
||||
sudpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
|
||||
sudpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
sudpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
sudpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
|
||||
sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
|
||||
sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(sudpCmd)
|
||||
}
|
||||
|
||||
var sudpCmd = &cobra.Command{
|
||||
Use: "sudp",
|
||||
Short: "Run frpc with a single sudp proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := make(map[string]config.ProxyConf)
|
||||
visitorConfs := make(map[string]config.VisitorConf)
|
||||
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "server":
|
||||
cfg := &config.SUDPProxyConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.SUDPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
proxyConfs[cfg.ProxyName] = cfg
|
||||
case "visitor":
|
||||
cfg := &config.SUDPVisitorConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.SUDPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.ServerName = serverName
|
||||
cfg.BindAddr = bindAddr
|
||||
cfg.BindPort = bindPort
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
visitorConfs[cfg.ProxyName] = cfg
|
||||
default:
|
||||
fmt.Println("invalid role")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = startService(clientCfg, proxyConfs, visitorConfs, "")
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(tcpCmd)
|
||||
|
||||
tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
|
||||
tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(tcpCmd)
|
||||
}
|
||||
|
||||
var tcpCmd = &cobra.Command{
|
||||
Use: "tcp",
|
||||
Short: "Run frpc with a single tcp proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &config.TCPProxyConf{}
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.TCPProxy
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.RemotePort = remotePort
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := map[string]config.ProxyConf{
|
||||
cfg.ProxyName: cfg,
|
||||
}
|
||||
err = startService(clientCfg, proxyConfs, nil, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
// Copyright 2020 guylewin, guy@lewin.co.il
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(tcpMuxCmd)
|
||||
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer")
|
||||
tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(tcpMuxCmd)
|
||||
}
|
||||
|
||||
var tcpMuxCmd = &cobra.Command{
|
||||
Use: "tcpmux",
|
||||
Short: "Run frpc with a single tcpmux proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &config.TCPMuxProxyConf{}
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.TCPMuxProxy
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.CustomDomains = strings.Split(customDomains, ",")
|
||||
cfg.SubDomain = subDomain
|
||||
cfg.Multiplexer = multiplexer
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := map[string]config.ProxyConf{
|
||||
cfg.ProxyName: cfg,
|
||||
}
|
||||
err = startService(clientCfg, proxyConfs, nil, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(udpCmd)
|
||||
|
||||
udpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
udpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
udpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
|
||||
udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
udpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(udpCmd)
|
||||
}
|
||||
|
||||
var udpCmd = &cobra.Command{
|
||||
Use: "udp",
|
||||
Short: "Run frpc with a single udp proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &config.UDPProxyConf{}
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.UDPProxy
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.RemotePort = remotePort
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := map[string]config.ProxyConf{
|
||||
cfg.ProxyName: cfg,
|
||||
}
|
||||
err = startService(clientCfg, proxyConfs, nil, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -21,6 +21,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -31,7 +33,21 @@ var verifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify that the configures is valid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, _, _, err := config.ParseClientConfig(cfgFile)
|
||||
if cfgFile == "" {
|
||||
fmt.Println("frpc: the configuration file is not specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommonFlags(xtcpCmd)
|
||||
|
||||
xtcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
xtcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
|
||||
xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
|
||||
xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
|
||||
xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
|
||||
xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
|
||||
rootCmd.AddCommand(xtcpCmd)
|
||||
}
|
||||
|
||||
var xtcpCmd = &cobra.Command{
|
||||
Use: "xtcp",
|
||||
Short: "Run frpc with a single xtcp proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCfg, err := parseClientCommonCfgFromCmd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxyConfs := make(map[string]config.ProxyConf)
|
||||
visitorConfs := make(map[string]config.VisitorConf)
|
||||
|
||||
var prefix string
|
||||
if user != "" {
|
||||
prefix = user + "."
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "server":
|
||||
cfg := &config.XTCPProxyConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.XTCPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.LocalIP = localIP
|
||||
cfg.LocalPort = localPort
|
||||
cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.BandwidthLimitMode = bandwidthLimitMode
|
||||
err = cfg.ValidateForClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
proxyConfs[cfg.ProxyName] = cfg
|
||||
case "visitor":
|
||||
cfg := &config.XTCPVisitorConf{}
|
||||
cfg.ProxyName = prefix + proxyName
|
||||
cfg.ProxyType = consts.XTCPProxy
|
||||
cfg.UseEncryption = useEncryption
|
||||
cfg.UseCompression = useCompression
|
||||
cfg.Role = role
|
||||
cfg.Sk = sk
|
||||
cfg.ServerName = serverName
|
||||
cfg.BindAddr = bindAddr
|
||||
cfg.BindPort = bindPort
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
visitorConfs[cfg.ProxyName] = cfg
|
||||
default:
|
||||
fmt.Println("invalid role")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = startService(clientCfg, proxyConfs, visitorConfs, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -15,19 +15,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/crypto"
|
||||
|
||||
_ "github.com/fatedier/frp/assets/frps"
|
||||
_ "github.com/fatedier/frp/pkg/metrics"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
crypto.DefaultSalt = "frp"
|
||||
// TODO: remove this when we drop support for go1.19
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
system.EnableCompatibilityMode()
|
||||
Execute()
|
||||
}
|
||||
|
||||
183
cmd/frps/root.go
@ -18,81 +18,36 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/server"
|
||||
)
|
||||
|
||||
const (
|
||||
CfgFileTypeIni = iota
|
||||
CfgFileTypeCmd
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
showVersion bool
|
||||
cfgFile string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
|
||||
bindAddr string
|
||||
bindPort int
|
||||
kcpBindPort int
|
||||
proxyBindAddr string
|
||||
vhostHTTPPort int
|
||||
vhostHTTPSPort int
|
||||
vhostHTTPTimeout int64
|
||||
dashboardAddr string
|
||||
dashboardPort int
|
||||
dashboardUser string
|
||||
dashboardPwd string
|
||||
enablePrometheus bool
|
||||
logFile string
|
||||
logLevel string
|
||||
logMaxDays int64
|
||||
disableLogColor bool
|
||||
token string
|
||||
subDomainHost string
|
||||
allowPorts string
|
||||
maxPortsPerClient int64
|
||||
tlsOnly bool
|
||||
dashboardTLSMode bool
|
||||
dashboardTLSCertFile string
|
||||
dashboardTLSKeyFile string
|
||||
serverCfg v1.ServerConfig
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
||||
|
||||
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(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
|
||||
rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
|
||||
rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
|
||||
rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
|
||||
rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
|
||||
rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
|
||||
rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port")
|
||||
rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user")
|
||||
rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password")
|
||||
rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
|
||||
rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file")
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
|
||||
rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days")
|
||||
rootCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
|
||||
rootCmd.PersistentFlags().StringVarP(&subDomainHost, "subdomain_host", "", "", "subdomain host")
|
||||
rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports")
|
||||
rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
|
||||
rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only")
|
||||
rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode")
|
||||
rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
|
||||
rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
|
||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@ -104,113 +59,69 @@ var rootCmd = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
var cfg config.ServerCommonConf
|
||||
var err error
|
||||
var (
|
||||
svrCfg *v1.ServerConfig
|
||||
isLegacyFormat bool
|
||||
err error
|
||||
)
|
||||
if cfgFile != "" {
|
||||
var content []byte
|
||||
content, err = config.GetRenderedConfFromFile(cfgFile)
|
||||
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if isLegacyFormat {
|
||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
}
|
||||
cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
|
||||
} else {
|
||||
cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
if err := serverCfg.Complete(); err != nil {
|
||||
fmt.Printf("failed to complete server config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
svrCfg = &serverCfg
|
||||
}
|
||||
|
||||
err = runServer(cfg)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := runServer(svrCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) {
|
||||
if fileType == CfgFileTypeIni {
|
||||
cfg, err = config.UnmarshalServerConfFromIni(source)
|
||||
} else if fileType == CfgFileTypeCmd {
|
||||
cfg, err = parseServerCommonCfgFromCmd()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cfg.Complete()
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("parse config error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
|
||||
cfg = config.GetDefaultServerConf()
|
||||
|
||||
cfg.BindAddr = bindAddr
|
||||
cfg.BindPort = bindPort
|
||||
cfg.KCPBindPort = kcpBindPort
|
||||
cfg.ProxyBindAddr = proxyBindAddr
|
||||
cfg.VhostHTTPPort = vhostHTTPPort
|
||||
cfg.VhostHTTPSPort = vhostHTTPSPort
|
||||
cfg.VhostHTTPTimeout = vhostHTTPTimeout
|
||||
cfg.DashboardAddr = dashboardAddr
|
||||
cfg.DashboardPort = dashboardPort
|
||||
cfg.DashboardUser = dashboardUser
|
||||
cfg.DashboardPwd = dashboardPwd
|
||||
cfg.EnablePrometheus = enablePrometheus
|
||||
cfg.DashboardTLSCertFile = dashboardTLSCertFile
|
||||
cfg.DashboardTLSKeyFile = dashboardTLSKeyFile
|
||||
cfg.DashboardTLSMode = dashboardTLSMode
|
||||
cfg.LogFile = logFile
|
||||
cfg.LogLevel = logLevel
|
||||
cfg.LogMaxDays = logMaxDays
|
||||
cfg.SubDomainHost = subDomainHost
|
||||
cfg.TLSOnly = tlsOnly
|
||||
|
||||
// Only token authentication is supported in cmd mode
|
||||
cfg.ServerConfig = auth.GetDefaultServerConf()
|
||||
cfg.Token = token
|
||||
if len(allowPorts) > 0 {
|
||||
// e.g. 1000-2000,2001,2002,3000-4000
|
||||
ports, errRet := util.ParseRangeNumbers(allowPorts)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("parse conf error: allow_ports: %v", errRet)
|
||||
return
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
cfg.AllowPorts[int(port)] = struct{}{}
|
||||
}
|
||||
}
|
||||
cfg.MaxPortsPerClient = maxPortsPerClient
|
||||
cfg.DisableLogColor = disableLogColor
|
||||
return
|
||||
}
|
||||
|
||||
func runServer(cfg config.ServerCommonConf) (err error) {
|
||||
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor)
|
||||
func runServer(cfg *v1.ServerConfig) (err error) {
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
log.Info("frps uses config file: %s", cfgFile)
|
||||
log.Infof("frps uses config file: %s", cfgFile)
|
||||
} else {
|
||||
log.Info("frps uses command line arguments for config")
|
||||
log.Infof("frps uses command line arguments for config")
|
||||
}
|
||||
|
||||
svr, err := server.NewService(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("frps started successfully")
|
||||
log.Infof("frps started successfully")
|
||||
svr.Run(context.Background())
|
||||
return
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -32,21 +34,25 @@ var verifyCmd = &cobra.Command{
|
||||
Short: "Verify that the configures is valid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cfgFile == "" {
|
||||
fmt.Println("no config file is specified")
|
||||
fmt.Println("frps: the configuration file is not specified")
|
||||
return nil
|
||||
}
|
||||
iniContent, err := config.GetRenderedConfFromFile(cfgFile)
|
||||
svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = parseServerCommonCfg(CfgFileTypeIni, iniContent)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
|
||||
return nil
|
||||
},
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
[common]
|
||||
server_addr = 127.0.0.1
|
||||
server_port = 7000
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
9
conf/frpc.toml
Normal file
@ -0,0 +1,9 @@
|
||||
serverAddr = "127.0.0.1"
|
||||
serverPort = 7000
|
||||
|
||||
[[proxies]]
|
||||
name = "test-tcp"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
remotePort = 6000
|
||||
462
conf/frpc_full_example.toml
Normal file
@ -0,0 +1,462 @@
|
||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||
|
||||
# Optional unique identifier for this frpc instance.
|
||||
clientID = "your_client_id"
|
||||
# your proxy name will be changed to {user}.{proxy}
|
||||
user = "your_name"
|
||||
|
||||
# A literal address or host name for IPv6 must be enclosed
|
||||
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||
# For single serverAddr field, no need square brackets, like serverAddr = "::".
|
||||
serverAddr = "0.0.0.0"
|
||||
serverPort = 7000
|
||||
|
||||
# STUN server to help penetrate NAT hole.
|
||||
# natHoleStunServer = "stun.easyvoip.com:3478"
|
||||
|
||||
# Decide if exit program when first login failed, otherwise continuous relogin to frps
|
||||
# default is true
|
||||
loginFailExit = true
|
||||
|
||||
# console or real logFile path like ./frpc.log
|
||||
log.to = "./frpc.log"
|
||||
# trace, debug, info, warn, error
|
||||
log.level = "info"
|
||||
log.maxDays = 3
|
||||
# disable log colors when log.to is console, default is false
|
||||
log.disablePrintColor = false
|
||||
|
||||
auth.method = "token"
|
||||
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||
# Optional values are HeartBeats, NewWorkConns.
|
||||
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||
|
||||
# auth token
|
||||
auth.token = "12345678"
|
||||
|
||||
# alternatively, you can use tokenSource to load the token from a file
|
||||
# this is mutually exclusive with auth.token
|
||||
# auth.tokenSource.type = "file"
|
||||
# auth.tokenSource.file.path = "/etc/frp/token"
|
||||
|
||||
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
|
||||
# auth.oidc.clientID = ""
|
||||
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
||||
# auth.oidc.clientSecret = ""
|
||||
# oidc.audience specifies the audience of the token in OIDC authentication.
|
||||
# auth.oidc.audience = ""
|
||||
# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
# auth.oidc.scope = ""
|
||||
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
|
||||
# It will be used to get an OIDC token.
|
||||
# auth.oidc.tokenEndpointURL = ""
|
||||
|
||||
# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint.
|
||||
# For example, if you want to specify the "audience" parameter, you can set as follow.
|
||||
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
|
||||
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
||||
# auth.oidc.additionalEndpointParams.var1 = "foobar"
|
||||
|
||||
# OIDC TLS and proxy configuration
|
||||
# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate.
|
||||
# This is useful when the OIDC provider uses a self-signed certificate or a custom CA.
|
||||
# auth.oidc.trustedCaFile = "/path/to/ca.crt"
|
||||
|
||||
# Skip TLS certificate verification for the OIDC token endpoint.
|
||||
# INSECURE: Only use this for debugging purposes, not recommended for production.
|
||||
# auth.oidc.insecureSkipVerify = false
|
||||
|
||||
# Specify a proxy server for OIDC token endpoint connections.
|
||||
# Supports http, https, socks5, and socks5h proxy protocols.
|
||||
# If not specified, no proxy is used for OIDC connections.
|
||||
# auth.oidc.proxyURL = "http://proxy.example.com:8080"
|
||||
|
||||
# Set admin address for control frpc's action by http api such as reload
|
||||
webServer.addr = "127.0.0.1"
|
||||
webServer.port = 7400
|
||||
webServer.user = "admin"
|
||||
webServer.password = "admin"
|
||||
# Admin assets directory. By default, these assets are bundled with frpc.
|
||||
# webServer.assetsDir = "./static"
|
||||
|
||||
# Enable golang pprof handlers in admin listener.
|
||||
webServer.pprofEnable = false
|
||||
|
||||
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
|
||||
# transport.dialServerTimeout = 10
|
||||
|
||||
# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||
# If negative, keep-alive probes are disabled.
|
||||
# transport.dialServerKeepalive = 7200
|
||||
|
||||
# connections will be established in advance, default value is zero
|
||||
transport.poolCount = 5
|
||||
|
||||
# If tcp stream multiplexing is used, default is true, it must be same with frps
|
||||
# transport.tcpMux = true
|
||||
|
||||
# Specify keep alive interval for tcp mux.
|
||||
# only valid if tcpMux is enabled.
|
||||
# transport.tcpMuxKeepaliveInterval = 30
|
||||
|
||||
# Communication protocol used to connect to server
|
||||
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||
transport.protocol = "tcp"
|
||||
|
||||
# set client binding ip when connect server, default is empty.
|
||||
# only when protocol = tcp or websocket, the value will be used.
|
||||
transport.connectServerLocalIP = "0.0.0.0"
|
||||
|
||||
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
|
||||
# it only works when protocol is tcp
|
||||
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
||||
# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080"
|
||||
# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080"
|
||||
|
||||
# quic protocol options
|
||||
# transport.quic.keepalivePeriod = 10
|
||||
# transport.quic.maxIdleTimeout = 30
|
||||
# transport.quic.maxIncomingStreams = 100000
|
||||
|
||||
# If tls.enable is true, frpc will connect frps by tls.
|
||||
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
|
||||
transport.tls.enable = true
|
||||
|
||||
# transport.tls.certFile = "client.crt"
|
||||
# transport.tls.keyFile = "client.key"
|
||||
# transport.tls.trustedCaFile = "ca.crt"
|
||||
# transport.tls.serverName = "example.com"
|
||||
|
||||
# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
|
||||
# first custom byte when tls is enabled.
|
||||
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
|
||||
# transport.tls.disableCustomTLSFirstByte = true
|
||||
|
||||
# Heartbeat configure, it's not recommended to modify the default value.
|
||||
# The default value of heartbeatInterval is 10 and heartbeatTimeout is 90. Set negative value
|
||||
# to disable it.
|
||||
# transport.heartbeatInterval = 30
|
||||
# transport.heartbeatTimeout = 90
|
||||
|
||||
# Specify a dns server, so frpc will use this instead of default one
|
||||
# dnsServer = "8.8.8.8"
|
||||
|
||||
# Proxy names you want to start.
|
||||
# Default is empty, means all proxies.
|
||||
# start = ["ssh", "dns"]
|
||||
|
||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||
# Set 'enabled = false' in a proxy configuration to disable it.
|
||||
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
||||
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
||||
|
||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
udpPacketSize = 1500
|
||||
|
||||
# Feature gates allows you to enable or disable experimental features
|
||||
# Format is a map of feature names to boolean values
|
||||
# You can enable specific features:
|
||||
#featureGates = { VirtualNet = true }
|
||||
|
||||
# VirtualNet settings for experimental virtual network capabilities
|
||||
# The virtual network feature requires enabling the VirtualNet feature gate above
|
||||
# virtualNet.address = "100.86.1.1/24"
|
||||
|
||||
# Additional metadatas for client.
|
||||
metadatas.var1 = "abc"
|
||||
metadatas.var2 = "123"
|
||||
|
||||
# Include other config files for proxies.
|
||||
# includes = ["./confd/*.ini"]
|
||||
|
||||
[[proxies]]
|
||||
# 'ssh' is the unique proxy name
|
||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||
# enabled = true
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# Limit bandwidth for this proxy, unit is KB and MB
|
||||
transport.bandwidthLimit = "1MB"
|
||||
# Where to limit bandwidth, can be 'client' or 'server', default is 'client'
|
||||
transport.bandwidthLimitMode = "client"
|
||||
# If true, traffic of this proxy will be encrypted, default is false
|
||||
transport.useEncryption = false
|
||||
# If true, traffic will be compressed
|
||||
transport.useCompression = false
|
||||
# Remote port listen by frps
|
||||
remotePort = 6001
|
||||
# frps will load balancing connections for proxies in same group
|
||||
loadBalancer.group = "test_group"
|
||||
# group should have same group key
|
||||
loadBalancer.groupKey = "123456"
|
||||
# Enable health check for the backend service, it supports 'tcp' and 'http' now.
|
||||
# frpc will connect local service's port to detect it's healthy status
|
||||
healthCheck.type = "tcp"
|
||||
# Health check connection timeout
|
||||
healthCheck.timeoutSeconds = 3
|
||||
# If continuous failed in 3 times, the proxy will be removed from frps
|
||||
healthCheck.maxFailed = 3
|
||||
# Every 10 seconds will do a health check
|
||||
healthCheck.intervalSeconds = 10
|
||||
# Additional meta info for each proxy. It will be passed to the server-side plugin for use.
|
||||
metadatas.var1 = "abc"
|
||||
metadatas.var2 = "123"
|
||||
# You can add some extra information to the proxy through annotations.
|
||||
# These annotations will be displayed on the frps dashboard.
|
||||
[proxies.annotations]
|
||||
key1 = "value1"
|
||||
"prefix/key2" = "value2"
|
||||
|
||||
[[proxies]]
|
||||
name = "ssh_random"
|
||||
type = "tcp"
|
||||
localIP = "192.168.31.100"
|
||||
localPort = 22
|
||||
# If remotePort is 0, frps will assign a random port for you
|
||||
remotePort = 0
|
||||
|
||||
[[proxies]]
|
||||
name = "dns"
|
||||
type = "udp"
|
||||
localIP = "114.114.114.114"
|
||||
localPort = 53
|
||||
remotePort = 6002
|
||||
|
||||
# Resolve your domain names to [serverAddr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
|
||||
[[proxies]]
|
||||
name = "web01"
|
||||
type = "http"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 80
|
||||
# http username and password are safety certification for http protocol
|
||||
# if not set, you can access this customDomains without certification
|
||||
httpUser = "admin"
|
||||
httpPassword = "admin"
|
||||
# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
|
||||
subdomain = "web01"
|
||||
customDomains = ["web01.yourdomain.com"]
|
||||
# locations is only available for http type
|
||||
locations = ["/", "/pic"]
|
||||
# route requests to this service if http basic auto user is abc
|
||||
# routeByHTTPUser = abc
|
||||
hostHeaderRewrite = "example.com"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
responseHeaders.set.foo = "bar"
|
||||
healthCheck.type = "http"
|
||||
# frpc will send a GET http request '/status' to local http service
|
||||
# http service is alive when it return 2xx http response code
|
||||
healthCheck.path = "/status"
|
||||
healthCheck.intervalSeconds = 10
|
||||
healthCheck.maxFailed = 3
|
||||
healthCheck.timeoutSeconds = 3
|
||||
# set health check headers
|
||||
healthCheck.httpHeaders=[
|
||||
{ name = "x-from-where", value = "frp" }
|
||||
]
|
||||
|
||||
[[proxies]]
|
||||
name = "web02"
|
||||
type = "https"
|
||||
# Disable this proxy by setting enabled to false
|
||||
# enabled = false
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 8000
|
||||
subdomain = "web02"
|
||||
customDomains = ["web02.yourdomain.com"]
|
||||
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
|
||||
# v1 or v2 or empty
|
||||
transport.proxyProtocolVersion = "v2"
|
||||
|
||||
[[proxies]]
|
||||
name = "tcpmuxhttpconnect"
|
||||
type = "tcpmux"
|
||||
multiplexer = "httpconnect"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 10701
|
||||
customDomains = ["tunnel1"]
|
||||
# routeByHTTPUser = "user1"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_unix_domain_socket"
|
||||
type = "tcp"
|
||||
remotePort = 6003
|
||||
# if plugin is defined, localIP and localPort is useless
|
||||
# plugin will handle connections got from frps
|
||||
[proxies.plugin]
|
||||
type = "unix_domain_socket"
|
||||
unixPath = "/var/run/docker.sock"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http_proxy"
|
||||
type = "tcp"
|
||||
remotePort = 6004
|
||||
[proxies.plugin]
|
||||
type = "http_proxy"
|
||||
httpUser = "abc"
|
||||
httpPassword = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_socks5"
|
||||
type = "tcp"
|
||||
remotePort = 6005
|
||||
[proxies.plugin]
|
||||
type = "socks5"
|
||||
username = "abc"
|
||||
password = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_static_file"
|
||||
type = "tcp"
|
||||
remotePort = 6006
|
||||
[proxies.plugin]
|
||||
type = "static_file"
|
||||
localPath = "/var/www/blog"
|
||||
stripPrefix = "static"
|
||||
httpUser = "abc"
|
||||
httpPassword = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_https2http"
|
||||
type = "https"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "https2http"
|
||||
localAddr = "127.0.0.1:80"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_https2https"
|
||||
type = "https"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "https2https"
|
||||
localAddr = "127.0.0.1:443"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http2https"
|
||||
type = "http"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "http2https"
|
||||
localAddr = "127.0.0.1:443"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http2http"
|
||||
type = "tcp"
|
||||
remotePort = 6007
|
||||
[proxies.plugin]
|
||||
type = "http2http"
|
||||
localAddr = "127.0.0.1:80"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_tls2raw"
|
||||
type = "tcp"
|
||||
remotePort = 6008
|
||||
[proxies.plugin]
|
||||
type = "tls2raw"
|
||||
localAddr = "127.0.0.1:80"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
|
||||
[[proxies]]
|
||||
name = "secret_tcp"
|
||||
# If the type is secret tcp, remotePort is useless
|
||||
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
|
||||
type = "stcp"
|
||||
# secretKey is used for authentication for visitors
|
||||
secretKey = "abcdefg"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# If not empty, only visitors from specified users can connect.
|
||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||
allowUsers = ["*"]
|
||||
|
||||
[[proxies]]
|
||||
name = "p2p_tcp"
|
||||
type = "xtcp"
|
||||
secretKey = "abcdefg"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# If not empty, only visitors from specified users can connect.
|
||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||
allowUsers = ["user1", "user2"]
|
||||
|
||||
# NAT traversal configuration (optional)
|
||||
[proxies.natTraversal]
|
||||
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||
# When enabled, only STUN-discovered public addresses will be used.
|
||||
# This can improve performance when you have slow VPN connections.
|
||||
# Default: false
|
||||
disableAssistedAddrs = false
|
||||
|
||||
[[proxies]]
|
||||
name = "vnet-server"
|
||||
type = "stcp"
|
||||
secretKey = "your-secret-key"
|
||||
[proxies.plugin]
|
||||
type = "virtual_net"
|
||||
|
||||
# frpc role visitor -> frps -> frpc role server
|
||||
[[visitors]]
|
||||
name = "secret_tcp_visitor"
|
||||
type = "stcp"
|
||||
# the server name you want to visitor
|
||||
serverName = "secret_tcp"
|
||||
secretKey = "abcdefg"
|
||||
# connect this address to visitor stcp server
|
||||
bindAddr = "127.0.0.1"
|
||||
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||
# other visitors. (This is not supported for SUDP now)
|
||||
bindPort = 9000
|
||||
|
||||
[[visitors]]
|
||||
name = "p2p_tcp_visitor"
|
||||
type = "xtcp"
|
||||
# if the server user is not set, it defaults to the current user
|
||||
serverUser = "user1"
|
||||
serverName = "p2p_tcp"
|
||||
secretKey = "abcdefg"
|
||||
bindAddr = "127.0.0.1"
|
||||
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||
# other visitors. (This is not supported for SUDP now)
|
||||
bindPort = 9001
|
||||
# when automatic tunnel persistence is required, set it to true
|
||||
keepTunnelOpen = false
|
||||
# effective when keepTunnelOpen is set to true, the number of attempts to punch through per hour
|
||||
maxRetriesAnHour = 8
|
||||
minRetryInterval = 90
|
||||
# fallbackTo = "stcp_visitor"
|
||||
# fallbackTimeoutMs = 500
|
||||
|
||||
# NAT traversal configuration (optional)
|
||||
[visitors.natTraversal]
|
||||
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||
# When enabled, only STUN-discovered public addresses will be used.
|
||||
# Default: false
|
||||
disableAssistedAddrs = false
|
||||
|
||||
[[visitors]]
|
||||
name = "vnet-visitor"
|
||||
type = "stcp"
|
||||
serverName = "vnet-server"
|
||||
secretKey = "your-secret-key"
|
||||
bindPort = -1
|
||||
[visitors.plugin]
|
||||
type = "virtual_net"
|
||||
destinationIP = "100.86.0.1"
|
||||
@ -1,2 +0,0 @@
|
||||
[common]
|
||||
bind_port = 7000
|
||||
1
conf/frps.toml
Normal file
@ -0,0 +1 @@
|
||||
bindPort = 7000
|
||||
169
conf/frps_full_example.toml
Normal file
@ -0,0 +1,169 @@
|
||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||
|
||||
# A literal address or host name for IPv6 must be enclosed
|
||||
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||
# For single "bindAddr" field, no need square brackets, like `bindAddr = "::"`.
|
||||
bindAddr = "0.0.0.0"
|
||||
bindPort = 7000
|
||||
|
||||
# udp port used for kcp protocol, it can be same with 'bindPort'.
|
||||
# if not set, kcp is disabled in frps.
|
||||
kcpBindPort = 7000
|
||||
|
||||
# udp port used for quic protocol.
|
||||
# if not set, quic is disabled in frps.
|
||||
# quicBindPort = 7002
|
||||
|
||||
# Specify which address proxy will listen for, default value is same with bindAddr
|
||||
# proxyBindAddr = "127.0.0.1"
|
||||
|
||||
# quic protocol options
|
||||
# transport.quic.keepalivePeriod = 10
|
||||
# transport.quic.maxIdleTimeout = 30
|
||||
# transport.quic.maxIncomingStreams = 100000
|
||||
|
||||
# Heartbeat configure, it's not recommended to modify the default value
|
||||
# The default value of heartbeatTimeout is 90. Set negative value to disable it.
|
||||
# transport.heartbeatTimeout = 90
|
||||
|
||||
# Pool count in each proxy will keep no more than maxPoolCount.
|
||||
transport.maxPoolCount = 5
|
||||
|
||||
# If tcp stream multiplexing is used, default is true
|
||||
# transport.tcpMux = true
|
||||
|
||||
# Specify keep alive interval for tcp mux.
|
||||
# only valid if tcpMux is true.
|
||||
# transport.tcpMuxKeepaliveInterval = 30
|
||||
|
||||
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||
# If negative, keep-alive probes are disabled.
|
||||
# transport.tcpKeepalive = 7200
|
||||
|
||||
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
|
||||
transport.tls.force = false
|
||||
|
||||
# transport.tls.certFile = "server.crt"
|
||||
# transport.tls.keyFile = "server.key"
|
||||
# transport.tls.trustedCaFile = "ca.crt"
|
||||
|
||||
# If you want to support virtual host, you must set the http port for listening (optional)
|
||||
# Note: http port and https port can be same with bindPort
|
||||
vhostHTTPPort = 80
|
||||
vhostHTTPSPort = 443
|
||||
|
||||
# Response header timeout(seconds) for vhost http server, default is 60s
|
||||
# vhostHTTPTimeout = 60
|
||||
|
||||
# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
|
||||
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
|
||||
# requests on one single port. If it's not - it will listen on this value for
|
||||
# HTTP CONNECT requests. By default, this value is 0.
|
||||
# tcpmuxHTTPConnectPort = 1337
|
||||
|
||||
# If tcpmuxPassthrough is true, frps won't do any update on traffic.
|
||||
# tcpmuxPassthrough = false
|
||||
|
||||
# Configure the web server to enable the dashboard for frps.
|
||||
# dashboard is available only if webServer.port is set.
|
||||
webServer.addr = "127.0.0.1"
|
||||
webServer.port = 7500
|
||||
webServer.user = "admin"
|
||||
webServer.password = "admin"
|
||||
# webServer.tls.certFile = "server.crt"
|
||||
# webServer.tls.keyFile = "server.key"
|
||||
# dashboard assets directory(only for debug mode)
|
||||
# webServer.assetsDir = "./static"
|
||||
|
||||
# Enable golang pprof handlers in dashboard listener.
|
||||
# Dashboard port must be set first
|
||||
webServer.pprofEnable = false
|
||||
|
||||
# enablePrometheus will export prometheus metrics on webServer in /metrics api.
|
||||
enablePrometheus = true
|
||||
|
||||
# console or real logFile path like ./frps.log
|
||||
log.to = "./frps.log"
|
||||
# trace, debug, info, warn, error
|
||||
log.level = "info"
|
||||
log.maxDays = 3
|
||||
# disable log colors when log.to is console, default is false
|
||||
log.disablePrintColor = false
|
||||
|
||||
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
|
||||
detailedErrorsToClient = true
|
||||
|
||||
# auth.method specifies what authentication method to use authenticate frpc with frps.
|
||||
# If "token" is specified - token will be read into login message.
|
||||
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
|
||||
auth.method = "token"
|
||||
|
||||
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||
# Optional values are HeartBeats, NewWorkConns.
|
||||
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||
|
||||
# auth token
|
||||
auth.token = "12345678"
|
||||
|
||||
# alternatively, you can use tokenSource to load the token from a file
|
||||
# this is mutually exclusive with auth.token
|
||||
# auth.tokenSource.type = "file"
|
||||
# auth.tokenSource.file.path = "/etc/frp/token"
|
||||
|
||||
# oidc issuer specifies the issuer to verify OIDC tokens with.
|
||||
auth.oidc.issuer = ""
|
||||
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
||||
auth.oidc.audience = ""
|
||||
# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired.
|
||||
auth.oidc.skipExpiryCheck = false
|
||||
# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
|
||||
auth.oidc.skipIssuerCheck = false
|
||||
|
||||
# userConnTimeout specifies the maximum time to wait for a work connection.
|
||||
# userConnTimeout = 10
|
||||
|
||||
# Only allow frpc to bind ports you list. By default, there won't be any limit.
|
||||
allowPorts = [
|
||||
{ start = 2000, end = 3000 },
|
||||
{ single = 3001 },
|
||||
{ single = 3003 },
|
||||
{ start = 4000, end = 50000 }
|
||||
]
|
||||
|
||||
# Max ports can be used for each client, default value is 0 means no limit
|
||||
maxPortsPerClient = 0
|
||||
|
||||
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
|
||||
# When subdomain is test, the host used by routing is test.frps.com
|
||||
subDomainHost = "frps.com"
|
||||
|
||||
# custom 404 page for HTTP requests
|
||||
# custom404Page = "/path/to/404.html"
|
||||
|
||||
# specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
udpPacketSize = 1500
|
||||
|
||||
# Retention time for NAT hole punching strategy data.
|
||||
natholeAnalysisDataReserveHours = 168
|
||||
|
||||
# ssh tunnel gateway
|
||||
# If you want to enable this feature, the bindPort parameter is required, while others are optional.
|
||||
# By default, this feature is disabled. It will be enabled if bindPort is greater than 0.
|
||||
# sshTunnelGateway.bindPort = 2200
|
||||
# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa"
|
||||
# sshTunnelGateway.autoGenPrivateKeyPath = ""
|
||||
# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys"
|
||||
|
||||
[[httpPlugins]]
|
||||
name = "user-manager"
|
||||
addr = "127.0.0.1:9000"
|
||||
path = "/handler"
|
||||
ops = ["Login"]
|
||||
|
||||
[[httpPlugins]]
|
||||
name = "port-manager"
|
||||
addr = "127.0.0.1:9001"
|
||||
path = "/handler"
|
||||
ops = ["NewProxy"]
|
||||
@ -56,7 +56,7 @@ oidc_client_secret =
|
||||
# oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
oidc_audience =
|
||||
|
||||
# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
oidc_scope =
|
||||
|
||||
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.
|
||||
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
doc/pic/sponsor_daytona.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 41 KiB |
BIN
doc/pic/sponsor_jetbrains.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
doc/pic/sponsor_olares.jpeg
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 37 KiB |
BIN
doc/pic/zsxq.jpg
|
Before Width: | Height: | Size: 12 KiB |
@ -121,7 +121,7 @@ Create new proxy
|
||||
// http and https only
|
||||
"custom_domains": []<string>,
|
||||
"subdomain": <string>,
|
||||
"locations": <string>,
|
||||
"locations": []<string>,
|
||||
"http_user": <string>,
|
||||
"http_pwd": <string>,
|
||||
"host_header_rewrite": <string>,
|
||||
@ -216,49 +216,50 @@ New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcp
|
||||
|
||||
### Server Plugin Configuration
|
||||
|
||||
```ini
|
||||
# frps.ini
|
||||
[common]
|
||||
bind_port = 7000
|
||||
```toml
|
||||
# frps.toml
|
||||
bindPort = 7000
|
||||
|
||||
[plugin.user-manager]
|
||||
addr = 127.0.0.1:9000
|
||||
path = /handler
|
||||
ops = Login
|
||||
[[httpPlugins]]
|
||||
name = "user-manager"
|
||||
addr = "127.0.0.1:9000"
|
||||
path = "/handler"
|
||||
ops = ["Login"]
|
||||
|
||||
[plugin.port-manager]
|
||||
addr = 127.0.0.1:9001
|
||||
path = /handler
|
||||
ops = NewProxy
|
||||
[[httpPlugins]]
|
||||
name = "port-manager"
|
||||
addr = "127.0.0.1:9001"
|
||||
path = "/handler"
|
||||
ops = ["NewProxy"]
|
||||
```
|
||||
|
||||
- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`.
|
||||
- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`.
|
||||
- path: http request url path for the POST request.
|
||||
- ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...).
|
||||
- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
|
||||
- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
|
||||
|
||||
### Metadata
|
||||
|
||||
Metadata will be sent to the server plugin in each RPC request.
|
||||
|
||||
There are 2 types of metadata entries - 1 under `[common]` and the other under each proxy configuration.
|
||||
Metadata entries under `[common]` will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
|
||||
There are 2 types of metadata entries - global one and the other under each proxy configuration.
|
||||
Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
|
||||
Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`.
|
||||
|
||||
Metadata entries start with `meta_`. This is an example of metadata entries in `[common]` and under the proxy named `[ssh]`:
|
||||
This is an example of metadata entries:
|
||||
|
||||
```
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = 127.0.0.1
|
||||
server_port = 7000
|
||||
user = fake
|
||||
meta_token = fake
|
||||
meta_version = 1.0.0
|
||||
```toml
|
||||
# frpc.toml
|
||||
serverAddr = "127.0.0.1"
|
||||
serverPort = 7000
|
||||
user = "fake"
|
||||
metadatas.token = "fake"
|
||||
metadatas.version = "1.0.0"
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
meta_id = 123
|
||||
[[proxies]]
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
localPort = 22
|
||||
remotePort = 6000
|
||||
metadatas.id = "123"
|
||||
```
|
||||
|
||||
160
doc/ssh_tunnel_gateway.md
Normal file
@ -0,0 +1,160 @@
|
||||
### SSH Tunnel Gateway
|
||||
|
||||
*Added in v0.53.0*
|
||||
|
||||
### Concept
|
||||
|
||||
SSH supports reverse proxy capabilities [rfc](https://www.rfc-editor.org/rfc/rfc4254#page-16).
|
||||
|
||||
frp supports listening on an SSH port on the frps side to achieve TCP protocol proxying using the SSH -R protocol. This mode does not rely on frpc.
|
||||
|
||||
SSH reverse tunneling proxying and proxying SSH ports through frp are two different concepts. SSH reverse tunneling proxying is essentially a basic reverse proxying accomplished by connecting to frps via an SSH client when you don't want to use frpc.
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 0
|
||||
sshTunnelGateway.privateKeyFile = ""
|
||||
sshTunnelGateway.autoGenPrivateKeyPath = ""
|
||||
sshTunnelGateway.authorizedKeysFile = ""
|
||||
```
|
||||
|
||||
| Field | Type | Description | Required |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| bindPort| int | The ssh server port that frps listens on.| Yes |
|
||||
| privateKeyFile | string | Default value is empty. The private key file used by the ssh server. If it is empty, frps will read the private key file under the autoGenPrivateKeyPath path. It can reuse the /home/user/.ssh/id_rsa file on the local machine, or a custom path can be specified.| No |
|
||||
| autoGenPrivateKeyPath | string |Default value is ./.autogen_ssh_key. If the file does not exist or its content is empty, frps will automatically generate RSA private key file content and store it in this file.|No|
|
||||
| authorizedKeysFile | string |Default value is empty. If it is empty, ssh client authentication is not authenticated. If it is not empty, it can implement ssh password-free login authentication. It can reuse the local /home/user/.ssh/authorized_keys file or a custom path can be specified.| No |
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### Server-side frps
|
||||
|
||||
Minimal configuration:
|
||||
|
||||
```toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
```
|
||||
|
||||
Place the above configuration in frps.toml and run `./frps -c frps.toml`. It will listen on port 2200 and accept SSH reverse proxy requests.
|
||||
|
||||
Note:
|
||||
|
||||
1. When using the minimal configuration, a `.autogen_ssh_key` private key file will be automatically created in the current working directory. The SSH server of frps will use this private key file for encryption and decryption. Alternatively, you can reuse an existing private key file on your local machine, such as `/home/user/.ssh/id_rsa`.
|
||||
|
||||
2. When running frps in the minimal configuration mode, connecting to frps via SSH does not require authentication. It is strongly recommended to configure a token in frps and specify the token in the SSH command line.
|
||||
|
||||
#### Client-side SSH
|
||||
|
||||
The command format is:
|
||||
|
||||
```bash
|
||||
ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token}
|
||||
```
|
||||
|
||||
1. `--proxy_name` is optional, and if left empty, a random one will be generated.
|
||||
2. The username for logging in to frps is always "v0" and currently has no significance, i.e., `v0@{frps_address}`.
|
||||
3. The server-side proxy listens on the port determined by `--remote_port`.
|
||||
4. `{tcp|http|https|stcp|tcpmux}` supports the complete command parameters, which can be obtained by using `--help`. For example: `ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help`.
|
||||
5. The token is optional, but for security reasons, it is strongly recommended to configure the token in frps.
|
||||
|
||||
#### TCP Proxy
|
||||
|
||||
```bash
|
||||
ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
|
||||
```
|
||||
|
||||
This sets up a proxy on frps that listens on port 9090 and proxies local service on port 8080.
|
||||
|
||||
```bash
|
||||
frp (via SSH) (Ctrl+C to quit)
|
||||
|
||||
User:
|
||||
ProxyName: test-tcp
|
||||
Type: tcp
|
||||
RemoteAddress: :9090
|
||||
```
|
||||
|
||||
Equivalent to:
|
||||
|
||||
```bash
|
||||
frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
|
||||
```
|
||||
|
||||
More parameters can be obtained by executing `--help`.
|
||||
|
||||
#### HTTP Proxy
|
||||
|
||||
```bash
|
||||
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 http --proxy_name "test-http" --custom_domain test-http.frps.com
|
||||
```
|
||||
|
||||
Equivalent to:
|
||||
```bash
|
||||
frpc http --proxy_name "test-http" --custom_domain test-http.frps.com
|
||||
```
|
||||
|
||||
You can access the HTTP service using the following command:
|
||||
|
||||
curl 'http://test-http.frps.com'
|
||||
|
||||
More parameters can be obtained by executing --help.
|
||||
|
||||
#### HTTPS/STCP/TCPMUX Proxy
|
||||
|
||||
To obtain the usage instructions, use the following command:
|
||||
|
||||
```bash
|
||||
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 {https|stcp|tcpmux} --help
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
#### Reusing the id_rsa File on the Local Machine
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa"
|
||||
```
|
||||
|
||||
During the SSH protocol handshake, public keys are exchanged for data encryption. Therefore, the SSH server on the frps side needs to specify a private key file, which can be reused from an existing file on the local machine. If the privateKeyFile field is empty, frps will automatically create an RSA private key file.
|
||||
|
||||
#### Specifying the Auto-Generated Private Key File Path
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file"
|
||||
```
|
||||
|
||||
frps will automatically create a private key file and store it at the specified path.
|
||||
|
||||
Note: Changing the private key file in frps can cause SSH client login failures. If you need to log in successfully, you can delete the old records from the `/home/user/.ssh/known_hosts` file.
|
||||
|
||||
#### Using an Existing authorized_keys File for SSH Public Key Authentication
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
The authorizedKeysFile is the file used for SSH public key authentication, which contains the public key information for users, with one key per line.
|
||||
|
||||
If authorizedKeysFile is empty, frps won't perform any authentication for SSH clients. Frps does not support SSH username and password authentication.
|
||||
|
||||
You can reuse an existing `authorized_keys` file on your local machine for client authentication.
|
||||
|
||||
Note: authorizedKeysFile is for user authentication during the SSH login phase, while the token is for frps authentication. These two authentication methods are independent. SSH authentication comes first, followed by frps token authentication. It is strongly recommended to enable at least one of them. If authorizedKeysFile is empty, it is highly recommended to enable token authentication in frps to avoid security risks.
|
||||
|
||||
#### Using a Custom authorized_keys File for SSH Public Key Authentication
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file"
|
||||
```
|
||||
|
||||
Specify the path to a custom `authorized_keys` file.
|
||||
|
||||
Note that changes to the authorizedKeysFile file may result in SSH authentication failures. You may need to re-add the public key information to the authorizedKeysFile.
|
||||
73
doc/virtual_net.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Virtual Network (VirtualNet)
|
||||
|
||||
*Alpha feature added in v0.62.0*
|
||||
|
||||
The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
|
||||
|
||||
> **Note**: VirtualNet is an Alpha stage feature and is currently unstable. Its configuration methods and functionality may be adjusted and changed at any time in subsequent versions. Do not use this feature in production environments; it is only recommended for testing and evaluation purposes.
|
||||
|
||||
## Enabling VirtualNet
|
||||
|
||||
Since VirtualNet is currently an alpha feature, you need to enable it with feature gates in your configuration:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
featureGates = { VirtualNet = true }
|
||||
```
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
To use the virtual network capabilities:
|
||||
|
||||
1. First, configure your frpc with a virtual network address:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
serverAddr = "x.x.x.x"
|
||||
serverPort = 7000
|
||||
featureGates = { VirtualNet = true }
|
||||
|
||||
# Configure the virtual network interface
|
||||
virtualNet.address = "100.86.0.1/24"
|
||||
```
|
||||
|
||||
2. For client proxies, use the `virtual_net` plugin:
|
||||
|
||||
```toml
|
||||
# frpc.toml (server side)
|
||||
[[proxies]]
|
||||
name = "vnet-server"
|
||||
type = "stcp"
|
||||
secretKey = "your-secret-key"
|
||||
[proxies.plugin]
|
||||
type = "virtual_net"
|
||||
```
|
||||
|
||||
3. For visitor connections, configure the `virtual_net` visitor plugin:
|
||||
|
||||
```toml
|
||||
# frpc.toml (client side)
|
||||
serverAddr = "x.x.x.x"
|
||||
serverPort = 7000
|
||||
featureGates = { VirtualNet = true }
|
||||
|
||||
# Configure the virtual network interface
|
||||
virtualNet.address = "100.86.0.2/24"
|
||||
|
||||
[[visitors]]
|
||||
name = "vnet-visitor"
|
||||
type = "stcp"
|
||||
serverName = "vnet-server"
|
||||
secretKey = "your-secret-key"
|
||||
bindPort = -1
|
||||
[visitors.plugin]
|
||||
type = "virtual_net"
|
||||
destinationIP = "100.86.0.1"
|
||||
```
|
||||
|
||||
## Requirements and Limitations
|
||||
|
||||
- **Permissions**: Creating a TUN interface requires elevated permissions (root/admin)
|
||||
- **Platform Support**: Currently supported on Linux and macOS
|
||||
- **Default Status**: As an alpha feature, VirtualNet is disabled by default
|
||||
- **Configuration**: A valid IP/CIDR must be provided for each endpoint in the virtual network
|
||||
@ -1,12 +1,22 @@
|
||||
FROM golang:1.20 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frpc
|
||||
COPY web/frpc/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
||||
WORKDIR /building
|
||||
|
||||
RUN make frpc
|
||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
COPY --from=building /building/bin/frpc /usr/bin/frpc
|
||||
|
||||
ENTRYPOINT ["/usr/bin/frpc"]
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
FROM golang:1.20 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frps
|
||||
COPY web/frps/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
||||
WORKDIR /building
|
||||
|
||||
RUN make frps
|
||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
COPY --from=building /building/bin/frps /usr/bin/frps
|
||||
|
||||
ENTRYPOINT ["/usr/bin/frps"]
|
||||
|
||||
122
go.mod
@ -1,80 +1,84 @@
|
||||
module github.com/fatedier/frp
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
|
||||
github.com/fatedier/golib v0.1.1-0.20230720124328-204db2e322f8
|
||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/fatedier/golib v0.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/yamux v0.1.1
|
||||
github.com/onsi/ginkgo/v2 v2.8.3
|
||||
github.com/onsi/gomega v1.27.0
|
||||
github.com/pion/stun v0.4.0
|
||||
github.com/pires/go-proxyproto v0.6.2
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/quic-go/quic-go v0.34.0
|
||||
github.com/rodaine/table v1.0.1
|
||||
github.com/samber/lo v1.38.1
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/oauth2 v0.3.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
|
||||
github.com/onsi/ginkgo/v2 v2.23.4
|
||||
github.com/onsi/gomega v1.36.3
|
||||
github.com/pelletier/go-toml/v2 v2.2.0
|
||||
github.com/pion/stun/v2 v2.0.0
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.55.0
|
||||
github.com/rodaine/table v1.2.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
k8s.io/apimachinery v0.26.1
|
||||
k8s.io/client-go v0.26.1
|
||||
k8s.io/apimachinery v0.28.8
|
||||
k8s.io/client-go v0.28.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.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/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
||||
github.com/pion/transport/v3 v3.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/quic-go/qtls-go1-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/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/templexxx/cpu v0.1.1 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // 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
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
|
||||
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d
|
||||
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
SCRIPT=$(readlink -f "$0")
|
||||
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
|
||||
|
||||
ginkgo_command=$(which ginkgo 2>/dev/null)
|
||||
if [ -z "$ginkgo_command" ]; then
|
||||
# Check if ginkgo is available
|
||||
if ! command -v ginkgo >/dev/null 2>&1; then
|
||||
echo "ginkgo not found, try to install..."
|
||||
go install github.com/onsi/ginkgo/v2/ginkgo@v2.8.3
|
||||
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
|
||||
fi
|
||||
|
||||
debug=false
|
||||
@ -26,5 +26,9 @@ frpsPath=${ROOT}/bin/frps
|
||||
if [ "${FRPS_PATH}" ]; then
|
||||
frpsPath="${FRPS_PATH}"
|
||||
fi
|
||||
concurrency="16"
|
||||
if [ "${CONCURRENCY}" ]; then
|
||||
concurrency="${CONCURRENCY}"
|
||||
fi
|
||||
|
||||
ginkgo -nodes=8 --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}
|
||||
ginkgo -nodes=${concurrency} --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}
|
||||
|
||||
83
package.sh
@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# compile for version
|
||||
make
|
||||
if [ $? -ne 0 ]; then
|
||||
@ -14,49 +17,57 @@ make -f ./Makefile.cross-compiles
|
||||
rm -rf ./release/packages
|
||||
mkdir -p ./release/packages
|
||||
|
||||
os_all='linux windows darwin freebsd'
|
||||
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
|
||||
os_all='linux windows darwin freebsd openbsd android'
|
||||
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
|
||||
extra_all='_ hf'
|
||||
|
||||
cd ./release
|
||||
|
||||
for os in $os_all; do
|
||||
for arch in $arch_all; do
|
||||
frp_dir_name="frp_${frp_version}_${os}_${arch}"
|
||||
frp_path="./packages/frp_${frp_version}_${os}_${arch}"
|
||||
for extra in $extra_all; do
|
||||
suffix="${os}_${arch}"
|
||||
if [ "x${extra}" != x"_" ]; then
|
||||
suffix="${os}_${arch}_${extra}"
|
||||
fi
|
||||
frp_dir_name="frp_${frp_version}_${suffix}"
|
||||
frp_path="./packages/frp_${frp_version}_${suffix}"
|
||||
|
||||
if [ "x${os}" = x"windows" ]; then
|
||||
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
|
||||
continue
|
||||
fi
|
||||
mkdir ${frp_path}
|
||||
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
|
||||
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
|
||||
else
|
||||
if [ ! -f "./frpc_${os}_${arch}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "./frps_${os}_${arch}" ]; then
|
||||
continue
|
||||
fi
|
||||
mkdir ${frp_path}
|
||||
mv ./frpc_${os}_${arch} ${frp_path}/frpc
|
||||
mv ./frps_${os}_${arch} ${frp_path}/frps
|
||||
fi
|
||||
cp ../LICENSE ${frp_path}
|
||||
cp -rf ../conf/* ${frp_path}
|
||||
if [ "x${os}" = x"windows" ]; then
|
||||
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
|
||||
continue
|
||||
fi
|
||||
mkdir ${frp_path}
|
||||
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
|
||||
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
|
||||
else
|
||||
if [ ! -f "./frpc_${suffix}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "./frps_${suffix}" ]; then
|
||||
continue
|
||||
fi
|
||||
mkdir ${frp_path}
|
||||
mv ./frpc_${suffix} ${frp_path}/frpc
|
||||
mv ./frps_${suffix} ${frp_path}/frps
|
||||
fi
|
||||
cp ../LICENSE ${frp_path}
|
||||
cp -f ../conf/frpc.toml ${frp_path}
|
||||
cp -f ../conf/frps.toml ${frp_path}
|
||||
|
||||
# packages
|
||||
cd ./packages
|
||||
if [ "x${os}" = x"windows" ]; then
|
||||
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
|
||||
else
|
||||
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
|
||||
fi
|
||||
cd ..
|
||||
rm -rf ${frp_path}
|
||||
# packages
|
||||
cd ./packages
|
||||
if [ "x${os}" = x"windows" ]; then
|
||||
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
|
||||
else
|
||||
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
|
||||
fi
|
||||
cd ..
|
||||
rm -rf ${frp_path}
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
|
||||
155
pkg/auth/auth.go
@ -15,79 +15,69 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
type BaseConfig struct {
|
||||
// AuthenticationMethod specifies what authentication method to use to
|
||||
// authenticate frpc with frps. If "token" is specified - token will be
|
||||
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
|
||||
// token will be issued using OIDC settings. By default, this value is "token".
|
||||
AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
|
||||
// AuthenticateHeartBeats specifies whether to include authentication token in
|
||||
// heartbeats sent to frps. By default, this value is false.
|
||||
AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
|
||||
// AuthenticateNewWorkConns specifies whether to include authentication token in
|
||||
// new work connections sent to frps. By default, this value is false.
|
||||
AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
|
||||
}
|
||||
|
||||
func getDefaultBaseConf() BaseConfig {
|
||||
return BaseConfig{
|
||||
AuthenticationMethod: "token",
|
||||
AuthenticateHeartBeats: false,
|
||||
AuthenticateNewWorkConns: false,
|
||||
}
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
BaseConfig `ini:",extends"`
|
||||
OidcClientConfig `ini:",extends"`
|
||||
TokenConfig `ini:",extends"`
|
||||
}
|
||||
|
||||
func GetDefaultClientConf() ClientConfig {
|
||||
return ClientConfig{
|
||||
BaseConfig: getDefaultBaseConf(),
|
||||
OidcClientConfig: getDefaultOidcClientConf(),
|
||||
TokenConfig: getDefaultTokenConf(),
|
||||
}
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
BaseConfig `ini:",extends"`
|
||||
OidcServerConfig `ini:",extends"`
|
||||
TokenConfig `ini:",extends"`
|
||||
}
|
||||
|
||||
func GetDefaultServerConf() ServerConfig {
|
||||
return ServerConfig{
|
||||
BaseConfig: getDefaultBaseConf(),
|
||||
OidcServerConfig: getDefaultOidcServerConf(),
|
||||
TokenConfig: getDefaultTokenConf(),
|
||||
}
|
||||
}
|
||||
|
||||
type Setter interface {
|
||||
SetLogin(*msg.Login) error
|
||||
SetPing(*msg.Ping) error
|
||||
SetNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
func NewAuthSetter(cfg ClientConfig) (authProvider Setter) {
|
||||
switch cfg.AuthenticationMethod {
|
||||
case consts.TokenAuthMethod:
|
||||
authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
|
||||
case consts.OidcAuthMethod:
|
||||
authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig)
|
||||
default:
|
||||
panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod))
|
||||
}
|
||||
type ClientAuth struct {
|
||||
Setter Setter
|
||||
key []byte
|
||||
}
|
||||
|
||||
return authProvider
|
||||
func (a *ClientAuth) EncryptionKey() []byte {
|
||||
return a.key
|
||||
}
|
||||
|
||||
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||
// Caller must run validation before calling this function.
|
||||
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("auth config is nil")
|
||||
}
|
||||
resolved := *cfg
|
||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
resolved.Token = token
|
||||
}
|
||||
setter, err := NewAuthSetter(resolved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ClientAuth{
|
||||
Setter: setter,
|
||||
key: []byte(resolved.Token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
||||
switch cfg.Method {
|
||||
case v1.AuthMethodToken:
|
||||
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||
case v1.AuthMethodOIDC:
|
||||
if cfg.OIDC.TokenSource != nil {
|
||||
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
|
||||
} else {
|
||||
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
||||
}
|
||||
return authProvider, nil
|
||||
}
|
||||
|
||||
type Verifier interface {
|
||||
@ -96,13 +86,42 @@ type Verifier interface {
|
||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) {
|
||||
switch cfg.AuthenticationMethod {
|
||||
case consts.TokenAuthMethod:
|
||||
authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
|
||||
case consts.OidcAuthMethod:
|
||||
authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig)
|
||||
}
|
||||
type ServerAuth struct {
|
||||
Verifier Verifier
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (a *ServerAuth) EncryptionKey() []byte {
|
||||
return a.key
|
||||
}
|
||||
|
||||
// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||
// Caller must run validation before calling this function.
|
||||
func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("auth config is nil")
|
||||
}
|
||||
resolved := *cfg
|
||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
resolved.Token = token
|
||||
}
|
||||
return &ServerAuth{
|
||||
Verifier: NewAuthVerifier(resolved),
|
||||
key: []byte(resolved.Token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||
switch cfg.Method {
|
||||
case v1.AuthMethodToken:
|
||||
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||
case v1.AuthMethodOIDC:
|
||||
tokenVerifier := NewTokenVerifier(cfg.OIDC)
|
||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
|
||||
}
|
||||
return authVerifier
|
||||
}
|
||||
|
||||
145
pkg/auth/legacy/legacy.go
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package legacy
|
||||
|
||||
type BaseConfig struct {
|
||||
// AuthenticationMethod specifies what authentication method to use to
|
||||
// authenticate frpc with frps. If "token" is specified - token will be
|
||||
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
|
||||
// token will be issued using OIDC settings. By default, this value is "token".
|
||||
AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
|
||||
// AuthenticateHeartBeats specifies whether to include authentication token in
|
||||
// heartbeats sent to frps. By default, this value is false.
|
||||
AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
|
||||
// AuthenticateNewWorkConns specifies whether to include authentication token in
|
||||
// new work connections sent to frps. By default, this value is false.
|
||||
AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
|
||||
}
|
||||
|
||||
func getDefaultBaseConf() BaseConfig {
|
||||
return BaseConfig{
|
||||
AuthenticationMethod: "token",
|
||||
AuthenticateHeartBeats: false,
|
||||
AuthenticateNewWorkConns: false,
|
||||
}
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
BaseConfig `ini:",extends"`
|
||||
OidcClientConfig `ini:",extends"`
|
||||
TokenConfig `ini:",extends"`
|
||||
}
|
||||
|
||||
func GetDefaultClientConf() ClientConfig {
|
||||
return ClientConfig{
|
||||
BaseConfig: getDefaultBaseConf(),
|
||||
OidcClientConfig: getDefaultOidcClientConf(),
|
||||
TokenConfig: getDefaultTokenConf(),
|
||||
}
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
BaseConfig `ini:",extends"`
|
||||
OidcServerConfig `ini:",extends"`
|
||||
TokenConfig `ini:",extends"`
|
||||
}
|
||||
|
||||
func GetDefaultServerConf() ServerConfig {
|
||||
return ServerConfig{
|
||||
BaseConfig: getDefaultBaseConf(),
|
||||
OidcServerConfig: getDefaultOidcServerConf(),
|
||||
TokenConfig: getDefaultTokenConf(),
|
||||
}
|
||||
}
|
||||
|
||||
type OidcClientConfig struct {
|
||||
// OidcClientID specifies the client ID to use to get a token in OIDC
|
||||
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||
// is "".
|
||||
OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
|
||||
// OidcClientSecret specifies the client secret to use to get a token in OIDC
|
||||
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||
// is "".
|
||||
OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
|
||||
// OidcAudience specifies the audience of the token in OIDC authentication
|
||||
// if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
|
||||
// OidcScope specifies the scope of the token in OIDC authentication
|
||||
// if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
|
||||
// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
|
||||
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
|
||||
// By default, this value is "".
|
||||
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
|
||||
|
||||
// OidcAdditionalEndpointParams specifies additional parameters to be sent
|
||||
// this field will be transfer to map[string][]string in OIDC token generator
|
||||
// The field will be set by prefix "oidc_additional_"
|
||||
OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
|
||||
}
|
||||
|
||||
func getDefaultOidcClientConf() OidcClientConfig {
|
||||
return OidcClientConfig{
|
||||
OidcClientID: "",
|
||||
OidcClientSecret: "",
|
||||
OidcAudience: "",
|
||||
OidcScope: "",
|
||||
OidcTokenEndpointURL: "",
|
||||
OidcAdditionalEndpointParams: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
type OidcServerConfig struct {
|
||||
// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
|
||||
// will be used to load public keys to verify signature and will be compared
|
||||
// with the issuer claim in the OIDC token. It will be used if
|
||||
// AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
|
||||
// OidcAudience specifies the audience OIDC tokens should contain when validated.
|
||||
// If this value is empty, audience ("client ID") verification will be skipped.
|
||||
// It will be used when AuthenticationMethod == "oidc". By default, this
|
||||
// value is "".
|
||||
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
|
||||
// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
|
||||
// expired. It will be used when AuthenticationMethod == "oidc". By default, this
|
||||
// value is false.
|
||||
OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
|
||||
// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
|
||||
// issuer claim matches the issuer specified in OidcIssuer. It will be used when
|
||||
// AuthenticationMethod == "oidc". By default, this value is false.
|
||||
OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
|
||||
}
|
||||
|
||||
func getDefaultOidcServerConf() OidcServerConfig {
|
||||
return OidcServerConfig{
|
||||
OidcIssuer: "",
|
||||
OidcAudience: "",
|
||||
OidcSkipExpiryCheck: false,
|
||||
OidcSkipIssuerCheck: false,
|
||||
}
|
||||
}
|
||||
|
||||
type TokenConfig struct {
|
||||
// Token specifies the authorization token used to create keys to be sent
|
||||
// to the server. The server must have a matching token for authorization
|
||||
// to succeed. By default, this value is "".
|
||||
Token string `ini:"token" json:"token"`
|
||||
}
|
||||
|
||||
func getDefaultTokenConf() TokenConfig {
|
||||
return TokenConfig{
|
||||
Token: "",
|
||||
}
|
||||
}
|
||||
242
pkg/auth/oidc.go
@ -16,113 +16,113 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
type OidcClientConfig struct {
|
||||
// OidcClientID specifies the client ID to use to get a token in OIDC
|
||||
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||
// is "".
|
||||
OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
|
||||
// OidcClientSecret specifies the client secret to use to get a token in OIDC
|
||||
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||
// is "".
|
||||
OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
|
||||
// OidcAudience specifies the audience of the token in OIDC authentication
|
||||
// if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
|
||||
// OidcScope specifies the scope of the token in OIDC authentication
|
||||
// if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
|
||||
// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
|
||||
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
|
||||
// By default, this value is "".
|
||||
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
|
||||
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
|
||||
func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) {
|
||||
// Clone the default transport to get all reasonable defaults
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// 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"`
|
||||
}
|
||||
// Configure TLS settings
|
||||
if trustedCAFile != "" || insecureSkipVerify {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
func getDefaultOidcClientConf() OidcClientConfig {
|
||||
return OidcClientConfig{
|
||||
OidcClientID: "",
|
||||
OidcClientSecret: "",
|
||||
OidcAudience: "",
|
||||
OidcScope: "",
|
||||
OidcTokenEndpointURL: "",
|
||||
OidcAdditionalEndpointParams: make(map[string]string),
|
||||
if trustedCAFile != "" && !insecureSkipVerify {
|
||||
caCert, err := os.ReadFile(trustedCAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile)
|
||||
}
|
||||
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
}
|
||||
|
||||
type OidcServerConfig struct {
|
||||
// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
|
||||
// will be used to load public keys to verify signature and will be compared
|
||||
// with the issuer claim in the OIDC token. It will be used if
|
||||
// AuthenticationMethod == "oidc". By default, this value is "".
|
||||
OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
|
||||
// OidcAudience specifies the audience OIDC tokens should contain when validated.
|
||||
// If this value is empty, audience ("client ID") verification will be skipped.
|
||||
// It will be used when AuthenticationMethod == "oidc". By default, this
|
||||
// value is "".
|
||||
OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
|
||||
// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
|
||||
// expired. It will be used when AuthenticationMethod == "oidc". By default, this
|
||||
// value is false.
|
||||
OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
|
||||
// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
|
||||
// issuer claim matches the issuer specified in OidcIssuer. It will be used when
|
||||
// AuthenticationMethod == "oidc". By default, this value is false.
|
||||
OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
|
||||
}
|
||||
|
||||
func getDefaultOidcServerConf() OidcServerConfig {
|
||||
return OidcServerConfig{
|
||||
OidcIssuer: "",
|
||||
OidcAudience: "",
|
||||
OidcSkipExpiryCheck: false,
|
||||
OidcSkipIssuerCheck: false,
|
||||
// Configure proxy settings
|
||||
if proxyURL != "" {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err)
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(parsedURL)
|
||||
} else {
|
||||
// Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment
|
||||
transport.Proxy = nil
|
||||
}
|
||||
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}
|
||||
|
||||
type OidcAuthProvider struct {
|
||||
BaseConfig
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenGenerator *clientcredentials.Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider {
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||
eps := make(map[string][]string)
|
||||
for k, v := range cfg.OidcAdditionalEndpointParams {
|
||||
for k, v := range cfg.AdditionalEndpointParams {
|
||||
eps[k] = []string{v}
|
||||
}
|
||||
|
||||
if cfg.OidcAudience != "" {
|
||||
eps["audience"] = []string{cfg.OidcAudience}
|
||||
if cfg.Audience != "" {
|
||||
eps["audience"] = []string{cfg.Audience}
|
||||
}
|
||||
|
||||
tokenGenerator := &clientcredentials.Config{
|
||||
ClientID: cfg.OidcClientID,
|
||||
ClientSecret: cfg.OidcClientSecret,
|
||||
Scopes: []string{cfg.OidcScope},
|
||||
TokenURL: cfg.OidcTokenEndpointURL,
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Scopes: []string{cfg.Scope},
|
||||
TokenURL: cfg.TokenEndpointURL,
|
||||
EndpointParams: eps,
|
||||
}
|
||||
|
||||
return &OidcAuthProvider{
|
||||
BaseConfig: baseCfg,
|
||||
tokenGenerator: tokenGenerator,
|
||||
// Create custom HTTP client if needed
|
||||
var httpClient *http.Client
|
||||
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
||||
var err error
|
||||
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &OidcAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
tokenGenerator: tokenGenerator,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||
ctx := context.Background()
|
||||
if auth.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
|
||||
}
|
||||
|
||||
tokenObj, err := auth.tokenGenerator.Token(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||
}
|
||||
@ -135,7 +135,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
if !auth.AuthenticateHeartBeats {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !auth.AuthenticateNewWorkConns {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -152,27 +152,81 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
||||
return err
|
||||
}
|
||||
|
||||
type OidcAuthConsumer struct {
|
||||
BaseConfig
|
||||
type OidcTokenSourceAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
verifier *oidc.IDTokenVerifier
|
||||
subjectFromLogin string
|
||||
valueSource *v1.ValueSource
|
||||
}
|
||||
|
||||
func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer {
|
||||
provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer)
|
||||
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
|
||||
return &OidcTokenSourceAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
valueSource: valueSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
ctx := context.Background()
|
||||
accessToken, err = auth.valueSource.Resolve(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
type TokenVerifier interface {
|
||||
Verify(context.Context, string) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
type OidcAuthConsumer struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
verifier TokenVerifier
|
||||
subjectsFromLogin []string
|
||||
}
|
||||
|
||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
verifierConf := oidc.Config{
|
||||
ClientID: cfg.OidcAudience,
|
||||
SkipClientIDCheck: cfg.OidcAudience == "",
|
||||
SkipExpiryCheck: cfg.OidcSkipExpiryCheck,
|
||||
SkipIssuerCheck: cfg.OidcSkipIssuerCheck,
|
||||
ClientID: cfg.Audience,
|
||||
SkipClientIDCheck: cfg.Audience == "",
|
||||
SkipExpiryCheck: cfg.SkipExpiryCheck,
|
||||
SkipIssuerCheck: cfg.SkipIssuerCheck,
|
||||
}
|
||||
return provider.Verifier(&verifierConf)
|
||||
}
|
||||
|
||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
|
||||
return &OidcAuthConsumer{
|
||||
BaseConfig: baseCfg,
|
||||
verifier: provider.Verifier(&verifierConf),
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
verifier: verifier,
|
||||
subjectsFromLogin: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,7 +235,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||
}
|
||||
auth.subjectFromLogin = token.Subject
|
||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -190,17 +246,17 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||
}
|
||||
if token.Subject != auth.subjectFromLogin {
|
||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||
"original subject: %s, "+
|
||||
"original subjects: %s, "+
|
||||
"new subject: %s",
|
||||
auth.subjectFromLogin, token.Subject)
|
||||
auth.subjectsFromLogin, token.Subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
|
||||
if !auth.AuthenticateHeartBeats {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -208,7 +264,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !auth.AuthenticateNewWorkConns {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
64
pkg/auth/oidc_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
type mockTokenVerifier struct{}
|
||||
|
||||
func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.IDToken, error) {
|
||||
return &oidc.IDToken{
|
||||
Subject: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
err := consumer.VerifyPing(&msg.Ping{
|
||||
PrivilegeKey: "ping-without-login",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||
}
|
||||
|
||||
func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
err := consumer.VerifyLogin(&msg.Login{
|
||||
PrivilegeKey: "ping-after-login",
|
||||
})
|
||||
r.NoError(err)
|
||||
|
||||
err = consumer.VerifyPing(&msg.Ping{
|
||||
PrivilegeKey: "ping-after-login",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
r.NoError(err)
|
||||
}
|
||||
|
||||
func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
err := consumer.VerifyLogin(&msg.Login{
|
||||
PrivilegeKey: "login-with-first-subject",
|
||||
})
|
||||
r.NoError(err)
|
||||
|
||||
err = consumer.VerifyPing(&msg.Ping{
|
||||
PrivilegeKey: "ping-with-different-subject",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2016 fatedier, fatedier@gmail.com
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,30 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package consts
|
||||
package auth
|
||||
|
||||
var (
|
||||
// proxy status
|
||||
Idle = "idle"
|
||||
Working = "working"
|
||||
Closed = "closed"
|
||||
Online = "online"
|
||||
Offline = "offline"
|
||||
|
||||
// proxy type
|
||||
TCPProxy = "tcp"
|
||||
UDPProxy = "udp"
|
||||
TCPMuxProxy = "tcpmux"
|
||||
HTTPProxy = "http"
|
||||
HTTPSProxy = "https"
|
||||
STCPProxy = "stcp"
|
||||
XTCPProxy = "xtcp"
|
||||
SUDPProxy = "sudp"
|
||||
|
||||
// authentication method
|
||||
TokenAuthMethod = "token"
|
||||
OidcAuthMethod = "oidc"
|
||||
|
||||
// TCP multiplexer
|
||||
HTTPConnectTCPMultiplexer = "httpconnect"
|
||||
import (
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
var AlwaysPassVerifier = &alwaysPass{}
|
||||
|
||||
var _ Verifier = &alwaysPass{}
|
||||
|
||||
type alwaysPass struct{}
|
||||
|
||||
func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil }
|
||||
|
||||
func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil }
|
||||
|
||||
func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil }
|
||||
@ -16,45 +16,33 @@ package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
type TokenConfig struct {
|
||||
// Token specifies the authorization token used to create keys to be sent
|
||||
// to the server. The server must have a matching token for authorization
|
||||
// to succeed. By default, this value is "".
|
||||
Token string `ini:"token" json:"token"`
|
||||
}
|
||||
|
||||
func getDefaultTokenConf() TokenConfig {
|
||||
return TokenConfig{
|
||||
Token: "",
|
||||
}
|
||||
}
|
||||
|
||||
type TokenAuthSetterVerifier struct {
|
||||
BaseConfig
|
||||
|
||||
token string
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
token string
|
||||
}
|
||||
|
||||
func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier {
|
||||
func NewTokenAuth(additionalAuthScopes []v1.AuthScope, token string) *TokenAuthSetterVerifier {
|
||||
return &TokenAuthSetterVerifier{
|
||||
BaseConfig: baseCfg,
|
||||
token: cfg.Token,
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error {
|
||||
loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
|
||||
if !auth.AuthenticateHeartBeats {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -64,7 +52,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
|
||||
if !auth.AuthenticateNewWorkConns {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -81,7 +69,7 @@ func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
|
||||
if !auth.AuthenticateHeartBeats {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -92,7 +80,7 @@ func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
|
||||
if !auth.AuthenticateNewWorkConns {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,680 +0,0 @@
|
||||
// Copyright 2020 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/consts"
|
||||
)
|
||||
|
||||
const (
|
||||
testUser = "test"
|
||||
)
|
||||
|
||||
var testClientBytesWithFull = []byte(`
|
||||
# [common] is integral section
|
||||
[common]
|
||||
server_addr = 0.0.0.9
|
||||
server_port = 7009
|
||||
http_proxy = http://user:passwd@192.168.1.128:8080
|
||||
log_file = ./frpc.log9
|
||||
log_way = file
|
||||
log_level = info9
|
||||
log_max_days = 39
|
||||
disable_log_color = false
|
||||
authenticate_heartbeats = false
|
||||
authenticate_new_work_conns = false
|
||||
token = 12345678
|
||||
oidc_client_id = client-id
|
||||
oidc_client_secret = client-secret
|
||||
oidc_audience = audience
|
||||
oidc_token_endpoint_url = endpoint_url
|
||||
admin_addr = 127.0.0.9
|
||||
admin_port = 7409
|
||||
admin_user = admin9
|
||||
admin_pwd = admin9
|
||||
assets_dir = ./static9
|
||||
pool_count = 59
|
||||
tcp_mux
|
||||
user = your_name
|
||||
login_fail_exit
|
||||
protocol = tcp
|
||||
tls_enable = true
|
||||
tls_cert_file = client.crt
|
||||
tls_key_file = client.key
|
||||
tls_trusted_ca_file = ca.crt
|
||||
tls_server_name = example.com
|
||||
dns_server = 8.8.8.9
|
||||
start = ssh,dns
|
||||
heartbeat_interval = 39
|
||||
heartbeat_timeout = 99
|
||||
meta_var1 = 123
|
||||
meta_var2 = 234
|
||||
udp_packet_size = 1509
|
||||
|
||||
# all proxy
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.9
|
||||
local_port = 29
|
||||
bandwidth_limit = 19MB
|
||||
bandwidth_limit_mode = server
|
||||
use_encryption
|
||||
use_compression
|
||||
remote_port = 6009
|
||||
group = test_group
|
||||
group_key = 123456
|
||||
health_check_type = tcp
|
||||
health_check_timeout_s = 3
|
||||
health_check_max_failed = 3
|
||||
health_check_interval_s = 19
|
||||
meta_var1 = 123
|
||||
meta_var2 = 234
|
||||
|
||||
[ssh_random]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.9
|
||||
local_port = 29
|
||||
remote_port = 9
|
||||
|
||||
[range:tcp_port]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.9
|
||||
local_port = 6010-6011,6019
|
||||
remote_port = 6010-6011,6019
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[dns]
|
||||
type = udp
|
||||
local_ip = 114.114.114.114
|
||||
local_port = 59
|
||||
remote_port = 6009
|
||||
use_encryption
|
||||
use_compression
|
||||
|
||||
[range:udp_port]
|
||||
type = udp
|
||||
local_ip = 114.114.114.114
|
||||
local_port = 6000,6010-6011
|
||||
remote_port = 6000,6010-6011
|
||||
use_encryption
|
||||
use_compression
|
||||
|
||||
[web01]
|
||||
type = http
|
||||
local_ip = 127.0.0.9
|
||||
local_port = 89
|
||||
use_encryption
|
||||
use_compression
|
||||
http_user = admin
|
||||
http_pwd = admin
|
||||
subdomain = web01
|
||||
custom_domains = web02.yourdomain.com
|
||||
locations = /,/pic
|
||||
host_header_rewrite = example.com
|
||||
header_X-From-Where = frp
|
||||
health_check_type = http
|
||||
health_check_url = /status
|
||||
health_check_interval_s = 19
|
||||
health_check_max_failed = 3
|
||||
health_check_timeout_s = 3
|
||||
|
||||
[web02]
|
||||
type = https
|
||||
local_ip = 127.0.0.9
|
||||
local_port = 8009
|
||||
use_encryption
|
||||
use_compression
|
||||
subdomain = web01
|
||||
custom_domains = web02.yourdomain.com
|
||||
proxy_protocol_version = v2
|
||||
|
||||
[secret_tcp]
|
||||
type = stcp
|
||||
sk = abcdefg
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 22
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[p2p_tcp]
|
||||
type = xtcp
|
||||
sk = abcdefg
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 22
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[tcpmuxhttpconnect]
|
||||
type = tcpmux
|
||||
multiplexer = httpconnect
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 10701
|
||||
custom_domains = tunnel1
|
||||
|
||||
[plugin_unix_domain_socket]
|
||||
type = tcp
|
||||
remote_port = 6003
|
||||
plugin = unix_domain_socket
|
||||
plugin_unix_path = /var/run/docker.sock
|
||||
|
||||
[plugin_http_proxy]
|
||||
type = tcp
|
||||
remote_port = 6004
|
||||
plugin = http_proxy
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
|
||||
[plugin_socks5]
|
||||
type = tcp
|
||||
remote_port = 6005
|
||||
plugin = socks5
|
||||
plugin_user = abc
|
||||
plugin_passwd = abc
|
||||
|
||||
[plugin_static_file]
|
||||
type = tcp
|
||||
remote_port = 6006
|
||||
plugin = static_file
|
||||
plugin_local_path = /var/www/blog
|
||||
plugin_strip_prefix = static
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
|
||||
[plugin_https2http]
|
||||
type = https
|
||||
custom_domains = test.yourdomain.com
|
||||
plugin = https2http
|
||||
plugin_local_addr = 127.0.0.1:80
|
||||
plugin_crt_path = ./server.crt
|
||||
plugin_key_path = ./server.key
|
||||
plugin_host_header_rewrite = 127.0.0.1
|
||||
plugin_header_X-From-Where = frp
|
||||
|
||||
[plugin_http2https]
|
||||
type = http
|
||||
custom_domains = test.yourdomain.com
|
||||
plugin = http2https
|
||||
plugin_local_addr = 127.0.0.1:443
|
||||
plugin_host_header_rewrite = 127.0.0.1
|
||||
plugin_header_X-From-Where = frp
|
||||
|
||||
# visitor
|
||||
[secret_tcp_visitor]
|
||||
role = visitor
|
||||
type = stcp
|
||||
server_name = secret_tcp
|
||||
sk = abcdefg
|
||||
bind_addr = 127.0.0.1
|
||||
bind_port = 9000
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[p2p_tcp_visitor]
|
||||
role = visitor
|
||||
type = xtcp
|
||||
server_name = p2p_tcp
|
||||
sk = abcdefg
|
||||
bind_addr = 127.0.0.1
|
||||
bind_port = 9001
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
`)
|
||||
|
||||
func Test_LoadClientCommonConf(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := ClientCommonConf{
|
||||
ClientConfig: auth.ClientConfig{
|
||||
BaseConfig: auth.BaseConfig{
|
||||
AuthenticationMethod: "token",
|
||||
AuthenticateHeartBeats: false,
|
||||
AuthenticateNewWorkConns: false,
|
||||
},
|
||||
TokenConfig: auth.TokenConfig{
|
||||
Token: "12345678",
|
||||
},
|
||||
OidcClientConfig: auth.OidcClientConfig{
|
||||
OidcClientID: "client-id",
|
||||
OidcClientSecret: "client-secret",
|
||||
OidcAudience: "audience",
|
||||
OidcTokenEndpointURL: "endpoint_url",
|
||||
},
|
||||
},
|
||||
ServerAddr: "0.0.0.9",
|
||||
ServerPort: 7009,
|
||||
NatHoleSTUNServer: "stun.easyvoip.com:3478",
|
||||
DialServerTimeout: 10,
|
||||
DialServerKeepAlive: 7200,
|
||||
HTTPProxy: "http://user:passwd@192.168.1.128:8080",
|
||||
LogFile: "./frpc.log9",
|
||||
LogWay: "file",
|
||||
LogLevel: "info9",
|
||||
LogMaxDays: 39,
|
||||
DisableLogColor: false,
|
||||
AdminAddr: "127.0.0.9",
|
||||
AdminPort: 7409,
|
||||
AdminUser: "admin9",
|
||||
AdminPwd: "admin9",
|
||||
AssetsDir: "./static9",
|
||||
PoolCount: 59,
|
||||
TCPMux: true,
|
||||
TCPMuxKeepaliveInterval: 60,
|
||||
User: "your_name",
|
||||
LoginFailExit: true,
|
||||
Protocol: "tcp",
|
||||
QUICKeepalivePeriod: 10,
|
||||
QUICMaxIdleTimeout: 30,
|
||||
QUICMaxIncomingStreams: 100000,
|
||||
TLSEnable: true,
|
||||
TLSCertFile: "client.crt",
|
||||
TLSKeyFile: "client.key",
|
||||
TLSTrustedCaFile: "ca.crt",
|
||||
TLSServerName: "example.com",
|
||||
DisableCustomTLSFirstByte: true,
|
||||
DNSServer: "8.8.8.9",
|
||||
Start: []string{"ssh", "dns"},
|
||||
HeartbeatInterval: 39,
|
||||
HeartbeatTimeout: 99,
|
||||
Metas: map[string]string{
|
||||
"var1": "123",
|
||||
"var2": "234",
|
||||
},
|
||||
UDPPacketSize: 1509,
|
||||
IncludeConfigFiles: []string{},
|
||||
}
|
||||
|
||||
common, err := UnmarshalClientConfFromIni(testClientBytesWithFull)
|
||||
assert.NoError(err)
|
||||
assert.EqualValues(expected, common)
|
||||
}
|
||||
|
||||
func Test_LoadClientBasicConf(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
proxyExpected := map[string]ProxyConf{
|
||||
testUser + ".ssh": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".ssh",
|
||||
ProxyType: consts.TCPProxy,
|
||||
UseCompression: true,
|
||||
UseEncryption: true,
|
||||
Group: "test_group",
|
||||
GroupKey: "123456",
|
||||
BandwidthLimit: MustBandwidthQuantity("19MB"),
|
||||
BandwidthLimitMode: BandwidthLimitModeServer,
|
||||
Metas: map[string]string{
|
||||
"var1": "123",
|
||||
"var2": "234",
|
||||
},
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 29,
|
||||
},
|
||||
HealthCheckConf: HealthCheckConf{
|
||||
HealthCheckType: consts.TCPProxy,
|
||||
HealthCheckTimeoutS: 3,
|
||||
HealthCheckMaxFailed: 3,
|
||||
HealthCheckIntervalS: 19,
|
||||
HealthCheckAddr: "127.0.0.9:29",
|
||||
},
|
||||
},
|
||||
RemotePort: 6009,
|
||||
},
|
||||
testUser + ".ssh_random": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".ssh_random",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 29,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 9,
|
||||
},
|
||||
testUser + ".tcp_port_0": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".tcp_port_0",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 6010,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6010,
|
||||
},
|
||||
testUser + ".tcp_port_1": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".tcp_port_1",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 6011,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6011,
|
||||
},
|
||||
testUser + ".tcp_port_2": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".tcp_port_2",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 6019,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6019,
|
||||
},
|
||||
testUser + ".dns": &UDPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".dns",
|
||||
ProxyType: consts.UDPProxy,
|
||||
UseEncryption: true,
|
||||
UseCompression: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "114.114.114.114",
|
||||
LocalPort: 59,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6009,
|
||||
},
|
||||
testUser + ".udp_port_0": &UDPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".udp_port_0",
|
||||
ProxyType: consts.UDPProxy,
|
||||
UseEncryption: true,
|
||||
UseCompression: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "114.114.114.114",
|
||||
LocalPort: 6000,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6000,
|
||||
},
|
||||
testUser + ".udp_port_1": &UDPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".udp_port_1",
|
||||
ProxyType: consts.UDPProxy,
|
||||
UseEncryption: true,
|
||||
UseCompression: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "114.114.114.114",
|
||||
LocalPort: 6010,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6010,
|
||||
},
|
||||
testUser + ".udp_port_2": &UDPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".udp_port_2",
|
||||
ProxyType: consts.UDPProxy,
|
||||
UseEncryption: true,
|
||||
UseCompression: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "114.114.114.114",
|
||||
LocalPort: 6011,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6011,
|
||||
},
|
||||
testUser + ".web01": &HTTPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".web01",
|
||||
ProxyType: consts.HTTPProxy,
|
||||
UseCompression: true,
|
||||
UseEncryption: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 89,
|
||||
},
|
||||
HealthCheckConf: HealthCheckConf{
|
||||
HealthCheckType: consts.HTTPProxy,
|
||||
HealthCheckTimeoutS: 3,
|
||||
HealthCheckMaxFailed: 3,
|
||||
HealthCheckIntervalS: 19,
|
||||
HealthCheckURL: "http://127.0.0.9:89/status",
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
DomainConf: DomainConf{
|
||||
CustomDomains: []string{"web02.yourdomain.com"},
|
||||
SubDomain: "web01",
|
||||
},
|
||||
Locations: []string{"/", "/pic"},
|
||||
HTTPUser: "admin",
|
||||
HTTPPwd: "admin",
|
||||
HostHeaderRewrite: "example.com",
|
||||
Headers: map[string]string{
|
||||
"X-From-Where": "frp",
|
||||
},
|
||||
},
|
||||
testUser + ".web02": &HTTPSProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".web02",
|
||||
ProxyType: consts.HTTPSProxy,
|
||||
UseCompression: true,
|
||||
UseEncryption: true,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.9",
|
||||
LocalPort: 8009,
|
||||
},
|
||||
ProxyProtocolVersion: "v2",
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
DomainConf: DomainConf{
|
||||
CustomDomains: []string{"web02.yourdomain.com"},
|
||||
SubDomain: "web01",
|
||||
},
|
||||
},
|
||||
testUser + ".secret_tcp": &STCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".secret_tcp",
|
||||
ProxyType: consts.STCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
LocalPort: 22,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RoleServerCommonConf: RoleServerCommonConf{
|
||||
Role: "server",
|
||||
Sk: "abcdefg",
|
||||
},
|
||||
},
|
||||
testUser + ".p2p_tcp": &XTCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".p2p_tcp",
|
||||
ProxyType: consts.XTCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
LocalPort: 22,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RoleServerCommonConf: RoleServerCommonConf{
|
||||
Role: "server",
|
||||
Sk: "abcdefg",
|
||||
},
|
||||
},
|
||||
testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".tcpmuxhttpconnect",
|
||||
ProxyType: consts.TCPMuxProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
LocalPort: 10701,
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
DomainConf: DomainConf{
|
||||
CustomDomains: []string{"tunnel1"},
|
||||
SubDomain: "",
|
||||
},
|
||||
Multiplexer: "httpconnect",
|
||||
},
|
||||
testUser + ".plugin_unix_domain_socket": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_unix_domain_socket",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "unix_domain_socket",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_unix_path": "/var/run/docker.sock",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6003,
|
||||
},
|
||||
testUser + ".plugin_http_proxy": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_http_proxy",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "http_proxy",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_http_user": "abc",
|
||||
"plugin_http_passwd": "abc",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6004,
|
||||
},
|
||||
testUser + ".plugin_socks5": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_socks5",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "socks5",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_user": "abc",
|
||||
"plugin_passwd": "abc",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6005,
|
||||
},
|
||||
testUser + ".plugin_static_file": &TCPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_static_file",
|
||||
ProxyType: consts.TCPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "static_file",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_local_path": "/var/www/blog",
|
||||
"plugin_strip_prefix": "static",
|
||||
"plugin_http_user": "abc",
|
||||
"plugin_http_passwd": "abc",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
RemotePort: 6006,
|
||||
},
|
||||
testUser + ".plugin_https2http": &HTTPSProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_https2http",
|
||||
ProxyType: consts.HTTPSProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "https2http",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_local_addr": "127.0.0.1:80",
|
||||
"plugin_crt_path": "./server.crt",
|
||||
"plugin_key_path": "./server.key",
|
||||
"plugin_host_header_rewrite": "127.0.0.1",
|
||||
"plugin_header_X-From-Where": "frp",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
DomainConf: DomainConf{
|
||||
CustomDomains: []string{"test.yourdomain.com"},
|
||||
},
|
||||
},
|
||||
testUser + ".plugin_http2https": &HTTPProxyConf{
|
||||
BaseProxyConf: BaseProxyConf{
|
||||
ProxyName: testUser + ".plugin_http2https",
|
||||
ProxyType: consts.HTTPProxy,
|
||||
LocalSvrConf: LocalSvrConf{
|
||||
LocalIP: "127.0.0.1",
|
||||
Plugin: "http2https",
|
||||
PluginParams: map[string]string{
|
||||
"plugin_local_addr": "127.0.0.1:443",
|
||||
"plugin_host_header_rewrite": "127.0.0.1",
|
||||
"plugin_header_X-From-Where": "frp",
|
||||
},
|
||||
},
|
||||
BandwidthLimitMode: BandwidthLimitModeClient,
|
||||
},
|
||||
DomainConf: DomainConf{
|
||||
CustomDomains: []string{"test.yourdomain.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
visitorExpected := map[string]VisitorConf{
|
||||
testUser + ".secret_tcp_visitor": &STCPVisitorConf{
|
||||
BaseVisitorConf: BaseVisitorConf{
|
||||
ProxyName: testUser + ".secret_tcp_visitor",
|
||||
ProxyType: consts.STCPProxy,
|
||||
Role: "visitor",
|
||||
Sk: "abcdefg",
|
||||
ServerName: testVisitorPrefix + "secret_tcp",
|
||||
BindAddr: "127.0.0.1",
|
||||
BindPort: 9000,
|
||||
},
|
||||
},
|
||||
testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{
|
||||
BaseVisitorConf: BaseVisitorConf{
|
||||
ProxyName: testUser + ".p2p_tcp_visitor",
|
||||
ProxyType: consts.XTCPProxy,
|
||||
Role: "visitor",
|
||||
Sk: "abcdefg",
|
||||
ServerName: testProxyPrefix + "p2p_tcp",
|
||||
BindAddr: "127.0.0.1",
|
||||
BindPort: 9001,
|
||||
},
|
||||
Protocol: "quic",
|
||||
MaxRetriesAnHour: 8,
|
||||
MinRetryInterval: 90,
|
||||
FallbackTimeoutMs: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil)
|
||||
assert.NoError(err)
|
||||
assert.Equal(proxyExpected, proxyActual)
|
||||
assert.Equal(visitorExpected, visitorActual)
|
||||
}
|
||||
259
pkg/config/flags.go
Normal file
@ -0,0 +1,259 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
)
|
||||
|
||||
// WordSepNormalizeFunc changes all flags that contain "_" separators
|
||||
func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
if strings.Contains(name, "_") {
|
||||
return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-"))
|
||||
}
|
||||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
type RegisterFlagOption func(*registerFlagOptions)
|
||||
|
||||
type registerFlagOptions struct {
|
||||
sshMode bool
|
||||
}
|
||||
|
||||
func WithSSHMode() RegisterFlagOption {
|
||||
return func(o *registerFlagOptions) {
|
||||
o.sshMode = true
|
||||
}
|
||||
}
|
||||
|
||||
type BandwidthQuantityFlag struct {
|
||||
V *types.BandwidthQuantity
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) Set(s string) error {
|
||||
return f.V.UnmarshalString(s)
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) String() string {
|
||||
return f.V.String()
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer, opts ...RegisterFlagOption) {
|
||||
registerProxyBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
|
||||
|
||||
switch cc := c.(type) {
|
||||
case *v1.TCPProxyConfig:
|
||||
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
|
||||
case *v1.UDPProxyConfig:
|
||||
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
|
||||
case *v1.HTTPProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations")
|
||||
cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
|
||||
cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
|
||||
cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
|
||||
case *v1.HTTPSProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
case *v1.TCPMuxProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer")
|
||||
cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
|
||||
cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
|
||||
case *v1.STCPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
|
||||
case *v1.SUDPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
|
||||
case *v1.XTCPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
|
||||
}
|
||||
}
|
||||
|
||||
func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
options := ®isterFlagOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
|
||||
cmd.Flags().StringToStringVarP(&c.Metadatas, "metadatas", "", nil, "metadata key-value pairs (e.g., key1=value1,key2=value2)")
|
||||
cmd.Flags().StringToStringVarP(&c.Annotations, "annotations", "", nil, "annotation key-value pairs (e.g., key1=value1,key2=value2)")
|
||||
|
||||
if !options.sshMode {
|
||||
cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
||||
cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)")
|
||||
}
|
||||
}
|
||||
|
||||
func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains")
|
||||
cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
|
||||
}
|
||||
|
||||
func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer, opts ...RegisterFlagOption) {
|
||||
registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
|
||||
|
||||
// add visitor flags if exist
|
||||
}
|
||||
|
||||
func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
||||
cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
|
||||
cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
|
||||
cmd.Flags().StringVarP(&c.ServerUser, "server-user", "", "", "server user")
|
||||
cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
|
||||
cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
|
||||
}
|
||||
|
||||
func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig, opts ...RegisterFlagOption) {
|
||||
options := ®isterFlagOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
if !options.sshMode {
|
||||
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
|
||||
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
|
||||
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
|
||||
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
|
||||
cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
|
||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||
}
|
||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
|
||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||
}
|
||||
|
||||
type PortsRangeSliceFlag struct {
|
||||
V *[]types.PortsRange
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) String() string {
|
||||
if f.V == nil {
|
||||
return ""
|
||||
}
|
||||
return types.PortsRangeSlice(*f.V).String()
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) Set(s string) error {
|
||||
slice, err := types.NewPortsRangeSliceFromString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f.V = slice
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
type BoolFuncFlag struct {
|
||||
TrueFunc func()
|
||||
FalseFunc func()
|
||||
|
||||
v bool
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) String() string {
|
||||
return strconv.FormatBool(f.v)
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) Set(s string) error {
|
||||
f.v = strconv.FormatBool(f.v) == "true"
|
||||
|
||||
if !f.v {
|
||||
if f.FalseFunc != nil {
|
||||
f.FalseFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.TrueFunc != nil {
|
||||
f.TrueFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) Type() string {
|
||||
return "bool"
|
||||
}
|
||||
|
||||
func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...RegisterFlagOption) {
|
||||
cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
|
||||
cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
|
||||
cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
|
||||
cmd.PersistentFlags().IntVarP(&c.QUICBindPort, "quic_bind_port", "", 0, "quic bind udp port")
|
||||
cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
|
||||
cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
|
||||
cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
|
||||
cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
|
||||
cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
|
||||
cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
|
||||
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||
cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
|
||||
cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
|
||||
cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
|
||||
|
||||
webServerTLS := v1.TLSConfig{}
|
||||
cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
|
||||
cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
|
||||
cmd.PersistentFlags().VarP(&BoolFuncFlag{
|
||||
TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
|
||||
}, "dashboard_tls_mode", "", "if enable dashboard tls mode")
|
||||
}
|
||||