Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7aa38b303 | ||
|
|
605f3bdece | ||
|
|
764a626b6e | ||
|
|
c2454e7114 | ||
|
|
017d71717f | ||
|
|
bd200b1a3b | ||
|
|
c70ceff370 | ||
|
|
bb3d0e7140 | ||
|
|
cf396563f8 | ||
|
|
0b4f83cd04 | ||
|
|
e9f7a1a9f2 | ||
|
|
d644593342 | ||
|
|
427c4ca3ae | ||
|
|
f2d1f3739a | ||
|
|
c23894f156 | ||
|
|
cb459b02b6 | ||
|
|
8f633fe363 | ||
|
|
c62a1da161 | ||
|
|
f22f7d539c | ||
|
|
462c987f6d | ||
|
|
541878af4d | ||
|
|
b7435967b0 | ||
|
|
774478d071 | ||
|
|
fbeb6ca43a | ||
|
|
381245a439 | ||
|
|
01997deb98 | ||
|
|
d0347325fc | ||
|
|
519368b1fd | ||
|
|
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 |
@@ -2,24 +2,23 @@ version: 2
|
||||
jobs:
|
||||
go-version-latest:
|
||||
docker:
|
||||
- image: cimg/go:1.21-node
|
||||
- image: cimg/go:1.25-node
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- run: make
|
||||
- run: make alltest
|
||||
go-version-last:
|
||||
docker:
|
||||
- image: cimg/go:1.20-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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: v1.53
|
||||
|
||||
# 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.25'
|
||||
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@v9
|
||||
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.10
|
||||
|
||||
18
.github/workflows/goreleaser.yml
vendored
@@ -8,21 +8,29 @@ 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@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
go-version: '1.25'
|
||||
- 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@v4
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --release-notes=./Release.md
|
||||
|
||||
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@v8
|
||||
- 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
|
||||
|
||||
20
.gitignore
vendored
@@ -7,18 +7,6 @@
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
@@ -33,6 +21,14 @@ lastversion/
|
||||
dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
.autogen_ssh_key
|
||||
client.crt
|
||||
client.key
|
||||
|
||||
# Cache
|
||||
*.swp
|
||||
|
||||
# AI
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
.sisyphus/
|
||||
|
||||
229
.golangci.yml
@@ -1,146 +1,117 @@
|
||||
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
|
||||
- modernize
|
||||
- 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
|
||||
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
errcheck:
|
||||
check-type-assertions: false
|
||||
check-blank: false
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- exitAfterDefer
|
||||
gosec:
|
||||
excludes: ["G115", "G117", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
||||
severity: low
|
||||
confidence: low
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
lll:
|
||||
line-length: 160
|
||||
tab-width: 1
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-rules:
|
||||
- cancelled
|
||||
- marshalled
|
||||
modernize:
|
||||
disable:
|
||||
- omitzero
|
||||
unparam:
|
||||
check-exported: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- errcheck
|
||||
- maligned
|
||||
path: _test\.go$|^tests/|^samples/
|
||||
- linters:
|
||||
- revive
|
||||
- staticcheck
|
||||
text: use underscores in Go names
|
||||
- linters:
|
||||
- revive
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- revive
|
||||
text: "avoid meaningless package names"
|
||||
- linters:
|
||||
- revive
|
||||
text: "Go standard library package names"
|
||||
- linters:
|
||||
- unparam
|
||||
text: is always false
|
||||
paths:
|
||||
- .*\.pb\.go
|
||||
- .*\.gen\.go
|
||||
- genfiles$
|
||||
- vendor$
|
||||
- bin$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/fatedier/frp/)
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- .*\.pb\.go
|
||||
- .*\.gen\.go
|
||||
- genfiles$
|
||||
- vendor$
|
||||
- bin$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
# exclude:
|
||||
# - composite literal uses unkeyed fields
|
||||
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on test files.
|
||||
- path: _test\.go$|^tests/|^samples/
|
||||
linters:
|
||||
- errcheck
|
||||
- maligned
|
||||
- linters:
|
||||
- revive
|
||||
- stylecheck
|
||||
text: "use underscores in Go names"
|
||||
- linters:
|
||||
- revive
|
||||
text: "unused-parameter"
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: true
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
39
Makefile
@@ -1,17 +1,24 @@
|
||||
export PATH := $(GOPATH)/bin:$(PATH)
|
||||
export PATH := $(PATH):`go env GOPATH`/bin
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
||||
|
||||
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 ./...
|
||||
@@ -23,22 +30,22 @@ gci:
|
||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
go vet -tags "$(NOWEB_TAG)" ./...
|
||||
|
||||
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$(NOWEB_TAG)" -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$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
|
||||
|
||||
test: gotest
|
||||
|
||||
gotest:
|
||||
go test -v --cover ./assets/...
|
||||
go test -v --cover ./cmd/...
|
||||
go test -v --cover ./client/...
|
||||
go test -v --cover ./server/...
|
||||
go test -v --cover ./pkg/...
|
||||
go test -tags "$(NOWEB_TAG)" -v --cover ./assets/...
|
||||
go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/...
|
||||
go test -tags "$(NOWEB_TAG)" -v --cover ./client/...
|
||||
go test -tags "$(NOWEB_TAG)" -v --cover ./server/...
|
||||
go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/...
|
||||
|
||||
e2e:
|
||||
./hack/run-e2e.sh
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
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:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
|
||||
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
|
||||
|
||||
all: build
|
||||
|
||||
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_amd64 ./release/frpc_windows_amd64.exe
|
||||
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe
|
||||
|
||||
226
README.md
@@ -2,14 +2,52 @@
|
||||
|
||||
[](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)
|
||||
|
||||
## Sponsors
|
||||
|
||||
frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors. If you'd like to join them, please consider [sponsoring frp's development](https://github.com/sponsors/fatedier).
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<!--gold sponsors end-->
|
||||
@@ -42,7 +80,7 @@ frp also offers a P2P connect mode.
|
||||
* [Using Environment Variables](#using-environment-variables)
|
||||
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
||||
* [Server Dashboard](#server-dashboard)
|
||||
* [Admin UI](#admin-ui)
|
||||
* [Client Admin UI](#client-admin-ui)
|
||||
* [Monitor](#monitor)
|
||||
* [Prometheus](#prometheus)
|
||||
* [Authenticating the Client](#authenticating-the-client)
|
||||
@@ -71,9 +109,17 @@ frp also offers a P2P connect mode.
|
||||
* [Custom Subdomain Names](#custom-subdomain-names)
|
||||
* [URL Routing](#url-routing)
|
||||
* [TCP Port Multiplexing](#tcp-port-multiplexing)
|
||||
* [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy)
|
||||
* [Connecting to frps via PROXY](#connecting-to-frps-via-proxy)
|
||||
* [Port range mapping](#port-range-mapping)
|
||||
* [Client Plugins](#client-plugins)
|
||||
* [Server Manage Plugins](#server-manage-plugins)
|
||||
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
|
||||
* [Virtual Network (VirtualNet)](#virtual-network-virtualnet)
|
||||
* [Feature Gates](#feature-gates)
|
||||
* [Available Feature Gates](#available-feature-gates)
|
||||
* [Enabling Feature Gates](#enabling-feature-gates)
|
||||
* [Feature Lifecycle](#feature-lifecycle)
|
||||
* [Related Projects](#related-projects)
|
||||
* [Contributing](#contributing)
|
||||
* [Donation](#donation)
|
||||
* [GitHub Sponsors](#github-sponsors)
|
||||
@@ -91,8 +137,6 @@ We will transition from version 0 to version 1 at the appropriate time and will
|
||||
|
||||
### About V2
|
||||
|
||||
The overall situation is currently unfavorable, and there is significant pressure in both personal and professional aspects.
|
||||
|
||||
The complexity and difficulty of the v2 version are much higher than anticipated. I can only work on its development during fragmented time periods, and the constant interruptions disrupt productivity significantly. Given this situation, we will continue to optimize and iterate on the current version until we have more free time to proceed with the major version overhaul.
|
||||
|
||||
The concept behind v2 is based on my years of experience and reflection in the cloud-native domain, particularly in K8s and ServiceMesh. Its core is a modernized four-layer and seven-layer proxy, similar to envoy. This proxy itself is highly scalable, not only capable of implementing the functionality of intranet penetration but also applicable to various other domains. Building upon this highly scalable core, we aim to implement all the capabilities of frp v1 while also addressing the functionalities that were previously unachievable or difficult to implement in an elegant manner. Furthermore, we will maintain efficient development and iteration capabilities.
|
||||
@@ -198,11 +242,11 @@ This example implements multiple SSH services exposed through the same port usin
|
||||
|
||||
4. To access internal machine A using SSH ProxyCommand, assuming the username is "test":
|
||||
|
||||
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-a.example.com:22,proxyport=5002' test@machine-a`
|
||||
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-a.example.com`
|
||||
|
||||
5. To access internal machine B, the only difference is the domain name, assuming the username is "test":
|
||||
|
||||
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-b.example.com:22,proxyport=5002' test@machine-b`
|
||||
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-b.example.com`
|
||||
|
||||
### Accessing Internal Web Services with Custom Domains in LAN
|
||||
|
||||
@@ -218,7 +262,7 @@ Unfortunately, we cannot resolve a domain name to a local IP. However, we can us
|
||||
vhostHTTPPort = 8080
|
||||
```
|
||||
|
||||
If you want to configure an https proxy, you need to set up the `vhost_https_port`.
|
||||
If you want to configure an https proxy, you need to set up the `vhostHTTPSPort`.
|
||||
|
||||
2. Start `frps`:
|
||||
|
||||
@@ -337,7 +381,7 @@ Configure `frps` as described above, then:
|
||||
|
||||
### Enable HTTPS for a local HTTP(S) service
|
||||
|
||||
You may substitute `https2https` for the plugin, and point the `plugin_local_addr` to a HTTPS endpoint.
|
||||
You may substitute `https2https` for the plugin, and point the `localAddr` to a HTTPS endpoint.
|
||||
|
||||
1. Start `frpc` with the following configuration:
|
||||
|
||||
@@ -345,7 +389,6 @@ You may substitute `https2https` for the plugin, and point the `plugin_local_add
|
||||
# frpc.toml
|
||||
serverAddr = "x.x.x.x"
|
||||
serverPort = 7000
|
||||
vhostHTTPSPort = 443
|
||||
|
||||
[[proxies]]
|
||||
name = "test_https2http"
|
||||
@@ -369,7 +412,7 @@ To mitigate risks associated with exposing certain services directly to the publ
|
||||
|
||||
Configure `frps` same as above.
|
||||
|
||||
1. Start `frpc` on machine B with the following config. This example is for exposing the SSH service (port 22), and note the `sk` field for the preshared key, and that the `remote_port` field is removed here:
|
||||
1. Start `frpc` on machine B with the following config. This example is for exposing the SSH service (port 22), and note the `secretKey` field for the preshared key, and that the `remotePort` field is removed here:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
@@ -384,7 +427,7 @@ Configure `frps` same as above.
|
||||
localPort = 22
|
||||
```
|
||||
|
||||
2. Start another `frpc` (typically on another machine C) with the following config to access the SSH service with a security key (`sk` field):
|
||||
2. Start another `frpc` (typically on another machine C) with the following config to access the SSH service with a security key (`secretKey` field):
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
@@ -461,9 +504,11 @@ Read the full example configuration files to find out even more features not des
|
||||
|
||||
Examples use TOML format, but you can still use YAML or JSON.
|
||||
|
||||
[Full configuration file for frps (Server)](./conf/frps.toml)
|
||||
These configuration files is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||
|
||||
[Full configuration file for frpc (Client)](./conf/frpc.toml)
|
||||
[Full configuration file for frps (Server)](./conf/frps_full_example.toml)
|
||||
|
||||
[Full configuration file for frpc (Client)](./conf/frpc_full_example.toml)
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
@@ -479,7 +524,7 @@ name = "ssh"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}"
|
||||
remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }}
|
||||
```
|
||||
|
||||
With the config above, variables can be passed into `frpc` program like this:
|
||||
@@ -505,6 +550,7 @@ includes = ["./confd/*.toml"]
|
||||
|
||||
```toml
|
||||
# ./confd/test.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
@@ -520,13 +566,15 @@ Check frp's status and proxies' statistics information by Dashboard.
|
||||
Configure a port for dashboard to enable this feature:
|
||||
|
||||
```toml
|
||||
# The default value is 127.0.0.1. Change it to 0.0.0.0 when you want to access it from a public network.
|
||||
webServer.addr = "0.0.0.0"
|
||||
webServer.port = 7500
|
||||
# dashboard's username and password are both optional
|
||||
webServer.user = "admin"
|
||||
webServer.password = "admin"
|
||||
```
|
||||
|
||||
Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`.
|
||||
Then visit `http://[serverAddr]:7500` to see the dashboard, with username and password both being `admin`.
|
||||
|
||||
Additionally, you can use HTTPS port by using your domains wildcard or normal SSL certificate:
|
||||
|
||||
@@ -539,7 +587,7 @@ webServer.tls.certFile = "server.crt"
|
||||
webServer.tls.keyFile = "server.key"
|
||||
```
|
||||
|
||||
Then visit `https://[server_addr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`.
|
||||
Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`.
|
||||
|
||||

|
||||
|
||||
@@ -586,6 +634,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b
|
||||
|
||||
Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
|
||||
|
||||
##### Token Source
|
||||
|
||||
frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported.
|
||||
|
||||
**File-based token source:**
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
auth.method = "token"
|
||||
auth.tokenSource.type = "file"
|
||||
auth.tokenSource.file.path = "/path/to/token/file"
|
||||
```
|
||||
|
||||
The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons.
|
||||
|
||||
#### OIDC Authentication
|
||||
|
||||
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
|
||||
@@ -616,6 +679,7 @@ The features are off by default. You can turn on encryption and/or compression:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
@@ -737,6 +801,14 @@ Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to l
|
||||
|
||||
**Note that global client parameters won't be modified except 'start'.**
|
||||
|
||||
`start` is a global allowlist evaluated after all sources are merged (config file/include/store).
|
||||
If `start` is non-empty, any proxy or visitor not listed there will not be started, including
|
||||
entries created via Store API.
|
||||
|
||||
`start` is kept mainly for compatibility and is generally not recommended for new configurations.
|
||||
Prefer per-proxy/per-visitor `enabled`, and keep `start` empty unless you explicitly want this
|
||||
global allowlist behavior.
|
||||
|
||||
You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors.
|
||||
|
||||
### Get proxy status from client
|
||||
@@ -771,6 +843,7 @@ We would like to try to allow multiple proxies bind a same remote port with diff
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
@@ -791,7 +864,7 @@ You can disable this feature by modify `frps.toml` and `frpc.toml`:
|
||||
|
||||
```toml
|
||||
# frps.toml and frpc.toml, must be same
|
||||
tcpMux = false
|
||||
transport.tcpMux = false
|
||||
```
|
||||
|
||||
### Support KCP Protocol
|
||||
@@ -836,7 +909,7 @@ Using QUIC in frp:
|
||||
quicBindPort = 7000
|
||||
```
|
||||
|
||||
The `quicBindPort` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port.
|
||||
The `quicBindPort` number can be the same number as `bindPort`, since `bindPort` field specifies a TCP port.
|
||||
|
||||
2. Configure `frpc.toml` to use QUIC to connect to frps:
|
||||
|
||||
@@ -876,6 +949,7 @@ This feature is only available for types `tcp`, `http`, `tcpmux` now.
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "test1"
|
||||
type = "tcp"
|
||||
@@ -911,6 +985,7 @@ With health check type **tcp**, the service port will be pinged (TCPing):
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "test1"
|
||||
type = "tcp"
|
||||
@@ -930,6 +1005,7 @@ With health check type **http**, an HTTP request will be sent to the service and
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
@@ -954,6 +1030,7 @@ However, speaking of web servers and HTTP requests, your web server might rely o
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
@@ -966,10 +1043,11 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com
|
||||
|
||||
### Setting other HTTP Headers
|
||||
|
||||
Similar to `Host`, You can override other HTTP request headers with proxy type `http`.
|
||||
Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`.
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
@@ -977,26 +1055,28 @@ localPort = 80
|
||||
customDomains = ["test.example.com"]
|
||||
hostHeaderRewrite = "dev.example.com"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
responseHeaders.set.foo = "bar"
|
||||
```
|
||||
|
||||
In this example, it will set header `x-from-where: frp` in the HTTP request.
|
||||
In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response.
|
||||
|
||||
### Get Real IP
|
||||
|
||||
#### HTTP X-Forwarded-For
|
||||
|
||||
This feature is for http proxy only.
|
||||
This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled.
|
||||
|
||||
You can get user's real IP from HTTP request headers `X-Forwarded-For`.
|
||||
|
||||
#### Proxy Protocol
|
||||
|
||||
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP.
|
||||
frp supports Proxy Protocol to send user's real IP to local services.
|
||||
|
||||
Here is an example for https service:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "https"
|
||||
@@ -1019,6 +1099,7 @@ It can only be enabled when proxy type is http.
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
@@ -1043,6 +1124,7 @@ Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
@@ -1062,6 +1144,7 @@ frp supports forwarding HTTP requests to different backend web services by url r
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "web01"
|
||||
type = "http"
|
||||
@@ -1137,6 +1220,24 @@ serverPort = 7000
|
||||
transport.proxyURL = "http://user:pwd@192.168.1.128:8080"
|
||||
```
|
||||
|
||||
### Port range mapping
|
||||
|
||||
*Added in v0.56.0*
|
||||
|
||||
We can use the range syntax of Go template combined with the built-in `parseNumberRangePair` function to achieve port range mapping.
|
||||
|
||||
The following example, when run, will create 8 proxies named `test-6000, test-6001 ... test-6007`, each mapping the remote port to the local port.
|
||||
|
||||
```
|
||||
{{- range $_, $v := parseNumberRangePair "6000-6006,6007" "6000-6006,6007" }}
|
||||
[[proxies]]
|
||||
name = "tcp-{{ $v.First }}"
|
||||
type = "tcp"
|
||||
localPort = {{ $v.First }}
|
||||
remotePort = {{ $v.Second }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
### Client Plugins
|
||||
|
||||
frpc only forwards requests to local TCP or UDP ports by default.
|
||||
@@ -1147,6 +1248,7 @@ Using plugin **http_proxy**:
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
||||
[[proxies]]
|
||||
name = "http_proxy"
|
||||
type = "tcp"
|
||||
@@ -1165,6 +1267,82 @@ Read the [document](/doc/server_plugin.md).
|
||||
|
||||
Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin).
|
||||
|
||||
### SSH Tunnel Gateway
|
||||
|
||||
*added in v0.53.0*
|
||||
|
||||
frp supports listening to an SSH port on the frps side and achieves TCP protocol proxying through the SSH -R protocol, without relying on frpc.
|
||||
|
||||
```toml
|
||||
# frps.toml
|
||||
sshTunnelGateway.bindPort = 2200
|
||||
```
|
||||
|
||||
When running `./frps -c frps.toml`, a private key file named `.autogen_ssh_key` will be automatically created in the current working directory. This generated private key file will be used by the SSH server in frps.
|
||||
|
||||
Executing the command
|
||||
|
||||
```bash
|
||||
ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
|
||||
```
|
||||
|
||||
sets up a proxy on frps that forwards the local 8080 service to the port 9090.
|
||||
|
||||
```bash
|
||||
frp (via SSH) (Ctrl+C to quit)
|
||||
|
||||
User:
|
||||
ProxyName: test-tcp
|
||||
Type: tcp
|
||||
RemoteAddress: :9090
|
||||
```
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
```bash
|
||||
frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
|
||||
```
|
||||
|
||||
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
|
||||
|
||||
### Virtual Network (VirtualNet)
|
||||
|
||||
*Alpha feature added in v0.62.0*
|
||||
|
||||
The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity.
|
||||
|
||||
For detailed information about configuration and usage, please refer to the [VirtualNet documentation](/doc/virtual_net.md).
|
||||
|
||||
## Feature Gates
|
||||
|
||||
frp supports feature gates to enable or disable experimental features. This allows users to try out new features before they're considered stable.
|
||||
|
||||
### Available Feature Gates
|
||||
|
||||
| Name | Stage | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| VirtualNet | ALPHA | false | Virtual network capabilities for frp |
|
||||
|
||||
### Enabling Feature Gates
|
||||
|
||||
To enable an experimental feature, add the feature gate to your configuration:
|
||||
|
||||
```toml
|
||||
featureGates = { VirtualNet = true }
|
||||
```
|
||||
|
||||
### Feature Lifecycle
|
||||
|
||||
Features typically go through three stages:
|
||||
1. **ALPHA**: Disabled by default, may be unstable
|
||||
2. **BETA**: May be enabled by default, more stable but still evolving
|
||||
3. **GA (Generally Available)**: Enabled by default, ready for production use
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
|
||||
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources.
|
||||
|
||||
## Contributing
|
||||
|
||||
Interested in getting involved? We would like to help you!
|
||||
|
||||
67
README_zh.md
@@ -2,16 +2,54 @@
|
||||
|
||||
[](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-->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<!--gold sponsors end-->
|
||||
@@ -41,9 +79,7 @@ master 分支用于发布稳定版本,dev 分支用于开发,您可以尝试
|
||||
|
||||
### 关于 v2 的一些说明
|
||||
|
||||
当前整体形势不佳,面临的生活工作压力很大。
|
||||
|
||||
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构。
|
||||
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构,或者也有可能放弃一次性的重构,而是采用渐进的方式在当前版本上逐步做一些可能会导致不兼容的修改。
|
||||
|
||||
v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceMesh 方面的工作经验和思考。它的核心是一个现代化的四层和七层代理,类似于 envoy。这个代理本身高度可扩展,不仅可以用于实现内网穿透的功能,还可以应用于更多领域。在这个高度可扩展的内核基础上,我们将实现 frp v1 中的所有功能,并且能够以一种更加优雅的方式实现原先架构中无法实现或不易实现的功能。同时,我们将保持高效的开发和迭代能力。
|
||||
|
||||
@@ -55,7 +91,7 @@ v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceM
|
||||
|
||||
## 文档
|
||||
|
||||
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org/docs)。
|
||||
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org)。
|
||||
|
||||
## 为 frp 做贡献
|
||||
|
||||
@@ -68,7 +104,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,16 +121,6 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
||||
|
||||
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
|
||||
|
||||
国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。
|
||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||
|
||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||
|
||||
### 知识星球
|
||||
|
||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
||||
|
||||

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

|
||||
|
||||
13
Release.md
@@ -1,6 +1,9 @@
|
||||
### Fixes
|
||||
## Features
|
||||
|
||||
* `transport.tls.disableCustomTLSFirstByte` doesn't have any effect.
|
||||
* The Server API did not return the data correctly.
|
||||
* The Dashboard is unable to display data.
|
||||
* `natHoleStunServer` is missing a default value.
|
||||
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
|
||||
|
||||
## Improvements
|
||||
|
||||
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
|
||||
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
|
||||
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
|
||||
|
||||
@@ -29,19 +29,28 @@ var (
|
||||
prefixPath string
|
||||
)
|
||||
|
||||
type emptyFS struct{}
|
||||
|
||||
func (emptyFS) Open(name string) (http.File, error) {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
// if path is empty, load assets in memory
|
||||
// or set FileSystem using disk files
|
||||
func Load(path string) {
|
||||
prefixPath = path
|
||||
if prefixPath != "" {
|
||||
switch {
|
||||
case prefixPath != "":
|
||||
FileSystem = http.Dir(prefixPath)
|
||||
} else {
|
||||
case content != nil:
|
||||
FileSystem = http.FS(content)
|
||||
default:
|
||||
FileSystem = emptyFS{}
|
||||
}
|
||||
}
|
||||
|
||||
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-9465253b.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.WebServer.PprofEnable {
|
||||
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
|
||||
}
|
||||
|
||||
subRouter := router.NewRoute().Subrouter()
|
||||
user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
|
||||
subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
|
||||
|
||||
// api, see admin_api.go
|
||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||
|
||||
// view
|
||||
subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
|
||||
subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
|
||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: router,
|
||||
ReadTimeout: httpServerReadTimeout,
|
||||
WriteTimeout: httpServerWriteTimeout,
|
||||
}
|
||||
if address == "" {
|
||||
address = ":http"
|
||||
}
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = server.Serve(ln)
|
||||
}()
|
||||
return
|
||||
}
|
||||
@@ -1,227 +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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
type GeneralResponse struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
// /healthz
|
||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
// GET /api/reload
|
||||
func (svr *Service) apiReload(w http.ResponseWriter, _ *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))
|
||||
}
|
||||
}()
|
||||
|
||||
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warn("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warn("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = err.Error()
|
||||
log.Warn("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
log.Info("success reload conf")
|
||||
}
|
||||
|
||||
// POST /api/stop
|
||||
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Info("api request [/api/stop]")
|
||||
defer func() {
|
||||
log.Info("api response [/api/stop], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
go svr.GracefulClose(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
type StatusResp map[string][]ProxyStatusResp
|
||||
|
||||
type ProxyStatusResp struct {
|
||||
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.Type
|
||||
|
||||
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, _ *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, _ *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 := 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
|
||||
}
|
||||
res.Msg = string(content)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
||||
log.Warn("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
85
client/api_router.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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/http"
|
||||
|
||||
adminapi "github.com/fatedier/frp/client/http"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if svr.storeSource != nil {
|
||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete)
|
||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func newAPIController(svr *Service) *adminapi.Controller {
|
||||
manager := newServiceConfigManager(svr)
|
||||
return adminapi.NewController(adminapi.ControllerParams{
|
||||
ServerAddr: svr.common.ServerAddr,
|
||||
Manager: manager,
|
||||
})
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
422
client/config_manager.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/configmgmt"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
type serviceConfigManager struct {
|
||||
svr *Service
|
||||
}
|
||||
|
||||
func newServiceConfigManager(svr *Service) configmgmt.ConfigManager {
|
||||
return &serviceConfigManager{svr: svr}
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) ReloadFromFile(strict bool) error {
|
||||
if m.svr.configFilePath == "" {
|
||||
return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers(
|
||||
result.Common,
|
||||
result.Proxies,
|
||||
result.Visitors,
|
||||
)
|
||||
proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation)
|
||||
visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation)
|
||||
|
||||
if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err)
|
||||
}
|
||||
|
||||
log.Infof("success reload conf")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) ReadConfigFile() (string, error) {
|
||||
if m.svr.configFilePath == "" {
|
||||
return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(m.svr.configFilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) WriteConfigFile(content []byte) error {
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||
return m.svr.getAllProxyStatus()
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
m.svr.reloadMu.Lock()
|
||||
storeSource := m.svr.storeSource
|
||||
m.svr.reloadMu.Unlock()
|
||||
|
||||
if storeSource == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cfg := storeSource.GetProxy(name)
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
enabled := cfg.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) StoreEnabled() bool {
|
||||
m.svr.reloadMu.Lock()
|
||||
storeSource := m.svr.storeSource
|
||||
m.svr.reloadMu.Unlock()
|
||||
return storeSource != nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||
storeSource, err := m.storeSourceOrError()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeSource.GetAllProxies()
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
storeSource, err := m.storeSourceOrError()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := storeSource.GetProxy(name)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
name := cfg.GetBaseConfig().Name
|
||||
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.AddProxy(cfg); err != nil {
|
||||
if errors.Is(err, source.ErrAlreadyExists) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("store: created proxy %q", name)
|
||||
return persisted, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
bodyName := cfg.GetBaseConfig().Name
|
||||
if bodyName != name {
|
||||
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.UpdateProxy(cfg); err != nil {
|
||||
if errors.Is(err, source.ErrNotFound) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: updated proxy %q", name)
|
||||
return persisted, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.RemoveProxy(name); err != nil {
|
||||
if errors.Is(err, source.ErrNotFound) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("store: deleted proxy %q", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||
storeSource, err := m.storeSourceOrError()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeSource.GetAllVisitors()
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
storeSource, err := m.storeSourceOrError()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := storeSource.GetVisitor(name)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
name := cfg.GetBaseConfig().Name
|
||||
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.AddVisitor(cfg); err != nil {
|
||||
if errors.Is(err, source.ErrAlreadyExists) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: created visitor %q", name)
|
||||
return persisted, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
bodyName := cfg.GetBaseConfig().Name
|
||||
if bodyName != name {
|
||||
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||
}
|
||||
|
||||
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
||||
if errors.Is(err, source.ErrNotFound) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: updated visitor %q", name)
|
||||
return persisted, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||
if err := storeSource.RemoveVisitor(name); err != nil {
|
||||
if errors.Is(err, source.ErrNotFound) {
|
||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("store: deleted visitor %q", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GracefulClose(d time.Duration) {
|
||||
m.svr.GracefulClose(d)
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) {
|
||||
m.svr.reloadMu.Lock()
|
||||
storeSource := m.svr.storeSource
|
||||
m.svr.reloadMu.Unlock()
|
||||
|
||||
if storeSource == nil {
|
||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||
}
|
||||
return storeSource, nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) withStoreMutationAndReload(
|
||||
fn func(storeSource *source.StoreSource) error,
|
||||
) error {
|
||||
m.svr.reloadMu.Lock()
|
||||
defer m.svr.reloadMu.Unlock()
|
||||
|
||||
storeSource := m.svr.storeSource
|
||||
if storeSource == nil {
|
||||
return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||
}
|
||||
|
||||
if err := fn(storeSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||
return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
||||
name string,
|
||||
fn func(storeSource *source.StoreSource) error,
|
||||
) (v1.ProxyConfigurer, error) {
|
||||
m.svr.reloadMu.Lock()
|
||||
defer m.svr.reloadMu.Unlock()
|
||||
|
||||
storeSource := m.svr.storeSource
|
||||
if storeSource == nil {
|
||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||
}
|
||||
|
||||
if err := fn(storeSource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||
}
|
||||
|
||||
persisted := storeSource.GetProxy(name)
|
||||
if persisted == nil {
|
||||
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||
}
|
||||
return persisted.Clone(), nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
||||
name string,
|
||||
fn func(storeSource *source.StoreSource) error,
|
||||
) (v1.VisitorConfigurer, error) {
|
||||
m.svr.reloadMu.Lock()
|
||||
defer m.svr.reloadMu.Unlock()
|
||||
|
||||
storeSource := m.svr.storeSource
|
||||
if storeSource == nil {
|
||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||
}
|
||||
|
||||
if err := fn(storeSource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||
}
|
||||
|
||||
persisted := storeSource.GetVisitor(name)
|
||||
if persisted == nil {
|
||||
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||
}
|
||||
return persisted.Clone(), nil
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("invalid proxy config")
|
||||
}
|
||||
runtimeCfg := cfg.Clone()
|
||||
if runtimeCfg == nil {
|
||||
return fmt.Errorf("invalid proxy config")
|
||||
}
|
||||
runtimeCfg.Complete()
|
||||
return validation.ValidateProxyConfigurerForClient(runtimeCfg)
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("invalid visitor config")
|
||||
}
|
||||
runtimeCfg := cfg.Clone()
|
||||
if runtimeCfg == nil {
|
||||
return fmt.Errorf("invalid visitor config")
|
||||
}
|
||||
runtimeCfg.Complete()
|
||||
return validation.ValidateVisitorConfigurer(runtimeCfg)
|
||||
}
|
||||
137
client/config_manager_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/fatedier/frp/client/configmgmt"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||
return &v1.TCPProxyConfig{
|
||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||
Name: name,
|
||||
Type: "tcp",
|
||||
ProxyBackend: v1.ProxyBackend{
|
||||
LocalPort: 10080,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new store source: %v", err)
|
||||
}
|
||||
if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil {
|
||||
t.Fatalf("seed proxy: %v", err)
|
||||
}
|
||||
|
||||
agg := source.NewAggregator(source.NewConfigSource())
|
||||
agg.SetStoreSource(storeSource)
|
||||
|
||||
mgr := &serviceConfigManager{
|
||||
svr: &Service{
|
||||
aggregator: agg,
|
||||
configSource: agg.ConfigSource(),
|
||||
storeSource: storeSource,
|
||||
reloadCommon: &v1.ClientCommonConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||
if err == nil {
|
||||
t.Fatal("expected conflict error")
|
||||
}
|
||||
if !errors.Is(err, configmgmt.ErrConflict) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) {
|
||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new store source: %v", err)
|
||||
}
|
||||
|
||||
mgr := &serviceConfigManager{
|
||||
svr: &Service{
|
||||
storeSource: storeSource,
|
||||
reloadCommon: &v1.ClientCommonConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||
if err == nil {
|
||||
t.Fatal("expected apply config error")
|
||||
}
|
||||
if !errors.Is(err, configmgmt.ErrApplyConfig) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if storeSource.GetProxy("p1") == nil {
|
||||
t.Fatal("proxy should remain in store after reload failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
||||
mgr := &serviceConfigManager{
|
||||
svr: &Service{
|
||||
reloadCommon: &v1.ClientCommonConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||
if err == nil {
|
||||
t.Fatal("expected store disabled error")
|
||||
}
|
||||
if !errors.Is(err, configmgmt.ErrStoreDisabled) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) {
|
||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new store source: %v", err)
|
||||
}
|
||||
agg := source.NewAggregator(source.NewConfigSource())
|
||||
agg.SetStoreSource(storeSource)
|
||||
|
||||
mgr := &serviceConfigManager{
|
||||
svr: &Service{
|
||||
aggregator: agg,
|
||||
configSource: agg.ConfigSource(),
|
||||
storeSource: storeSource,
|
||||
reloadCommon: &v1.ClientCommonConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
||||
if err != nil {
|
||||
t.Fatalf("create store proxy: %v", err)
|
||||
}
|
||||
if persisted == nil {
|
||||
t.Fatal("expected persisted proxy to be returned")
|
||||
}
|
||||
|
||||
got := storeSource.GetProxy("raw-proxy")
|
||||
if got == nil {
|
||||
t.Fatal("proxy not found in store")
|
||||
}
|
||||
if got.GetBaseConfig().LocalIP != "" {
|
||||
t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP)
|
||||
}
|
||||
if got.GetBaseConfig().Transport.BandwidthLimitMode != "" {
|
||||
t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode)
|
||||
}
|
||||
}
|
||||
42
client/configmgmt/types.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package configmgmt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidArgument = errors.New("invalid argument")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrConflict = errors.New("conflict")
|
||||
ErrStoreDisabled = errors.New("store disabled")
|
||||
ErrApplyConfig = errors.New("apply config failed")
|
||||
)
|
||||
|
||||
type ConfigManager interface {
|
||||
ReloadFromFile(strict bool) error
|
||||
|
||||
ReadConfigFile() (string, error)
|
||||
WriteConfigFile(content []byte) error
|
||||
|
||||
GetProxyStatus() []*proxy.WorkingStatus
|
||||
IsStoreProxyEnabled(name string) bool
|
||||
StoreEnabled() bool
|
||||
|
||||
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||
DeleteStoreProxy(name string) error
|
||||
|
||||
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
||||
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
||||
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||
DeleteStoreVisitor(name string) error
|
||||
|
||||
GracefulClose(d time.Duration)
|
||||
}
|
||||
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,174 +16,197 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/control/shutdown"
|
||||
"github.com/fatedier/golib/crypto"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/client/visitor"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"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 []v1.ProxyConfigurer
|
||||
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 *v1.ClientCommonConfig
|
||||
|
||||
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 *v1.ClientCommonConfig,
|
||||
pxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
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(_ *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
|
||||
}
|
||||
|
||||
startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName)
|
||||
|
||||
// dispatch this work connection to related proxy
|
||||
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)
|
||||
proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName)
|
||||
err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||
if err != nil {
|
||||
xl.Warn("[%s] start error: %v", inMsg.ProxyName, err)
|
||||
xl.Warnf("[%s] start error: %v", proxyName, err)
|
||||
} else {
|
||||
xl.Info("[%s] start proxy success", inMsg.ProxyName)
|
||||
xl.Infof("[%s] start proxy success", 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)
|
||||
}
|
||||
@@ -194,170 +217,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.Auth.Token))
|
||||
for {
|
||||
m, err := msg.ReadMsg(encReader)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
xl.Debug("read from control connection EOF")
|
||||
return
|
||||
}
|
||||
xl.Warn("read error: %v", err)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
ctl.readCh <- m
|
||||
}
|
||||
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.Auth.Token))
|
||||
if err != nil {
|
||||
xl.Error("crypto new writer error: %v", err)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
for {
|
||||
m, ok := <-ctl.sendCh
|
||||
if !ok {
|
||||
xl.Info("control writer is closing")
|
||||
return
|
||||
}
|
||||
|
||||
if err := msg.WriteMsg(encWriter, m); err != nil {
|
||||
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.Transport.HeartbeatInterval > 0 {
|
||||
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second)
|
||||
defer hbSend.Stop()
|
||||
hbSendCh = hbSend.C
|
||||
}
|
||||
|
||||
var hbCheckCh <-chan time.Time
|
||||
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
|
||||
if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 &&
|
||||
!lo.FromPtr(ctl.clientCfg.Transport.TCPMux) {
|
||||
hbCheck := time.NewTicker(time.Second)
|
||||
defer hbCheck.Stop()
|
||||
hbCheckCh = hbCheck.C
|
||||
}
|
||||
|
||||
ctl.lastPong = time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-hbSendCh:
|
||||
// send heartbeat to server
|
||||
xl.Debug("send heartbeat to server")
|
||||
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.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warn("heartbeat timeout")
|
||||
// let reader() stop
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
case rawMsg, ok := <-ctl.readCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.ReqWorkConn:
|
||||
go ctl.HandleReqWorkConn(m)
|
||||
case *msg.NewProxyResp:
|
||||
ctl.HandleNewProxyResp(m)
|
||||
case *msg.NatHoleResp:
|
||||
ctl.HandleNatHoleResp(m)
|
||||
case *msg.Pong:
|
||||
if m.Error != "" {
|
||||
xl.Error("Pong contains error: %s", m.Error)
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
ctl.lastPong = time.Now()
|
||||
xl.Debug("receive heartbeat from server")
|
||||
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 []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) 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
|
||||
|
||||
@@ -40,8 +40,8 @@ type Monitor struct {
|
||||
addr string
|
||||
|
||||
// For http
|
||||
url string
|
||||
|
||||
url string
|
||||
header http.Header
|
||||
failedTimes uint64
|
||||
statusOK bool
|
||||
statusNormalFn func()
|
||||
@@ -73,6 +73,11 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
|
||||
}
|
||||
url = s + cfg.Path
|
||||
}
|
||||
header := make(http.Header)
|
||||
for _, h := range cfg.HTTPHeaders {
|
||||
header.Set(h.Name, h.Value)
|
||||
}
|
||||
|
||||
return &Monitor{
|
||||
checkType: cfg.Type,
|
||||
interval: time.Duration(cfg.IntervalSeconds) * time.Second,
|
||||
@@ -80,6 +85,7 @@ func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
|
||||
maxFailedTimes: cfg.MaxFailed,
|
||||
addr: addr,
|
||||
url: url,
|
||||
header: header,
|
||||
statusOK: false,
|
||||
statusNormalFn: statusNormalFn,
|
||||
statusFailedFn: statusFailedFn,
|
||||
@@ -112,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()
|
||||
}
|
||||
@@ -163,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
|
||||
|
||||
395
client/http/controller.go
Normal file
@@ -0,0 +1,395 @@
|
||||
// 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 http
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/configmgmt"
|
||||
"github.com/fatedier/frp/client/http/model"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
// Controller handles HTTP API requests for frpc.
|
||||
type Controller struct {
|
||||
serverAddr string
|
||||
manager configmgmt.ConfigManager
|
||||
}
|
||||
|
||||
// ControllerParams contains parameters for creating an APIController.
|
||||
type ControllerParams struct {
|
||||
ServerAddr string
|
||||
Manager configmgmt.ConfigManager
|
||||
}
|
||||
|
||||
func NewController(params ControllerParams) *Controller {
|
||||
return &Controller{
|
||||
serverAddr: params.ServerAddr,
|
||||
manager: params.Manager,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) toHTTPError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, configmgmt.ErrInvalidArgument):
|
||||
code = http.StatusBadRequest
|
||||
case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled):
|
||||
code = http.StatusNotFound
|
||||
case errors.Is(err, configmgmt.ErrConflict):
|
||||
code = http.StatusConflict
|
||||
}
|
||||
return httppkg.NewError(code, err.Error())
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if err := c.manager.ReloadFromFile(strictConfigMode); err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Stop handles POST /api/stop
|
||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||
go c.manager.GracefulClose(100 * time.Millisecond)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Status handles GET /api/status
|
||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||
res := make(model.StatusResp)
|
||||
ps := c.manager.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 model.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) {
|
||||
content, err := c.manager.ReadConfigFile()
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
return 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 := c.manager.WriteConfigFile(body); err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
||||
psr := model.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
|
||||
}
|
||||
}
|
||||
|
||||
if c.manager.IsStoreProxyEnabled(status.Name) {
|
||||
psr.Source = model.SourceStore
|
||||
}
|
||||
return psr
|
||||
}
|
||||
|
||||
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||
proxies, err := c.manager.ListStoreProxies()
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
||||
for _, p := range proxies {
|
||||
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
resp.Proxies = append(resp.Proxies, payload)
|
||||
}
|
||||
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
p, err := c.manager.GetStoreProxy(name)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var payload model.ProxyDefinition
|
||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if err := payload.Validate("", false); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
cfg, err := payload.ToConfigurer()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
created, err := c.manager.CreateStoreProxy(cfg)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var payload model.ProxyDefinition
|
||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if err := payload.Validate(name, true); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
cfg, err := payload.ToConfigurer()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
if err := c.manager.DeleteStoreProxy(name); err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
||||
visitors, err := c.manager.ListStoreVisitors()
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
||||
for _, v := range visitors {
|
||||
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
resp.Visitors = append(resp.Visitors, payload)
|
||||
}
|
||||
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
v, err := c.manager.GetStoreVisitor(name)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var payload model.VisitorDefinition
|
||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if err := payload.Validate("", false); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
cfg, err := payload.ToConfigurer()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
created, err := c.manager.CreateStoreVisitor(cfg)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var payload model.VisitorDefinition
|
||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if err := payload.Validate(name, true); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
cfg, err := payload.ToConfigurer()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
||||
if err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
|
||||
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
if err := c.manager.DeleteStoreVisitor(name); err != nil {
|
||||
return nil, c.toHTTPError(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
531
client/http/controller_test.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/fatedier/frp/client/configmgmt"
|
||||
"github.com/fatedier/frp/client/http/model"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
)
|
||||
|
||||
type fakeConfigManager struct {
|
||||
reloadFromFileFn func(strict bool) error
|
||||
readConfigFileFn func() (string, error)
|
||||
writeConfigFileFn func(content []byte) error
|
||||
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||
isStoreProxyEnabledFn func(name string) bool
|
||||
storeEnabledFn func() bool
|
||||
|
||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||
deleteStoreProxyFn func(name string) error
|
||||
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
||||
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
||||
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||
deleteStoreVisitFn func(name string) error
|
||||
gracefulCloseFn func(d time.Duration)
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
||||
if m.reloadFromFileFn != nil {
|
||||
return m.reloadFromFileFn(strict)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
||||
if m.readConfigFileFn != nil {
|
||||
return m.readConfigFileFn()
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
||||
if m.writeConfigFileFn != nil {
|
||||
return m.writeConfigFileFn(content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||
if m.getProxyStatusFn != nil {
|
||||
return m.getProxyStatusFn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||
if m.isStoreProxyEnabledFn != nil {
|
||||
return m.isStoreProxyEnabledFn(name)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) StoreEnabled() bool {
|
||||
if m.storeEnabledFn != nil {
|
||||
return m.storeEnabledFn()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||
if m.listStoreProxiesFn != nil {
|
||||
return m.listStoreProxiesFn()
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||
if m.getStoreProxyFn != nil {
|
||||
return m.getStoreProxyFn(name)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
if m.createStoreProxyFn != nil {
|
||||
return m.createStoreProxyFn(cfg)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
if m.updateStoreProxyFn != nil {
|
||||
return m.updateStoreProxyFn(name, cfg)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
||||
if m.deleteStoreProxyFn != nil {
|
||||
return m.deleteStoreProxyFn(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||
if m.listStoreVisitorsFn != nil {
|
||||
return m.listStoreVisitorsFn()
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||
if m.getStoreVisitorFn != nil {
|
||||
return m.getStoreVisitorFn(name)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
if m.createStoreVisitFn != nil {
|
||||
return m.createStoreVisitFn(cfg)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
if m.updateStoreVisitFn != nil {
|
||||
return m.updateStoreVisitFn(name, cfg)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
||||
if m.deleteStoreVisitFn != nil {
|
||||
return m.deleteStoreVisitFn(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
||||
if m.gracefulCloseFn != nil {
|
||||
m.gracefulCloseFn(d)
|
||||
}
|
||||
}
|
||||
|
||||
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||
return &v1.TCPProxyConfig{
|
||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||
Name: name,
|
||||
Type: "tcp",
|
||||
ProxyBackend: v1.ProxyBackend{
|
||||
LocalPort: 10080,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
||||
status := &proxy.WorkingStatus{
|
||||
Name: "shared-proxy",
|
||||
Type: "tcp",
|
||||
Phase: proxy.ProxyPhaseRunning,
|
||||
RemoteAddr: ":8080",
|
||||
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
||||
}
|
||||
|
||||
controller := &Controller{
|
||||
serverAddr: "127.0.0.1",
|
||||
manager: &fakeConfigManager{
|
||||
isStoreProxyEnabledFn: func(name string) bool {
|
||||
return name == "shared-proxy"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp := controller.buildProxyStatusResp(status)
|
||||
if resp.Source != "store" {
|
||||
t.Fatalf("unexpected source: %q", resp.Source)
|
||||
}
|
||||
if resp.RemoteAddr != "127.0.0.1:8080" {
|
||||
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadErrorMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expectedCode int
|
||||
}{
|
||||
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
||||
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
||||
}
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
||||
_, err := controller.Reload(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assertHTTPCode(t, err, tc.expectedCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreProxyErrorMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expectedCode int
|
||||
}{
|
||||
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
||||
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
||||
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
return nil, tc.err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controller.UpdateStoreProxy(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assertHTTPCode(t, err, tc.expectedCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreVisitorErrorMapping(t *testing.T) {
|
||||
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
deleteStoreVisitFn: func(string) error {
|
||||
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controller.DeleteStoreVisitor(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assertHTTPCode(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
||||
var gotName string
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
gotName = cfg.GetBaseConfig().Name
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
resp, err := controller.CreateStoreProxy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create store proxy: %v", err)
|
||||
}
|
||||
if gotName != "raw-proxy" {
|
||||
t.Fatalf("unexpected proxy name: %q", gotName)
|
||||
}
|
||||
|
||||
payload, ok := resp.(model.ProxyDefinition)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if payload.Type != "tcp" || payload.TCP == nil {
|
||||
t.Fatalf("unexpected payload: %#v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
||||
var gotName string
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
gotName = cfg.GetBaseConfig().Name
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body := []byte(`{
|
||||
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
||||
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
resp, err := controller.CreateStoreVisitor(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create store visitor: %v", err)
|
||||
}
|
||||
if gotName != "raw-visitor" {
|
||||
t.Fatalf("unexpected visitor name: %q", gotName)
|
||||
}
|
||||
|
||||
payload, ok := resp.(model.VisitorDefinition)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if payload.Type != "xtcp" || payload.XTCP == nil {
|
||||
t.Fatalf("unexpected payload: %#v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||
var gotPluginType string
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
resp, err := controller.CreateStoreProxy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create store proxy: %v", err)
|
||||
}
|
||||
if gotPluginType != "http2https" {
|
||||
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||
}
|
||||
payload, ok := resp.(model.ProxyDefinition)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if payload.TCP == nil {
|
||||
t.Fatalf("unexpected response payload: %#v", payload)
|
||||
}
|
||||
pluginType := payload.TCP.Plugin.Type
|
||||
|
||||
if pluginType != "http2https" {
|
||||
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||
var gotPluginType string
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body := []byte(`{
|
||||
"name":"plugin-visitor","type":"stcp",
|
||||
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
resp, err := controller.CreateStoreVisitor(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create store visitor: %v", err)
|
||||
}
|
||||
if gotPluginType != "virtual_net" {
|
||||
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||
}
|
||||
payload, ok := resp.(model.VisitorDefinition)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if payload.STCP == nil {
|
||||
t.Fatalf("unexpected response payload: %#v", payload)
|
||||
}
|
||||
pluginType := payload.STCP.Plugin.Type
|
||||
|
||||
if pluginType != "virtual_net" {
|
||||
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
||||
controller := &Controller{manager: &fakeConfigManager{}}
|
||||
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
_, err := controller.UpdateStoreProxy(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
||||
controller := &Controller{manager: &fakeConfigManager{}}
|
||||
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
_, err := controller.UpdateStoreProxy(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
||||
b := newRawTCPProxyConfig("b")
|
||||
a := newRawTCPProxyConfig("a")
|
||||
return []v1.ProxyConfigurer{b, a}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
||||
|
||||
resp, err := controller.ListStoreProxies(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list store proxies: %v", err)
|
||||
}
|
||||
out, ok := resp.(model.ProxyListResp)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if len(out.Proxies) != 2 {
|
||||
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
||||
}
|
||||
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
||||
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
||||
}
|
||||
}
|
||||
|
||||
func fmtError(sentinel error, msg string) error {
|
||||
return fmt.Errorf("%w: %s", sentinel, msg)
|
||||
}
|
||||
|
||||
func assertHTTPCode(t *testing.T, err error, expected int) {
|
||||
t.Helper()
|
||||
var httpErr *httppkg.Error
|
||||
if !errors.As(err, &httpErr) {
|
||||
t.Fatalf("unexpected error type: %T", err)
|
||||
}
|
||||
if httpErr.Code != expected {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||
controller := &Controller{
|
||||
manager: &fakeConfigManager{
|
||||
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"name": "shared-proxy",
|
||||
"type": "tcp",
|
||||
"tcp": map[string]any{
|
||||
"localPort": 10080,
|
||||
"remotePort": 7000,
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||
|
||||
resp, err := controller.UpdateStoreProxy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("update store proxy: %v", err)
|
||||
}
|
||||
payload, ok := resp.(model.ProxyDefinition)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected response type: %T", resp)
|
||||
}
|
||||
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
||||
t.Fatalf("unexpected response payload: %#v", payload)
|
||||
}
|
||||
}
|
||||
148
client/http/model/proxy_definition.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type ProxyDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
|
||||
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
||||
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
||||
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
||||
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
||||
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
||||
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
||||
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
||||
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
||||
if strings.TrimSpace(p.Name) == "" {
|
||||
return fmt.Errorf("proxy name is required")
|
||||
}
|
||||
if !IsProxyType(p.Type) {
|
||||
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
||||
}
|
||||
if isUpdate && pathName != "" && pathName != p.Name {
|
||||
return fmt.Errorf("proxy name in URL must match name in body")
|
||||
}
|
||||
|
||||
_, blockType, blockCount := p.activeBlock()
|
||||
if blockCount != 1 {
|
||||
return fmt.Errorf("exactly one proxy type block is required")
|
||||
}
|
||||
if blockType != p.Type {
|
||||
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
||||
block, _, _ := p.activeBlock()
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("exactly one proxy type block is required")
|
||||
}
|
||||
|
||||
cfg := block
|
||||
cfg.GetBaseConfig().Name = p.Name
|
||||
cfg.GetBaseConfig().Type = p.Type
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
||||
if cfg == nil {
|
||||
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
||||
}
|
||||
|
||||
base := cfg.GetBaseConfig()
|
||||
payload := ProxyDefinition{
|
||||
Name: base.Name,
|
||||
Type: base.Type,
|
||||
}
|
||||
|
||||
switch c := cfg.(type) {
|
||||
case *v1.TCPProxyConfig:
|
||||
payload.TCP = c
|
||||
case *v1.UDPProxyConfig:
|
||||
payload.UDP = c
|
||||
case *v1.HTTPProxyConfig:
|
||||
payload.HTTP = c
|
||||
case *v1.HTTPSProxyConfig:
|
||||
payload.HTTPS = c
|
||||
case *v1.TCPMuxProxyConfig:
|
||||
payload.TCPMux = c
|
||||
case *v1.STCPProxyConfig:
|
||||
payload.STCP = c
|
||||
case *v1.SUDPProxyConfig:
|
||||
payload.SUDP = c
|
||||
case *v1.XTCPProxyConfig:
|
||||
payload.XTCP = c
|
||||
default:
|
||||
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
||||
count := 0
|
||||
var block v1.ProxyConfigurer
|
||||
var blockType string
|
||||
|
||||
if p.TCP != nil {
|
||||
count++
|
||||
block = p.TCP
|
||||
blockType = "tcp"
|
||||
}
|
||||
if p.UDP != nil {
|
||||
count++
|
||||
block = p.UDP
|
||||
blockType = "udp"
|
||||
}
|
||||
if p.HTTP != nil {
|
||||
count++
|
||||
block = p.HTTP
|
||||
blockType = "http"
|
||||
}
|
||||
if p.HTTPS != nil {
|
||||
count++
|
||||
block = p.HTTPS
|
||||
blockType = "https"
|
||||
}
|
||||
if p.TCPMux != nil {
|
||||
count++
|
||||
block = p.TCPMux
|
||||
blockType = "tcpmux"
|
||||
}
|
||||
if p.STCP != nil {
|
||||
count++
|
||||
block = p.STCP
|
||||
blockType = "stcp"
|
||||
}
|
||||
if p.SUDP != nil {
|
||||
count++
|
||||
block = p.SUDP
|
||||
blockType = "sudp"
|
||||
}
|
||||
if p.XTCP != nil {
|
||||
count++
|
||||
block = p.XTCP
|
||||
blockType = "xtcp"
|
||||
}
|
||||
|
||||
return block, blockType, count
|
||||
}
|
||||
|
||||
func IsProxyType(typ string) bool {
|
||||
switch typ {
|
||||
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
42
client/http/model/types.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 model
|
||||
|
||||
const SourceStore = "store"
|
||||
|
||||
// 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"`
|
||||
Source string `json:"source,omitempty"` // "store" or "config"
|
||||
}
|
||||
|
||||
// ProxyListResp is the response for GET /api/store/proxies
|
||||
type ProxyListResp struct {
|
||||
Proxies []ProxyDefinition `json:"proxies"`
|
||||
}
|
||||
|
||||
// VisitorListResp is the response for GET /api/store/visitors
|
||||
type VisitorListResp struct {
|
||||
Visitors []VisitorDefinition `json:"visitors"`
|
||||
}
|
||||
107
client/http/model/visitor_definition.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type VisitorDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
|
||||
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
||||
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
||||
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
||||
}
|
||||
|
||||
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
||||
if strings.TrimSpace(p.Name) == "" {
|
||||
return fmt.Errorf("visitor name is required")
|
||||
}
|
||||
if !IsVisitorType(p.Type) {
|
||||
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
||||
}
|
||||
if isUpdate && pathName != "" && pathName != p.Name {
|
||||
return fmt.Errorf("visitor name in URL must match name in body")
|
||||
}
|
||||
|
||||
_, blockType, blockCount := p.activeBlock()
|
||||
if blockCount != 1 {
|
||||
return fmt.Errorf("exactly one visitor type block is required")
|
||||
}
|
||||
if blockType != p.Type {
|
||||
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
||||
block, _, _ := p.activeBlock()
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("exactly one visitor type block is required")
|
||||
}
|
||||
|
||||
cfg := block
|
||||
cfg.GetBaseConfig().Name = p.Name
|
||||
cfg.GetBaseConfig().Type = p.Type
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
||||
if cfg == nil {
|
||||
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
||||
}
|
||||
|
||||
base := cfg.GetBaseConfig()
|
||||
payload := VisitorDefinition{
|
||||
Name: base.Name,
|
||||
Type: base.Type,
|
||||
}
|
||||
|
||||
switch c := cfg.(type) {
|
||||
case *v1.STCPVisitorConfig:
|
||||
payload.STCP = c
|
||||
case *v1.SUDPVisitorConfig:
|
||||
payload.SUDP = c
|
||||
case *v1.XTCPVisitorConfig:
|
||||
payload.XTCP = c
|
||||
default:
|
||||
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
||||
count := 0
|
||||
var block v1.VisitorConfigurer
|
||||
var blockType string
|
||||
|
||||
if p.STCP != nil {
|
||||
count++
|
||||
block = p.STCP
|
||||
blockType = "stcp"
|
||||
}
|
||||
if p.SUDP != nil {
|
||||
count++
|
||||
block = p.SUDP
|
||||
blockType = "sudp"
|
||||
}
|
||||
if p.XTCP != nil {
|
||||
count++
|
||||
block = p.XTCP
|
||||
blockType = "xtcp"
|
||||
}
|
||||
return block, blockType, count
|
||||
}
|
||||
|
||||
func IsVisitorType(typ string) bool {
|
||||
switch typ {
|
||||
case "stcp", "sudp", "xtcp":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,16 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/types"
|
||||
@@ -35,7 +34,9 @@ import (
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
||||
@@ -47,10 +48,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ func NewProxy(
|
||||
ctx context.Context,
|
||||
pxyConf v1.ProxyConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) (pxy Proxy) {
|
||||
var limiter *rate.Limiter
|
||||
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
|
||||
@@ -69,8 +71,10 @@ func NewProxy(
|
||||
baseProxy := BaseProxy{
|
||||
baseCfg: pxyConf.GetBaseConfig(),
|
||||
clientCfg: clientCfg,
|
||||
encryptionKey: encryptionKey,
|
||||
limiter: limiter,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
ctx: ctx,
|
||||
}
|
||||
@@ -85,11 +89,14 @@ func NewProxy(
|
||||
type BaseProxy struct {
|
||||
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
|
||||
@@ -98,7 +105,10 @@ type BaseProxy struct {
|
||||
|
||||
func (pxy *BaseProxy) Run() error {
|
||||
if pxy.baseCfg.Plugin.Type != "" {
|
||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||
Name: pxy.baseCfg.Name,
|
||||
VnetController: pxy.vnetController,
|
||||
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,106 +123,118 @@ func (pxy *BaseProxy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// wrapWorkConn applies rate limiting, encryption, and compression
|
||||
// to a work connection based on the proxy's transport configuration.
|
||||
// The returned recycle function should be called when the stream is no longer in use
|
||||
// to return compression resources to the pool. It is safe to not call recycle,
|
||||
// in which case resources will be garbage collected normally.
|
||||
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.baseCfg.Transport.UseEncryption {
|
||||
var err error
|
||||
rwc, err = libio.WithEncryption(rwc, encKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
||||
}
|
||||
}
|
||||
var recycleFn func()
|
||||
if pxy.baseCfg.Transport.UseCompression {
|
||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||
}
|
||||
return rwc, recycleFn, nil
|
||||
}
|
||||
|
||||
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.Auth.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
|
||||
baseCfg := pxy.baseCfg
|
||||
var (
|
||||
remote io.ReadWriteCloser
|
||||
err error
|
||||
)
|
||||
remote = workConn
|
||||
if pxy.limiter != nil {
|
||||
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
|
||||
return workConn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
var compressionResourceRecycleFn func()
|
||||
if baseCfg.Transport.UseCompression {
|
||||
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
||||
|
||||
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// check if we need to send proxy protocol info
|
||||
var extraInfo plugin.ExtraInfo
|
||||
if baseCfg.Transport.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 baseCfg.Transport.ProxyProtocolVersion == "v1" {
|
||||
h.Version = 1
|
||||
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
|
||||
h.Version = 2
|
||||
}
|
||||
|
||||
extraInfo.ProxyProtocolHeader = h
|
||||
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 {
|
||||
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")
|
||||
// Don't recycle compression resources here because plugins may
|
||||
// retain the connection after Handle returns.
|
||||
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(
|
||||
if recycleFn != nil {
|
||||
defer recycleFn()
|
||||
}
|
||||
|
||||
localConn, err := libnet.Dial(
|
||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||
libdial.WithTimeout(10*time.Second),
|
||||
libnet.WithTimeout(10*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
workConn.Close()
|
||||
xl.Error("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
|
||||
xl.Errorf("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
|
||||
return
|
||||
}
|
||||
|
||||
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 extraInfo.ProxyProtocolHeader != nil {
|
||||
if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||
if connInfo.ProxyProtocolHeader != nil {
|
||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||
workConn.Close()
|
||||
xl.Error("write proxy protocol header to local conn error: %v", err)
|
||||
localConn.Close()
|
||||
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)
|
||||
}
|
||||
if compressionResourceRecycleFn != nil {
|
||||
compressionResourceRecycleFn()
|
||||
xl.Tracef("join connections errors: %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,16 +28,20 @@ import (
|
||||
"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 *v1.ClientCommonConfig
|
||||
encryptionKey []byte
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
|
||||
ctx context.Context
|
||||
}
|
||||
@@ -45,12 +49,16 @@ type Manager struct {
|
||||
func NewManager(
|
||||
ctx context.Context,
|
||||
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,
|
||||
}
|
||||
@@ -71,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()
|
||||
@@ -91,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:
|
||||
@@ -106,18 +118,27 @@ func (pm *Manager) HandleEvent(payload interface{}) error {
|
||||
}
|
||||
|
||||
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
||||
ps := make([]*WorkingStatus, 0)
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
ps := make([]*WorkingStatus, 0, len(pm.proxies))
|
||||
for _, pxy := range pm.proxies {
|
||||
ps = append(ps, pxy.GetStatus())
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
|
||||
func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
if pxy, ok := pm.proxies[name]; ok {
|
||||
return pxy.GetStatus(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
||||
xl := xlog.FromContextSafe(pm.ctx)
|
||||
pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string {
|
||||
proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
|
||||
return c.GetBaseConfig().Name
|
||||
})
|
||||
pm.mu.Lock()
|
||||
@@ -126,7 +147,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
|
||||
delPxyNames := make([]string, 0)
|
||||
for name, pxy := range pm.proxies {
|
||||
del := false
|
||||
cfg, ok := pxyCfgsMap[name]
|
||||
cfg, ok := proxyCfgsMap[name]
|
||||
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
|
||||
del = true
|
||||
}
|
||||
@@ -138,14 +159,17 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
|
||||
}
|
||||
}
|
||||
if len(delPxyNames) > 0 {
|
||||
xl.Info("proxy removed: %s", delPxyNames)
|
||||
xl.Infof("proxy removed: %s", delPxyNames)
|
||||
}
|
||||
|
||||
addPxyNames := make([]string, 0)
|
||||
for _, 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)
|
||||
|
||||
@@ -153,6 +177,6 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
|
||||
}
|
||||
}
|
||||
if len(addPxyNames) > 0 {
|
||||
xl.Info("proxy added: %s", addPxyNames)
|
||||
xl.Infof("proxy added: %s", addPxyNames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ import (
|
||||
"github.com/fatedier/frp/client/health"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -73,6 +75,8 @@ type Wrapper struct {
|
||||
handler event.Handler
|
||||
|
||||
msgTransporter transport.MessageTransporter
|
||||
// vnet controller
|
||||
vnetController *vnet.Controller
|
||||
|
||||
health uint32
|
||||
lastSendStartMsg time.Time
|
||||
@@ -83,14 +87,18 @@ type Wrapper struct {
|
||||
|
||||
xl *xlog.Logger
|
||||
ctx context.Context
|
||||
|
||||
wireName string
|
||||
}
|
||||
|
||||
func NewWrapper(
|
||||
ctx context.Context,
|
||||
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.Name)
|
||||
@@ -105,8 +113,10 @@ func NewWrapper(
|
||||
healthNotifyCh: make(chan struct{}),
|
||||
handler: eventHandler,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
xl: xl,
|
||||
ctx: xlog.NewContext(ctx, xl),
|
||||
wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
||||
}
|
||||
|
||||
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||
@@ -114,13 +124,17 @@ func NewWrapper(
|
||||
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
|
||||
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
|
||||
pw.statusNormalCallback, pw.statusFailedCallback)
|
||||
xl.Trace("enable health check monitor")
|
||||
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()
|
||||
@@ -133,7 +147,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 {
|
||||
@@ -172,7 +186,7 @@ func (pw *Wrapper) Stop() {
|
||||
func (pw *Wrapper) close() {
|
||||
_ = pw.handler(&event.CloseProxyPayload{
|
||||
CloseProxyMsg: &msg.CloseProxy{
|
||||
ProxyName: pw.Name,
|
||||
ProxyName: pw.wireName,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -193,11 +207,12 @@ 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
|
||||
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
||||
newProxyMsg.ProxyName = pw.wireName
|
||||
pw.lastSendStartMsg = now
|
||||
_ = pw.handler(&event.StartProxyPayload{
|
||||
NewProxyMsg: &newProxyMsg,
|
||||
@@ -208,7 +223,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()
|
||||
@@ -232,7 +247,7 @@ func (pw *Wrapper) statusNormalCallback() {
|
||||
default:
|
||||
}
|
||||
})
|
||||
xl.Info("health check success")
|
||||
xl.Infof("health check success")
|
||||
}
|
||||
|
||||
func (pw *Wrapper) statusFailedCallback() {
|
||||
@@ -244,7 +259,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) {
|
||||
@@ -253,7 +268,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,10 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -23,17 +24,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||
}
|
||||
|
||||
type SUDPProxy struct {
|
||||
@@ -79,29 +78,15 @@ func (pxy *SUDPProxy) Close() {
|
||||
|
||||
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
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
workConn := conn
|
||||
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||
readCh := make(chan *msg.UDPPacket, 1024)
|
||||
sendCh := make(chan msg.Message, 1024)
|
||||
isClose := false
|
||||
@@ -131,21 +116,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *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
|
||||
}
|
||||
}
|
||||
@@ -155,21 +140,21 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *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
|
||||
}
|
||||
}
|
||||
@@ -189,11 +174,11 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *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
|
||||
}
|
||||
}
|
||||
@@ -203,5 +188,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
go workConnReaderFn(workConn, readCh)
|
||||
go heartbeatFn(sendCh)
|
||||
|
||||
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
|
||||
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||
}
|
||||
|
||||
@@ -12,27 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||
}
|
||||
|
||||
type UDPProxy struct {
|
||||
@@ -88,32 +87,18 @@ func (pxy *UDPProxy) Close() {
|
||||
|
||||
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
|
||||
var err error
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
pxy.mu.Lock()
|
||||
pxy.workConn = conn
|
||||
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||
pxy.sendCh = make(chan msg.Message, 1024)
|
||||
pxy.closed = false
|
||||
@@ -123,32 +108,32 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *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, len: %d", len(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, len: %d", len(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
|
||||
}
|
||||
}
|
||||
@@ -160,7 +145,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *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
|
||||
}
|
||||
}
|
||||
@@ -169,5 +154,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
||||
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
||||
go heartbeatFn(pxy.sendCh)
|
||||
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
|
||||
|
||||
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
|
||||
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||
}
|
||||
|
||||
@@ -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,13 +27,14 @@ import (
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"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(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
||||
}
|
||||
|
||||
type XTCPProxy struct {
|
||||
@@ -57,17 +60,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,20 +86,20 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
transactionID := nathole.NewTransactionID()
|
||||
natHoleClientMsg := &msg.NatHoleClient{
|
||||
TransactionID: transactionID,
|
||||
ProxyName: pxy.cfg.Name,
|
||||
ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, 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)
|
||||
|
||||
@@ -96,7 +107,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
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 +115,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 +137,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 +154,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,7 +162,7 @@ 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.Secretkey))
|
||||
@@ -164,7 +175,7 @@ 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"}
|
||||
@@ -176,22 +187,22 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
|
||||
},
|
||||
)
|
||||
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.Secretkey))
|
||||
go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,262 +16,327 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
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"
|
||||
// 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 *v1.ClientCommonConfig
|
||||
pxyCfgs []v1.ProxyConfigurer
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
cfgMu sync.RWMutex
|
||||
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
cfgFile string
|
||||
|
||||
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 *v1.ClientCommonConfig,
|
||||
pxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
cfgFile string,
|
||||
) (svr *Service, err error) {
|
||||
svr = &Service{
|
||||
authSetter: auth.NewAuthSetter(cfg.Auth),
|
||||
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
|
||||
|
||||
// ConfigSourceAggregator manages internal config and optional store sources.
|
||||
// It is required for creating a Service.
|
||||
ConfigSourceAggregator *source.Aggregator
|
||||
|
||||
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 lo.FromPtr(svr.cfg.LoginFailExit) {
|
||||
return err
|
||||
}
|
||||
util.RandomSleep(5*time.Second, 0.9, 1.1)
|
||||
} else {
|
||||
// login success
|
||||
ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
|
||||
ctl.Run()
|
||||
svr.ctlMu.Lock()
|
||||
svr.ctl = ctl
|
||||
svr.ctlMu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
go svr.keepControllerWorking()
|
||||
|
||||
if svr.cfg.WebServer.Port != 0 {
|
||||
// Init admin server assets
|
||||
assets.Load(svr.cfg.WebServer.AssetsDir)
|
||||
|
||||
address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port))
|
||||
err := svr.RunAdminServer(address)
|
||||
if err != nil {
|
||||
log.Warn("run admin server error: %v", err)
|
||||
}
|
||||
log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
|
||||
}
|
||||
<-svr.ctx.Done()
|
||||
// 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
|
||||
// reloadMu serializes reload transactions to keep reloadCommon and applied
|
||||
// config in sync across concurrent API operations.
|
||||
reloadMu sync.Mutex
|
||||
common *v1.ClientCommonConfig
|
||||
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
||||
// It can be updated by /api/reload without mutating startup-only common behavior.
|
||||
reloadCommon *v1.ClientCommonConfig
|
||||
proxyCfgs []v1.ProxyConfigurer
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
clientSpec *msg.ClientSpec
|
||||
|
||||
for {
|
||||
if atomic.LoadUint32(&svr.exit) != 0 {
|
||||
return
|
||||
}
|
||||
// aggregator manages multiple configuration sources.
|
||||
// When set, the service watches for config changes and reloads automatically.
|
||||
aggregator *source.Aggregator
|
||||
configSource *source.ConfigSource
|
||||
storeSource *source.StoreSource
|
||||
|
||||
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)
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
delayTime *= 2
|
||||
if delayTime > maxDelayTime {
|
||||
delayTime = maxDelayTime
|
||||
}
|
||||
continue
|
||||
}
|
||||
// reconnect success, init delayTime
|
||||
delayTime = time.Second
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
configFilePath string
|
||||
|
||||
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
|
||||
}
|
||||
// service context
|
||||
ctx context.Context
|
||||
// call cancel to stop service
|
||||
cancel context.CancelCauseFunc
|
||||
gracefulShutdownDuration time.Duration
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if options.ConfigSourceAggregator == nil {
|
||||
return nil, fmt.Errorf("config source aggregator is required")
|
||||
}
|
||||
|
||||
configSource := options.ConfigSourceAggregator.ConfigSource()
|
||||
storeSource := options.ConfigSourceAggregator.StoreSource()
|
||||
|
||||
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
||||
if loadErr != nil {
|
||||
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
||||
}
|
||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||
|
||||
s := &Service{
|
||||
ctx: context.Background(),
|
||||
auth: authRuntime,
|
||||
webServer: webServer,
|
||||
common: options.Common,
|
||||
reloadCommon: options.Common,
|
||||
configFilePath: options.ConfigFilePath,
|
||||
unsafeFeatures: options.UnsafeFeatures,
|
||||
proxyCfgs: proxyCfgs,
|
||||
visitorCfgs: visitorCfgs,
|
||||
clientSpec: options.ClientSpec,
|
||||
aggregator: options.ConfigSourceAggregator,
|
||||
configSource: configSource,
|
||||
storeSource: storeSource,
|
||||
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.Transport.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.Metadatas,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -288,21 +353,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 []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
||||
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
||||
xl := xlog.FromContextSafe(svr.ctx)
|
||||
|
||||
loginFunc := func() (bool, error) {
|
||||
xl.Infof("try to connect to server...")
|
||||
conn, connector, err := svr.login()
|
||||
if err != nil {
|
||||
xl.Warnf("connect to server error: %v", err)
|
||||
if firstLoginExit {
|
||||
svr.cancel(cancelErr{Err: err})
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
svr.cfgMu.RLock()
|
||||
proxyCfgs := svr.proxyCfgs
|
||||
visitorCfgs := svr.visitorCfgs
|
||||
svr.cfgMu.RUnlock()
|
||||
|
||||
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
|
||||
|
||||
sessionCtx := &SessionContext{
|
||||
Common: svr.common,
|
||||
RunID: svr.runID,
|
||||
Conn: conn,
|
||||
ConnEncrypted: connEncrypted,
|
||||
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()
|
||||
|
||||
@@ -311,7 +434,36 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi
|
||||
svr.ctlMu.RUnlock()
|
||||
|
||||
if ctl != nil {
|
||||
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
|
||||
return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svr *Service) UpdateConfigSource(
|
||||
common *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
) error {
|
||||
svr.reloadMu.Lock()
|
||||
defer svr.reloadMu.Unlock()
|
||||
|
||||
cfgSource := svr.configSource
|
||||
if cfgSource == nil {
|
||||
return fmt.Errorf("config source is not available")
|
||||
}
|
||||
|
||||
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Non-atomic update semantics: source has been updated at this point.
|
||||
// Even if reload fails below, keep this common config for subsequent reloads.
|
||||
svr.cfgMu.Lock()
|
||||
svr.reloadCommon = common
|
||||
svr.cfgMu.Unlock()
|
||||
|
||||
if err := svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -321,191 +473,89 @@ 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() {
|
||||
// Coordinate shutdown with reload/update paths that read source pointers.
|
||||
svr.reloadMu.Lock()
|
||||
if svr.aggregator != nil {
|
||||
svr.aggregator = nil
|
||||
}
|
||||
svr.configSource = nil
|
||||
svr.storeSource = nil
|
||||
svr.reloadMu.Unlock()
|
||||
|
||||
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 *v1.ClientCommonConfig
|
||||
|
||||
muxSession *fmux.Session
|
||||
quicConn quic.Connection
|
||||
type StatusExporter interface {
|
||||
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
|
||||
}
|
||||
|
||||
func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *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)
|
||||
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||
return s.getProxyStatusFunc(name)
|
||||
}
|
||||
|
||||
// special for quic
|
||||
if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") {
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
sn := cm.cfg.Transport.TLS.ServerName
|
||||
if sn == "" {
|
||||
sn = cm.cfg.ServerAddr
|
||||
}
|
||||
if lo.FromPtr(cm.cfg.Transport.TLS.Enable) {
|
||||
tlsConfig, err = transport.NewClientTLSConfig(
|
||||
cm.cfg.Transport.TLS.CertFile,
|
||||
cm.cfg.Transport.TLS.KeyFile,
|
||||
cm.cfg.Transport.TLS.TrustedCaFile,
|
||||
sn)
|
||||
} else {
|
||||
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
|
||||
}
|
||||
if err != nil {
|
||||
xl.Warn("fail to build tls configuration, err: %v", err)
|
||||
return err
|
||||
}
|
||||
tlsConfig.NextProtos = []string{"frp"}
|
||||
func (svr *Service) reloadConfigFromSources() error {
|
||||
svr.reloadMu.Lock()
|
||||
defer svr.reloadMu.Unlock()
|
||||
return svr.reloadConfigFromSourcesLocked()
|
||||
}
|
||||
|
||||
conn, err := quic.DialAddr(
|
||||
cm.ctx,
|
||||
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
|
||||
tlsConfig, &quic.Config{
|
||||
MaxIdleTimeout: time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||
MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams),
|
||||
KeepAlivePeriod: time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cm.quicConn = conn
|
||||
return nil
|
||||
func (svr *Service) reloadConfigFromSourcesLocked() error {
|
||||
aggregator := svr.aggregator
|
||||
if aggregator == nil {
|
||||
return errors.New("config aggregator is not initialized")
|
||||
}
|
||||
|
||||
if !lo.FromPtr(cm.cfg.Transport.TCPMux) {
|
||||
return nil
|
||||
}
|
||||
svr.cfgMu.RLock()
|
||||
reloadCommon := svr.reloadCommon
|
||||
svr.cfgMu.RUnlock()
|
||||
|
||||
conn, err := cm.realConnect()
|
||||
proxies, visitors, err := aggregator.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload config from sources failed: %w", err)
|
||||
}
|
||||
|
||||
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
||||
proxies = config.CompleteProxyConfigurers(proxies)
|
||||
visitors = config.CompleteVisitorConfigurers(visitors)
|
||||
|
||||
// Atomically replace the entire configuration
|
||||
if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmuxCfg := fmux.DefaultConfig()
|
||||
fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
||||
fmuxCfg.LogOutput = io.Discard
|
||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||
session, err := fmux.Client(conn, fmuxCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cm.muxSession = session
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *ConnectionManager) Connect() (net.Conn, error) {
|
||||
if cm.quicConn != nil {
|
||||
stream, err := cm.quicConn.OpenStreamSync(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil
|
||||
} else if cm.muxSession != nil {
|
||||
stream, err := cm.muxSession.OpenStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
return cm.realConnect()
|
||||
}
|
||||
|
||||
func (cm *ConnectionManager) realConnect() (net.Conn, error) {
|
||||
xl := xlog.FromContextSafe(cm.ctx)
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable)
|
||||
if cm.cfg.Transport.Protocol == "wss" {
|
||||
tlsEnable = true
|
||||
}
|
||||
if tlsEnable {
|
||||
sn := cm.cfg.Transport.TLS.ServerName
|
||||
if sn == "" {
|
||||
sn = cm.cfg.ServerAddr
|
||||
}
|
||||
|
||||
tlsConfig, err = transport.NewClientTLSConfig(
|
||||
cm.cfg.Transport.TLS.CertFile,
|
||||
cm.cfg.Transport.TLS.KeyFile,
|
||||
cm.cfg.Transport.TLS.TrustedCaFile,
|
||||
sn)
|
||||
if err != nil {
|
||||
xl.Warn("fail to build tls configuration, err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL)
|
||||
if err != nil {
|
||||
xl.Error("fail to parse proxy url")
|
||||
return nil, err
|
||||
}
|
||||
dialOptions := []libdial.DialOption{}
|
||||
protocol := cm.cfg.Transport.Protocol
|
||||
switch protocol {
|
||||
case "websocket":
|
||||
protocol = "tcp"
|
||||
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")}))
|
||||
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
|
||||
Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||
}))
|
||||
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
|
||||
case "wss":
|
||||
protocol = "tcp"
|
||||
dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
|
||||
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
|
||||
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
|
||||
default:
|
||||
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
|
||||
Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||
}))
|
||||
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
|
||||
}
|
||||
|
||||
if cm.cfg.Transport.ConnectServerLocalIP != "" {
|
||||
dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP))
|
||||
}
|
||||
dialOptions = append(dialOptions,
|
||||
libdial.WithProtocol(protocol),
|
||||
libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second),
|
||||
libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second),
|
||||
libdial.WithProxy(proxyType, addr),
|
||||
libdial.WithProxyAuth(auth),
|
||||
)
|
||||
conn, err := libdial.DialContext(
|
||||
cm.ctx,
|
||||
net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
|
||||
dialOptions...,
|
||||
)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (cm *ConnectionManager) Close() error {
|
||||
if cm.quicConn != nil {
|
||||
_ = cm.quicConn.CloseWithError(0, "")
|
||||
}
|
||||
if cm.muxSession != nil {
|
||||
_ = cm.muxSession.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
140
client/service_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||
|
||||
svr := &Service{
|
||||
configSource: source.NewConfigSource(),
|
||||
reloadCommon: prevCommon,
|
||||
}
|
||||
|
||||
invalidProxy := &v1.TCPProxyConfig{}
|
||||
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "proxy name cannot be empty") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if svr.reloadCommon != prevCommon {
|
||||
t.Fatalf("reloadCommon should roll back on ReplaceAll failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) {
|
||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||
|
||||
svr := &Service{
|
||||
// Keep configSource valid so ReplaceAll succeeds first.
|
||||
configSource: source.NewConfigSource(),
|
||||
reloadCommon: prevCommon,
|
||||
// Keep aggregator nil to force reload failure.
|
||||
aggregator: nil,
|
||||
}
|
||||
|
||||
validProxy := &v1.TCPProxyConfig{
|
||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||
Name: "p1",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "config aggregator is not initialized") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if svr.reloadCommon != newCommon {
|
||||
t.Fatalf("reloadCommon should keep new value on reload failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) {
|
||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new store source: %v", err)
|
||||
}
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{
|
||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||
Name: "store-proxy",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
visitorCfg := &v1.STCPVisitorConfig{
|
||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||
Name: "store-visitor",
|
||||
Type: "stcp",
|
||||
},
|
||||
}
|
||||
if err := storeSource.AddProxy(proxyCfg); err != nil {
|
||||
t.Fatalf("add proxy to store: %v", err)
|
||||
}
|
||||
if err := storeSource.AddVisitor(visitorCfg); err != nil {
|
||||
t.Fatalf("add visitor to store: %v", err)
|
||||
}
|
||||
|
||||
agg := source.NewAggregator(source.NewConfigSource())
|
||||
agg.SetStoreSource(storeSource)
|
||||
svr := &Service{
|
||||
aggregator: agg,
|
||||
configSource: agg.ConfigSource(),
|
||||
storeSource: storeSource,
|
||||
reloadCommon: &v1.ClientCommonConfig{},
|
||||
}
|
||||
|
||||
if err := svr.reloadConfigFromSources(); err != nil {
|
||||
t.Fatalf("reload config from sources: %v", err)
|
||||
}
|
||||
|
||||
gotProxy := storeSource.GetProxy("store-proxy")
|
||||
if gotProxy == nil {
|
||||
t.Fatalf("proxy not found in store")
|
||||
}
|
||||
if gotProxy.GetBaseConfig().LocalIP != "" {
|
||||
t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP)
|
||||
}
|
||||
|
||||
gotVisitor := storeSource.GetVisitor("store-visitor")
|
||||
if gotVisitor == nil {
|
||||
t.Fatalf("visitor not found in store")
|
||||
}
|
||||
if gotVisitor.GetBaseConfig().BindAddr != "" {
|
||||
t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr)
|
||||
}
|
||||
|
||||
svr.cfgMu.RLock()
|
||||
defer svr.cfgMu.RUnlock()
|
||||
|
||||
if len(svr.proxyCfgs) != 1 {
|
||||
t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs))
|
||||
}
|
||||
if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" {
|
||||
t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP)
|
||||
}
|
||||
|
||||
if len(svr.visitorCfgs) != 1 {
|
||||
t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs))
|
||||
}
|
||||
if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" {
|
||||
t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr)
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,12 @@
|
||||
package visitor
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
|
||||
@@ -40,10 +36,14 @@ func (sv *STCPVisitor) Run() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go sv.worker()
|
||||
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
||||
}
|
||||
|
||||
go sv.internalConnWorker()
|
||||
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
||||
|
||||
if sv.plugin != nil {
|
||||
sv.plugin.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,85 +51,35 @@ func (sv *STCPVisitor) Close() {
|
||||
sv.BaseVisitor.Close()
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) worker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("stcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) internalConnWorker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("stcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
defer userConn.Close()
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
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")
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
xl.Debugf("get a new stcp user connection")
|
||||
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
xl.Warnf("dialRawVisitorConn error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
defer visitorConn.Close()
|
||||
|
||||
now := time.Now().Unix()
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
UseCompression: sv.cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
xl.Warn("send newVisitorConnMsg to server error: %v", err)
|
||||
xl.Warnf("wrapVisitorConn error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
|
||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
xl.Warn("get newVisitorConnRespMsg error: %v", err)
|
||||
return
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
return
|
||||
}
|
||||
|
||||
var remote io.ReadWriteCloser
|
||||
remote = visitorConn
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
var recycleFn func()
|
||||
remote, recycleFn = libio.WithCompressionFromPool(remote)
|
||||
defer recycleFn()
|
||||
}
|
||||
defer recycleFn()
|
||||
|
||||
libio.Join(userConn, remote)
|
||||
}
|
||||
|
||||
@@ -16,20 +16,17 @@ package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
utilnet "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
|
||||
@@ -62,7 +59,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))
|
||||
@@ -75,6 +72,7 @@ func (sv *SUDPVisitor) dispatcher() {
|
||||
|
||||
var (
|
||||
visitorConn net.Conn
|
||||
recycleFn func()
|
||||
err error
|
||||
|
||||
firstPacket *msg.UDPPacket
|
||||
@@ -84,22 +82,25 @@ 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()
|
||||
visitorConn, recycleFn, 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
|
||||
}
|
||||
|
||||
// visitorConn always be closed when worker done.
|
||||
sv.worker(visitorConn, firstPacket)
|
||||
func() {
|
||||
defer recycleFn()
|
||||
sv.worker(visitorConn, firstPacket)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-sv.checkCloseCh:
|
||||
@@ -111,7 +112,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 +135,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, len: %d", len(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 +166,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, len: %d", len(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, len: %d", len(udpMsg.Content))
|
||||
case <-closeCh:
|
||||
return
|
||||
}
|
||||
@@ -194,55 +195,20 @@ 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) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
||||
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("frpc connect frps error: %v", err)
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
UseCompression: sv.cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
|
||||
rawConn.Close()
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
}
|
||||
|
||||
var remote io.ReadWriteCloser
|
||||
remote = visitorConn
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
remote = libio.WithCompression(remote)
|
||||
}
|
||||
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
||||
return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil
|
||||
}
|
||||
|
||||
func (sv *SUDPVisitor) Close() {
|
||||
|
||||
@@ -16,16 +16,26 @@ package visitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
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/util"
|
||||
"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 +44,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
|
||||
}
|
||||
@@ -50,13 +62,33 @@ func NewVisitor(
|
||||
cfg v1.VisitorConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
helper Helper,
|
||||
) (visitor Visitor) {
|
||||
) (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 *v1.STCPVisitorConfig:
|
||||
@@ -77,14 +109,15 @@ func NewVisitor(
|
||||
checkCloseCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
return
|
||||
return visitor, nil
|
||||
}
|
||||
|
||||
type BaseVisitor struct {
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
helper Helper
|
||||
l net.Listener
|
||||
internalLn *utilnet.InternalListener
|
||||
internalLn *netpkg.InternalListener
|
||||
plugin plugin.Plugin
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@@ -94,6 +127,18 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
||||
return v.internalLn.PutConn(conn)
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
||||
xl := xlog.FromContextSafe(v.ctx)
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("%s listener closed", name)
|
||||
return
|
||||
}
|
||||
go handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) Close() {
|
||||
if v.l != nil {
|
||||
v.l.Close()
|
||||
@@ -101,4 +146,61 @@ func (v *BaseVisitor) Close() {
|
||||
if v.internalLn != nil {
|
||||
v.internalLn.Close()
|
||||
}
|
||||
if v.plugin != nil {
|
||||
v.plugin.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
||||
visitorConn, err := v.helper.ConnectServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to server error: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: v.helper.RunID(),
|
||||
ProxyName: targetProxyName,
|
||||
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: cfg.Transport.UseEncryption,
|
||||
UseCompression: cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||
}
|
||||
|
||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
}
|
||||
return visitorConn, nil
|
||||
}
|
||||
|
||||
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
||||
rwc := conn
|
||||
if cfg.Transport.UseEncryption {
|
||||
var err error
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
||||
if err != nil {
|
||||
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
||||
}
|
||||
}
|
||||
recycleFn := func() {}
|
||||
if cfg.Transport.UseCompression {
|
||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||
}
|
||||
return rwc, recycleFn, nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
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 {
|
||||
@@ -35,7 +36,8 @@ type Manager struct {
|
||||
visitors map[string]Visitor
|
||||
helper Helper
|
||||
|
||||
checkInterval time.Duration
|
||||
checkInterval time.Duration
|
||||
keepVisitorsRunningOnce sync.Once
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@@ -49,6 +51,7 @@ func NewManager(
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
connectServer func() (net.Conn, error),
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) *Manager {
|
||||
m := &Manager{
|
||||
clientCfg: clientCfg,
|
||||
@@ -61,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)
|
||||
@@ -76,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().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)
|
||||
}
|
||||
}
|
||||
@@ -109,18 +115,29 @@ func (vm *Manager) Close() {
|
||||
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
|
||||
xl := xlog.FromContextSafe(vm.ctx)
|
||||
name := cfg.GetBaseConfig().Name
|
||||
visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||
visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||
if err != nil {
|
||||
xl.Warnf("new visitor error: %v", err)
|
||||
return
|
||||
}
|
||||
err = visitor.Run()
|
||||
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 []v1.VisitorConfigurer) {
|
||||
func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
|
||||
if len(cfgs) > 0 {
|
||||
// Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
|
||||
vm.keepVisitorsRunningOnce.Do(func() {
|
||||
go vm.keepVisitorsRunning()
|
||||
})
|
||||
}
|
||||
|
||||
xl := xlog.FromContextSafe(vm.ctx)
|
||||
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
|
||||
return c.GetBaseConfig().Name
|
||||
@@ -146,7 +163,7 @@ func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
|
||||
}
|
||||
}
|
||||
if len(delNames) > 0 {
|
||||
xl.Info("visitor removed: %v", delNames)
|
||||
xl.Infof("visitor removed: %v", delNames)
|
||||
}
|
||||
|
||||
addNames := make([]string, 0)
|
||||
@@ -159,7 +176,7 @@ func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
|
||||
}
|
||||
}
|
||||
if len(addNames) > 0 {
|
||||
xl.Info("visitor added: %v", addNames)
|
||||
xl.Infof("visitor added: %v", addNames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
||||
type visitorHelperImpl struct {
|
||||
connectServerFn func() (net.Conn, error)
|
||||
msgTransporter transport.MessageTransporter
|
||||
vnetController *vnet.Controller
|
||||
transferConnFn func(name string, conn net.Conn) error
|
||||
runID string
|
||||
}
|
||||
@@ -193,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
|
||||
}
|
||||
|
||||
@@ -31,9 +31,10 @@ import (
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"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"
|
||||
)
|
||||
@@ -64,15 +65,19 @@ func (sv *XTCPVisitor) Run() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go sv.worker()
|
||||
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
||||
}
|
||||
|
||||
go sv.internalConnWorker()
|
||||
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
||||
go sv.processTunnelStartEvents()
|
||||
if sv.cfg.KeepTunnelOpen {
|
||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||
go sv.keepTunnelOpenWorker()
|
||||
}
|
||||
|
||||
if sv.plugin != nil {
|
||||
sv.plugin.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,30 +93,6 @@ func (sv *XTCPVisitor) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) worker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("xtcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) internalConnWorker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warn("xtcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||
for {
|
||||
select {
|
||||
@@ -140,14 +121,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 +138,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 +165,73 @@ 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.Transport.UseEncryption {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Error("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
var recycleFn func()
|
||||
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
||||
defer recycleFn()
|
||||
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
xl.Errorf("%v", err)
|
||||
tunnelConn.Close()
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
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 +251,28 @@ 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")
|
||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
|
||||
xl.Warn("nathole precheck error: %v", err)
|
||||
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||
xl.Tracef("makeNatHole start")
|
||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil {
|
||||
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
|
||||
@@ -290,7 +282,7 @@ func (sv *XTCPVisitor) makeNatHole() {
|
||||
transactionID := nathole.NewTransactionID()
|
||||
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
||||
TransactionID: transactionID,
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
ProxyName: targetProxyName,
|
||||
Protocol: sv.cfg.Protocol,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
@@ -298,30 +290,30 @@ func (sv *XTCPVisitor) makeNatHole() {
|
||||
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.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,8 +341,9 @@ 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 {
|
||||
lConn.Close()
|
||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||
}
|
||||
|
||||
@@ -394,7 +387,7 @@ func (ks *KCPTunnelSession) Close() {
|
||||
}
|
||||
|
||||
type QUICTunnelSession struct {
|
||||
session quic.Connection
|
||||
session *quic.Conn
|
||||
listenConn *net.UDPConn
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -440,7 +433,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()
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -27,24 +29,24 @@ import (
|
||||
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
||||
)
|
||||
|
||||
var adminAPITimeout = 30 * time.Second
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewAdminCommand(
|
||||
"reload",
|
||||
"Hot-Reload frpc configuration",
|
||||
ReloadHandler,
|
||||
))
|
||||
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},
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(NewAdminCommand(
|
||||
"status",
|
||||
"Overview of all proxies status",
|
||||
StatusHandler,
|
||||
))
|
||||
|
||||
rootCmd.AddCommand(NewAdminCommand(
|
||||
"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 {
|
||||
@@ -52,7 +54,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
|
||||
Use: name,
|
||||
Short: short,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -73,7 +75,9 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
|
||||
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
if err := client.Reload(); err != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
if err := client.Reload(ctx, strictConfigMode); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("reload success")
|
||||
@@ -83,7 +87,9 @@ func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
res, err := client.GetAllProxyStatus()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
res, err := client.GetAllProxyStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,7 +115,9 @@ func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||
if err := client.Stop(); err != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||
defer cancel()
|
||||
if err := client.Stop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("stop success")
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
)
|
||||
|
||||
type BandwidthQuantityFlag struct {
|
||||
V *types.BandwidthQuantity
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) Set(s string) error {
|
||||
return f.V.UnmarshalString(s)
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) String() string {
|
||||
return f.V.String()
|
||||
}
|
||||
|
||||
func (f *BandwidthQuantityFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
|
||||
registerProxyBaseConfigFlags(cmd, c.GetBaseConfig())
|
||||
switch cc := c.(type) {
|
||||
case *v1.TCPProxyConfig:
|
||||
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
|
||||
case *v1.UDPProxyConfig:
|
||||
cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
|
||||
case *v1.HTTPProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations")
|
||||
cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
|
||||
cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
|
||||
cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
|
||||
case *v1.HTTPSProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
case *v1.TCPMuxProxyConfig:
|
||||
registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
|
||||
cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer")
|
||||
case *v1.STCPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
case *v1.SUDPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
case *v1.XTCPProxyConfig:
|
||||
cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
|
||||
}
|
||||
}
|
||||
|
||||
func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
|
||||
cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||
cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
||||
cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
|
||||
cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)")
|
||||
}
|
||||
|
||||
func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains")
|
||||
cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
|
||||
}
|
||||
|
||||
func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) {
|
||||
registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig())
|
||||
|
||||
// add visitor flags if exist
|
||||
}
|
||||
|
||||
func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
|
||||
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
||||
cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
|
||||
cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
|
||||
cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
|
||||
cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
|
||||
}
|
||||
|
||||
func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig) {
|
||||
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
|
||||
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
|
||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
|
||||
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
|
||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
|
||||
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
|
||||
cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
|
||||
|
||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||
}
|
||||
@@ -47,10 +47,14 @@ var natholeDiscoveryCmd = &cobra.Command{
|
||||
Use: "discover",
|
||||
Short: "Discover nathole information from stun server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// ignore error here, because we can use command line pameters
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
|
||||
// ignore error here, because we can use command line parameters
|
||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
@@ -17,12 +17,15 @@ package sub
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
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{
|
||||
@@ -50,17 +53,17 @@ func init() {
|
||||
}
|
||||
clientCfg := v1.ClientCommonConfig{}
|
||||
cmd := NewProxyCommand(string(typ), c, &clientCfg)
|
||||
RegisterClientCommonConfigFlags(cmd, &clientCfg)
|
||||
RegisterProxyFlags(cmd, c)
|
||||
config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
|
||||
config.RegisterProxyFlags(cmd, c)
|
||||
|
||||
// add sub command for visitor
|
||||
if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
|
||||
if slices.Contains(visitorTypes, v1.VisitorType(typ)) {
|
||||
vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
|
||||
if vc == nil {
|
||||
panic("visitor type: " + typ + " not support")
|
||||
}
|
||||
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
|
||||
RegisterVisitorFlags(visitorCmd, vc)
|
||||
config.RegisterVisitorFlags(visitorCmd, vc)
|
||||
cmd.AddCommand(visitorCmd)
|
||||
}
|
||||
rootCmd.AddCommand(cmd)
|
||||
@@ -72,19 +75,26 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
||||
Use: name,
|
||||
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
clientCfg.Complete()
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Complete(clientCfg.User)
|
||||
c.GetBaseConfig().Type = name
|
||||
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
||||
|
||||
c.GetBaseConfig().Type = name
|
||||
c.Complete()
|
||||
proxyCfg := c
|
||||
if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -98,19 +108,25 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
||||
Use: "visitor",
|
||||
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
clientCfg.Complete()
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
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 {
|
||||
c.Complete()
|
||||
visitorCfg := c
|
||||
if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -118,3 +134,18 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func startService(
|
||||
cfg *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) error {
|
||||
configSource := source.NewConfigSource()
|
||||
if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||
return fmt.Errorf("failed to set config source: %w", err)
|
||||
}
|
||||
aggregator := source.NewAggregator(configSource)
|
||||
return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -29,22 +30,31 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
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"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
showVersion bool
|
||||
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")
|
||||
|
||||
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{
|
||||
@@ -56,15 +66,17 @@ 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)
|
||||
@@ -73,7 +85,7 @@ var rootCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -83,7 +95,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)
|
||||
}
|
||||
@@ -95,6 +107,7 @@ func runMultipleClients(cfgDir string) error {
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -107,49 +120,100 @@ func handleTermSignal(svr *client.Service) {
|
||||
svr.GracefulClose(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func runClient(cfgFilePath string) error {
|
||||
cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
|
||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
// Load configuration
|
||||
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isLegacyFormat {
|
||||
if result.IsLegacyFormat {
|
||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
|
||||
if len(result.Common.FeatureGates) > 0 {
|
||||
if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return runClientWithAggregator(result, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
// runClientWithAggregator runs the client using the internal source aggregator.
|
||||
func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error {
|
||||
configSource := source.NewConfigSource()
|
||||
if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
||||
return fmt.Errorf("failed to set config source: %w", err)
|
||||
}
|
||||
|
||||
var storeSource *source.StoreSource
|
||||
|
||||
if result.Common.Store.IsEnabled() {
|
||||
storePath := result.Common.Store.Path
|
||||
if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) {
|
||||
storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath)
|
||||
}
|
||||
|
||||
s, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: storePath,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create store source: %w", err)
|
||||
}
|
||||
storeSource = s
|
||||
}
|
||||
|
||||
aggregator := source.NewAggregator(configSource)
|
||||
if storeSource != nil {
|
||||
aggregator.SetStoreSource(storeSource)
|
||||
}
|
||||
|
||||
proxyCfgs, visitorCfgs, err := aggregator.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config from sources: %w", err)
|
||||
}
|
||||
|
||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
|
||||
|
||||
return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
func startService(
|
||||
func startServiceWithAggregator(
|
||||
cfg *v1.ClientCommonConfig,
|
||||
pxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
aggregator *source.Aggregator,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) error {
|
||||
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
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] with aggregated configuration", cfgFile)
|
||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||
}
|
||||
svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
|
||||
svr, err := client.NewService(client.ServiceOptions{
|
||||
Common: cfg,
|
||||
ConfigSourceAggregator: aggregator,
|
||||
UnsafeFeatures: unsafeFeatures,
|
||||
ConfigFilePath: cfgFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 nil
|
||||
return svr.Run(context.Background())
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -37,12 +38,13 @@ var verifyCmd = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile)
|
||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type PortsRangeSliceFlag struct {
|
||||
V *[]types.PortsRange
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) String() string {
|
||||
if f.V == nil {
|
||||
return ""
|
||||
}
|
||||
return types.PortsRangeSlice(*f.V).String()
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) Set(s string) error {
|
||||
slice, err := types.NewPortsRangeSliceFromString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f.V = slice
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *PortsRangeSliceFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
type BoolFuncFlag struct {
|
||||
TrueFunc func()
|
||||
FalseFunc func()
|
||||
|
||||
v bool
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) String() string {
|
||||
return strconv.FormatBool(f.v)
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) Set(s string) error {
|
||||
f.v = strconv.FormatBool(f.v) == "true"
|
||||
|
||||
if !f.v {
|
||||
if f.FalseFunc != nil {
|
||||
f.FalseFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.TrueFunc != nil {
|
||||
f.TrueFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *BoolFuncFlag) Type() string {
|
||||
return "bool"
|
||||
}
|
||||
|
||||
func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
|
||||
cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
|
||||
cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
|
||||
cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
|
||||
cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
|
||||
cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
|
||||
cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
|
||||
cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
|
||||
cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
|
||||
cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
|
||||
cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
|
||||
cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||
cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
|
||||
cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
|
||||
cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
|
||||
|
||||
webServerTLS := v1.TLSConfig{}
|
||||
cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
|
||||
cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
|
||||
cmd.PersistentFlags().VarP(&BoolFuncFlag{
|
||||
TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
|
||||
}, "dashboard_tls_mode", "", "if enable dashboard tls mode")
|
||||
}
|
||||
@@ -15,13 +15,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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"
|
||||
system.EnableCompatibilityMode()
|
||||
Execute()
|
||||
}
|
||||
|
||||
@@ -18,20 +18,24 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/server"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
showVersion bool
|
||||
cfgFile string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
|
||||
serverCfg v1.ServerConfig
|
||||
)
|
||||
@@ -39,8 +43,11 @@ var (
|
||||
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, ", ")))
|
||||
|
||||
RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -58,7 +65,7 @@ var rootCmd = &cobra.Command{
|
||||
err error
|
||||
)
|
||||
if cfgFile != "" {
|
||||
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
|
||||
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -68,11 +75,16 @@ var rootCmd = &cobra.Command{
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
}
|
||||
} else {
|
||||
serverCfg.Complete()
|
||||
if err := serverCfg.Complete(); err != nil {
|
||||
fmt.Printf("failed to complete server config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
svrCfg = &serverCfg
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
@@ -90,25 +102,26 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runServer(cfg *v1.ServerConfig) (err error) {
|
||||
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -36,13 +37,15 @@ var verifyCmd = &cobra.Command{
|
||||
fmt.Println("frps: the configuration file is not specified")
|
||||
return nil
|
||||
}
|
||||
svrCfg, _, err := config.LoadServerConfig(cfgFile)
|
||||
svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
357
conf/frpc.toml
@@ -1,360 +1,9 @@
|
||||
# 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"
|
||||
serverAddr = "127.0.0.1"
|
||||
serverPort = 7000
|
||||
|
||||
# STUN server to help penetrate NAT hole.
|
||||
# natHoleStunServer = "stun.easyvoip.com:3478"
|
||||
|
||||
# Decide if exit program when first login failed, otherwise continuous relogin to frps
|
||||
# default is true
|
||||
loginFailExit = true
|
||||
|
||||
# console or real logFile path like ./frpc.log
|
||||
log.to = "./frpc.log"
|
||||
# trace, debug, info, warn, error
|
||||
log.level = "info"
|
||||
log.maxDays = 3
|
||||
# disable log colors when log.to is console, default is false
|
||||
log.disablePrintColor = false
|
||||
|
||||
auth.method = "token"
|
||||
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||
# Optional values are HeartBeats, NewWorkConns.
|
||||
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||
|
||||
# auth token
|
||||
auth.token = "12345678"
|
||||
|
||||
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
|
||||
# auth.oidc.clientID = ""
|
||||
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
||||
# auth.oidc.clientSecret = ""
|
||||
# oidc.audience specifies the audience of the token in OIDC authentication.
|
||||
# auth.oidc.audience = ""
|
||||
# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||
# auth.oidc.scope = ""
|
||||
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
|
||||
# It will be used to get an OIDC token.
|
||||
# auth.oidc.tokenEndpointURL = ""
|
||||
|
||||
# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint.
|
||||
# For example, if you want to specify the "audience" parameter, you can set as follow.
|
||||
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
|
||||
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
||||
# auth.oidc.additionalEndpointParams.var1 = "foobar"
|
||||
|
||||
# Set admin address for control frpc's action by http api such as reload
|
||||
webServer.addr = "127.0.0.1"
|
||||
webServer.port = 7400
|
||||
webServer.user = "admin"
|
||||
webServer.password = "admin"
|
||||
# Admin assets directory. By default, these assets are bundled with frpc.
|
||||
# webServer.assetsDir = "./static"
|
||||
|
||||
# Enable golang pprof handlers in admin listener.
|
||||
webServer.pprofEnable = false
|
||||
|
||||
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
|
||||
# transport.dialServerTimeout = 10
|
||||
|
||||
# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||
# If negative, keep-alive probes are disabled.
|
||||
# transport.dialServerKeepalive = 7200
|
||||
|
||||
# connections will be established in advance, default value is zero
|
||||
transport.poolCount = 5
|
||||
|
||||
# If tcp stream multiplexing is used, default is true, it must be same with frps
|
||||
# transport.tcpMux = true
|
||||
|
||||
# Specify keep alive interval for tcp mux.
|
||||
# only valid if tcpMux is enabled.
|
||||
# transport.tcpMuxKeepaliveInterval = 60
|
||||
|
||||
# Communication protocol used to connect to server
|
||||
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||
transport.protocol = "tcp"
|
||||
|
||||
# set client binding ip when connect server, default is empty.
|
||||
# only when protocol = tcp or websocket, the value will be used.
|
||||
transport.connectServerLocalIP = "0.0.0.0"
|
||||
|
||||
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
|
||||
# it only works when protocol is tcp
|
||||
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
||||
# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080"
|
||||
# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080"
|
||||
|
||||
# quic protocol options
|
||||
# transport.quic.keepalivePeriod = 10
|
||||
# transport.quic.maxIdleTimeout = 30
|
||||
# transport.quic.maxIncomingStreams = 100000
|
||||
|
||||
# If tls.enable is true, frpc will connect frps by tls.
|
||||
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
|
||||
transport.tls.enable = true
|
||||
|
||||
# transport.tls.certFile = "client.crt"
|
||||
# transport.tls.keyFile = "client.key"
|
||||
# transport.tls.trustedCaFile = "ca.crt"
|
||||
# transport.tls.serverName = "example.com"
|
||||
|
||||
# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
|
||||
# first custom byte when tls is enabled.
|
||||
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
|
||||
# transport.tls.disableCustomTLSFirstByte = true
|
||||
|
||||
# Heartbeat configure, it's not recommended to modify the default value.
|
||||
# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
|
||||
# to disable it.
|
||||
# transport.heartbeatInterval = 30
|
||||
# transport.heartbeatTimeout = 90
|
||||
|
||||
# Specify a dns server, so frpc will use this instead of default one
|
||||
# dnsServer = "8.8.8.8"
|
||||
|
||||
# Proxy names you want to start.
|
||||
# Default is empty, means all proxies.
|
||||
# start = ["ssh", "dns"]
|
||||
|
||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
udpPacketSize = 1500
|
||||
|
||||
# Additional metadatas for client.
|
||||
metadatas.var1 = "abc"
|
||||
metadatas.var2 = "123"
|
||||
|
||||
# Include other config files for proxies.
|
||||
# includes = ["./confd/*.ini"]
|
||||
|
||||
[[proxies]]
|
||||
# 'ssh' is the unique proxy name
|
||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||
name = "ssh"
|
||||
name = "test-tcp"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# Limit bandwidth for this proxy, unit is KB and MB
|
||||
transport.bandwidthLimit = "1MB"
|
||||
# Where to limit bandwidth, can be 'client' or 'server', default is 'client'
|
||||
transport.bandwidthLimitMode = "client"
|
||||
# If true, traffic of this proxy will be encrypted, default is false
|
||||
transport.useEncryption = false
|
||||
# If true, traffic will be compressed
|
||||
transport.useCompression = false
|
||||
# Remote port listen by frps
|
||||
remotePort = 6001
|
||||
# frps will load balancing connections for proxies in same group
|
||||
loadBalancer.group = "test_group"
|
||||
# group should have same group key
|
||||
loadBalancer.groupKey = "123456"
|
||||
# Enable health check for the backend service, it supports 'tcp' and 'http' now.
|
||||
# frpc will connect local service's port to detect it's healthy status
|
||||
healthCheck.type = "tcp"
|
||||
# Health check connection timeout
|
||||
healthCheck.timeoutSeconds = 3
|
||||
# If continuous failed in 3 times, the proxy will be removed from frps
|
||||
healthCheck.maxFailed = 3
|
||||
# every 10 seconds will do a health check
|
||||
healthCheck.intervalSeconds = 10
|
||||
# additional meta info for each proxy
|
||||
metadatas.var1 = "abc"
|
||||
metadatas.var2 = "123"
|
||||
|
||||
[[proxies]]
|
||||
name = "ssh_random"
|
||||
type = "tcp"
|
||||
localIP = "192.168.31.100"
|
||||
localPort = 22
|
||||
# If remote_port is 0, frps will assign a random port for you
|
||||
remotePort = 0
|
||||
|
||||
[[proxies]]
|
||||
name = "dns"
|
||||
type = "udp"
|
||||
localIP = "114.114.114.114"
|
||||
localPort = 53
|
||||
remotePort = 6002
|
||||
|
||||
# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
|
||||
[[proxies]]
|
||||
name = "web01"
|
||||
type = "http"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 80
|
||||
# http username and password are safety certification for http protocol
|
||||
# if not set, you can access this custom_domains without certification
|
||||
httpUser = "admin"
|
||||
httpPassword = "admin"
|
||||
# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
|
||||
subdomain = "web01"
|
||||
customDomains = ["web01.yourdomain.com"]
|
||||
# locations is only available for http type
|
||||
locations = ["/", "/pic"]
|
||||
# route requests to this service if http basic auto user is abc
|
||||
# route_by_http_user = abc
|
||||
hostHeaderRewrite = "example.com"
|
||||
# params with prefix "header_" will be used to update http request headers
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
healthCheck.type = "http"
|
||||
# frpc will send a GET http request '/status' to local http service
|
||||
# http service is alive when it return 2xx http response code
|
||||
healthCheck.path = "/status"
|
||||
healthCheck.intervalSeconds = 10
|
||||
healthCheck.maxFailed = 3
|
||||
healthCheck.timeoutSeconds = 3
|
||||
|
||||
[[proxies]]
|
||||
name = "web02"
|
||||
type = "https"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 8000
|
||||
subdomain = "web02"
|
||||
customDomains = ["web02.yourdomain.com"]
|
||||
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
|
||||
# v1 or v2 or empty
|
||||
transport.proxyProtocolVersion = "v2"
|
||||
|
||||
[[proxies]]
|
||||
name = "tcpmuxhttpconnect"
|
||||
type = "tcpmux"
|
||||
multiplexer = "httpconnect"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 10701
|
||||
customDomains = ["tunnel1"]
|
||||
# routeByHTTPUser = "user1"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_unix_domain_socket"
|
||||
type = "tcp"
|
||||
remotePort = 6003
|
||||
# if plugin is defined, local_ip and local_port is useless
|
||||
# plugin will handle connections got from frps
|
||||
[proxies.plugin]
|
||||
type = "unix_domain_socket"
|
||||
unixPath = "/var/run/docker.sock"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http_proxy"
|
||||
type = "tcp"
|
||||
remotePort = 6004
|
||||
[proxies.plugin]
|
||||
type = "http_proxy"
|
||||
httpUser = "abc"
|
||||
httpPassword = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_socks5"
|
||||
type = "tcp"
|
||||
remotePort = 6005
|
||||
[proxies.plugin]
|
||||
type = "socks5"
|
||||
username = "abc"
|
||||
password = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_static_file"
|
||||
type = "tcp"
|
||||
remotePort = 6006
|
||||
[proxies.plugin]
|
||||
type = "static_file"
|
||||
localPath = "/var/www/blog"
|
||||
stripPrefix = "static"
|
||||
httpUser = "abc"
|
||||
httpPassword = "abc"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_https2http"
|
||||
type = "https"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "https2http"
|
||||
localAddr = "127.0.0.1:80"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_https2https"
|
||||
type = "https"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "https2https"
|
||||
localAddr = "127.0.0.1:443"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http2https"
|
||||
type = "http"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "http2https"
|
||||
localAddr = "127.0.0.1:443"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "secret_tcp"
|
||||
# If the type is secret tcp, remote_port is useless
|
||||
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
|
||||
type = "stcp"
|
||||
# secretKey is used for authentication for visitors
|
||||
secretKey = "abcdefg"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# If not empty, only visitors from specified users can connect.
|
||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||
allowUsers = ["*"]
|
||||
|
||||
[[proxies]]
|
||||
name = "p2p_tcp"
|
||||
type = "xtcp"
|
||||
secretKey = "abcdefg"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# If not empty, only visitors from specified users can connect.
|
||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||
allowUsers = ["user1", "user2"]
|
||||
|
||||
# frpc role visitor -> frps -> frpc role server
|
||||
[[visitors]]
|
||||
name = "secret_tcp_visitor"
|
||||
type = "stcp"
|
||||
# the server name you want to visitor
|
||||
serverName = "secret_tcp"
|
||||
secretKey = "abcdefg"
|
||||
# connect this address to visitor stcp server
|
||||
bindAddr = "127.0.0.1"
|
||||
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||
# other visitors. (This is not supported for SUDP now)
|
||||
bindPort = 9000
|
||||
|
||||
[[visitors]]
|
||||
name = "p2p_tcp_visitor"
|
||||
type = "xtcp"
|
||||
# if the server user is not set, it defaults to the current user
|
||||
serverUser = "user1"
|
||||
serverName = "p2p_tcp"
|
||||
secretKey = "abcdefg"
|
||||
bindAddr = "127.0.0.1"
|
||||
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||
# other visitors. (This is not supported for SUDP now)
|
||||
bindPort = 9001
|
||||
# when automatic tunnel persistence is required, set it to true
|
||||
keepTunnelOpen = false
|
||||
# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
|
||||
maxRetriesAnHour = 8
|
||||
minRetryInterval = 90
|
||||
# fallbackTo = "stcp_visitor"
|
||||
# fallbackTimeoutMs = 500
|
||||
remotePort = 6000
|
||||
|
||||
465
conf/frpc_full_example.toml
Normal file
@@ -0,0 +1,465 @@
|
||||
# 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.
|
||||
# This list is a global allowlist after config + store are merged, so entries
|
||||
# created via Store API are also filtered by this list.
|
||||
# If start is non-empty, any proxy/visitor not listed here will not be started.
|
||||
# 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"
|
||||
153
conf/frps.toml
@@ -1,154 +1 @@
|
||||
# A literal address or host name for IPv6 must be enclosed
|
||||
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||
# For single "bind_addr" field, no need square brackets, like "bind_addr = ::".
|
||||
bindAddr = "0.0.0.0"
|
||||
bindPort = 7000
|
||||
|
||||
# udp port used for kcp protocol, it can be same with 'bind_port'.
|
||||
# if not set, kcp is disabled in frps.
|
||||
kcpBindPort = 7000
|
||||
|
||||
# udp port used for quic protocol.
|
||||
# if not set, quic is disabled in frps.
|
||||
# quicBindPort = 7002
|
||||
|
||||
# Specify which address proxy will listen for, default value is same with bind_addr
|
||||
# proxy_bind_addr = "127.0.0.1"
|
||||
|
||||
# quic protocol options
|
||||
# transport.quic.keepalivePeriod = 10
|
||||
# transport.quic.maxIdleTimeout = 30
|
||||
# transport.quic.maxIncomingStreams = 100000
|
||||
|
||||
# Heartbeat configure, it's not recommended to modify the default value
|
||||
# The default value of heartbeat_timeout is 90. Set negative value to disable it.
|
||||
# transport.heartbeatTimeout = 90
|
||||
|
||||
# Pool count in each proxy will keep no more than maxPoolCount.
|
||||
transport.maxPoolCount = 5
|
||||
|
||||
# If tcp stream multiplexing is used, default is true
|
||||
# transport.tcpMux = true
|
||||
|
||||
# Specify keep alive interval for tcp mux.
|
||||
# only valid if tcpMux is true.
|
||||
# transport.tcpMuxKeepaliveInterval = 60
|
||||
|
||||
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||
# If negative, keep-alive probes are disabled.
|
||||
# transport.tcpKeepalive = 7200
|
||||
|
||||
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
|
||||
tls.force = false
|
||||
|
||||
# transport.tls.certFile = "server.crt"
|
||||
# transport.tls.keyFile = "server.key"
|
||||
# transport.tls.trustedCaFile = "ca.crt"
|
||||
|
||||
# If you want to support virtual host, you must set the http port for listening (optional)
|
||||
# Note: http port and https port can be same with bind_port
|
||||
vhostHTTPPort = 80
|
||||
vhostHTTPSPort = 443
|
||||
|
||||
# Response header timeout(seconds) for vhost http server, default is 60s
|
||||
# vhostHTTPTimeout = 60
|
||||
|
||||
# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
|
||||
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
|
||||
# requests on one single port. If it's not - it will listen on this value for
|
||||
# HTTP CONNECT requests. By default, this value is 0.
|
||||
# tcpmuxHTTPConnectPort = 1337
|
||||
|
||||
# If tcpmux_passthrough is true, frps won't do any update on traffic.
|
||||
# tcpmuxPassthrough = false
|
||||
|
||||
# Configure the web server to enable the dashboard for frps.
|
||||
# dashboard is available only if webServer.port is set.
|
||||
webServer.addr = "127.0.0.1"
|
||||
webServer.port = 7500
|
||||
webServer.user = "admin"
|
||||
webServer.password = "admin"
|
||||
# webServer.tls.certFile = "server.crt"
|
||||
# webServer.tls.keyFile = "server.key"
|
||||
# dashboard assets directory(only for debug mode)
|
||||
# webServer.assetsDir = "./static"
|
||||
|
||||
# Enable golang pprof handlers in dashboard listener.
|
||||
# Dashboard port must be set first
|
||||
webServer.pprofEnable = false
|
||||
|
||||
# enablePrometheus will export prometheus metrics on webServer in /metrics api.
|
||||
enablePrometheus = true
|
||||
|
||||
# console or real logFile path like ./frps.log
|
||||
log.to = "./frps.log"
|
||||
# trace, debug, info, warn, error
|
||||
log.level = "info"
|
||||
log.maxDays = 3
|
||||
# disable log colors when log.to is console, default is false
|
||||
log.disablePrintColor = false
|
||||
|
||||
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
|
||||
detailedErrorsToClient = true
|
||||
|
||||
# auth.method specifies what authentication method to use authenticate frpc with frps.
|
||||
# If "token" is specified - token will be read into login message.
|
||||
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
|
||||
auth.method = "token"
|
||||
|
||||
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||
# Optional values are HeartBeats, NewWorkConns.
|
||||
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||
|
||||
# auth token
|
||||
auth.token = "12345678"
|
||||
|
||||
# oidc issuer specifies the issuer to verify OIDC tokens with.
|
||||
auth.oidc.issuer = ""
|
||||
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
||||
auth.oidc.audience = ""
|
||||
# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired.
|
||||
auth.oidc.skipExpiryCheck = false
|
||||
# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
|
||||
auth.oidc.skipIssuerCheck = false
|
||||
|
||||
# userConnTimeout specifies the maximum time to wait for a work connection.
|
||||
# userConnTimeout = 10
|
||||
|
||||
# Only allow frpc to bind ports you list. By default, there won't be any limit.
|
||||
allowPorts = [
|
||||
{ start = 2000, end = 3000 },
|
||||
{ single = 3001 },
|
||||
{ single = 3003 },
|
||||
{ start = 4000, end = 50000 }
|
||||
]
|
||||
|
||||
# Max ports can be used for each client, default value is 0 means no limit
|
||||
maxPortsPerClient = 0
|
||||
|
||||
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
|
||||
# When subdomain is est, the host used by routing is test.frps.com
|
||||
subDomainHost = "frps.com"
|
||||
|
||||
# custom 404 page for HTTP requests
|
||||
# custom404Page = "/path/to/404.html"
|
||||
|
||||
# specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
udpPacketSize = 1500
|
||||
|
||||
# Retention time for NAT hole punching strategy data.
|
||||
natholeAnalysisDataReserveHours = 168
|
||||
|
||||
[[httpPlugins]]
|
||||
name = "user-manager"
|
||||
addr = "127.0.0.1:9000"
|
||||
path = "/handler"
|
||||
ops = ["Login"]
|
||||
|
||||
[[httpPlugins]]
|
||||
name = "port-manager"
|
||||
addr = "127.0.0.1:9001"
|
||||
path = "/handler"
|
||||
ops = ["NewProxy"]
|
||||
|
||||
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>,
|
||||
|
||||
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.21 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frpc
|
||||
COPY web/frpc/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.25 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.21 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frps
|
||||
COPY web/frps/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.25 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"]
|
||||
|
||||
107
go.mod
@@ -1,83 +1,84 @@
|
||||
module github.com/fatedier/frp
|
||||
|
||||
go 1.20
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/coreos/go-oidc/v3 v3.6.0
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
|
||||
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40
|
||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
|
||||
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.11.0
|
||||
github.com/onsi/gomega v1.27.8
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/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.16.0
|
||||
github.com/quic-go/quic-go v0.37.4
|
||||
github.com/rodaine/table v1.1.0
|
||||
github.com/samber/lo v1.38.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/oauth2 v0.10.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/time v0.3.0
|
||||
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.27.4
|
||||
k8s.io/client-go v0.27.4
|
||||
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.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // 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/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.0.6 // indirect
|
||||
github.com/klauspost/reedsolomon v1.9.15 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
|
||||
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
|
||||
github.com/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.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/tools v0.9.3 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.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-20230209194617-a36077c30491 // indirect
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/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
|
||||
|
||||
265
go.sum
@@ -1,6 +1,6 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
@@ -9,242 +9,248 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
|
||||
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
|
||||
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40 h1:BVdpWT6viE/mpuRa6txNyRNjtHa1Efrii9Du6/gHfJ0=
|
||||
github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40/go.mod h1:Lmi9U4VfvdRvonSMh1FgXVy1hCXycVyJk4E9ktokknE=
|
||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74=
|
||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
||||
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI=
|
||||
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo=
|
||||
github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAKAP/qs14y4GCBOk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
||||
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
|
||||
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
|
||||
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
|
||||
github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4=
|
||||
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
|
||||
github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4=
|
||||
github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
||||
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
|
||||
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
|
||||
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI=
|
||||
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
||||
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
|
||||
github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
|
||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -257,12 +263,11 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
@@ -270,14 +275,16 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs=
|
||||
k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
|
||||
k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk=
|
||||
k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc=
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
|
||||
k8s.io/apimachinery v0.28.8/go.mod h1:cBnwIM3fXoRo28SqbV/Ihxf/iviw85KyXOrzxvZQ83U=
|
||||
k8s.io/client-go v0.28.8 h1:TE59Tjd87WKvS2FPBTfIKLFX0nQJ4SSHsnDo5IHjgOw=
|
||||
k8s.io/client-go v0.28.8/go.mod h1:uDVQ/rPzWpWIy40c6lZ4mUwaEvRWGnpoqSO4FM65P3o=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
|
||||
@@ -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.11.0
|
||||
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}
|
||||
|
||||
84
package.sh
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# compile for version
|
||||
make
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -14,50 +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}
|
||||
rm -rf ${frp_path}/legacy
|
||||
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
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
@@ -27,16 +28,56 @@ type Setter interface {
|
||||
SetNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
|
||||
type ClientAuth struct {
|
||||
Setter Setter
|
||||
key []byte
|
||||
}
|
||||
|
||||
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:
|
||||
authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||
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:
|
||||
panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
|
||||
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
||||
}
|
||||
return authProvider
|
||||
return authProvider, nil
|
||||
}
|
||||
|
||||
type Verifier interface {
|
||||
@@ -45,12 +86,42 @@ type Verifier interface {
|
||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
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:
|
||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC)
|
||||
tokenVerifier := NewTokenVerifier(cfg.OIDC)
|
||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
|
||||
}
|
||||
return authVerifier
|
||||
}
|
||||
|
||||
157
pkg/auth/oidc.go
@@ -16,23 +16,73 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
// 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()
|
||||
|
||||
// Configure TLS settings
|
||||
if trustedCAFile != "" || insecureSkipVerify {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenGenerator *clientcredentials.Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||
eps := make(map[string][]string)
|
||||
for k, v := range cfg.AdditionalEndpointParams {
|
||||
eps[k] = []string{v}
|
||||
@@ -50,14 +100,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
||||
EndpointParams: eps,
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -70,7 +136,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,7 +145,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,14 +153,64 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
||||
return err
|
||||
}
|
||||
|
||||
type OidcTokenSourceAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
valueSource *v1.ValueSource
|
||||
}
|
||||
|
||||
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 *oidc.IDTokenVerifier
|
||||
subjectFromLogin string
|
||||
verifier TokenVerifier
|
||||
mu sync.RWMutex
|
||||
subjectsFromLogin map[string]struct{}
|
||||
}
|
||||
|
||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer {
|
||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -105,9 +221,14 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCSer
|
||||
SkipExpiryCheck: cfg.SkipExpiryCheck,
|
||||
SkipIssuerCheck: cfg.SkipIssuerCheck,
|
||||
}
|
||||
return provider.Verifier(&verifierConf)
|
||||
}
|
||||
|
||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
|
||||
return &OidcAuthConsumer{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
verifier: provider.Verifier(&verifierConf),
|
||||
verifier: verifier,
|
||||
subjectsFromLogin: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +237,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
|
||||
auth.mu.Lock()
|
||||
auth.subjectsFromLogin[token.Subject] = struct{}{}
|
||||
auth.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,17 +248,19 @@ 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 {
|
||||
auth.mu.RLock()
|
||||
_, ok := auth.subjectsFromLogin[token.Subject]
|
||||
auth.mu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||
"original subject: %s, "+
|
||||
"new subject: %s",
|
||||
auth.subjectFromLogin, token.Subject)
|
||||
token.Subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -143,7 +268,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
|
||||
}
|
||||
|
||||
func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
31
pkg/auth/pass.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
var AlwaysPassVerifier = &alwaysPass{}
|
||||
|
||||
var _ Verifier = &alwaysPass{}
|
||||
|
||||
type alwaysPass struct{}
|
||||
|
||||
func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil }
|
||||
|
||||
func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil }
|
||||
|
||||
func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil }
|
||||
@@ -16,10 +16,9 @@ package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
@@ -43,7 +42,7 @@ func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -81,7 +80,7 @@ func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
|
||||
}
|
||||
|
||||
func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
|
||||
if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
|
||||
@@ -99,7 +99,7 @@ type ClientCommonConf struct {
|
||||
// the server must have TCP multiplexing enabled as well. By default, this
|
||||
// value is true.
|
||||
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
|
||||
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
|
||||
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
|
||||
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
|
||||
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
|
||||
// User specifies a prefix for proxy names to distinguish them from other
|
||||
@@ -170,7 +170,7 @@ type ClientCommonConf struct {
|
||||
}
|
||||
|
||||
// Supported sources including: string(file path), []byte, Reader interface.
|
||||
func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
||||
func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) {
|
||||
f, err := ini.LoadSources(ini.LoadOptions{
|
||||
Insensitive: false,
|
||||
InsensitiveSections: false,
|
||||
@@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
||||
}
|
||||
|
||||
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
|
||||
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
|
||||
common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
|
||||
|
||||
return common, nil
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
||||
// otherwise just start proxies in startProxy map
|
||||
func LoadAllProxyConfsFromIni(
|
||||
prefix string,
|
||||
source interface{},
|
||||
source any,
|
||||
start []string,
|
||||
) (map[string]ProxyConf, map[string]VisitorConf, error) {
|
||||
f, err := ini.LoadSources(ini.LoadOptions{
|
||||
@@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni(
|
||||
startProxy[s] = struct{}{}
|
||||
}
|
||||
|
||||
startAll := true
|
||||
if len(startProxy) > 0 {
|
||||
startAll = false
|
||||
}
|
||||
startAll := len(startProxy) == 0
|
||||
|
||||
// Build template sections from range section And append to ini.File.
|
||||
rangeSections := make([]*ini.Section, 0)
|
||||
@@ -345,35 +342,19 @@ func copySection(source, target *ini.Section) {
|
||||
}
|
||||
|
||||
// GetDefaultClientConf returns a client configuration with default values.
|
||||
// Note: Some default values here will be set to empty and will be converted to them
|
||||
// new configuration through the 'Complete' function to set them as the default
|
||||
// values of the new configuration.
|
||||
func GetDefaultClientConf() ClientCommonConf {
|
||||
return ClientCommonConf{
|
||||
ClientConfig: legacyauth.GetDefaultClientConf(),
|
||||
ServerAddr: "0.0.0.0",
|
||||
ServerPort: 7000,
|
||||
NatHoleSTUNServer: "stun.easyvoip.com:3478",
|
||||
DialServerTimeout: 10,
|
||||
DialServerKeepAlive: 7200,
|
||||
HTTPProxy: os.Getenv("http_proxy"),
|
||||
LogFile: "console",
|
||||
LogWay: "console",
|
||||
LogLevel: "info",
|
||||
LogMaxDays: 3,
|
||||
AdminAddr: "127.0.0.1",
|
||||
PoolCount: 1,
|
||||
TCPMux: true,
|
||||
TCPMuxKeepaliveInterval: 60,
|
||||
LoginFailExit: true,
|
||||
Start: make([]string, 0),
|
||||
Protocol: "tcp",
|
||||
QUICKeepalivePeriod: 10,
|
||||
QUICMaxIdleTimeout: 30,
|
||||
QUICMaxIncomingStreams: 100000,
|
||||
Start: make([]string, 0),
|
||||
TLSEnable: true,
|
||||
DisableCustomTLSFirstByte: true,
|
||||
HeartbeatInterval: 30,
|
||||
HeartbeatTimeout: 90,
|
||||
Metas: make(map[string]string),
|
||||
UDPPacketSize: 1500,
|
||||
IncludeConfigFiles: make([]string, 0),
|
||||
}
|
||||
}
|
||||
@@ -399,7 +380,7 @@ func (cfg *ClientCommonConf) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
|
||||
if !slices.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
|
||||
return fmt.Errorf("invalid protocol")
|
||||
}
|
||||
|
||||
|
||||
@@ -26,20 +26,20 @@ import (
|
||||
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
|
||||
out := &v1.ClientCommonConfig{}
|
||||
out.User = conf.User
|
||||
out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod)
|
||||
out.Auth.Token = conf.ClientConfig.Token
|
||||
if conf.ClientConfig.AuthenticateHeartBeats {
|
||||
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
|
||||
out.Auth.Token = conf.Token
|
||||
if conf.AuthenticateHeartBeats {
|
||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
||||
}
|
||||
if conf.ClientConfig.AuthenticateNewWorkConns {
|
||||
if conf.AuthenticateNewWorkConns {
|
||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
||||
}
|
||||
out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
|
||||
out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
|
||||
out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience
|
||||
out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope
|
||||
out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL
|
||||
out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams
|
||||
out.Auth.OIDC.ClientID = conf.OidcClientID
|
||||
out.Auth.OIDC.ClientSecret = conf.OidcClientSecret
|
||||
out.Auth.OIDC.Audience = conf.OidcAudience
|
||||
out.Auth.OIDC.Scope = conf.OidcScope
|
||||
out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL
|
||||
out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams
|
||||
|
||||
out.ServerAddr = conf.ServerAddr
|
||||
out.ServerPort = conf.ServerPort
|
||||
@@ -59,10 +59,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
||||
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
|
||||
out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
|
||||
out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
|
||||
out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile
|
||||
out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile
|
||||
out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile
|
||||
out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName
|
||||
out.Transport.TLS.CertFile = conf.TLSCertFile
|
||||
out.Transport.TLS.KeyFile = conf.TLSKeyFile
|
||||
out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
|
||||
out.Transport.TLS.ServerName = conf.TLSServerName
|
||||
|
||||
out.Log.To = conf.LogFile
|
||||
out.Log.Level = conf.LogLevel
|
||||
@@ -71,6 +71,7 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
||||
|
||||
out.WebServer.Addr = conf.AdminAddr
|
||||
out.WebServer.Port = conf.AdminPort
|
||||
out.WebServer.User = conf.AdminUser
|
||||
out.WebServer.Password = conf.AdminPwd
|
||||
out.WebServer.AssetsDir = conf.AssetsDir
|
||||
out.WebServer.PprofEnable = conf.PprofEnable
|
||||
@@ -86,18 +87,18 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
||||
|
||||
func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
||||
out := &v1.ServerConfig{}
|
||||
out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod)
|
||||
out.Auth.Token = conf.ServerConfig.Token
|
||||
if conf.ServerConfig.AuthenticateHeartBeats {
|
||||
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
|
||||
out.Auth.Token = conf.Token
|
||||
if conf.AuthenticateHeartBeats {
|
||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
||||
}
|
||||
if conf.ServerConfig.AuthenticateNewWorkConns {
|
||||
if conf.AuthenticateNewWorkConns {
|
||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
||||
}
|
||||
out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
|
||||
out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
|
||||
out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck
|
||||
out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck
|
||||
out.Auth.OIDC.Audience = conf.OidcAudience
|
||||
out.Auth.OIDC.Issuer = conf.OidcIssuer
|
||||
out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck
|
||||
out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck
|
||||
|
||||
out.BindAddr = conf.BindAddr
|
||||
out.BindPort = conf.BindPort
|
||||
@@ -170,12 +171,14 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
||||
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
||||
out := v1.HeaderOperations{}
|
||||
for k, v := range params {
|
||||
if !strings.HasPrefix(k, "plugin_header_") {
|
||||
k, ok := strings.CutPrefix(k, "plugin_header_")
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
|
||||
out.Set[k] = v
|
||||
if out.Set == nil {
|
||||
out.Set = make(map[string]string)
|
||||
}
|
||||
out.Set[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
func ParseClientConfig(filePath string) (
|
||||
cfg ClientCommonConf,
|
||||
pxyCfgs map[string]ProxyConf,
|
||||
proxyCfgs map[string]ProxyConf,
|
||||
visitorCfgs map[string]VisitorConf,
|
||||
err error,
|
||||
) {
|
||||
@@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) (
|
||||
configBuffer.Write(buf)
|
||||
|
||||
// Parse all proxy and visitor configs.
|
||||
pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
|
||||
proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ const (
|
||||
// Proxy
|
||||
var (
|
||||
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
|
||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
|
||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
|
||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
|
||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
|
||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
|
||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
|
||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
|
||||
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](),
|
||||
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](),
|
||||
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](),
|
||||
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](),
|
||||
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](),
|
||||
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](),
|
||||
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](),
|
||||
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -206,7 +206,7 @@ func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section)
|
||||
}
|
||||
|
||||
// plugin_xxx
|
||||
cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
|
||||
cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ type ServerCommonConf struct {
|
||||
// from a client to share a single TCP connection. By default, this value
|
||||
// is true.
|
||||
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
|
||||
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
|
||||
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
|
||||
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
|
||||
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
|
||||
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||
@@ -200,38 +200,24 @@ type ServerCommonConf struct {
|
||||
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
|
||||
}
|
||||
|
||||
// GetDefaultServerConf returns a server configuration with reasonable
|
||||
// defaults.
|
||||
// GetDefaultServerConf returns a server configuration with reasonable defaults.
|
||||
// Note: Some default values here will be set to empty and will be converted to them
|
||||
// new configuration through the 'Complete' function to set them as the default
|
||||
// values of the new configuration.
|
||||
func GetDefaultServerConf() ServerCommonConf {
|
||||
return ServerCommonConf{
|
||||
ServerConfig: legacyauth.GetDefaultServerConf(),
|
||||
BindAddr: "0.0.0.0",
|
||||
BindPort: 7000,
|
||||
QUICKeepalivePeriod: 10,
|
||||
QUICMaxIdleTimeout: 30,
|
||||
QUICMaxIncomingStreams: 100000,
|
||||
VhostHTTPTimeout: 60,
|
||||
DashboardAddr: "0.0.0.0",
|
||||
LogFile: "console",
|
||||
LogWay: "console",
|
||||
LogLevel: "info",
|
||||
LogMaxDays: 3,
|
||||
DetailedErrorsToClient: true,
|
||||
TCPMux: true,
|
||||
TCPMuxKeepaliveInterval: 60,
|
||||
TCPKeepAlive: 7200,
|
||||
AllowPorts: make(map[int]struct{}),
|
||||
MaxPoolCount: 5,
|
||||
MaxPortsPerClient: 0,
|
||||
HeartbeatTimeout: 90,
|
||||
UserConnTimeout: 10,
|
||||
HTTPPlugins: make(map[string]HTTPPluginOptions),
|
||||
UDPPacketSize: 1500,
|
||||
NatHoleAnalysisDataReserveHours: 7 * 24,
|
||||
ServerConfig: legacyauth.GetDefaultServerConf(),
|
||||
DashboardAddr: "0.0.0.0",
|
||||
LogFile: "console",
|
||||
LogWay: "console",
|
||||
DetailedErrorsToClient: true,
|
||||
TCPMux: true,
|
||||
AllowPorts: make(map[int]struct{}),
|
||||
HTTPPlugins: make(map[string]HTTPPluginOptions),
|
||||
}
|
||||
}
|
||||
|
||||
func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
|
||||
func UnmarshalServerConfFromIni(source any) (ServerCommonConf, error) {
|
||||
f, err := ini.LoadSources(ini.LoadOptions{
|
||||
Insensitive: false,
|
||||
InsensitiveSections: false,
|
||||
|
||||
@@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string
|
||||
m := make(map[string]string)
|
||||
|
||||
for key, value := range set {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
m[strings.TrimPrefix(key, prefix)] = value
|
||||
if trimmed, ok := strings.CutPrefix(key, prefix); ok {
|
||||
m[trimmed] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ const (
|
||||
// Visitor
|
||||
var (
|
||||
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
|
||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
|
||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
|
||||
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](),
|
||||
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](),
|
||||
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
"github.com/samber/lo"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
@@ -80,7 +82,10 @@ func DetectLegacyINIFormatFromFile(path string) bool {
|
||||
}
|
||||
|
||||
func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
|
||||
tmpl, err := template.New("frp").Parse(string(in))
|
||||
tmpl, err := template.New("frp").Funcs(template.FuncMap{
|
||||
"parseNumberRange": parseNumberRange,
|
||||
"parseNumberRangePair": parseNumberRangePair,
|
||||
}).Parse(string(in))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -100,26 +105,174 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
|
||||
return RenderWithTemplate(b, values)
|
||||
}
|
||||
|
||||
func LoadConfigureFromFile(path string, c any) error {
|
||||
func LoadConfigureFromFile(path string, c any, strict bool) error {
|
||||
content, err := LoadFileContentWithTemplate(path, GetValues())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return LoadConfigure(content, c)
|
||||
return LoadConfigure(content, c, strict, detectFormatFromPath(path))
|
||||
}
|
||||
|
||||
// detectFormatFromPath returns a format hint based on the file extension.
|
||||
func detectFormatFromPath(path string) string {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".toml":
|
||||
return "toml"
|
||||
case ".yaml", ".yml":
|
||||
return "yaml"
|
||||
case ".json":
|
||||
return "json"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
||||
// This function handles both cases efficiently: with or without dot fields
|
||||
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
||||
var temp any
|
||||
if err := yaml.Unmarshal(content, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove dot fields if it's a map
|
||||
if tempMap, ok := temp.(map[string]any); ok {
|
||||
for key := range tempMap {
|
||||
if strings.HasPrefix(key, ".") {
|
||||
delete(tempMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to JSON and decode with strict validation
|
||||
jsonBytes, err := jsonx.Marshal(temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeJSONContent(jsonBytes, target, true)
|
||||
}
|
||||
|
||||
func decodeJSONContent(content []byte, target any, strict bool) error {
|
||||
if clientCfg, ok := target.(*v1.ClientConfig); ok {
|
||||
decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{
|
||||
DisallowUnknownFields: strict,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*clientCfg = decoded
|
||||
return nil
|
||||
}
|
||||
|
||||
return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{
|
||||
RejectUnknownMembers: strict,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
||||
// Now it supports json, yaml and toml format.
|
||||
func LoadConfigure(b []byte, c any) error {
|
||||
var tomlObj interface{}
|
||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||
b, err = json.Marshal(&tomlObj)
|
||||
// An optional format hint (e.g. "toml", "yaml", "json") can be provided
|
||||
// to enable better error messages with line number information.
|
||||
func LoadConfigure(b []byte, c any, strict bool, formats ...string) error {
|
||||
format := ""
|
||||
if len(formats) > 0 {
|
||||
format = formats[0]
|
||||
}
|
||||
|
||||
originalBytes := b
|
||||
parsedFromTOML := false
|
||||
|
||||
var tomlObj any
|
||||
tomlErr := toml.Unmarshal(b, &tomlObj)
|
||||
if tomlErr == nil {
|
||||
parsedFromTOML = true
|
||||
var err error
|
||||
b, err = jsonx.Marshal(&tomlObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if format == "toml" {
|
||||
// File is known to be TOML but has syntax errors.
|
||||
return formatTOMLError(tomlErr)
|
||||
}
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
|
||||
return decoder.Decode(c)
|
||||
|
||||
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
||||
if yaml.IsJSONBuffer(b) {
|
||||
if err := decodeJSONContent(b, c, strict); err != nil {
|
||||
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle YAML content
|
||||
if strict {
|
||||
// In strict mode, always use our custom handler to support YAML merge
|
||||
if err := parseYAMLWithDotFieldsHandling(b, c); err != nil {
|
||||
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Non-strict mode, parse normally
|
||||
return yaml.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
// formatTOMLError extracts line/column information from TOML decode errors.
|
||||
func formatTOMLError(err error) error {
|
||||
var decErr *toml.DecodeError
|
||||
if errors.As(err, &decErr) {
|
||||
row, col := decErr.Position()
|
||||
return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error())
|
||||
}
|
||||
var strictErr *toml.StrictMissingError
|
||||
if errors.As(err, &strictErr) {
|
||||
return strictErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors.
|
||||
func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error {
|
||||
var typeErr *json.UnmarshalTypeError
|
||||
if errors.As(err, &typeErr) && typeErr.Field != "" {
|
||||
if includeLine {
|
||||
line := findFieldLineInContent(originalContent, typeErr.Field)
|
||||
if line > 0 {
|
||||
return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// findFieldLineInContent searches the original config content for a field name
|
||||
// and returns the 1-indexed line number where it appears, or 0 if not found.
|
||||
func findFieldLineInContent(content []byte, fieldPath string) int {
|
||||
if fieldPath == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Use the last component of the field path (e.g. "proxies" from "proxies" or
|
||||
// "protocol" from "transport.protocol").
|
||||
parts := strings.Split(fieldPath, ".")
|
||||
searchKey := parts[len(parts)-1]
|
||||
|
||||
lines := bytes.Split(content, []byte("\n"))
|
||||
for i, line := range lines {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
// Match TOML key assignments like: key = ...
|
||||
if bytes.HasPrefix(trimmed, []byte(searchKey)) {
|
||||
rest := bytes.TrimSpace(trimmed[len(searchKey):])
|
||||
if len(rest) > 0 && rest[0] == '=' {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
// Match TOML table array headers like: [[proxies]]
|
||||
if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||
@@ -131,7 +284,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
|
||||
}
|
||||
|
||||
configurer.UnmarshalFromMsg(m)
|
||||
configurer.Complete("")
|
||||
configurer.Complete()
|
||||
|
||||
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
||||
return nil, err
|
||||
@@ -139,7 +292,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
|
||||
return configurer, nil
|
||||
}
|
||||
|
||||
func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
|
||||
func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) {
|
||||
var (
|
||||
svrCfg *v1.ServerConfig
|
||||
isLegacyFormat bool
|
||||
@@ -158,71 +311,145 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
|
||||
isLegacyFormat = true
|
||||
} else {
|
||||
svrCfg = &v1.ServerConfig{}
|
||||
if err := LoadConfigureFromFile(path, svrCfg); err != nil {
|
||||
if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if svrCfg != nil {
|
||||
svrCfg.Complete()
|
||||
if err := svrCfg.Complete(); err != nil {
|
||||
return nil, isLegacyFormat, err
|
||||
}
|
||||
}
|
||||
return svrCfg, isLegacyFormat, nil
|
||||
}
|
||||
|
||||
func LoadClientConfig(path string) (
|
||||
// ClientConfigLoadResult contains the result of loading a client configuration file.
|
||||
type ClientConfigLoadResult struct {
|
||||
// Common contains the common client configuration.
|
||||
Common *v1.ClientCommonConfig
|
||||
|
||||
// Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles.
|
||||
// These are NOT completed (user prefix not added).
|
||||
Proxies []v1.ProxyConfigurer
|
||||
|
||||
// Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles.
|
||||
// These are NOT completed.
|
||||
Visitors []v1.VisitorConfigurer
|
||||
|
||||
// IsLegacyFormat indicates whether the config file is in legacy INI format.
|
||||
IsLegacyFormat bool
|
||||
}
|
||||
|
||||
// LoadClientConfigResult loads and parses a client configuration file.
|
||||
// It returns the raw configuration without completing proxies/visitors.
|
||||
// The caller should call Complete on the configs manually for legacy behavior.
|
||||
func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) {
|
||||
result := &ClientConfigLoadResult{
|
||||
Proxies: make([]v1.ProxyConfigurer, 0),
|
||||
Visitors: make([]v1.VisitorConfigurer, 0),
|
||||
}
|
||||
|
||||
if DetectLegacyINIFormatFromFile(path) {
|
||||
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||
for _, c := range legacyProxyCfgs {
|
||||
result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c))
|
||||
}
|
||||
for _, c := range legacyVisitorCfgs {
|
||||
result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c))
|
||||
}
|
||||
result.IsLegacyFormat = true
|
||||
} else {
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Common = &allCfg.ClientCommonConfig
|
||||
for _, c := range allCfg.Proxies {
|
||||
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
||||
}
|
||||
for _, c := range allCfg.Visitors {
|
||||
result.Visitors = append(result.Visitors, c.VisitorConfigurer)
|
||||
}
|
||||
}
|
||||
|
||||
// Load additional config from includes.
|
||||
// legacy ini format already handle this in ParseClientConfig.
|
||||
if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat {
|
||||
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Proxies = append(result.Proxies, extProxyCfgs...)
|
||||
result.Visitors = append(result.Visitors, extVisitorCfgs...)
|
||||
}
|
||||
|
||||
// Complete the common config
|
||||
if result.Common != nil {
|
||||
if err := result.Common.Complete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func LoadClientConfig(path string, strict bool) (
|
||||
*v1.ClientCommonConfig,
|
||||
[]v1.ProxyConfigurer,
|
||||
[]v1.VisitorConfigurer,
|
||||
bool, error,
|
||||
) {
|
||||
var (
|
||||
cliCfg *v1.ClientCommonConfig
|
||||
pxyCfgs = make([]v1.ProxyConfigurer, 0)
|
||||
visitorCfgs = make([]v1.VisitorConfigurer, 0)
|
||||
isLegacyFormat bool
|
||||
)
|
||||
|
||||
if DetectLegacyINIFormatFromFile(path) {
|
||||
legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, true, err
|
||||
}
|
||||
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||
for _, c := range legacyPxyCfgs {
|
||||
pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
|
||||
}
|
||||
for _, c := range legacyVisitorCfgs {
|
||||
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
|
||||
}
|
||||
isLegacyFormat = true
|
||||
} else {
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := LoadConfigureFromFile(path, &allCfg); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
cliCfg = &allCfg.ClientCommonConfig
|
||||
for _, c := range allCfg.Proxies {
|
||||
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
|
||||
}
|
||||
for _, c := range allCfg.Visitors {
|
||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
||||
}
|
||||
result, err := LoadClientConfigResult(path, strict)
|
||||
if err != nil {
|
||||
return nil, nil, nil, result != nil && result.IsLegacyFormat, err
|
||||
}
|
||||
|
||||
// Load additional config from includes.
|
||||
// legacy ini format alredy handle this in ParseClientConfig.
|
||||
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
|
||||
extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat)
|
||||
if err != nil {
|
||||
return nil, nil, nil, isLegacyFormat, err
|
||||
}
|
||||
pxyCfgs = append(pxyCfgs, extPxyCfgs...)
|
||||
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
|
||||
proxyCfgs := result.Proxies
|
||||
visitorCfgs := result.Visitors
|
||||
|
||||
proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = CompleteVisitorConfigurers(visitorCfgs)
|
||||
return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil
|
||||
}
|
||||
|
||||
func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer {
|
||||
proxyCfgs := proxies
|
||||
for _, c := range proxyCfgs {
|
||||
c.Complete()
|
||||
}
|
||||
return proxyCfgs
|
||||
}
|
||||
|
||||
func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer {
|
||||
visitorCfgs := visitors
|
||||
for _, c := range visitorCfgs {
|
||||
c.Complete()
|
||||
}
|
||||
return visitorCfgs
|
||||
}
|
||||
|
||||
func FilterClientConfigurers(
|
||||
common *v1.ClientCommonConfig,
|
||||
proxies []v1.ProxyConfigurer,
|
||||
visitors []v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||
if common == nil {
|
||||
common = &v1.ClientCommonConfig{}
|
||||
}
|
||||
|
||||
// Filter by start
|
||||
if len(cliCfg.Start) > 0 {
|
||||
startSet := sets.New(cliCfg.Start...)
|
||||
pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
proxyCfgs := proxies
|
||||
visitorCfgs := visitors
|
||||
|
||||
// Filter by start across merged configurers from all sources.
|
||||
// For example, store entries are also filtered by this set.
|
||||
if len(common.Start) > 0 {
|
||||
startSet := sets.New(common.Start...)
|
||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
return startSet.Has(c.GetBaseConfig().Name)
|
||||
})
|
||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
||||
@@ -230,20 +457,21 @@ func LoadClientConfig(path string) (
|
||||
})
|
||||
}
|
||||
|
||||
if cliCfg != nil {
|
||||
cliCfg.Complete()
|
||||
}
|
||||
for _, c := range pxyCfgs {
|
||||
c.Complete(cliCfg.User)
|
||||
}
|
||||
for _, c := range visitorCfgs {
|
||||
c.Complete(cliCfg)
|
||||
}
|
||||
return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil
|
||||
// Filter by enabled field in each proxy
|
||||
// nil or true means enabled, false means disabled
|
||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
return proxyCfgs, visitorCfgs
|
||||
}
|
||||
|
||||
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
pxyCfgs := make([]v1.ProxyConfigurer, 0)
|
||||
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
proxyCfgs := make([]v1.ProxyConfigurer, 0)
|
||||
visitorCfgs := make([]v1.VisitorConfigurer, 0)
|
||||
for _, path := range paths {
|
||||
absDir, err := filepath.Abs(filepath.Dir(path))
|
||||
@@ -265,11 +493,11 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
|
||||
if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
|
||||
// support yaml/json/toml
|
||||
cfg := v1.ClientConfig{}
|
||||
if err := LoadConfigureFromFile(absFile, &cfg); err != nil {
|
||||
if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil {
|
||||
return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
|
||||
}
|
||||
for _, c := range cfg.Proxies {
|
||||
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
|
||||
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
||||
}
|
||||
for _, c := range cfg.Visitors {
|
||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
||||
@@ -277,5 +505,5 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
|
||||
}
|
||||
}
|
||||
}
|
||||
return pxyCfgs, visitorCfgs, nil
|
||||
return proxyCfgs, visitorCfgs, nil
|
||||
}
|
||||
|
||||