mirror of
https://github.com/fatedier/frp.git
synced 2026-04-27 03:19:11 +08:00
Compare commits
58 Commits
19fec05c12
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cef71fb949 | ||
|
|
410c4861c4 | ||
|
|
e9464919d1 | ||
|
|
e8dfd6efcc | ||
|
|
a9a4416ecf | ||
|
|
d667be7a0a | ||
|
|
31c3deb4f7 | ||
|
|
31e271939b | ||
|
|
061c141756 | ||
|
|
98ee1adb13 | ||
|
|
76abeff881 | ||
|
|
c694b1f6a9 | ||
|
|
5ed02275da | ||
|
|
60c4f5d4bd | ||
|
|
d20e384bf1 | ||
|
|
c95dc9d88a | ||
|
|
38a71a6803 | ||
|
|
6cdef90113 | ||
|
|
85e8e2c830 | ||
|
|
ff4ad2f907 | ||
|
|
94a631fe9c | ||
|
|
6b1be922e1 | ||
|
|
4f584f81d0 | ||
|
|
9669e1ca0c | ||
|
|
48e8901466 | ||
|
|
bcd2424c24 | ||
|
|
c7ac12ea0f | ||
|
|
eeb0dacfc1 | ||
|
|
535eb3db35 | ||
|
|
605f3bdece | ||
|
|
764a626b6e | ||
|
|
c2454e7114 | ||
|
|
017d71717f | ||
|
|
bd200b1a3b | ||
|
|
c70ceff370 | ||
|
|
bb3d0e7140 | ||
|
|
cf396563f8 | ||
|
|
0b4f83cd04 | ||
|
|
e9f7a1a9f2 | ||
|
|
d644593342 | ||
|
|
427c4ca3ae | ||
|
|
f2d1f3739a | ||
|
|
c23894f156 | ||
|
|
cb459b02b6 | ||
|
|
8f633fe363 | ||
|
|
c62a1da161 | ||
|
|
f22f7d539c | ||
|
|
462c987f6d | ||
|
|
541878af4d | ||
|
|
b7435967b0 | ||
|
|
774478d071 | ||
|
|
fbeb6ca43a | ||
|
|
381245a439 | ||
|
|
01997deb98 | ||
|
|
d0347325fc | ||
|
|
519368b1fd | ||
|
|
9634fd99d1 | ||
|
|
7a1c248b67 |
@@ -2,7 +2,7 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
go-version-latest:
|
go-version-latest:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:1.24-node
|
- image: cimg/go:1.25-node
|
||||||
resource_class: large
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|||||||
14
.github/workflows/build-and-push-image.yml
vendored
14
.github/workflows/build-and-push-image.yml
vendored
@@ -19,15 +19,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# environment
|
# environment
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
# get image tag name
|
# get image tag name
|
||||||
- name: Get Image Tag Name
|
- name: Get Image Tag Name
|
||||||
@@ -38,13 +38,13 @@ jobs:
|
|||||||
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to the GPR
|
- name: Login to the GPR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push frpc
|
- name: Build and push frpc
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frpc
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
${{ env.TAG_FRPC_GPR }}
|
${{ env.TAG_FRPC_GPR }}
|
||||||
|
|
||||||
- name: Build and push frps
|
- name: Build and push frps
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frps
|
file: ./dockerfiles/Dockerfile-for-frps
|
||||||
|
|||||||
16
.github/workflows/golangci-lint.yml
vendored
16
.github/workflows/golangci-lint.yml
vendored
@@ -14,22 +14,22 @@ jobs:
|
|||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.25'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
run: make install build
|
run: make build
|
||||||
working-directory: web/frps
|
working-directory: web/frps
|
||||||
- name: Build web assets (frpc)
|
- name: Build web assets (frpc)
|
||||||
run: make install build
|
run: make build
|
||||||
working-directory: web/frpc
|
working-directory: web/frpc
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v8
|
uses: golangci/golangci-lint-action@v9
|
||||||
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: v2.3
|
version: v2.11
|
||||||
|
|||||||
14
.github/workflows/goreleaser.yml
vendored
14
.github/workflows/goreleaser.yml
vendored
@@ -8,29 +8,29 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.25'
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
run: make install build
|
run: make build
|
||||||
working-directory: web/frps
|
working-directory: web/frps
|
||||||
- name: Build web assets (frpc)
|
- name: Build web assets (frpc)
|
||||||
run: make install build
|
run: make build
|
||||||
working-directory: web/frpc
|
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@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --release-notes=./Release.md
|
args: release --clean --release-notes=./Release.md
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
||||||
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -7,18 +7,6 @@
|
|||||||
_obj
|
_obj
|
||||||
_test
|
_test
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
@@ -37,8 +25,12 @@ dist/
|
|||||||
client.crt
|
client.crt
|
||||||
client.key
|
client.key
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
CLAUDE.md
|
.claude/
|
||||||
|
.sisyphus/
|
||||||
|
.superpowers/
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ linters:
|
|||||||
- lll
|
- lll
|
||||||
- makezero
|
- makezero
|
||||||
- misspell
|
- misspell
|
||||||
|
- modernize
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- revive
|
- revive
|
||||||
@@ -33,13 +34,7 @@ linters:
|
|||||||
disabled-checks:
|
disabled-checks:
|
||||||
- exitAfterDefer
|
- exitAfterDefer
|
||||||
gosec:
|
gosec:
|
||||||
excludes:
|
excludes: ["G115", "G117", "G118", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
||||||
- G401
|
|
||||||
- G402
|
|
||||||
- G404
|
|
||||||
- G501
|
|
||||||
- G115
|
|
||||||
- G204
|
|
||||||
severity: low
|
severity: low
|
||||||
confidence: low
|
confidence: low
|
||||||
govet:
|
govet:
|
||||||
@@ -53,6 +48,9 @@ linters:
|
|||||||
ignore-rules:
|
ignore-rules:
|
||||||
- cancelled
|
- cancelled
|
||||||
- marshalled
|
- marshalled
|
||||||
|
modernize:
|
||||||
|
disable:
|
||||||
|
- omitzero
|
||||||
unparam:
|
unparam:
|
||||||
check-exported: false
|
check-exported: false
|
||||||
exclusions:
|
exclusions:
|
||||||
@@ -77,6 +75,9 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- revive
|
- revive
|
||||||
text: "avoid meaningless package names"
|
text: "avoid meaningless package names"
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "Go standard library package names"
|
||||||
- linters:
|
- linters:
|
||||||
- unparam
|
- unparam
|
||||||
text: is always false
|
text: is always false
|
||||||
@@ -89,6 +90,7 @@ linters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gci
|
- gci
|
||||||
@@ -111,6 +113,7 @@ formatters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
|
|||||||
39
AGENTS.md
Normal file
39
AGENTS.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- `make build` - Build both frps and frpc binaries
|
||||||
|
- `make frps` - Build server binary only
|
||||||
|
- `make frpc` - Build client binary only
|
||||||
|
- `make all` - Build everything with formatting
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `make test` - Run unit tests
|
||||||
|
- `make e2e` - Run end-to-end tests
|
||||||
|
- `make e2e-trace` - Run e2e tests with trace logging
|
||||||
|
- `make alltest` - Run all tests including vet, unit tests, and e2e
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- `make fmt` - Run go fmt
|
||||||
|
- `make fmt-more` - Run gofumpt for more strict formatting
|
||||||
|
- `make gci` - Run gci import organizer
|
||||||
|
- `make vet` - Run go vet
|
||||||
|
- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml)
|
||||||
|
|
||||||
|
### Assets
|
||||||
|
- `make web` - Build web dashboards (frps and frpc)
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- `make clean` - Remove built binaries and temporary files
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- E2E tests using Ginkgo/Gomega framework
|
||||||
|
- Mock servers in `/test/e2e/mock/`
|
||||||
|
- Run: `make e2e` or `make alltest`
|
||||||
|
|
||||||
|
## Agent Runbooks
|
||||||
|
|
||||||
|
Operational procedures for agents are in `doc/agents/`:
|
||||||
|
- `doc/agents/release.md` - Release process
|
||||||
21
Makefile
21
Makefile
@@ -1,6 +1,7 @@
|
|||||||
export PATH := $(PATH):`go env GOPATH`/bin
|
export PATH := $(PATH):`go env GOPATH`/bin
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
||||||
|
|
||||||
.PHONY: web frps-web frpc-web frps frpc
|
.PHONY: web frps-web frpc-web frps frpc
|
||||||
|
|
||||||
@@ -28,23 +29,23 @@ 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: web
|
vet:
|
||||||
go vet ./...
|
go vet -tags "$(NOWEB_TAG)" ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps
|
||||||
|
|
||||||
frpc:
|
frpc:
|
||||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
|
||||||
|
|
||||||
test: gotest
|
test: gotest
|
||||||
|
|
||||||
gotest: web
|
gotest:
|
||||||
go test -v --cover ./assets/...
|
go test -tags "$(NOWEB_TAG)" -v --cover ./assets/...
|
||||||
go test -v --cover ./cmd/...
|
go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/...
|
||||||
go test -v --cover ./client/...
|
go test -tags "$(NOWEB_TAG)" -v --cover ./client/...
|
||||||
go test -v --cover ./server/...
|
go test -tags "$(NOWEB_TAG)" -v --cover ./server/...
|
||||||
go test -v --cover ./pkg/...
|
go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/...
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
./hack/run-e2e.sh
|
./hack/run-e2e.sh
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -13,6 +13,26 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
@@ -30,25 +50,6 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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?
|
||||||
@@ -80,6 +81,7 @@ frp also offers a P2P connect mode.
|
|||||||
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
||||||
* [Server Dashboard](#server-dashboard)
|
* [Server Dashboard](#server-dashboard)
|
||||||
* [Client Admin UI](#client-admin-ui)
|
* [Client Admin UI](#client-admin-ui)
|
||||||
|
* [Dynamic Proxy Management (Store)](#dynamic-proxy-management-store)
|
||||||
* [Monitor](#monitor)
|
* [Monitor](#monitor)
|
||||||
* [Prometheus](#prometheus)
|
* [Prometheus](#prometheus)
|
||||||
* [Authenticating the Client](#authenticating-the-client)
|
* [Authenticating the Client](#authenticating-the-client)
|
||||||
@@ -148,7 +150,9 @@ We sincerely appreciate your support for frp.
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<img src="/doc/pic/architecture.jpg" alt="architecture" width="760">
|
||||||
|
</p>
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
@@ -592,7 +596,7 @@ Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS conn
|
|||||||
|
|
||||||
### Client Admin UI
|
### Client Admin UI
|
||||||
|
|
||||||
The Client Admin UI helps you check and manage frpc's configuration.
|
The Client Admin UI helps you check and manage frpc's configuration and proxies.
|
||||||
|
|
||||||
Configure an address for admin UI to enable this feature:
|
Configure an address for admin UI to enable this feature:
|
||||||
|
|
||||||
@@ -605,6 +609,19 @@ webServer.password = "admin"
|
|||||||
|
|
||||||
Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
|
Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
|
||||||
|
|
||||||
|
#### Dynamic Proxy Management (Store)
|
||||||
|
|
||||||
|
You can dynamically create, update, and delete proxies and visitors at runtime through the Web UI or API, without restarting frpc.
|
||||||
|
|
||||||
|
To enable this feature, configure `store.path` to specify a file for persisting the configurations:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[store]
|
||||||
|
path = "./db.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
Proxies and visitors managed through the Store are saved to disk and automatically restored on frpc restart. They work alongside proxies defined in the configuration file — Store entries take precedence when names conflict.
|
||||||
|
|
||||||
### Monitor
|
### Monitor
|
||||||
|
|
||||||
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
|
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
|
||||||
@@ -800,6 +817,14 @@ Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to l
|
|||||||
|
|
||||||
**Note that global client parameters won't be modified except 'start'.**
|
**Note that global client parameters won't be modified except 'start'.**
|
||||||
|
|
||||||
|
`start` is a global allowlist evaluated after all sources are merged (config file/include/store).
|
||||||
|
If `start` is non-empty, any proxy or visitor not listed there will not be started, including
|
||||||
|
entries created via Store API.
|
||||||
|
|
||||||
|
`start` is kept mainly for compatibility and is generally not recommended for new configurations.
|
||||||
|
Prefer per-proxy/per-visitor `enabled`, and keep `start` empty unless you explicitly want this
|
||||||
|
global allowlist behavior.
|
||||||
|
|
||||||
You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors.
|
You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors.
|
||||||
|
|
||||||
### Get proxy status from client
|
### Get proxy status from client
|
||||||
|
|||||||
39
README_zh.md
39
README_zh.md
@@ -15,6 +15,26 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
@@ -32,25 +52,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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 ?
|
||||||
|
|||||||
22
Release.md
22
Release.md
@@ -1,8 +1,18 @@
|
|||||||
|
## Compatibility Policy
|
||||||
|
|
||||||
|
Starting with v0.69.0, each minor release is supported until there are nine newer minor releases. For example, v0.69.0 will be supported until v0.78.0 is released. Within this window, frpc v0.69.0 is guaranteed to work with any frps from v0.61.0 to v0.77.0, and vice versa. Patch releases within the same minor are always compatible. Versions outside the support window may continue to work on a best-effort basis, but compatibility is no longer guaranteed.
|
||||||
|
|
||||||
|
For mixed-version deployments, upgrade frps first, then upgrade frpc. This keeps the server side ready for newer client-side protocol behavior before clients start using it.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
This release introduces wire protocol v2 as a transition path for future frpc/frps protocol changes. The existing wire protocol is difficult to extend without compatibility risk, and upcoming changes, including replacing deprecated stream encryption methods, require a versioned protocol.
|
||||||
|
|
||||||
|
**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
|
||||||
|
|
||||||
|
v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* 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.
|
* Added `transport.wireProtocol` for frpc to select the internal message protocol used between frpc and frps. Supported values are `v1` and `v2`.
|
||||||
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
* Added client protocol visibility in the frps dashboard and `/api/clients` API. Online clients now report their negotiated protocol as `v1` or `v2`.
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
|
||||||
|
|||||||
@@ -29,14 +29,23 @@ var (
|
|||||||
prefixPath string
|
prefixPath string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type emptyFS struct{}
|
||||||
|
|
||||||
|
func (emptyFS) Open(name string) (http.File, error) {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
|
||||||
// if path is empty, load assets in memory
|
// if path is empty, load assets in memory
|
||||||
// or set FileSystem using disk files
|
// or set FileSystem using disk files
|
||||||
func Load(path string) {
|
func Load(path string) {
|
||||||
prefixPath = path
|
prefixPath = path
|
||||||
if prefixPath != "" {
|
switch {
|
||||||
|
case prefixPath != "":
|
||||||
FileSystem = http.Dir(prefixPath)
|
FileSystem = http.Dir(prefixPath)
|
||||||
} else {
|
case content != nil:
|
||||||
FileSystem = http.FS(content)
|
FileSystem = http.FS(content)
|
||||||
|
default:
|
||||||
|
FileSystem = emptyFS{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
adminapi "github.com/fatedier/frp/client/http"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -38,6 +38,22 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/proxy/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetProxyConfig)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/visitor/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetVisitorConfig)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
if svr.storeSource != nil {
|
||||||
|
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete)
|
||||||
|
}
|
||||||
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
subRouter.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))),
|
||||||
@@ -51,14 +67,11 @@ func healthz(w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *api.Controller {
|
func newAPIController(svr *Service) *adminapi.Controller {
|
||||||
return api.NewController(api.ControllerParams{
|
manager := newServiceConfigManager(svr)
|
||||||
GetProxyStatus: svr.getAllProxyStatus,
|
return adminapi.NewController(adminapi.ControllerParams{
|
||||||
ServerAddr: svr.common.ServerAddr,
|
ServerAddr: svr.common.ServerAddr,
|
||||||
ConfigFilePath: svr.configFilePath,
|
Manager: manager,
|
||||||
UnsafeFeatures: svr.unsafeFeatures,
|
|
||||||
UpdateConfig: svr.UpdateAllConfigurer,
|
|
||||||
GracefulClose: svr.GracefulClose,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
464
client/config_manager.go
Normal file
464
client/config_manager.go
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceConfigManager struct {
|
||||||
|
svr *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServiceConfigManager(svr *Service) configmgmt.ConfigManager {
|
||||||
|
return &serviceConfigManager{svr: svr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ReloadFromFile(strict bool) error {
|
||||||
|
if m.svr.configFilePath == "" {
|
||||||
|
return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers(
|
||||||
|
result.Common,
|
||||||
|
result.Proxies,
|
||||||
|
result.Visitors,
|
||||||
|
)
|
||||||
|
proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation)
|
||||||
|
visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation)
|
||||||
|
|
||||||
|
if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("success reload conf")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ReadConfigFile() (string, error) {
|
||||||
|
if m.svr.configFilePath == "" {
|
||||||
|
return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(m.svr.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) WriteConfigFile(content []byte) error {
|
||||||
|
if len(content) == 0 {
|
||||||
|
return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
return m.svr.getAllProxyStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
// Try running proxy manager first
|
||||||
|
ws, ok := m.svr.getProxyStatus(name)
|
||||||
|
if ok {
|
||||||
|
return ws.Cfg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to store
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource != nil {
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg != nil {
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
// Try running visitor manager first
|
||||||
|
cfg, ok := m.svr.getVisitorCfg(name)
|
||||||
|
if ok {
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to store
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource != nil {
|
||||||
|
vcfg := storeSource.GetVisitor(name)
|
||||||
|
if vcfg != nil {
|
||||||
|
return vcfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
enabled := cfg.GetBaseConfig().Enabled
|
||||||
|
return enabled == nil || *enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) StoreEnabled() bool {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
return storeSource != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storeSource.GetAllProxies()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.AddProxy(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Infof("store: created proxy %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
|
if bodyName != name {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.UpdateProxy(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: updated proxy %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.RemoveProxy(name); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: deleted proxy %q", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storeSource.GetAllVisitors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetVisitor(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.AddVisitor(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: created visitor %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
|
if bodyName != name {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: updated visitor %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.RemoveVisitor(name); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: deleted visitor %q", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GracefulClose(d time.Duration) {
|
||||||
|
m.svr.GracefulClose(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
return storeSource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreMutationAndReload(
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) error {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.ProxyConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetProxy(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.VisitorConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetVisitor(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
}
|
||||||
|
runtimeCfg := cfg.Clone()
|
||||||
|
if runtimeCfg == nil {
|
||||||
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
}
|
||||||
|
runtimeCfg.Complete()
|
||||||
|
return validation.ValidateProxyConfigurerForClient(runtimeCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("invalid visitor config")
|
||||||
|
}
|
||||||
|
runtimeCfg := cfg.Clone()
|
||||||
|
if runtimeCfg == nil {
|
||||||
|
return fmt.Errorf("invalid visitor config")
|
||||||
|
}
|
||||||
|
runtimeCfg.Complete()
|
||||||
|
return validation.ValidateVisitorConfigurer(runtimeCfg)
|
||||||
|
}
|
||||||
137
client/config_manager_test.go
Normal file
137
client/config_manager_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
|
return &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 10080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil {
|
||||||
|
t.Fatalf("seed proxy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected conflict error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrConflict) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected apply config error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrApplyConfig) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if storeSource.GetProxy("p1") == nil {
|
||||||
|
t.Fatal("proxy should remain in store after reload failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected store disabled error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrStoreDisabled) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if persisted == nil {
|
||||||
|
t.Fatal("expected persisted proxy to be returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
got := storeSource.GetProxy("raw-proxy")
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("proxy not found in store")
|
||||||
|
}
|
||||||
|
if got.GetBaseConfig().LocalIP != "" {
|
||||||
|
t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
if got.GetBaseConfig().Transport.BandwidthLimitMode != "" {
|
||||||
|
t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
client/configmgmt/types.go
Normal file
45
client/configmgmt/types.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package configmgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidArgument = errors.New("invalid argument")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrConflict = errors.New("conflict")
|
||||||
|
ErrStoreDisabled = errors.New("store disabled")
|
||||||
|
ErrApplyConfig = errors.New("apply config failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigManager interface {
|
||||||
|
ReloadFromFile(strict bool) error
|
||||||
|
|
||||||
|
ReadConfigFile() (string, error)
|
||||||
|
WriteConfigFile(content []byte) error
|
||||||
|
|
||||||
|
GetProxyStatus() []*proxy.WorkingStatus
|
||||||
|
IsStoreProxyEnabled(name string) bool
|
||||||
|
StoreEnabled() bool
|
||||||
|
|
||||||
|
GetProxyConfig(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
GetVisitorConfig(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
|
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||||
|
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||||
|
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
DeleteStoreProxy(name string) error
|
||||||
|
|
||||||
|
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
||||||
|
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
||||||
|
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
DeleteStoreVisitor(name string) error
|
||||||
|
|
||||||
|
GracefulClose(d time.Duration)
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ 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/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
"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"
|
||||||
@@ -41,6 +43,39 @@ type Connector interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageConnector interface {
|
||||||
|
Connect() (*msg.Conn, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageConnector struct {
|
||||||
|
connector Connector
|
||||||
|
wireProtocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMessageConnector(connector Connector, wireProtocol string) *messageConnector {
|
||||||
|
return &messageConnector{
|
||||||
|
connector: connector,
|
||||||
|
wireProtocol: wireProtocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *messageConnector) Connect() (*msg.Conn, error) {
|
||||||
|
conn, err := c.connector.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = wire.WriteMagicIfV2(conn, c.wireProtocol); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg.NewConn(conn, msg.NewReadWriter(conn, c.wireProtocol)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *messageConnector) Close() error {
|
||||||
|
return c.connector.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// defaultConnectorImpl is the default implementation of Connector for normal frpc.
|
// defaultConnectorImpl is the default implementation of Connector for normal frpc.
|
||||||
type defaultConnectorImpl struct {
|
type defaultConnectorImpl struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -119,6 +154,7 @@ func (c *defaultConnectorImpl) Open() error {
|
|||||||
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 {
|
||||||
|
conn.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.muxSession = session
|
c.muxSession = session
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ 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/naming"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
|
||||||
"github.com/fatedier/frp/pkg/util/wait"
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
@@ -40,13 +40,11 @@ type SessionContext struct {
|
|||||||
// It should be attached to the login message when reconnecting.
|
// It should be attached to the login message when reconnecting.
|
||||||
RunID string
|
RunID string
|
||||||
// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
|
// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
|
||||||
Conn net.Conn
|
Conn *msg.Conn
|
||||||
// Indicates whether the connection is encrypted.
|
|
||||||
ConnEncrypted bool
|
|
||||||
// Auth runtime used for login, heartbeats, and encryption.
|
// Auth runtime used for login, heartbeats, and encryption.
|
||||||
Auth *auth.ClientAuth
|
Auth *auth.ClientAuth
|
||||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
// Connector is used to create message connections to frps.
|
||||||
Connector Connector
|
Connector MessageConnector
|
||||||
// Virtual net controller
|
// Virtual net controller
|
||||||
VnetController *vnet.Controller
|
VnetController *vnet.Controller
|
||||||
}
|
}
|
||||||
@@ -90,15 +88,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
}
|
}
|
||||||
ctl.lastPong.Store(time.Now())
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
|
||||||
} else {
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
}
|
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
@@ -138,14 +128,14 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = msg.WriteMsg(workConn, m); err != nil {
|
if err = workConn.WriteMsg(m); err != nil {
|
||||||
xl.Warnf("work connection write to server error: %v", err)
|
xl.Warnf("work connection write to server error: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var startMsg msg.StartWorkConn
|
var startMsg msg.StartWorkConn
|
||||||
if err = msg.ReadMsgInto(workConn, &startMsg); err != nil {
|
if err = workConn.ReadMsgInto(&startMsg); err != nil {
|
||||||
xl.Tracef("work connection closed before response StartWorkConn message: %v", err)
|
xl.Tracef("work connection closed before response StartWorkConn message: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
@@ -156,6 +146,8 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName)
|
||||||
|
|
||||||
// dispatch this work connection to related proxy
|
// dispatch this work connection to related proxy
|
||||||
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
||||||
}
|
}
|
||||||
@@ -165,11 +157,12 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
|||||||
inMsg := m.(*msg.NewProxyResp)
|
inMsg := m.(*msg.NewProxyResp)
|
||||||
// Server will return NewProxyResp message to each NewProxy message.
|
// Server will return NewProxyResp message to each NewProxy message.
|
||||||
// Start a new proxy handler if no error got
|
// Start a new proxy handler if no error got
|
||||||
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName)
|
||||||
|
err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
|
xl.Warnf("[%s] start error: %v", proxyName, err)
|
||||||
} else {
|
} else {
|
||||||
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
|
xl.Infof("[%s] start proxy success", proxyName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +216,7 @@ func (ctl *Control) Done() <-chan struct{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connectServer return a new connection to frps
|
// connectServer return a new connection to frps
|
||||||
func (ctl *Control) connectServer() (net.Conn, error) {
|
func (ctl *Control) connectServer() (*msg.Conn, error) {
|
||||||
return ctl.sessionCtx.Connector.Connect()
|
return ctl.sessionCtx.Connector.Connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
172
client/control_session.go
Normal file
172
client/control_session.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlSessionDialer struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
common *v1.ClientCommonConfig
|
||||||
|
auth *auth.ClientAuth
|
||||||
|
clientSpec *msg.ClientSpec
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
|
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, error) {
|
||||||
|
connector := d.connectorCreator(d.ctx, d.common)
|
||||||
|
if err := connector.Open(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
_ = connector.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := connector.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
loginMsg, err := d.buildLoginMsg(previousRunID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginRespMsg, err := d.exchangeLogin(conn, loginMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if loginRespMsg.Error != "" {
|
||||||
|
return nil, errors.New(loginRespMsg.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlRW io.ReadWriter = conn
|
||||||
|
if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
|
||||||
|
controlRW, err = netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create control crypto read writer: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success = true
|
||||||
|
return &SessionContext{
|
||||||
|
Common: d.common,
|
||||||
|
RunID: loginRespMsg.RunID,
|
||||||
|
Conn: msg.NewConn(conn, msg.NewReadWriter(controlRW, d.common.Transport.WireProtocol)),
|
||||||
|
Auth: d.auth,
|
||||||
|
Connector: newMessageConnector(connector, d.common.Transport.WireProtocol),
|
||||||
|
VnetController: d.vnetController,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login, error) {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
loginMsg := &msg.Login{
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
Os: runtime.GOOS,
|
||||||
|
Hostname: hostname,
|
||||||
|
PoolCount: d.common.Transport.PoolCount,
|
||||||
|
User: d.common.User,
|
||||||
|
ClientID: d.common.ClientID,
|
||||||
|
Version: version.Full(),
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
RunID: previousRunID,
|
||||||
|
Metas: d.common.Metadatas,
|
||||||
|
}
|
||||||
|
if d.clientSpec != nil {
|
||||||
|
loginMsg.ClientSpec = *d.clientSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return loginMsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*msg.LoginResp, error) {
|
||||||
|
rw := msg.NewV1ReadWriter(conn)
|
||||||
|
var wireConn *wire.Conn
|
||||||
|
|
||||||
|
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
|
||||||
|
if err := wire.WriteMagic(conn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wireConn = wire.NewConn(conn)
|
||||||
|
rw = msg.NewV2ReadWriterWithConn(wireConn)
|
||||||
|
hello := wire.DefaultClientHello(wire.BootstrapInfo{
|
||||||
|
Transport: d.common.Transport.Protocol,
|
||||||
|
TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic",
|
||||||
|
TCPMux: lo.FromPtr(d.common.Transport.TCPMux),
|
||||||
|
})
|
||||||
|
if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rw.WriteMsg(loginMsg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
defer func() {
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
}()
|
||||||
|
|
||||||
|
if wireConn != nil {
|
||||||
|
var serverHello wire.ServerHello
|
||||||
|
if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if serverHello.Error != "" {
|
||||||
|
return nil, errors.New(serverHello.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginRespMsg msg.LoginResp
|
||||||
|
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &loginRespMsg, nil
|
||||||
|
}
|
||||||
245
client/control_session_test.go
Normal file
245
client/control_session_test.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testConnector struct {
|
||||||
|
conn net.Conn
|
||||||
|
closed atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testConnector) Open() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testConnector) Connect() (net.Conn, error) {
|
||||||
|
return c.conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testConnector) Close() error {
|
||||||
|
c.closed.Store(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackingConn struct {
|
||||||
|
net.Conn
|
||||||
|
closed atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackingConn) Close() error {
|
||||||
|
c.closed.Store(true)
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestControlSessionDialer(t *testing.T, protocol string, connector Connector, clientSpec *msg.ClientSpec) *controlSessionDialer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
authRuntime, err := auth.BuildClientAuth(&v1.AuthClientConfig{
|
||||||
|
Method: v1.AuthMethodToken,
|
||||||
|
Token: "token",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &controlSessionDialer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
common: &v1.ClientCommonConfig{
|
||||||
|
User: "test-user",
|
||||||
|
Transport: v1.ClientTransportConfig{
|
||||||
|
Protocol: "tcp",
|
||||||
|
WireProtocol: protocol,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: authRuntime,
|
||||||
|
clientSpec: clientSpec,
|
||||||
|
connectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector {
|
||||||
|
return connector
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlSessionDialerDialV1(t *testing.T) {
|
||||||
|
clientRaw, serverRaw := net.Pipe()
|
||||||
|
defer serverRaw.Close()
|
||||||
|
|
||||||
|
connector := &testConnector{conn: &trackingConn{Conn: clientRaw}}
|
||||||
|
serverErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
rw := msg.NewV1ReadWriter(serverRaw)
|
||||||
|
var loginMsg msg.Login
|
||||||
|
if err := rw.ReadMsgInto(&loginMsg); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if loginMsg.RunID != "previous-run-id" {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected previous run id: %s", loginMsg.RunID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if loginMsg.User != "test-user" {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v1"})
|
||||||
|
}()
|
||||||
|
|
||||||
|
dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, nil)
|
||||||
|
sessionCtx, err := dialer.Dial("previous-run-id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer sessionCtx.Conn.Close()
|
||||||
|
defer sessionCtx.Connector.Close()
|
||||||
|
|
||||||
|
require.Equal(t, "run-v1", sessionCtx.RunID)
|
||||||
|
require.NotNil(t, sessionCtx.Conn)
|
||||||
|
require.NotNil(t, sessionCtx.Connector)
|
||||||
|
require.False(t, connector.closed.Load())
|
||||||
|
require.NoError(t, <-serverErrCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlSessionDialerDialV2(t *testing.T) {
|
||||||
|
clientRaw, serverRaw := net.Pipe()
|
||||||
|
defer serverRaw.Close()
|
||||||
|
|
||||||
|
connector := &testConnector{conn: &trackingConn{Conn: clientRaw}}
|
||||||
|
serverErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
magic := make([]byte, len(wire.MagicV2))
|
||||||
|
if _, err := io.ReadFull(serverRaw, magic); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(magic) != wire.MagicV2 {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected magic: %q", string(magic))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wireConn := wire.NewConn(serverRaw)
|
||||||
|
var hello wire.ClientHello
|
||||||
|
if err := wireConn.ReadJSONFrame(wire.FrameTypeClientHello, &hello); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := wire.ValidateClientHello(hello); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := msg.NewV2ReadWriterWithConn(wireConn)
|
||||||
|
var loginMsg msg.Login
|
||||||
|
if err := rw.ReadMsgInto(&loginMsg); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if loginMsg.User != "test-user" {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, wire.DefaultServerHello()); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"})
|
||||||
|
}()
|
||||||
|
|
||||||
|
dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil)
|
||||||
|
sessionCtx, err := dialer.Dial("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer sessionCtx.Conn.Close()
|
||||||
|
defer sessionCtx.Connector.Close()
|
||||||
|
|
||||||
|
require.Equal(t, "run-v2", sessionCtx.RunID)
|
||||||
|
require.NotNil(t, sessionCtx.Conn)
|
||||||
|
require.NotNil(t, sessionCtx.Connector)
|
||||||
|
require.False(t, connector.closed.Load())
|
||||||
|
require.NoError(t, <-serverErrCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlSessionDialerDialLoginErrorClosesResources(t *testing.T) {
|
||||||
|
clientRaw, serverRaw := net.Pipe()
|
||||||
|
defer serverRaw.Close()
|
||||||
|
|
||||||
|
clientConn := &trackingConn{Conn: clientRaw}
|
||||||
|
connector := &testConnector{conn: clientConn}
|
||||||
|
serverErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
rw := msg.NewV1ReadWriter(serverRaw)
|
||||||
|
var loginMsg msg.Login
|
||||||
|
if err := rw.ReadMsgInto(&loginMsg); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverErrCh <- rw.WriteMsg(&msg.LoginResp{Error: "login denied"})
|
||||||
|
}()
|
||||||
|
|
||||||
|
dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, nil)
|
||||||
|
sessionCtx, err := dialer.Dial("")
|
||||||
|
require.Nil(t, sessionCtx)
|
||||||
|
require.ErrorContains(t, err, "login denied")
|
||||||
|
require.True(t, clientConn.closed.Load())
|
||||||
|
require.True(t, connector.closed.Load())
|
||||||
|
require.NoError(t, <-serverErrCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlSessionDialerDialSSHTunnelSkipsControlEncryption(t *testing.T) {
|
||||||
|
clientRaw, serverRaw := net.Pipe()
|
||||||
|
defer serverRaw.Close()
|
||||||
|
|
||||||
|
connector := &testConnector{conn: &trackingConn{Conn: clientRaw}}
|
||||||
|
serverErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
rw := msg.NewV1ReadWriter(serverRaw)
|
||||||
|
var loginMsg msg.Login
|
||||||
|
if err := rw.ReadMsgInto(&loginMsg); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-ssh-tunnel"}); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = serverRaw.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
|
var ping msg.Ping
|
||||||
|
if err := rw.ReadMsgInto(&ping); err != nil {
|
||||||
|
serverErrCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverErrCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, &msg.ClientSpec{Type: "ssh-tunnel"})
|
||||||
|
sessionCtx, err := dialer.Dial("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer sessionCtx.Conn.Close()
|
||||||
|
defer sessionCtx.Connector.Close()
|
||||||
|
|
||||||
|
require.Equal(t, "run-ssh-tunnel", sessionCtx.RunID)
|
||||||
|
require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{}))
|
||||||
|
require.NoError(t, <-serverErrCh)
|
||||||
|
}
|
||||||
433
client/http/controller.go
Normal file
433
client/http/controller.go
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller handles HTTP API requests for frpc.
|
||||||
|
type Controller struct {
|
||||||
|
serverAddr string
|
||||||
|
manager configmgmt.ConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControllerParams contains parameters for creating an APIController.
|
||||||
|
type ControllerParams struct {
|
||||||
|
ServerAddr string
|
||||||
|
Manager configmgmt.ConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(params ControllerParams) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
serverAddr: params.ServerAddr,
|
||||||
|
manager: params.Manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) toHTTPError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, configmgmt.ErrInvalidArgument):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled):
|
||||||
|
code = http.StatusNotFound
|
||||||
|
case errors.Is(err, configmgmt.ErrConflict):
|
||||||
|
code = http.StatusConflict
|
||||||
|
}
|
||||||
|
return httppkg.NewError(code, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload handles GET /api/reload
|
||||||
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
|
strictConfigMode := false
|
||||||
|
strictStr := ctx.Query("strictConfig")
|
||||||
|
if strictStr != "" {
|
||||||
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.ReloadFromFile(strictConfigMode); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop handles POST /api/stop
|
||||||
|
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||||
|
go c.manager.GracefulClose(100 * time.Millisecond)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status handles GET /api/status
|
||||||
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
|
res := make(model.StatusResp)
|
||||||
|
ps := c.manager.GetProxyStatus()
|
||||||
|
if ps == nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig handles GET /api/config
|
||||||
|
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
content, err := c.manager.ReadConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutConfig handles PUT /api/config
|
||||||
|
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.WriteConfigFile(body); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
||||||
|
psr := model.ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.manager.IsStoreProxyEnabled(status.Name) {
|
||||||
|
psr.Source = model.SourceStore
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProxyConfig handles GET /api/proxy/{name}/config
|
||||||
|
func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := c.manager.GetProxyConfig(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVisitorConfig handles GET /api/visitor/{name}/config
|
||||||
|
func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := c.manager.GetVisitorConfig(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||||
|
proxies, err := c.manager.ListStoreProxies()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
||||||
|
for _, p := range proxies {
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
resp.Proxies = append(resp.Proxies, payload)
|
||||||
|
}
|
||||||
|
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := c.manager.GetStoreProxy(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.ProxyDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate("", false); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreProxy(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.ProxyDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate(name, true); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.DeleteStoreProxy(name); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
||||||
|
visitors, err := c.manager.ListStoreVisitors()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
||||||
|
for _, v := range visitors {
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
resp.Visitors = append(resp.Visitors, payload)
|
||||||
|
}
|
||||||
|
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := c.manager.GetStoreVisitor(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.VisitorDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate("", false); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreVisitor(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.VisitorDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate(name, true); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.DeleteStoreVisitor(name); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
662
client/http/controller_test.go
Normal file
662
client/http/controller_test.go
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeConfigManager struct {
|
||||||
|
reloadFromFileFn func(strict bool) error
|
||||||
|
readConfigFileFn func() (string, error)
|
||||||
|
writeConfigFileFn func(content []byte) error
|
||||||
|
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||||
|
isStoreProxyEnabledFn func(name string) bool
|
||||||
|
storeEnabledFn func() bool
|
||||||
|
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
|
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||||
|
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||||
|
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
deleteStoreProxyFn func(name string) error
|
||||||
|
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
||||||
|
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
||||||
|
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
deleteStoreVisitFn func(name string) error
|
||||||
|
gracefulCloseFn func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
||||||
|
if m.reloadFromFileFn != nil {
|
||||||
|
return m.reloadFromFileFn(strict)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
||||||
|
if m.readConfigFileFn != nil {
|
||||||
|
return m.readConfigFileFn()
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
||||||
|
if m.writeConfigFileFn != nil {
|
||||||
|
return m.writeConfigFileFn(content)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
if m.getProxyStatusFn != nil {
|
||||||
|
return m.getProxyStatusFn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
|
if m.isStoreProxyEnabledFn != nil {
|
||||||
|
return m.isStoreProxyEnabledFn(name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) StoreEnabled() bool {
|
||||||
|
if m.storeEnabledFn != nil {
|
||||||
|
return m.storeEnabledFn()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if m.getProxyConfigFn != nil {
|
||||||
|
return m.getProxyConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if m.getVisitorConfigFn != nil {
|
||||||
|
return m.getVisitorConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
if m.listStoreProxiesFn != nil {
|
||||||
|
return m.listStoreProxiesFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.getStoreProxyFn != nil {
|
||||||
|
return m.getStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.createStoreProxyFn != nil {
|
||||||
|
return m.createStoreProxyFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.updateStoreProxyFn != nil {
|
||||||
|
return m.updateStoreProxyFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
||||||
|
if m.deleteStoreProxyFn != nil {
|
||||||
|
return m.deleteStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
if m.listStoreVisitorsFn != nil {
|
||||||
|
return m.listStoreVisitorsFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.getStoreVisitorFn != nil {
|
||||||
|
return m.getStoreVisitorFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.createStoreVisitFn != nil {
|
||||||
|
return m.createStoreVisitFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.updateStoreVisitFn != nil {
|
||||||
|
return m.updateStoreVisitFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
|
if m.deleteStoreVisitFn != nil {
|
||||||
|
return m.deleteStoreVisitFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
||||||
|
if m.gracefulCloseFn != nil {
|
||||||
|
m.gracefulCloseFn(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
|
return &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 10080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
||||||
|
status := &proxy.WorkingStatus{
|
||||||
|
Name: "shared-proxy",
|
||||||
|
Type: "tcp",
|
||||||
|
Phase: proxy.ProxyPhaseRunning,
|
||||||
|
RemoteAddr: ":8080",
|
||||||
|
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
||||||
|
}
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
serverAddr: "127.0.0.1",
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
isStoreProxyEnabledFn: func(name string) bool {
|
||||||
|
return name == "shared-proxy"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := controller.buildProxyStatusResp(status)
|
||||||
|
if resp.Source != "store" {
|
||||||
|
t.Fatalf("unexpected source: %q", resp.Source)
|
||||||
|
}
|
||||||
|
if resp.RemoteAddr != "127.0.0.1:8080" {
|
||||||
|
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
||||||
|
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
||||||
|
_, err := controller.Reload(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreProxyErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
||||||
|
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
||||||
|
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return nil, tc.err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreVisitorErrorMapping(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
deleteStoreVisitFn: func(string) error {
|
||||||
|
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.DeleteStoreVisitor(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-proxy" {
|
||||||
|
t.Fatalf("unexpected proxy name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
||||||
|
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-visitor" {
|
||||||
|
t.Fatalf("unexpected visitor name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "xtcp" || payload.XTCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.TCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"plugin-visitor","type":"stcp",
|
||||||
|
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.STCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
||||||
|
b := newRawTCPProxyConfig("b")
|
||||||
|
a := newRawTCPProxyConfig("a")
|
||||||
|
return []v1.ProxyConfigurer{b, a}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
||||||
|
|
||||||
|
resp, err := controller.ListStoreProxies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list store proxies: %v", err)
|
||||||
|
}
|
||||||
|
out, ok := resp.(model.ProxyListResp)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if len(out.Proxies) != 2 {
|
||||||
|
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
||||||
|
}
|
||||||
|
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
||||||
|
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtError(sentinel error, msg string) error {
|
||||||
|
return fmt.Errorf("%w: %s", sentinel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertHTTPCode(t *testing.T, err error, expected int) {
|
||||||
|
t.Helper()
|
||||||
|
var httpErr *httppkg.Error
|
||||||
|
if !errors.As(err, &httpErr) {
|
||||||
|
t.Fatalf("unexpected error type: %T", err)
|
||||||
|
}
|
||||||
|
if httpErr.Code != expected {
|
||||||
|
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"name": "shared-proxy",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localPort": 10080,
|
||||||
|
"remotePort": 7000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("update store proxy: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if name == "ssh" {
|
||||||
|
cfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "ssh",
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "ssh"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get proxy config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if name == "my-stcp" {
|
||||||
|
cfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "my-stcp",
|
||||||
|
Type: "stcp",
|
||||||
|
ServerName: "server1",
|
||||||
|
BindPort: 9000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get visitor config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
148
client/http/model/proxy_definition.go
Normal file
148
client/http/model/proxy_definition.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
||||||
|
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
||||||
|
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
||||||
|
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
||||||
|
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
||||||
|
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("proxy name is required")
|
||||||
|
}
|
||||||
|
if !IsProxyType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("proxy name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := ProxyDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.TCPProxyConfig:
|
||||||
|
payload.TCP = c
|
||||||
|
case *v1.UDPProxyConfig:
|
||||||
|
payload.UDP = c
|
||||||
|
case *v1.HTTPProxyConfig:
|
||||||
|
payload.HTTP = c
|
||||||
|
case *v1.HTTPSProxyConfig:
|
||||||
|
payload.HTTPS = c
|
||||||
|
case *v1.TCPMuxProxyConfig:
|
||||||
|
payload.TCPMux = c
|
||||||
|
case *v1.STCPProxyConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPProxyConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPProxyConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.ProxyConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.TCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCP
|
||||||
|
blockType = "tcp"
|
||||||
|
}
|
||||||
|
if p.UDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.UDP
|
||||||
|
blockType = "udp"
|
||||||
|
}
|
||||||
|
if p.HTTP != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTP
|
||||||
|
blockType = "http"
|
||||||
|
}
|
||||||
|
if p.HTTPS != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTPS
|
||||||
|
blockType = "https"
|
||||||
|
}
|
||||||
|
if p.TCPMux != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCPMux
|
||||||
|
blockType = "tcpmux"
|
||||||
|
}
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProxyType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package model
|
||||||
|
|
||||||
|
const SourceStore = "store"
|
||||||
|
|
||||||
// StatusResp is the response for GET /api/status
|
// StatusResp is the response for GET /api/status
|
||||||
type StatusResp map[string][]ProxyStatusResp
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
@@ -26,4 +28,15 @@ type ProxyStatusResp struct {
|
|||||||
LocalAddr string `json:"local_addr"`
|
LocalAddr string `json:"local_addr"`
|
||||||
Plugin string `json:"plugin"`
|
Plugin string `json:"plugin"`
|
||||||
RemoteAddr string `json:"remote_addr"`
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
Source string `json:"source,omitempty"` // "store" or "config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyListResp is the response for GET /api/store/proxies
|
||||||
|
type ProxyListResp struct {
|
||||||
|
Proxies []ProxyDefinition `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisitorListResp is the response for GET /api/store/visitors
|
||||||
|
type VisitorListResp struct {
|
||||||
|
Visitors []VisitorDefinition `json:"visitors"`
|
||||||
}
|
}
|
||||||
107
client/http/model/visitor_definition.go
Normal file
107
client/http/model/visitor_definition.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VisitorDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("visitor name is required")
|
||||||
|
}
|
||||||
|
if !IsVisitorType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("visitor name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := VisitorDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.STCPVisitorConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPVisitorConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPVisitorConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.VisitorConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsVisitorType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -122,6 +123,33 @@ func (pxy *BaseProxy) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapWorkConn applies rate limiting, encryption, and compression
|
||||||
|
// to a work connection based on the proxy's transport configuration.
|
||||||
|
// The returned recycle function should be called when the stream is no longer in use
|
||||||
|
// to return compression resources to the pool. It is safe to not call recycle,
|
||||||
|
// in which case resources will be garbage collected normally.
|
||||||
|
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
||||||
|
var rwc io.ReadWriteCloser = conn
|
||||||
|
if pxy.limiter != nil {
|
||||||
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||||
|
return conn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if pxy.baseCfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, encKey)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var recycleFn func()
|
||||||
|
if pxy.baseCfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
pxy.inWorkConnCallback = cb
|
pxy.inWorkConnCallback = cb
|
||||||
}
|
}
|
||||||
@@ -139,31 +167,15 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
baseCfg := pxy.baseCfg
|
baseCfg := pxy.baseCfg
|
||||||
var (
|
|
||||||
remote io.ReadWriteCloser
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
remote = workConn
|
|
||||||
if pxy.limiter != nil {
|
|
||||||
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
|
|
||||||
return workConn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||||
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||||
if baseCfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, encKey)
|
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
workConn.Close()
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
var compressionResourceRecycleFn func()
|
|
||||||
if baseCfg.Transport.UseCompression {
|
|
||||||
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we need to send proxy protocol info
|
// check if we need to send proxy protocol info
|
||||||
var connInfo plugin.ConnectionInfo
|
var connInfo plugin.ConnectionInfo
|
||||||
@@ -178,7 +190,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
// Use the common proxy protocol builder function
|
|
||||||
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||||
connInfo.ProxyProtocolHeader = header
|
connInfo.ProxyProtocolHeader = header
|
||||||
}
|
}
|
||||||
@@ -187,12 +198,18 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
|
|
||||||
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
|
||||||
|
// Don't recycle compression resources here because plugins may
|
||||||
|
// retain the connection after Handle returns.
|
||||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||||
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||||
xl.Debugf("handle by plugin finished")
|
xl.Debugf("handle by plugin finished")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recycleFn != nil {
|
||||||
|
defer recycleFn()
|
||||||
|
}
|
||||||
|
|
||||||
localConn, err := libnet.Dial(
|
localConn, err := libnet.Dial(
|
||||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||||
libnet.WithTimeout(10*time.Second),
|
libnet.WithTimeout(10*time.Second),
|
||||||
@@ -209,6 +226,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if connInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
|
localConn.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
|
||||||
}
|
}
|
||||||
@@ -219,7 +237,4 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
xl.Tracef("join connections errors: %v", errs)
|
xl.Tracef("join connections errors: %v", errs)
|
||||||
}
|
}
|
||||||
if compressionResourceRecycleFn != nil {
|
|
||||||
compressionResourceRecycleFn()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ func (pm *Manager) HandleEvent(payload any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
||||||
ps := make([]*WorkingStatus, 0)
|
|
||||||
pm.mu.RLock()
|
pm.mu.RLock()
|
||||||
defer pm.mu.RUnlock()
|
defer pm.mu.RUnlock()
|
||||||
|
ps := make([]*WorkingStatus, 0, len(pm.proxies))
|
||||||
for _, pxy := range pm.proxies {
|
for _, pxy := range pm.proxies {
|
||||||
ps = append(ps, pxy.GetStatus())
|
ps = append(ps, pxy.GetStatus())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/fatedier/frp/client/health"
|
"github.com/fatedier/frp/client/health"
|
||||||
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/naming"
|
||||||
"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"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
@@ -86,6 +87,8 @@ type Wrapper struct {
|
|||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
|
wireName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWrapper(
|
func NewWrapper(
|
||||||
@@ -113,6 +116,7 @@ func NewWrapper(
|
|||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
|
wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||||
@@ -182,7 +186,7 @@ func (pw *Wrapper) Stop() {
|
|||||||
func (pw *Wrapper) close() {
|
func (pw *Wrapper) close() {
|
||||||
_ = pw.handler(&event.CloseProxyPayload{
|
_ = pw.handler(&event.CloseProxyPayload{
|
||||||
CloseProxyMsg: &msg.CloseProxy{
|
CloseProxyMsg: &msg.CloseProxy{
|
||||||
ProxyName: pw.Name,
|
ProxyName: pw.wireName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -208,6 +212,7 @@ func (pw *Wrapper) checkWorker() {
|
|||||||
|
|
||||||
var newProxyMsg msg.NewProxy
|
var newProxyMsg msg.NewProxy
|
||||||
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
||||||
|
newProxyMsg.ProxyName = pw.wireName
|
||||||
pw.lastSendStartMsg = now
|
pw.lastSendStartMsg = now
|
||||||
_ = pw.handler(&event.StartProxyPayload{
|
_ = pw.handler(&event.StartProxyPayload{
|
||||||
NewProxyMsg: &newProxyMsg,
|
NewProxyMsg: &newProxyMsg,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -25,17 +24,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
"github.com/fatedier/frp/pkg/util/limit"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SUDPProxy struct {
|
type SUDPProxy struct {
|
||||||
@@ -83,27 +80,13 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||||
|
|
||||||
var rwc io.ReadWriteCloser = conn
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
var err error
|
|
||||||
if pxy.limiter != nil {
|
|
||||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
|
||||||
return conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseCompression {
|
|
||||||
rwc = libio.WithCompression(rwc)
|
|
||||||
}
|
|
||||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
|
||||||
|
|
||||||
workConn := conn
|
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
readCh := make(chan *msg.UDPPacket, 1024)
|
readCh := make(chan *msg.UDPPacket, 1024)
|
||||||
sendCh := make(chan msg.Message, 1024)
|
sendCh := make(chan msg.Message, 1024)
|
||||||
isClose := false
|
isClose := false
|
||||||
|
|||||||
@@ -17,24 +17,21 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
"github.com/fatedier/frp/pkg/util/limit"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UDPProxy struct {
|
type UDPProxy struct {
|
||||||
@@ -94,28 +91,14 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
// close resources related with old workConn
|
// close resources related with old workConn
|
||||||
pxy.Close()
|
pxy.Close()
|
||||||
|
|
||||||
var rwc io.ReadWriteCloser = conn
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
var err error
|
|
||||||
if pxy.limiter != nil {
|
|
||||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
|
||||||
return conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseCompression {
|
|
||||||
rwc = libio.WithCompression(rwc)
|
|
||||||
}
|
|
||||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
|
||||||
|
|
||||||
pxy.mu.Lock()
|
pxy.mu.Lock()
|
||||||
pxy.workConn = conn
|
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||||
pxy.sendCh = make(chan msg.Message, 1024)
|
pxy.sendCh = make(chan msg.Message, 1024)
|
||||||
pxy.closed = false
|
pxy.closed = false
|
||||||
@@ -129,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
xl.Tracef("get udp package from workConn: %s", udpMsg.Content)
|
xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content))
|
||||||
readCh <- &udpMsg
|
readCh <- &udpMsg
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
||||||
@@ -145,7 +128,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
for rawMsg := range sendCh {
|
for rawMsg := range sendCh {
|
||||||
switch m := rawMsg.(type) {
|
switch m := rawMsg.(type) {
|
||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
xl.Tracef("send udp package to workConn: %s", m.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(m.Content))
|
||||||
case *msg.Ping:
|
case *msg.Ping:
|
||||||
xl.Tracef("send ping message to udp workConn")
|
xl.Tracef("send ping message to udp workConn")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ import (
|
|||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
"github.com/fatedier/frp/pkg/nathole"
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type XTCPProxy struct {
|
type XTCPProxy struct {
|
||||||
@@ -85,7 +86,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
|||||||
transactionID := nathole.NewTransactionID()
|
transactionID := nathole.NewTransactionID()
|
||||||
natHoleClientMsg := &msg.NatHoleClient{
|
natHoleClientMsg := &msg.NatHoleClient{
|
||||||
TransactionID: transactionID,
|
TransactionID: transactionID,
|
||||||
ProxyName: pxy.cfg.Name,
|
ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name),
|
||||||
Sid: natHoleSidMsg.Sid,
|
Sid: natHoleSidMsg.Sid,
|
||||||
MappedAddrs: prepareResult.Addrs,
|
MappedAddrs: prepareResult.Addrs,
|
||||||
AssistedAddrs: prepareResult.AssistedAddrs,
|
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -29,13 +29,14 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
"github.com/fatedier/frp/pkg/auth"
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
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"
|
"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/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"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
@@ -62,8 +63,10 @@ func (e cancelErr) Error() string {
|
|||||||
// ServiceOptions contains options for creating a new client service.
|
// ServiceOptions contains options for creating a new client service.
|
||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
Common *v1.ClientCommonConfig
|
Common *v1.ClientCommonConfig
|
||||||
ProxyCfgs []v1.ProxyConfigurer
|
|
||||||
VisitorCfgs []v1.VisitorConfigurer
|
// ConfigSourceAggregator manages internal config and optional store sources.
|
||||||
|
// It is required for creating a Service.
|
||||||
|
ConfigSourceAggregator *source.Aggregator
|
||||||
|
|
||||||
UnsafeFeatures *security.UnsafeFeatures
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
@@ -120,11 +123,23 @@ type Service struct {
|
|||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
cfgMu sync.RWMutex
|
cfgMu sync.RWMutex
|
||||||
|
// reloadMu serializes reload transactions to keep reloadCommon and applied
|
||||||
|
// config in sync across concurrent API operations.
|
||||||
|
reloadMu sync.Mutex
|
||||||
common *v1.ClientCommonConfig
|
common *v1.ClientCommonConfig
|
||||||
|
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
||||||
|
// It can be updated by /api/reload without mutating startup-only common behavior.
|
||||||
|
reloadCommon *v1.ClientCommonConfig
|
||||||
proxyCfgs []v1.ProxyConfigurer
|
proxyCfgs []v1.ProxyConfigurer
|
||||||
visitorCfgs []v1.VisitorConfigurer
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
clientSpec *msg.ClientSpec
|
clientSpec *msg.ClientSpec
|
||||||
|
|
||||||
|
// aggregator manages multiple configuration sources.
|
||||||
|
// When set, the service watches for config changes and reloads automatically.
|
||||||
|
aggregator *source.Aggregator
|
||||||
|
configSource *source.ConfigSource
|
||||||
|
storeSource *source.StoreSource
|
||||||
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
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
|
||||||
@@ -146,6 +161,28 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ConfigSourceAggregator == nil {
|
||||||
|
return nil, fmt.Errorf("config source aggregator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
configSource := options.ConfigSourceAggregator.ConfigSource()
|
||||||
|
storeSource := options.ConfigSourceAggregator.StoreSource()
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
||||||
|
if loadErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
||||||
|
}
|
||||||
|
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
||||||
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||||
|
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||||
|
|
||||||
|
// Create the web server after all fallible steps so its listener is not
|
||||||
|
// leaked when an earlier error causes NewService to return.
|
||||||
var webServer *httppkg.Server
|
var webServer *httppkg.Server
|
||||||
if options.Common.WebServer.Port > 0 {
|
if options.Common.WebServer.Port > 0 {
|
||||||
ws, err := httppkg.NewServer(options.Common.WebServer)
|
ws, err := httppkg.NewServer(options.Common.WebServer)
|
||||||
@@ -155,24 +192,24 @@ 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(),
|
||||||
auth: authRuntime,
|
auth: authRuntime,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
|
reloadCommon: options.Common,
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
unsafeFeatures: options.UnsafeFeatures,
|
unsafeFeatures: options.UnsafeFeatures,
|
||||||
proxyCfgs: options.ProxyCfgs,
|
proxyCfgs: proxyCfgs,
|
||||||
visitorCfgs: options.VisitorCfgs,
|
visitorCfgs: visitorCfgs,
|
||||||
clientSpec: options.ClientSpec,
|
clientSpec: options.ClientSpec,
|
||||||
|
aggregator: options.ConfigSourceAggregator,
|
||||||
|
configSource: configSource,
|
||||||
|
storeSource: storeSource,
|
||||||
connectorCreator: options.ConnectorCreator,
|
connectorCreator: options.ConnectorCreator,
|
||||||
handleWorkConnCb: options.HandleWorkConnCb,
|
handleWorkConnCb: options.HandleWorkConnCb,
|
||||||
}
|
}
|
||||||
|
|
||||||
if webServer != nil {
|
if webServer != nil {
|
||||||
webServer.RouteRegister(s.registerRouteHandlers)
|
webServer.RouteRegister(s.registerRouteHandlers)
|
||||||
}
|
}
|
||||||
@@ -193,22 +230,25 @@ func (svr *Service) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if svr.vnetController != nil {
|
if svr.vnetController != nil {
|
||||||
|
vnetController := svr.vnetController
|
||||||
if err := svr.vnetController.Init(); err != nil {
|
if err := svr.vnetController.Init(); err != nil {
|
||||||
log.Errorf("init virtual network controller error: %v", err)
|
log.Errorf("init virtual network controller error: %v", err)
|
||||||
|
svr.stop()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
log.Infof("virtual network controller start...")
|
log.Infof("virtual network controller start...")
|
||||||
if err := svr.vnetController.Run(); err != nil {
|
if err := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
log.Warnf("virtual network controller exit with error: %v", err)
|
log.Warnf("virtual network controller exit with error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if svr.webServer != nil {
|
if svr.webServer != nil {
|
||||||
|
webServer := svr.webServer
|
||||||
go func() {
|
go func() {
|
||||||
log.Infof("admin server listen on %s", svr.webServer.Address())
|
log.Infof("admin server listen on %s", webServer.Address())
|
||||||
if err := svr.webServer.Run(); err != nil {
|
if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Warnf("admin server exit with error: %v", err)
|
log.Warnf("admin server exit with error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -219,6 +259,7 @@ func (svr *Service) Run(ctx context.Context) error {
|
|||||||
if svr.ctl == nil {
|
if svr.ctl == nil {
|
||||||
cancelCause := cancelErr{}
|
cancelCause := cancelErr{}
|
||||||
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
||||||
|
svr.stop()
|
||||||
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,80 +301,20 @@ func (svr *Service) keepControllerWorking() {
|
|||||||
), true, svr.ctx.Done())
|
), true, svr.ctx.Done())
|
||||||
}
|
}
|
||||||
|
|
||||||
// login creates a connection to frps and registers it self as a client
|
|
||||||
// conn: control connection
|
|
||||||
// session: if it's not nil, using tcp mux
|
|
||||||
func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|
||||||
xl := xlog.FromContextSafe(svr.ctx)
|
|
||||||
connector = svr.connectorCreator(svr.ctx, svr.common)
|
|
||||||
if err = connector.Open(); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
connector.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err = connector.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
|
|
||||||
loginMsg := &msg.Login{
|
|
||||||
Arch: runtime.GOARCH,
|
|
||||||
Os: runtime.GOOS,
|
|
||||||
Hostname: hostname,
|
|
||||||
PoolCount: svr.common.Transport.PoolCount,
|
|
||||||
User: svr.common.User,
|
|
||||||
ClientID: svr.common.ClientID,
|
|
||||||
Version: version.Full(),
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
RunID: svr.runID,
|
|
||||||
Metas: svr.common.Metadatas,
|
|
||||||
}
|
|
||||||
if svr.clientSpec != nil {
|
|
||||||
loginMsg.ClientSpec = *svr.clientSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add auth
|
|
||||||
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = msg.WriteMsg(conn, loginMsg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var loginRespMsg msg.LoginResp
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
if err = msg.ReadMsgInto(conn, &loginRespMsg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if loginRespMsg.Error != "" {
|
|
||||||
err = fmt.Errorf("%s", loginRespMsg.Error)
|
|
||||||
xl.Errorf("%s", loginRespMsg.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
svr.runID = loginRespMsg.RunID
|
|
||||||
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
|
||||||
|
|
||||||
xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
||||||
xl := xlog.FromContextSafe(svr.ctx)
|
xl := xlog.FromContextSafe(svr.ctx)
|
||||||
|
|
||||||
loginFunc := func() (bool, error) {
|
loginFunc := func() (bool, error) {
|
||||||
xl.Infof("try to connect to server...")
|
xl.Infof("try to connect to server...")
|
||||||
conn, connector, err := svr.login()
|
dialer := &controlSessionDialer{
|
||||||
|
ctx: svr.ctx,
|
||||||
|
common: svr.common,
|
||||||
|
auth: svr.auth,
|
||||||
|
clientSpec: svr.clientSpec,
|
||||||
|
vnetController: svr.vnetController,
|
||||||
|
connectorCreator: svr.connectorCreator,
|
||||||
|
}
|
||||||
|
sessionCtx, err := dialer.Dial(svr.runID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("connect to server error: %v", err)
|
xl.Warnf("connect to server error: %v", err)
|
||||||
if firstLoginExit {
|
if firstLoginExit {
|
||||||
@@ -342,25 +323,19 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svr.runID = sessionCtx.RunID
|
||||||
|
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
||||||
|
xl.Infof("login to server success, get run id [%s]", svr.runID)
|
||||||
|
|
||||||
svr.cfgMu.RLock()
|
svr.cfgMu.RLock()
|
||||||
proxyCfgs := svr.proxyCfgs
|
proxyCfgs := svr.proxyCfgs
|
||||||
visitorCfgs := svr.visitorCfgs
|
visitorCfgs := svr.visitorCfgs
|
||||||
svr.cfgMu.RUnlock()
|
svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
|
|
||||||
|
|
||||||
sessionCtx := &SessionContext{
|
|
||||||
Common: svr.common,
|
|
||||||
RunID: svr.runID,
|
|
||||||
Conn: conn,
|
|
||||||
ConnEncrypted: connEncrypted,
|
|
||||||
Auth: svr.auth,
|
|
||||||
Connector: connector,
|
|
||||||
VnetController: svr.vnetController,
|
|
||||||
}
|
|
||||||
ctl, err := NewControl(svr.ctx, sessionCtx)
|
ctl, err := NewControl(svr.ctx, sessionCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
sessionCtx.Conn.Close()
|
||||||
|
sessionCtx.Connector.Close()
|
||||||
xl.Errorf("new control error: %v", err)
|
xl.Errorf("new control error: %v", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -403,6 +378,35 @@ func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorC
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svr *Service) UpdateConfigSource(
|
||||||
|
common *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
) error {
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
defer svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
cfgSource := svr.configSource
|
||||||
|
if cfgSource == nil {
|
||||||
|
return fmt.Errorf("config source is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-atomic update semantics: source has been updated at this point.
|
||||||
|
// Even if reload fails below, keep this common config for subsequent reloads.
|
||||||
|
svr.cfgMu.Lock()
|
||||||
|
svr.reloadCommon = common
|
||||||
|
svr.cfgMu.Unlock()
|
||||||
|
|
||||||
|
if err := svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Service) Close() {
|
func (svr *Service) Close() {
|
||||||
svr.GracefulClose(time.Duration(0))
|
svr.GracefulClose(time.Duration(0))
|
||||||
}
|
}
|
||||||
@@ -413,6 +417,15 @@ func (svr *Service) GracefulClose(d time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) stop() {
|
func (svr *Service) stop() {
|
||||||
|
// Coordinate shutdown with reload/update paths that read source pointers.
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
if svr.aggregator != nil {
|
||||||
|
svr.aggregator = nil
|
||||||
|
}
|
||||||
|
svr.configSource = nil
|
||||||
|
svr.storeSource = nil
|
||||||
|
svr.reloadMu.Unlock()
|
||||||
|
|
||||||
svr.ctlMu.Lock()
|
svr.ctlMu.Lock()
|
||||||
defer svr.ctlMu.Unlock()
|
defer svr.ctlMu.Unlock()
|
||||||
if svr.ctl != nil {
|
if svr.ctl != nil {
|
||||||
@@ -423,6 +436,10 @@ func (svr *Service) stop() {
|
|||||||
svr.webServer.Close()
|
svr.webServer.Close()
|
||||||
svr.webServer = nil
|
svr.webServer = nil
|
||||||
}
|
}
|
||||||
|
if svr.vnetController != nil {
|
||||||
|
_ = svr.vnetController.Stop()
|
||||||
|
svr.vnetController = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
@@ -436,6 +453,17 @@ func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
|||||||
return ctl.pm.GetProxyStatus(name)
|
return ctl.pm.GetProxyStatus(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getVisitorCfg(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
svr.ctlMu.RLock()
|
||||||
|
ctl := svr.ctl
|
||||||
|
svr.ctlMu.RUnlock()
|
||||||
|
|
||||||
|
if ctl == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ctl.vm.GetVisitorCfg(name)
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Service) StatusExporter() StatusExporter {
|
func (svr *Service) StatusExporter() StatusExporter {
|
||||||
return &statusExporterImpl{
|
return &statusExporterImpl{
|
||||||
getProxyStatusFunc: svr.getProxyStatus,
|
getProxyStatusFunc: svr.getProxyStatus,
|
||||||
@@ -453,3 +481,35 @@ type statusExporterImpl struct {
|
|||||||
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
return s.getProxyStatusFunc(name)
|
return s.getProxyStatusFunc(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svr *Service) reloadConfigFromSources() error {
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
defer svr.reloadMu.Unlock()
|
||||||
|
return svr.reloadConfigFromSourcesLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) reloadConfigFromSourcesLocked() error {
|
||||||
|
aggregator := svr.aggregator
|
||||||
|
if aggregator == nil {
|
||||||
|
return errors.New("config aggregator is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.cfgMu.RLock()
|
||||||
|
reloadCommon := svr.reloadCommon
|
||||||
|
svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
proxies, visitors, err := aggregator.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reload config from sources failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
||||||
|
proxies = config.CompleteProxyConfigurers(proxies)
|
||||||
|
visitors = config.CompleteVisitorConfigurers(visitors)
|
||||||
|
|
||||||
|
// Atomically replace the entire configuration
|
||||||
|
if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
246
client/service_test.go
Normal file
246
client/service_test.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type failingConnector struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Open() error {
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Connect() (net.Conn, error) {
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFreeTCPPort(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen on ephemeral port: %v", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
return ln.Addr().(*net.TCPAddr).Port
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunStopsStartedComponentsOnInitialLoginFailure(t *testing.T) {
|
||||||
|
port := getFreeTCPPort(t)
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
|
||||||
|
svr, err := NewService(ServiceOptions{
|
||||||
|
Common: &v1.ClientCommonConfig{
|
||||||
|
LoginFailExit: lo.ToPtr(true),
|
||||||
|
WebServer: v1.WebServerConfig{
|
||||||
|
Addr: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigSourceAggregator: agg,
|
||||||
|
ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector {
|
||||||
|
return &failingConnector{err: errors.New("login boom")}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svr.Run(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected run error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "login boom") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if svr.webServer != nil {
|
||||||
|
t.Fatal("expected web server to be cleaned up after initial login failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected admin port to be released: %v", err)
|
||||||
|
}
|
||||||
|
_ = ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewServiceDoesNotLeakAdminListenerOnAuthBuildFailure(t *testing.T) {
|
||||||
|
port := getFreeTCPPort(t)
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
|
||||||
|
_, err := NewService(ServiceOptions{
|
||||||
|
Common: &v1.ClientCommonConfig{
|
||||||
|
Auth: v1.AuthClientConfig{
|
||||||
|
Method: v1.AuthMethodOIDC,
|
||||||
|
OIDC: v1.AuthOIDCClientConfig{
|
||||||
|
TokenEndpointURL: "://bad",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WebServer: v1.WebServerConfig{
|
||||||
|
Addr: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigSourceAggregator: agg,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected new service error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "auth.oidc.tokenEndpointURL") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected admin port to remain free: %v", err)
|
||||||
|
}
|
||||||
|
_ = ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
||||||
|
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||||
|
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||||
|
|
||||||
|
svr := &Service{
|
||||||
|
configSource: source.NewConfigSource(),
|
||||||
|
reloadCommon: prevCommon,
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidProxy := &v1.TCPProxyConfig{}
|
||||||
|
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "proxy name cannot be empty") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.reloadCommon != prevCommon {
|
||||||
|
t.Fatalf("reloadCommon should roll back on ReplaceAll failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) {
|
||||||
|
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||||
|
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||||
|
|
||||||
|
svr := &Service{
|
||||||
|
// Keep configSource valid so ReplaceAll succeeds first.
|
||||||
|
configSource: source.NewConfigSource(),
|
||||||
|
reloadCommon: prevCommon,
|
||||||
|
// Keep aggregator nil to force reload failure.
|
||||||
|
aggregator: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
validProxy := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "p1",
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "config aggregator is not initialized") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.reloadCommon != newCommon {
|
||||||
|
t.Fatalf("reloadCommon should keep new value on reload failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "store-proxy",
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
visitorCfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "store-visitor",
|
||||||
|
Type: "stcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := storeSource.AddProxy(proxyCfg); err != nil {
|
||||||
|
t.Fatalf("add proxy to store: %v", err)
|
||||||
|
}
|
||||||
|
if err := storeSource.AddVisitor(visitorCfg); err != nil {
|
||||||
|
t.Fatalf("add visitor to store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
svr := &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svr.reloadConfigFromSources(); err != nil {
|
||||||
|
t.Fatalf("reload config from sources: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotProxy := storeSource.GetProxy("store-proxy")
|
||||||
|
if gotProxy == nil {
|
||||||
|
t.Fatalf("proxy not found in store")
|
||||||
|
}
|
||||||
|
if gotProxy.GetBaseConfig().LocalIP != "" {
|
||||||
|
t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotVisitor := storeSource.GetVisitor("store-visitor")
|
||||||
|
if gotVisitor == nil {
|
||||||
|
t.Fatalf("visitor not found in store")
|
||||||
|
}
|
||||||
|
if gotVisitor.GetBaseConfig().BindAddr != "" {
|
||||||
|
t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.cfgMu.RLock()
|
||||||
|
defer svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
if len(svr.proxyCfgs) != 1 {
|
||||||
|
t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs))
|
||||||
|
}
|
||||||
|
if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" {
|
||||||
|
t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(svr.visitorCfgs) != 1 {
|
||||||
|
t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs))
|
||||||
|
}
|
||||||
|
if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" {
|
||||||
|
t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,17 +15,12 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
libio "github.com/fatedier/golib/io"
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/util/util"
|
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,10 +36,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.worker()
|
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.internalConnWorker()
|
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
||||||
|
|
||||||
if sv.plugin != nil {
|
if sv.plugin != nil {
|
||||||
sv.plugin.Start()
|
sv.plugin.Start()
|
||||||
@@ -56,35 +51,10 @@ func (sv *STCPVisitor) Close() {
|
|||||||
sv.BaseVisitor.Close()
|
sv.BaseVisitor.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *STCPVisitor) worker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("stcp local listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *STCPVisitor) internalConnWorker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.internalLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("stcp internal listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
var tunnelErr error
|
var tunnelErr error
|
||||||
defer func() {
|
defer func() {
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
if tunnelErr != nil {
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
@@ -95,61 +65,21 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
xl.Warnf("dialRawVisitorConn error: %v", err)
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
|
|
||||||
now := time.Now().Unix()
|
remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig())
|
||||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
|
||||||
RunID: sv.helper.RunID(),
|
|
||||||
ProxyName: sv.cfg.ServerName,
|
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
|
||||||
Timestamp: now,
|
|
||||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
|
||||||
UseCompression: sv.cfg.Transport.UseCompression,
|
|
||||||
}
|
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("wrapVisitorConn error: %v", err)
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
|
||||||
tunnelErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
|
||||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote io.ReadWriteCloser
|
|
||||||
remote = visitorConn
|
|
||||||
if sv.cfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
tunnelErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
var recycleFn func()
|
|
||||||
remote, recycleFn = libio.WithCompressionFromPool(remote)
|
|
||||||
defer recycleFn()
|
defer recycleFn()
|
||||||
}
|
|
||||||
|
|
||||||
libio.Join(userConn, remote)
|
libio.Join(userConn, remote)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,20 +16,17 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +72,7 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
visitorConn net.Conn
|
visitorConn net.Conn
|
||||||
|
recycleFn func()
|
||||||
err error
|
err error
|
||||||
|
|
||||||
firstPacket *msg.UDPPacket
|
firstPacket *msg.UDPPacket
|
||||||
@@ -92,14 +90,17 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
visitorConn, err = sv.getNewVisitorConn()
|
visitorConn, recycleFn, err = sv.getNewVisitorConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitorConn always be closed when worker done.
|
// visitorConn always be closed when worker done.
|
||||||
|
func() {
|
||||||
|
defer recycleFn()
|
||||||
sv.worker(visitorConn, firstPacket)
|
sv.worker(visitorConn, firstPacket)
|
||||||
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sv.checkCloseCh:
|
case <-sv.checkCloseCh:
|
||||||
@@ -146,7 +147,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
sv.readCh <- m
|
sv.readCh <- m
|
||||||
xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content)
|
xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content))
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed")
|
xl.Infof("reader goroutine for udp work connection closed")
|
||||||
return
|
return
|
||||||
@@ -168,7 +169,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn: %s", firstPacket.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content))
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -183,7 +184,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn: %s", udpMsg.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content))
|
||||||
case <-closeCh:
|
case <-closeCh:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -197,52 +198,17 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Infof("sudp worker is closed")
|
xl.Infof("sudp worker is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("frpc connect frps error: %v", err)
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig())
|
||||||
now := time.Now().Unix()
|
|
||||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
|
||||||
RunID: sv.helper.RunID(),
|
|
||||||
ProxyName: sv.cfg.ServerName,
|
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
|
||||||
Timestamp: now,
|
|
||||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
|
||||||
UseCompression: sv.cfg.Transport.UseCompression,
|
|
||||||
}
|
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
|
rawConn.Close()
|
||||||
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
|
|
||||||
}
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
|
||||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote io.ReadWriteCloser
|
|
||||||
remote = visitorConn
|
|
||||||
if sv.cfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
remote = libio.WithCompression(remote)
|
|
||||||
}
|
|
||||||
return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) Close() {
|
func (sv *SUDPVisitor) Close() {
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/naming"
|
||||||
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
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/util"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
@@ -30,7 +38,7 @@ import (
|
|||||||
// Helper wraps some functions for visitor to use.
|
// Helper wraps some functions for visitor to use.
|
||||||
type Helper interface {
|
type Helper interface {
|
||||||
// ConnectServer directly connects to the frp server.
|
// ConnectServer directly connects to the frp server.
|
||||||
ConnectServer() (net.Conn, error)
|
ConnectServer() (*msg.Conn, error)
|
||||||
// TransferConn transfers the connection to another visitor.
|
// TransferConn transfers the connection to another visitor.
|
||||||
TransferConn(string, net.Conn) error
|
TransferConn(string, net.Conn) error
|
||||||
// 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
|
||||||
@@ -119,6 +127,18 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
|||||||
return v.internalLn.PutConn(conn)
|
return v.internalLn.PutConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
||||||
|
xl := xlog.FromContextSafe(v.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("%s listener closed", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *BaseVisitor) Close() {
|
func (v *BaseVisitor) Close() {
|
||||||
if v.l != nil {
|
if v.l != nil {
|
||||||
v.l.Close()
|
v.l.Close()
|
||||||
@@ -130,3 +150,57 @@ func (v *BaseVisitor) Close() {
|
|||||||
v.plugin.Close()
|
v.plugin.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
||||||
|
visitorConn, err := v.helper.ConnectServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
||||||
|
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||||
|
RunID: v.helper.RunID(),
|
||||||
|
ProxyName: targetProxyName,
|
||||||
|
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
UseEncryption: cfg.Transport.UseEncryption,
|
||||||
|
UseCompression: cfg.Transport.UseCompression,
|
||||||
|
}
|
||||||
|
err = visitorConn.WriteMsg(newVisitorConnMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
err = visitorConn.ReadMsgInto(&newVisitorConnRespMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||||
|
}
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
}
|
||||||
|
return visitorConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
||||||
|
rwc := conn
|
||||||
|
if cfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recycleFn := func() {}
|
||||||
|
if cfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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/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"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
@@ -49,7 +50,7 @@ func NewManager(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
runID string,
|
runID string,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
connectServer func() (net.Conn, error),
|
connectServer func() (*msg.Conn, error),
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
@@ -191,15 +192,22 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
|||||||
return v.AcceptConn(conn)
|
return v.AcceptConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vm *Manager) GetVisitorCfg(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
vm.mu.RLock()
|
||||||
|
defer vm.mu.RUnlock()
|
||||||
|
cfg, ok := vm.cfgs[name]
|
||||||
|
return cfg, ok
|
||||||
|
}
|
||||||
|
|
||||||
type visitorHelperImpl struct {
|
type visitorHelperImpl struct {
|
||||||
connectServerFn func() (net.Conn, error)
|
connectServerFn func() (*msg.Conn, error)
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
transferConnFn func(name string, conn net.Conn) error
|
transferConnFn func(name string, conn net.Conn) error
|
||||||
runID string
|
runID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitorHelperImpl) ConnectServer() (net.Conn, error) {
|
func (v *visitorHelperImpl) ConnectServer() (*msg.Conn, error) {
|
||||||
return v.connectServerFn()
|
return v.connectServerFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
"github.com/fatedier/frp/pkg/nathole"
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
"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"
|
||||||
@@ -64,10 +65,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.worker()
|
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.internalConnWorker()
|
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
||||||
go sv.processTunnelStartEvents()
|
go sv.processTunnelStartEvents()
|
||||||
if sv.cfg.KeepTunnelOpen {
|
if sv.cfg.KeepTunnelOpen {
|
||||||
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)
|
||||||
@@ -92,30 +93,6 @@ func (sv *XTCPVisitor) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *XTCPVisitor) worker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("xtcp local listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *XTCPVisitor) internalConnWorker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.internalLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("xtcp internal listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -205,20 +182,14 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
|
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
||||||
if sv.cfg.Transport.UseEncryption {
|
|
||||||
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("%v", err)
|
||||||
|
tunnelConn.Close()
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
var recycleFn func()
|
|
||||||
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
|
||||||
defer recycleFn()
|
defer recycleFn()
|
||||||
}
|
|
||||||
|
|
||||||
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||||
xl.Debugf("join connections closed")
|
xl.Debugf("join connections closed")
|
||||||
@@ -280,8 +251,9 @@ func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
|||||||
// 4. Create a tunnel session using an underlying UDP connection.
|
// 4. Create a tunnel session using an underlying UDP connection.
|
||||||
func (sv *XTCPVisitor) makeNatHole() {
|
func (sv *XTCPVisitor) makeNatHole() {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||||
xl.Tracef("makeNatHole start")
|
xl.Tracef("makeNatHole start")
|
||||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
|
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil {
|
||||||
xl.Warnf("nathole precheck error: %v", err)
|
xl.Warnf("nathole precheck error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -310,7 +282,7 @@ func (sv *XTCPVisitor) makeNatHole() {
|
|||||||
transactionID := nathole.NewTransactionID()
|
transactionID := nathole.NewTransactionID()
|
||||||
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
||||||
TransactionID: transactionID,
|
TransactionID: transactionID,
|
||||||
ProxyName: sv.cfg.ServerName,
|
ProxyName: targetProxyName,
|
||||||
Protocol: sv.cfg.Protocol,
|
Protocol: sv.cfg.Protocol,
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
@@ -371,6 +343,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
|||||||
}
|
}
|
||||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
lConn.Close()
|
||||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
Use: "discover",
|
Use: "discover",
|
||||||
Short: "Discover nathole information from stun server",
|
Short: "Discover nathole information from stun server",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// ignore error here, because we can use command line pameters
|
// ignore error here, because we can use command line parameters
|
||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg = &v1.ClientCommonConfig{}
|
cfg = &v1.ClientCommonConfig{}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
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/policy/security"
|
||||||
@@ -86,13 +87,14 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Complete(clientCfg.User)
|
|
||||||
c.GetBaseConfig().Type = name
|
c.GetBaseConfig().Type = name
|
||||||
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
|
c.Complete()
|
||||||
|
proxyCfg := c
|
||||||
|
if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -117,13 +119,14 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Complete(clientCfg)
|
|
||||||
c.GetBaseConfig().Type = name
|
c.GetBaseConfig().Type = name
|
||||||
if err := validation.ValidateVisitorConfigurer(c); err != nil {
|
c.Complete()
|
||||||
|
visitorCfg := c
|
||||||
|
if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -131,3 +134,18 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startService(
|
||||||
|
cfg *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
|
cfgFile string,
|
||||||
|
) error {
|
||||||
|
configSource := source.NewConfigSource()
|
||||||
|
if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
return fmt.Errorf("failed to set config source: %w", err)
|
||||||
|
}
|
||||||
|
aggregator := source.NewAggregator(configSource)
|
||||||
|
return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile)
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/client"
|
"github.com/fatedier/frp/client"
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
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/featuregate"
|
||||||
@@ -120,22 +121,64 @@ func handleTermSignal(svr *client.Service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
// Load configuration
|
||||||
|
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if isLegacyFormat {
|
if result.IsLegacyFormat {
|
||||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||||
"please use yaml/json/toml format instead!\n")
|
"please use yaml/json/toml format instead!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.FeatureGates) > 0 {
|
if len(result.Common.FeatureGates) > 0 {
|
||||||
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
return runClientWithAggregator(result, unsafeFeatures, cfgFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runClientWithAggregator runs the client using the internal source aggregator.
|
||||||
|
func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error {
|
||||||
|
configSource := source.NewConfigSource()
|
||||||
|
if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
||||||
|
return fmt.Errorf("failed to set config source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storeSource *source.StoreSource
|
||||||
|
|
||||||
|
if result.Common.Store.IsEnabled() {
|
||||||
|
storePath := result.Common.Store.Path
|
||||||
|
if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) {
|
||||||
|
storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: storePath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create store source: %w", err)
|
||||||
|
}
|
||||||
|
storeSource = s
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregator := source.NewAggregator(configSource)
|
||||||
|
if storeSource != nil {
|
||||||
|
aggregator.SetStoreSource(storeSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs, err := aggregator.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config from sources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||||
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||||
|
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||||
|
|
||||||
|
warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
@@ -143,26 +186,24 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
func startServiceWithAggregator(
|
||||||
cfg *v1.ClientCommonConfig,
|
cfg *v1.ClientCommonConfig,
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
aggregator *source.Aggregator,
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
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)
|
||||||
|
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
log.Infof("start frpc service for config file [%s]", cfgFile)
|
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
|
||||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||||
}
|
}
|
||||||
svr, err := client.NewService(client.ServiceOptions{
|
svr, err := client.NewService(client.ServiceOptions{
|
||||||
Common: cfg,
|
Common: cfg,
|
||||||
ProxyCfgs: proxyCfgs,
|
ConfigSourceAggregator: aggregator,
|
||||||
VisitorCfgs: visitorCfgs,
|
|
||||||
UnsafeFeatures: unsafeFeatures,
|
UnsafeFeatures: unsafeFeatures,
|
||||||
ConfigFilePath: cfgFile,
|
ConfigFilePath: cfgFile,
|
||||||
})
|
})
|
||||||
@@ -171,7 +212,6 @@ func startService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
||||||
// Capture the exit signal if we use kcp or quic.
|
|
||||||
if shouldGracefulClose {
|
if shouldGracefulClose {
|
||||||
go handleTermSignal(svr)
|
go handleTermSignal(svr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ transport.poolCount = 5
|
|||||||
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||||
transport.protocol = "tcp"
|
transport.protocol = "tcp"
|
||||||
|
|
||||||
|
# FRP wire protocol used inside the selected transport.
|
||||||
|
# supports v1 and v2, default is v1. v2 requires frps support and must be enabled explicitly.
|
||||||
|
# transport.wireProtocol = "v1"
|
||||||
|
|
||||||
# set client binding ip when connect server, default is empty.
|
# set client binding ip when connect server, default is empty.
|
||||||
# only when protocol = tcp or websocket, the value will be used.
|
# only when protocol = tcp or websocket, the value will be used.
|
||||||
transport.connectServerLocalIP = "0.0.0.0"
|
transport.connectServerLocalIP = "0.0.0.0"
|
||||||
@@ -143,6 +147,9 @@ transport.tls.enable = true
|
|||||||
|
|
||||||
# Proxy names you want to start.
|
# Proxy names you want to start.
|
||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
|
# This list is a global allowlist after config + store are merged, so entries
|
||||||
|
# created via Store API are also filtered by this list.
|
||||||
|
# If start is non-empty, any proxy/visitor not listed here will not be started.
|
||||||
# start = ["ssh", "dns"]
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||||
|
|||||||
80
doc/agents/release.md
Normal file
80
doc/agents/release.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Release Process
|
||||||
|
|
||||||
|
## 1. Update Release Notes
|
||||||
|
|
||||||
|
Edit `Release.md` in the project root with the changes for this version:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Features
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is used by GoReleaser as the GitHub Release body.
|
||||||
|
|
||||||
|
## 2. Bump Version
|
||||||
|
|
||||||
|
Update the version string in `pkg/util/version/version.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var version = "0.X.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit and push to `dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pkg/util/version/version.go Release.md
|
||||||
|
git commit -m "bump version to vX.Y.Z"
|
||||||
|
git push origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Merge dev → master
|
||||||
|
|
||||||
|
Create a PR from `dev` to `master`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr create --base master --head dev --title "bump version"
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for CI to pass, then merge using **merge commit** (not squash).
|
||||||
|
|
||||||
|
## 4. Tag the Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
git pull origin master
|
||||||
|
git tag -a vX.Y.Z -m "bump version"
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Trigger GoReleaser
|
||||||
|
|
||||||
|
Manually trigger the `goreleaser` workflow in GitHub Actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run goreleaser --ref master
|
||||||
|
```
|
||||||
|
|
||||||
|
GoReleaser will:
|
||||||
|
1. Run `package.sh` to cross-compile all platforms and create archives
|
||||||
|
2. Create a GitHub Release with all packages, using `Release.md` as release notes
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `pkg/util/version/version.go` | Version string |
|
||||||
|
| `Release.md` | Release notes (read by GoReleaser) |
|
||||||
|
| `.goreleaser.yml` | GoReleaser config |
|
||||||
|
| `package.sh` | Cross-compile and packaging script |
|
||||||
|
| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) |
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
- Minor release: `v0.X.0`
|
||||||
|
- Patch release: `v0.X.Y` (e.g., `v0.62.1`)
|
||||||
38
doc/deprecations.md
Normal file
38
doc/deprecations.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Deprecations
|
||||||
|
|
||||||
|
This document tracks deprecated features and APIs that are still shipped but scheduled for removal. Maintainers should review this list before each release to decide whether any items are due for removal.
|
||||||
|
|
||||||
|
For the version compatibility policy that bounds these support windows, see the latest `Release.md`.
|
||||||
|
|
||||||
|
## Active
|
||||||
|
|
||||||
|
### Wire protocol v1
|
||||||
|
|
||||||
|
- **Deprecated since:** v0.70.0 (planned, when v2 becomes the default).
|
||||||
|
- **Removal target:** v0.78.0 or later. v0.69.0 (the last release where v1 is the default) is supported until v0.78.0 is released, so v0.77.0 is the last release that must keep v1 support.
|
||||||
|
- **Replacement:** wire protocol v2 (`transport.wireProtocol = "v2"` in frpc).
|
||||||
|
- **Code references:** v1 message types and codec under `pkg/msg/` and the protocol negotiation path in `client/` and `server/`.
|
||||||
|
- **Notes:** Removing v1 will also drop compatibility with any frpc/frps that does not negotiate v2.
|
||||||
|
|
||||||
|
### INI configuration format
|
||||||
|
|
||||||
|
- **Deprecated since:** predates this document; startup warning has been in place for several releases.
|
||||||
|
- **Removal target:** TBD.
|
||||||
|
- **Replacement:** YAML / JSON / TOML.
|
||||||
|
- **Code references:**
|
||||||
|
- `cmd/frpc/sub/root.go` — frpc startup warning.
|
||||||
|
- `cmd/frps/root.go` — frps startup warning.
|
||||||
|
- `pkg/config/legacy/` — legacy INI parser; remove together with the warnings.
|
||||||
|
|
||||||
|
### Visitor connections without `runID`
|
||||||
|
|
||||||
|
- **Deprecated since:** v0.50.0 (when `runID` was introduced).
|
||||||
|
- **Removal target:** TBD.
|
||||||
|
- **Replacement:** require `runID` on every visitor connection.
|
||||||
|
- **Code references:**
|
||||||
|
- `server/service.go` — `RegisterVisitorConn` still accepts empty `runID` for backward compatibility.
|
||||||
|
- **Notes:** Removal will break frpc clients released before v0.50.0. Schedule for a release where dropping pre-v0.50.0 frpc is acceptable.
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
_None yet._
|
||||||
BIN
doc/pic/architecture.jpg
Normal file
BIN
doc/pic/architecture.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,11 +1,14 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
WORKDIR /web/frpc
|
COPY web/package.json /web/package.json
|
||||||
COPY web/frpc/ ./
|
COPY web/shared/ /web/shared/
|
||||||
|
COPY web/frpc/ /web/frpc/
|
||||||
|
WORKDIR /web
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
WORKDIR /web/frpc
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.24 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
WORKDIR /web/frps
|
COPY web/package.json /web/package.json
|
||||||
COPY web/frps/ ./
|
COPY web/shared/ /web/shared/
|
||||||
|
COPY web/frps/ /web/frps/
|
||||||
|
WORKDIR /web
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
WORKDIR /web/frps
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.24 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
||||||
|
|||||||
34
go.mod
34
go.mod
@@ -1,11 +1,11 @@
|
|||||||
module github.com/fatedier/frp
|
module github.com/fatedier/frp
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.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.14.1
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/fatedier/golib v0.5.1
|
github.com/fatedier/golib v0.6.0
|
||||||
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
|
||||||
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/onsi/ginkgo/v2 v2.23.4
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.36.3
|
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/v3 v3.1.1
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/quic-go/quic-go v0.55.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
@@ -22,15 +22,15 @@ require (
|
|||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
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.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tidwall/gjson v1.17.1
|
github.com/tidwall/gjson v1.17.1
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.10.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
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
|
||||||
@@ -38,7 +38,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
@@ -51,10 +51,9 @@ require (
|
|||||||
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/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
@@ -66,11 +65,12 @@ require (
|
|||||||
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
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // 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
|
||||||
|
|||||||
105
go.sum
105
go.sum
@@ -1,6 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
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.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
github.com/fatedier/golib v0.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww=
|
||||||
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
@@ -78,16 +78,14 @@ github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
|||||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
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/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
|
||||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
|
||||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -128,11 +126,10 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
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.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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
||||||
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
github.com/templexxx/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 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
|
||||||
@@ -149,11 +146,12 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
|
|||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
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 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
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/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=
|
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/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 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
@@ -161,89 +159,54 @@ 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
golang.org/x/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-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 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
|
|||||||
111
pkg/auth/oidc.go
111
pkg/auth/oidc.go
@@ -23,12 +23,14 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"golang.org/x/oauth2"
|
"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/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,14 +76,64 @@ func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyUR
|
|||||||
return &http.Client{Transport: transport}, nil
|
return &http.Client{Transport: transport}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nonCachingTokenSource wraps a clientcredentials.Config to fetch a fresh
|
||||||
|
// token on every call. This is used as a fallback when the OIDC provider
|
||||||
|
// does not return expires_in, which would cause a caching TokenSource to
|
||||||
|
// hold onto a stale token forever.
|
||||||
|
type nonCachingTokenSource struct {
|
||||||
|
cfg *clientcredentials.Config
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
return s.cfg.Token(s.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oidcTokenSource wraps a caching oauth2.TokenSource and, on the first
|
||||||
|
// successful Token() call, checks whether the provider returns an expiry.
|
||||||
|
// If not, it permanently switches to nonCachingTokenSource so that a fresh
|
||||||
|
// token is fetched every time. This avoids an eager network call at
|
||||||
|
// construction time, letting the login retry loop handle transient IdP
|
||||||
|
// outages.
|
||||||
|
type oidcTokenSource struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
initialized bool
|
||||||
|
source oauth2.TokenSource
|
||||||
|
fallbackCfg *clientcredentials.Config
|
||||||
|
fallbackCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *oidcTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if !s.initialized {
|
||||||
|
token, err := s.source.Token()
|
||||||
|
if err != nil {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token.Expiry.IsZero() {
|
||||||
|
s.source = &nonCachingTokenSource{cfg: s.fallbackCfg, ctx: s.fallbackCtx}
|
||||||
|
}
|
||||||
|
s.initialized = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
source := s.source
|
||||||
|
s.mu.Unlock()
|
||||||
|
return source.Token()
|
||||||
|
}
|
||||||
|
|
||||||
type OidcAuthProvider struct {
|
type OidcAuthProvider struct {
|
||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
tokenGenerator *clientcredentials.Config
|
tokenSource oauth2.TokenSource
|
||||||
httpClient *http.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||||
|
if err := validation.ValidateOIDCClientCredentialsConfig(&cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
||||||
@@ -99,30 +151,42 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
|||||||
EndpointParams: eps,
|
EndpointParams: eps,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create custom HTTP client if needed
|
// Build the context that TokenSource will use for all future HTTP requests.
|
||||||
var httpClient *http.Client
|
// context.Background() is appropriate here because the token source is
|
||||||
|
// long-lived and outlives any single request.
|
||||||
|
ctx := context.Background()
|
||||||
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
||||||
var err error
|
httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
||||||
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a persistent TokenSource that caches the token and refreshes
|
||||||
|
// it before expiry. This avoids making a new HTTP request to the OIDC
|
||||||
|
// provider on every heartbeat/ping.
|
||||||
|
//
|
||||||
|
// We wrap it in an oidcTokenSource so that the first Token() call
|
||||||
|
// (deferred to SetLogin inside the login retry loop) probes whether the
|
||||||
|
// provider returns expires_in. If not, it switches to a non-caching
|
||||||
|
// source. This avoids an eager network call at construction time, which
|
||||||
|
// would prevent loopLoginUntilSuccess from retrying on transient IdP
|
||||||
|
// outages.
|
||||||
|
cachingSource := tokenGenerator.TokenSource(ctx)
|
||||||
|
|
||||||
return &OidcAuthProvider{
|
return &OidcAuthProvider{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
tokenGenerator: tokenGenerator,
|
tokenSource: &oidcTokenSource{
|
||||||
httpClient: httpClient,
|
source: cachingSource,
|
||||||
|
fallbackCfg: tokenGenerator,
|
||||||
|
fallbackCtx: ctx,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||||
ctx := context.Background()
|
tokenObj, err := auth.tokenSource.Token()
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -205,7 +269,8 @@ type OidcAuthConsumer struct {
|
|||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
verifier TokenVerifier
|
verifier TokenVerifier
|
||||||
subjectsFromLogin []string
|
mu sync.RWMutex
|
||||||
|
subjectsFromLogin map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||||
@@ -226,7 +291,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri
|
|||||||
return &OidcAuthConsumer{
|
return &OidcAuthConsumer{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
subjectsFromLogin: []string{},
|
subjectsFromLogin: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +300,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)
|
||||||
}
|
}
|
||||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
auth.mu.Lock()
|
||||||
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
auth.subjectsFromLogin[token.Subject] = struct{}{}
|
||||||
}
|
auth.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +311,13 @@ 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 !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
auth.mu.RLock()
|
||||||
|
_, ok := auth.subjectsFromLogin[token.Subject]
|
||||||
|
auth.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||||
"original subjects: %s, "+
|
|
||||||
"new subject: %s",
|
"new subject: %s",
|
||||||
auth.subjectsFromLogin, token.Subject)
|
token.Subject)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package auth_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -62,3 +66,188 @@ func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
|||||||
r.Error(err)
|
r.Error(err)
|
||||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
requestCount.Add(1)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "fresh-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// Constructor no longer fetches a token eagerly.
|
||||||
|
// The first SetLogin triggers the adaptive probe.
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("fresh-test-token", loginMsg.PrivilegeKey)
|
||||||
|
|
||||||
|
for range 3 {
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("fresh-test-token", pingMsg.PrivilegeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch)
|
||||||
|
r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
requestCount.Add(1)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "cached-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// Constructor no longer fetches eagerly; first SetLogin triggers the probe.
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
// SetLogin triggers the adaptive probe and caches the token.
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("cached-test-token", loginMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(1), requestCount.Load())
|
||||||
|
|
||||||
|
// Subsequent calls should also reuse the cached token
|
||||||
|
for range 5 {
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("cached-test-token", pingMsg.PrivilegeKey)
|
||||||
|
}
|
||||||
|
r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
n := requestCount.Add(1)
|
||||||
|
// The oauth2 library retries once internally, so we need two
|
||||||
|
// consecutive failures to surface an error to the caller.
|
||||||
|
if n <= 2 {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"error": "temporarily_unavailable",
|
||||||
|
"error_description": "service is starting up",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "retry-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
// Constructor succeeds even though the IdP is "down".
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
// First SetLogin hits the IdP, which returns an error (after internal retry).
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.Error(err)
|
||||||
|
r.Equal(int32(2), requestCount.Load())
|
||||||
|
|
||||||
|
// Second SetLogin retries and succeeds.
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("retry-test-token", loginMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(3), requestCount.Load())
|
||||||
|
|
||||||
|
// Subsequent calls use cached token.
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("retry-test-token", pingMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(3), requestCount.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
tokenServer := httptest.NewServer(http.NotFoundHandler())
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
_, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: "://bad",
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.tokenEndpointURL")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.clientID is required")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
AdditionalEndpointParams: map[string]string{
|
||||||
|
"scope": "profile",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
Audience: "api",
|
||||||
|
AdditionalEndpointParams: map[string]string{"audience": "override"},
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,16 +171,15 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
|||||||
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
||||||
out := v1.HeaderOperations{}
|
out := v1.HeaderOperations{}
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
if !strings.HasPrefix(k, "plugin_header_") {
|
k, ok := strings.CutPrefix(k, "plugin_header_")
|
||||||
|
if !ok || k == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
|
|
||||||
if out.Set == nil {
|
if out.Set == nil {
|
||||||
out.Set = make(map[string]string)
|
out.Set = make(map[string]string)
|
||||||
}
|
}
|
||||||
out.Set[k] = v
|
out.Set[k] = v
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ const (
|
|||||||
// Proxy
|
// Proxy
|
||||||
var (
|
var (
|
||||||
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
|
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](),
|
||||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
|
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](),
|
||||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
|
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](),
|
||||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
|
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](),
|
||||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
|
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](),
|
||||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
|
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](),
|
||||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
|
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](),
|
||||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
|
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string
|
|||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
|
|
||||||
for key, value := range set {
|
for key, value := range set {
|
||||||
if strings.HasPrefix(key, prefix) {
|
if trimmed, ok := strings.CutPrefix(key, prefix); ok {
|
||||||
m[strings.TrimPrefix(key, prefix)] = value
|
m[trimmed] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const (
|
|||||||
// Visitor
|
// Visitor
|
||||||
var (
|
var (
|
||||||
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
|
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](),
|
||||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
|
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](),
|
||||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
|
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -33,6 +34,7 @@ import (
|
|||||||
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/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +110,21 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return LoadConfigure(content, c, strict)
|
return LoadConfigure(content, c, strict, detectFormatFromPath(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectFormatFromPath returns a format hint based on the file extension.
|
||||||
|
func detectFormatFromPath(path string) string {
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".toml":
|
||||||
|
return "toml"
|
||||||
|
case ".yaml", ".yml":
|
||||||
|
return "yaml"
|
||||||
|
case ".json":
|
||||||
|
return "json"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
||||||
@@ -129,48 +145,136 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON and decode with strict validation
|
// Convert to JSON and decode with strict validation
|
||||||
jsonBytes, err := json.Marshal(temp)
|
jsonBytes, err := jsonx.Marshal(temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
return decodeJSONContent(jsonBytes, target, true)
|
||||||
decoder.DisallowUnknownFields()
|
}
|
||||||
return decoder.Decode(target)
|
|
||||||
|
func decodeJSONContent(content []byte, target any, strict bool) error {
|
||||||
|
if clientCfg, ok := target.(*v1.ClientConfig); ok {
|
||||||
|
decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{
|
||||||
|
DisallowUnknownFields: strict,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*clientCfg = decoded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: strict,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
// 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 {
|
// An optional format hint (e.g. "toml", "yaml", "json") can be provided
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
// to enable better error messages with line number information.
|
||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
func LoadConfigure(b []byte, c any, strict bool, formats ...string) error {
|
||||||
v1.DisallowUnknownFields = strict
|
format := ""
|
||||||
|
if len(formats) > 0 {
|
||||||
|
format = formats[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
originalBytes := b
|
||||||
|
parsedFromTOML := false
|
||||||
|
|
||||||
var tomlObj any
|
var tomlObj any
|
||||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
tomlErr := toml.Unmarshal(b, &tomlObj)
|
||||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
if tomlErr == nil {
|
||||||
b, err = json.Marshal(&tomlObj)
|
parsedFromTOML = true
|
||||||
|
var err error
|
||||||
|
b, err = jsonx.Marshal(&tomlObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if format == "toml" {
|
||||||
|
// File is known to be TOML but has syntax errors.
|
||||||
|
return formatTOMLError(tomlErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
||||||
if yaml.IsJSONBuffer(b) {
|
if yaml.IsJSONBuffer(b) {
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
if err := decodeJSONContent(b, c, strict); err != nil {
|
||||||
if strict {
|
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
}
|
||||||
return decoder.Decode(c)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle YAML content
|
// Handle YAML content
|
||||||
if strict {
|
if strict {
|
||||||
// In strict mode, always use our custom handler to support YAML merge
|
// In strict mode, always use our custom handler to support YAML merge
|
||||||
return parseYAMLWithDotFieldsHandling(b, c)
|
if err := parseYAMLWithDotFieldsHandling(b, c); err != nil {
|
||||||
|
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
// Non-strict mode, parse normally
|
// Non-strict mode, parse normally
|
||||||
return yaml.Unmarshal(b, c)
|
return yaml.Unmarshal(b, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatTOMLError extracts line/column information from TOML decode errors.
|
||||||
|
func formatTOMLError(err error) error {
|
||||||
|
var decErr *toml.DecodeError
|
||||||
|
if errors.As(err, &decErr) {
|
||||||
|
row, col := decErr.Position()
|
||||||
|
return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error())
|
||||||
|
}
|
||||||
|
var strictErr *toml.StrictMissingError
|
||||||
|
if errors.As(err, &strictErr) {
|
||||||
|
return strictErr
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors.
|
||||||
|
func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error {
|
||||||
|
var typeErr *json.UnmarshalTypeError
|
||||||
|
if errors.As(err, &typeErr) && typeErr.Field != "" {
|
||||||
|
if includeLine {
|
||||||
|
line := findFieldLineInContent(originalContent, typeErr.Field)
|
||||||
|
if line > 0 {
|
||||||
|
return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// findFieldLineInContent searches the original config content for a field name
|
||||||
|
// and returns the 1-indexed line number where it appears, or 0 if not found.
|
||||||
|
func findFieldLineInContent(content []byte, fieldPath string) int {
|
||||||
|
if fieldPath == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the last component of the field path (e.g. "proxies" from "proxies" or
|
||||||
|
// "protocol" from "transport.protocol").
|
||||||
|
parts := strings.Split(fieldPath, ".")
|
||||||
|
searchKey := parts[len(parts)-1]
|
||||||
|
|
||||||
|
lines := bytes.Split(content, []byte("\n"))
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := bytes.TrimSpace(line)
|
||||||
|
// Match TOML key assignments like: key = ...
|
||||||
|
if bytes.HasPrefix(trimmed, []byte(searchKey)) {
|
||||||
|
rest := bytes.TrimSpace(trimmed[len(searchKey):])
|
||||||
|
if len(rest) > 0 && rest[0] == '=' {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match TOML table array headers like: [[proxies]]
|
||||||
|
if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||||
m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
|
m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
|
||||||
|
|
||||||
@@ -180,7 +284,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
|
|||||||
}
|
}
|
||||||
|
|
||||||
configurer.UnmarshalFromMsg(m)
|
configurer.UnmarshalFromMsg(m)
|
||||||
configurer.Complete("")
|
configurer.Complete()
|
||||||
|
|
||||||
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -219,60 +323,132 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
|
|||||||
return svrCfg, isLegacyFormat, nil
|
return svrCfg, isLegacyFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientConfigLoadResult contains the result of loading a client configuration file.
|
||||||
|
type ClientConfigLoadResult struct {
|
||||||
|
// Common contains the common client configuration.
|
||||||
|
Common *v1.ClientCommonConfig
|
||||||
|
|
||||||
|
// Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles.
|
||||||
|
// These are NOT completed (user prefix not added).
|
||||||
|
Proxies []v1.ProxyConfigurer
|
||||||
|
|
||||||
|
// Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles.
|
||||||
|
// These are NOT completed.
|
||||||
|
Visitors []v1.VisitorConfigurer
|
||||||
|
|
||||||
|
// IsLegacyFormat indicates whether the config file is in legacy INI format.
|
||||||
|
IsLegacyFormat bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadClientConfigResult loads and parses a client configuration file.
|
||||||
|
// It returns the raw configuration without completing proxies/visitors.
|
||||||
|
// The caller should call Complete on the configs manually for legacy behavior.
|
||||||
|
func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) {
|
||||||
|
result := &ClientConfigLoadResult{
|
||||||
|
Proxies: make([]v1.ProxyConfigurer, 0),
|
||||||
|
Visitors: make([]v1.VisitorConfigurer, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if DetectLegacyINIFormatFromFile(path) {
|
||||||
|
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||||
|
for _, c := range legacyProxyCfgs {
|
||||||
|
result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c))
|
||||||
|
}
|
||||||
|
for _, c := range legacyVisitorCfgs {
|
||||||
|
result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c))
|
||||||
|
}
|
||||||
|
result.IsLegacyFormat = true
|
||||||
|
} else {
|
||||||
|
allCfg := v1.ClientConfig{}
|
||||||
|
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.Common = &allCfg.ClientCommonConfig
|
||||||
|
for _, c := range allCfg.Proxies {
|
||||||
|
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
||||||
|
}
|
||||||
|
for _, c := range allCfg.Visitors {
|
||||||
|
result.Visitors = append(result.Visitors, c.VisitorConfigurer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load additional config from includes.
|
||||||
|
// legacy ini format already handle this in ParseClientConfig.
|
||||||
|
if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat {
|
||||||
|
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.Proxies = append(result.Proxies, extProxyCfgs...)
|
||||||
|
result.Visitors = append(result.Visitors, extVisitorCfgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the common config
|
||||||
|
if result.Common != nil {
|
||||||
|
if err := result.Common.Complete(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoadClientConfig(path string, strict bool) (
|
func LoadClientConfig(path string, strict bool) (
|
||||||
*v1.ClientCommonConfig,
|
*v1.ClientCommonConfig,
|
||||||
[]v1.ProxyConfigurer,
|
[]v1.ProxyConfigurer,
|
||||||
[]v1.VisitorConfigurer,
|
[]v1.VisitorConfigurer,
|
||||||
bool, error,
|
bool, error,
|
||||||
) {
|
) {
|
||||||
var (
|
result, err := LoadClientConfigResult(path, strict)
|
||||||
cliCfg *v1.ClientCommonConfig
|
|
||||||
proxyCfgs = make([]v1.ProxyConfigurer, 0)
|
|
||||||
visitorCfgs = make([]v1.VisitorConfigurer, 0)
|
|
||||||
isLegacyFormat bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if DetectLegacyINIFormatFromFile(path) {
|
|
||||||
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, true, err
|
return nil, nil, nil, result != nil && result.IsLegacyFormat, err
|
||||||
}
|
|
||||||
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
|
||||||
for _, c := range legacyProxyCfgs {
|
|
||||||
proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
|
|
||||||
}
|
|
||||||
for _, c := range legacyVisitorCfgs {
|
|
||||||
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
|
|
||||||
}
|
|
||||||
isLegacyFormat = true
|
|
||||||
} else {
|
|
||||||
allCfg := v1.ClientConfig{}
|
|
||||||
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
|
||||||
return nil, nil, nil, false, err
|
|
||||||
}
|
|
||||||
cliCfg = &allCfg.ClientCommonConfig
|
|
||||||
for _, c := range allCfg.Proxies {
|
|
||||||
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
|
||||||
}
|
|
||||||
for _, c := range allCfg.Visitors {
|
|
||||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load additional config from includes.
|
proxyCfgs := result.Proxies
|
||||||
// legacy ini format already handle this in ParseClientConfig.
|
visitorCfgs := result.Visitors
|
||||||
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
|
|
||||||
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
|
proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||||
if err != nil {
|
proxyCfgs = CompleteProxyConfigurers(proxyCfgs)
|
||||||
return nil, nil, nil, isLegacyFormat, err
|
visitorCfgs = CompleteVisitorConfigurers(visitorCfgs)
|
||||||
}
|
return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil
|
||||||
proxyCfgs = append(proxyCfgs, extProxyCfgs...)
|
|
||||||
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by start
|
func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer {
|
||||||
if len(cliCfg.Start) > 0 {
|
proxyCfgs := proxies
|
||||||
startSet := sets.New(cliCfg.Start...)
|
for _, c := range proxyCfgs {
|
||||||
|
c.Complete()
|
||||||
|
}
|
||||||
|
return proxyCfgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer {
|
||||||
|
visitorCfgs := visitors
|
||||||
|
for _, c := range visitorCfgs {
|
||||||
|
c.Complete()
|
||||||
|
}
|
||||||
|
return visitorCfgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterClientConfigurers(
|
||||||
|
common *v1.ClientCommonConfig,
|
||||||
|
proxies []v1.ProxyConfigurer,
|
||||||
|
visitors []v1.VisitorConfigurer,
|
||||||
|
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||||
|
if common == nil {
|
||||||
|
common = &v1.ClientCommonConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgs := proxies
|
||||||
|
visitorCfgs := visitors
|
||||||
|
|
||||||
|
// Filter by start across merged configurers from all sources.
|
||||||
|
// For example, store entries are also filtered by this set.
|
||||||
|
if len(common.Start) > 0 {
|
||||||
|
startSet := sets.New(common.Start...)
|
||||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||||
return startSet.Has(c.GetBaseConfig().Name)
|
return startSet.Has(c.GetBaseConfig().Name)
|
||||||
})
|
})
|
||||||
@@ -291,19 +467,7 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
enabled := c.GetBaseConfig().Enabled
|
enabled := c.GetBaseConfig().Enabled
|
||||||
return enabled == nil || *enabled
|
return enabled == nil || *enabled
|
||||||
})
|
})
|
||||||
|
return proxyCfgs, visitorCfgs
|
||||||
if cliCfg != nil {
|
|
||||||
if err := cliCfg.Complete(); err != nil {
|
|
||||||
return nil, nil, nil, isLegacyFormat, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, c := range proxyCfgs {
|
|
||||||
c.Complete(cliCfg.User)
|
|
||||||
}
|
|
||||||
for _, c := range visitorCfgs {
|
|
||||||
c.Complete(cliCfg)
|
|
||||||
}
|
|
||||||
return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -188,6 +189,31 @@ unixPath = "/tmp/uds.sock"
|
|||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = 6000
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2https"
|
||||||
|
localAddr = "127.0.0.1:8080"
|
||||||
|
unknownInPlugin = "value"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
err = LoadConfigure([]byte(content), &clientCfg, true)
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
||||||
// even in strict mode by properly handling dot-prefixed fields
|
// even in strict mode by properly handling dot-prefixed fields
|
||||||
func TestYAMLMergeInStrictMode(t *testing.T) {
|
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||||
@@ -273,6 +299,169 @@ proxies:
|
|||||||
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy-raw"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
proxyCfg.Enabled = &enabled
|
||||||
|
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor-raw"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server-raw"
|
||||||
|
visitorCfg.FallbackTo = "fallback-raw"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
visitorCfg.Enabled = &enabled
|
||||||
|
|
||||||
|
common := &v1.ClientCommonConfig{
|
||||||
|
User: "alice",
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Len(visitors, 1)
|
||||||
|
|
||||||
|
p := proxies[0].GetBaseConfig()
|
||||||
|
require.Equal("proxy-raw", p.Name)
|
||||||
|
require.Empty(p.LocalIP)
|
||||||
|
|
||||||
|
v := visitors[0].GetBaseConfig()
|
||||||
|
require.Equal("visitor-raw", v.Name)
|
||||||
|
require.Equal("server-raw", v.ServerName)
|
||||||
|
require.Empty(v.BindAddr)
|
||||||
|
|
||||||
|
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
||||||
|
require.Equal("fallback-raw", xtcp.FallbackTo)
|
||||||
|
require.Empty(xtcp.Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy-raw"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
proxyCfg.Enabled = &enabled
|
||||||
|
|
||||||
|
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
|
||||||
|
p := proxies[0].GetBaseConfig()
|
||||||
|
require.Equal("proxy-raw", p.Name)
|
||||||
|
require.Equal("127.0.0.1", p.LocalIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor-raw"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server-raw"
|
||||||
|
visitorCfg.FallbackTo = "fallback-raw"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
visitorCfg.Enabled = &enabled
|
||||||
|
|
||||||
|
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
||||||
|
require.Len(visitors, 1)
|
||||||
|
|
||||||
|
v := visitors[0].GetBaseConfig()
|
||||||
|
require.Equal("visitor-raw", v.Name)
|
||||||
|
require.Equal("server-raw", v.ServerName)
|
||||||
|
require.Equal("127.0.0.1", v.BindAddr)
|
||||||
|
|
||||||
|
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
||||||
|
require.Equal("fallback-raw", xtcp.FallbackTo)
|
||||||
|
require.Equal("quic", xtcp.Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteProxyConfigurers_Idempotent(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
|
||||||
|
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
||||||
|
firstProxyJSON, err := json.Marshal(proxies[0])
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies = CompleteProxyConfigurers(proxies)
|
||||||
|
secondProxyJSON, err := json.Marshal(proxies[0])
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
require.Equal(string(firstProxyJSON), string(secondProxyJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
|
||||||
|
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
||||||
|
firstVisitorJSON, err := json.Marshal(visitors[0])
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
visitors = CompleteVisitorConfigurers(visitors)
|
||||||
|
secondVisitorJSON, err := json.Marshal(visitors[0])
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
require.Equal(string(firstVisitorJSON), string(secondVisitorJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
disabled := false
|
||||||
|
|
||||||
|
proxyKeep := &v1.TCPProxyConfig{}
|
||||||
|
proxyKeep.Name = "keep"
|
||||||
|
proxyKeep.Type = "tcp"
|
||||||
|
proxyKeep.LocalPort = 10080
|
||||||
|
proxyKeep.Enabled = &enabled
|
||||||
|
|
||||||
|
proxyDropByStart := &v1.TCPProxyConfig{}
|
||||||
|
proxyDropByStart.Name = "drop-by-start"
|
||||||
|
proxyDropByStart.Type = "tcp"
|
||||||
|
proxyDropByStart.LocalPort = 10081
|
||||||
|
proxyDropByStart.Enabled = &enabled
|
||||||
|
|
||||||
|
proxyDropByEnabled := &v1.TCPProxyConfig{}
|
||||||
|
proxyDropByEnabled.Name = "drop-by-enabled"
|
||||||
|
proxyDropByEnabled.Type = "tcp"
|
||||||
|
proxyDropByEnabled.LocalPort = 10082
|
||||||
|
proxyDropByEnabled.Enabled = &disabled
|
||||||
|
|
||||||
|
common := &v1.ClientCommonConfig{
|
||||||
|
Start: []string{"keep"},
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{
|
||||||
|
proxyKeep,
|
||||||
|
proxyDropByStart,
|
||||||
|
proxyDropByEnabled,
|
||||||
|
}, nil)
|
||||||
|
require.Len(visitors, 0)
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Equal("keep", proxies[0].GetBaseConfig().Name)
|
||||||
|
}
|
||||||
|
|
||||||
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
||||||
func TestYAMLEdgeCases(t *testing.T) {
|
func TestYAMLEdgeCases(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
@@ -306,3 +495,111 @@ serverPort: 7000
|
|||||||
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
require.Equal(7000, clientCfg.ServerPort)
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTOMLSyntaxErrorWithPosition(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// TOML with syntax error (unclosed table array header)
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]
|
||||||
|
name = "test"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.Error(err)
|
||||||
|
require.Contains(err.Error(), "toml")
|
||||||
|
require.Contains(err.Error(), "line")
|
||||||
|
require.Contains(err.Error(), "column")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// TOML with wrong type: proxies should be a table array, not a string
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
proxies = "this should be a table array"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.Error(err)
|
||||||
|
// The error should contain field info
|
||||||
|
errMsg := err.Error()
|
||||||
|
require.Contains(errMsg, "proxies")
|
||||||
|
require.NotContains(errMsg, "line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindFieldLineInContent(t *testing.T) {
|
||||||
|
content := []byte(`serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6000
|
||||||
|
`)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
fieldPath string
|
||||||
|
wantLine int
|
||||||
|
}{
|
||||||
|
{"serverAddr", 1},
|
||||||
|
{"serverPort", 2},
|
||||||
|
{"name", 5},
|
||||||
|
{"type", 6},
|
||||||
|
{"remotePort", 7},
|
||||||
|
{"nonexistent", 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.fieldPath, func(t *testing.T) {
|
||||||
|
got := findFieldLineInContent(content, tt.fieldPath)
|
||||||
|
require.Equal(t, tt.wantLine, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatDetection(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
format string
|
||||||
|
}{
|
||||||
|
{"config.toml", "toml"},
|
||||||
|
{"config.TOML", "toml"},
|
||||||
|
{"config.yaml", "yaml"},
|
||||||
|
{"config.yml", "yaml"},
|
||||||
|
{"config.json", "json"},
|
||||||
|
{"config.ini", ""},
|
||||||
|
{"config", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
require.Equal(t, tt.format, detectFormatFromPath(tt.path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidTOMLStillWorks(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// Valid TOML with format hint should work fine
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6000
|
||||||
|
`
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
|
require.Len(clientCfg.Proxies, 1)
|
||||||
|
}
|
||||||
|
|||||||
109
pkg/config/source/aggregator.go
Normal file
109
pkg/config/source/aggregator.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Aggregator struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
configSource *ConfigSource
|
||||||
|
storeSource *StoreSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAggregator(configSource *ConfigSource) *Aggregator {
|
||||||
|
if configSource == nil {
|
||||||
|
configSource = NewConfigSource()
|
||||||
|
}
|
||||||
|
return &Aggregator{
|
||||||
|
configSource: configSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) SetStoreSource(storeSource *StoreSource) {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
a.storeSource = storeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) ConfigSource() *ConfigSource {
|
||||||
|
return a.configSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) StoreSource() *StoreSource {
|
||||||
|
return a.storeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) getSourcesLocked() []Source {
|
||||||
|
sources := make([]Source, 0, 2)
|
||||||
|
if a.configSource != nil {
|
||||||
|
sources = append(sources, a.configSource)
|
||||||
|
}
|
||||||
|
if a.storeSource != nil {
|
||||||
|
sources = append(sources, a.storeSource)
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||||
|
a.mu.RLock()
|
||||||
|
entries := a.getSourcesLocked()
|
||||||
|
a.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return nil, nil, errors.New("no sources configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyMap := make(map[string]v1.ProxyConfigurer)
|
||||||
|
visitorMap := make(map[string]v1.VisitorConfigurer)
|
||||||
|
|
||||||
|
for _, src := range entries {
|
||||||
|
proxies, visitors, err := src.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("load source: %w", err)
|
||||||
|
}
|
||||||
|
for _, p := range proxies {
|
||||||
|
proxyMap[p.GetBaseConfig().Name] = p
|
||||||
|
}
|
||||||
|
for _, v := range visitors {
|
||||||
|
visitorMap[v.GetBaseConfig().Name] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap)
|
||||||
|
return proxies, visitors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Aggregator) mapsToSortedSlices(
|
||||||
|
proxyMap map[string]v1.ProxyConfigurer,
|
||||||
|
visitorMap map[string]v1.VisitorConfigurer,
|
||||||
|
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||||
|
proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int {
|
||||||
|
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||||
|
})
|
||||||
|
visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int {
|
||||||
|
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||||
|
})
|
||||||
|
return proxies, visitors
|
||||||
|
}
|
||||||
238
pkg/config/source/aggregator_test.go
Normal file
238
pkg/config/source/aggregator_test.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockProxy creates a TCP proxy config for testing
|
||||||
|
func mockProxy(name string) v1.ProxyConfigurer {
|
||||||
|
cfg := &v1.TCPProxyConfig{}
|
||||||
|
cfg.Name = name
|
||||||
|
cfg.Type = "tcp"
|
||||||
|
cfg.LocalPort = 8080
|
||||||
|
cfg.RemotePort = 9090
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockVisitor creates a STCP visitor config for testing
|
||||||
|
func mockVisitor(name string) v1.VisitorConfigurer {
|
||||||
|
cfg := &v1.STCPVisitorConfig{}
|
||||||
|
cfg.Name = name
|
||||||
|
cfg.Type = "stcp"
|
||||||
|
cfg.ServerName = "test-server"
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestStoreSource(t *testing.T) *StoreSource {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
|
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return storeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
configSource := NewConfigSource()
|
||||||
|
agg := NewAggregator(configSource)
|
||||||
|
if storeSource != nil {
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
}
|
||||||
|
return agg
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
agg := NewAggregator(nil)
|
||||||
|
require.NotNil(agg)
|
||||||
|
require.NotNil(agg.ConfigSource())
|
||||||
|
require.Nil(agg.StoreSource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAggregator_WithoutStore(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
configSource := NewConfigSource()
|
||||||
|
agg := NewAggregator(configSource)
|
||||||
|
require.NotNil(agg)
|
||||||
|
require.Same(configSource, agg.ConfigSource())
|
||||||
|
require.Nil(agg.StoreSource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAggregator_WithStore(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
storeSource := newTestStoreSource(t)
|
||||||
|
configSource := NewConfigSource()
|
||||||
|
agg := NewAggregator(configSource)
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
|
||||||
|
require.Same(configSource, agg.ConfigSource())
|
||||||
|
require.Same(storeSource, agg.StoreSource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_SetStoreSource_Overwrite(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
agg := newTestAggregator(t, nil)
|
||||||
|
first := newTestStoreSource(t)
|
||||||
|
second := newTestStoreSource(t)
|
||||||
|
|
||||||
|
agg.SetStoreSource(first)
|
||||||
|
require.Same(first, agg.StoreSource())
|
||||||
|
|
||||||
|
agg.SetStoreSource(second)
|
||||||
|
require.Same(second, agg.StoreSource())
|
||||||
|
|
||||||
|
agg.SetStoreSource(nil)
|
||||||
|
require.Nil(agg.StoreSource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_MergeBySourceOrder(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
storeSource := newTestStoreSource(t)
|
||||||
|
agg := newTestAggregator(t, storeSource)
|
||||||
|
|
||||||
|
configSource := agg.ConfigSource()
|
||||||
|
|
||||||
|
configShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||||
|
configShared.LocalPort = 1111
|
||||||
|
configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig)
|
||||||
|
configOnly.LocalPort = 1112
|
||||||
|
|
||||||
|
err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
storeShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||||
|
storeShared.LocalPort = 2222
|
||||||
|
storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig)
|
||||||
|
storeOnly.LocalPort = 2223
|
||||||
|
err = storeSource.AddProxy(storeShared)
|
||||||
|
require.NoError(err)
|
||||||
|
err = storeSource.AddProxy(storeOnly)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(visitors, 0)
|
||||||
|
require.Len(proxies, 3)
|
||||||
|
|
||||||
|
var sharedProxy *v1.TCPProxyConfig
|
||||||
|
for _, p := range proxies {
|
||||||
|
if p.GetBaseConfig().Name == "shared" {
|
||||||
|
sharedProxy = p.(*v1.TCPProxyConfig)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(sharedProxy)
|
||||||
|
require.Equal(2222, sharedProxy.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
storeSource := newTestStoreSource(t)
|
||||||
|
agg := newTestAggregator(t, storeSource)
|
||||||
|
configSource := agg.ConfigSource()
|
||||||
|
|
||||||
|
lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||||
|
lowProxy.LocalPort = 1111
|
||||||
|
err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
disabled := false
|
||||||
|
highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||||
|
highProxy.LocalPort = 2222
|
||||||
|
highProxy.Enabled = &disabled
|
||||||
|
err = storeSource.AddProxy(highProxy)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Len(visitors, 0)
|
||||||
|
|
||||||
|
proxy := proxies[0].(*v1.TCPProxyConfig)
|
||||||
|
require.Equal("shared-proxy", proxy.Name)
|
||||||
|
require.Equal(1111, proxy.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_VisitorMerge(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
storeSource := newTestStoreSource(t)
|
||||||
|
agg := newTestAggregator(t, storeSource)
|
||||||
|
|
||||||
|
err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")})
|
||||||
|
require.NoError(err)
|
||||||
|
err = storeSource.AddVisitor(mockVisitor("visitor2"))
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, visitors, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(visitors, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_Load_ReturnsSortedByName(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
agg := newTestAggregator(t, nil)
|
||||||
|
err := agg.ConfigSource().ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")},
|
||||||
|
[]v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")},
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 3)
|
||||||
|
require.Equal("alice", proxies[0].GetBaseConfig().Name)
|
||||||
|
require.Equal("bob", proxies[1].GetBaseConfig().Name)
|
||||||
|
require.Equal("charlie", proxies[2].GetBaseConfig().Name)
|
||||||
|
require.Len(visitors, 2)
|
||||||
|
require.Equal("alpha", visitors[0].GetBaseConfig().Name)
|
||||||
|
require.Equal("zulu", visitors[1].GetBaseConfig().Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
agg := newTestAggregator(t, nil)
|
||||||
|
err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, _, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Equal("ssh", proxies[0].GetBaseConfig().Name)
|
||||||
|
|
||||||
|
proxies[0].GetBaseConfig().Name = "alice.ssh"
|
||||||
|
|
||||||
|
proxies2, _, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies2, 1)
|
||||||
|
require.Equal("ssh", proxies2[0].GetBaseConfig().Name)
|
||||||
|
}
|
||||||
65
pkg/config/source/base_source.go
Normal file
65
pkg/config/source/base_source.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// baseSource provides shared state and behavior for Source implementations.
|
||||||
|
// It manages proxy/visitor storage.
|
||||||
|
// Concrete types (ConfigSource, StoreSource) embed this struct.
|
||||||
|
type baseSource struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
proxies map[string]v1.ProxyConfigurer
|
||||||
|
visitors map[string]v1.VisitorConfigurer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBaseSource() baseSource {
|
||||||
|
return baseSource{
|
||||||
|
proxies: make(map[string]v1.ProxyConfigurer),
|
||||||
|
visitors: make(map[string]v1.VisitorConfigurer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load returns all enabled proxy and visitor configurations.
|
||||||
|
// Configurations with Enabled explicitly set to false are filtered out.
|
||||||
|
func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||||
|
for _, p := range s.proxies {
|
||||||
|
// Filter out disabled proxies (nil or true means enabled)
|
||||||
|
if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxies = append(proxies, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||||
|
for _, v := range s.visitors {
|
||||||
|
// Filter out disabled visitors (nil or true means enabled)
|
||||||
|
if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visitors = append(visitors, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneConfigurers(proxies, visitors)
|
||||||
|
}
|
||||||
48
pkg/config/source/base_source_test.go
Normal file
48
pkg/config/source/base_source_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "proxy1",
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
visitorCfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "visitor1",
|
||||||
|
Type: "stcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
firstProxies, firstVisitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(firstProxies, 1)
|
||||||
|
require.Len(firstVisitors, 1)
|
||||||
|
|
||||||
|
// Mutate loaded objects as runtime completion would do.
|
||||||
|
firstProxies[0].Complete()
|
||||||
|
firstVisitors[0].Complete()
|
||||||
|
|
||||||
|
secondProxies, secondVisitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(secondProxies, 1)
|
||||||
|
require.Len(secondVisitors, 1)
|
||||||
|
|
||||||
|
require.Empty(secondProxies[0].GetBaseConfig().LocalIP)
|
||||||
|
require.Empty(secondVisitors[0].GetBaseConfig().BindAddr)
|
||||||
|
}
|
||||||
43
pkg/config/source/clone.go
Normal file
43
pkg/config/source/clone.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cloneConfigurers(
|
||||||
|
proxies []v1.ProxyConfigurer,
|
||||||
|
visitors []v1.VisitorConfigurer,
|
||||||
|
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||||
|
clonedProxies := make([]v1.ProxyConfigurer, 0, len(proxies))
|
||||||
|
clonedVisitors := make([]v1.VisitorConfigurer, 0, len(visitors))
|
||||||
|
|
||||||
|
for _, cfg := range proxies {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, nil, fmt.Errorf("proxy cannot be nil")
|
||||||
|
}
|
||||||
|
clonedProxies = append(clonedProxies, cfg.Clone())
|
||||||
|
}
|
||||||
|
for _, cfg := range visitors {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, nil, fmt.Errorf("visitor cannot be nil")
|
||||||
|
}
|
||||||
|
clonedVisitors = append(clonedVisitors, cfg.Clone())
|
||||||
|
}
|
||||||
|
return clonedProxies, clonedVisitors, nil
|
||||||
|
}
|
||||||
65
pkg/config/source/config_source.go
Normal file
65
pkg/config/source/config_source.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigSource implements Source for in-memory configuration.
|
||||||
|
// All operations are thread-safe.
|
||||||
|
type ConfigSource struct {
|
||||||
|
baseSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigSource() *ConfigSource {
|
||||||
|
return &ConfigSource{
|
||||||
|
baseSource: newBaseSource(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceAll replaces all proxy and visitor configurations atomically.
|
||||||
|
func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies))
|
||||||
|
for _, p := range proxies {
|
||||||
|
if p == nil {
|
||||||
|
return fmt.Errorf("proxy cannot be nil")
|
||||||
|
}
|
||||||
|
name := p.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
nextProxies[name] = p
|
||||||
|
}
|
||||||
|
nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors))
|
||||||
|
for _, v := range visitors {
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("visitor cannot be nil")
|
||||||
|
}
|
||||||
|
name := v.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
nextVisitors[name] = v
|
||||||
|
}
|
||||||
|
s.proxies = nextProxies
|
||||||
|
s.visitors = nextVisitors
|
||||||
|
return nil
|
||||||
|
}
|
||||||
173
pkg/config/source/config_source_test.go
Normal file
173
pkg/config/source/config_source_test.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConfigSource(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
require.NotNil(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSource_ReplaceAll(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
|
||||||
|
err := src.ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||||
|
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 2)
|
||||||
|
require.Len(visitors, 1)
|
||||||
|
|
||||||
|
// ReplaceAll again should replace everything
|
||||||
|
err = src.ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{mockProxy("proxy3")},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err = src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Len(visitors, 0)
|
||||||
|
require.Equal("proxy3", proxies[0].GetBaseConfig().Name)
|
||||||
|
|
||||||
|
// ReplaceAll with nil proxy should fail
|
||||||
|
err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil)
|
||||||
|
require.Error(err)
|
||||||
|
|
||||||
|
// ReplaceAll with empty name proxy should fail
|
||||||
|
err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil)
|
||||||
|
require.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSource_Load(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
|
||||||
|
err := src.ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||||
|
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 2)
|
||||||
|
require.Len(visitors, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out
|
||||||
|
// proxies and visitors with Enabled explicitly set to false.
|
||||||
|
func TestConfigSource_Load_FiltersDisabled(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
|
||||||
|
disabled := false
|
||||||
|
enabled := true
|
||||||
|
|
||||||
|
// Create enabled proxy (nil Enabled = enabled by default)
|
||||||
|
enabledProxy := mockProxy("enabled-proxy")
|
||||||
|
|
||||||
|
// Create disabled proxy
|
||||||
|
disabledProxy := &v1.TCPProxyConfig{}
|
||||||
|
disabledProxy.Name = "disabled-proxy"
|
||||||
|
disabledProxy.Type = "tcp"
|
||||||
|
disabledProxy.Enabled = &disabled
|
||||||
|
|
||||||
|
// Create explicitly enabled proxy
|
||||||
|
explicitEnabledProxy := &v1.TCPProxyConfig{}
|
||||||
|
explicitEnabledProxy.Name = "explicit-enabled-proxy"
|
||||||
|
explicitEnabledProxy.Type = "tcp"
|
||||||
|
explicitEnabledProxy.Enabled = &enabled
|
||||||
|
|
||||||
|
// Create enabled visitor (nil Enabled = enabled by default)
|
||||||
|
enabledVisitor := mockVisitor("enabled-visitor")
|
||||||
|
|
||||||
|
// Create disabled visitor
|
||||||
|
disabledVisitor := &v1.STCPVisitorConfig{}
|
||||||
|
disabledVisitor.Name = "disabled-visitor"
|
||||||
|
disabledVisitor.Type = "stcp"
|
||||||
|
disabledVisitor.Enabled = &disabled
|
||||||
|
|
||||||
|
err := src.ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy},
|
||||||
|
[]v1.VisitorConfigurer{enabledVisitor, disabledVisitor},
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Load should filter out disabled configs
|
||||||
|
proxies, visitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 2, "Should have 2 enabled proxies")
|
||||||
|
require.Len(visitors, 1, "Should have 1 enabled visitor")
|
||||||
|
|
||||||
|
// Verify the correct proxies are returned
|
||||||
|
proxyNames := make([]string, 0, len(proxies))
|
||||||
|
for _, p := range proxies {
|
||||||
|
proxyNames = append(proxyNames, p.GetBaseConfig().Name)
|
||||||
|
}
|
||||||
|
require.Contains(proxyNames, "enabled-proxy")
|
||||||
|
require.Contains(proxyNames, "explicit-enabled-proxy")
|
||||||
|
require.NotContains(proxyNames, "disabled-proxy")
|
||||||
|
|
||||||
|
// Verify the correct visitor is returned
|
||||||
|
require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
src := NewConfigSource()
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy1"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor1"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server1"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
|
||||||
|
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := src.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 1)
|
||||||
|
require.Len(visitors, 1)
|
||||||
|
require.Empty(proxies[0].GetBaseConfig().LocalIP)
|
||||||
|
require.Empty(visitors[0].GetBaseConfig().BindAddr)
|
||||||
|
require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol)
|
||||||
|
}
|
||||||
37
pkg/config/source/source.go
Normal file
37
pkg/config/source/source.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Source is the interface for configuration sources.
|
||||||
|
// A Source provides proxy and visitor configurations from various backends.
|
||||||
|
// Aggregator currently uses the built-in config source as base and an optional
|
||||||
|
// store source as higher-priority overlay.
|
||||||
|
type Source interface {
|
||||||
|
// Load loads the proxy and visitor configurations from this source.
|
||||||
|
// Returns the loaded configurations and any error encountered.
|
||||||
|
// A disabled entry in one source is source-local filtering, not a cross-source
|
||||||
|
// tombstone for entries from lower-priority sources.
|
||||||
|
//
|
||||||
|
// Error handling contract with Aggregator:
|
||||||
|
// - When err is nil, returned slices are consumed.
|
||||||
|
// - When err is non-nil, Aggregator aborts the merge and returns the error.
|
||||||
|
// - To publish best-effort or partial results, return those results with
|
||||||
|
// err set to nil.
|
||||||
|
Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error)
|
||||||
|
}
|
||||||
367
pkg/config/source/store.go
Normal file
367
pkg/config/source/store.go
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreSourceConfig struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type storeData struct {
|
||||||
|
Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"`
|
||||||
|
Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreSource struct {
|
||||||
|
baseSource
|
||||||
|
config StoreSourceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlreadyExists = errors.New("already exists")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
|
||||||
|
if cfg.Path == "" {
|
||||||
|
return nil, fmt.Errorf("path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &StoreSource{
|
||||||
|
baseSource: newBaseSource(),
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.loadFromFile(); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("failed to load existing data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) loadFromFile() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.loadFromFileUnlocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) loadFromFileUnlocked() error {
|
||||||
|
data, err := os.ReadFile(s.config.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawStoreData struct {
|
||||||
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
|
}
|
||||||
|
stored := rawStoreData{}
|
||||||
|
if err := jsonx.Unmarshal(data, &stored); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||||
|
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||||
|
|
||||||
|
for i, proxyData := range stored.Proxies {
|
||||||
|
proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{
|
||||||
|
DisallowUnknownFields: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode proxy at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
name := proxyCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
s.proxies[name] = proxyCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, visitorData := range stored.Visitors {
|
||||||
|
visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{
|
||||||
|
DisallowUnknownFields: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode visitor at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
name := visitorCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
s.visitors[name] = visitorCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) saveToFileUnlocked() error {
|
||||||
|
stored := storeData{
|
||||||
|
Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)),
|
||||||
|
Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range s.proxies {
|
||||||
|
stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p})
|
||||||
|
}
|
||||||
|
for _, v := range s.visitors {
|
||||||
|
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := jsonx.MarshalIndent(stored, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(s.config.Path)
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpPath := s.config.Path + ".tmp"
|
||||||
|
|
||||||
|
f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Sync(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("failed to sync temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("failed to close temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tmpPath, s.config.Path); err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
||||||
|
if proxy == nil {
|
||||||
|
return fmt.Errorf("proxy cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := proxy.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.proxies[name]; exists {
|
||||||
|
return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.proxies[name] = proxy
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
delete(s.proxies, name)
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
|
||||||
|
if proxy == nil {
|
||||||
|
return fmt.Errorf("proxy cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := proxy.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
oldProxy, exists := s.proxies[name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.proxies[name] = proxy
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
s.proxies[name] = oldProxy
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) RemoveProxy(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
oldProxy, exists := s.proxies[name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.proxies, name)
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
s.proxies[name] = oldProxy
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
p, exists := s.proxies[name]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
|
||||||
|
if visitor == nil {
|
||||||
|
return fmt.Errorf("visitor cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := visitor.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.visitors[name]; exists {
|
||||||
|
return fmt.Errorf("%w: visitor %q", ErrAlreadyExists, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.visitors[name] = visitor
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
delete(s.visitors, name)
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
|
||||||
|
if visitor == nil {
|
||||||
|
return fmt.Errorf("visitor cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := visitor.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
oldVisitor, exists := s.visitors[name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.visitors[name] = visitor
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
s.visitors[name] = oldVisitor
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) RemoveVisitor(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
oldVisitor, exists := s.visitors[name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.visitors, name)
|
||||||
|
|
||||||
|
if err := s.saveToFileUnlocked(); err != nil {
|
||||||
|
s.visitors[name] = oldVisitor
|
||||||
|
return fmt.Errorf("failed to persist: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
v, exists := s.visitors[name]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||||
|
for _, p := range s.proxies {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||||
|
for _, v := range s.visitors {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
121
pkg/config/source/store_test.go
Normal file
121
pkg/config/source/store_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2026 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
|
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy1"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor1"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server1"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
|
||||||
|
err = storeSource.AddProxy(proxyCfg)
|
||||||
|
require.NoError(err)
|
||||||
|
err = storeSource.AddVisitor(visitorCfg)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
gotProxy := storeSource.GetProxy("proxy1")
|
||||||
|
require.NotNil(gotProxy)
|
||||||
|
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||||
|
|
||||||
|
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||||
|
require.NotNil(gotVisitor)
|
||||||
|
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||||
|
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{}
|
||||||
|
proxyCfg.Name = "proxy1"
|
||||||
|
proxyCfg.Type = "tcp"
|
||||||
|
proxyCfg.LocalPort = 10080
|
||||||
|
|
||||||
|
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||||
|
visitorCfg.Name = "visitor1"
|
||||||
|
visitorCfg.Type = "xtcp"
|
||||||
|
visitorCfg.ServerName = "server1"
|
||||||
|
visitorCfg.SecretKey = "secret"
|
||||||
|
visitorCfg.BindPort = 10081
|
||||||
|
|
||||||
|
stored := storeData{
|
||||||
|
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||||
|
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||||
|
}
|
||||||
|
data, err := jsonx.Marshal(stored)
|
||||||
|
require.NoError(err)
|
||||||
|
err = os.WriteFile(path, data, 0o600)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
gotProxy := storeSource.GetProxy("proxy1")
|
||||||
|
require.NotNil(gotProxy)
|
||||||
|
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||||
|
|
||||||
|
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||||
|
require.NotNil(gotVisitor)
|
||||||
|
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||||
|
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
|
raw := []byte(`{
|
||||||
|
"proxies": [
|
||||||
|
{"name":"proxy1","type":"tcp","localPort":10080,"unexpected":"value"}
|
||||||
|
],
|
||||||
|
"visitors": [
|
||||||
|
{"name":"visitor1","type":"xtcp","serverName":"server1","secretKey":"secret","bindPort":10081,"unexpected":"value"}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
err := os.WriteFile(path, raw, 0o600)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||||
|
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, e
|
|||||||
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
||||||
}
|
}
|
||||||
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
||||||
for i := 0; i < len(firstRangeNumbers); i++ {
|
for i := range firstRangeNumbers {
|
||||||
pairs = append(pairs, NumberPair{
|
pairs = append(pairs, NumberPair{
|
||||||
First: firstRangeNumbers[i],
|
First: firstRangeNumbers[i],
|
||||||
Second: secondRangeNumbers[i],
|
Second: secondRangeNumbers[i],
|
||||||
|
|||||||
@@ -70,24 +70,18 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error {
|
|||||||
f float64
|
f float64
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
switch {
|
if fstr, ok := strings.CutSuffix(s, "MB"); ok {
|
||||||
case strings.HasSuffix(s, "MB"):
|
|
||||||
base = MB
|
base = MB
|
||||||
fstr := strings.TrimSuffix(s, "MB")
|
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
if err != nil {
|
} else if fstr, ok := strings.CutSuffix(s, "KB"); ok {
|
||||||
return err
|
|
||||||
}
|
|
||||||
case strings.HasSuffix(s, "KB"):
|
|
||||||
base = KB
|
base = KB
|
||||||
fstr := strings.TrimSuffix(s, "KB")
|
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
|
} else {
|
||||||
|
return errors.New("unit not support")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return errors.New("unit not support")
|
|
||||||
}
|
|
||||||
|
|
||||||
q.s = s
|
q.s = s
|
||||||
q.i = int64(f * float64(base))
|
q.i = int64(f * float64(base))
|
||||||
@@ -143,8 +137,8 @@ func (p PortsRangeSlice) String() string {
|
|||||||
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
out := []PortsRange{}
|
out := []PortsRange{}
|
||||||
numRanges := strings.Split(str, ",")
|
numRanges := strings.SplitSeq(str, ",")
|
||||||
for _, numRangeStr := range numRanges {
|
for numRangeStr := range numRanges {
|
||||||
// 1000-2000 or 2001
|
// 1000-2000 or 2001
|
||||||
numArray := strings.Split(numRangeStr, "-")
|
numArray := strings.Split(numRangeStr, "-")
|
||||||
// length: only 1 or 2 is correct
|
// length: only 1 or 2 is correct
|
||||||
|
|||||||
@@ -39,6 +39,31 @@ func TestBandwidthQuantity(t *testing.T) {
|
|||||||
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_MB(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w)
|
||||||
|
require.NoError(err)
|
||||||
|
require.EqualValues(2*MB, w.B.Bytes())
|
||||||
|
|
||||||
|
buf, err := json.Marshal(&w)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(`{"b":"2MB","int":1}`, string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_InvalidUnit(t *testing.T) {
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_InvalidNumber(t *testing.T) {
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPortsRangeSlice2String(t *testing.T) {
|
func TestPortsRangeSlice2String(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ type ClientCommonConfig struct {
|
|||||||
|
|
||||||
// Include other config files for proxies.
|
// Include other config files for proxies.
|
||||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
IncludeConfigFiles []string `json:"includes,omitempty"`
|
||||||
|
|
||||||
|
// Store config enables the built-in store source (not configurable via sources list).
|
||||||
|
Store StoreConfig `json:"store,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientCommonConfig) Complete() error {
|
func (c *ClientCommonConfig) Complete() error {
|
||||||
@@ -101,6 +104,9 @@ type ClientTransportConfig struct {
|
|||||||
// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
|
// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
|
||||||
// is "tcp".
|
// is "tcp".
|
||||||
Protocol string `json:"protocol,omitempty"`
|
Protocol string `json:"protocol,omitempty"`
|
||||||
|
// WireProtocol specifies the frpc/frps internal wire protocol version.
|
||||||
|
// Valid values are "v1" and "v2". By default, this value is "v1".
|
||||||
|
WireProtocol string `json:"wireProtocol,omitempty"`
|
||||||
// The maximum amount of time a dial to server will wait for a connect to complete.
|
// The maximum amount of time a dial to server will wait for a connect to complete.
|
||||||
DialServerTimeout int64 `json:"dialServerTimeout,omitempty"`
|
DialServerTimeout int64 `json:"dialServerTimeout,omitempty"`
|
||||||
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
@@ -140,6 +146,7 @@ type ClientTransportConfig struct {
|
|||||||
|
|
||||||
func (c *ClientTransportConfig) Complete() {
|
func (c *ClientTransportConfig) Complete() {
|
||||||
c.Protocol = util.EmptyOr(c.Protocol, "tcp")
|
c.Protocol = util.EmptyOr(c.Protocol, "tcp")
|
||||||
|
c.WireProtocol = util.EmptyOr(c.WireProtocol, "v1")
|
||||||
c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10)
|
c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10)
|
||||||
c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200)
|
c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200)
|
||||||
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
|
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func TestClientConfigComplete(t *testing.T) {
|
|||||||
|
|
||||||
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("v1", c.Transport.WireProtocol)
|
||||||
require.Equal(true, lo.FromPtr(c.LoginFailExit))
|
require.Equal(true, lo.FromPtr(c.LoginFailExit))
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable))
|
require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable))
|
||||||
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
|
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
|
||||||
|
|||||||
109
pkg/config/v1/clone_test.go
Normal file
109
pkg/config/v1/clone_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProxyCloneDeepCopy(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
pluginHTTP2 := true
|
||||||
|
cfg := &HTTPProxyConfig{
|
||||||
|
ProxyBaseConfig: ProxyBaseConfig{
|
||||||
|
Name: "p1",
|
||||||
|
Type: "http",
|
||||||
|
Enabled: &enabled,
|
||||||
|
Annotations: map[string]string{"a": "1"},
|
||||||
|
Metadatas: map[string]string{"m": "1"},
|
||||||
|
HealthCheck: HealthCheckConfig{
|
||||||
|
Type: "http",
|
||||||
|
HTTPHeaders: []HTTPHeader{
|
||||||
|
{Name: "X-Test", Value: "v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProxyBackend: ProxyBackend{
|
||||||
|
Plugin: TypedClientPluginOptions{
|
||||||
|
Type: PluginHTTPS2HTTP,
|
||||||
|
ClientPluginOptions: &HTTPS2HTTPPluginOptions{
|
||||||
|
Type: PluginHTTPS2HTTP,
|
||||||
|
EnableHTTP2: &pluginHTTP2,
|
||||||
|
RequestHeaders: HeaderOperations{Set: map[string]string{"k": "v"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DomainConfig: DomainConfig{
|
||||||
|
CustomDomains: []string{"a.example.com"},
|
||||||
|
SubDomain: "a",
|
||||||
|
},
|
||||||
|
Locations: []string{"/api"},
|
||||||
|
RequestHeaders: HeaderOperations{Set: map[string]string{"h1": "v1"}},
|
||||||
|
ResponseHeaders: HeaderOperations{Set: map[string]string{"h2": "v2"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := cfg.Clone().(*HTTPProxyConfig)
|
||||||
|
|
||||||
|
*cloned.Enabled = false
|
||||||
|
cloned.Annotations["a"] = "changed"
|
||||||
|
cloned.Metadatas["m"] = "changed"
|
||||||
|
cloned.HealthCheck.HTTPHeaders[0].Value = "changed"
|
||||||
|
cloned.CustomDomains[0] = "b.example.com"
|
||||||
|
cloned.Locations[0] = "/new"
|
||||||
|
cloned.RequestHeaders.Set["h1"] = "changed"
|
||||||
|
cloned.ResponseHeaders.Set["h2"] = "changed"
|
||||||
|
clientPlugin := cloned.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions)
|
||||||
|
*clientPlugin.EnableHTTP2 = false
|
||||||
|
clientPlugin.RequestHeaders.Set["k"] = "changed"
|
||||||
|
|
||||||
|
require.True(*cfg.Enabled)
|
||||||
|
require.Equal("1", cfg.Annotations["a"])
|
||||||
|
require.Equal("1", cfg.Metadatas["m"])
|
||||||
|
require.Equal("v1", cfg.HealthCheck.HTTPHeaders[0].Value)
|
||||||
|
require.Equal("a.example.com", cfg.CustomDomains[0])
|
||||||
|
require.Equal("/api", cfg.Locations[0])
|
||||||
|
require.Equal("v1", cfg.RequestHeaders.Set["h1"])
|
||||||
|
require.Equal("v2", cfg.ResponseHeaders.Set["h2"])
|
||||||
|
|
||||||
|
origPlugin := cfg.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions)
|
||||||
|
require.True(*origPlugin.EnableHTTP2)
|
||||||
|
require.Equal("v", origPlugin.RequestHeaders.Set["k"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisitorCloneDeepCopy(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
cfg := &XTCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: VisitorBaseConfig{
|
||||||
|
Name: "v1",
|
||||||
|
Type: "xtcp",
|
||||||
|
Enabled: &enabled,
|
||||||
|
ServerName: "server",
|
||||||
|
BindPort: 7000,
|
||||||
|
Plugin: TypedVisitorPluginOptions{
|
||||||
|
Type: VisitorPluginVirtualNet,
|
||||||
|
VisitorPluginOptions: &VirtualNetVisitorPluginOptions{
|
||||||
|
Type: VisitorPluginVirtualNet,
|
||||||
|
DestinationIP: "10.0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NatTraversal: &NatTraversalConfig{
|
||||||
|
DisableAssistedAddrs: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := cfg.Clone().(*XTCPVisitorConfig)
|
||||||
|
*cloned.Enabled = false
|
||||||
|
cloned.NatTraversal.DisableAssistedAddrs = false
|
||||||
|
visitorPlugin := cloned.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions)
|
||||||
|
visitorPlugin.DestinationIP = "10.0.0.2"
|
||||||
|
|
||||||
|
require.True(*cfg.Enabled)
|
||||||
|
require.True(cfg.NatTraversal.DisableAssistedAddrs)
|
||||||
|
origPlugin := cfg.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions)
|
||||||
|
require.Equal("10.0.0.1", origPlugin.DestinationIP)
|
||||||
|
}
|
||||||
@@ -15,23 +15,11 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"maps"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method
|
|
||||||
// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder.
|
|
||||||
// Here, a global variable is temporarily used to control whether unknown fields are allowed.
|
|
||||||
// Once the v2 version is implemented by the community, we can switch to a standardized approach.
|
|
||||||
//
|
|
||||||
// https://github.com/golang/go/issues/41144
|
|
||||||
// https://github.com/golang/go/discussions/63397
|
|
||||||
var (
|
|
||||||
DisallowUnknownFields = false
|
|
||||||
DisallowUnknownFieldsMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthScope string
|
type AuthScope string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -104,6 +92,14 @@ type NatTraversalConfig struct {
|
|||||||
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *NatTraversalConfig) Clone() *NatTraversalConfig {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *c
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -138,6 +134,12 @@ type HeaderOperations struct {
|
|||||||
Set map[string]string `json:"set,omitempty"`
|
Set map[string]string `json:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o HeaderOperations) Clone() HeaderOperations {
|
||||||
|
return HeaderOperations{
|
||||||
|
Set: maps.Clone(o.Set),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPHeader struct {
|
type HTTPHeader struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|||||||
195
pkg/config/v1/decode.go
Normal file
195
pkg/config/v1/decode.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2026 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DecodeOptions struct {
|
||||||
|
DisallowUnknownFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error {
|
||||||
|
return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: options.DisallowUnknownFields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONNull(b []byte) bool {
|
||||||
|
return len(b) == 0 || string(b) == "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
type typedEnvelope struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Plugin jsonx.RawMessage `json:"plugin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewProxyConfigurerByType(ProxyType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown proxy type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewVisitorConfigurerByType(VisitorType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown visitor type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedClientPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedClientPluginOptions{}, errors.New("plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := clientPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedClientPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
ClientPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedVisitorPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := visitorPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedVisitorPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
VisitorPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
|
||||||
|
type rawClientConfig struct {
|
||||||
|
ClientCommonConfig
|
||||||
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := rawClientConfig{}
|
||||||
|
if err := decodeJSONWithOptions(b, &raw, options); err != nil {
|
||||||
|
return ClientConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := ClientConfig{
|
||||||
|
ClientCommonConfig: raw.ClientCommonConfig,
|
||||||
|
Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)),
|
||||||
|
Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, proxyData := range raw.Proxies {
|
||||||
|
proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{
|
||||||
|
Type: proxyCfg.GetBaseConfig().Type,
|
||||||
|
ProxyConfigurer: proxyCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, visitorData := range raw.Visitors {
|
||||||
|
visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{
|
||||||
|
Type: visitorCfg.GetBaseConfig().Type,
|
||||||
|
VisitorConfigurer: visitorCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
86
pkg/config/v1/decode_test.go
Normal file
86
pkg/config/v1/decode_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2026 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"plugin":{
|
||||||
|
"type":"http2https",
|
||||||
|
"localAddr":"127.0.0.1:8080",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"v1",
|
||||||
|
"type":"stcp",
|
||||||
|
"serverName":"server",
|
||||||
|
"bindPort":10081,
|
||||||
|
"plugin":{
|
||||||
|
"type":"virtual_net",
|
||||||
|
"destinationIP":"10.0.0.1",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"serverPort":7000,
|
||||||
|
"proxies":[
|
||||||
|
{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"unknownField":"value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownField")
|
||||||
|
}
|
||||||
@@ -15,16 +15,13 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"maps"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"github.com/samber/lo"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,11 +99,23 @@ type HealthCheckConfig struct {
|
|||||||
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
|
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c HealthCheckConfig) Clone() HealthCheckConfig {
|
||||||
|
out := c
|
||||||
|
out.HTTPHeaders = slices.Clone(c.HTTPHeaders)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type DomainConfig struct {
|
type DomainConfig struct {
|
||||||
CustomDomains []string `json:"customDomains,omitempty"`
|
CustomDomains []string `json:"customDomains,omitempty"`
|
||||||
SubDomain string `json:"subdomain,omitempty"`
|
SubDomain string `json:"subdomain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c DomainConfig) Clone() DomainConfig {
|
||||||
|
out := c
|
||||||
|
out.CustomDomains = slices.Clone(c.CustomDomains)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -122,12 +131,27 @@ type ProxyBaseConfig struct {
|
|||||||
ProxyBackend
|
ProxyBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c ProxyBaseConfig) Clone() ProxyBaseConfig {
|
||||||
|
out := c
|
||||||
|
out.Enabled = util.ClonePtr(c.Enabled)
|
||||||
|
out.Annotations = maps.Clone(c.Annotations)
|
||||||
|
out.Metadatas = maps.Clone(c.Metadatas)
|
||||||
|
out.HealthCheck = c.HealthCheck.Clone()
|
||||||
|
out.ProxyBackend = c.ProxyBackend.Clone()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ProxyBackend) Clone() ProxyBackend {
|
||||||
|
out := c
|
||||||
|
out.Plugin = c.Plugin.Clone()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
|
func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ProxyBaseConfig) Complete(namePrefix string) {
|
func (c *ProxyBaseConfig) Complete() {
|
||||||
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)
|
||||||
|
|
||||||
@@ -175,40 +199,24 @@ type TypedProxyConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.ProxyConfigurer = configurer
|
c.ProxyConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ProxyConfigurer)
|
return jsonx.Marshal(c.ProxyConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyConfigurer interface {
|
type ProxyConfigurer interface {
|
||||||
Complete(namePrefix string)
|
Complete()
|
||||||
GetBaseConfig() *ProxyBaseConfig
|
GetBaseConfig() *ProxyBaseConfig
|
||||||
|
Clone() ProxyConfigurer
|
||||||
// MarshalToMsg marshals this config into a msg.NewProxy message. This
|
// MarshalToMsg marshals this config into a msg.NewProxy message. This
|
||||||
// function will be called on the frpc side.
|
// function will be called on the frpc side.
|
||||||
MarshalToMsg(*msg.NewProxy)
|
MarshalToMsg(*msg.NewProxy)
|
||||||
@@ -231,14 +239,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}),
|
ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](),
|
||||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}),
|
ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](),
|
||||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}),
|
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](),
|
||||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}),
|
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](),
|
||||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
|
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](),
|
||||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}),
|
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](),
|
||||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}),
|
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](),
|
||||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}),
|
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](),
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
||||||
@@ -271,6 +279,12 @@ func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RemotePort = m.RemotePort
|
c.RemotePort = m.RemotePort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TCPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &UDPProxyConfig{}
|
var _ ProxyConfigurer = &UDPProxyConfig{}
|
||||||
|
|
||||||
type UDPProxyConfig struct {
|
type UDPProxyConfig struct {
|
||||||
@@ -291,6 +305,12 @@ func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RemotePort = m.RemotePort
|
c.RemotePort = m.RemotePort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *UDPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &HTTPProxyConfig{}
|
var _ ProxyConfigurer = &HTTPProxyConfig{}
|
||||||
|
|
||||||
type HTTPProxyConfig struct {
|
type HTTPProxyConfig struct {
|
||||||
@@ -334,6 +354,16 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RouteByHTTPUser = m.RouteByHTTPUser
|
c.RouteByHTTPUser = m.RouteByHTTPUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *HTTPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.DomainConfig = c.DomainConfig.Clone()
|
||||||
|
out.Locations = slices.Clone(c.Locations)
|
||||||
|
out.RequestHeaders = c.RequestHeaders.Clone()
|
||||||
|
out.ResponseHeaders = c.ResponseHeaders.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &HTTPSProxyConfig{}
|
var _ ProxyConfigurer = &HTTPSProxyConfig{}
|
||||||
|
|
||||||
type HTTPSProxyConfig struct {
|
type HTTPSProxyConfig struct {
|
||||||
@@ -355,6 +385,13 @@ func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.SubDomain = m.SubDomain
|
c.SubDomain = m.SubDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *HTTPSProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.DomainConfig = c.DomainConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
type TCPMultiplexerType string
|
type TCPMultiplexerType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -395,6 +432,13 @@ func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RouteByHTTPUser = m.RouteByHTTPUser
|
c.RouteByHTTPUser = m.RouteByHTTPUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TCPMuxProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.DomainConfig = c.DomainConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &STCPProxyConfig{}
|
var _ ProxyConfigurer = &STCPProxyConfig{}
|
||||||
|
|
||||||
type STCPProxyConfig struct {
|
type STCPProxyConfig struct {
|
||||||
@@ -418,6 +462,13 @@ func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *STCPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.AllowUsers = slices.Clone(c.AllowUsers)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &XTCPProxyConfig{}
|
var _ ProxyConfigurer = &XTCPProxyConfig{}
|
||||||
|
|
||||||
type XTCPProxyConfig struct {
|
type XTCPProxyConfig struct {
|
||||||
@@ -444,6 +495,14 @@ func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *XTCPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.AllowUsers = slices.Clone(c.AllowUsers)
|
||||||
|
out.NatTraversal = c.NatTraversal.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ ProxyConfigurer = &SUDPProxyConfig{}
|
var _ ProxyConfigurer = &SUDPProxyConfig{}
|
||||||
|
|
||||||
type SUDPProxyConfig struct {
|
type SUDPProxyConfig struct {
|
||||||
@@ -466,3 +525,10 @@ func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.Secretkey = m.Sk
|
c.Secretkey = m.Sk
|
||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SUDPProxyConfig) Clone() ProxyConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
||||||
|
out.AllowUsers = slices.Clone(c.AllowUsers)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,14 +15,11 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,20 +37,21 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](),
|
||||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](),
|
||||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](),
|
||||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](),
|
||||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](),
|
||||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
PluginSocks5: reflect.TypeFor[Socks5PluginOptions](),
|
||||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](),
|
||||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](),
|
||||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](),
|
||||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientPluginOptions interface {
|
type ClientPluginOptions interface {
|
||||||
Complete()
|
Complete()
|
||||||
|
Clone() ClientPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedClientPluginOptions struct {
|
type TypedClientPluginOptions struct {
|
||||||
@@ -61,43 +59,25 @@ type TypedClientPluginOptions struct {
|
|||||||
ClientPluginOptions
|
ClientPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
out := c
|
||||||
return nil
|
if c.ClientPluginOptions != nil {
|
||||||
|
out.ClientPluginOptions = c.ClientPluginOptions.Clone()
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
typeStruct := struct {
|
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
Type string `json:"type"`
|
decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{})
|
||||||
}{}
|
if err != nil {
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
c.Type = typeStruct.Type
|
|
||||||
if c.Type == "" {
|
|
||||||
return errors.New("plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
options := reflect.New(v).Interface().(ClientPluginOptions)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := decoder.Decode(options); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
c.ClientPluginOptions = options
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ClientPluginOptions)
|
return jsonx.Marshal(c.ClientPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2HTTPSPluginOptions struct {
|
type HTTP2HTTPSPluginOptions struct {
|
||||||
@@ -109,6 +89,15 @@ type HTTP2HTTPSPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *HTTP2HTTPSPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
out.RequestHeaders = o.RequestHeaders.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -117,6 +106,14 @@ type HTTPProxyPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTPProxyPluginOptions) Complete() {}
|
func (o *HTTPProxyPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *HTTPProxyPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -131,6 +128,16 @@ func (o *HTTPS2HTTPPluginOptions) Complete() {
|
|||||||
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTPS2HTTPPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
out.RequestHeaders = o.RequestHeaders.Clone()
|
||||||
|
out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -145,6 +152,16 @@ func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
|||||||
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *HTTPS2HTTPSPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
out.RequestHeaders = o.RequestHeaders.Clone()
|
||||||
|
out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
type HTTP2HTTPPluginOptions struct {
|
type HTTP2HTTPPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -154,6 +171,15 @@ type HTTP2HTTPPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTP2HTTPPluginOptions) Complete() {}
|
func (o *HTTP2HTTPPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *HTTP2HTTPPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
out.RequestHeaders = o.RequestHeaders.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -162,6 +188,14 @@ type Socks5PluginOptions struct {
|
|||||||
|
|
||||||
func (o *Socks5PluginOptions) Complete() {}
|
func (o *Socks5PluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *Socks5PluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -172,6 +206,14 @@ type StaticFilePluginOptions struct {
|
|||||||
|
|
||||||
func (o *StaticFilePluginOptions) Complete() {}
|
func (o *StaticFilePluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *StaticFilePluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@@ -179,6 +221,14 @@ type UnixDomainSocketPluginOptions struct {
|
|||||||
|
|
||||||
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *UnixDomainSocketPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
type TLS2RawPluginOptions struct {
|
type TLS2RawPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -188,8 +238,24 @@ type TLS2RawPluginOptions struct {
|
|||||||
|
|
||||||
func (o *TLS2RawPluginOptions) Complete() {}
|
func (o *TLS2RawPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *TLS2RawPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
type VirtualNetPluginOptions struct {
|
type VirtualNetPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *VirtualNetPluginOptions) Complete() {}
|
func (o *VirtualNetPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *VirtualNetPluginOptions) Clone() ClientPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|||||||
26
pkg/config/v1/store.go
Normal file
26
pkg/config/v1/store.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2026 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
|
||||||
|
|
||||||
|
// StoreConfig configures the built-in store source.
|
||||||
|
type StoreConfig struct {
|
||||||
|
// Path is the store file path.
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled returns true if the store is configured with a valid path.
|
||||||
|
func (c *StoreConfig) IsEnabled() bool {
|
||||||
|
return c.Path != ""
|
||||||
|
}
|
||||||
@@ -88,6 +88,11 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
|
|||||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
|
if c.Method == v1.AuthMethodOIDC && c.OIDC.TokenSource == nil {
|
||||||
|
if err := ValidateOIDCClientCredentialsConfig(&c.OIDC); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, errs
|
return nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +146,9 @@ func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
|||||||
if !slices.Contains(SupportedTransportProtocols, c.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))
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(SupportedWireProtocols, c.WireProtocol) {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("invalid transport.wireProtocol, optional values are %v", SupportedWireProtocols))
|
||||||
|
}
|
||||||
return warnings, errs
|
return warnings, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
pkg/config/v1/validation/oidc.go
Normal file
57
pkg/config/v1/validation/oidc.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2026 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateOIDCClientCredentialsConfig(c *v1.AuthOIDCClientConfig) error {
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
if c.ClientID == "" {
|
||||||
|
errs = append(errs, "auth.oidc.clientID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TokenEndpointURL == "" {
|
||||||
|
errs = append(errs, "auth.oidc.tokenEndpointURL is required")
|
||||||
|
} else {
|
||||||
|
tokenURL, err := url.Parse(c.TokenEndpointURL)
|
||||||
|
if err != nil || !tokenURL.IsAbs() || tokenURL.Host == "" {
|
||||||
|
errs = append(errs, "auth.oidc.tokenEndpointURL must be an absolute http or https URL")
|
||||||
|
} else if tokenURL.Scheme != "http" && tokenURL.Scheme != "https" {
|
||||||
|
errs = append(errs, "auth.oidc.tokenEndpointURL must use http or https")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := c.AdditionalEndpointParams["scope"]; ok {
|
||||||
|
errs = append(errs, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Audience != "" {
|
||||||
|
if _, ok := c.AdditionalEndpointParams["audience"]; ok {
|
||||||
|
errs = append(errs, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
78
pkg/config/v1/validation/oidc_test.go
Normal file
78
pkg/config/v1/validation/oidc_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2026 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateOIDCClientCredentialsConfig(t *testing.T) {
|
||||||
|
tokenServer := httptest.NewServer(http.NotFoundHandler())
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
require.NoError(t, ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
AdditionalEndpointParams: map[string]string{
|
||||||
|
"resource": "api",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid token endpoint url", func(t *testing.T) {
|
||||||
|
err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: "://bad",
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "auth.oidc.tokenEndpointURL")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing client id", func(t *testing.T) {
|
||||||
|
err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "auth.oidc.clientID is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scope endpoint param is not allowed", func(t *testing.T) {
|
||||||
|
err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
AdditionalEndpointParams: map[string]string{
|
||||||
|
"scope": "email",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("audience conflict", func(t *testing.T) {
|
||||||
|
err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
Audience: "api",
|
||||||
|
AdditionalEndpointParams: map[string]string{
|
||||||
|
"audience": "override",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ func validateDomainConfigForClient(c *v1.DomainConfig) error {
|
|||||||
func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
|
func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
|
||||||
for _, domain := range c.CustomDomains {
|
for _, domain := range c.CustomDomains {
|
||||||
if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
|
if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
|
||||||
if strings.Contains(domain, s.SubDomainHost) {
|
if strings.HasSuffix(domain, "."+s.SubDomainHost) {
|
||||||
return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
|
return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ var (
|
|||||||
"websocket",
|
"websocket",
|
||||||
"wss",
|
"wss",
|
||||||
}
|
}
|
||||||
|
SupportedWireProtocols = []string{
|
||||||
|
"v1",
|
||||||
|
"v2",
|
||||||
|
}
|
||||||
|
|
||||||
SupportedAuthMethods = []v1.AuthMethod{
|
SupportedAuthMethods = []v1.AuthMethod{
|
||||||
"token",
|
"token",
|
||||||
|
|||||||
@@ -15,14 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,31 +47,27 @@ type VisitorBaseConfig struct {
|
|||||||
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
|
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c VisitorBaseConfig) Clone() VisitorBaseConfig {
|
||||||
|
out := c
|
||||||
|
out.Enabled = util.ClonePtr(c.Enabled)
|
||||||
|
out.Plugin = c.Plugin.Clone()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) {
|
func (c *VisitorBaseConfig) Complete() {
|
||||||
if c.BindAddr == "" {
|
if c.BindAddr == "" {
|
||||||
c.BindAddr = "127.0.0.1"
|
c.BindAddr = "127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
namePrefix := ""
|
|
||||||
if g.User != "" {
|
|
||||||
namePrefix = g.User + "."
|
|
||||||
}
|
|
||||||
c.Name = namePrefix + c.Name
|
|
||||||
|
|
||||||
if c.ServerUser != "" {
|
|
||||||
c.ServerName = c.ServerUser + "." + c.ServerName
|
|
||||||
} else {
|
|
||||||
c.ServerName = namePrefix + c.ServerName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorConfigurer interface {
|
type VisitorConfigurer interface {
|
||||||
Complete(*ClientCommonConfig)
|
Complete()
|
||||||
GetBaseConfig() *VisitorBaseConfig
|
GetBaseConfig() *VisitorBaseConfig
|
||||||
|
Clone() VisitorConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorType string
|
type VisitorType string
|
||||||
@@ -88,9 +79,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
|
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](),
|
||||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
|
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](),
|
||||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
|
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedVisitorConfig struct {
|
type TypedVisitorConfig struct {
|
||||||
@@ -99,35 +90,18 @@ type TypedVisitorConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.VisitorConfigurer = configurer
|
c.VisitorConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorConfigurer)
|
return jsonx.Marshal(c.VisitorConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||||
@@ -146,12 +120,24 @@ type STCPVisitorConfig struct {
|
|||||||
VisitorBaseConfig
|
VisitorBaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *STCPVisitorConfig) Clone() VisitorConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ VisitorConfigurer = &SUDPVisitorConfig{}
|
var _ VisitorConfigurer = &SUDPVisitorConfig{}
|
||||||
|
|
||||||
type SUDPVisitorConfig struct {
|
type SUDPVisitorConfig struct {
|
||||||
VisitorBaseConfig
|
VisitorBaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SUDPVisitorConfig) Clone() VisitorConfigurer {
|
||||||
|
out := *c
|
||||||
|
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
var _ VisitorConfigurer = &XTCPVisitorConfig{}
|
var _ VisitorConfigurer = &XTCPVisitorConfig{}
|
||||||
|
|
||||||
type XTCPVisitorConfig struct {
|
type XTCPVisitorConfig struct {
|
||||||
@@ -168,15 +154,18 @@ type XTCPVisitorConfig struct {
|
|||||||
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
|
func (c *XTCPVisitorConfig) Complete() {
|
||||||
c.VisitorBaseConfig.Complete(g)
|
c.VisitorBaseConfig.Complete()
|
||||||
|
|
||||||
c.Protocol = util.EmptyOr(c.Protocol, "quic")
|
c.Protocol = util.EmptyOr(c.Protocol, "quic")
|
||||||
c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
|
c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
|
||||||
c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
|
c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
|
||||||
c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
|
c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
if c.FallbackTo != "" {
|
func (c *XTCPVisitorConfig) Clone() VisitorConfigurer {
|
||||||
c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo
|
out := *c
|
||||||
}
|
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
||||||
|
out.NatTraversal = c.NatTraversal.Clone()
|
||||||
|
return &out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,11 +25,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
|
VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorPluginOptions interface {
|
type VisitorPluginOptions interface {
|
||||||
Complete()
|
Complete()
|
||||||
|
Clone() VisitorPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedVisitorPluginOptions struct {
|
type TypedVisitorPluginOptions struct {
|
||||||
@@ -39,43 +38,25 @@ type TypedVisitorPluginOptions struct {
|
|||||||
VisitorPluginOptions
|
VisitorPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
out := c
|
||||||
return nil
|
if c.VisitorPluginOptions != nil {
|
||||||
|
out.VisitorPluginOptions = c.VisitorPluginOptions.Clone()
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
typeStruct := struct {
|
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
Type string `json:"type"`
|
decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{})
|
||||||
}{}
|
if err != nil {
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorPluginOptions)
|
return jsonx.Marshal(c.VisitorPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetVisitorPluginOptions struct {
|
type VirtualNetVisitorPluginOptions struct {
|
||||||
@@ -84,3 +65,11 @@ type VirtualNetVisitorPluginOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *VirtualNetVisitorPluginOptions) Complete() {}
|
func (o *VirtualNetVisitorPluginOptions) Complete() {}
|
||||||
|
|
||||||
|
func (o *VirtualNetVisitorPluginOptions) Clone() VisitorPluginOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *o
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ func (m *serverMetrics) OpenConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Inc(1)
|
proxyStats.CurConns.Inc(1)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +154,6 @@ func (m *serverMetrics) CloseConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Dec(1)
|
proxyStats.CurConns.Dec(1)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +166,6 @@ func (m *serverMetrics) AddTrafficIn(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficIn.Inc(trafficBytes)
|
proxyStats.TrafficIn.Inc(trafficBytes)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +178,6 @@ func (m *serverMetrics) AddTrafficOut(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficOut.Inc(trafficBytes)
|
proxyStats.TrafficOut.Inc(trafficBytes)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,16 +199,7 @@ func (m *serverMetrics) GetServer() *ServerStats {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats {
|
||||||
res := make([]*ProxyStats, 0)
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
for name, proxyStats := range m.info.ProxyStatistics {
|
|
||||||
if proxyStats.ProxyType != proxyType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := &ProxyStats{
|
ps := &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
@@ -228,7 +215,19 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
if !proxyStats.LastCloseTime.IsZero() {
|
if !proxyStats.LastCloseTime.IsZero() {
|
||||||
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||||
}
|
}
|
||||||
res = append(res, ps)
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
||||||
|
res := make([]*ProxyStats, 0)
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
for name, proxyStats := range m.info.ProxyStatistics {
|
||||||
|
if proxyStats.ProxyType != proxyType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, toProxyStats(name, proxyStats))
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -237,31 +236,9 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
for name, proxyStats := range m.info.ProxyStatistics {
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
if proxyStats.ProxyType != proxyType {
|
if ok && proxyStats.ProxyType == proxyType {
|
||||||
continue
|
res = toProxyStats(proxyName, proxyStats)
|
||||||
}
|
|
||||||
|
|
||||||
if name != proxyName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
res = &ProxyStats{
|
|
||||||
Name: name,
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -272,21 +249,7 @@ func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
|||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
if ok {
|
if ok {
|
||||||
res = &ProxyStats{
|
res = toProxyStats(proxyName, 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
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
56
pkg/msg/conn_test.go
Normal file
56
pkg/msg/conn_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2026 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 msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnReadWriteMsg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
protocol string
|
||||||
|
}{
|
||||||
|
{name: "v1", protocol: wire.ProtocolV1},
|
||||||
|
{name: "v2", protocol: wire.ProtocolV2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, server := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
clientConn := NewConn(client, NewReadWriter(client, tt.protocol))
|
||||||
|
serverConn := NewConn(server, NewReadWriter(server, tt.protocol))
|
||||||
|
|
||||||
|
in := &Ping{PrivilegeKey: "key", Timestamp: 123}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- clientConn.WriteMsg(in)
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := serverConn.ReadMsg()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, in, out)
|
||||||
|
require.NoError(t, <-errCh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,10 +15,90 @@
|
|||||||
package msg
|
package msg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ReadWriter interface {
|
||||||
|
ReadMsg() (Message, error)
|
||||||
|
ReadMsgInto(Message) error
|
||||||
|
WriteMsg(Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
rw ReadWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(conn net.Conn, rw ReadWriter) *Conn {
|
||||||
|
return &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
rw: rw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadMsg() (Message, error) {
|
||||||
|
return c.rw.ReadMsg()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadMsgInto(m Message) error {
|
||||||
|
return c.rw.ReadMsgInto(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriteMsg(m Message) error {
|
||||||
|
return c.rw.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Context() context.Context {
|
||||||
|
if getter, ok := c.Conn.(interface{ Context() context.Context }); ok {
|
||||||
|
return getter.Context()
|
||||||
|
}
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WithContext(ctx context.Context) {
|
||||||
|
if setter, ok := c.Conn.(interface{ WithContext(context.Context) }); ok {
|
||||||
|
setter.WithContext(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type V1ReadWriter struct {
|
||||||
|
rw io.ReadWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewV1ReadWriter(rw io.ReadWriter) ReadWriter {
|
||||||
|
return &V1ReadWriter{rw: rw}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReadWriter wraps rw with the message codec for the selected wire protocol.
|
||||||
|
// An empty protocol keeps the historical v1 behavior for tests and older call sites.
|
||||||
|
func NewReadWriter(rw io.ReadWriter, wireProtocol string) ReadWriter {
|
||||||
|
switch wireProtocol {
|
||||||
|
case wire.ProtocolV2:
|
||||||
|
return NewV2ReadWriter(rw)
|
||||||
|
case "", wire.ProtocolV1:
|
||||||
|
return NewV1ReadWriter(rw)
|
||||||
|
default:
|
||||||
|
return NewV1ReadWriter(rw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V1ReadWriter) ReadMsg() (Message, error) {
|
||||||
|
return ReadMsg(rw.rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V1ReadWriter) ReadMsgInto(m Message) error {
|
||||||
|
return ReadMsgInto(rw.rw, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V1ReadWriter) WriteMsg(m Message) error {
|
||||||
|
return WriteMsg(rw.rw, m)
|
||||||
|
}
|
||||||
|
|
||||||
func AsyncHandler(f func(Message)) func(Message) {
|
func AsyncHandler(f func(Message)) func(Message) {
|
||||||
return func(m Message) {
|
return func(m Message) {
|
||||||
go f(m)
|
go f(m)
|
||||||
@@ -27,7 +107,7 @@ func AsyncHandler(f func(Message)) func(Message) {
|
|||||||
|
|
||||||
// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn.
|
// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn.
|
||||||
type Dispatcher struct {
|
type Dispatcher struct {
|
||||||
rw io.ReadWriter
|
rw ReadWriter
|
||||||
|
|
||||||
sendCh chan Message
|
sendCh chan Message
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
@@ -35,7 +115,7 @@ type Dispatcher struct {
|
|||||||
defaultHandler func(Message)
|
defaultHandler func(Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDispatcher(rw io.ReadWriter) *Dispatcher {
|
func NewDispatcher(rw ReadWriter) *Dispatcher {
|
||||||
return &Dispatcher{
|
return &Dispatcher{
|
||||||
rw: rw,
|
rw: rw,
|
||||||
sendCh: make(chan Message, 100),
|
sendCh: make(chan Message, 100),
|
||||||
@@ -56,14 +136,14 @@ func (d *Dispatcher) sendLoop() {
|
|||||||
case <-d.doneCh:
|
case <-d.doneCh:
|
||||||
return
|
return
|
||||||
case m := <-d.sendCh:
|
case m := <-d.sendCh:
|
||||||
_ = WriteMsg(d.rw, m)
|
_ = d.rw.WriteMsg(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dispatcher) readLoop() {
|
func (d *Dispatcher) readLoop() {
|
||||||
for {
|
for {
|
||||||
m, err := ReadMsg(d.rw)
|
m, err := d.rw.ReadMsg()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(d.doneCh)
|
close(d.doneCh)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,24 +20,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeLogin = 'o'
|
TypeLogin byte = 'o'
|
||||||
TypeLoginResp = '1'
|
TypeLoginResp byte = '1'
|
||||||
TypeNewProxy = 'p'
|
TypeNewProxy byte = 'p'
|
||||||
TypeNewProxyResp = '2'
|
TypeNewProxyResp byte = '2'
|
||||||
TypeCloseProxy = 'c'
|
TypeCloseProxy byte = 'c'
|
||||||
TypeNewWorkConn = 'w'
|
TypeNewWorkConn byte = 'w'
|
||||||
TypeReqWorkConn = 'r'
|
TypeReqWorkConn byte = 'r'
|
||||||
TypeStartWorkConn = 's'
|
TypeStartWorkConn byte = 's'
|
||||||
TypeNewVisitorConn = 'v'
|
TypeNewVisitorConn byte = 'v'
|
||||||
TypeNewVisitorConnResp = '3'
|
TypeNewVisitorConnResp byte = '3'
|
||||||
TypePing = 'h'
|
TypePing byte = 'h'
|
||||||
TypePong = '4'
|
TypePong byte = '4'
|
||||||
TypeUDPPacket = 'u'
|
TypeUDPPacket byte = 'u'
|
||||||
TypeNatHoleVisitor = 'i'
|
TypeNatHoleVisitor byte = 'i'
|
||||||
TypeNatHoleClient = 'n'
|
TypeNatHoleClient byte = 'n'
|
||||||
TypeNatHoleResp = 'm'
|
TypeNatHoleResp byte = 'm'
|
||||||
TypeNatHoleSid = '5'
|
TypeNatHoleSid byte = '5'
|
||||||
TypeNatHoleReport = '6'
|
TypeNatHoleReport byte = '6'
|
||||||
)
|
)
|
||||||
|
|
||||||
var msgTypeMap = map[byte]any{
|
var msgTypeMap = map[byte]any{
|
||||||
@@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{
|
|||||||
TypeNatHoleReport: NatHoleReport{},
|
TypeNatHoleReport: NatHoleReport{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
|
var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name()
|
||||||
|
|
||||||
type ClientSpec struct {
|
type ClientSpec struct {
|
||||||
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
||||||
@@ -184,7 +184,7 @@ type Pong struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPPacket struct {
|
type UDPPacket struct {
|
||||||
Content string `json:"c,omitempty"`
|
Content []byte `json:"c,omitempty"`
|
||||||
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
||||||
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
55
pkg/msg/msg_test.go
Normal file
55
pkg/msg/msg_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2026 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 msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV1MessageTypeIDsAreStable(t *testing.T) {
|
||||||
|
require.Equal(t, byte('o'), TypeLogin)
|
||||||
|
require.Equal(t, byte('1'), TypeLoginResp)
|
||||||
|
require.Equal(t, byte('p'), TypeNewProxy)
|
||||||
|
require.Equal(t, byte('2'), TypeNewProxyResp)
|
||||||
|
require.Equal(t, byte('c'), TypeCloseProxy)
|
||||||
|
require.Equal(t, byte('w'), TypeNewWorkConn)
|
||||||
|
require.Equal(t, byte('r'), TypeReqWorkConn)
|
||||||
|
require.Equal(t, byte('s'), TypeStartWorkConn)
|
||||||
|
require.Equal(t, byte('v'), TypeNewVisitorConn)
|
||||||
|
require.Equal(t, byte('3'), TypeNewVisitorConnResp)
|
||||||
|
require.Equal(t, byte('h'), TypePing)
|
||||||
|
require.Equal(t, byte('4'), TypePong)
|
||||||
|
require.Equal(t, byte('u'), TypeUDPPacket)
|
||||||
|
require.Equal(t, byte('i'), TypeNatHoleVisitor)
|
||||||
|
require.Equal(t, byte('n'), TypeNatHoleClient)
|
||||||
|
require.Equal(t, byte('m'), TypeNatHoleResp)
|
||||||
|
require.Equal(t, byte('5'), TypeNatHoleSid)
|
||||||
|
require.Equal(t, byte('6'), TypeNatHoleReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageTypeMapIsCompleteAndUnique(t *testing.T) {
|
||||||
|
require.Len(t, msgTypeMap, 18)
|
||||||
|
|
||||||
|
msgTypes := make(map[reflect.Type]struct{}, len(msgTypeMap))
|
||||||
|
|
||||||
|
for _, m := range msgTypeMap {
|
||||||
|
msgType := reflect.TypeOf(m)
|
||||||
|
require.NotContains(t, msgTypes, msgType)
|
||||||
|
msgTypes[msgType] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
192
pkg/msg/wire_v2.go
Normal file
192
pkg/msg/wire_v2.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2026 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 msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
V2TypeLogin uint16 = 1
|
||||||
|
V2TypeLoginResp uint16 = 2
|
||||||
|
V2TypeNewProxy uint16 = 3
|
||||||
|
V2TypeNewProxyResp uint16 = 4
|
||||||
|
V2TypeCloseProxy uint16 = 5
|
||||||
|
V2TypeNewWorkConn uint16 = 6
|
||||||
|
V2TypeReqWorkConn uint16 = 7
|
||||||
|
V2TypeStartWorkConn uint16 = 8
|
||||||
|
V2TypeNewVisitorConn uint16 = 9
|
||||||
|
V2TypeNewVisitorConnResp uint16 = 10
|
||||||
|
V2TypePing uint16 = 11
|
||||||
|
V2TypePong uint16 = 12
|
||||||
|
V2TypeUDPPacket uint16 = 13
|
||||||
|
V2TypeNatHoleVisitor uint16 = 14
|
||||||
|
V2TypeNatHoleClient uint16 = 15
|
||||||
|
V2TypeNatHoleResp uint16 = 16
|
||||||
|
V2TypeNatHoleSid uint16 = 17
|
||||||
|
V2TypeNatHoleReport uint16 = 18
|
||||||
|
)
|
||||||
|
|
||||||
|
var v2MsgTypeMap = map[uint16]any{
|
||||||
|
V2TypeLogin: Login{},
|
||||||
|
V2TypeLoginResp: LoginResp{},
|
||||||
|
V2TypeNewProxy: NewProxy{},
|
||||||
|
V2TypeNewProxyResp: NewProxyResp{},
|
||||||
|
V2TypeCloseProxy: CloseProxy{},
|
||||||
|
V2TypeNewWorkConn: NewWorkConn{},
|
||||||
|
V2TypeReqWorkConn: ReqWorkConn{},
|
||||||
|
V2TypeStartWorkConn: StartWorkConn{},
|
||||||
|
V2TypeNewVisitorConn: NewVisitorConn{},
|
||||||
|
V2TypeNewVisitorConnResp: NewVisitorConnResp{},
|
||||||
|
V2TypePing: Ping{},
|
||||||
|
V2TypePong: Pong{},
|
||||||
|
V2TypeUDPPacket: UDPPacket{},
|
||||||
|
V2TypeNatHoleVisitor: NatHoleVisitor{},
|
||||||
|
V2TypeNatHoleClient: NatHoleClient{},
|
||||||
|
V2TypeNatHoleResp: NatHoleResp{},
|
||||||
|
V2TypeNatHoleSid: NatHoleSid{},
|
||||||
|
V2TypeNatHoleReport: NatHoleReport{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var v2MsgReflectTypeMap, v2MsgTypeIDMap = buildV2MsgTypeMaps()
|
||||||
|
|
||||||
|
func buildV2MsgTypeMaps() (map[uint16]reflect.Type, map[reflect.Type]uint16) {
|
||||||
|
reflectTypeMap := make(map[uint16]reflect.Type, len(v2MsgTypeMap))
|
||||||
|
typeIDMap := make(map[reflect.Type]uint16, len(v2MsgTypeMap))
|
||||||
|
for typeID, m := range v2MsgTypeMap {
|
||||||
|
t := reflect.TypeOf(m)
|
||||||
|
reflectTypeMap[typeID] = t
|
||||||
|
typeIDMap[t] = typeID
|
||||||
|
}
|
||||||
|
return reflectTypeMap, typeIDMap
|
||||||
|
}
|
||||||
|
|
||||||
|
type V2ReadWriter struct {
|
||||||
|
conn *wire.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewV2ReadWriter(rw io.ReadWriter) *V2ReadWriter {
|
||||||
|
return NewV2ReadWriterWithConn(wire.NewConn(rw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewV2ReadWriterWithConn(conn *wire.Conn) *V2ReadWriter {
|
||||||
|
return &V2ReadWriter{conn: conn}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V2ReadWriter) WireConn() *wire.Conn {
|
||||||
|
return rw.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V2ReadWriter) ReadMsg() (Message, error) {
|
||||||
|
f, err := rw.conn.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return DecodeV2MessageFrame(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V2ReadWriter) ReadMsgInto(m Message) error {
|
||||||
|
f, err := rw.conn.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return DecodeV2MessageFrameInto(f, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *V2ReadWriter) WriteMsg(m Message) error {
|
||||||
|
f, err := EncodeV2MessageFrame(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rw.conn.WriteFrame(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeV2MessageFrame(f *wire.Frame) (Message, error) {
|
||||||
|
if f.Type != wire.FrameTypeMessage {
|
||||||
|
return nil, fmt.Errorf("unexpected frame type %d, want %d", f.Type, wire.FrameTypeMessage)
|
||||||
|
}
|
||||||
|
if len(f.Payload) < 2 {
|
||||||
|
return nil, fmt.Errorf("message frame payload too short")
|
||||||
|
}
|
||||||
|
typeID := binary.BigEndian.Uint16(f.Payload[:2])
|
||||||
|
t, ok := v2MsgReflectTypeMap[typeID]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown v2 message type %d", typeID)
|
||||||
|
}
|
||||||
|
m := reflect.New(t).Interface()
|
||||||
|
if err := json.Unmarshal(f.Payload[2:], m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeV2MessageFrameInto(f *wire.Frame, out Message) error {
|
||||||
|
if f.Type != wire.FrameTypeMessage {
|
||||||
|
return fmt.Errorf("unexpected frame type %d, want %d", f.Type, wire.FrameTypeMessage)
|
||||||
|
}
|
||||||
|
if len(f.Payload) < 2 {
|
||||||
|
return fmt.Errorf("message frame payload too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
typeID := binary.BigEndian.Uint16(f.Payload[:2])
|
||||||
|
outType := reflect.TypeOf(out)
|
||||||
|
if outType == nil || outType.Kind() != reflect.Pointer {
|
||||||
|
return fmt.Errorf("message target must be a pointer")
|
||||||
|
}
|
||||||
|
elemType := outType.Elem()
|
||||||
|
expectedTypeID, ok := v2MsgTypeIDMap[elemType]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown v2 message type %s", elemType.String())
|
||||||
|
}
|
||||||
|
if typeID != expectedTypeID {
|
||||||
|
actualType, ok := v2MsgReflectTypeMap[typeID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown v2 message type %d", typeID)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unexpected message type %s, want %s", actualType.String(), elemType.String())
|
||||||
|
}
|
||||||
|
return json.Unmarshal(f.Payload[2:], out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeV2MessageFrame(m Message) (*wire.Frame, error) {
|
||||||
|
t := reflect.TypeOf(m)
|
||||||
|
if t == nil {
|
||||||
|
return nil, fmt.Errorf("nil message")
|
||||||
|
}
|
||||||
|
if t.Kind() == reflect.Pointer {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
typeID, ok := v2MsgTypeIDMap[t]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown v2 message type %s", t.String())
|
||||||
|
}
|
||||||
|
content, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload := make([]byte, 2+len(content))
|
||||||
|
binary.BigEndian.PutUint16(payload[:2], typeID)
|
||||||
|
copy(payload[2:], content)
|
||||||
|
return &wire.Frame{
|
||||||
|
Type: wire.FrameTypeMessage,
|
||||||
|
Payload: payload,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
121
pkg/msg/wire_v2_test.go
Normal file
121
pkg/msg/wire_v2_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2026 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 msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV2ReadWriterRoundTrip(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
rw := NewV2ReadWriter(&buf)
|
||||||
|
|
||||||
|
in := &Login{
|
||||||
|
Version: "test-version",
|
||||||
|
RunID: "run-id",
|
||||||
|
User: "user",
|
||||||
|
}
|
||||||
|
require.NoError(t, rw.WriteMsg(in))
|
||||||
|
|
||||||
|
out, err := rw.ReadMsg()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewReadWriter(t *testing.T) {
|
||||||
|
require.IsType(t, &V1ReadWriter{}, NewReadWriter(&bytes.Buffer{}, ""))
|
||||||
|
require.IsType(t, &V1ReadWriter{}, NewReadWriter(&bytes.Buffer{}, wire.ProtocolV1))
|
||||||
|
require.IsType(t, &V2ReadWriter{}, NewReadWriter(&bytes.Buffer{}, wire.ProtocolV2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2MessageTypeIDsAreStable(t *testing.T) {
|
||||||
|
require.Equal(t, uint16(1), V2TypeLogin)
|
||||||
|
require.Equal(t, uint16(2), V2TypeLoginResp)
|
||||||
|
require.Equal(t, uint16(3), V2TypeNewProxy)
|
||||||
|
require.Equal(t, uint16(4), V2TypeNewProxyResp)
|
||||||
|
require.Equal(t, uint16(5), V2TypeCloseProxy)
|
||||||
|
require.Equal(t, uint16(6), V2TypeNewWorkConn)
|
||||||
|
require.Equal(t, uint16(7), V2TypeReqWorkConn)
|
||||||
|
require.Equal(t, uint16(8), V2TypeStartWorkConn)
|
||||||
|
require.Equal(t, uint16(9), V2TypeNewVisitorConn)
|
||||||
|
require.Equal(t, uint16(10), V2TypeNewVisitorConnResp)
|
||||||
|
require.Equal(t, uint16(11), V2TypePing)
|
||||||
|
require.Equal(t, uint16(12), V2TypePong)
|
||||||
|
require.Equal(t, uint16(13), V2TypeUDPPacket)
|
||||||
|
require.Equal(t, uint16(14), V2TypeNatHoleVisitor)
|
||||||
|
require.Equal(t, uint16(15), V2TypeNatHoleClient)
|
||||||
|
require.Equal(t, uint16(16), V2TypeNatHoleResp)
|
||||||
|
require.Equal(t, uint16(17), V2TypeNatHoleSid)
|
||||||
|
require.Equal(t, uint16(18), V2TypeNatHoleReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2MessageFrameEncoding(t *testing.T) {
|
||||||
|
frame, err := EncodeV2MessageFrame(&ReqWorkConn{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, wire.FrameTypeMessage, frame.Type)
|
||||||
|
require.Len(t, frame.Payload, 4)
|
||||||
|
require.Equal(t, V2TypeReqWorkConn, binary.BigEndian.Uint16(frame.Payload[:2]))
|
||||||
|
|
||||||
|
out, err := DecodeV2MessageFrame(frame)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.IsType(t, &ReqWorkConn{}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeV2MessageFrameInto(t *testing.T) {
|
||||||
|
in := &StartWorkConn{ProxyName: "tcp", SrcAddr: "127.0.0.1", SrcPort: 1234}
|
||||||
|
frame, err := EncodeV2MessageFrame(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var out StartWorkConn
|
||||||
|
require.NoError(t, DecodeV2MessageFrameInto(frame, &out))
|
||||||
|
require.Equal(t, *in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeV2MessageFrameRejectsInvalidFrame(t *testing.T) {
|
||||||
|
_, err := DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeClientHello})
|
||||||
|
require.ErrorContains(t, err, "unexpected frame type")
|
||||||
|
|
||||||
|
_, err = DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeMessage, Payload: []byte{0}})
|
||||||
|
require.ErrorContains(t, err, "payload too short")
|
||||||
|
|
||||||
|
payload := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint16(payload[:2], 65535)
|
||||||
|
copy(payload[2:], []byte("{}"))
|
||||||
|
_, err = DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeMessage, Payload: payload})
|
||||||
|
require.ErrorContains(t, err, "unknown v2 message type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeV2MessageFrameIntoRejectsWrongTarget(t *testing.T) {
|
||||||
|
frame, err := EncodeV2MessageFrame(&ReqWorkConn{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var out StartWorkConn
|
||||||
|
err = DecodeV2MessageFrameInto(frame, &out)
|
||||||
|
require.ErrorContains(t, err, "unexpected message type")
|
||||||
|
|
||||||
|
err = DecodeV2MessageFrameInto(frame, StartWorkConn{})
|
||||||
|
require.ErrorContains(t, err, "must be a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeV2MessageFrameRejectsUnknownMessage(t *testing.T) {
|
||||||
|
_, err := EncodeV2MessageFrame(struct{}{})
|
||||||
|
require.ErrorContains(t, err, "unknown v2 message type")
|
||||||
|
}
|
||||||
32
pkg/naming/names.go
Normal file
32
pkg/naming/names.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package naming
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// AddUserPrefix builds the wire-level proxy name for frps by prefixing user.
|
||||||
|
func AddUserPrefix(user, name string) string {
|
||||||
|
if user == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return user + "." + name
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripUserPrefix converts a wire-level proxy name to an internal raw name.
|
||||||
|
// It strips only one exact "{user}." prefix.
|
||||||
|
func StripUserPrefix(user, name string) string {
|
||||||
|
if user == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if trimmed, ok := strings.CutPrefix(name, user+"."); ok {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTargetServerProxyName resolves visitor target proxy name for wire-level
|
||||||
|
// protocol messages. serverUser overrides local user when set.
|
||||||
|
func BuildTargetServerProxyName(localUser, serverUser, serverName string) string {
|
||||||
|
if serverUser != "" {
|
||||||
|
return AddUserPrefix(serverUser, serverName)
|
||||||
|
}
|
||||||
|
return AddUserPrefix(localUser, serverName)
|
||||||
|
}
|
||||||
27
pkg/naming/names_test.go
Normal file
27
pkg/naming/names_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package naming
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddUserPrefix(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
require.Equal("test", AddUserPrefix("", "test"))
|
||||||
|
require.Equal("alice.test", AddUserPrefix("alice", "test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripUserPrefix(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
require.Equal("test", StripUserPrefix("", "test"))
|
||||||
|
require.Equal("test", StripUserPrefix("alice", "alice.test"))
|
||||||
|
require.Equal("alice.test", StripUserPrefix("alice", "alice.alice.test"))
|
||||||
|
require.Equal("bob.test", StripUserPrefix("alice", "bob.test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTargetServerProxyName(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
require.Equal("alice.test", BuildTargetServerProxyName("alice", "", "test"))
|
||||||
|
require.Equal("bob.test", BuildTargetServerProxyName("alice", "bob", "test"))
|
||||||
|
}
|
||||||
@@ -151,7 +151,7 @@ func getBehaviorScoresByMode(mode int, defaultScore int) []*BehaviorScore {
|
|||||||
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
||||||
behaviors := getBehaviorByMode(mode)
|
behaviors := getBehaviorByMode(mode)
|
||||||
scores := make([]*BehaviorScore, 0, len(behaviors))
|
scores := make([]*BehaviorScore, 0, len(behaviors))
|
||||||
for i := 0; i < len(behaviors); i++ {
|
for i := range behaviors {
|
||||||
score := receiverScore
|
score := receiverScore
|
||||||
if behaviors[i].A.Role == DetectRoleSender {
|
if behaviors[i].A.Role == DetectRoleSender {
|
||||||
score = senderScore
|
score = senderScore
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user