Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9634fd99d1 | ||
|
|
7a1c248b67 | ||
|
|
886c9c2fdb | ||
|
|
266c492b5d | ||
|
|
5dd70ace6b | ||
|
|
fb2c98e87b | ||
|
|
ed13141c56 | ||
|
|
3370bd53f5 | ||
|
|
1245f8804e | ||
|
|
479e9f50c2 | ||
|
|
a4175a2595 | ||
|
|
36718d88e4 | ||
|
|
bc378bcbec | ||
|
|
33428ab538 | ||
|
|
ef96481f58 | ||
|
|
7526d7a69a | ||
|
|
2bdf25bae6 | ||
|
|
0fe8f7a0b6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 | ||
|
|
b5e90c03a1 | ||
|
|
b642a6323c | ||
|
|
6561107945 | ||
|
|
abf4942e8a | ||
|
|
7cfa546b55 | ||
|
|
0a798a7a69 | ||
|
|
604700cea5 | ||
|
|
610e5ed479 | ||
|
|
80d3f332e1 | ||
|
|
14253afe2f | ||
|
|
024c334d9d | ||
|
|
f795950742 | ||
|
|
024e4f5f1d | ||
|
|
dc3bc9182c | ||
|
|
e6dacf3a67 | ||
|
|
7fe295f4f4 | ||
|
|
c3bf952d8f | ||
|
|
f9065a6a78 | ||
|
|
61330d4d79 | ||
|
|
c777891f75 | ||
|
|
43cf1688e4 | ||
|
|
720c09c06b | ||
|
|
3fa76b72f3 | ||
|
|
8eb525a648 | ||
|
|
077ba80ba3 | ||
|
|
c99986fa28 | ||
|
|
b41d8f8e40 | ||
|
|
3c8d648ddc | ||
|
|
27f66baf54 | ||
|
|
c5a8f6ef4a | ||
|
|
e208043323 | ||
|
|
a78814a2e9 | ||
|
|
31b44c1feb | ||
|
|
773169e0c4 | ||
|
|
9757a351c6 | ||
|
|
1e8db66743 | ||
|
|
e0dd947e6a | ||
|
|
8b86e1473c | ||
|
|
b8d3ace113 | ||
|
|
450b8393bc | ||
|
|
27db6217ec | ||
|
|
6542dcd4ed | ||
|
|
092e5d3f94 | ||
|
|
01fed8d1a9 | ||
|
|
2a7aa69890 | ||
|
|
f47d8ab97f | ||
|
|
bb912d6c37 | ||
|
|
c73096f2bf | ||
|
|
0358113948 | ||
|
|
8593eff752 | ||
|
|
dff56cb0ca | ||
|
|
4383756fd4 | ||
|
|
6ba849fc75 | ||
|
|
9d5638cae6 | ||
|
|
62352c7ba5 | ||
|
|
4bbec09d57 | ||
|
|
f7a06cbe61 | ||
|
|
3a08c2aeb0 | ||
|
|
b14192a8d3 | ||
|
|
2466e65f43 | ||
|
|
2855ac71e3 | ||
|
|
fe4ca1b54e | ||
|
|
edd7cf8967 | ||
|
|
ccfe8c97f4 | ||
|
|
03c8d7bf96 | ||
|
|
2dcdb24cc4 | ||
|
|
d47e138bc9 | ||
|
|
f1fb2d721a | ||
|
|
ae73ec2fed | ||
|
|
e8045194cd | ||
|
|
69cc422edf | ||
|
|
243ca994e0 | ||
|
|
b4d5d8c756 | ||
|
|
c6f9d8d403 | ||
|
|
939c490768 | ||
|
|
f390e4a401 | ||
|
|
e649692217 | ||
|
|
77990c31ef | ||
|
|
e680acf42d | ||
|
|
522e2c94c1 | ||
|
|
301515d2e8 | ||
|
|
f0442d0cd5 | ||
|
|
9ced717d69 |
@ -2,10 +2,18 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
go-version-latest:
|
go-version-latest:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:1.22-node
|
- image: cimg/go:1.24-node
|
||||||
resource_class: large
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- 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
|
||||||
- run: make alltest
|
- run: make alltest
|
||||||
|
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [fatedier]
|
github: [fatedier]
|
||||||
custom: ["https://afdian.net/a/fatedier"]
|
custom: ["https://afdian.com/a/fatedier"]
|
||||||
|
|||||||
4
.github/workflows/build-and-push-image.yml
vendored
@ -2,7 +2,7 @@ name: Build Image and Publish to Dockerhub & GPR
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ created ]
|
types: [ published ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push frpc
|
- name: Build and push frpc
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frpc
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
|
|||||||
31
.github/workflows/golangci-lint.yml
vendored
@ -17,26 +17,19 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.24'
|
||||||
cache: false
|
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
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v4
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# 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.57
|
version: v2.3
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|||||||
14
.github/workflows/goreleaser.yml
vendored
@ -15,14 +15,22 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.24'
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- name: Build web assets (frps)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frps
|
||||||
|
- name: Build web assets (frpc)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frpc
|
||||||
- name: Make All
|
- name: Make All
|
||||||
run: |
|
run: |
|
||||||
./package.sh
|
./package.sh
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v5
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --release-notes=./Release.md
|
args: release --clean --release-notes=./Release.md
|
||||||
|
|||||||
10
.github/workflows/stale.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "Close stale issues"
|
name: "Close stale issues and PRs"
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "20 0 * * *"
|
- cron: "20 0 * * *"
|
||||||
@ -21,14 +21,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'Issues go stale after 21d of inactivity. Stale issues 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 21d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
|
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
||||||
stale-issue-label: 'lifecycle/stale'
|
stale-issue-label: 'lifecycle/stale'
|
||||||
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||||
stale-pr-label: 'lifecycle/stale'
|
stale-pr-label: 'lifecycle/stale'
|
||||||
exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||||
days-before-stale: 21
|
days-before-stale: 14
|
||||||
days-before-close: 7
|
days-before-close: 3
|
||||||
debug-only: ${{ github.event.inputs.debug-only }}
|
debug-only: ${{ github.event.inputs.debug-only }}
|
||||||
exempt-all-pr-milestones: true
|
exempt-all-pr-milestones: true
|
||||||
exempt-all-pr-assignees: true
|
exempt-all-pr-assignees: true
|
||||||
|
|||||||
3
.gitignore
vendored
@ -39,3 +39,6 @@ client.key
|
|||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# AI
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
218
.golangci.yml
@ -1,136 +1,116 @@
|
|||||||
service:
|
version: "2"
|
||||||
golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
concurrency: 4
|
concurrency: 4
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
timeout: 20m
|
||||||
deadline: 20m
|
|
||||||
build-tags:
|
build-tags:
|
||||||
- integ
|
- integ
|
||||||
- integfuzz
|
- integfuzz
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- unused
|
- asciicheck
|
||||||
|
- copyloopvar
|
||||||
- errcheck
|
- errcheck
|
||||||
- exportloopref
|
|
||||||
- gocritic
|
- gocritic
|
||||||
- gofumpt
|
- gosec
|
||||||
- goimports
|
|
||||||
- revive
|
|
||||||
- gosimple
|
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- lll
|
- lll
|
||||||
|
- makezero
|
||||||
- misspell
|
- misspell
|
||||||
- staticcheck
|
|
||||||
- stylecheck
|
|
||||||
- typecheck
|
|
||||||
- unconvert
|
|
||||||
- unparam
|
|
||||||
- gci
|
|
||||||
- gosec
|
|
||||||
- asciicheck
|
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- makezero
|
- revive
|
||||||
fast: false
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
linters-settings:
|
- unparam
|
||||||
errcheck:
|
- unused
|
||||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
settings:
|
||||||
# default is false: such cases aren't reported by default.
|
errcheck:
|
||||||
check-type-assertions: false
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
gocritic:
|
||||||
# default is false: such cases aren't reported by default.
|
disabled-checks:
|
||||||
check-blank: false
|
- exitAfterDefer
|
||||||
govet:
|
gosec:
|
||||||
# report about shadowed variables
|
excludes:
|
||||||
check-shadowing: false
|
- G401
|
||||||
maligned:
|
- G402
|
||||||
# print struct with more effective memory layout or not, false by default
|
- G404
|
||||||
suggest-new: true
|
- G501
|
||||||
misspell:
|
- G115
|
||||||
# Correct spellings using locale preferences for US or UK.
|
- G204
|
||||||
# Default is to use a neutral variety of English.
|
severity: low
|
||||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
confidence: low
|
||||||
locale: US
|
govet:
|
||||||
ignore-words:
|
disable:
|
||||||
- cancelled
|
- shadow
|
||||||
- marshalled
|
lll:
|
||||||
lll:
|
line-length: 160
|
||||||
# max line length, lines longer will be reported. Default is 120.
|
tab-width: 1
|
||||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
misspell:
|
||||||
line-length: 160
|
locale: US
|
||||||
# tab width in spaces. Default to 1.
|
ignore-rules:
|
||||||
tab-width: 1
|
- cancelled
|
||||||
gocritic:
|
- marshalled
|
||||||
disabled-checks:
|
unparam:
|
||||||
- exitAfterDefer
|
check-exported: false
|
||||||
unused:
|
exclusions:
|
||||||
check-exported: false
|
generated: lax
|
||||||
unparam:
|
presets:
|
||||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
- comments
|
||||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
- common-false-positives
|
||||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
- legacy
|
||||||
# with golangci-lint call it on a directory with the changed file.
|
- std-error-handling
|
||||||
check-exported: false
|
rules:
|
||||||
gci:
|
- linters:
|
||||||
sections:
|
- errcheck
|
||||||
- standard
|
- maligned
|
||||||
- default
|
path: _test\.go$|^tests/|^samples/
|
||||||
- prefix(github.com/fatedier/frp/)
|
- linters:
|
||||||
gosec:
|
- revive
|
||||||
severity: "low"
|
- staticcheck
|
||||||
confidence: "low"
|
text: use underscores in Go names
|
||||||
excludes:
|
- linters:
|
||||||
- G401
|
- revive
|
||||||
- G402
|
text: unused-parameter
|
||||||
- G501
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "avoid meaningless package names"
|
||||||
|
- linters:
|
||||||
|
- unparam
|
||||||
|
text: is always false
|
||||||
|
paths:
|
||||||
|
- .*\.pb\.go
|
||||||
|
- .*\.gen\.go
|
||||||
|
- genfiles$
|
||||||
|
- vendor$
|
||||||
|
- bin$
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(github.com/fatedier/frp/)
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- .*\.pb\.go
|
||||||
|
- .*\.gen\.go
|
||||||
|
- genfiles$
|
||||||
|
- vendor$
|
||||||
|
- bin$
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
issues:
|
issues:
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
max-issues-per-linter: 0
|
||||||
# But independently from this option we use default exclude patterns,
|
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
|
||||||
# excluded by default patterns execute `golangci-lint run --help`
|
|
||||||
# exclude:
|
|
||||||
# - composite literal uses unkeyed fields
|
|
||||||
|
|
||||||
exclude-rules:
|
|
||||||
# Exclude some linters from running on test files.
|
|
||||||
- path: _test\.go$|^tests/|^samples/
|
|
||||||
linters:
|
|
||||||
- errcheck
|
|
||||||
- maligned
|
|
||||||
- linters:
|
|
||||||
- revive
|
|
||||||
- stylecheck
|
|
||||||
text: "use underscores in Go names"
|
|
||||||
- linters:
|
|
||||||
- revive
|
|
||||||
text: "unused-parameter"
|
|
||||||
- linters:
|
|
||||||
- unparam
|
|
||||||
text: "is always false"
|
|
||||||
|
|
||||||
exclude-dirs:
|
|
||||||
- genfiles$
|
|
||||||
- vendor$
|
|
||||||
- bin$
|
|
||||||
exclude-files:
|
|
||||||
- ".*\\.pb\\.go"
|
|
||||||
- ".*\\.gen\\.go"
|
|
||||||
|
|
||||||
# Independently from option `exclude` we use default exclude patterns,
|
|
||||||
# it can be disabled by this option. To list all
|
|
||||||
# excluded by default patterns execute `golangci-lint run --help`.
|
|
||||||
# Default value for this option is true.
|
|
||||||
exclude-use-default: true
|
|
||||||
|
|
||||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
|
||||||
max-per-linter: 0
|
|
||||||
|
|
||||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
|
|||||||
21
Makefile
@ -2,19 +2,22 @@ export PATH := $(PATH):`go env GOPATH`/bin
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
all: env fmt build
|
.PHONY: web frps-web frpc-web frps frpc
|
||||||
|
|
||||||
|
all: env fmt web build
|
||||||
|
|
||||||
build: frps frpc
|
build: frps frpc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@go version
|
@go version
|
||||||
|
|
||||||
# compile assets into binary file
|
web: frps-web frpc-web
|
||||||
file:
|
|
||||||
rm -rf ./assets/frps/static/*
|
frps-web:
|
||||||
rm -rf ./assets/frpc/static/*
|
$(MAKE) -C web/frps build
|
||||||
cp -rf ./web/frps/dist/* ./assets/frps/static
|
|
||||||
cp -rf ./web/frpc/dist/* ./assets/frpc/static
|
frpc-web:
|
||||||
|
$(MAKE) -C web/frpc build
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
@ -25,7 +28,7 @@ fmt-more:
|
|||||||
gci:
|
gci:
|
||||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||||
|
|
||||||
vet:
|
vet: web
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
@ -36,7 +39,7 @@ frpc:
|
|||||||
|
|
||||||
test: gotest
|
test: gotest
|
||||||
|
|
||||||
gotest:
|
gotest: web
|
||||||
go test -v --cover ./assets/...
|
go test -v --cover ./assets/...
|
||||||
go test -v --cover ./cmd/...
|
go test -v --cover ./cmd/...
|
||||||
go test -v --cover ./client/...
|
go test -v --cover ./client/...
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
|
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
|||||||
104
README.md
@ -7,17 +7,48 @@
|
|||||||
|
|
||||||
[README](README.md) | [中文文档](README_zh.md)
|
[README](README.md) | [中文文档](README_zh.md)
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors. If you'd like to join them, please consider [sponsoring frp's development](https://github.com/sponsors/fatedier).
|
||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
</a>
|
<br>
|
||||||
<a> </a>
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
<br>
|
||||||
<img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
|
<br>
|
||||||
|
<b>The sovereign cloud that puts you in control</b>
|
||||||
|
<br>
|
||||||
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
@ -82,7 +113,12 @@ frp also offers a P2P connect mode.
|
|||||||
* [Client Plugins](#client-plugins)
|
* [Client Plugins](#client-plugins)
|
||||||
* [Server Manage Plugins](#server-manage-plugins)
|
* [Server Manage Plugins](#server-manage-plugins)
|
||||||
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
|
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
|
||||||
* [Releated Projects](#releated-projects)
|
* [Virtual Network (VirtualNet)](#virtual-network-virtualnet)
|
||||||
|
* [Feature Gates](#feature-gates)
|
||||||
|
* [Available Feature Gates](#available-feature-gates)
|
||||||
|
* [Enabling Feature Gates](#enabling-feature-gates)
|
||||||
|
* [Feature Lifecycle](#feature-lifecycle)
|
||||||
|
* [Related Projects](#related-projects)
|
||||||
* [Contributing](#contributing)
|
* [Contributing](#contributing)
|
||||||
* [Donation](#donation)
|
* [Donation](#donation)
|
||||||
* [GitHub Sponsors](#github-sponsors)
|
* [GitHub Sponsors](#github-sponsors)
|
||||||
@ -487,7 +523,7 @@ name = "ssh"
|
|||||||
type = "tcp"
|
type = "tcp"
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 22
|
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:
|
With the config above, variables can be passed into `frpc` program like this:
|
||||||
@ -597,6 +633,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
|
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
|
#### OIDC Authentication
|
||||||
|
|
||||||
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
|
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
|
||||||
@ -1010,7 +1061,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`.
|
|||||||
|
|
||||||
#### Proxy Protocol
|
#### Proxy Protocol
|
||||||
|
|
||||||
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP.
|
frp supports Proxy Protocol to send user's real IP to local services.
|
||||||
|
|
||||||
Here is an example for https service:
|
Here is an example for https service:
|
||||||
|
|
||||||
@ -1245,7 +1296,40 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote
|
|||||||
|
|
||||||
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
|
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
|
||||||
|
|
||||||
## Releated Projects
|
### 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/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.
|
* [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.
|
||||||
|
|||||||
51
README_zh.md
@ -9,17 +9,48 @@
|
|||||||
|
|
||||||
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
|
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。
|
||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
</a>
|
<br>
|
||||||
<a> </a>
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
<br>
|
||||||
<img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
|
<br>
|
||||||
|
<b>The sovereign cloud that puts you in control</b>
|
||||||
|
<br>
|
||||||
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
@ -89,12 +120,6 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
|||||||
|
|
||||||
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
|
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
|
||||||
|
|
||||||
国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。
|
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||||
|
|
||||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||||
|
|
||||||
### 知识星球
|
|
||||||
|
|
||||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
16
Release.md
@ -1,14 +1,8 @@
|
|||||||
### Notable Changes
|
## Features
|
||||||
|
|
||||||
We have optimized the heartbeat mechanism when tcpmux is enabled (enabled by default). The default value of `heartbeatInterval` has been adjusted to -1. This update ensures that when tcpmux is active, the client does not send additional heartbeats to the server. Since tcpmux incorporates its own heartbeat system, this change effectively reduces unnecessary data consumption, streamlining communication efficiency between client and server.
|
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
||||||
|
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
||||||
|
|
||||||
When connecting to frps versions older than v0.39.0 might encounter compatibility issues due to changes in the heartbeat mechanism. As a temporary workaround, setting the `heartbeatInterval` to 30 can help maintain stable connectivity with these older versions. We recommend updating to the latest frps version to leverage full functionality and improvements.
|
## Fixes
|
||||||
|
|
||||||
### Features
|
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
||||||
|
|
||||||
* Show tcpmux proxies on the frps dashboard.
|
|
||||||
* `http` proxy can modify the response header. For example, `responseHeaders.set.foo = "bar"` will add a new header `foo: bar` to the response.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* When an HTTP proxy request times out, it returns 504 instead of 404 now.
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func Load(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(fileSystem fs.FS) {
|
func Register(fileSystem fs.FS) {
|
||||||
subFs, err := fs.Sub(fileSystem, "static")
|
subFs, err := fs.Sub(fileSystem, "dist")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
content = subFs
|
content = subFs
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>frp client admin UI</title>
|
|
||||||
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.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,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>frps dashboard</title>
|
|
||||||
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -15,44 +15,29 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/api"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeneralResponse struct {
|
|
||||||
Code int
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
helper.Router.HandleFunc("/healthz", svr.healthz)
|
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 := helper.Router.NewRoute().Subrouter()
|
||||||
|
subRouter.Use(helper.AuthMiddleware)
|
||||||
subRouter.Use(helper.AuthMiddleware.Middleware)
|
subRouter.Use(httppkg.NewRequestLogger)
|
||||||
|
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||||
// api, see admin_api.go
|
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
|
||||||
|
|
||||||
// view
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
@ -62,201 +47,28 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// /healthz
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
w.WriteHeader(http.StatusOK)
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/reload
|
func newAPIController(svr *Service) *api.Controller {
|
||||||
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
return api.NewController(api.ControllerParams{
|
||||||
res := GeneralResponse{Code: 200}
|
GetProxyStatus: svr.getAllProxyStatus,
|
||||||
strictConfigMode := false
|
ServerAddr: svr.common.ServerAddr,
|
||||||
strictStr := r.URL.Query().Get("strictConfig")
|
ConfigFilePath: svr.configFilePath,
|
||||||
if strictStr != "" {
|
UnsafeFeatures: svr.unsafeFeatures,
|
||||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
UpdateConfig: svr.UpdateAllConfigurer,
|
||||||
}
|
GracefulClose: svr.GracefulClose,
|
||||||
|
})
|
||||||
log.Infof("api request [/api/reload]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("api response [/api/reload], code [%d]", res.Code)
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
|
|
||||||
if err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
res.Code = 500
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("success reload conf")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/stop
|
// getAllProxyStatus returns all proxy statuses.
|
||||||
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
|
|
||||||
log.Infof("api request [/api/stop]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("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 slices.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.Infof("Http request [/api/status]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("Http response [/api/status]")
|
|
||||||
buf, _ = json.Marshal(&res)
|
|
||||||
_, _ = w.Write(buf)
|
|
||||||
}()
|
|
||||||
|
|
||||||
svr.ctlMu.RLock()
|
svr.ctlMu.RLock()
|
||||||
ctl := svr.ctl
|
ctl := svr.ctl
|
||||||
svr.ctlMu.RUnlock()
|
svr.ctlMu.RUnlock()
|
||||||
if ctl == nil {
|
if ctl == nil {
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
ps := ctl.pm.GetAllProxyStatus()
|
|
||||||
for _, status := range ps {
|
|
||||||
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arrs := range res {
|
|
||||||
if len(arrs) <= 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/config
|
|
||||||
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
|
|
||||||
log.Infof("Http get request [/api/config]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("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.configFilePath == "" {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = "frpc has no config file path"
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(svr.configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("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.Infof("Http put request [/api/config]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("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.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = "body can't be empty"
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
|
|
||||||
res.Code = 500
|
|
||||||
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return ctl.pm.GetAllProxyStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
189
client/api/controller.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller handles HTTP API requests for frpc.
|
||||||
|
type Controller struct {
|
||||||
|
// getProxyStatus returns the current proxy status.
|
||||||
|
// Returns nil if the control connection is not established.
|
||||||
|
getProxyStatus func() []*proxy.WorkingStatus
|
||||||
|
|
||||||
|
// serverAddr is the frps server address for display.
|
||||||
|
serverAddr string
|
||||||
|
|
||||||
|
// configFilePath is the path to the configuration file.
|
||||||
|
configFilePath string
|
||||||
|
|
||||||
|
// unsafeFeatures is used for validation.
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
|
// updateConfig updates proxy and visitor configurations.
|
||||||
|
updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||||
|
|
||||||
|
// gracefulClose gracefully stops the service.
|
||||||
|
gracefulClose func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControllerParams contains parameters for creating an APIController.
|
||||||
|
type ControllerParams struct {
|
||||||
|
GetProxyStatus func() []*proxy.WorkingStatus
|
||||||
|
ServerAddr string
|
||||||
|
ConfigFilePath string
|
||||||
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||||
|
GracefulClose func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewController creates a new Controller.
|
||||||
|
func NewController(params ControllerParams) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
getProxyStatus: params.GetProxyStatus,
|
||||||
|
serverAddr: params.ServerAddr,
|
||||||
|
configFilePath: params.ConfigFilePath,
|
||||||
|
unsafeFeatures: params.UnsafeFeatures,
|
||||||
|
updateConfig: params.UpdateConfig,
|
||||||
|
gracefulClose: params.GracefulClose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload handles GET /api/reload
|
||||||
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
|
strictConfigMode := false
|
||||||
|
strictStr := ctx.Query("strictConfig")
|
||||||
|
if strictStr != "" {
|
||||||
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("success reload conf")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop handles POST /api/stop
|
||||||
|
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||||
|
go c.gracefulClose(100 * time.Millisecond)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status handles GET /api/status
|
||||||
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
|
res := make(StatusResp)
|
||||||
|
ps := c.getProxyStatus()
|
||||||
|
if ps == nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig handles GET /api/config
|
||||||
|
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
if c.configFilePath == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(c.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("load frpc config file error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutConfig handles PUT /api/config
|
||||||
|
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus
|
||||||
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
||||||
|
psr := ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
29
client/api/types.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
// StatusResp is the response for GET /api/status
|
||||||
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
|
|
||||||
|
// ProxyStatusResp contains proxy status information
|
||||||
|
type ProxyStatusResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
@ -17,7 +17,6 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -48,7 +47,7 @@ type defaultConnectorImpl struct {
|
|||||||
cfg *v1.ClientCommonConfig
|
cfg *v1.ClientCommonConfig
|
||||||
|
|
||||||
muxSession *fmux.Session
|
muxSession *fmux.Session
|
||||||
quicConn quic.Connection
|
quicConn *quic.Conn
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error {
|
|||||||
|
|
||||||
fmuxCfg := fmux.DefaultConfig()
|
fmuxCfg := fmux.DefaultConfig()
|
||||||
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
||||||
fmuxCfg.LogOutput = io.Discard
|
// Use trace level for yamux logs
|
||||||
|
fmuxCfg.LogOutput = xlog.NewTraceWriter(xl)
|
||||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||||
session, err := fmux.Client(conn, fmuxCfg)
|
session, err := fmux.Client(conn, fmuxCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/wait"
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionContext struct {
|
type SessionContext struct {
|
||||||
@ -42,10 +43,12 @@ type SessionContext struct {
|
|||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
// Indicates whether the connection is encrypted.
|
// Indicates whether the connection is encrypted.
|
||||||
ConnEncrypted bool
|
ConnEncrypted bool
|
||||||
// Sets authentication based on selected method
|
// Auth runtime used for login, heartbeats, and encryption.
|
||||||
AuthSetter auth.Setter
|
Auth *auth.ClientAuth
|
||||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||||
Connector Connector
|
Connector Connector
|
||||||
|
// Virtual net controller
|
||||||
|
VnetController *vnet.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
type Control struct {
|
type Control struct {
|
||||||
@ -88,7 +91,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.lastPong.Store(time.Now())
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
if sessionCtx.ConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -97,10 +100,11 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter)
|
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)
|
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||||
|
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
m := &msg.NewWorkConn{
|
m := &msg.NewWorkConn{
|
||||||
RunID: ctl.sessionCtx.RunID,
|
RunID: ctl.sessionCtx.RunID,
|
||||||
}
|
}
|
||||||
if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
|
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
||||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
@ -185,7 +189,7 @@ func (ctl *Control) handlePong(m msg.Message) {
|
|||||||
inMsg := m.(*msg.Pong)
|
inMsg := m.(*msg.Pong)
|
||||||
|
|
||||||
if inMsg.Error != "" {
|
if inMsg.Error != "" {
|
||||||
xl.Errorf("Pong message contains error: %s", inMsg.Error)
|
xl.Errorf("pong message contains error: %s", inMsg.Error)
|
||||||
ctl.closeSession()
|
ctl.closeSession()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -230,7 +234,7 @@ func (ctl *Control) registerMsgHandlers() {
|
|||||||
ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
|
ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
|
||||||
}
|
}
|
||||||
|
|
||||||
// headerWorker sends heartbeat to server and check heartbeat timeout.
|
// heartbeatWorker sends heartbeat to server and check heartbeat timeout.
|
||||||
func (ctl *Control) heartbeatWorker() {
|
func (ctl *Control) heartbeatWorker() {
|
||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
|
|
||||||
@ -239,7 +243,7 @@ func (ctl *Control) heartbeatWorker() {
|
|||||||
sendHeartBeat := func() (bool, error) {
|
sendHeartBeat := func() (bool, error) {
|
||||||
xl.Debugf("send heartbeat to server")
|
xl.Debugf("send heartbeat to server")
|
||||||
pingMsg := &msg.Ping{}
|
pingMsg := &msg.Ping{}
|
||||||
if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
|
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
||||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -272,10 +276,12 @@ func (ctl *Control) heartbeatWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) worker() {
|
func (ctl *Control) worker() {
|
||||||
|
xl := ctl.xl
|
||||||
go ctl.heartbeatWorker()
|
go ctl.heartbeatWorker()
|
||||||
go ctl.msgDispatcher.Run()
|
go ctl.msgDispatcher.Run()
|
||||||
|
|
||||||
<-ctl.msgDispatcher.Done()
|
<-ctl.msgDispatcher.Done()
|
||||||
|
xl.Debugf("control message dispatcher exited")
|
||||||
ctl.closeSession()
|
ctl.closeSession()
|
||||||
|
|
||||||
ctl.pm.Close()
|
ctl.pm.Close()
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
var ErrPayloadType = errors.New("error payload type")
|
var ErrPayloadType = errors.New("error payload type")
|
||||||
|
|
||||||
type Handler func(payload interface{}) error
|
type Handler func(payload any) error
|
||||||
|
|
||||||
type StartProxyPayload struct {
|
type StartProxyPayload struct {
|
||||||
NewProxyMsg *msg.NewProxy
|
NewProxyMsg *msg.NewProxy
|
||||||
|
|||||||
@ -20,13 +20,11 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
libio "github.com/fatedier/golib/io"
|
libio "github.com/fatedier/golib/io"
|
||||||
libnet "github.com/fatedier/golib/net"
|
libnet "github.com/fatedier/golib/net"
|
||||||
pp "github.com/pires/go-proxyproto"
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
@ -35,7 +33,9 @@ import (
|
|||||||
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/pkg/util/limit"
|
"github.com/fatedier/frp/pkg/util/limit"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
||||||
@ -57,7 +57,9 @@ func NewProxy(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pxyConf v1.ProxyConfigurer,
|
pxyConf v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
) (pxy Proxy) {
|
) (pxy Proxy) {
|
||||||
var limiter *rate.Limiter
|
var limiter *rate.Limiter
|
||||||
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
|
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
|
||||||
@ -68,8 +70,10 @@ func NewProxy(
|
|||||||
baseProxy := BaseProxy{
|
baseProxy := BaseProxy{
|
||||||
baseCfg: pxyConf.GetBaseConfig(),
|
baseCfg: pxyConf.GetBaseConfig(),
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
xl: xlog.FromContextSafe(ctx),
|
xl: xlog.FromContextSafe(ctx),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
@ -84,7 +88,9 @@ func NewProxy(
|
|||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
baseCfg *v1.ProxyBaseConfig
|
baseCfg *v1.ProxyBaseConfig
|
||||||
clientCfg *v1.ClientCommonConfig
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
encryptionKey []byte
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
|
vnetController *vnet.Controller
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
// proxyPlugin is used to handle connections instead of dialing to local service.
|
// proxyPlugin is used to handle connections instead of dialing to local service.
|
||||||
// It's only validate for TCP protocol now.
|
// It's only validate for TCP protocol now.
|
||||||
@ -98,7 +104,10 @@ type BaseProxy struct {
|
|||||||
|
|
||||||
func (pxy *BaseProxy) Run() error {
|
func (pxy *BaseProxy) Run() error {
|
||||||
if pxy.baseCfg.Plugin.Type != "" {
|
if pxy.baseCfg.Plugin.Type != "" {
|
||||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions)
|
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||||
|
Name: pxy.baseCfg.Name,
|
||||||
|
VnetController: pxy.vnetController,
|
||||||
|
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -123,7 +132,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
|
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common handler for tcp work connections.
|
// Common handler for tcp work connections.
|
||||||
@ -157,42 +166,29 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if we need to send proxy protocol info
|
// check if we need to send proxy protocol info
|
||||||
var extraInfo plugin.ExtraInfo
|
var connInfo plugin.ConnectionInfo
|
||||||
if m.SrcAddr != "" && m.SrcPort != 0 {
|
if m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
if m.DstAddr == "" {
|
if m.DstAddr == "" {
|
||||||
m.DstAddr = "127.0.0.1"
|
m.DstAddr = "127.0.0.1"
|
||||||
}
|
}
|
||||||
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
|
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))))
|
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
|
||||||
extraInfo.SrcAddr = srcAddr
|
connInfo.SrcAddr = srcAddr
|
||||||
extraInfo.DstAddr = dstAddr
|
connInfo.DstAddr = dstAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
h := &pp.Header{
|
// Use the common proxy protocol builder function
|
||||||
Command: pp.PROXY,
|
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||||
SourceAddr: extraInfo.SrcAddr,
|
connInfo.ProxyProtocolHeader = header
|
||||||
DestinationAddr: extraInfo.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
|
|
||||||
}
|
}
|
||||||
|
connInfo.Conn = remote
|
||||||
|
connInfo.UnderlyingConn = workConn
|
||||||
|
|
||||||
if pxy.proxyPlugin != nil {
|
if pxy.proxyPlugin != nil {
|
||||||
// if plugin is set, let plugin handle connection first
|
// if plugin is set, let plugin handle connection first
|
||||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||||
pxy.proxyPlugin.Handle(remote, workConn, &extraInfo)
|
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||||
xl.Debugf("handle by plugin finished")
|
xl.Debugf("handle by plugin finished")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -210,8 +206,8 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
|
xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
|
||||||
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||||
|
|
||||||
if extraInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -28,17 +28,20 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
proxies map[string]*Wrapper
|
proxies map[string]*Wrapper
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
closed bool
|
closed bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
clientCfg *v1.ClientCommonConfig
|
encryptionKey []byte
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@ -46,12 +49,16 @@ type Manager struct {
|
|||||||
func NewManager(
|
func NewManager(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
proxies: make(map[string]*Wrapper),
|
proxies: make(map[string]*Wrapper),
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
closed: false,
|
closed: false,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
@ -96,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
|
var m msg.Message
|
||||||
switch e := payload.(type) {
|
switch e := payload.(type) {
|
||||||
case *event.StartProxyPayload:
|
case *event.StartProxyPayload:
|
||||||
@ -159,7 +166,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
|||||||
for _, cfg := range proxyCfgs {
|
for _, cfg := range proxyCfgs {
|
||||||
name := cfg.GetBaseConfig().Name
|
name := cfg.GetBaseConfig().Name
|
||||||
if _, ok := pm.proxies[name]; !ok {
|
if _, ok := pm.proxies[name]; !ok {
|
||||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
|
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||||
if pm.inWorkConnCallback != nil {
|
if pm.inWorkConnCallback != nil {
|
||||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -73,6 +74,8 @@ type Wrapper struct {
|
|||||||
handler event.Handler
|
handler event.Handler
|
||||||
|
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
|
// vnet controller
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
health uint32
|
health uint32
|
||||||
lastSendStartMsg time.Time
|
lastSendStartMsg time.Time
|
||||||
@ -89,8 +92,10 @@ func NewWrapper(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg v1.ProxyConfigurer,
|
cfg v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
eventHandler event.Handler,
|
eventHandler event.Handler,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
) *Wrapper {
|
) *Wrapper {
|
||||||
baseInfo := cfg.GetBaseConfig()
|
baseInfo := cfg.GetBaseConfig()
|
||||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
|
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
|
||||||
@ -105,6 +110,7 @@ func NewWrapper(
|
|||||||
healthNotifyCh: make(chan struct{}),
|
healthNotifyCh: make(chan struct{}),
|
||||||
handler: eventHandler,
|
handler: eventHandler,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
}
|
}
|
||||||
@ -117,7 +123,7 @@ func NewWrapper(
|
|||||||
xl.Tracef("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
|
return pw
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +143,7 @@ func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
|
|||||||
pw.Phase = ProxyPhaseStartErr
|
pw.Phase = ProxyPhaseStartErr
|
||||||
pw.Err = respErr
|
pw.Err = respErr
|
||||||
pw.lastStartErr = time.Now()
|
pw.lastStartErr = time.Now()
|
||||||
return fmt.Errorf(pw.Err)
|
return fmt.Errorf("%s", pw.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pw.pxy.Run(); err != nil {
|
if err := pw.pxy.Run(); err != nil {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
go workConnReaderFn(workConn, readCh)
|
go workConnReaderFn(workConn, readCh)
|
||||||
go heartbeatFn(sendCh)
|
go heartbeatFn(sendCh)
|
||||||
|
|
||||||
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize))
|
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
||||||
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
||||||
go heartbeatFn(pxy.sendCh)
|
go heartbeatFn(pxy.sendCh)
|
||||||
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
|
|
||||||
|
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
|
||||||
|
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,11 +64,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
|||||||
}
|
}
|
||||||
|
|
||||||
xl.Tracef("nathole prepare start")
|
xl.Tracef("nathole prepare start")
|
||||||
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
xl.Warnf("nathole prepare error: %v", err)
|
xl.Warnf("nathole prepare error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||||
defer prepareResult.ListenConn.Close()
|
defer prepareResult.ListenConn.Close()
|
||||||
|
|||||||
@ -31,12 +31,14 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/auth"
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/pkg/util/wait"
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -63,6 +65,8 @@ type ServiceOptions struct {
|
|||||||
ProxyCfgs []v1.ProxyConfigurer
|
ProxyCfgs []v1.ProxyConfigurer
|
||||||
VisitorCfgs []v1.VisitorConfigurer
|
VisitorCfgs []v1.VisitorConfigurer
|
||||||
|
|
||||||
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
// 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.
|
// 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.
|
// It may be initialized using command line parameters or called directly.
|
||||||
@ -87,13 +91,16 @@ type ServiceOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
||||||
func setServiceOptionsDefault(options *ServiceOptions) {
|
func setServiceOptionsDefault(options *ServiceOptions) error {
|
||||||
if options.Common != nil {
|
if options.Common != nil {
|
||||||
options.Common.Complete()
|
if err := options.Common.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.ConnectorCreator == nil {
|
if options.ConnectorCreator == nil {
|
||||||
options.ConnectorCreator = NewConnector
|
options.ConnectorCreator = NewConnector
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service is the client service that connects to frps and provides proxy services.
|
// Service is the client service that connects to frps and provides proxy services.
|
||||||
@ -104,18 +111,22 @@ type Service struct {
|
|||||||
// Uniq id got from frps, it will be attached to loginMsg.
|
// Uniq id got from frps, it will be attached to loginMsg.
|
||||||
runID string
|
runID string
|
||||||
|
|
||||||
// Sets authentication based on selected method
|
// Auth runtime and encryption materials
|
||||||
authSetter auth.Setter
|
auth *auth.ClientAuth
|
||||||
|
|
||||||
// web server for admin UI and apis
|
// web server for admin UI and apis
|
||||||
webServer *httppkg.Server
|
webServer *httppkg.Server
|
||||||
|
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
cfgMu sync.RWMutex
|
cfgMu sync.RWMutex
|
||||||
common *v1.ClientCommonConfig
|
common *v1.ClientCommonConfig
|
||||||
proxyCfgs []v1.ProxyConfigurer
|
proxyCfgs []v1.ProxyConfigurer
|
||||||
visitorCfgs []v1.VisitorConfigurer
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
clientSpec *msg.ClientSpec
|
clientSpec *msg.ClientSpec
|
||||||
|
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
// The configuration file used to initialize this client, or an empty
|
// The configuration file used to initialize this client, or an empty
|
||||||
// string if no configuration file was used.
|
// string if no configuration file was used.
|
||||||
configFilePath string
|
configFilePath string
|
||||||
@ -131,7 +142,9 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewService(options ServiceOptions) (*Service, error) {
|
func NewService(options ServiceOptions) (*Service, error) {
|
||||||
setServiceOptionsDefault(&options)
|
if err := setServiceOptionsDefault(&options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var webServer *httppkg.Server
|
var webServer *httppkg.Server
|
||||||
if options.Common.WebServer.Port > 0 {
|
if options.Common.WebServer.Port > 0 {
|
||||||
@ -141,12 +154,19 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
}
|
}
|
||||||
webServer = ws
|
webServer = ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
authSetter: auth.NewAuthSetter(options.Common.Auth),
|
auth: authRuntime,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
|
unsafeFeatures: options.UnsafeFeatures,
|
||||||
proxyCfgs: options.ProxyCfgs,
|
proxyCfgs: options.ProxyCfgs,
|
||||||
visitorCfgs: options.VisitorCfgs,
|
visitorCfgs: options.VisitorCfgs,
|
||||||
clientSpec: options.ClientSpec,
|
clientSpec: options.ClientSpec,
|
||||||
@ -156,6 +176,9 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
if webServer != nil {
|
if webServer != nil {
|
||||||
webServer.RouteRegister(s.registerRouteHandlers)
|
webServer.RouteRegister(s.registerRouteHandlers)
|
||||||
}
|
}
|
||||||
|
if options.Common.VirtualNet.Address != "" {
|
||||||
|
s.vnetController = vnet.NewController(options.Common.VirtualNet)
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +192,28 @@ func (svr *Service) Run(ctx context.Context) error {
|
|||||||
netpkg.SetDefaultDNSAddress(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
|
// first login to frps
|
||||||
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
|
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
|
||||||
if svr.ctl == nil {
|
if svr.ctl == nil {
|
||||||
@ -179,14 +224,6 @@ func (svr *Service) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
go svr.keepControllerWorking()
|
go svr.keepControllerWorking()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
<-svr.ctx.Done()
|
<-svr.ctx.Done()
|
||||||
svr.stop()
|
svr.stop()
|
||||||
return nil
|
return nil
|
||||||
@ -244,11 +281,15 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
loginMsg := &msg.Login{
|
loginMsg := &msg.Login{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
|
Hostname: hostname,
|
||||||
PoolCount: svr.common.Transport.PoolCount,
|
PoolCount: svr.common.Transport.PoolCount,
|
||||||
User: svr.common.User,
|
User: svr.common.User,
|
||||||
|
ClientID: svr.common.ClientID,
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
@ -259,7 +300,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add auth
|
// Add auth
|
||||||
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,22 +346,22 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
proxyCfgs := svr.proxyCfgs
|
proxyCfgs := svr.proxyCfgs
|
||||||
visitorCfgs := svr.visitorCfgs
|
visitorCfgs := svr.visitorCfgs
|
||||||
svr.cfgMu.RUnlock()
|
svr.cfgMu.RUnlock()
|
||||||
connEncrypted := true
|
|
||||||
if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" {
|
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
|
||||||
connEncrypted = false
|
|
||||||
}
|
|
||||||
sessionCtx := &SessionContext{
|
sessionCtx := &SessionContext{
|
||||||
Common: svr.common,
|
Common: svr.common,
|
||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ConnEncrypted: connEncrypted,
|
ConnEncrypted: connEncrypted,
|
||||||
AuthSetter: svr.authSetter,
|
Auth: svr.auth,
|
||||||
Connector: connector,
|
Connector: connector,
|
||||||
|
VnetController: svr.vnetController,
|
||||||
}
|
}
|
||||||
ctl, err := NewControl(svr.ctx, sessionCtx)
|
ctl, err := NewControl(svr.ctx, sessionCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("NewControl error: %v", err)
|
xl.Errorf("new control error: %v", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
|
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
|
||||||
@ -378,6 +419,10 @@ func (svr *Service) stop() {
|
|||||||
svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
|
svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
|
||||||
svr.ctl = nil
|
svr.ctl = nil
|
||||||
}
|
}
|
||||||
|
if svr.webServer != nil {
|
||||||
|
svr.webServer.Close()
|
||||||
|
svr.webServer = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -44,6 +45,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go sv.internalConnWorker()
|
go sv.internalConnWorker()
|
||||||
|
|
||||||
|
if sv.plugin != nil {
|
||||||
|
sv.plugin.Start()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() {
|
|||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
defer userConn.Close()
|
var tunnelErr error
|
||||||
|
defer func() {
|
||||||
|
// If there was an error and connection supports CloseWithError, use it
|
||||||
|
if tunnelErr != nil {
|
||||||
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
@ -98,6 +114,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,12 +123,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +140,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper wraps some functions for visitor to use.
|
// Helper wraps some functions for visitor to use.
|
||||||
@ -34,6 +36,8 @@ type Helper interface {
|
|||||||
// MsgTransporter returns the message transporter that is used to send and receive messages
|
// MsgTransporter returns the message transporter that is used to send and receive messages
|
||||||
// to the frp server through the controller.
|
// to the frp server through the controller.
|
||||||
MsgTransporter() transport.MessageTransporter
|
MsgTransporter() transport.MessageTransporter
|
||||||
|
// VNetController returns the vnet controller that is used to manage the virtual network.
|
||||||
|
VNetController() *vnet.Controller
|
||||||
// RunID returns the run id of current controller.
|
// RunID returns the run id of current controller.
|
||||||
RunID() string
|
RunID() string
|
||||||
}
|
}
|
||||||
@ -50,14 +54,34 @@ func NewVisitor(
|
|||||||
cfg v1.VisitorConfigurer,
|
cfg v1.VisitorConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
helper Helper,
|
helper Helper,
|
||||||
) (visitor Visitor) {
|
) (Visitor, error) {
|
||||||
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
|
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
|
||||||
|
ctx = xlog.NewContext(ctx, xl)
|
||||||
|
var visitor Visitor
|
||||||
baseVisitor := BaseVisitor{
|
baseVisitor := BaseVisitor{
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
helper: helper,
|
helper: helper,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: ctx,
|
||||||
internalLn: netpkg.NewInternalListener(),
|
internalLn: netpkg.NewInternalListener(),
|
||||||
}
|
}
|
||||||
|
if cfg.GetBaseConfig().Plugin.Type != "" {
|
||||||
|
p, err := plugin.Create(
|
||||||
|
cfg.GetBaseConfig().Plugin.Type,
|
||||||
|
plugin.PluginContext{
|
||||||
|
Name: cfg.GetBaseConfig().Name,
|
||||||
|
Ctx: ctx,
|
||||||
|
VnetController: helper.VNetController(),
|
||||||
|
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) {
|
switch cfg := cfg.(type) {
|
||||||
case *v1.STCPVisitorConfig:
|
case *v1.STCPVisitorConfig:
|
||||||
visitor = &STCPVisitor{
|
visitor = &STCPVisitor{
|
||||||
@ -77,7 +101,7 @@ func NewVisitor(
|
|||||||
checkCloseCh: make(chan struct{}),
|
checkCloseCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return visitor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseVisitor struct {
|
type BaseVisitor struct {
|
||||||
@ -85,6 +109,7 @@ type BaseVisitor struct {
|
|||||||
helper Helper
|
helper Helper
|
||||||
l net.Listener
|
l net.Listener
|
||||||
internalLn *netpkg.InternalListener
|
internalLn *netpkg.InternalListener
|
||||||
|
plugin plugin.Plugin
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() {
|
|||||||
if v.internalLn != nil {
|
if v.internalLn != nil {
|
||||||
v.internalLn.Close()
|
v.internalLn.Close()
|
||||||
}
|
}
|
||||||
|
if v.plugin != nil {
|
||||||
|
v.plugin.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -50,6 +51,7 @@ func NewManager(
|
|||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
connectServer func() (net.Conn, error),
|
connectServer func() (net.Conn, error),
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
@ -62,6 +64,7 @@ func NewManager(
|
|||||||
m.helper = &visitorHelperImpl{
|
m.helper = &visitorHelperImpl{
|
||||||
connectServerFn: connectServer,
|
connectServerFn: connectServer,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
transferConnFn: m.TransferConn,
|
transferConnFn: m.TransferConn,
|
||||||
runID: runID,
|
runID: runID,
|
||||||
}
|
}
|
||||||
@ -112,7 +115,11 @@ func (vm *Manager) Close() {
|
|||||||
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
|
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
|
||||||
xl := xlog.FromContextSafe(vm.ctx)
|
xl := xlog.FromContextSafe(vm.ctx)
|
||||||
name := cfg.GetBaseConfig().Name
|
name := cfg.GetBaseConfig().Name
|
||||||
visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("new visitor error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
err = visitor.Run()
|
err = visitor.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("start error: %v", err)
|
xl.Warnf("start error: %v", err)
|
||||||
@ -187,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
|||||||
type visitorHelperImpl struct {
|
type visitorHelperImpl struct {
|
||||||
connectServerFn func() (net.Conn, error)
|
connectServerFn func() (net.Conn, error)
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
|
vnetController *vnet.Controller
|
||||||
transferConnFn func(name string, conn net.Conn) error
|
transferConnFn func(name string, conn net.Conn) error
|
||||||
runID string
|
runID string
|
||||||
}
|
}
|
||||||
@ -203,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
|
|||||||
return v.msgTransporter
|
return v.msgTransporter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) VNetController() *vnet.Controller {
|
||||||
|
return v.vnetController
|
||||||
|
}
|
||||||
|
|
||||||
func (v *visitorHelperImpl) RunID() string {
|
func (v *visitorHelperImpl) RunID() string {
|
||||||
return v.runID
|
return v.runID
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
|||||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||||
go sv.keepTunnelOpenWorker()
|
go sv.keepTunnelOpenWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sv.plugin != nil {
|
||||||
|
sv.plugin.Start()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
|||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
|
xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
|
||||||
conn, err := sv.getTunnelConn()
|
conn, err := sv.getTunnelConn(sv.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
||||||
_ = sv.retryLimiter.Wait(sv.ctx)
|
_ = sv.retryLimiter.Wait(sv.ctx)
|
||||||
@ -157,9 +161,17 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
|||||||
|
|
||||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
isConnTrasfered := false
|
isConnTransferred := false
|
||||||
|
var tunnelErr error
|
||||||
defer func() {
|
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()
|
userConn.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -168,7 +180,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
|
|
||||||
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
|
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
|
||||||
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
|
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
|
||||||
ctx := context.Background()
|
ctx := sv.ctx
|
||||||
if sv.cfg.FallbackTo != "" {
|
if sv.cfg.FallbackTo != "" {
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -177,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
tunnelConn, err := sv.openTunnel(ctx)
|
tunnelConn, err := sv.openTunnel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("open tunnel error: %v", err)
|
xl.Errorf("open tunnel error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
|
||||||
// no fallback, just return
|
// no fallback, just return
|
||||||
if sv.cfg.FallbackTo == "" {
|
if sv.cfg.FallbackTo == "" {
|
||||||
return
|
return
|
||||||
@ -187,7 +201,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isConnTrasfered = true
|
isConnTransferred = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,40 +230,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
// openTunnel will open a tunnel connection to the target server.
|
// openTunnel will open a tunnel connection to the target server.
|
||||||
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
|
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||||
defer ticker.Stop()
|
defer cancel()
|
||||||
|
|
||||||
timeoutC := time.After(20 * time.Second)
|
timer := time.NewTimer(0)
|
||||||
immediateTrigger := make(chan struct{}, 1)
|
defer timer.Stop()
|
||||||
defer close(immediateTrigger)
|
|
||||||
immediateTrigger <- struct{}{}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-sv.ctx.Done():
|
case <-sv.ctx.Done():
|
||||||
return nil, sv.ctx.Err()
|
return nil, sv.ctx.Err()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
case <-immediateTrigger:
|
return nil, fmt.Errorf("open tunnel timeout")
|
||||||
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.Warnf("get tunnel connection error: %v", err)
|
|
||||||
}
|
}
|
||||||
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) {
|
func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
||||||
conn, err := sv.session.OpenConn(sv.ctx)
|
conn, err := sv.session.OpenConn(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
@ -275,11 +287,19 @@ func (sv *XTCPVisitor) makeNatHole() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xl.Tracef("nathole prepare start")
|
xl.Tracef("nathole prepare start")
|
||||||
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
xl.Warnf("nathole prepare error: %v", err)
|
xl.Warnf("nathole prepare error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||||
|
|
||||||
@ -394,7 +414,7 @@ func (ks *KCPTunnelSession) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QUICTunnelSession struct {
|
type QUICTunnelSession struct {
|
||||||
session quic.Connection
|
session *quic.Conn
|
||||||
listenConn *net.UDPConn
|
listenConn *net.UDPConn
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/fatedier/frp/assets/frpc"
|
|
||||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
|
_ "github.com/fatedier/frp/web/frpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@ -15,9 +15,11 @@
|
|||||||
package sub
|
package sub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rodaine/table"
|
"github.com/rodaine/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -27,24 +29,24 @@ import (
|
|||||||
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var adminAPITimeout = 30 * time.Second
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(NewAdminCommand(
|
commands := []struct {
|
||||||
"reload",
|
name string
|
||||||
"Hot-Reload frpc configuration",
|
description string
|
||||||
ReloadHandler,
|
handler func(*v1.ClientCommonConfig) error
|
||||||
))
|
}{
|
||||||
|
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
|
||||||
|
{"status", "Overview of all proxies status", StatusHandler},
|
||||||
|
{"stop", "Stop the running frpc", StopHandler},
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd.AddCommand(NewAdminCommand(
|
for _, cmdConfig := range commands {
|
||||||
"status",
|
cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler)
|
||||||
"Overview of all proxies status",
|
cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls")
|
||||||
StatusHandler,
|
rootCmd.AddCommand(cmd)
|
||||||
))
|
}
|
||||||
|
|
||||||
rootCmd.AddCommand(NewAdminCommand(
|
|
||||||
"stop",
|
|
||||||
"Stop the running frpc",
|
|
||||||
StopHandler,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
|
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
|
||||||
@ -73,7 +75,9 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
|
|||||||
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
if err := client.Reload(strictConfigMode); err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := client.Reload(ctx, strictConfigMode); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("reload success")
|
fmt.Println("reload success")
|
||||||
@ -83,7 +87,9 @@ func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
|||||||
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
res, err := client.GetAllProxyStatus()
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
res, err := client.GetAllProxyStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -109,7 +115,9 @@ func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
|||||||
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
|
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
if err := client.Stop(); err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := client.Stop(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("stop success")
|
fmt.Println("stop success")
|
||||||
|
|||||||
@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg = &v1.ClientCommonConfig{}
|
cfg = &v1.ClientCommonConfig{}
|
||||||
cfg.Complete()
|
if err := cfg.Complete(); err != nil {
|
||||||
|
fmt.Printf("failed to complete config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if natHoleSTUNServer != "" {
|
if natHoleSTUNServer != "" {
|
||||||
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
var proxyTypes = []v1.ProxyType{
|
var proxyTypes = []v1.ProxyType{
|
||||||
@ -73,8 +74,14 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
Use: name,
|
Use: name,
|
||||||
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
clientCfg.Complete()
|
if err := clientCfg.Complete(); err != nil {
|
||||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); 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)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@ -85,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -99,8 +106,13 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
Use: "visitor",
|
Use: "visitor",
|
||||||
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
clientCfg.Complete()
|
if err := clientCfg.Complete(); err != nil {
|
||||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); 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)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@ -111,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -31,6 +32,8 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
)
|
)
|
||||||
@ -40,6 +43,7 @@ var (
|
|||||||
cfgDir string
|
cfgDir string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -47,6 +51,9 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
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{
|
var rootCmd = &cobra.Command{
|
||||||
@ -58,15 +65,17 @@ var rootCmd = &cobra.Command{
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
|
||||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
// 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.
|
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||||
if cfgDir != "" {
|
if cfgDir != "" {
|
||||||
_ = runMultipleClients(cfgDir)
|
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not show command usage here.
|
// Do not show command usage here.
|
||||||
err := runClient(cfgFile)
|
err := runClient(cfgFile, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -75,7 +84,7 @@ var rootCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMultipleClients(cfgDir string) error {
|
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil || d.IsDir() {
|
if err != nil || d.IsDir() {
|
||||||
@ -85,7 +94,7 @@ func runMultipleClients(cfgDir string) error {
|
|||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := runClient(path)
|
err := runClient(path, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||||
}
|
}
|
||||||
@ -110,7 +119,7 @@ func handleTermSignal(svr *client.Service) {
|
|||||||
svr.GracefulClose(500 * time.Millisecond)
|
svr.GracefulClose(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient(cfgFilePath string) error {
|
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -120,20 +129,28 @@ func runClient(cfgFilePath string) error {
|
|||||||
"please use yaml/json/toml format instead!\n")
|
"please use yaml/json/toml format instead!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
|
if len(cfg.FeatureGates) > 0 {
|
||||||
|
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
|
|
||||||
|
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
func startService(
|
||||||
cfg *v1.ClientCommonConfig,
|
cfg *v1.ClientCommonConfig,
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
cfgFile string,
|
cfgFile string,
|
||||||
) error {
|
) error {
|
||||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
@ -146,6 +163,7 @@ func startService(
|
|||||||
Common: cfg,
|
Common: cfg,
|
||||||
ProxyCfgs: proxyCfgs,
|
ProxyCfgs: proxyCfgs,
|
||||||
VisitorCfgs: visitorCfgs,
|
VisitorCfgs: visitorCfgs,
|
||||||
|
UnsafeFeatures: unsafeFeatures,
|
||||||
ConfigFilePath: cfgFile,
|
ConfigFilePath: cfgFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -42,7 +43,8 @@ var verifyCmd = &cobra.Command{
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/fatedier/frp/assets/frps"
|
|
||||||
_ "github.com/fatedier/frp/pkg/metrics"
|
_ "github.com/fatedier/frp/pkg/metrics"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
|
_ "github.com/fatedier/frp/web/frps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@ -18,12 +18,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/server"
|
"github.com/fatedier/frp/server"
|
||||||
@ -33,6 +35,7 @@ var (
|
|||||||
cfgFile string
|
cfgFile string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
|
|
||||||
serverCfg v1.ServerConfig
|
serverCfg v1.ServerConfig
|
||||||
)
|
)
|
||||||
@ -41,6 +44,8 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
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, ", ")))
|
||||||
|
|
||||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||||
}
|
}
|
||||||
@ -70,11 +75,16 @@ var rootCmd = &cobra.Command{
|
|||||||
"please use yaml/json/toml format instead!\n")
|
"please use yaml/json/toml format instead!\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serverCfg.Complete()
|
if err := serverCfg.Complete(); err != nil {
|
||||||
|
fmt.Printf("failed to complete server config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
svrCfg = &serverCfg
|
svrCfg = &serverCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -42,7 +43,9 @@ var verifyCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
# 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}
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
user = "your_name"
|
user = "your_name"
|
||||||
|
|
||||||
@ -32,6 +34,11 @@ auth.method = "token"
|
|||||||
# auth token
|
# auth token
|
||||||
auth.token = "12345678"
|
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.
|
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
|
||||||
# auth.oidc.clientID = ""
|
# auth.oidc.clientID = ""
|
||||||
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
||||||
@ -50,6 +57,20 @@ auth.token = "12345678"
|
|||||||
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
||||||
# auth.oidc.additionalEndpointParams.var1 = "foobar"
|
# 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
|
# Set admin address for control frpc's action by http api such as reload
|
||||||
webServer.addr = "127.0.0.1"
|
webServer.addr = "127.0.0.1"
|
||||||
webServer.port = 7400
|
webServer.port = 7400
|
||||||
@ -76,7 +97,7 @@ transport.poolCount = 5
|
|||||||
|
|
||||||
# Specify keep alive interval for tcp mux.
|
# Specify keep alive interval for tcp mux.
|
||||||
# only valid if tcpMux is enabled.
|
# only valid if tcpMux is enabled.
|
||||||
# transport.tcpMuxKeepaliveInterval = 60
|
# transport.tcpMuxKeepaliveInterval = 30
|
||||||
|
|
||||||
# Communication protocol used to connect to server
|
# Communication protocol used to connect to server
|
||||||
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||||
@ -124,11 +145,25 @@ transport.tls.enable = true
|
|||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
# start = ["ssh", "dns"]
|
# 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.
|
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
# This parameter should be same between client and server.
|
# This parameter should be same between client and server.
|
||||||
# It affects the udp and sudp proxy.
|
# It affects the udp and sudp proxy.
|
||||||
udpPacketSize = 1500
|
udpPacketSize = 1500
|
||||||
|
|
||||||
|
# Feature gates allows you to enable or disable experimental features
|
||||||
|
# Format is a map of feature names to boolean values
|
||||||
|
# You can enable specific features:
|
||||||
|
#featureGates = { VirtualNet = true }
|
||||||
|
|
||||||
|
# VirtualNet settings for experimental virtual network capabilities
|
||||||
|
# The virtual network feature requires enabling the VirtualNet feature gate above
|
||||||
|
# virtualNet.address = "100.86.1.1/24"
|
||||||
|
|
||||||
# Additional metadatas for client.
|
# Additional metadatas for client.
|
||||||
metadatas.var1 = "abc"
|
metadatas.var1 = "abc"
|
||||||
metadatas.var2 = "123"
|
metadatas.var2 = "123"
|
||||||
@ -141,6 +176,8 @@ metadatas.var2 = "123"
|
|||||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
name = "ssh"
|
name = "ssh"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
|
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||||
|
# enabled = true
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 22
|
localPort = 22
|
||||||
# Limit bandwidth for this proxy, unit is KB and MB
|
# Limit bandwidth for this proxy, unit is KB and MB
|
||||||
@ -225,6 +262,8 @@ healthCheck.httpHeaders=[
|
|||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "web02"
|
name = "web02"
|
||||||
type = "https"
|
type = "https"
|
||||||
|
# Disable this proxy by setting enabled to false
|
||||||
|
# enabled = false
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 8000
|
localPort = 8000
|
||||||
subdomain = "web02"
|
subdomain = "web02"
|
||||||
@ -315,6 +354,26 @@ localAddr = "127.0.0.1:443"
|
|||||||
hostHeaderRewrite = "127.0.0.1"
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
requestHeaders.set.x-from-where = "frp"
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_http2http"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6007
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2http"
|
||||||
|
localAddr = "127.0.0.1:80"
|
||||||
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_tls2raw"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6008
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "tls2raw"
|
||||||
|
localAddr = "127.0.0.1:80"
|
||||||
|
crtPath = "./server.crt"
|
||||||
|
keyPath = "./server.key"
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "secret_tcp"
|
name = "secret_tcp"
|
||||||
# If the type is secret tcp, remotePort is useless
|
# If the type is secret tcp, remotePort is useless
|
||||||
@ -338,6 +397,21 @@ localPort = 22
|
|||||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||||
allowUsers = ["user1", "user2"]
|
allowUsers = ["user1", "user2"]
|
||||||
|
|
||||||
|
# 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
|
# frpc role visitor -> frps -> frpc role server
|
||||||
[[visitors]]
|
[[visitors]]
|
||||||
name = "secret_tcp_visitor"
|
name = "secret_tcp_visitor"
|
||||||
@ -369,3 +443,20 @@ maxRetriesAnHour = 8
|
|||||||
minRetryInterval = 90
|
minRetryInterval = 90
|
||||||
# fallbackTo = "stcp_visitor"
|
# fallbackTo = "stcp_visitor"
|
||||||
# fallbackTimeoutMs = 500
|
# 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"
|
||||||
|
|||||||
@ -34,7 +34,7 @@ transport.maxPoolCount = 5
|
|||||||
|
|
||||||
# Specify keep alive interval for tcp mux.
|
# Specify keep alive interval for tcp mux.
|
||||||
# only valid if tcpMux is true.
|
# only valid if tcpMux is true.
|
||||||
# transport.tcpMuxKeepaliveInterval = 60
|
# transport.tcpMuxKeepaliveInterval = 30
|
||||||
|
|
||||||
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
# If negative, keep-alive probes are disabled.
|
# If negative, keep-alive probes are disabled.
|
||||||
@ -105,6 +105,11 @@ auth.method = "token"
|
|||||||
# auth token
|
# auth token
|
||||||
auth.token = "12345678"
|
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.
|
# oidc issuer specifies the issuer to verify OIDC tokens with.
|
||||||
auth.oidc.issuer = ""
|
auth.oidc.issuer = ""
|
||||||
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 56 KiB 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 |
|
Before Width: | Height: | Size: 12 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
|
// http and https only
|
||||||
"custom_domains": []<string>,
|
"custom_domains": []<string>,
|
||||||
"subdomain": <string>,
|
"subdomain": <string>,
|
||||||
"locations": <string>,
|
"locations": []<string>,
|
||||||
"http_user": <string>,
|
"http_user": <string>,
|
||||||
"http_pwd": <string>,
|
"http_pwd": <string>,
|
||||||
"host_header_rewrite": <string>,
|
"host_header_rewrite": <string>,
|
||||||
|
|||||||
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.22 AS building
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
|
WORKDIR /web/frpc
|
||||||
|
COPY web/frpc/ ./
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
|
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
||||||
WORKDIR /building
|
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
|
FROM alpine:3
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
COPY --from=building /building/bin/frpc /usr/bin/frpc
|
COPY --from=building /building/bin/frpc /usr/bin/frpc
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/frpc"]
|
ENTRYPOINT ["/usr/bin/frpc"]
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
FROM golang:1.22 AS building
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
|
WORKDIR /web/frps
|
||||||
|
COPY web/frps/ ./
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
|
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
||||||
WORKDIR /building
|
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
|
FROM alpine:3
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
COPY --from=building /building/bin/frps /usr/bin/frps
|
COPY --from=building /building/bin/frps /usr/bin/frps
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/frps"]
|
ENTRYPOINT ["/usr/bin/frps"]
|
||||||
|
|||||||
66
go.mod
@ -1,34 +1,37 @@
|
|||||||
module github.com/fatedier/frp
|
module github.com/fatedier/frp
|
||||||
|
|
||||||
go 1.22
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/coreos/go-oidc/v3 v3.10.0
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/fatedier/golib v0.5.0
|
github.com/fatedier/golib v0.5.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/hashicorp/yamux v0.1.1
|
github.com/hashicorp/yamux v0.1.1
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.32.0
|
github.com/onsi/gomega v1.36.3
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0
|
github.com/pelletier/go-toml/v2 v2.2.0
|
||||||
github.com/pion/stun/v2 v2.0.0
|
github.com/pion/stun/v2 v2.0.0
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/quic-go/quic-go v0.42.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
github.com/rodaine/table v1.2.0
|
github.com/rodaine/table v1.2.0
|
||||||
github.com/samber/lo v1.39.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/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/tidwall/gjson v1.17.1
|
github.com/tidwall/gjson v1.17.1
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.8
|
github.com/vishvananda/netlink v1.3.0
|
||||||
golang.org/x/crypto v0.22.0
|
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||||
golang.org/x/net v0.24.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/oauth2 v0.16.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sync v0.6.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
|
golang.org/x/sync v0.16.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
k8s.io/apimachinery v0.28.8
|
k8s.io/apimachinery v0.28.8
|
||||||
k8s.io/client-go v0.28.8
|
k8s.io/client-go v0.28.8
|
||||||
@ -39,17 +42,15 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v2 v2.2.1 // indirect
|
||||||
@ -59,20 +60,19 @@ require (
|
|||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/templexxx/cpu v0.1.1 // indirect
|
||||||
github.com/templexxx/cpu v0.1.0 // indirect
|
github.com/templexxx/xorsimd v0.4.3 // indirect
|
||||||
github.com/templexxx/xorsimd v0.4.2 // indirect
|
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
golang.org/x/tools v0.17.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||||
@ -81,4 +81,4 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
|
// 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
|
||||||
|
|||||||
153
go.sum
@ -9,31 +9,27 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs=
|
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
||||||
github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
||||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@ -45,28 +41,25 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
|||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||||
@ -79,10 +72,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
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/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
|
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 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
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 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||||
@ -101,8 +94,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
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/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
@ -110,17 +105,19 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
|
|||||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
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 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
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 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
||||||
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
|
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
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 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
@ -129,17 +126,17 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI=
|
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
||||||
github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI=
|
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 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
@ -148,31 +145,35 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.8 h1:jlI/0jAyjoOjT/SaGB58s4bQMJiNS41A2RKzR6TMWeI=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.8/go.mod h1:oE9j2NVqAkuKO5o8ByKGch3vgVX3BNf8zqP8JiGq0bM=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
||||||
|
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
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-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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -186,50 +187,50 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
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.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.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.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -240,14 +241,16 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
@ -260,10 +263,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
|||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@ -274,6 +275,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
|
k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
SCRIPT=$(readlink -f "$0")
|
SCRIPT=$(readlink -f "$0")
|
||||||
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
|
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
|
||||||
|
|
||||||
ginkgo_command=$(which ginkgo 2>/dev/null)
|
# Check if ginkgo is available
|
||||||
if [ -z "$ginkgo_command" ]; then
|
if ! command -v ginkgo >/dev/null 2>&1; then
|
||||||
echo "ginkgo not found, try to install..."
|
echo "ginkgo not found, try to install..."
|
||||||
go install github.com/onsi/ginkgo/v2/ginkgo@v2.17.1
|
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
debug=false
|
debug=false
|
||||||
|
|||||||
@ -17,8 +17,8 @@ make -f ./Makefile.cross-compiles
|
|||||||
rm -rf ./release/packages
|
rm -rf ./release/packages
|
||||||
mkdir -p ./release/packages
|
mkdir -p ./release/packages
|
||||||
|
|
||||||
os_all='linux windows darwin freebsd android'
|
os_all='linux windows darwin freebsd openbsd android'
|
||||||
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
|
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
|
||||||
extra_all='_ hf'
|
extra_all='_ hf'
|
||||||
|
|
||||||
cd ./release
|
cd ./release
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@ -27,16 +28,56 @@ type Setter interface {
|
|||||||
SetNewWorkConn(*msg.NewWorkConn) error
|
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 {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||||
case v1.AuthMethodOIDC:
|
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:
|
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 {
|
type Verifier interface {
|
||||||
@ -45,12 +86,42 @@ type Verifier interface {
|
|||||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
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) {
|
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||||
case v1.AuthMethodOIDC:
|
case v1.AuthMethodOIDC:
|
||||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC)
|
tokenVerifier := NewTokenVerifier(cfg.OIDC)
|
||||||
|
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
|
||||||
}
|
}
|
||||||
return authVerifier
|
return authVerifier
|
||||||
}
|
}
|
||||||
|
|||||||
143
pkg/auth/oidc.go
@ -16,23 +16,72 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/clientcredentials"
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"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 {
|
type OidcAuthProvider struct {
|
||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
tokenGenerator *clientcredentials.Config
|
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)
|
eps := make(map[string][]string)
|
||||||
for k, v := range cfg.AdditionalEndpointParams {
|
for k, v := range cfg.AdditionalEndpointParams {
|
||||||
eps[k] = []string{v}
|
eps[k] = []string{v}
|
||||||
@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
|||||||
EndpointParams: eps,
|
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{
|
return &OidcAuthProvider{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
tokenGenerator: tokenGenerator,
|
tokenGenerator: tokenGenerator,
|
||||||
}
|
httpClient: httpClient,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||||
}
|
}
|
||||||
@ -87,14 +152,63 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
|||||||
return err
|
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 {
|
type OidcAuthConsumer struct {
|
||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier TokenVerifier
|
||||||
subjectFromLogin string
|
subjectsFromLogin []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer {
|
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||||
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
|
provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -105,9 +219,14 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCSer
|
|||||||
SkipExpiryCheck: cfg.SkipExpiryCheck,
|
SkipExpiryCheck: cfg.SkipExpiryCheck,
|
||||||
SkipIssuerCheck: cfg.SkipIssuerCheck,
|
SkipIssuerCheck: cfg.SkipIssuerCheck,
|
||||||
}
|
}
|
||||||
|
return provider.Verifier(&verifierConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
|
||||||
return &OidcAuthConsumer{
|
return &OidcAuthConsumer{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
verifier: provider.Verifier(&verifierConf),
|
verifier: verifier,
|
||||||
|
subjectsFromLogin: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +235,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||||
}
|
}
|
||||||
auth.subjectFromLogin = token.Subject
|
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||||
|
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,11 +246,11 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||||
}
|
}
|
||||||
if token.Subject != auth.subjectFromLogin {
|
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||||
"original subject: %s, "+
|
"original subjects: %s, "+
|
||||||
"new subject: %s",
|
"new subject: %s",
|
||||||
auth.subjectFromLogin, token.Subject)
|
auth.subjectsFromLogin, token.Subject)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
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")
|
||||||
|
}
|
||||||
@ -106,6 +106,8 @@ func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
|
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 {
|
if !options.sshMode {
|
||||||
cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
|
cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
|
||||||
@ -140,6 +142,7 @@ func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig,
|
|||||||
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
|
||||||
cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
|
cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
|
||||||
cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
|
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().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
|
||||||
cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
|
cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
|
||||||
}
|
}
|
||||||
@ -164,6 +167,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
|||||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
cmd.PersistentFlags().StringVarP(&c.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")
|
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +230,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R
|
|||||||
cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
|
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.BindPort, "bind_port", "p", 7000, "bind port")
|
||||||
cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp 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().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.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
|
||||||
cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
|
cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
|
||||||
|
|||||||
@ -170,7 +170,7 @@ type ClientCommonConf struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Supported sources including: string(file path), []byte, Reader interface.
|
// Supported sources including: string(file path), []byte, Reader interface.
|
||||||
func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) {
|
||||||
f, err := ini.LoadSources(ini.LoadOptions{
|
f, err := ini.LoadSources(ini.LoadOptions{
|
||||||
Insensitive: false,
|
Insensitive: false,
|
||||||
InsensitiveSections: false,
|
InsensitiveSections: false,
|
||||||
@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
|
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
|
||||||
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
|
common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
|
||||||
|
|
||||||
return common, nil
|
return common, nil
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
|
|||||||
// otherwise just start proxies in startProxy map
|
// otherwise just start proxies in startProxy map
|
||||||
func LoadAllProxyConfsFromIni(
|
func LoadAllProxyConfsFromIni(
|
||||||
prefix string,
|
prefix string,
|
||||||
source interface{},
|
source any,
|
||||||
start []string,
|
start []string,
|
||||||
) (map[string]ProxyConf, map[string]VisitorConf, error) {
|
) (map[string]ProxyConf, map[string]VisitorConf, error) {
|
||||||
f, err := ini.LoadSources(ini.LoadOptions{
|
f, err := ini.LoadSources(ini.LoadOptions{
|
||||||
@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni(
|
|||||||
startProxy[s] = struct{}{}
|
startProxy[s] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
startAll := true
|
startAll := len(startProxy) == 0
|
||||||
if len(startProxy) > 0 {
|
|
||||||
startAll = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build template sections from range section And append to ini.File.
|
// Build template sections from range section And append to ini.File.
|
||||||
rangeSections := make([]*ini.Section, 0)
|
rangeSections := make([]*ini.Section, 0)
|
||||||
@ -345,35 +342,19 @@ func copySection(source, target *ini.Section) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultClientConf returns a client configuration with default values.
|
// GetDefaultClientConf returns a client configuration with default values.
|
||||||
|
// Note: Some default values here will be set to empty and will be converted to them
|
||||||
|
// new configuration through the 'Complete' function to set them as the default
|
||||||
|
// values of the new configuration.
|
||||||
func GetDefaultClientConf() ClientCommonConf {
|
func GetDefaultClientConf() ClientCommonConf {
|
||||||
return ClientCommonConf{
|
return ClientCommonConf{
|
||||||
ClientConfig: legacyauth.GetDefaultClientConf(),
|
ClientConfig: legacyauth.GetDefaultClientConf(),
|
||||||
ServerAddr: "0.0.0.0",
|
|
||||||
ServerPort: 7000,
|
|
||||||
NatHoleSTUNServer: "stun.easyvoip.com:3478",
|
|
||||||
DialServerTimeout: 10,
|
|
||||||
DialServerKeepAlive: 7200,
|
|
||||||
HTTPProxy: os.Getenv("http_proxy"),
|
|
||||||
LogFile: "console",
|
|
||||||
LogWay: "console",
|
|
||||||
LogLevel: "info",
|
|
||||||
LogMaxDays: 3,
|
|
||||||
AdminAddr: "127.0.0.1",
|
|
||||||
PoolCount: 1,
|
|
||||||
TCPMux: true,
|
TCPMux: true,
|
||||||
TCPMuxKeepaliveInterval: 60,
|
|
||||||
LoginFailExit: true,
|
LoginFailExit: true,
|
||||||
Start: make([]string, 0),
|
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
QUICKeepalivePeriod: 10,
|
Start: make([]string, 0),
|
||||||
QUICMaxIdleTimeout: 30,
|
|
||||||
QUICMaxIncomingStreams: 100000,
|
|
||||||
TLSEnable: true,
|
TLSEnable: true,
|
||||||
DisableCustomTLSFirstByte: true,
|
DisableCustomTLSFirstByte: true,
|
||||||
HeartbeatInterval: 30,
|
|
||||||
HeartbeatTimeout: 90,
|
|
||||||
Metas: make(map[string]string),
|
Metas: make(map[string]string),
|
||||||
UDPPacketSize: 1500,
|
|
||||||
IncludeConfigFiles: make([]string, 0),
|
IncludeConfigFiles: make([]string, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,20 +26,20 @@ import (
|
|||||||
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
|
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
|
||||||
out := &v1.ClientCommonConfig{}
|
out := &v1.ClientCommonConfig{}
|
||||||
out.User = conf.User
|
out.User = conf.User
|
||||||
out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod)
|
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
|
||||||
out.Auth.Token = conf.ClientConfig.Token
|
out.Auth.Token = conf.Token
|
||||||
if conf.ClientConfig.AuthenticateHeartBeats {
|
if conf.AuthenticateHeartBeats {
|
||||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
||||||
}
|
}
|
||||||
if conf.ClientConfig.AuthenticateNewWorkConns {
|
if conf.AuthenticateNewWorkConns {
|
||||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
||||||
}
|
}
|
||||||
out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
|
out.Auth.OIDC.ClientID = conf.OidcClientID
|
||||||
out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
|
out.Auth.OIDC.ClientSecret = conf.OidcClientSecret
|
||||||
out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience
|
out.Auth.OIDC.Audience = conf.OidcAudience
|
||||||
out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope
|
out.Auth.OIDC.Scope = conf.OidcScope
|
||||||
out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL
|
out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL
|
||||||
out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams
|
out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams
|
||||||
|
|
||||||
out.ServerAddr = conf.ServerAddr
|
out.ServerAddr = conf.ServerAddr
|
||||||
out.ServerPort = conf.ServerPort
|
out.ServerPort = conf.ServerPort
|
||||||
@ -59,10 +59,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
|||||||
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
|
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
|
||||||
out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
|
out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
|
||||||
out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
|
out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
|
||||||
out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile
|
out.Transport.TLS.CertFile = conf.TLSCertFile
|
||||||
out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile
|
out.Transport.TLS.KeyFile = conf.TLSKeyFile
|
||||||
out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile
|
out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
|
||||||
out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName
|
out.Transport.TLS.ServerName = conf.TLSServerName
|
||||||
|
|
||||||
out.Log.To = conf.LogFile
|
out.Log.To = conf.LogFile
|
||||||
out.Log.Level = conf.LogLevel
|
out.Log.Level = conf.LogLevel
|
||||||
@ -87,18 +87,18 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
|||||||
|
|
||||||
func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
||||||
out := &v1.ServerConfig{}
|
out := &v1.ServerConfig{}
|
||||||
out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod)
|
out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
|
||||||
out.Auth.Token = conf.ServerConfig.Token
|
out.Auth.Token = conf.Token
|
||||||
if conf.ServerConfig.AuthenticateHeartBeats {
|
if conf.AuthenticateHeartBeats {
|
||||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
|
||||||
}
|
}
|
||||||
if conf.ServerConfig.AuthenticateNewWorkConns {
|
if conf.AuthenticateNewWorkConns {
|
||||||
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
|
||||||
}
|
}
|
||||||
out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
|
out.Auth.OIDC.Audience = conf.OidcAudience
|
||||||
out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
|
out.Auth.OIDC.Issuer = conf.OidcIssuer
|
||||||
out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck
|
out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck
|
||||||
out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck
|
out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck
|
||||||
|
|
||||||
out.BindAddr = conf.BindAddr
|
out.BindAddr = conf.BindAddr
|
||||||
out.BindPort = conf.BindPort
|
out.BindPort = conf.BindPort
|
||||||
|
|||||||
@ -206,7 +206,7 @@ func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// plugin_xxx
|
// plugin_xxx
|
||||||
cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
|
cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -200,38 +200,24 @@ type ServerCommonConf struct {
|
|||||||
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
|
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultServerConf returns a server configuration with reasonable
|
// GetDefaultServerConf returns a server configuration with reasonable defaults.
|
||||||
// defaults.
|
// Note: Some default values here will be set to empty and will be converted to them
|
||||||
|
// new configuration through the 'Complete' function to set them as the default
|
||||||
|
// values of the new configuration.
|
||||||
func GetDefaultServerConf() ServerCommonConf {
|
func GetDefaultServerConf() ServerCommonConf {
|
||||||
return ServerCommonConf{
|
return ServerCommonConf{
|
||||||
ServerConfig: legacyauth.GetDefaultServerConf(),
|
ServerConfig: legacyauth.GetDefaultServerConf(),
|
||||||
BindAddr: "0.0.0.0",
|
DashboardAddr: "0.0.0.0",
|
||||||
BindPort: 7000,
|
LogFile: "console",
|
||||||
QUICKeepalivePeriod: 10,
|
LogWay: "console",
|
||||||
QUICMaxIdleTimeout: 30,
|
DetailedErrorsToClient: true,
|
||||||
QUICMaxIncomingStreams: 100000,
|
TCPMux: true,
|
||||||
VhostHTTPTimeout: 60,
|
AllowPorts: make(map[int]struct{}),
|
||||||
DashboardAddr: "0.0.0.0",
|
HTTPPlugins: make(map[string]HTTPPluginOptions),
|
||||||
LogFile: "console",
|
|
||||||
LogWay: "console",
|
|
||||||
LogLevel: "info",
|
|
||||||
LogMaxDays: 3,
|
|
||||||
DetailedErrorsToClient: true,
|
|
||||||
TCPMux: true,
|
|
||||||
TCPMuxKeepaliveInterval: 60,
|
|
||||||
TCPKeepAlive: 7200,
|
|
||||||
AllowPorts: make(map[int]struct{}),
|
|
||||||
MaxPoolCount: 5,
|
|
||||||
MaxPortsPerClient: 0,
|
|
||||||
HeartbeatTimeout: 90,
|
|
||||||
UserConnTimeout: 10,
|
|
||||||
HTTPPlugins: make(map[string]HTTPPluginOptions),
|
|
||||||
UDPPacketSize: 1500,
|
|
||||||
NatHoleAnalysisDataReserveHours: 7 * 24,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
|
func UnmarshalServerConfFromIni(source any) (ServerCommonConf, error) {
|
||||||
f, err := ini.LoadSources(ini.LoadOptions{
|
f, err := ini.LoadSources(ini.LoadOptions{
|
||||||
Insensitive: false,
|
Insensitive: false,
|
||||||
InsensitiveSections: false,
|
InsensitiveSections: false,
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
toml "github.com/pelletier/go-toml/v2"
|
toml "github.com/pelletier/go-toml/v2"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
|
|||||||
return LoadConfigure(content, c, strict)
|
return LoadConfigure(content, c, strict)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
||||||
|
// This function handles both cases efficiently: with or without dot fields
|
||||||
|
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
||||||
|
var temp any
|
||||||
|
if err := yaml.Unmarshal(content, &temp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dot fields if it's a map
|
||||||
|
if tempMap, ok := temp.(map[string]any); ok {
|
||||||
|
for key := range tempMap {
|
||||||
|
if strings.HasPrefix(key, ".") {
|
||||||
|
delete(tempMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to JSON and decode with strict validation
|
||||||
|
jsonBytes, err := json.Marshal(temp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
return decoder.Decode(target)
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
||||||
// Now it supports json, yaml and toml format.
|
// Now it supports json, yaml and toml format.
|
||||||
func LoadConfigure(b []byte, c any, strict bool) error {
|
func LoadConfigure(b []byte, c any, strict bool) error {
|
||||||
@ -118,7 +145,7 @@ func LoadConfigure(b []byte, c any, strict bool) error {
|
|||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
defer v1.DisallowUnknownFieldsMu.Unlock()
|
||||||
v1.DisallowUnknownFields = strict
|
v1.DisallowUnknownFields = strict
|
||||||
|
|
||||||
var tomlObj interface{}
|
var tomlObj any
|
||||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
||||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||||
b, err = json.Marshal(&tomlObj)
|
b, err = json.Marshal(&tomlObj)
|
||||||
@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error {
|
|||||||
}
|
}
|
||||||
return decoder.Decode(c)
|
return decoder.Decode(c)
|
||||||
}
|
}
|
||||||
// It wasn't JSON. Unmarshal as YAML.
|
|
||||||
|
// Handle YAML content
|
||||||
if strict {
|
if strict {
|
||||||
return yaml.UnmarshalStrict(b, c)
|
// In strict mode, always use our custom handler to support YAML merge
|
||||||
|
return parseYAMLWithDotFieldsHandling(b, c)
|
||||||
}
|
}
|
||||||
|
// Non-strict mode, parse normally
|
||||||
return yaml.Unmarshal(b, c)
|
return yaml.Unmarshal(b, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if svrCfg != nil {
|
if svrCfg != nil {
|
||||||
svrCfg.Complete()
|
if err := svrCfg.Complete(); err != nil {
|
||||||
|
return nil, isLegacyFormat, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return svrCfg, isLegacyFormat, nil
|
return svrCfg, isLegacyFormat, nil
|
||||||
}
|
}
|
||||||
@ -249,8 +281,21 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
|
||||||
if cliCfg != nil {
|
if cliCfg != nil {
|
||||||
cliCfg.Complete()
|
if err := cliCfg.Complete(); err != nil {
|
||||||
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, c := range proxyCfgs {
|
for _, c := range proxyCfgs {
|
||||||
c.Complete(cliCfg.User)
|
c.Complete(cliCfg.User)
|
||||||
|
|||||||
@ -112,6 +112,29 @@ func TestLoadServerConfigStrictMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderWithTemplate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"toml", tomlServerContent, tomlServerContent},
|
||||||
|
{"yaml", yamlServerContent, yamlServerContent},
|
||||||
|
{"json", jsonServerContent, jsonServerContent},
|
||||||
|
{"template numeric", `key = {{ 123 }}`, "key = 123"},
|
||||||
|
{"template string", `key = {{ "xyz" }}`, "key = xyz"},
|
||||||
|
{"template quote", `key = {{ printf "%q" "with space" }}`, `key = "with space"`},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
got, err := RenderWithTemplate([]byte(test.content), nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.EqualValues(test.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCustomStructStrictMode(t *testing.T) {
|
func TestCustomStructStrictMode(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
@ -164,3 +187,122 @@ unixPath = "/tmp/uds.sock"
|
|||||||
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
|
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
||||||
|
// even in strict mode by properly handling dot-prefixed fields
|
||||||
|
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
yamlContent := `
|
||||||
|
serverAddr: "127.0.0.1"
|
||||||
|
serverPort: 7000
|
||||||
|
|
||||||
|
.common: &common
|
||||||
|
type: stcp
|
||||||
|
secretKey: "test-secret"
|
||||||
|
localIP: 127.0.0.1
|
||||||
|
transport:
|
||||||
|
useEncryption: true
|
||||||
|
useCompression: true
|
||||||
|
|
||||||
|
proxies:
|
||||||
|
- name: ssh
|
||||||
|
localPort: 22
|
||||||
|
<<: *common
|
||||||
|
- name: web
|
||||||
|
localPort: 80
|
||||||
|
<<: *common
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
// This should work in strict mode
|
||||||
|
err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Verify the merge worked correctly
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
|
require.Len(clientCfg.Proxies, 2)
|
||||||
|
|
||||||
|
// Check first proxy
|
||||||
|
sshProxy := clientCfg.Proxies[0].ProxyConfigurer
|
||||||
|
require.Equal("ssh", sshProxy.GetBaseConfig().Name)
|
||||||
|
require.Equal("stcp", sshProxy.GetBaseConfig().Type)
|
||||||
|
|
||||||
|
// Check second proxy
|
||||||
|
webProxy := clientCfg.Proxies[1].ProxyConfigurer
|
||||||
|
require.Equal("web", webProxy.GetBaseConfig().Name)
|
||||||
|
require.Equal("stcp", webProxy.GetBaseConfig().Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
|
||||||
|
func TestOptimizedYAMLProcessing(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
yamlWithDotFields := []byte(`
|
||||||
|
serverAddr: "127.0.0.1"
|
||||||
|
.common: &common
|
||||||
|
type: stcp
|
||||||
|
proxies:
|
||||||
|
- name: test
|
||||||
|
<<: *common
|
||||||
|
`)
|
||||||
|
|
||||||
|
yamlWithoutDotFields := []byte(`
|
||||||
|
serverAddr: "127.0.0.1"
|
||||||
|
proxies:
|
||||||
|
- name: test
|
||||||
|
type: tcp
|
||||||
|
localPort: 22
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Test that YAML without dot fields works in strict mode
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Len(clientCfg.Proxies, 1)
|
||||||
|
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
|
||||||
|
|
||||||
|
// Test that YAML with dot fields still works in strict mode
|
||||||
|
err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Len(clientCfg.Proxies, 1)
|
||||||
|
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
|
||||||
|
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
||||||
|
func TestYAMLEdgeCases(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// Test array at root (should fail for frp config)
|
||||||
|
arrayYAML := []byte(`
|
||||||
|
- item1
|
||||||
|
- item2
|
||||||
|
`)
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure(arrayYAML, &clientCfg, true)
|
||||||
|
require.Error(err) // Should fail because ClientConfig expects an object
|
||||||
|
|
||||||
|
// Test scalar at root (should fail for frp config)
|
||||||
|
scalarYAML := []byte(`"just a string"`)
|
||||||
|
err = LoadConfigure(scalarYAML, &clientCfg, true)
|
||||||
|
require.Error(err) // Should fail because ClientConfig expects an object
|
||||||
|
|
||||||
|
// Test empty object (should work)
|
||||||
|
emptyYAML := []byte(`{}`)
|
||||||
|
err = LoadConfigure(emptyYAML, &clientCfg, true)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Test nested structure without dots (should work)
|
||||||
|
nestedYAML := []byte(`
|
||||||
|
serverAddr: "127.0.0.1"
|
||||||
|
serverPort: 7000
|
||||||
|
`)
|
||||||
|
err = LoadConfigure(nestedYAML, &clientCfg, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
|
}
|
||||||
|
|||||||
@ -159,18 +159,18 @@ func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
|||||||
out = append(out, PortsRange{Single: int(singleNum)})
|
out = append(out, PortsRange{Single: int(singleNum)})
|
||||||
case 2:
|
case 2:
|
||||||
// range numbers
|
// range numbers
|
||||||
min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
|
minNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("range number is invalid, %v", err)
|
return nil, fmt.Errorf("range number is invalid, %v", err)
|
||||||
}
|
}
|
||||||
max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
|
maxNum, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("range number is invalid, %v", err)
|
return nil, fmt.Errorf("range number is invalid, %v", err)
|
||||||
}
|
}
|
||||||
if max < min {
|
if maxNum < minNum {
|
||||||
return nil, fmt.Errorf("range number is invalid")
|
return nil, fmt.Errorf("range number is invalid")
|
||||||
}
|
}
|
||||||
out = append(out, PortsRange{Start: int(min), End: int(max)})
|
out = append(out, PortsRange{Start: int(minNum), End: int(maxNum)})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("range number is invalid")
|
return nil, fmt.Errorf("range number is invalid")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,8 @@ type ClientCommonConfig struct {
|
|||||||
// clients. If this value is not "", proxy names will automatically be
|
// clients. If this value is not "", proxy names will automatically be
|
||||||
// changed to "{user}.{proxy_name}".
|
// changed to "{user}.{proxy_name}".
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
|
// ClientID uniquely identifies this frpc instance.
|
||||||
|
ClientID string `json:"clientID,omitempty"`
|
||||||
|
|
||||||
// ServerAddr specifies the address of the server to connect to. By
|
// ServerAddr specifies the address of the server to connect to. By
|
||||||
// default, this value is "0.0.0.0".
|
// default, this value is "0.0.0.0".
|
||||||
@ -58,9 +60,14 @@ type ClientCommonConfig struct {
|
|||||||
// set.
|
// set.
|
||||||
Start []string `json:"start,omitempty"`
|
Start []string `json:"start,omitempty"`
|
||||||
|
|
||||||
Log LogConfig `json:"log,omitempty"`
|
Log LogConfig `json:"log,omitempty"`
|
||||||
WebServer WebServerConfig `json:"webServer,omitempty"`
|
WebServer WebServerConfig `json:"webServer,omitempty"`
|
||||||
Transport ClientTransportConfig `json:"transport,omitempty"`
|
Transport ClientTransportConfig `json:"transport,omitempty"`
|
||||||
|
VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
|
||||||
|
|
||||||
|
// FeatureGates specifies a set of feature gates to enable or disable.
|
||||||
|
// This can be used to enable alpha/beta features or disable default features.
|
||||||
|
FeatureGates map[string]bool `json:"featureGates,omitempty"`
|
||||||
|
|
||||||
// UDPPacketSize specifies the udp packet size
|
// UDPPacketSize specifies the udp packet size
|
||||||
// By default, this value is 1500
|
// By default, this value is 1500
|
||||||
@ -72,18 +79,21 @@ type ClientCommonConfig struct {
|
|||||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
IncludeConfigFiles []string `json:"includes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientCommonConfig) Complete() {
|
func (c *ClientCommonConfig) Complete() error {
|
||||||
c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
|
c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
|
||||||
c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
|
c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
|
||||||
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
|
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
|
||||||
c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
|
c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
|
||||||
|
|
||||||
c.Auth.Complete()
|
if err := c.Auth.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.Log.Complete()
|
c.Log.Complete()
|
||||||
c.Transport.Complete()
|
c.Transport.Complete()
|
||||||
c.WebServer.Complete()
|
c.WebServer.Complete()
|
||||||
|
|
||||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientTransportConfig struct {
|
type ClientTransportConfig struct {
|
||||||
@ -135,7 +145,7 @@ func (c *ClientTransportConfig) Complete() {
|
|||||||
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
|
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
|
||||||
c.PoolCount = util.EmptyOr(c.PoolCount, 1)
|
c.PoolCount = util.EmptyOr(c.PoolCount, 1)
|
||||||
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
|
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
|
||||||
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
|
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
|
||||||
if lo.FromPtr(c.TCPMux) {
|
if lo.FromPtr(c.TCPMux) {
|
||||||
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
|
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
|
||||||
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
|
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
|
||||||
@ -179,12 +189,16 @@ type AuthClientConfig struct {
|
|||||||
// Token specifies the authorization token used to create keys to be sent
|
// Token specifies the authorization token used to create keys to be sent
|
||||||
// to the server. The server must have a matching token for authorization
|
// to the server. The server must have a matching token for authorization
|
||||||
// to succeed. By default, this value is "".
|
// to succeed. By default, this value is "".
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
|
// TokenSource specifies a dynamic source for the authorization token.
|
||||||
|
// This is mutually exclusive with Token field.
|
||||||
|
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||||
|
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AuthClientConfig) Complete() {
|
func (c *AuthClientConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthOIDCClientConfig struct {
|
type AuthOIDCClientConfig struct {
|
||||||
@ -203,4 +217,23 @@ type AuthOIDCClientConfig struct {
|
|||||||
// AdditionalEndpointParams specifies additional parameters to be sent
|
// AdditionalEndpointParams specifies additional parameters to be sent
|
||||||
// this field will be transfer to map[string][]string in OIDC token generator.
|
// this field will be transfer to map[string][]string in OIDC token generator.
|
||||||
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
|
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
|
||||||
|
|
||||||
|
// TrustedCaFile specifies the path to a custom CA certificate file
|
||||||
|
// for verifying the OIDC token endpoint's TLS certificate.
|
||||||
|
TrustedCaFile string `json:"trustedCaFile,omitempty"`
|
||||||
|
// InsecureSkipVerify disables TLS certificate verification for the
|
||||||
|
// OIDC token endpoint. Only use this for debugging, not recommended for production.
|
||||||
|
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||||
|
// ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint.
|
||||||
|
// Supports http, https, socks5, and socks5h proxy protocols.
|
||||||
|
// If empty, no proxy is used for OIDC connections.
|
||||||
|
ProxyURL string `json:"proxyURL,omitempty"`
|
||||||
|
|
||||||
|
// TokenSource specifies a custom dynamic source for the authorization token.
|
||||||
|
// This is mutually exclusive with every other field of this structure.
|
||||||
|
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualNetConfig struct {
|
||||||
|
Address string `json:"address,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ import (
|
|||||||
func TestClientConfigComplete(t *testing.T) {
|
func TestClientConfigComplete(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
c := &ClientConfig{}
|
c := &ClientConfig{}
|
||||||
c.Complete()
|
err := c.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
require.EqualValues("token", c.Auth.Method)
|
require.EqualValues("token", c.Auth.Method)
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
||||||
@ -33,3 +34,11 @@ func TestClientConfigComplete(t *testing.T) {
|
|||||||
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
|
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
|
||||||
require.NotEmpty(c.NatHoleSTUNServer)
|
require.NotEmpty(c.NatHoleSTUNServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthClientConfig_Complete(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
cfg := &AuthClientConfig{}
|
||||||
|
err := cfg.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
require.EqualValues("token", cfg.Method)
|
||||||
|
}
|
||||||
|
|||||||
@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TLSConfig struct {
|
type TLSConfig struct {
|
||||||
// CertPath specifies the path of the cert file that client will load.
|
// CertFile specifies the path of the cert file that client will load.
|
||||||
CertFile string `json:"certFile,omitempty"`
|
CertFile string `json:"certFile,omitempty"`
|
||||||
// KeyPath specifies the path of the secret key file that client will load.
|
// KeyFile specifies the path of the secret key file that client will load.
|
||||||
KeyFile string `json:"keyFile,omitempty"`
|
KeyFile string `json:"keyFile,omitempty"`
|
||||||
// TrustedCaFile specifies the path of the trusted ca file that will load.
|
// TrustedCaFile specifies the path of the trusted ca file that will load.
|
||||||
TrustedCaFile string `json:"trustedCaFile,omitempty"`
|
TrustedCaFile string `json:"trustedCaFile,omitempty"`
|
||||||
@ -96,6 +96,14 @@ type TLSConfig struct {
|
|||||||
ServerName string `json:"serverName,omitempty"`
|
ServerName string `json:"serverName,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NatTraversalConfig defines configuration options for NAT traversal
|
||||||
|
type NatTraversalConfig struct {
|
||||||
|
// DisableAssistedAddrs disables the use of local network interfaces
|
||||||
|
// for assisted connections during NAT traversal. When enabled,
|
||||||
|
// only STUN-discovered public addresses will be used.
|
||||||
|
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
// This is destination where frp should write the logs.
|
// This is destination where frp should write the logs.
|
||||||
// If "console" is used, logs will be printed to stdout, otherwise,
|
// If "console" is used, logs will be printed to stdout, otherwise,
|
||||||
|
|||||||
@ -108,8 +108,11 @@ type DomainConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
||||||
|
// This allows individual control over each proxy, complementing the global "start" field.
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
Transport ProxyTransport `json:"transport,omitempty"`
|
Transport ProxyTransport `json:"transport,omitempty"`
|
||||||
// metadata info for each proxy
|
// metadata info for each proxy
|
||||||
@ -127,6 +130,10 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
|
|||||||
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
|
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
|
||||||
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
|
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
|
||||||
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
|
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
|
||||||
|
|
||||||
|
if c.Plugin.ClientPluginOptions != nil {
|
||||||
|
c.Plugin.Complete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
|
func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
|
||||||
@ -195,6 +202,10 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c.ProxyConfigurer)
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyConfigurer interface {
|
type ProxyConfigurer interface {
|
||||||
Complete(namePrefix string)
|
Complete(namePrefix string)
|
||||||
GetBaseConfig() *ProxyBaseConfig
|
GetBaseConfig() *ProxyBaseConfig
|
||||||
@ -414,6 +425,9 @@ type XTCPProxyConfig struct {
|
|||||||
|
|
||||||
Secretkey string `json:"secretKey,omitempty"`
|
Secretkey string `json:"secretKey,omitempty"`
|
||||||
AllowUsers []string `json:"allowUsers,omitempty"`
|
AllowUsers []string `json:"allowUsers,omitempty"`
|
||||||
|
|
||||||
|
// NatTraversal configuration for NAT traversal
|
||||||
|
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
||||||
|
|||||||
@ -17,11 +17,44 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientPluginOptions interface{}
|
const (
|
||||||
|
PluginHTTP2HTTPS = "http2https"
|
||||||
|
PluginHTTPProxy = "http_proxy"
|
||||||
|
PluginHTTPS2HTTP = "https2http"
|
||||||
|
PluginHTTPS2HTTPS = "https2https"
|
||||||
|
PluginHTTP2HTTP = "http2http"
|
||||||
|
PluginSocks5 = "socks5"
|
||||||
|
PluginStaticFile = "static_file"
|
||||||
|
PluginUnixDomainSocket = "unix_domain_socket"
|
||||||
|
PluginTLS2Raw = "tls2raw"
|
||||||
|
PluginVirtualNet = "virtual_net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
|
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||||
|
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||||
|
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||||
|
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||||
|
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||||
|
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||||
|
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||||
|
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||||
|
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||||
|
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientPluginOptions interface {
|
||||||
|
Complete()
|
||||||
|
}
|
||||||
|
|
||||||
type TypedClientPluginOptions struct {
|
type TypedClientPluginOptions struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -42,7 +75,7 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
|||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = typeStruct.Type
|
||||||
if c.Type == "" {
|
if c.Type == "" {
|
||||||
return nil
|
return errors.New("plugin type is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
||||||
@ -63,24 +96,8 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
PluginHTTP2HTTPS = "http2https"
|
return json.Marshal(c.ClientPluginOptions)
|
||||||
PluginHTTPProxy = "http_proxy"
|
|
||||||
PluginHTTPS2HTTP = "https2http"
|
|
||||||
PluginHTTPS2HTTPS = "https2https"
|
|
||||||
PluginSocks5 = "socks5"
|
|
||||||
PluginStaticFile = "static_file"
|
|
||||||
PluginUnixDomainSocket = "unix_domain_socket"
|
|
||||||
)
|
|
||||||
|
|
||||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
|
||||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
|
||||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
|
||||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
|
||||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
|
||||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
|
||||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
|
||||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2HTTPSPluginOptions struct {
|
type HTTP2HTTPSPluginOptions struct {
|
||||||
@ -90,36 +107,61 @@ type HTTP2HTTPSPluginOptions struct {
|
|||||||
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
||||||
|
|
||||||
type HTTPProxyPluginOptions struct {
|
type HTTPProxyPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
HTTPUser string `json:"httpUser,omitempty"`
|
HTTPUser string `json:"httpUser,omitempty"`
|
||||||
HTTPPassword string `json:"httpPassword,omitempty"`
|
HTTPPassword string `json:"httpPassword,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTPProxyPluginOptions) Complete() {}
|
||||||
|
|
||||||
type HTTPS2HTTPPluginOptions struct {
|
type HTTPS2HTTPPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
||||||
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
||||||
|
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||||
CrtPath string `json:"crtPath,omitempty"`
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
KeyPath string `json:"keyPath,omitempty"`
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTPS2HTTPPluginOptions) Complete() {
|
||||||
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPS2HTTPSPluginOptions struct {
|
type HTTPS2HTTPSPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
||||||
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
||||||
|
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||||
CrtPath string `json:"crtPath,omitempty"`
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
KeyPath string `json:"keyPath,omitempty"`
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
||||||
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP2HTTPPluginOptions struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
|
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
||||||
|
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *HTTP2HTTPPluginOptions) Complete() {}
|
||||||
|
|
||||||
type Socks5PluginOptions struct {
|
type Socks5PluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Socks5PluginOptions) Complete() {}
|
||||||
|
|
||||||
type StaticFilePluginOptions struct {
|
type StaticFilePluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalPath string `json:"localPath,omitempty"`
|
LocalPath string `json:"localPath,omitempty"`
|
||||||
@ -128,7 +170,26 @@ type StaticFilePluginOptions struct {
|
|||||||
HTTPPassword string `json:"httpPassword,omitempty"`
|
HTTPPassword string `json:"httpPassword,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *StaticFilePluginOptions) Complete() {}
|
||||||
|
|
||||||
type UnixDomainSocketPluginOptions struct {
|
type UnixDomainSocketPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
UnixPath string `json:"unixPath,omitempty"`
|
UnixPath string `json:"unixPath,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
type TLS2RawPluginOptions struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TLS2RawPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
type VirtualNetPluginOptions struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *VirtualNetPluginOptions) Complete() {}
|
||||||
@ -98,8 +98,10 @@ type ServerConfig struct {
|
|||||||
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
|
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ServerConfig) Complete() {
|
func (c *ServerConfig) Complete() error {
|
||||||
c.Auth.Complete()
|
if err := c.Auth.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.Log.Complete()
|
c.Log.Complete()
|
||||||
c.Transport.Complete()
|
c.Transport.Complete()
|
||||||
c.WebServer.Complete()
|
c.WebServer.Complete()
|
||||||
@ -120,17 +122,20 @@ func (c *ServerConfig) Complete() {
|
|||||||
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
|
||||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
||||||
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
|
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthServerConfig struct {
|
type AuthServerConfig struct {
|
||||||
Method AuthMethod `json:"method,omitempty"`
|
Method AuthMethod `json:"method,omitempty"`
|
||||||
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
|
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
|
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||||
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
|
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AuthServerConfig) Complete() {
|
func (c *AuthServerConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthOIDCServerConfig struct {
|
type AuthOIDCServerConfig struct {
|
||||||
@ -176,7 +181,7 @@ type ServerTransportConfig struct {
|
|||||||
|
|
||||||
func (c *ServerTransportConfig) Complete() {
|
func (c *ServerTransportConfig) Complete() {
|
||||||
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
|
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
|
||||||
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
|
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
|
||||||
c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
|
c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
|
||||||
c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
|
c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
|
||||||
if lo.FromPtr(c.TCPMux) {
|
if lo.FromPtr(c.TCPMux) {
|
||||||
|
|||||||
@ -24,9 +24,18 @@ import (
|
|||||||
func TestServerConfigComplete(t *testing.T) {
|
func TestServerConfigComplete(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
c := &ServerConfig{}
|
c := &ServerConfig{}
|
||||||
c.Complete()
|
err := c.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
require.EqualValues("token", c.Auth.Method)
|
require.EqualValues("token", c.Auth.Method)
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
|
||||||
require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
|
require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthServerConfig_Complete(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
cfg := &AuthServerConfig{}
|
||||||
|
err := cfg.Complete()
|
||||||
|
require.NoError(err)
|
||||||
|
require.EqualValues("token", cfg.Method)
|
||||||
|
}
|
||||||
|
|||||||
@ -23,35 +23,109 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
)
|
)
|
||||||
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
|
|
||||||
|
validators := []func() (Warning, error){
|
||||||
|
func() (Warning, error) { return validateFeatureGates(c) },
|
||||||
|
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
||||||
|
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||||
|
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||||
|
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||||
|
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, validator := range validators {
|
||||||
|
w, err := validator()
|
||||||
|
warnings = AppendError(warnings, w)
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
return warnings, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
||||||
|
if c.VirtualNet.Address != "" {
|
||||||
|
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||||
|
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
|
||||||
|
var errs error
|
||||||
|
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||||
}
|
}
|
||||||
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
|
if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateLogConfig(&c.Log); err != nil {
|
// Validate token/tokenSource mutual exclusivity
|
||||||
errs = AppendError(errs, err)
|
if c.Token != "" && c.TokenSource != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
// Validate tokenSource if specified
|
||||||
errs = AppendError(errs, err)
|
if c.TokenSource != nil {
|
||||||
|
if c.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
|
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||||
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
||||||
|
if c.TokenSource == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var errs error
|
||||||
|
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
|
||||||
|
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
|
||||||
|
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
|
||||||
|
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
||||||
|
}
|
||||||
|
if c.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
||||||
|
var (
|
||||||
|
warnings Warning
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
|
||||||
|
if c.HeartbeatTimeout < c.HeartbeatInterval {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lo.FromPtr(c.Transport.TLS.Enable) {
|
if !lo.FromPtr(c.TLS.Enable) {
|
||||||
checkTLSConfig := func(name string, value string) Warning {
|
checkTLSConfig := func(name string, value string) Warning {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
||||||
@ -59,16 +133,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
|
if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||||
}
|
}
|
||||||
|
return warnings, errs
|
||||||
|
}
|
||||||
|
|
||||||
for _, f := range c.IncludeConfigFiles {
|
func validateIncludeFiles(files []string) (Warning, error) {
|
||||||
|
var errs error
|
||||||
|
for _, f := range files {
|
||||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
||||||
@ -78,13 +156,19 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
|||||||
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return warnings, errs
|
return nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
|
func ValidateAllClientConfig(
|
||||||
|
c *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
|
) (Warning, error) {
|
||||||
|
validator := NewConfigValidator(unsafeFeatures)
|
||||||
var warnings Warning
|
var warnings Warning
|
||||||
if c != nil {
|
if c != nil {
|
||||||
warning, err := ValidateClientCommonConfig(c)
|
warning, err := validator.ValidateClientCommonConfig(c)
|
||||||
warnings = AppendError(warnings, warning)
|
warnings = AppendError(warnings, warning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return warnings, err
|
return warnings, err
|
||||||
|
|||||||
@ -32,6 +32,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
|
|||||||
return validateStaticFilePluginOptions(v)
|
return validateStaticFilePluginOptions(v)
|
||||||
case *v1.UnixDomainSocketPluginOptions:
|
case *v1.UnixDomainSocketPluginOptions:
|
||||||
return validateUnixDomainSocketPluginOptions(v)
|
return validateUnixDomainSocketPluginOptions(v)
|
||||||
|
case *v1.TLS2RawPluginOptions:
|
||||||
|
return validateTLS2RawPluginOptions(v)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -70,3 +72,10 @@ func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error {
|
||||||
|
if c.LocalAddr == "" {
|
||||||
|
return errors.New("localAddr is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -21,9 +21,10 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
@ -35,6 +36,23 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
|||||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate token/tokenSource mutual exclusivity
|
||||||
|
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tokenSource if specified
|
||||||
|
if c.Auth.TokenSource != nil {
|
||||||
|
if c.Auth.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateLogConfig(&c.Log); err != nil {
|
if err := validateLogConfig(&c.Log); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
|
|||||||
28
pkg/config/v1/validation/validator.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigValidator holds the context dependencies for configuration validation.
|
||||||
|
type ConfigValidator struct {
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigValidator creates a new ConfigValidator instance.
|
||||||
|
func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator {
|
||||||
|
return &ConfigValidator{
|
||||||
|
unsafeFeatures: unsafeFeatures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUnsafeFeature checks if a specific unsafe feature is enabled.
|
||||||
|
func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error {
|
||||||
|
if !v.unsafeFeatures.IsEnabled(feature) {
|
||||||
|
return fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||||
|
"To enable it, ensure it is allowed in the configuration or command line flags", feature)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
158
pkg/config/v1/value_source.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// 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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValueSource provides a way to dynamically resolve configuration values
|
||||||
|
// from various sources like files, environment variables, or external services.
|
||||||
|
type ValueSource struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
File *FileSource `json:"file,omitempty"`
|
||||||
|
Exec *ExecSource `json:"exec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSource specifies how to load a value from a file.
|
||||||
|
type FileSource struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecSource specifies how to get a value from another program launched as subprocess.
|
||||||
|
type ExecSource struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
Env []ExecEnvVar `json:"env,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecEnvVar struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the ValueSource configuration.
|
||||||
|
func (v *ValueSource) Validate() error {
|
||||||
|
if v == nil {
|
||||||
|
return errors.New("valueSource cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Type {
|
||||||
|
case "file":
|
||||||
|
if v.File == nil {
|
||||||
|
return errors.New("file configuration is required when type is 'file'")
|
||||||
|
}
|
||||||
|
return v.File.Validate()
|
||||||
|
case "exec":
|
||||||
|
if v.Exec == nil {
|
||||||
|
return errors.New("exec configuration is required when type is 'exec'")
|
||||||
|
}
|
||||||
|
return v.Exec.Validate()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve resolves the value from the configured source.
|
||||||
|
func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Type {
|
||||||
|
case "file":
|
||||||
|
return v.File.Resolve(ctx)
|
||||||
|
case "exec":
|
||||||
|
return v.Exec.Resolve(ctx)
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the FileSource configuration.
|
||||||
|
func (f *FileSource) Validate() error {
|
||||||
|
if f == nil {
|
||||||
|
return errors.New("fileSource cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Path == "" {
|
||||||
|
return errors.New("file path cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve reads and returns the content from the specified file.
|
||||||
|
func (f *FileSource) Resolve(_ context.Context) (string, error) {
|
||||||
|
if err := f.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(f.Path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read file %s: %v", f.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace, which is important for file-based tokens
|
||||||
|
return strings.TrimSpace(string(content)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the ExecSource configuration.
|
||||||
|
func (e *ExecSource) Validate() error {
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("execSource cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == "" {
|
||||||
|
return errors.New("exec command cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range e.Env {
|
||||||
|
if env.Name == "" {
|
||||||
|
return errors.New("exec env name cannot be empty")
|
||||||
|
}
|
||||||
|
if strings.Contains(env.Name, "=") {
|
||||||
|
return errors.New("exec env name cannot contain '='")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve reads and returns the content captured from stdout of launched subprocess.
|
||||||
|
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
|
||||||
|
if err := e.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
|
||||||
|
if len(e.Env) != 0 {
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
for _, env := range e.Env {
|
||||||
|
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace, which is important for exec-based tokens
|
||||||
|
return strings.TrimSpace(string(content)), nil
|
||||||
|
}
|
||||||
246
pkg/config/v1/value_source_test.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
// 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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValueSource_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
vs *ValueSource
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil valueSource",
|
||||||
|
vs: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported type",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "unsupported",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file type without file config",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: nil,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid file type with absolute path",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "/tmp/test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid file type with relative path",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "configs/token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.vs.Validate()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSource_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fs *FileSource
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil fileSource",
|
||||||
|
fs: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty path",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative path (allowed)",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: "relative/path",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absolute path",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: "/absolute/path",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.fs.Validate()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSource_Resolve(t *testing.T) {
|
||||||
|
// Create a temporary file for testing
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
|
testContent := "test-token-value\n\t "
|
||||||
|
expectedContent := "test-token-value"
|
||||||
|
|
||||||
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fs *FileSource
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid file path",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
want: expectedContent,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-existent file",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: "/non/existent/file",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path traversal attempt (should fail validation)",
|
||||||
|
fs: &FileSource{
|
||||||
|
Path: "../../../etc/passwd",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.fs.Resolve(context.Background())
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValueSource_Resolve(t *testing.T) {
|
||||||
|
// Create a temporary file for testing
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
|
testContent := "test-token-value"
|
||||||
|
|
||||||
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
vs *ValueSource
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid file type",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: testContent,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported type",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "unsupported",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file type with path traversal",
|
||||||
|
vs: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "../../../etc/passwd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.vs.Resolve(ctx)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,8 +32,11 @@ type VisitorTransport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VisitorBaseConfig struct {
|
type VisitorBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
||||||
|
// This allows individual control over each visitor, complementing the global "start" field.
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Transport VisitorTransport `json:"transport,omitempty"`
|
Transport VisitorTransport `json:"transport,omitempty"`
|
||||||
SecretKey string `json:"secretKey,omitempty"`
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
// if the server user is not set, it defaults to the current user
|
// if the server user is not set, it defaults to the current user
|
||||||
@ -44,6 +47,9 @@ type VisitorBaseConfig struct {
|
|||||||
// It can be less than 0, it means don't bind to the port and only receive connections redirected from
|
// It can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||||
// other visitors. (This is not supported for SUDP now)
|
// other visitors. (This is not supported for SUDP now)
|
||||||
BindPort int `json:"bindPort,omitempty"`
|
BindPort int `json:"bindPort,omitempty"`
|
||||||
|
|
||||||
|
// Plugin specifies what plugin should be used.
|
||||||
|
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
||||||
@ -120,6 +126,10 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c.VisitorConfigurer)
|
||||||
|
}
|
||||||
|
|
||||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||||
v, ok := visitorConfigTypeMap[t]
|
v, ok := visitorConfigTypeMap[t]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -153,6 +163,9 @@ type XTCPVisitorConfig struct {
|
|||||||
MinRetryInterval int `json:"minRetryInterval,omitempty"`
|
MinRetryInterval int `json:"minRetryInterval,omitempty"`
|
||||||
FallbackTo string `json:"fallbackTo,omitempty"`
|
FallbackTo string `json:"fallbackTo,omitempty"`
|
||||||
FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"`
|
FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"`
|
||||||
|
|
||||||
|
// NatTraversal configuration for NAT traversal
|
||||||
|
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
|
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
|
||||||
|
|||||||
86
pkg/config/v1/visitor_plugin.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// 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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VisitorPluginVirtualNet = "virtual_net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
|
VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
type VisitorPluginOptions interface {
|
||||||
|
Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypedVisitorPluginOptions struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
VisitorPluginOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 4 && string(b) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStruct := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Type = typeStruct.Type
|
||||||
|
if c.Type == "" {
|
||||||
|
return errors.New("visitor plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := visitorPluginOptionsTypeMap[typeStruct.Type]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type)
|
||||||
|
}
|
||||||
|
options := reflect.New(v).Interface().(VisitorPluginOptions)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
if DisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(options); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
c.VisitorPluginOptions = options
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c.VisitorPluginOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualNetVisitorPluginOptions struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
DestinationIP string `json:"destinationIP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *VirtualNetVisitorPluginOptions) Complete() {}
|
||||||
@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
||||||
for _, v := range m.ms {
|
for _, v := range m.ms {
|
||||||
v.NewProxy(name, proxyType)
|
v.NewProxy(name, proxyType, user, clientID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.info.ClientCounts.Dec(1)
|
m.info.ClientCounts.Dec(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
||||||
@ -109,7 +109,7 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
|||||||
m.info.ProxyTypeCounts[proxyType] = counter
|
m.info.ProxyTypeCounts[proxyType] = counter
|
||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if !(ok && proxyStats.ProxyType == proxyType) {
|
if !ok || proxyStats.ProxyType != proxyType {
|
||||||
proxyStats = &ProxyStatistics{
|
proxyStats = &ProxyStatistics{
|
||||||
Name: name,
|
Name: name,
|
||||||
ProxyType: proxyType,
|
ProxyType: proxyType,
|
||||||
@ -119,6 +119,8 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
|||||||
}
|
}
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
|
proxyStats.User = user
|
||||||
|
proxyStats.ClientID = clientID
|
||||||
proxyStats.LastStartTime = time.Now()
|
proxyStats.LastStartTime = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +216,8 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
ps := &ProxyStats{
|
ps := &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
|
User: proxyStats.User,
|
||||||
|
ClientID: proxyStats.ClientID,
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@ -245,6 +249,8 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
res = &ProxyStats{
|
res = &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
|
User: proxyStats.User,
|
||||||
|
ClientID: proxyStats.ClientID,
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@ -260,6 +266,31 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
|
if ok {
|
||||||
|
res = &ProxyStats{
|
||||||
|
Name: proxyName,
|
||||||
|
Type: proxyStats.ProxyType,
|
||||||
|
User: proxyStats.User,
|
||||||
|
ClientID: proxyStats.ClientID,
|
||||||
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
|
}
|
||||||
|
if !proxyStats.LastStartTime.IsZero() {
|
||||||
|
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
if !proxyStats.LastCloseTime.IsZero() {
|
||||||
|
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|||||||
@ -35,6 +35,8 @@ type ServerStats struct {
|
|||||||
type ProxyStats struct {
|
type ProxyStats struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
|
User string
|
||||||
|
ClientID string
|
||||||
TodayTrafficIn int64
|
TodayTrafficIn int64
|
||||||
TodayTrafficOut int64
|
TodayTrafficOut int64
|
||||||
LastStartTime string
|
LastStartTime string
|
||||||
@ -51,6 +53,8 @@ type ProxyTrafficInfo struct {
|
|||||||
type ProxyStatistics struct {
|
type ProxyStatistics struct {
|
||||||
Name string
|
Name string
|
||||||
ProxyType string
|
ProxyType string
|
||||||
|
User string
|
||||||
|
ClientID string
|
||||||
TrafficIn metric.DateCounter
|
TrafficIn metric.DateCounter
|
||||||
TrafficOut metric.DateCounter
|
TrafficOut metric.DateCounter
|
||||||
CurConns metric.Counter
|
CurConns metric.Counter
|
||||||
@ -78,6 +82,7 @@ type Collector interface {
|
|||||||
GetServer() *ServerStats
|
GetServer() *ServerStats
|
||||||
GetProxiesByType(proxyType string) []*ProxyStats
|
GetProxiesByType(proxyType string) []*ProxyStats
|
||||||
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
||||||
|
GetProxyByName(proxyName string) *ProxyStats
|
||||||
GetProxyTraffic(name string) *ProxyTrafficInfo
|
GetProxyTraffic(name string) *ProxyTrafficInfo
|
||||||
ClearOfflineProxies() (int, int)
|
ClearOfflineProxies() (int, int)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,11 +14,12 @@ const (
|
|||||||
var ServerMetrics metrics.ServerMetrics = newServerMetrics()
|
var ServerMetrics metrics.ServerMetrics = newServerMetrics()
|
||||||
|
|
||||||
type serverMetrics struct {
|
type serverMetrics struct {
|
||||||
clientCount prometheus.Gauge
|
clientCount prometheus.Gauge
|
||||||
proxyCount *prometheus.GaugeVec
|
proxyCount *prometheus.GaugeVec
|
||||||
connectionCount *prometheus.GaugeVec
|
proxyCountDetailed *prometheus.GaugeVec
|
||||||
trafficIn *prometheus.CounterVec
|
connectionCount *prometheus.GaugeVec
|
||||||
trafficOut *prometheus.CounterVec
|
trafficIn *prometheus.CounterVec
|
||||||
|
trafficOut *prometheus.CounterVec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewClient() {
|
func (m *serverMetrics) NewClient() {
|
||||||
@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.clientCount.Dec()
|
m.clientCount.Dec()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(_ string, proxyType string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) {
|
||||||
m.proxyCount.WithLabelValues(proxyType).Inc()
|
m.proxyCount.WithLabelValues(proxyType).Inc()
|
||||||
|
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) CloseProxy(_ string, proxyType string) {
|
func (m *serverMetrics) CloseProxy(name string, proxyType string) {
|
||||||
m.proxyCount.WithLabelValues(proxyType).Dec()
|
m.proxyCount.WithLabelValues(proxyType).Dec()
|
||||||
|
m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) OpenConnection(name string, proxyType string) {
|
func (m *serverMetrics) OpenConnection(name string, proxyType string) {
|
||||||
@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics {
|
|||||||
Name: "proxy_counts",
|
Name: "proxy_counts",
|
||||||
Help: "The current proxy counts",
|
Help: "The current proxy counts",
|
||||||
}, []string{"type"}),
|
}, []string{"type"}),
|
||||||
|
proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: serverSubsystem,
|
||||||
|
Name: "proxy_counts_detailed",
|
||||||
|
Help: "The current number of proxies grouped by type and name",
|
||||||
|
}, []string{"type", "name"}),
|
||||||
connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Subsystem: serverSubsystem,
|
Subsystem: serverSubsystem,
|
||||||
@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics {
|
|||||||
}
|
}
|
||||||
prometheus.MustRegister(m.clientCount)
|
prometheus.MustRegister(m.clientCount)
|
||||||
prometheus.MustRegister(m.proxyCount)
|
prometheus.MustRegister(m.proxyCount)
|
||||||
|
prometheus.MustRegister(m.proxyCountDetailed)
|
||||||
prometheus.MustRegister(m.connectionCount)
|
prometheus.MustRegister(m.connectionCount)
|
||||||
prometheus.MustRegister(m.trafficIn)
|
prometheus.MustRegister(m.trafficIn)
|
||||||
prometheus.MustRegister(m.trafficOut)
|
prometheus.MustRegister(m.trafficOut)
|
||||||
|
|||||||
@ -39,6 +39,6 @@ func ReadMsgInto(c io.Reader, msg Message) (err error) {
|
|||||||
return msgCtl.ReadMsgInto(c, msg)
|
return msgCtl.ReadMsgInto(c, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteMsg(c io.Writer, msg interface{}) (err error) {
|
func WriteMsg(c io.Writer, msg any) (err error) {
|
||||||
return msgCtl.WriteMsg(c, msg)
|
return msgCtl.WriteMsg(c, msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dispatcher) SendChannel() chan Message {
|
|
||||||
return d.sendCh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
||||||
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const (
|
|||||||
TypeNatHoleReport = '6'
|
TypeNatHoleReport = '6'
|
||||||
)
|
)
|
||||||
|
|
||||||
var msgTypeMap = map[byte]interface{}{
|
var msgTypeMap = map[byte]any{
|
||||||
TypeLogin: Login{},
|
TypeLogin: Login{},
|
||||||
TypeLoginResp: LoginResp{},
|
TypeLoginResp: LoginResp{},
|
||||||
TypeNewProxy: NewProxy{},
|
TypeNewProxy: NewProxy{},
|
||||||
@ -82,6 +82,7 @@ type Login struct {
|
|||||||
PrivilegeKey string `json:"privilege_key,omitempty"`
|
PrivilegeKey string `json:"privilege_key,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
RunID string `json:"run_id,omitempty"`
|
RunID string `json:"run_id,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
Metas map[string]string `json:"metas,omitempty"`
|
Metas map[string]string `json:"metas,omitempty"`
|
||||||
|
|
||||||
// Currently only effective for VirtualClient.
|
// Currently only effective for VirtualClient.
|
||||||
|
|||||||