mirror of
https://github.com/fatedier/frp.git
synced 2026-04-27 19:39:10 +08:00
Compare commits
30 Commits
f7aa38b303
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57bb9e80fe | ||
|
|
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 |
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
|
||||||
|
|||||||
8
.github/workflows/golangci-lint.yml
vendored
8
.github/workflows/golangci-lint.yml
vendored
@@ -14,12 +14,12 @@ 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.25'
|
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)
|
||||||
@@ -32,4 +32,4 @@ jobs:
|
|||||||
uses: golangci/golangci-lint-action@v9
|
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.10
|
version: v2.11
|
||||||
|
|||||||
8
.github/workflows/goreleaser.yml
vendored
8
.github/workflows/goreleaser.yml
vendored
@@ -8,15 +8,15 @@ 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.25'
|
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)
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
./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."
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -18,6 +18,7 @@ release/
|
|||||||
test/bin/
|
test/bin/
|
||||||
vendor/
|
vendor/
|
||||||
lastversion/
|
lastversion/
|
||||||
|
.cache/
|
||||||
dist/
|
dist/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -25,10 +26,12 @@ dist/
|
|||||||
client.crt
|
client.crt
|
||||||
client.key
|
client.key
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
CLAUDE.md
|
.claude/
|
||||||
AGENTS.md
|
|
||||||
.sisyphus/
|
.sisyphus/
|
||||||
|
.superpowers/
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ linters:
|
|||||||
disabled-checks:
|
disabled-checks:
|
||||||
- exitAfterDefer
|
- exitAfterDefer
|
||||||
gosec:
|
gosec:
|
||||||
excludes: ["G115", "G117", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
excludes: ["G115", "G117", "G118", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
||||||
severity: low
|
severity: low
|
||||||
confidence: low
|
confidence: low
|
||||||
govet:
|
govet:
|
||||||
@@ -90,6 +90,7 @@ linters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gci
|
- gci
|
||||||
@@ -112,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
|
||||||
15
Makefile
15
Makefile
@@ -2,8 +2,10 @@ 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')
|
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
||||||
|
FRP_COMPAT_BASELINE_COUNT ?= 8
|
||||||
|
FRP_COMPAT_FLOOR_VERSION ?= 0.61.0
|
||||||
|
|
||||||
.PHONY: web frps-web frpc-web frps frpc
|
.PHONY: web frps-web frpc-web frps frpc e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor
|
||||||
|
|
||||||
all: env fmt web build
|
all: env fmt web build
|
||||||
|
|
||||||
@@ -53,6 +55,15 @@ e2e:
|
|||||||
e2e-trace:
|
e2e-trace:
|
||||||
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
|
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
|
||||||
|
|
||||||
|
e2e-compatibility-smoke: build
|
||||||
|
FRP_COMPAT_BASELINE_COUNT=1 ./hack/run-e2e-compatibility.sh
|
||||||
|
|
||||||
|
e2e-compatibility: build
|
||||||
|
FRP_COMPAT_BASELINE_COUNT="$(FRP_COMPAT_BASELINE_COUNT)" ./hack/run-e2e-compatibility.sh
|
||||||
|
|
||||||
|
e2e-compatibility-floor: build
|
||||||
|
FRP_COMPAT_BASELINE_VERSIONS="$(FRP_COMPAT_FLOOR_VERSION)" ./hack/run-e2e-compatibility.sh
|
||||||
|
|
||||||
e2e-compatibility-last-frpc:
|
e2e-compatibility-last-frpc:
|
||||||
if [ ! -d "./lastversion" ]; then \
|
if [ ! -d "./lastversion" ]; then \
|
||||||
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
||||||
@@ -73,3 +84,5 @@ clean:
|
|||||||
rm -f ./bin/frpc
|
rm -f ./bin/frpc
|
||||||
rm -f ./bin/frps
|
rm -f ./bin/frps
|
||||||
rm -rf ./lastversion
|
rm -rf ./lastversion
|
||||||
|
rm -rf ./.cache
|
||||||
|
rm -rf ./.compat
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -13,6 +13,16 @@ 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">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -40,16 +50,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<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>
|
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
@@ -81,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)
|
||||||
@@ -149,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
|
||||||
|
|
||||||
@@ -593,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:
|
||||||
|
|
||||||
@@ -606,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.
|
||||||
|
|||||||
20
README_zh.md
20
README_zh.md
@@ -15,6 +15,16 @@ 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">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -42,16 +52,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<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>
|
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
|
|||||||
23
Release.md
23
Release.md
@@ -1,9 +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
|
||||||
|
|
||||||
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
|
* Added `transport.wireProtocol` for frpc to select the internal message protocol used between frpc and frps. Supported values are `v1` and `v2`.
|
||||||
|
* Added client protocol visibility in the frps dashboard and `/api/clients` API. Online clients now report their negotiated protocol as `v1` or `v2`.
|
||||||
## Improvements
|
|
||||||
|
|
||||||
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
|
|
||||||
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
|
|
||||||
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ 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 {
|
if svr.storeSource != nil {
|
||||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||||
|
|||||||
@@ -80,6 +80,48 @@ func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
|||||||
return m.svr.getAllProxyStatus()
|
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 {
|
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ type ConfigManager interface {
|
|||||||
IsStoreProxyEnabled(name string) bool
|
IsStoreProxyEnabled(name string) bool
|
||||||
StoreEnabled() bool
|
StoreEnabled() bool
|
||||||
|
|
||||||
|
GetProxyConfig(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
GetVisitorConfig(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||||
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||||
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/naming"
|
"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"
|
||||||
@@ -41,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
|
||||||
}
|
}
|
||||||
@@ -91,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 {
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
|
||||||
} else {
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
|
||||||
}
|
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
@@ -139,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
|
||||||
@@ -227,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)
|
||||||
|
}
|
||||||
@@ -162,6 +162,44 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.Pro
|
|||||||
return psr
|
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) {
|
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||||
proxies, err := c.manager.ListStoreProxies()
|
proxies, err := c.manager.ListStoreProxies()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type fakeConfigManager struct {
|
|||||||
getProxyStatusFn func() []*proxy.WorkingStatus
|
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||||
isStoreProxyEnabledFn func(name string) bool
|
isStoreProxyEnabledFn func(name string) bool
|
||||||
storeEnabledFn func() bool
|
storeEnabledFn func() bool
|
||||||
|
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||||
@@ -82,6 +84,20 @@ func (m *fakeConfigManager) StoreEnabled() bool {
|
|||||||
return false
|
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) {
|
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
if m.listStoreProxiesFn != nil {
|
if m.listStoreProxiesFn != nil {
|
||||||
return m.listStoreProxiesFn()
|
return m.listStoreProxiesFn()
|
||||||
@@ -529,3 +545,118 @@ func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
|||||||
t.Fatalf("unexpected response payload: %#v", payload)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,7 +37,6 @@ import (
|
|||||||
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"
|
||||||
@@ -162,15 +161,6 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var webServer *httppkg.Server
|
|
||||||
if options.Common.WebServer.Port > 0 {
|
|
||||||
ws, err := httppkg.NewServer(options.Common.WebServer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
webServer = ws
|
|
||||||
}
|
|
||||||
|
|
||||||
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -191,6 +181,17 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
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
|
||||||
|
if options.Common.WebServer.Port > 0 {
|
||||||
|
ws, err := httppkg.NewServer(options.Common.WebServer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
webServer = ws
|
||||||
|
}
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
auth: authRuntime,
|
auth: authRuntime,
|
||||||
@@ -229,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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -255,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,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 {
|
||||||
@@ -378,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
|
||||||
}
|
}
|
||||||
@@ -497,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) {
|
||||||
@@ -510,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,
|
||||||
|
|||||||
@@ -1,14 +1,120 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
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) {
|
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
||||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||||
|
|||||||
@@ -38,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
|
||||||
@@ -167,15 +167,15 @@ func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, e
|
|||||||
UseEncryption: cfg.Transport.UseEncryption,
|
UseEncryption: cfg.Transport.UseEncryption,
|
||||||
UseCompression: cfg.Transport.UseCompression,
|
UseCompression: cfg.Transport.UseCompression,
|
||||||
}
|
}
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
err = visitorConn.WriteMsg(newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
visitorConn.Close()
|
visitorConn.Close()
|
||||||
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
err = visitorConn.ReadMsgInto(&newVisitorConnRespMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
visitorConn.Close()
|
visitorConn.Close()
|
||||||
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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,8 +1,11 @@
|
|||||||
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.25 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
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.25 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|||||||
32
go.mod
32
go.mod
@@ -5,7 +5,7 @@ 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=
|
||||||
|
|||||||
162
hack/run-e2e-compatibility.sh
Executable file
162
hack/run-e2e-compatibility.sh
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT=$(readlink -f "$0")
|
||||||
|
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
|
||||||
|
|
||||||
|
if ! command -v ginkgo >/dev/null 2>&1; then
|
||||||
|
echo "ginkgo not found, try to install..."
|
||||||
|
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
|
||||||
|
fi
|
||||||
|
|
||||||
|
debug=false
|
||||||
|
if [ "x${DEBUG:-}" = "xtrue" ]; then
|
||||||
|
debug=true
|
||||||
|
fi
|
||||||
|
logLevel=debug
|
||||||
|
if [ "${LOG_LEVEL:-}" ]; then
|
||||||
|
logLevel="${LOG_LEVEL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
currentFrpsPath=${CURRENT_FRPS_PATH:-${ROOT}/bin/frps}
|
||||||
|
currentFrpcPath=${CURRENT_FRPC_PATH:-${ROOT}/bin/frpc}
|
||||||
|
baselineCount=${FRP_COMPAT_BASELINE_COUNT:-8}
|
||||||
|
targetOS=${TARGET_OS:-$(go env GOOS)}
|
||||||
|
targetArch=${TARGET_ARCH:-$(go env GOARCH)}
|
||||||
|
targetPlatform="${targetOS}_${targetArch}"
|
||||||
|
cacheRoot=${FRP_COMPAT_CACHE_DIR:-${ROOT}/.cache/e2e-compat}
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
if [ ! -f "$2" ]; then
|
||||||
|
echo "$1 not found: $2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file "current frps" "${currentFrpsPath}"
|
||||||
|
check_file "current frpc" "${currentFrpcPath}"
|
||||||
|
|
||||||
|
run_current_current=true
|
||||||
|
|
||||||
|
run_compatibility() {
|
||||||
|
baselineVersion=$1
|
||||||
|
baselineFrpsPath=$2
|
||||||
|
baselineFrpcPath=$3
|
||||||
|
|
||||||
|
check_file "baseline frps" "${baselineFrpsPath}"
|
||||||
|
check_file "baseline frpc" "${baselineFrpcPath}"
|
||||||
|
|
||||||
|
echo "Running compatibility e2e with baseline ${baselineVersion}"
|
||||||
|
ginkgo -nodes=1 --poll-progress-after=60s "${ROOT}/test/e2e/compatibility" -- \
|
||||||
|
-current-frps-path="${currentFrpsPath}" \
|
||||||
|
-current-frpc-path="${currentFrpcPath}" \
|
||||||
|
-baseline-frps-path="${baselineFrpsPath}" \
|
||||||
|
-baseline-frpc-path="${baselineFrpcPath}" \
|
||||||
|
-baseline-version="${baselineVersion}" \
|
||||||
|
-run-current-current="${run_current_current}" \
|
||||||
|
-log-level="${logLevel}" \
|
||||||
|
-debug="${debug}"
|
||||||
|
run_current_current=false
|
||||||
|
}
|
||||||
|
|
||||||
|
github_api_curl() {
|
||||||
|
if [ "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
curl -fsSL \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||||
|
"$1"
|
||||||
|
else
|
||||||
|
curl -fsSL "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_versions() {
|
||||||
|
if [ "${FRP_COMPAT_BASELINE_VERSIONS:-}" ]; then
|
||||||
|
printf "%s\n" "${FRP_COMPAT_BASELINE_VERSIONS}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${baselineCount}" in
|
||||||
|
'' | *[!0-9]*)
|
||||||
|
echo "FRP_COMPAT_BASELINE_COUNT must be a positive integer: ${baselineCount}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "${baselineCount}" -eq 0 ]; then
|
||||||
|
echo "FRP_COMPAT_BASELINE_COUNT must be greater than 0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "${baselineCount}" -gt 100 ]; then
|
||||||
|
echo "FRP_COMPAT_BASELINE_COUNT must be less than or equal to 100" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
releaseURL="https://api.github.com/repos/fatedier/frp/releases?per_page=100"
|
||||||
|
resolvedVersions=""
|
||||||
|
if releases=$(github_api_curl "${releaseURL}" 2>/dev/null); then
|
||||||
|
resolvedVersions=$(printf "%s\n" "${releases}" |
|
||||||
|
sed -n 's/.*"tag_name":[[:space:]]*"v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' |
|
||||||
|
awk '!seen[$0]++' |
|
||||||
|
head -n "${baselineCount}" |
|
||||||
|
tr '\n' ' ' |
|
||||||
|
sed 's/[[:space:]]*$//')
|
||||||
|
else
|
||||||
|
echo "Failed to fetch release metadata from GitHub API, falling back to GitHub releases page." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${resolvedVersions}" ]; then
|
||||||
|
releasesPageURL="https://github.com/fatedier/frp/releases"
|
||||||
|
if ! releases=$(curl -fsSL "${releasesPageURL}"); then
|
||||||
|
echo "Failed to fetch release metadata from GitHub: ${releasesPageURL}" >&2
|
||||||
|
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
resolvedVersions=$(printf "%s\n" "${releases}" |
|
||||||
|
grep -o 'releases/tag/v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"' |
|
||||||
|
sed 's#.*/v##; s/"$//' |
|
||||||
|
awk '!seen[$0]++' |
|
||||||
|
head -n "${baselineCount}" |
|
||||||
|
tr '\n' ' ' |
|
||||||
|
sed 's/[[:space:]]*$//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -- ${resolvedVersions}
|
||||||
|
if [ "$#" -lt "${baselineCount}" ]; then
|
||||||
|
echo "Only resolved $# stable release versions from GitHub, expected ${baselineCount}." >&2
|
||||||
|
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s\n" "${resolvedVersions}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASELINE_FRPS_PATH:-}" ] || [ "${BASELINE_FRPC_PATH:-}" ]; then
|
||||||
|
if [ -z "${BASELINE_FRPS_PATH:-}" ] || [ -z "${BASELINE_FRPC_PATH:-}" ]; then
|
||||||
|
echo "BASELINE_FRPS_PATH and BASELINE_FRPC_PATH must be set together"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
run_compatibility "${FRP_COMPAT_BASELINE_VERSION:-custom}" "${BASELINE_FRPS_PATH}" "${BASELINE_FRPC_PATH}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
versions=$(resolve_versions)
|
||||||
|
echo "Compatibility baseline versions: ${versions}"
|
||||||
|
|
||||||
|
mkdir -p "${cacheRoot}"
|
||||||
|
for version in ${versions}; do
|
||||||
|
baselineDir="${cacheRoot}/${version}/${targetPlatform}"
|
||||||
|
if [ ! -f "${baselineDir}/frps" ] || [ ! -f "${baselineDir}/frpc" ]; then
|
||||||
|
tmpDir="${cacheRoot}/.download-${version}-${targetPlatform}"
|
||||||
|
rm -rf "${tmpDir}"
|
||||||
|
(
|
||||||
|
cd "${cacheRoot}"
|
||||||
|
FRP_VERSION="${version}" TARGET_DIRNAME="$(basename "${tmpDir}")" "${ROOT}/hack/download.sh"
|
||||||
|
)
|
||||||
|
mkdir -p "$(dirname "${baselineDir}")"
|
||||||
|
rm -rf "${baselineDir}"
|
||||||
|
mv "${tmpDir}" "${baselineDir}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_compatibility "${version}" "${baselineDir}/frps" "${baselineDir}/frpc"
|
||||||
|
done
|
||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,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}
|
||||||
@@ -100,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,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.
|
||||||
@@ -143,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))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ var (
|
|||||||
"websocket",
|
"websocket",
|
||||||
"wss",
|
"wss",
|
||||||
}
|
}
|
||||||
|
SupportedWireProtocols = []string{
|
||||||
|
"v1",
|
||||||
|
"v2",
|
||||||
|
}
|
||||||
|
|
||||||
SupportedAuthMethods = []v1.AuthMethod{
|
SupportedAuthMethods = []v1.AuthMethod{
|
||||||
"token",
|
"token",
|
||||||
|
|||||||
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{
|
||||||
|
|||||||
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")
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var responseTimeout = 3 * time.Second
|
var responseTimeout = 3 * time.Second
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ type HTTPProxy struct {
|
|||||||
s *http.Server
|
s *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const httpProxyReadHeaderTimeout = 60 * time.Second
|
||||||
|
|
||||||
func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||||
opts := options.(*v1.HTTPProxyPluginOptions)
|
opts := options.(*v1.HTTPProxyPluginOptions)
|
||||||
listener := NewProxyListener()
|
listener := NewProxyListener()
|
||||||
@@ -56,7 +58,7 @@ func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
|
|||||||
|
|
||||||
hp.s = &http.Server{
|
hp.s = &http.Server{
|
||||||
Handler: hp,
|
Handler: hp,
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
ReadHeaderTimeout: httpProxyReadHeaderTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -73,16 +75,19 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
|||||||
wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
|
wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
|
||||||
|
|
||||||
sc, rd := libnet.NewSharedConn(wrapConn)
|
sc, rd := libnet.NewSharedConn(wrapConn)
|
||||||
firstBytes := make([]byte, 7)
|
firstBytes := make([]byte, len(http.MethodConnect))
|
||||||
_, err := rd.Read(firstBytes)
|
_ = wrapConn.SetReadDeadline(time.Now().Add(httpProxyReadHeaderTimeout))
|
||||||
|
_, err := io.ReadFull(rd, firstBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||||
wrapConn.Close()
|
wrapConn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.ToUpper(string(firstBytes)) == "CONNECT" {
|
if strings.EqualFold(string(firstBytes), http.MethodConnect) {
|
||||||
bufRd := bufio.NewReader(sc)
|
bufRd := bufio.NewReader(sc)
|
||||||
request, err := http.ReadRequest(bufRd)
|
request, err := http.ReadRequest(bufRd)
|
||||||
|
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wrapConn.Close()
|
wrapConn.Close()
|
||||||
return
|
return
|
||||||
@@ -91,6 +96,7 @@ func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = wrapConn.SetReadDeadline(time.Time{})
|
||||||
_ = hp.l.PutConn(sc)
|
_ = hp.l.PutConn(sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +113,7 @@ func (hp *HTTPProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Method == http.MethodConnect {
|
hp.HTTPHandler(rw, req)
|
||||||
// deprecated
|
|
||||||
// Connect request is handled in Handle function.
|
|
||||||
hp.ConnectHandler(rw, req)
|
|
||||||
} else {
|
|
||||||
hp.HTTPHandler(rw, req)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) {
|
func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
@@ -135,33 +135,6 @@ func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// deprecated
|
|
||||||
// Hijack needs to SetReadDeadline on the Conn of the request, but if we use stream compression here,
|
|
||||||
// we may always get i/o timeout error.
|
|
||||||
func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
hj, ok := rw.(http.Hijacker)
|
|
||||||
if !ok {
|
|
||||||
rw.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _, err := hj.Hijack()
|
|
||||||
if err != nil {
|
|
||||||
rw.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
remote, err := net.Dial("tcp", req.URL.Host)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, "Failed", http.StatusBadRequest)
|
|
||||||
client.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = client.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
|
|
||||||
|
|
||||||
go libio.Join(remote, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hp *HTTPProxy) Auth(req *http.Request) bool {
|
func (hp *HTTPProxy) Auth(req *http.Request) bool {
|
||||||
if hp.opts.HTTPUser == "" && hp.opts.HTTPPassword == "" {
|
if hp.opts.HTTPUser == "" && hp.opts.HTTPPassword == "" {
|
||||||
return true
|
return true
|
||||||
|
|||||||
107
pkg/plugin/client/http_proxy_test.go
Normal file
107
pkg/plugin/client/http_proxy_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go:build !frps
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPProxyHandleFragmentedConnectMethod(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(err)
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
const payload = "ping"
|
||||||
|
echoErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
echoErr <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, len(payload))
|
||||||
|
if _, err = io.ReadFull(conn, buf); err != nil {
|
||||||
|
echoErr <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(buf) != payload {
|
||||||
|
echoErr <- fmt.Errorf("unexpected payload %q", string(buf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = conn.Write([]byte("echo:" + payload))
|
||||||
|
echoErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
hp := &HTTPProxy{
|
||||||
|
opts: &v1.HTTPProxyPluginOptions{},
|
||||||
|
l: NewProxyListener(),
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
go hp.Handle(context.Background(), &ConnectionInfo{
|
||||||
|
Conn: serverConn,
|
||||||
|
UnderlyingConn: serverConn,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(clientConn.SetDeadline(time.Now().Add(5 * time.Second)))
|
||||||
|
|
||||||
|
targetAddr := ln.Addr().String()
|
||||||
|
req := "CONNECT " + targetAddr + " HTTP/1.1\r\nHost: " + targetAddr + "\r\n\r\n"
|
||||||
|
_, err = clientConn.Write([]byte("CON"))
|
||||||
|
require.NoError(err)
|
||||||
|
_, err = clientConn.Write([]byte(req[len("CON"):]))
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
rd := bufio.NewReader(clientConn)
|
||||||
|
status, err := rd.ReadString('\n')
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("HTTP/1.1 200 OK\r\n", status)
|
||||||
|
line, err := rd.ReadString('\n')
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("\r\n", line)
|
||||||
|
|
||||||
|
_, err = clientConn.Write([]byte(payload))
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
got := make([]byte, len("echo:"+payload))
|
||||||
|
_, err = io.ReadFull(rd, got)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("echo:"+payload, string(got))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-echoErr:
|
||||||
|
require.NoError(err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for echo server")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,8 +93,7 @@ type featureGate struct {
|
|||||||
|
|
||||||
// NewFeatureGate creates a new feature gate with the default features
|
// NewFeatureGate creates a new feature gate with the default features
|
||||||
func NewFeatureGate() MutableFeatureGate {
|
func NewFeatureGate() MutableFeatureGate {
|
||||||
known := map[Feature]FeatureSpec{}
|
known := maps.Clone(defaultFeatures)
|
||||||
maps.Copy(known, defaultFeatures)
|
|
||||||
|
|
||||||
f := &featureGate{}
|
f := &featureGate{}
|
||||||
f.known.Store(known)
|
f.known.Store(known)
|
||||||
@@ -108,10 +107,8 @@ func (f *featureGate) SetFromMap(m map[string]bool) error {
|
|||||||
defer f.lock.Unlock()
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
// Copy existing state
|
// Copy existing state
|
||||||
known := map[Feature]FeatureSpec{}
|
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))
|
||||||
maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec))
|
enabled := maps.Clone(f.enabled.Load().(map[Feature]bool))
|
||||||
enabled := map[Feature]bool{}
|
|
||||||
maps.Copy(enabled, f.enabled.Load().(map[Feature]bool))
|
|
||||||
|
|
||||||
// Apply the new settings
|
// Apply the new settings
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@@ -142,8 +139,7 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy existing state
|
// Copy existing state
|
||||||
known := map[Feature]FeatureSpec{}
|
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))
|
||||||
maps.Copy(known, f.known.Load().(map[Feature]FeatureSpec))
|
|
||||||
|
|
||||||
// Add new features
|
// Add new features
|
||||||
for name, spec := range features {
|
for name, spec := range features {
|
||||||
|
|||||||
222
pkg/proto/wire/wire.go
Normal file
222
pkg/proto/wire/wire.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// 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 wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
libnet "github.com/fatedier/golib/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProtocolV1 = "v1"
|
||||||
|
ProtocolV2 = "v2"
|
||||||
|
|
||||||
|
WireVersionV2 = 2
|
||||||
|
|
||||||
|
FrameTypeClientHello uint16 = 1
|
||||||
|
FrameTypeServerHello uint16 = 2
|
||||||
|
FrameTypeMessage uint16 = 16
|
||||||
|
|
||||||
|
MessageCodecJSON = "json"
|
||||||
|
DefaultMaxFramePayloadSize = 64 * 1024
|
||||||
|
|
||||||
|
MagicV2 = "FRP\x00\x02\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Frame struct {
|
||||||
|
Type uint16
|
||||||
|
Flags uint16
|
||||||
|
Payload []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
rw io.ReadWriter
|
||||||
|
maxFramePayloadSize uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(rw io.ReadWriter) *Conn {
|
||||||
|
return &Conn{
|
||||||
|
rw: rw,
|
||||||
|
maxFramePayloadSize: DefaultMaxFramePayloadSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadFrame() (*Frame, error) {
|
||||||
|
header := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(c.rw, header); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
frameType := binary.BigEndian.Uint16(header[0:2])
|
||||||
|
flags := binary.BigEndian.Uint16(header[2:4])
|
||||||
|
length := binary.BigEndian.Uint32(header[4:8])
|
||||||
|
if flags != 0 {
|
||||||
|
return nil, fmt.Errorf("unsupported frame flags: %d", flags)
|
||||||
|
}
|
||||||
|
if length > c.maxFramePayloadSize {
|
||||||
|
return nil, fmt.Errorf("frame payload length %d exceeds limit %d", length, c.maxFramePayloadSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(c.rw, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Frame{
|
||||||
|
Type: frameType,
|
||||||
|
Flags: flags,
|
||||||
|
Payload: payload,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriteFrame(f *Frame) error {
|
||||||
|
if f.Flags != 0 {
|
||||||
|
return fmt.Errorf("unsupported frame flags: %d", f.Flags)
|
||||||
|
}
|
||||||
|
if len(f.Payload) > int(c.maxFramePayloadSize) {
|
||||||
|
return fmt.Errorf("frame payload length %d exceeds limit %d", len(f.Payload), c.maxFramePayloadSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
header := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint16(header[0:2], f.Type)
|
||||||
|
binary.BigEndian.PutUint16(header[2:4], f.Flags)
|
||||||
|
binary.BigEndian.PutUint32(header[4:8], uint32(len(f.Payload)))
|
||||||
|
if _, err := c.rw.Write(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := c.rw.Write(f.Payload)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadJSONFrame(frameType uint16, out any) error {
|
||||||
|
f, err := c.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if f.Type != frameType {
|
||||||
|
return fmt.Errorf("unexpected frame type %d, want %d", f.Type, frameType)
|
||||||
|
}
|
||||||
|
return c.UnmarshalFrame(f, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) UnmarshalFrame(f *Frame, out any) error {
|
||||||
|
return json.Unmarshal(f.Payload, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriteJSONFrame(frameType uint16, in any) error {
|
||||||
|
payload, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.WriteFrame(&Frame{
|
||||||
|
Type: frameType,
|
||||||
|
Payload: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteMagic(w io.Writer) error {
|
||||||
|
_, err := io.WriteString(w, MagicV2)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteMagicIfV2(w io.Writer, wireProtocol string) error {
|
||||||
|
if wireProtocol != ProtocolV2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return WriteMagic(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckMagic(conn net.Conn) (out net.Conn, isV2 bool, err error) {
|
||||||
|
sharedConn, r := libnet.NewSharedConnSize(conn, len(MagicV2))
|
||||||
|
buf := make([]byte, len(MagicV2))
|
||||||
|
if _, err = io.ReadFull(r, buf); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
for i := range MagicV2 {
|
||||||
|
if buf[i] != MagicV2[i] {
|
||||||
|
return sharedConn, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootstrapInfo struct {
|
||||||
|
Transport string `json:"transport,omitempty"`
|
||||||
|
TLS bool `json:"tls,omitempty"`
|
||||||
|
TCPMux bool `json:"tcpMux,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientHello struct {
|
||||||
|
Bootstrap BootstrapInfo `json:"bootstrap,omitempty"`
|
||||||
|
Capabilities ClientCapabilities `json:"capabilities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientCapabilities struct {
|
||||||
|
Message MessageCapabilities `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageCapabilities struct {
|
||||||
|
Codecs []string `json:"codecs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerHello struct {
|
||||||
|
Selected ServerSelection `json:"selected,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerSelection struct {
|
||||||
|
Message MessageSelection `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageSelection struct {
|
||||||
|
Codec string `json:"codec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultClientHello(bootstrap BootstrapInfo) ClientHello {
|
||||||
|
return ClientHello{
|
||||||
|
Bootstrap: bootstrap,
|
||||||
|
Capabilities: ClientCapabilities{
|
||||||
|
Message: MessageCapabilities{
|
||||||
|
Codecs: []string{MessageCodecJSON},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultServerHello() ServerHello {
|
||||||
|
return ServerHello{
|
||||||
|
Selected: ServerSelection{
|
||||||
|
Message: MessageSelection{
|
||||||
|
Codec: MessageCodecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Supports(list []string, value string) bool {
|
||||||
|
return slices.Contains(list, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateClientHello(h ClientHello) error {
|
||||||
|
if !Supports(h.Capabilities.Message.Codecs, MessageCodecJSON) {
|
||||||
|
return fmt.Errorf("unsupported message codec")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
120
pkg/proto/wire/wire_test.go
Normal file
120
pkg/proto/wire/wire_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// 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 wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFrameRoundTrip(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
conn := NewConn(&buf)
|
||||||
|
|
||||||
|
in := DefaultClientHello(BootstrapInfo{
|
||||||
|
Transport: "tcp",
|
||||||
|
TLS: true,
|
||||||
|
TCPMux: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, conn.WriteJSONFrame(FrameTypeClientHello, in))
|
||||||
|
|
||||||
|
var out ClientHello
|
||||||
|
require.NoError(t, conn.ReadJSONFrame(FrameTypeClientHello, &out))
|
||||||
|
require.Equal(t, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadFrameRejectsUnsupportedFlags(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
header := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint16(header[0:2], FrameTypeMessage)
|
||||||
|
binary.BigEndian.PutUint16(header[2:4], 1)
|
||||||
|
binary.BigEndian.PutUint32(header[4:8], 0)
|
||||||
|
buf.Write(header)
|
||||||
|
|
||||||
|
_, err := NewConn(&buf).ReadFrame()
|
||||||
|
require.ErrorContains(t, err, "unsupported frame flags")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadFrameRejectsOversizedPayload(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
header := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint16(header[0:2], FrameTypeMessage)
|
||||||
|
binary.BigEndian.PutUint32(header[4:8], DefaultMaxFramePayloadSize+1)
|
||||||
|
buf.Write(header)
|
||||||
|
|
||||||
|
_, err := NewConn(&buf).ReadFrame()
|
||||||
|
require.ErrorContains(t, err, "exceeds limit")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckMagicV2ConsumesMagic(t *testing.T) {
|
||||||
|
client, server := net.Pipe()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
want := []byte("payload")
|
||||||
|
go func() {
|
||||||
|
defer client.Close()
|
||||||
|
_, _ = client.Write(append([]byte(MagicV2), want...))
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, isV2, err := CheckMagic(server)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, isV2)
|
||||||
|
|
||||||
|
got := make([]byte, len(want))
|
||||||
|
_, err = io.ReadFull(out, got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteMagicIfV2(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
require.NoError(t, WriteMagicIfV2(&buf, ProtocolV1))
|
||||||
|
require.Empty(t, buf.Bytes())
|
||||||
|
|
||||||
|
require.NoError(t, WriteMagicIfV2(&buf, ProtocolV2))
|
||||||
|
require.Equal(t, []byte(MagicV2), buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckMagicV1PreservesReadBytes(t *testing.T) {
|
||||||
|
client, server := net.Pipe()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
want := []byte("legacy payload")
|
||||||
|
go func() {
|
||||||
|
defer client.Close()
|
||||||
|
_, _ = client.Write(want)
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, isV2, err := CheckMagic(server)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, isV2)
|
||||||
|
|
||||||
|
got, err := io.ReadAll(out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClientHello(t *testing.T) {
|
||||||
|
require.NoError(t, ValidateClientHello(DefaultClientHello(BootstrapInfo{})))
|
||||||
|
|
||||||
|
hello := DefaultClientHello(BootstrapInfo{})
|
||||||
|
hello.Capabilities.Message.Codecs = []string{"unknown"}
|
||||||
|
require.ErrorContains(t, ValidateClientHello(hello), "unsupported message codec")
|
||||||
|
}
|
||||||
@@ -100,7 +100,11 @@ func (s *Server) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Close() error {
|
func (s *Server) Close() error {
|
||||||
return s.hs.Close()
|
err := s.hs.Close()
|
||||||
|
if s.ln != nil {
|
||||||
|
_ = s.ln.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouterRegisterHelper struct {
|
type RouterRegisterHelper struct {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ type CloseNotifyConn struct {
|
|||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
// 1 means closed
|
// 1 means closed
|
||||||
closeFlag int32
|
closeFlag atomic.Int32
|
||||||
|
|
||||||
closeFn func(error)
|
closeFn func(error)
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cc *CloseNotifyConn) Close() (err error) {
|
func (cc *CloseNotifyConn) Close() (err error) {
|
||||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
pflag := cc.closeFlag.Swap(1)
|
||||||
if pflag == 0 {
|
if pflag == 0 {
|
||||||
err = cc.Conn.Close()
|
err = cc.Conn.Close()
|
||||||
if cc.closeFn != nil {
|
if cc.closeFn != nil {
|
||||||
@@ -159,7 +159,7 @@ func (cc *CloseNotifyConn) Close() (err error) {
|
|||||||
|
|
||||||
// CloseWithError closes the connection and passes the error to the close callback.
|
// CloseWithError closes the connection and passes the error to the close callback.
|
||||||
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
||||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
pflag := cc.closeFlag.Swap(1)
|
||||||
if pflag == 0 {
|
if pflag == 0 {
|
||||||
closeErr := cc.Conn.Close()
|
closeErr := cc.Conn.Close()
|
||||||
if cc.closeFn != nil {
|
if cc.closeFn != nil {
|
||||||
@@ -173,7 +173,7 @@ func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
|||||||
type StatsConn struct {
|
type StatsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
closed int64 // 1 means closed
|
closed atomic.Int64 // 1 means closed
|
||||||
totalRead int64
|
totalRead int64
|
||||||
totalWrite int64
|
totalWrite int64
|
||||||
statsFunc func(totalRead, totalWrite int64)
|
statsFunc func(totalRead, totalWrite int64)
|
||||||
@@ -199,7 +199,7 @@ func (statsConn *StatsConn) Write(p []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (statsConn *StatsConn) Close() (err error) {
|
func (statsConn *StatsConn) Close() (err error) {
|
||||||
old := atomic.SwapInt64(&statsConn.closed, 1)
|
old := statsConn.closed.Swap(1)
|
||||||
if old != 1 {
|
if old != 1 {
|
||||||
err = statsConn.Conn.Close()
|
err = statsConn.Conn.Close()
|
||||||
if statsConn.statsFunc != nil {
|
if statsConn.statsFunc != nil {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "0.68.0"
|
var version = "0.69.0"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
@@ -187,16 +187,25 @@ func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byE
|
|||||||
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
|
func checkRouteAuthByRequest(req *http.Request, rc *RouteConfig) bool {
|
||||||
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
|
if rc == nil {
|
||||||
if ok {
|
return true
|
||||||
checkUser := vr.payload.(*RouteConfig).Username
|
}
|
||||||
checkPasswd := vr.payload.(*RouteConfig).Password
|
if rc.Username == "" && rc.Password == "" {
|
||||||
if (checkUser != "" || checkPasswd != "") && (checkUser != user || checkPasswd != passwd) {
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.URL.Host != "" {
|
||||||
|
proxyAuth := req.Header.Get("Proxy-Authorization")
|
||||||
|
if proxyAuth == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
user, passwd, ok := httppkg.ParseBasicAuth(proxyAuth)
|
||||||
|
return ok && user == rc.Username && passwd == rc.Password
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
user, passwd, ok := req.BasicAuth()
|
||||||
|
return ok && user == rc.Username && passwd == rc.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVhost tries to get vhost router by route policy.
|
// getVhost tries to get vhost router by route policy.
|
||||||
@@ -266,18 +275,25 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
|
|||||||
go libio.Join(remote, client)
|
go libio.Join(remote, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
func getRequestRouteUser(req *http.Request) string {
|
||||||
user := ""
|
|
||||||
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
|
|
||||||
if req.URL.Host != "" {
|
if req.URL.Host != "" {
|
||||||
proxyAuth := req.Header.Get("Proxy-Authorization")
|
proxyAuth := req.Header.Get("Proxy-Authorization")
|
||||||
if proxyAuth != "" {
|
if proxyAuth == "" {
|
||||||
user, _, _ = httppkg.ParseBasicAuth(proxyAuth)
|
// Preserve legacy proxy-mode routing when clients send only Authorization,
|
||||||
|
// so requests still hit the matched route and return 407 instead of 404.
|
||||||
|
// Auth validation intentionally does not share this fallback.
|
||||||
|
user, _, _ := req.BasicAuth()
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
user, _, _ := httppkg.ParseBasicAuth(proxyAuth)
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
if user == "" {
|
user, _, _ := req.BasicAuth()
|
||||||
user, _, _ = req.BasicAuth()
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
||||||
|
user := getRequestRouteUser(req)
|
||||||
|
|
||||||
reqRouteInfo := &RequestRouteInfo{
|
reqRouteInfo := &RequestRouteInfo{
|
||||||
URL: req.URL.Path,
|
URL: req.URL.Path,
|
||||||
@@ -297,16 +313,19 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
domain, _ := httppkg.CanonicalHost(req.Host)
|
newreq := rp.injectRequestInfoToCtx(req)
|
||||||
location := req.URL.Path
|
rc := newreq.Context().Value(RouteConfigKey).(*RouteConfig)
|
||||||
user, passwd, _ := req.BasicAuth()
|
if !checkRouteAuthByRequest(req, rc) {
|
||||||
if !rp.CheckAuth(domain, location, user, user, passwd) {
|
if req.URL.Host != "" {
|
||||||
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
rw.Header().Set("Proxy-Authenticate", `Basic realm="Restricted"`)
|
||||||
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(rw, http.StatusText(http.StatusProxyAuthRequired), http.StatusProxyAuthRequired)
|
||||||
|
} else {
|
||||||
|
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newreq := rp.injectRequestInfoToCtx(req)
|
|
||||||
if req.Method == http.MethodConnect {
|
if req.Method == http.MethodConnect {
|
||||||
rp.connectHandler(rw, newreq)
|
rp.connectHandler(rw, newreq)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
102
pkg/util/vhost/http_test.go
Normal file
102
pkg/util/vhost/http_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package vhost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckRouteAuthByRequest(t *testing.T) {
|
||||||
|
rc := &RouteConfig{
|
||||||
|
Username: "alice",
|
||||||
|
Password: "secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("accepts nil route config", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts route without credentials", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, &RouteConfig{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts authorization header", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts proxy authorization header", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects authorization fallback for proxy request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects when neither header matches", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "wrong")
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects proxy authorization on direct request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequestRouteUser(t *testing.T) {
|
||||||
|
t.Run("proxy request uses proxy authorization username", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Host = "target.example.com"
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "proxy-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connect request keeps proxy authorization routing", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil)
|
||||||
|
req.Host = "target.example.com:443"
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "proxy-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("direct request uses authorization username", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.Host = "example.com"
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "direct-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Host = "target.example.com"
|
||||||
|
req.Header.Set("Proxy-Authorization", "Basic !!!")
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Empty(t, getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -131,6 +131,9 @@ func (c *Controller) handlePacket(buf []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) Stop() error {
|
func (c *Controller) Stop() error {
|
||||||
|
if c.tun == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return c.tun.Close()
|
return c.tun.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -32,9 +31,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
||||||
"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/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
"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/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
@@ -108,9 +105,7 @@ type SessionContext struct {
|
|||||||
// key used for connection encryption
|
// key used for connection encryption
|
||||||
EncryptionKey []byte
|
EncryptionKey []byte
|
||||||
// control connection
|
// control connection
|
||||||
Conn net.Conn
|
Conn *msg.Conn
|
||||||
// indicates whether the connection is encrypted
|
|
||||||
ConnEncrypted bool
|
|
||||||
// login message
|
// login message
|
||||||
LoginMsg *msg.Login
|
LoginMsg *msg.Login
|
||||||
// server configuration
|
// server configuration
|
||||||
@@ -131,7 +126,7 @@ type Control struct {
|
|||||||
msgDispatcher *msg.Dispatcher
|
msgDispatcher *msg.Dispatcher
|
||||||
|
|
||||||
// work connections
|
// work connections
|
||||||
workConnCh chan net.Conn
|
workConnCh chan *proxy.WorkConn
|
||||||
|
|
||||||
// proxies in one client
|
// proxies in one client
|
||||||
proxies map[string]proxy.Proxy
|
proxies map[string]proxy.Proxy
|
||||||
@@ -161,7 +156,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount))
|
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount))
|
||||||
ctl := &Control{
|
ctl := &Control{
|
||||||
sessionCtx: sessionCtx,
|
sessionCtx: sessionCtx,
|
||||||
workConnCh: make(chan net.Conn, poolCount+10),
|
workConnCh: make(chan *proxy.WorkConn, poolCount+10),
|
||||||
proxies: make(map[string]proxy.Proxy),
|
proxies: make(map[string]proxy.Proxy),
|
||||||
poolCount: poolCount,
|
poolCount: poolCount,
|
||||||
portsUsedNum: 0,
|
portsUsedNum: 0,
|
||||||
@@ -172,29 +167,14 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
}
|
}
|
||||||
ctl.lastPing.Store(time.Now())
|
ctl.lastPing.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.EncryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
|
||||||
} else {
|
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
|
||||||
}
|
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start send a login success message to client and start working.
|
// Start starts the control session workers after login succeeds.
|
||||||
func (ctl *Control) Start() {
|
func (ctl *Control) Start() {
|
||||||
loginRespMsg := &msg.LoginResp{
|
|
||||||
Version: version.Full(),
|
|
||||||
RunID: ctl.runID,
|
|
||||||
Error: "",
|
|
||||||
}
|
|
||||||
_ = msg.WriteMsg(ctl.sessionCtx.Conn, loginRespMsg)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for i := 0; i < ctl.poolCount; i++ {
|
for i := 0; i < ctl.poolCount; i++ {
|
||||||
// ignore error here, that means that this control is closed
|
// ignore error here, that means that this control is closed
|
||||||
@@ -216,7 +196,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
|
|||||||
ctl.sessionCtx.Conn.Close()
|
ctl.sessionCtx.Conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
|
func (ctl *Control) RegisterWorkConn(conn *proxy.WorkConn) error {
|
||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
@@ -239,7 +219,7 @@ func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
|
|||||||
// If no workConn available in the pool, send message to frpc to get one or more
|
// If no workConn available in the pool, send message to frpc to get one or more
|
||||||
// and wait until it is available.
|
// and wait until it is available.
|
||||||
// return an error if wait timeout
|
// return an error if wait timeout
|
||||||
func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
func (ctl *Control) GetWorkConn() (workConn *proxy.WorkConn, err error) {
|
||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
|
|||||||
77
server/group/base.go
Normal file
77
server/group/base.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
gerr "github.com/fatedier/golib/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// baseGroup contains the shared plumbing for listener-based groups
|
||||||
|
// (TCP, HTTPS, TCPMux). Each concrete group embeds this and provides
|
||||||
|
// its own Listen method with protocol-specific validation.
|
||||||
|
type baseGroup struct {
|
||||||
|
group string
|
||||||
|
groupKey string
|
||||||
|
|
||||||
|
acceptCh chan net.Conn
|
||||||
|
realLn net.Listener
|
||||||
|
lns []*Listener
|
||||||
|
mu sync.Mutex
|
||||||
|
cleanupFn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initBase resets the baseGroup for a fresh listen cycle.
|
||||||
|
// Must be called under mu when len(lns) == 0.
|
||||||
|
func (bg *baseGroup) initBase(group, groupKey string, realLn net.Listener, cleanupFn func()) {
|
||||||
|
bg.group = group
|
||||||
|
bg.groupKey = groupKey
|
||||||
|
bg.realLn = realLn
|
||||||
|
bg.acceptCh = make(chan net.Conn)
|
||||||
|
bg.cleanupFn = cleanupFn
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker reads from the real listener and fans out to acceptCh.
|
||||||
|
// The parameters are captured at creation time so that the worker is
|
||||||
|
// bound to a specific listen cycle and cannot observe a later initBase.
|
||||||
|
func (bg *baseGroup) worker(realLn net.Listener, acceptCh chan<- net.Conn) {
|
||||||
|
for {
|
||||||
|
c, err := realLn.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = gerr.PanicToError(func() {
|
||||||
|
acceptCh <- c
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newListener creates a new Listener wired to this baseGroup.
|
||||||
|
// Must be called under mu.
|
||||||
|
func (bg *baseGroup) newListener(addr net.Addr) *Listener {
|
||||||
|
ln := newListener(bg.acceptCh, addr, bg.closeListener)
|
||||||
|
bg.lns = append(bg.lns, ln)
|
||||||
|
return ln
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeListener removes ln from the list. When the last listener is removed,
|
||||||
|
// it closes acceptCh, closes the real listener, and calls cleanupFn.
|
||||||
|
func (bg *baseGroup) closeListener(ln *Listener) {
|
||||||
|
bg.mu.Lock()
|
||||||
|
defer bg.mu.Unlock()
|
||||||
|
for i, l := range bg.lns {
|
||||||
|
if l == ln {
|
||||||
|
bg.lns = append(bg.lns[:i], bg.lns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(bg.lns) == 0 {
|
||||||
|
close(bg.acceptCh)
|
||||||
|
bg.realLn.Close()
|
||||||
|
bg.cleanupFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
169
server/group/base_test.go
Normal file
169
server/group/base_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeLn is a controllable net.Listener for tests.
|
||||||
|
type fakeLn struct {
|
||||||
|
connCh chan net.Conn
|
||||||
|
closed chan struct{}
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakeLn() *fakeLn {
|
||||||
|
return &fakeLn{
|
||||||
|
connCh: make(chan net.Conn, 8),
|
||||||
|
closed: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLn) Accept() (net.Conn, error) {
|
||||||
|
select {
|
||||||
|
case c := <-f.connCh:
|
||||||
|
return c, nil
|
||||||
|
case <-f.closed:
|
||||||
|
return nil, net.ErrClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLn) Close() error {
|
||||||
|
f.once.Do(func() { close(f.closed) })
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLn) Addr() net.Addr { return fakeAddr("127.0.0.1:9999") }
|
||||||
|
|
||||||
|
func (f *fakeLn) inject(c net.Conn) {
|
||||||
|
select {
|
||||||
|
case f.connCh <- c:
|
||||||
|
case <-f.closed:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseGroup_WorkerFanOut(t *testing.T) {
|
||||||
|
fl := newFakeLn()
|
||||||
|
var bg baseGroup
|
||||||
|
bg.initBase("g", "key", fl, func() {})
|
||||||
|
|
||||||
|
go bg.worker(fl, bg.acceptCh)
|
||||||
|
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c2.Close()
|
||||||
|
fl.inject(c1)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case got := <-bg.acceptCh:
|
||||||
|
assert.Equal(t, c1, got)
|
||||||
|
got.Close()
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for connection on acceptCh")
|
||||||
|
}
|
||||||
|
|
||||||
|
fl.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseGroup_WorkerStopsOnListenerClose(t *testing.T) {
|
||||||
|
fl := newFakeLn()
|
||||||
|
var bg baseGroup
|
||||||
|
bg.initBase("g", "key", fl, func() {})
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
bg.worker(fl, bg.acceptCh)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fl.Close()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("worker did not stop after listener close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseGroup_WorkerClosesConnOnClosedChannel(t *testing.T) {
|
||||||
|
fl := newFakeLn()
|
||||||
|
var bg baseGroup
|
||||||
|
bg.initBase("g", "key", fl, func() {})
|
||||||
|
|
||||||
|
// Close acceptCh before worker sends.
|
||||||
|
close(bg.acceptCh)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
bg.worker(fl, bg.acceptCh)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c2.Close()
|
||||||
|
fl.inject(c1)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("worker did not stop after panic recovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// c1 should have been closed by worker's panic recovery path.
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := c1.Read(buf)
|
||||||
|
assert.Error(t, err, "connection should be closed by worker")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseGroup_CloseLastListenerTriggersCleanup(t *testing.T) {
|
||||||
|
fl := newFakeLn()
|
||||||
|
var bg baseGroup
|
||||||
|
cleanupCalled := 0
|
||||||
|
bg.initBase("g", "key", fl, func() { cleanupCalled++ })
|
||||||
|
|
||||||
|
bg.mu.Lock()
|
||||||
|
ln1 := bg.newListener(fl.Addr())
|
||||||
|
ln2 := bg.newListener(fl.Addr())
|
||||||
|
bg.mu.Unlock()
|
||||||
|
|
||||||
|
go bg.worker(fl, bg.acceptCh)
|
||||||
|
|
||||||
|
ln1.Close()
|
||||||
|
assert.Equal(t, 0, cleanupCalled, "cleanup should not run while listeners remain")
|
||||||
|
|
||||||
|
ln2.Close()
|
||||||
|
assert.Equal(t, 1, cleanupCalled, "cleanup should run after last listener closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseGroup_CloseOneOfTwoListeners(t *testing.T) {
|
||||||
|
fl := newFakeLn()
|
||||||
|
var bg baseGroup
|
||||||
|
cleanupCalled := 0
|
||||||
|
bg.initBase("g", "key", fl, func() { cleanupCalled++ })
|
||||||
|
|
||||||
|
bg.mu.Lock()
|
||||||
|
ln1 := bg.newListener(fl.Addr())
|
||||||
|
ln2 := bg.newListener(fl.Addr())
|
||||||
|
bg.mu.Unlock()
|
||||||
|
|
||||||
|
go bg.worker(fl, bg.acceptCh)
|
||||||
|
|
||||||
|
ln1.Close()
|
||||||
|
assert.Equal(t, 0, cleanupCalled)
|
||||||
|
|
||||||
|
// ln2 should still receive connections.
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c2.Close()
|
||||||
|
fl.inject(c1)
|
||||||
|
|
||||||
|
got, err := ln2.Accept()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c1, got)
|
||||||
|
got.Close()
|
||||||
|
|
||||||
|
ln2.Close()
|
||||||
|
assert.Equal(t, 1, cleanupCalled)
|
||||||
|
}
|
||||||
@@ -24,4 +24,6 @@ var (
|
|||||||
ErrListenerClosed = errors.New("group listener closed")
|
ErrListenerClosed = errors.New("group listener closed")
|
||||||
ErrGroupDifferentPort = errors.New("group should have same remote port")
|
ErrGroupDifferentPort = errors.New("group should have same remote port")
|
||||||
ErrProxyRepeated = errors.New("group proxy repeated")
|
ErrProxyRepeated = errors.New("group proxy repeated")
|
||||||
|
|
||||||
|
errGroupStale = errors.New("stale group reference")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,53 +9,42 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTTPGroupController manages HTTP groups that use round-robin
|
||||||
|
// callback routing (fundamentally different from listener-based groups).
|
||||||
type HTTPGroupController struct {
|
type HTTPGroupController struct {
|
||||||
// groups indexed by group name
|
groupRegistry[*HTTPGroup]
|
||||||
groups map[string]*HTTPGroup
|
|
||||||
|
|
||||||
// register createConn for each group to vhostRouter.
|
|
||||||
// createConn will get a connection from one proxy of the group
|
|
||||||
vhostRouter *vhost.Routers
|
vhostRouter *vhost.Routers
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
|
func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
|
||||||
return &HTTPGroupController{
|
return &HTTPGroupController{
|
||||||
groups: make(map[string]*HTTPGroup),
|
groupRegistry: newGroupRegistry[*HTTPGroup](),
|
||||||
vhostRouter: vhostRouter,
|
vhostRouter: vhostRouter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *HTTPGroupController) Register(
|
func (ctl *HTTPGroupController) Register(
|
||||||
proxyName, group, groupKey string,
|
proxyName, group, groupKey string,
|
||||||
routeConfig vhost.RouteConfig,
|
routeConfig vhost.RouteConfig,
|
||||||
) (err error) {
|
) error {
|
||||||
indexKey := group
|
for {
|
||||||
ctl.mu.Lock()
|
g := ctl.getOrCreate(group, func() *HTTPGroup {
|
||||||
g, ok := ctl.groups[indexKey]
|
return NewHTTPGroup(ctl)
|
||||||
if !ok {
|
})
|
||||||
g = NewHTTPGroup(ctl)
|
err := g.Register(proxyName, group, groupKey, routeConfig)
|
||||||
ctl.groups[indexKey] = g
|
if err == errGroupStale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
ctl.mu.Unlock()
|
|
||||||
|
|
||||||
return g.Register(proxyName, group, groupKey, routeConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) {
|
func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) {
|
||||||
indexKey := group
|
g, ok := ctl.get(group)
|
||||||
ctl.mu.Lock()
|
|
||||||
defer ctl.mu.Unlock()
|
|
||||||
g, ok := ctl.groups[indexKey]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
g.UnRegister(proxyName)
|
||||||
isEmpty := g.UnRegister(proxyName)
|
|
||||||
if isEmpty {
|
|
||||||
delete(ctl.groups, indexKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPGroup struct {
|
type HTTPGroup struct {
|
||||||
@@ -68,7 +57,7 @@ type HTTPGroup struct {
|
|||||||
// CreateConnFuncs indexed by proxy name
|
// CreateConnFuncs indexed by proxy name
|
||||||
createFuncs map[string]vhost.CreateConnFunc
|
createFuncs map[string]vhost.CreateConnFunc
|
||||||
pxyNames []string
|
pxyNames []string
|
||||||
index uint64
|
index atomic.Uint64
|
||||||
ctl *HTTPGroupController
|
ctl *HTTPGroupController
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -87,6 +76,9 @@ func (g *HTTPGroup) Register(
|
|||||||
) (err error) {
|
) (err error) {
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
defer g.mu.Unlock()
|
defer g.mu.Unlock()
|
||||||
|
if !g.ctl.isCurrent(group, func(cur *HTTPGroup) bool { return cur == g }) {
|
||||||
|
return errGroupStale
|
||||||
|
}
|
||||||
if len(g.createFuncs) == 0 {
|
if len(g.createFuncs) == 0 {
|
||||||
// the first proxy in this group
|
// the first proxy in this group
|
||||||
tmp := routeConfig // copy object
|
tmp := routeConfig // copy object
|
||||||
@@ -123,7 +115,7 @@ func (g *HTTPGroup) Register(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
|
func (g *HTTPGroup) UnRegister(proxyName string) {
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
defer g.mu.Unlock()
|
defer g.mu.Unlock()
|
||||||
delete(g.createFuncs, proxyName)
|
delete(g.createFuncs, proxyName)
|
||||||
@@ -135,15 +127,16 @@ func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(g.createFuncs) == 0 {
|
if len(g.createFuncs) == 0 {
|
||||||
isEmpty = true
|
|
||||||
g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser)
|
g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser)
|
||||||
|
g.ctl.removeIf(g.group, func(cur *HTTPGroup) bool {
|
||||||
|
return cur == g
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
|
func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
|
||||||
var f vhost.CreateConnFunc
|
var f vhost.CreateConnFunc
|
||||||
newIndex := atomic.AddUint64(&g.index, 1)
|
newIndex := g.index.Add(1)
|
||||||
|
|
||||||
g.mu.RLock()
|
g.mu.RLock()
|
||||||
group := g.group
|
group := g.group
|
||||||
@@ -151,7 +144,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
|
|||||||
location := g.location
|
location := g.location
|
||||||
routeByHTTPUser := g.routeByHTTPUser
|
routeByHTTPUser := g.routeByHTTPUser
|
||||||
if len(g.pxyNames) > 0 {
|
if len(g.pxyNames) > 0 {
|
||||||
name := g.pxyNames[int(newIndex)%len(g.pxyNames)]
|
name := g.pxyNames[newIndex%uint64(len(g.pxyNames))]
|
||||||
f = g.createFuncs[name]
|
f = g.createFuncs[name]
|
||||||
}
|
}
|
||||||
g.mu.RUnlock()
|
g.mu.RUnlock()
|
||||||
@@ -165,7 +158,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *HTTPGroup) chooseEndpoint() (string, error) {
|
func (g *HTTPGroup) chooseEndpoint() (string, error) {
|
||||||
newIndex := atomic.AddUint64(&g.index, 1)
|
newIndex := g.index.Add(1)
|
||||||
name := ""
|
name := ""
|
||||||
|
|
||||||
g.mu.RLock()
|
g.mu.RLock()
|
||||||
@@ -174,7 +167,7 @@ func (g *HTTPGroup) chooseEndpoint() (string, error) {
|
|||||||
location := g.location
|
location := g.location
|
||||||
routeByHTTPUser := g.routeByHTTPUser
|
routeByHTTPUser := g.routeByHTTPUser
|
||||||
if len(g.pxyNames) > 0 {
|
if len(g.pxyNames) > 0 {
|
||||||
name = g.pxyNames[int(newIndex)%len(g.pxyNames)]
|
name = g.pxyNames[newIndex%uint64(len(g.pxyNames))]
|
||||||
}
|
}
|
||||||
g.mu.RUnlock()
|
g.mu.RUnlock()
|
||||||
|
|
||||||
|
|||||||
@@ -17,25 +17,19 @@ package group
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
|
|
||||||
gerr "github.com/fatedier/golib/errors"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPSGroupController struct {
|
type HTTPSGroupController struct {
|
||||||
groups map[string]*HTTPSGroup
|
groupRegistry[*HTTPSGroup]
|
||||||
|
|
||||||
httpsMuxer *vhost.HTTPSMuxer
|
httpsMuxer *vhost.HTTPSMuxer
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
|
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
|
||||||
return &HTTPSGroupController{
|
return &HTTPSGroupController{
|
||||||
groups: make(map[string]*HTTPSGroup),
|
groupRegistry: newGroupRegistry[*HTTPSGroup](),
|
||||||
httpsMuxer: httpsMuxer,
|
httpsMuxer: httpsMuxer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,41 +38,28 @@ func (ctl *HTTPSGroupController) Listen(
|
|||||||
group, groupKey string,
|
group, groupKey string,
|
||||||
routeConfig vhost.RouteConfig,
|
routeConfig vhost.RouteConfig,
|
||||||
) (l net.Listener, err error) {
|
) (l net.Listener, err error) {
|
||||||
indexKey := group
|
for {
|
||||||
ctl.mu.Lock()
|
g := ctl.getOrCreate(group, func() *HTTPSGroup {
|
||||||
g, ok := ctl.groups[indexKey]
|
return NewHTTPSGroup(ctl)
|
||||||
if !ok {
|
})
|
||||||
g = NewHTTPSGroup(ctl)
|
l, err = g.Listen(ctx, group, groupKey, routeConfig)
|
||||||
ctl.groups[indexKey] = g
|
if err == errGroupStale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctl.mu.Unlock()
|
|
||||||
|
|
||||||
return g.Listen(ctx, group, groupKey, routeConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctl *HTTPSGroupController) RemoveGroup(group string) {
|
|
||||||
ctl.mu.Lock()
|
|
||||||
defer ctl.mu.Unlock()
|
|
||||||
delete(ctl.groups, group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPSGroup struct {
|
type HTTPSGroup struct {
|
||||||
group string
|
baseGroup
|
||||||
groupKey string
|
|
||||||
domain string
|
|
||||||
|
|
||||||
acceptCh chan net.Conn
|
domain string
|
||||||
httpsLn *vhost.Listener
|
ctl *HTTPSGroupController
|
||||||
lns []*HTTPSGroupListener
|
|
||||||
ctl *HTTPSGroupController
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
|
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
|
||||||
return &HTTPSGroup{
|
return &HTTPSGroup{
|
||||||
lns: make([]*HTTPSGroupListener, 0),
|
ctl: ctl,
|
||||||
ctl: ctl,
|
|
||||||
acceptCh: make(chan net.Conn),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,23 +67,27 @@ func (g *HTTPSGroup) Listen(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
group, groupKey string,
|
group, groupKey string,
|
||||||
routeConfig vhost.RouteConfig,
|
routeConfig vhost.RouteConfig,
|
||||||
) (ln *HTTPSGroupListener, err error) {
|
) (ln *Listener, err error) {
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
defer g.mu.Unlock()
|
defer g.mu.Unlock()
|
||||||
|
if !g.ctl.isCurrent(group, func(cur *HTTPSGroup) bool { return cur == g }) {
|
||||||
|
return nil, errGroupStale
|
||||||
|
}
|
||||||
if len(g.lns) == 0 {
|
if len(g.lns) == 0 {
|
||||||
// the first listener, listen on the real address
|
// the first listener, listen on the real address
|
||||||
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
|
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
|
||||||
if errRet != nil {
|
if errRet != nil {
|
||||||
return nil, errRet
|
return nil, errRet
|
||||||
}
|
}
|
||||||
ln = newHTTPSGroupListener(group, g, httpsLn.Addr())
|
|
||||||
|
|
||||||
g.group = group
|
|
||||||
g.groupKey = groupKey
|
|
||||||
g.domain = routeConfig.Domain
|
g.domain = routeConfig.Domain
|
||||||
g.httpsLn = httpsLn
|
g.initBase(group, groupKey, httpsLn, func() {
|
||||||
g.lns = append(g.lns, ln)
|
g.ctl.removeIf(g.group, func(cur *HTTPSGroup) bool {
|
||||||
go g.worker()
|
return cur == g
|
||||||
|
})
|
||||||
|
})
|
||||||
|
ln = g.newListener(httpsLn.Addr())
|
||||||
|
go g.worker(httpsLn, g.acceptCh)
|
||||||
} else {
|
} else {
|
||||||
// route config in the same group must be equal
|
// route config in the same group must be equal
|
||||||
if g.group != group || g.domain != routeConfig.Domain {
|
if g.group != group || g.domain != routeConfig.Domain {
|
||||||
@@ -111,87 +96,7 @@ func (g *HTTPSGroup) Listen(
|
|||||||
if g.groupKey != groupKey {
|
if g.groupKey != groupKey {
|
||||||
return nil, ErrGroupAuthFailed
|
return nil, ErrGroupAuthFailed
|
||||||
}
|
}
|
||||||
ln = newHTTPSGroupListener(group, g, g.lns[0].Addr())
|
ln = g.newListener(g.lns[0].Addr())
|
||||||
g.lns = append(g.lns, ln)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *HTTPSGroup) worker() {
|
|
||||||
for {
|
|
||||||
c, err := g.httpsLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = gerr.PanicToError(func() {
|
|
||||||
g.acceptCh <- c
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *HTTPSGroup) Accept() <-chan net.Conn {
|
|
||||||
return g.acceptCh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) {
|
|
||||||
g.mu.Lock()
|
|
||||||
defer g.mu.Unlock()
|
|
||||||
for i, tmpLn := range g.lns {
|
|
||||||
if tmpLn == ln {
|
|
||||||
g.lns = append(g.lns[:i], g.lns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(g.lns) == 0 {
|
|
||||||
close(g.acceptCh)
|
|
||||||
if g.httpsLn != nil {
|
|
||||||
g.httpsLn.Close()
|
|
||||||
}
|
|
||||||
g.ctl.RemoveGroup(g.group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSGroupListener struct {
|
|
||||||
groupName string
|
|
||||||
group *HTTPSGroup
|
|
||||||
|
|
||||||
addr net.Addr
|
|
||||||
closeCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener {
|
|
||||||
return &HTTPSGroupListener{
|
|
||||||
groupName: name,
|
|
||||||
group: group,
|
|
||||||
addr: addr,
|
|
||||||
closeCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) {
|
|
||||||
var ok bool
|
|
||||||
select {
|
|
||||||
case <-ln.closeCh:
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
case c, ok = <-ln.group.Accept():
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *HTTPSGroupListener) Addr() net.Addr {
|
|
||||||
return ln.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *HTTPSGroupListener) Close() (err error) {
|
|
||||||
close(ln.closeCh)
|
|
||||||
|
|
||||||
// remove self from HTTPSGroup
|
|
||||||
ln.group.CloseListener(ln)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
49
server/group/listener.go
Normal file
49
server/group/listener.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Listener is a per-proxy virtual listener that receives connections
|
||||||
|
// from a shared group. It implements net.Listener.
|
||||||
|
type Listener struct {
|
||||||
|
acceptCh <-chan net.Conn
|
||||||
|
addr net.Addr
|
||||||
|
closeCh chan struct{}
|
||||||
|
onClose func(*Listener)
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListener(acceptCh <-chan net.Conn, addr net.Addr, onClose func(*Listener)) *Listener {
|
||||||
|
return &Listener{
|
||||||
|
acceptCh: acceptCh,
|
||||||
|
addr: addr,
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
onClose: onClose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *Listener) Accept() (net.Conn, error) {
|
||||||
|
select {
|
||||||
|
case <-ln.closeCh:
|
||||||
|
return nil, ErrListenerClosed
|
||||||
|
case c, ok := <-ln.acceptCh:
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrListenerClosed
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *Listener) Addr() net.Addr {
|
||||||
|
return ln.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *Listener) Close() error {
|
||||||
|
ln.once.Do(func() {
|
||||||
|
close(ln.closeCh)
|
||||||
|
ln.onClose(ln)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
68
server/group/listener_test.go
Normal file
68
server/group/listener_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListener_Accept(t *testing.T) {
|
||||||
|
acceptCh := make(chan net.Conn, 1)
|
||||||
|
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
|
||||||
|
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
acceptCh <- c1
|
||||||
|
got, err := ln.Accept()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c1, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_AcceptAfterChannelClose(t *testing.T) {
|
||||||
|
acceptCh := make(chan net.Conn)
|
||||||
|
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
|
||||||
|
|
||||||
|
close(acceptCh)
|
||||||
|
_, err := ln.Accept()
|
||||||
|
assert.ErrorIs(t, err, ErrListenerClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_AcceptAfterListenerClose(t *testing.T) {
|
||||||
|
acceptCh := make(chan net.Conn) // open, not closed
|
||||||
|
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
|
||||||
|
|
||||||
|
ln.Close()
|
||||||
|
_, err := ln.Accept()
|
||||||
|
assert.ErrorIs(t, err, ErrListenerClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_DoubleClose(t *testing.T) {
|
||||||
|
closeCalls := 0
|
||||||
|
ln := newListener(
|
||||||
|
make(chan net.Conn),
|
||||||
|
fakeAddr("127.0.0.1:1234"),
|
||||||
|
func(*Listener) { closeCalls++ },
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
ln.Close()
|
||||||
|
ln.Close()
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, closeCalls, "onClose should be called exactly once")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_Addr(t *testing.T) {
|
||||||
|
addr := fakeAddr("10.0.0.1:5555")
|
||||||
|
ln := newListener(make(chan net.Conn), addr, func(*Listener) {})
|
||||||
|
assert.Equal(t, addr, ln.Addr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakeAddr implements net.Addr for testing.
|
||||||
|
type fakeAddr string
|
||||||
|
|
||||||
|
func (a fakeAddr) Network() string { return "tcp" }
|
||||||
|
func (a fakeAddr) String() string { return string(a) }
|
||||||
59
server/group/registry.go
Normal file
59
server/group/registry.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// groupRegistry is a concurrent map of named groups with
|
||||||
|
// automatic creation on first access.
|
||||||
|
type groupRegistry[G any] struct {
|
||||||
|
groups map[string]G
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGroupRegistry[G any]() groupRegistry[G] {
|
||||||
|
return groupRegistry[G]{
|
||||||
|
groups: make(map[string]G),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRegistry[G]) getOrCreate(key string, newFn func() G) G {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
g, ok := r.groups[key]
|
||||||
|
if !ok {
|
||||||
|
g = newFn()
|
||||||
|
r.groups[key] = g
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRegistry[G]) get(key string) (G, bool) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
g, ok := r.groups[key]
|
||||||
|
return g, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCurrent returns true if key exists in the registry and matchFn
|
||||||
|
// returns true for the stored value.
|
||||||
|
func (r *groupRegistry[G]) isCurrent(key string, matchFn func(G) bool) bool {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
g, ok := r.groups[key]
|
||||||
|
return ok && matchFn(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeIf atomically looks up the group for key, calls fn on it,
|
||||||
|
// and removes the entry if fn returns true.
|
||||||
|
func (r *groupRegistry[G]) removeIf(key string, fn func(G) bool) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
g, ok := r.groups[key]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fn(g) {
|
||||||
|
delete(r.groups, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
102
server/group/registry_test.go
Normal file
102
server/group/registry_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetOrCreate_New(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
called := 0
|
||||||
|
v := 42
|
||||||
|
got := r.getOrCreate("k", func() *int { called++; return &v })
|
||||||
|
assert.Equal(t, 1, called)
|
||||||
|
assert.Equal(t, &v, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOrCreate_Existing(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
v := 42
|
||||||
|
r.getOrCreate("k", func() *int { return &v })
|
||||||
|
|
||||||
|
called := 0
|
||||||
|
got := r.getOrCreate("k", func() *int { called++; return nil })
|
||||||
|
assert.Equal(t, 0, called)
|
||||||
|
assert.Equal(t, &v, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_ExistingAndMissing(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
v := 1
|
||||||
|
r.getOrCreate("k", func() *int { return &v })
|
||||||
|
|
||||||
|
got, ok := r.get("k")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, &v, got)
|
||||||
|
|
||||||
|
_, ok = r.get("missing")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsCurrent(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
v1 := 1
|
||||||
|
v2 := 2
|
||||||
|
r.getOrCreate("k", func() *int { return &v1 })
|
||||||
|
|
||||||
|
assert.True(t, r.isCurrent("k", func(g *int) bool { return g == &v1 }))
|
||||||
|
assert.False(t, r.isCurrent("k", func(g *int) bool { return g == &v2 }))
|
||||||
|
assert.False(t, r.isCurrent("missing", func(g *int) bool { return true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveIf(t *testing.T) {
|
||||||
|
t.Run("removes when fn returns true", func(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
v := 1
|
||||||
|
r.getOrCreate("k", func() *int { return &v })
|
||||||
|
r.removeIf("k", func(g *int) bool { return g == &v })
|
||||||
|
_, ok := r.get("k")
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps when fn returns false", func(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
v := 1
|
||||||
|
r.getOrCreate("k", func() *int { return &v })
|
||||||
|
r.removeIf("k", func(g *int) bool { return false })
|
||||||
|
_, ok := r.get("k")
|
||||||
|
assert.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("noop on missing key", func(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
r.removeIf("missing", func(g *int) bool { return true }) // should not panic
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentGetOrCreateAndRemoveIf(t *testing.T) {
|
||||||
|
r := newGroupRegistry[*int]()
|
||||||
|
const n = 100
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n * 2)
|
||||||
|
for i := range n {
|
||||||
|
v := i
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
r.getOrCreate("k", func() *int { return &v })
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
r.removeIf("k", func(*int) bool { return true })
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// After all goroutines finish, accessing the key must not panic.
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
_, _ = r.get("k")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,83 +17,67 @@ package group
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
|
|
||||||
gerr "github.com/fatedier/golib/errors"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/server/ports"
|
"github.com/fatedier/frp/server/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TCPGroupCtl manage all TCPGroups
|
// TCPGroupCtl manages all TCPGroups.
|
||||||
type TCPGroupCtl struct {
|
type TCPGroupCtl struct {
|
||||||
groups map[string]*TCPGroup
|
groupRegistry[*TCPGroup]
|
||||||
|
|
||||||
// portManager is used to manage port
|
|
||||||
portManager *ports.Manager
|
portManager *ports.Manager
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTCPGroupCtl return a new TcpGroupCtl
|
// NewTCPGroupCtl returns a new TCPGroupCtl.
|
||||||
func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl {
|
func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl {
|
||||||
return &TCPGroupCtl{
|
return &TCPGroupCtl{
|
||||||
groups: make(map[string]*TCPGroup),
|
groupRegistry: newGroupRegistry[*TCPGroup](),
|
||||||
portManager: portManager,
|
portManager: portManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen is the wrapper for TCPGroup's Listen
|
// Listen is the wrapper for TCPGroup's Listen.
|
||||||
// If there are no group, we will create one here
|
// If there is no group, one will be created.
|
||||||
func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string,
|
func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string,
|
||||||
addr string, port int,
|
addr string, port int,
|
||||||
) (l net.Listener, realPort int, err error) {
|
) (l net.Listener, realPort int, err error) {
|
||||||
tgc.mu.Lock()
|
for {
|
||||||
tcpGroup, ok := tgc.groups[group]
|
tcpGroup := tgc.getOrCreate(group, func() *TCPGroup {
|
||||||
if !ok {
|
return NewTCPGroup(tgc)
|
||||||
tcpGroup = NewTCPGroup(tgc)
|
})
|
||||||
tgc.groups[group] = tcpGroup
|
l, realPort, err = tcpGroup.Listen(proxyName, group, groupKey, addr, port)
|
||||||
|
if err == errGroupStale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
tgc.mu.Unlock()
|
|
||||||
|
|
||||||
return tcpGroup.Listen(proxyName, group, groupKey, addr, port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveGroup remove TCPGroup from controller
|
// TCPGroup routes connections to different proxies.
|
||||||
func (tgc *TCPGroupCtl) RemoveGroup(group string) {
|
|
||||||
tgc.mu.Lock()
|
|
||||||
defer tgc.mu.Unlock()
|
|
||||||
delete(tgc.groups, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCPGroup route connections to different proxies
|
|
||||||
type TCPGroup struct {
|
type TCPGroup struct {
|
||||||
group string
|
baseGroup
|
||||||
groupKey string
|
|
||||||
addr string
|
addr string
|
||||||
port int
|
port int
|
||||||
realPort int
|
realPort int
|
||||||
|
|
||||||
acceptCh chan net.Conn
|
|
||||||
tcpLn net.Listener
|
|
||||||
lns []*TCPGroupListener
|
|
||||||
ctl *TCPGroupCtl
|
ctl *TCPGroupCtl
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTCPGroup return a new TCPGroup
|
// NewTCPGroup returns a new TCPGroup.
|
||||||
func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup {
|
func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup {
|
||||||
return &TCPGroup{
|
return &TCPGroup{
|
||||||
lns: make([]*TCPGroupListener, 0),
|
ctl: ctl,
|
||||||
ctl: ctl,
|
|
||||||
acceptCh: make(chan net.Conn),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen will return a new TCPGroupListener
|
// Listen will return a new Listener.
|
||||||
// if TCPGroup already has a listener, just add a new TCPGroupListener to the queues
|
// If TCPGroup already has a listener, just add a new Listener to the queues,
|
||||||
// otherwise, listen on the real address
|
// otherwise listen on the real address.
|
||||||
func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *TCPGroupListener, realPort int, err error) {
|
func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *Listener, realPort int, err error) {
|
||||||
tg.mu.Lock()
|
tg.mu.Lock()
|
||||||
defer tg.mu.Unlock()
|
defer tg.mu.Unlock()
|
||||||
|
if !tg.ctl.isCurrent(group, func(cur *TCPGroup) bool { return cur == tg }) {
|
||||||
|
return nil, 0, errGroupStale
|
||||||
|
}
|
||||||
if len(tg.lns) == 0 {
|
if len(tg.lns) == 0 {
|
||||||
// the first listener, listen on the real address
|
// the first listener, listen on the real address
|
||||||
realPort, err = tg.ctl.portManager.Acquire(proxyName, port)
|
realPort, err = tg.ctl.portManager.Acquire(proxyName, port)
|
||||||
@@ -106,19 +90,18 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
|
|||||||
err = errRet
|
err = errRet
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ln = newTCPGroupListener(group, tg, tcpLn.Addr())
|
|
||||||
|
|
||||||
tg.group = group
|
|
||||||
tg.groupKey = groupKey
|
|
||||||
tg.addr = addr
|
tg.addr = addr
|
||||||
tg.port = port
|
tg.port = port
|
||||||
tg.realPort = realPort
|
tg.realPort = realPort
|
||||||
tg.tcpLn = tcpLn
|
tg.initBase(group, groupKey, tcpLn, func() {
|
||||||
tg.lns = append(tg.lns, ln)
|
tg.ctl.portManager.Release(tg.realPort)
|
||||||
if tg.acceptCh == nil {
|
tg.ctl.removeIf(tg.group, func(cur *TCPGroup) bool {
|
||||||
tg.acceptCh = make(chan net.Conn)
|
return cur == tg
|
||||||
}
|
})
|
||||||
go tg.worker()
|
})
|
||||||
|
ln = tg.newListener(tcpLn.Addr())
|
||||||
|
go tg.worker(tcpLn, tg.acceptCh)
|
||||||
} else {
|
} else {
|
||||||
// address and port in the same group must be equal
|
// address and port in the same group must be equal
|
||||||
if tg.group != group || tg.addr != addr {
|
if tg.group != group || tg.addr != addr {
|
||||||
@@ -133,92 +116,8 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
|
|||||||
err = ErrGroupAuthFailed
|
err = ErrGroupAuthFailed
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ln = newTCPGroupListener(group, tg, tg.lns[0].Addr())
|
ln = tg.newListener(tg.lns[0].Addr())
|
||||||
realPort = tg.realPort
|
realPort = tg.realPort
|
||||||
tg.lns = append(tg.lns, ln)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// worker is called when the real tcp listener has been created
|
|
||||||
func (tg *TCPGroup) worker() {
|
|
||||||
for {
|
|
||||||
c, err := tg.tcpLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = gerr.PanicToError(func() {
|
|
||||||
tg.acceptCh <- c
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tg *TCPGroup) Accept() <-chan net.Conn {
|
|
||||||
return tg.acceptCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseListener remove the TCPGroupListener from the TCPGroup
|
|
||||||
func (tg *TCPGroup) CloseListener(ln *TCPGroupListener) {
|
|
||||||
tg.mu.Lock()
|
|
||||||
defer tg.mu.Unlock()
|
|
||||||
for i, tmpLn := range tg.lns {
|
|
||||||
if tmpLn == ln {
|
|
||||||
tg.lns = append(tg.lns[:i], tg.lns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(tg.lns) == 0 {
|
|
||||||
close(tg.acceptCh)
|
|
||||||
tg.tcpLn.Close()
|
|
||||||
tg.ctl.portManager.Release(tg.realPort)
|
|
||||||
tg.ctl.RemoveGroup(tg.group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCPGroupListener
|
|
||||||
type TCPGroupListener struct {
|
|
||||||
groupName string
|
|
||||||
group *TCPGroup
|
|
||||||
|
|
||||||
addr net.Addr
|
|
||||||
closeCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTCPGroupListener(name string, group *TCPGroup, addr net.Addr) *TCPGroupListener {
|
|
||||||
return &TCPGroupListener{
|
|
||||||
groupName: name,
|
|
||||||
group: group,
|
|
||||||
addr: addr,
|
|
||||||
closeCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept will accept connections from TCPGroup
|
|
||||||
func (ln *TCPGroupListener) Accept() (c net.Conn, err error) {
|
|
||||||
var ok bool
|
|
||||||
select {
|
|
||||||
case <-ln.closeCh:
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
case c, ok = <-ln.group.Accept():
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *TCPGroupListener) Addr() net.Addr {
|
|
||||||
return ln.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close close the listener
|
|
||||||
func (ln *TCPGroupListener) Close() (err error) {
|
|
||||||
close(ln.closeCh)
|
|
||||||
|
|
||||||
// remove self from TcpGroup
|
|
||||||
ln.group.CloseListener(ln)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,118 +18,100 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
|
|
||||||
gerr "github.com/fatedier/golib/errors"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/util/tcpmux"
|
"github.com/fatedier/frp/pkg/util/tcpmux"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TCPMuxGroupCtl manage all TCPMuxGroups
|
// TCPMuxGroupCtl manages all TCPMuxGroups.
|
||||||
type TCPMuxGroupCtl struct {
|
type TCPMuxGroupCtl struct {
|
||||||
groups map[string]*TCPMuxGroup
|
groupRegistry[*TCPMuxGroup]
|
||||||
|
|
||||||
// portManager is used to manage port
|
|
||||||
tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer
|
tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTCPMuxGroupCtl return a new TCPMuxGroupCtl
|
// NewTCPMuxGroupCtl returns a new TCPMuxGroupCtl.
|
||||||
func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl {
|
func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl {
|
||||||
return &TCPMuxGroupCtl{
|
return &TCPMuxGroupCtl{
|
||||||
groups: make(map[string]*TCPMuxGroup),
|
groupRegistry: newGroupRegistry[*TCPMuxGroup](),
|
||||||
tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer,
|
tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen is the wrapper for TCPMuxGroup's Listen
|
// Listen is the wrapper for TCPMuxGroup's Listen.
|
||||||
// If there are no group, we will create one here
|
// If there is no group, one will be created.
|
||||||
func (tmgc *TCPMuxGroupCtl) Listen(
|
func (tmgc *TCPMuxGroupCtl) Listen(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
multiplexer, group, groupKey string,
|
multiplexer, group, groupKey string,
|
||||||
routeConfig vhost.RouteConfig,
|
routeConfig vhost.RouteConfig,
|
||||||
) (l net.Listener, err error) {
|
) (l net.Listener, err error) {
|
||||||
tmgc.mu.Lock()
|
for {
|
||||||
tcpMuxGroup, ok := tmgc.groups[group]
|
tcpMuxGroup := tmgc.getOrCreate(group, func() *TCPMuxGroup {
|
||||||
if !ok {
|
return NewTCPMuxGroup(tmgc)
|
||||||
tcpMuxGroup = NewTCPMuxGroup(tmgc)
|
})
|
||||||
tmgc.groups[group] = tcpMuxGroup
|
|
||||||
}
|
|
||||||
tmgc.mu.Unlock()
|
|
||||||
|
|
||||||
switch v1.TCPMultiplexerType(multiplexer) {
|
switch v1.TCPMultiplexerType(multiplexer) {
|
||||||
case v1.TCPMultiplexerHTTPConnect:
|
case v1.TCPMultiplexerHTTPConnect:
|
||||||
return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
|
l, err = tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
|
||||||
default:
|
if err == errGroupStale {
|
||||||
err = fmt.Errorf("unknown multiplexer [%s]", multiplexer)
|
continue
|
||||||
return
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown multiplexer [%s]", multiplexer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveGroup remove TCPMuxGroup from controller
|
// TCPMuxGroup routes connections to different proxies.
|
||||||
func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) {
|
|
||||||
tmgc.mu.Lock()
|
|
||||||
defer tmgc.mu.Unlock()
|
|
||||||
delete(tmgc.groups, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCPMuxGroup route connections to different proxies
|
|
||||||
type TCPMuxGroup struct {
|
type TCPMuxGroup struct {
|
||||||
group string
|
baseGroup
|
||||||
groupKey string
|
|
||||||
domain string
|
domain string
|
||||||
routeByHTTPUser string
|
routeByHTTPUser string
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
|
ctl *TCPMuxGroupCtl
|
||||||
acceptCh chan net.Conn
|
|
||||||
tcpMuxLn net.Listener
|
|
||||||
lns []*TCPMuxGroupListener
|
|
||||||
ctl *TCPMuxGroupCtl
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTCPMuxGroup return a new TCPMuxGroup
|
// NewTCPMuxGroup returns a new TCPMuxGroup.
|
||||||
func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup {
|
func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup {
|
||||||
return &TCPMuxGroup{
|
return &TCPMuxGroup{
|
||||||
lns: make([]*TCPMuxGroupListener, 0),
|
ctl: ctl,
|
||||||
ctl: ctl,
|
|
||||||
acceptCh: make(chan net.Conn),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen will return a new TCPMuxGroupListener
|
// HTTPConnectListen will return a new Listener.
|
||||||
// if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues
|
// If TCPMuxGroup already has a listener, just add a new Listener to the queues,
|
||||||
// otherwise, listen on the real address
|
// otherwise listen on the real address.
|
||||||
func (tmg *TCPMuxGroup) HTTPConnectListen(
|
func (tmg *TCPMuxGroup) HTTPConnectListen(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
group, groupKey string,
|
group, groupKey string,
|
||||||
routeConfig vhost.RouteConfig,
|
routeConfig vhost.RouteConfig,
|
||||||
) (ln *TCPMuxGroupListener, err error) {
|
) (ln *Listener, err error) {
|
||||||
tmg.mu.Lock()
|
tmg.mu.Lock()
|
||||||
defer tmg.mu.Unlock()
|
defer tmg.mu.Unlock()
|
||||||
|
if !tmg.ctl.isCurrent(group, func(cur *TCPMuxGroup) bool { return cur == tmg }) {
|
||||||
|
return nil, errGroupStale
|
||||||
|
}
|
||||||
if len(tmg.lns) == 0 {
|
if len(tmg.lns) == 0 {
|
||||||
// the first listener, listen on the real address
|
// the first listener, listen on the real address
|
||||||
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
|
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
|
||||||
if errRet != nil {
|
if errRet != nil {
|
||||||
return nil, errRet
|
return nil, errRet
|
||||||
}
|
}
|
||||||
ln = newTCPMuxGroupListener(group, tmg, tcpMuxLn.Addr())
|
|
||||||
|
|
||||||
tmg.group = group
|
|
||||||
tmg.groupKey = groupKey
|
|
||||||
tmg.domain = routeConfig.Domain
|
tmg.domain = routeConfig.Domain
|
||||||
tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
|
tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
|
||||||
tmg.username = routeConfig.Username
|
tmg.username = routeConfig.Username
|
||||||
tmg.password = routeConfig.Password
|
tmg.password = routeConfig.Password
|
||||||
tmg.tcpMuxLn = tcpMuxLn
|
tmg.initBase(group, groupKey, tcpMuxLn, func() {
|
||||||
tmg.lns = append(tmg.lns, ln)
|
tmg.ctl.removeIf(tmg.group, func(cur *TCPMuxGroup) bool {
|
||||||
if tmg.acceptCh == nil {
|
return cur == tmg
|
||||||
tmg.acceptCh = make(chan net.Conn)
|
})
|
||||||
}
|
})
|
||||||
go tmg.worker()
|
ln = tmg.newListener(tcpMuxLn.Addr())
|
||||||
|
go tmg.worker(tcpMuxLn, tmg.acceptCh)
|
||||||
} else {
|
} else {
|
||||||
// route config in the same group must be equal
|
// route config in the same group must be equal
|
||||||
if tmg.group != group || tmg.domain != routeConfig.Domain ||
|
if tmg.group != group || tmg.domain != routeConfig.Domain ||
|
||||||
@@ -141,90 +123,7 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(
|
|||||||
if tmg.groupKey != groupKey {
|
if tmg.groupKey != groupKey {
|
||||||
return nil, ErrGroupAuthFailed
|
return nil, ErrGroupAuthFailed
|
||||||
}
|
}
|
||||||
ln = newTCPMuxGroupListener(group, tmg, tmg.lns[0].Addr())
|
ln = tmg.newListener(tmg.lns[0].Addr())
|
||||||
tmg.lns = append(tmg.lns, ln)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// worker is called when the real TCP listener has been created
|
|
||||||
func (tmg *TCPMuxGroup) worker() {
|
|
||||||
for {
|
|
||||||
c, err := tmg.tcpMuxLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = gerr.PanicToError(func() {
|
|
||||||
tmg.acceptCh <- c
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmg *TCPMuxGroup) Accept() <-chan net.Conn {
|
|
||||||
return tmg.acceptCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseListener remove the TCPMuxGroupListener from the TCPMuxGroup
|
|
||||||
func (tmg *TCPMuxGroup) CloseListener(ln *TCPMuxGroupListener) {
|
|
||||||
tmg.mu.Lock()
|
|
||||||
defer tmg.mu.Unlock()
|
|
||||||
for i, tmpLn := range tmg.lns {
|
|
||||||
if tmpLn == ln {
|
|
||||||
tmg.lns = append(tmg.lns[:i], tmg.lns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(tmg.lns) == 0 {
|
|
||||||
close(tmg.acceptCh)
|
|
||||||
tmg.tcpMuxLn.Close()
|
|
||||||
tmg.ctl.RemoveGroup(tmg.group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCPMuxGroupListener
|
|
||||||
type TCPMuxGroupListener struct {
|
|
||||||
groupName string
|
|
||||||
group *TCPMuxGroup
|
|
||||||
|
|
||||||
addr net.Addr
|
|
||||||
closeCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTCPMuxGroupListener(name string, group *TCPMuxGroup, addr net.Addr) *TCPMuxGroupListener {
|
|
||||||
return &TCPMuxGroupListener{
|
|
||||||
groupName: name,
|
|
||||||
group: group,
|
|
||||||
addr: addr,
|
|
||||||
closeCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept will accept connections from TCPMuxGroup
|
|
||||||
func (ln *TCPMuxGroupListener) Accept() (c net.Conn, err error) {
|
|
||||||
var ok bool
|
|
||||||
select {
|
|
||||||
case <-ln.closeCh:
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
case c, ok = <-ln.group.Accept():
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrListenerClosed
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *TCPMuxGroupListener) Addr() net.Addr {
|
|
||||||
return ln.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close close the listener
|
|
||||||
func (ln *TCPMuxGroupListener) Close() (err error) {
|
|
||||||
close(ln.closeCh)
|
|
||||||
|
|
||||||
// remove self from TcpMuxGroup
|
|
||||||
ln.group.CloseListener(ln)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp {
|
|||||||
ClientID: info.ClientID(),
|
ClientID: info.ClientID(),
|
||||||
RunID: info.RunID,
|
RunID: info.RunID,
|
||||||
Version: info.Version,
|
Version: info.Version,
|
||||||
|
WireProtocol: info.WireProtocol,
|
||||||
Hostname: info.Hostname,
|
Hostname: info.Hostname,
|
||||||
ClientIP: info.IP,
|
ClientIP: info.IP,
|
||||||
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ package http
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
"github.com/fatedier/frp/server/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
|
func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
|
||||||
@@ -69,3 +72,24 @@ func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
|
|||||||
t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got)
|
t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildClientInfoRespIncludesWireProtocol(t *testing.T) {
|
||||||
|
info := registry.ClientInfo{
|
||||||
|
Key: "user.client",
|
||||||
|
User: "user",
|
||||||
|
RawClientID: "client",
|
||||||
|
RunID: "run-id",
|
||||||
|
Version: "1.0.0",
|
||||||
|
WireProtocol: wire.ProtocolV2,
|
||||||
|
Hostname: "host",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
FirstConnectedAt: time.Unix(1, 0),
|
||||||
|
LastConnectedAt: time.Unix(2, 0),
|
||||||
|
Online: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := buildClientInfoResp(info)
|
||||||
|
if resp.WireProtocol != wire.ProtocolV2 {
|
||||||
|
t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, resp.WireProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type ClientInfoResp struct {
|
|||||||
ClientID string `json:"clientID"`
|
ClientID string `json:"clientID"`
|
||||||
RunID string `json:"runID"`
|
RunID string `json:"runID"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
|
WireProtocol string `json:"wireProtocol,omitempty"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
ClientIP string `json:"clientIP,omitempty"`
|
ClientIP string `json:"clientIP,omitempty"`
|
||||||
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
||||||
|
|||||||
@@ -44,7 +44,26 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy) P
|
|||||||
proxyFactoryRegistry[proxyConfType] = factory
|
proxyFactoryRegistry[proxyConfType] = factory
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetWorkConnFn func() (net.Conn, error)
|
type WorkConn struct {
|
||||||
|
conn *msg.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorkConn(conn *msg.Conn) *WorkConn {
|
||||||
|
return &WorkConn{conn: conn}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WorkConn) Start(m *msg.StartWorkConn) (net.Conn, error) {
|
||||||
|
if err := c.conn.WriteMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WorkConn) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetWorkConnFn func() (*WorkConn, error)
|
||||||
|
|
||||||
type Proxy interface {
|
type Proxy interface {
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
@@ -125,13 +144,13 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
|||||||
xl := xlog.FromContextSafe(pxy.ctx)
|
xl := xlog.FromContextSafe(pxy.ctx)
|
||||||
// try all connections from the pool
|
// try all connections from the pool
|
||||||
for i := 0; i < pxy.poolCount+1; i++ {
|
for i := 0; i < pxy.poolCount+1; i++ {
|
||||||
if workConn, err = pxy.getWorkConnFn(); err != nil {
|
var pxyWorkConn *WorkConn
|
||||||
|
if pxyWorkConn, err = pxy.getWorkConnFn(); err != nil {
|
||||||
xl.Warnf("failed to get work connection: %v", err)
|
xl.Warnf("failed to get work connection: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Debugf("get a new work connection: [%s]", workConn.RemoteAddr().String())
|
xl.Debugf("get a new work connection: [%s]", pxyWorkConn.conn.RemoteAddr().String())
|
||||||
xl.Spawn().AppendPrefix(pxy.GetName())
|
xl.Spawn().AppendPrefix(pxy.GetName())
|
||||||
workConn = netpkg.NewContextConn(pxy.ctx, workConn)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
srcAddr string
|
srcAddr string
|
||||||
@@ -150,7 +169,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
|||||||
dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
|
dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
|
||||||
dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16)
|
dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16)
|
||||||
}
|
}
|
||||||
err = msg.WriteMsg(workConn, &msg.StartWorkConn{
|
workConn, err = pxyWorkConn.Start(&msg.StartWorkConn{
|
||||||
ProxyName: pxy.GetName(),
|
ProxyName: pxy.GetName(),
|
||||||
SrcAddr: srcAddr,
|
SrcAddr: srcAddr,
|
||||||
SrcPort: uint16(srcPort),
|
SrcPort: uint16(srcPort),
|
||||||
@@ -160,9 +179,10 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i)
|
xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i)
|
||||||
workConn.Close()
|
pxyWorkConn.Close()
|
||||||
workConn = nil
|
workConn = nil
|
||||||
} else {
|
} else {
|
||||||
|
workConn = netpkg.NewContextConn(pxy.ctx, workConn)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
server/proxy/proxy_test.go
Normal file
53
server/proxy/proxy_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// 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 proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWorkConnStartWritesStartWorkConn(t *testing.T) {
|
||||||
|
client, server := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
serverMsgConn := msg.NewConn(server, msg.NewV2ReadWriter(server))
|
||||||
|
clientMsgConn := msg.NewConn(client, msg.NewV2ReadWriter(client))
|
||||||
|
workConn := NewWorkConn(serverMsgConn)
|
||||||
|
|
||||||
|
in := &msg.StartWorkConn{ProxyName: "tcp", SrcAddr: "127.0.0.1", SrcPort: 1234}
|
||||||
|
type startResult struct {
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan startResult, 1)
|
||||||
|
go func() {
|
||||||
|
conn, err := workConn.Start(in)
|
||||||
|
resultCh <- startResult{conn: conn, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := clientMsgConn.ReadMsg()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, in, out)
|
||||||
|
|
||||||
|
result := <-resultCh
|
||||||
|
require.NoError(t, result.err)
|
||||||
|
require.Same(t, serverMsgConn, result.conn)
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ type ClientInfo struct {
|
|||||||
Hostname string
|
Hostname string
|
||||||
IP string
|
IP string
|
||||||
Version string
|
Version string
|
||||||
|
WireProtocol string
|
||||||
FirstConnectedAt time.Time
|
FirstConnectedAt time.Time
|
||||||
LastConnectedAt time.Time
|
LastConnectedAt time.Time
|
||||||
DisconnectedAt time.Time
|
DisconnectedAt time.Time
|
||||||
@@ -51,7 +52,7 @@ func NewClientRegistry() *ClientRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
|
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
|
||||||
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr string) (key string, conflict bool) {
|
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr, wireProtocol string) (key string, conflict bool) {
|
||||||
if runID == "" {
|
if runID == "" {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -88,6 +89,7 @@ func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version,
|
|||||||
info.Hostname = hostname
|
info.Hostname = hostname
|
||||||
info.IP = remoteAddr
|
info.IP = remoteAddr
|
||||||
info.Version = version
|
info.Version = version
|
||||||
|
info.WireProtocol = wireProtocol
|
||||||
if info.FirstConnectedAt.IsZero() {
|
if info.FirstConnectedAt.IsZero() {
|
||||||
info.FirstConnectedAt = now
|
info.FirstConnectedAt = now
|
||||||
}
|
}
|
||||||
|
|||||||
37
server/registry/registry_test.go
Normal file
37
server/registry/registry_test.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 registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientRegistryRegisterStoresWireProtocol(t *testing.T) {
|
||||||
|
registry := NewClientRegistry()
|
||||||
|
key, conflict := registry.Register("user", "client-id", "run-id", "host", "1.0.0", "127.0.0.1", wire.ProtocolV2)
|
||||||
|
if conflict {
|
||||||
|
t.Fatal("unexpected client conflict")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := registry.GetByKey(key)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("client %q not found", key)
|
||||||
|
}
|
||||||
|
if info.WireProtocol != wire.ProtocolV2 {
|
||||||
|
t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, info.WireProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -37,6 +38,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/nathole"
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/wire"
|
||||||
"github.com/fatedier/frp/pkg/ssh"
|
"github.com/fatedier/frp/pkg/ssh"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
@@ -432,20 +434,15 @@ func (svr *Service) Close() error {
|
|||||||
func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) {
|
func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) {
|
||||||
xl := xlog.FromContextSafe(ctx)
|
xl := xlog.FromContextSafe(ctx)
|
||||||
|
|
||||||
var (
|
acceptedConn, err := svr.acceptConnection(ctx, conn)
|
||||||
rawMsg msg.Message
|
if err != nil {
|
||||||
err error
|
log.Tracef("failed to accept frp connection: %v", err)
|
||||||
)
|
|
||||||
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(connReadTimeout))
|
|
||||||
if rawMsg, err = msg.ReadMsg(conn); err != nil {
|
|
||||||
log.Tracef("failed to read message: %v", err)
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
conn = acceptedConn.conn
|
||||||
|
|
||||||
switch m := rawMsg.(type) {
|
switch m := acceptedConn.firstMsg.(type) {
|
||||||
case *msg.Login:
|
case *msg.Login:
|
||||||
// server plugin hook
|
// server plugin hook
|
||||||
content := &plugin.LoginContent{
|
content := &plugin.LoginContent{
|
||||||
@@ -453,35 +450,66 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
|
|||||||
ClientAddress: conn.RemoteAddr().String(),
|
ClientAddress: conn.RemoteAddr().String(),
|
||||||
}
|
}
|
||||||
retContent, err := svr.pluginManager.Login(content)
|
retContent, err := svr.pluginManager.Login(content)
|
||||||
|
var ctl *Control
|
||||||
if err == nil {
|
if err == nil {
|
||||||
m = &retContent.Login
|
m = &retContent.Login
|
||||||
err = svr.RegisterControl(conn, m, internal)
|
controlConn := acceptedConn.conn
|
||||||
|
if !internal {
|
||||||
|
var controlRW io.ReadWriter
|
||||||
|
controlRW, err = netpkg.NewCryptoReadWriter(conn, svr.auth.EncryptionKey())
|
||||||
|
if err == nil {
|
||||||
|
controlConn = acceptedConn.messageConnFor(controlRW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
ctl, err = svr.RegisterControl(controlConn, m, internal, acceptedConn.wireProtocol)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If login failed, send error message there.
|
|
||||||
// Otherwise send success message in control's work goroutine.
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("register control error: %v", err)
|
xl.Warnf("register control error: %v", err)
|
||||||
_ = msg.WriteMsg(conn, &msg.LoginResp{
|
_ = acceptedConn.conn.WriteMsg(&msg.LoginResp{
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
Error: util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
Error: util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
||||||
})
|
})
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if err = acceptedConn.conn.WriteMsg(&msg.LoginResp{
|
||||||
|
Version: version.Full(),
|
||||||
|
RunID: ctl.runID,
|
||||||
|
Error: "",
|
||||||
|
}); err != nil {
|
||||||
|
xl.Warnf("write login response error: %v", err)
|
||||||
|
svr.ctlManager.Del(m.RunID, ctl)
|
||||||
|
svr.clientRegistry.MarkOfflineByRunID(m.RunID)
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctl.Start()
|
||||||
|
metrics.Server.NewClient()
|
||||||
|
go func() {
|
||||||
|
// block until control closed
|
||||||
|
ctl.WaitClosed()
|
||||||
|
svr.ctlManager.Del(m.RunID, ctl)
|
||||||
|
}()
|
||||||
case *msg.NewWorkConn:
|
case *msg.NewWorkConn:
|
||||||
if err := svr.RegisterWorkConn(conn, m); err != nil {
|
if err := svr.RegisterWorkConn(acceptedConn.conn, m); err != nil {
|
||||||
|
_ = acceptedConn.conn.WriteMsg(&msg.StartWorkConn{
|
||||||
|
Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
||||||
|
})
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
case *msg.NewVisitorConn:
|
case *msg.NewVisitorConn:
|
||||||
if err = svr.RegisterVisitorConn(conn, m); err != nil {
|
if err = svr.RegisterVisitorConn(conn, m); err != nil {
|
||||||
xl.Warnf("register visitor conn error: %v", err)
|
xl.Warnf("register visitor conn error: %v", err)
|
||||||
_ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{
|
_ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{
|
||||||
ProxyName: m.ProxyName,
|
ProxyName: m.ProxyName,
|
||||||
Error: util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
Error: util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
||||||
})
|
})
|
||||||
conn.Close()
|
conn.Close()
|
||||||
} else {
|
} else {
|
||||||
_ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{
|
_ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{
|
||||||
ProxyName: m.ProxyName,
|
ProxyName: m.ProxyName,
|
||||||
Error: "",
|
Error: "",
|
||||||
})
|
})
|
||||||
@@ -492,6 +520,87 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type acceptedConnection struct {
|
||||||
|
conn *msg.Conn
|
||||||
|
wireProtocol string
|
||||||
|
firstMsg msg.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) acceptConnection(ctx context.Context, conn net.Conn) (*acceptedConnection, error) {
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(connReadTimeout))
|
||||||
|
checkedConn, isV2, err := wire.CheckMagic(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read wire protocol magic: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wireProtocol := wire.ProtocolV1
|
||||||
|
if isV2 {
|
||||||
|
wireProtocol = wire.ProtocolV2
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = netpkg.NewContextConn(ctx, checkedConn)
|
||||||
|
acceptedConn := &acceptedConnection{wireProtocol: wireProtocol}
|
||||||
|
if isV2 {
|
||||||
|
wireConn := wire.NewConn(conn)
|
||||||
|
rw := msg.NewV2ReadWriterWithConn(wireConn)
|
||||||
|
acceptedConn.conn = msg.NewConn(conn, rw)
|
||||||
|
acceptedConn.firstMsg, err = acceptedConn.readFirstV2Msg(wireConn)
|
||||||
|
} else {
|
||||||
|
rw := msg.NewV1ReadWriter(conn)
|
||||||
|
acceptedConn.conn = msg.NewConn(conn, rw)
|
||||||
|
acceptedConn.firstMsg, err = acceptedConn.conn.ReadMsg()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
return acceptedConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *acceptedConnection) messageConnFor(rw io.ReadWriter) *msg.Conn {
|
||||||
|
return msg.NewConn(ac.conn, msg.NewReadWriter(rw, ac.wireProtocol))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *acceptedConnection) readFirstV2Msg(wireConn *wire.Conn) (msg.Message, error) {
|
||||||
|
frame, err := wireConn.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read v2 frame: %w", err)
|
||||||
|
}
|
||||||
|
if frame.Type == wire.FrameTypeClientHello {
|
||||||
|
if err := ac.handleClientHello(wireConn, frame); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
frame, err = wireConn.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read first v2 message frame: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := msg.DecodeV2MessageFrame(frame)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode v2 message: %w", err)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *acceptedConnection) handleClientHello(wireConn *wire.Conn, frame *wire.Frame) error {
|
||||||
|
var hello wire.ClientHello
|
||||||
|
if err := wireConn.UnmarshalFrame(frame, &hello); err != nil {
|
||||||
|
return fmt.Errorf("decode ClientHello: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverHello := wire.DefaultServerHello()
|
||||||
|
if err := wire.ValidateClientHello(hello); err != nil {
|
||||||
|
serverHello.Error = err.Error()
|
||||||
|
_ = wireConn.WriteJSONFrame(wire.FrameTypeServerHello, serverHello)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, serverHello); err != nil {
|
||||||
|
return fmt.Errorf("write ServerHello: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HandleListener accepts connections from client and call handleConnection to handle them.
|
// HandleListener accepts connections from client and call handleConnection to handle them.
|
||||||
// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway.
|
// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway.
|
||||||
// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters.
|
// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters.
|
||||||
@@ -577,14 +686,19 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, internal bool) error {
|
func (svr *Service) RegisterControl(
|
||||||
|
ctlConn *msg.Conn,
|
||||||
|
loginMsg *msg.Login,
|
||||||
|
internal bool,
|
||||||
|
wireProtocol string,
|
||||||
|
) (*Control, error) {
|
||||||
// If client's RunID is empty, it's a new client, we just create a new controller.
|
// If client's RunID is empty, it's a new client, we just create a new controller.
|
||||||
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
|
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
|
||||||
var err error
|
var err error
|
||||||
if loginMsg.RunID == "" {
|
if loginMsg.RunID == "" {
|
||||||
loginMsg.RunID, err = util.RandID()
|
loginMsg.RunID, err = util.RandID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,7 +715,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
authVerifier = auth.AlwaysPassVerifier
|
authVerifier = auth.AlwaysPassVerifier
|
||||||
}
|
}
|
||||||
if err := authVerifier.VerifyLogin(loginMsg); err != nil {
|
if err := authVerifier.VerifyLogin(loginMsg); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctl, err := NewControl(ctx, &SessionContext{
|
ctl, err := NewControl(ctx, &SessionContext{
|
||||||
@@ -611,7 +725,6 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
AuthVerifier: authVerifier,
|
AuthVerifier: authVerifier,
|
||||||
EncryptionKey: svr.auth.EncryptionKey(),
|
EncryptionKey: svr.auth.EncryptionKey(),
|
||||||
Conn: ctlConn,
|
Conn: ctlConn,
|
||||||
ConnEncrypted: !internal,
|
|
||||||
LoginMsg: loginMsg,
|
LoginMsg: loginMsg,
|
||||||
ServerCfg: svr.cfg,
|
ServerCfg: svr.cfg,
|
||||||
ClientRegistry: svr.clientRegistry,
|
ClientRegistry: svr.clientRegistry,
|
||||||
@@ -619,7 +732,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("create new controller error: %v", err)
|
xl.Warnf("create new controller error: %v", err)
|
||||||
// don't return detailed errors to client
|
// don't return detailed errors to client
|
||||||
return fmt.Errorf("unexpected error when creating new controller")
|
return nil, fmt.Errorf("unexpected error when creating new controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
||||||
@@ -630,34 +743,24 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
||||||
remoteAddr = host
|
remoteAddr = host
|
||||||
}
|
}
|
||||||
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Version, remoteAddr)
|
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Version, remoteAddr, wireProtocol)
|
||||||
if conflict {
|
if conflict {
|
||||||
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
||||||
ctl.Close()
|
return nil, fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
|
||||||
return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctl.Start()
|
return ctl, nil
|
||||||
|
|
||||||
// for statistics
|
|
||||||
metrics.Server.NewClient()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// block until control closed
|
|
||||||
ctl.WaitClosed()
|
|
||||||
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterWorkConn register a new work connection to control and proxies need it.
|
// RegisterWorkConn register a new work connection to control and proxies need it.
|
||||||
func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error {
|
func (svr *Service) RegisterWorkConn(workConn *msg.Conn, newMsg *msg.NewWorkConn) error {
|
||||||
xl := netpkg.NewLogFromConn(workConn)
|
xl := netpkg.NewLogFromConn(workConn)
|
||||||
ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
|
ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
|
||||||
if !exist {
|
if !exist {
|
||||||
xl.Warnf("no client control found for run id [%s]", newMsg.RunID)
|
xl.Warnf("no client control found for run id [%s]", newMsg.RunID)
|
||||||
return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID)
|
return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// server plugin hook
|
// server plugin hook
|
||||||
content := &plugin.NewWorkConnContent{
|
content := &plugin.NewWorkConnContent{
|
||||||
User: plugin.UserInfo{
|
User: plugin.UserInfo{
|
||||||
@@ -675,12 +778,9 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
|
xl.Warnf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
|
||||||
_ = msg.WriteMsg(workConn, &msg.StartWorkConn{
|
return err
|
||||||
Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
|
|
||||||
}
|
}
|
||||||
return ctl.RegisterWorkConn(workConn)
|
return ctl.RegisterWorkConn(proxy.NewWorkConn(workConn))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error {
|
func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error {
|
||||||
|
|||||||
236
test/e2e/compatibility/compatibility_test.go
Normal file
236
test/e2e/compatibility/compatibility_test.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package compatibility
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/onsi/ginkgo/v2"
|
||||||
|
"github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/test/e2e/framework"
|
||||||
|
"github.com/fatedier/frp/test/e2e/framework/consts"
|
||||||
|
"github.com/fatedier/frp/test/e2e/pkg/port"
|
||||||
|
"github.com/fatedier/frp/test/e2e/pkg/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
type compatTestContext struct {
|
||||||
|
CurrentFRPSPath string
|
||||||
|
CurrentFRPCPath string
|
||||||
|
BaselineFRPSPath string
|
||||||
|
BaselineFRPCPath string
|
||||||
|
BaselineVersion string
|
||||||
|
LogLevel string
|
||||||
|
Debug bool
|
||||||
|
RunCurrentCurrent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var compatCtx compatTestContext
|
||||||
|
|
||||||
|
func registerFlags(flags *flag.FlagSet) {
|
||||||
|
flags.StringVar(&compatCtx.CurrentFRPSPath, "current-frps-path", "../../bin/frps", "The current frps binary to use.")
|
||||||
|
flags.StringVar(&compatCtx.CurrentFRPCPath, "current-frpc-path", "../../bin/frpc", "The current frpc binary to use.")
|
||||||
|
flags.StringVar(&compatCtx.BaselineFRPSPath, "baseline-frps-path", "", "The baseline frps binary to use.")
|
||||||
|
flags.StringVar(&compatCtx.BaselineFRPCPath, "baseline-frpc-path", "", "The baseline frpc binary to use.")
|
||||||
|
flags.StringVar(&compatCtx.BaselineVersion, "baseline-version", "custom", "The baseline version label for reporting.")
|
||||||
|
flags.StringVar(&compatCtx.LogLevel, "log-level", "debug", "Log level.")
|
||||||
|
flags.BoolVar(&compatCtx.Debug, "debug", false, "Enable debug mode to print detailed info.")
|
||||||
|
flags.BoolVar(&compatCtx.RunCurrentCurrent, "run-current-current", true, "Run current frps/current frpc sanity checks.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCompatContext(t *compatTestContext) error {
|
||||||
|
paths := map[string]string{
|
||||||
|
"current-frps-path": t.CurrentFRPSPath,
|
||||||
|
"current-frpc-path": t.CurrentFRPCPath,
|
||||||
|
"baseline-frps-path": t.BaselineFRPSPath,
|
||||||
|
"baseline-frpc-path": t.BaselineFRPCPath,
|
||||||
|
}
|
||||||
|
for name, path := range paths {
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Errorf("%s can't be empty", name)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return fmt.Errorf("load %s error: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
registerFlags(flag.CommandLine)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := validateCompatContext(&compatCtx); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
framework.TestContext.Debug = compatCtx.Debug
|
||||||
|
framework.TestContext.LogLevel = compatCtx.LogLevel
|
||||||
|
log.InitLogger("console", compatCtx.LogLevel, 0, true)
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompatibilityE2E(t *testing.T) {
|
||||||
|
gomega.RegisterFailHandler(framework.Fail)
|
||||||
|
|
||||||
|
suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration()
|
||||||
|
suiteConfig.EmitSpecProgress = true
|
||||||
|
suiteConfig.RandomizeAllSpecs = true
|
||||||
|
ginkgo.RunSpecs(t, "frp compatibility e2e suite", suiteConfig, reporterConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = ginkgo.Describe("[Compatibility: WireProtocol]", func() {
|
||||||
|
f := framework.NewDefaultFramework()
|
||||||
|
|
||||||
|
ginkgo.It("current frps and current frpc support v1 and v2", func() {
|
||||||
|
if !compatCtx.RunCurrentCurrent {
|
||||||
|
ginkgo.Skip("current/current sanity checks already ran")
|
||||||
|
}
|
||||||
|
|
||||||
|
webPort := f.AllocPort()
|
||||||
|
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
|
||||||
|
webServer.port = %d
|
||||||
|
`, webPort)
|
||||||
|
|
||||||
|
v1PortName := port.GenName("CompatCurrentV1")
|
||||||
|
v1ClientConf := tcpClientConfig("compat-current-v1", v1PortName, `
|
||||||
|
clientID = "compat-current-v1"
|
||||||
|
transport.wireProtocol = "v1"
|
||||||
|
`)
|
||||||
|
|
||||||
|
v2PortName := port.GenName("CompatCurrentV2")
|
||||||
|
v2ClientConf := tcpClientConfig("compat-current-v2", v2PortName, `
|
||||||
|
clientID = "compat-current-v2"
|
||||||
|
transport.wireProtocol = "v2"
|
||||||
|
`)
|
||||||
|
|
||||||
|
f.RunProcessesWithBinaries(
|
||||||
|
compatCtx.CurrentFRPSPath,
|
||||||
|
compatCtx.CurrentFRPCPath,
|
||||||
|
serverConf,
|
||||||
|
[]string{v1ClientConf, v2ClientConf},
|
||||||
|
)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).PortName(v1PortName).Ensure()
|
||||||
|
framework.NewRequestExpect(f).PortName(v2PortName).Ensure()
|
||||||
|
expectClientWireProtocol(webPort, "compat-current-v1", "v1")
|
||||||
|
expectClientWireProtocol(webPort, "compat-current-v2", "v2")
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("current frps accepts baseline frpc using v1", func() {
|
||||||
|
webPort := f.AllocPort()
|
||||||
|
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
|
||||||
|
webServer.port = %d
|
||||||
|
`, webPort)
|
||||||
|
|
||||||
|
portName := port.GenName("CompatBaselineFRPC")
|
||||||
|
clientConf := tcpClientConfig("tcp", portName, "")
|
||||||
|
|
||||||
|
f.RunProcessesWithBinaries(
|
||||||
|
compatCtx.CurrentFRPSPath,
|
||||||
|
compatCtx.BaselineFRPCPath,
|
||||||
|
serverConf,
|
||||||
|
[]string{clientConf},
|
||||||
|
)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).PortName(portName).Ensure()
|
||||||
|
expectSingleClientWireProtocol(webPort, "v1")
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("baseline frps accepts current frpc defaulting to v1", func() {
|
||||||
|
portName := port.GenName("CompatBaselineFRPS")
|
||||||
|
clientConf := tcpClientConfig("tcp", portName, "")
|
||||||
|
|
||||||
|
f.RunProcessesWithBinaries(
|
||||||
|
compatCtx.BaselineFRPSPath,
|
||||||
|
compatCtx.CurrentFRPCPath,
|
||||||
|
consts.DefaultServerConfig,
|
||||||
|
[]string{clientConf},
|
||||||
|
)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).PortName(portName).Ensure()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("baseline frps rejects current frpc forced to v2", func() {
|
||||||
|
portName := port.GenName("CompatBaselineFRPSForcedV2")
|
||||||
|
clientConf := tcpClientConfig("tcp", portName, `
|
||||||
|
transport.wireProtocol = "v2"
|
||||||
|
`)
|
||||||
|
|
||||||
|
_, clientProcesses := f.RunProcessesWithBinaries(
|
||||||
|
compatCtx.BaselineFRPSPath,
|
||||||
|
compatCtx.CurrentFRPCPath,
|
||||||
|
consts.DefaultServerConfig,
|
||||||
|
[]string{clientConf},
|
||||||
|
)
|
||||||
|
expectProcessExit(clientProcesses[0], 5*time.Second)
|
||||||
|
framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
func tcpClientConfig(proxyName string, remotePortName string, extra string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = {{ .%s }}
|
||||||
|
loginFailExit = true
|
||||||
|
log.level = "trace"
|
||||||
|
%s
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "%s"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = {{ .%s }}
|
||||||
|
remotePort = {{ .%s }}
|
||||||
|
`, consts.PortServerName, extra, proxyName, framework.TCPEchoServerPort, remotePortName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectProcessExit(p *process.Process, timeout time.Duration) {
|
||||||
|
select {
|
||||||
|
case <-p.Done():
|
||||||
|
case <-time.After(timeout):
|
||||||
|
framework.Failf("process did not exit within %s; output:\n%s", timeout, p.Output())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wireClientInfo struct {
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
WireProtocol string `json:"wireProtocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectSingleClientWireProtocol(webPort int, wireProtocol string) {
|
||||||
|
clients := getWireClientInfos(webPort)
|
||||||
|
framework.ExpectEqual(len(clients), 1)
|
||||||
|
framework.ExpectEqual(clients[0].WireProtocol, wireProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectClientWireProtocol(webPort int, clientID string, wireProtocol string) {
|
||||||
|
for _, client := range getWireClientInfos(webPort) {
|
||||||
|
if client.ClientID == clientID {
|
||||||
|
framework.ExpectEqual(client.WireProtocol, wireProtocol)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
framework.Failf("client %q not found in /api/clients response", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWireClientInfos(webPort int) []wireClientInfo {
|
||||||
|
client := http.Client{Timeout: consts.DefaultTimeout}
|
||||||
|
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/api/clients", webPort))
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
framework.ExpectEqual(resp.StatusCode, http.StatusOK)
|
||||||
|
|
||||||
|
content, err := io.ReadAll(resp.Body)
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
var clients []wireClientInfo
|
||||||
|
framework.ExpectNoError(json.Unmarshal(content, &clients))
|
||||||
|
return clients
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ var _ = ginkgo.Describe("[Feature: Example]", func() {
|
|||||||
remotePort = %d
|
remotePort = %d
|
||||||
`, framework.TCPEchoServerPort, remotePort)
|
`, framework.TCPEchoServerPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -241,6 +241,31 @@ func (f *Framework) AllocPort() int {
|
|||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Framework) AllocPortExcludingRanges(ranges ...[2]int) int {
|
||||||
|
for range 1000 {
|
||||||
|
port := f.portAllocator.Get()
|
||||||
|
ExpectTrue(port > 0, "alloc port failed")
|
||||||
|
|
||||||
|
inExcludedRange := false
|
||||||
|
for _, portRange := range ranges {
|
||||||
|
if port >= portRange[0] && port <= portRange[1] {
|
||||||
|
inExcludedRange = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inExcludedRange {
|
||||||
|
f.portAllocator.Release(port)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.allocatedPorts = append(f.allocatedPorts, port)
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
Failf("alloc port outside excluded ranges failed")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Framework) ReleasePort(port int) {
|
func (f *Framework) ReleasePort(port int) {
|
||||||
f.portAllocator.Release(port)
|
f.portAllocator.Release(port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,68 +3,94 @@ package framework
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
flog "github.com/fatedier/frp/pkg/util/log"
|
flog "github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/test/e2e/framework/consts"
|
||||||
"github.com/fatedier/frp/test/e2e/pkg/process"
|
"github.com/fatedier/frp/test/e2e/pkg/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunProcesses run multiple processes from templates.
|
// RunProcesses starts one frps and zero or more frpc processes from templates.
|
||||||
// The first template should always be frps.
|
func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string) (*process.Process, []*process.Process) {
|
||||||
func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) {
|
return f.RunProcessesWithBinaries(TestContext.FRPServerPath, TestContext.FRPClientPath, serverTemplate, clientTemplates)
|
||||||
templates := make([]string, 0, len(serverTemplates)+len(clientTemplates))
|
}
|
||||||
templates = append(templates, serverTemplates...)
|
|
||||||
templates = append(templates, clientTemplates...)
|
// RunProcessesWithBinaries starts one frps and zero or more frpc processes with explicit binary paths.
|
||||||
|
func (f *Framework) RunProcessesWithBinaries(
|
||||||
|
serverBinaryPath string,
|
||||||
|
clientBinaryPath string,
|
||||||
|
serverTemplate string,
|
||||||
|
clientTemplates []string,
|
||||||
|
) (*process.Process, []*process.Process) {
|
||||||
|
templates := append([]string{serverTemplate}, clientTemplates...)
|
||||||
outs, ports, err := f.RenderTemplates(templates)
|
outs, ports, err := f.RenderTemplates(templates)
|
||||||
ExpectNoError(err)
|
ExpectNoError(err)
|
||||||
ExpectTrue(len(templates) > 0)
|
|
||||||
|
|
||||||
maps.Copy(f.usedPorts, ports)
|
maps.Copy(f.usedPorts, ports)
|
||||||
|
|
||||||
currentServerProcesses := make([]*process.Process, 0, len(serverTemplates))
|
// Start frps.
|
||||||
for i := range serverTemplates {
|
serverPath := filepath.Join(f.TempDirectory, "frp-e2e-server-0")
|
||||||
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
|
err = os.WriteFile(serverPath, []byte(outs[0]), 0o600)
|
||||||
err = os.WriteFile(path, []byte(outs[i]), 0o600)
|
ExpectNoError(err)
|
||||||
ExpectNoError(err)
|
|
||||||
|
|
||||||
if TestContext.Debug {
|
if TestContext.Debug {
|
||||||
flog.Debugf("[%s] %s", path, outs[i])
|
flog.Debugf("[%s] %s", serverPath, outs[0])
|
||||||
}
|
|
||||||
|
|
||||||
p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs)
|
|
||||||
f.serverConfPaths = append(f.serverConfPaths, path)
|
|
||||||
f.serverProcesses = append(f.serverProcesses, p)
|
|
||||||
currentServerProcesses = append(currentServerProcesses, p)
|
|
||||||
err = p.Start()
|
|
||||||
ExpectNoError(err)
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
currentClientProcesses := make([]*process.Process, 0, len(clientTemplates))
|
serverProcess := process.NewWithEnvs(serverBinaryPath, []string{"-c", serverPath}, f.osEnvs)
|
||||||
|
f.serverConfPaths = append(f.serverConfPaths, serverPath)
|
||||||
|
f.serverProcesses = append(f.serverProcesses, serverProcess)
|
||||||
|
err = serverProcess.Start()
|
||||||
|
ExpectNoError(err)
|
||||||
|
|
||||||
|
if port, ok := ports[consts.PortServerName]; ok {
|
||||||
|
ExpectNoError(WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 5*time.Second))
|
||||||
|
} else {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start frpc(s).
|
||||||
|
clientProcesses := make([]*process.Process, 0, len(clientTemplates))
|
||||||
for i := range clientTemplates {
|
for i := range clientTemplates {
|
||||||
index := i + len(serverTemplates)
|
|
||||||
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
|
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
|
||||||
err = os.WriteFile(path, []byte(outs[index]), 0o600)
|
err = os.WriteFile(path, []byte(outs[1+i]), 0o600)
|
||||||
ExpectNoError(err)
|
ExpectNoError(err)
|
||||||
|
|
||||||
if TestContext.Debug {
|
if TestContext.Debug {
|
||||||
flog.Debugf("[%s] %s", path, outs[index])
|
flog.Debugf("[%s] %s", path, outs[1+i])
|
||||||
}
|
}
|
||||||
|
|
||||||
p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs)
|
p := process.NewWithEnvs(clientBinaryPath, []string{"-c", path}, f.osEnvs)
|
||||||
f.clientConfPaths = append(f.clientConfPaths, path)
|
f.clientConfPaths = append(f.clientConfPaths, path)
|
||||||
f.clientProcesses = append(f.clientProcesses, p)
|
f.clientProcesses = append(f.clientProcesses, p)
|
||||||
currentClientProcesses = append(currentClientProcesses, p)
|
clientProcesses = append(clientProcesses, p)
|
||||||
err = p.Start()
|
err = p.Start()
|
||||||
ExpectNoError(err)
|
ExpectNoError(err)
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
time.Sleep(3 * time.Second)
|
// Wait for each client's proxies to register with frps.
|
||||||
|
// If any client has no proxies (e.g. visitor-only), fall back to sleep
|
||||||
|
// for the remaining time since visitors have no deterministic readiness signal.
|
||||||
|
allConfirmed := len(clientProcesses) > 0
|
||||||
|
start := time.Now()
|
||||||
|
for i, p := range clientProcesses {
|
||||||
|
configPath := f.clientConfPaths[len(f.clientConfPaths)-len(clientProcesses)+i]
|
||||||
|
if !waitForClientProxyReady(configPath, p, 5*time.Second) {
|
||||||
|
allConfirmed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clientProcesses) > 0 && !allConfirmed {
|
||||||
|
remaining := 1500*time.Millisecond - time.Since(start)
|
||||||
|
if remaining > 0 {
|
||||||
|
time.Sleep(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return currentServerProcesses, currentClientProcesses
|
return serverProcess, clientProcesses
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
|
func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
|
||||||
@@ -72,11 +98,13 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
|
|||||||
f.serverProcesses = append(f.serverProcesses, p)
|
f.serverProcesses = append(f.serverProcesses, p)
|
||||||
err := p.Start()
|
err := p.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, p.StdOutput(), err
|
return p, p.Output(), err
|
||||||
}
|
}
|
||||||
// Give frps extra time to finish binding ports before proceeding.
|
select {
|
||||||
time.Sleep(4 * time.Second)
|
case <-p.Done():
|
||||||
return p, p.StdOutput(), nil
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
return p, p.Output(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
|
func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
|
||||||
@@ -84,10 +112,13 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
|
|||||||
f.clientProcesses = append(f.clientProcesses, p)
|
f.clientProcesses = append(f.clientProcesses, p)
|
||||||
err := p.Start()
|
err := p.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, p.StdOutput(), err
|
return p, p.Output(), err
|
||||||
}
|
}
|
||||||
time.Sleep(2 * time.Second)
|
select {
|
||||||
return p, p.StdOutput(), nil
|
case <-p.Done():
|
||||||
|
case <-time.After(1500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
return p, p.Output(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Framework) GenerateConfigFile(content string) string {
|
func (f *Framework) GenerateConfigFile(content string) string {
|
||||||
@@ -97,3 +128,74 @@ func (f *Framework) GenerateConfigFile(content string) string {
|
|||||||
ExpectNoError(err)
|
ExpectNoError(err)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForClientProxyReady parses the client config to extract proxy names,
|
||||||
|
// then waits for each proxy's "start proxy success" log in the process output.
|
||||||
|
// Returns true only if proxies were expected and all registered successfully.
|
||||||
|
func waitForClientProxyReady(configPath string, p *process.Process, timeout time.Duration) bool {
|
||||||
|
_, proxyCfgs, _, _, err := config.LoadClientConfig(configPath, false)
|
||||||
|
if err != nil || len(proxyCfgs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a single deadline so the total wait across all proxies does not exceed timeout.
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for _, cfg := range proxyCfgs {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
pattern := fmt.Sprintf("[%s] start proxy success", name)
|
||||||
|
if err := p.WaitForOutput(pattern, 1, remaining); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForTCPUnreachable polls a TCP address until a connection fails or timeout.
|
||||||
|
func WaitForTCPUnreachable(addr string, interval, timeout time.Duration) error {
|
||||||
|
if interval <= 0 {
|
||||||
|
return fmt.Errorf("invalid interval for TCP unreachable on %s: interval must be positive", addr)
|
||||||
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
return fmt.Errorf("invalid timeout for TCP unreachable on %s: timeout must be positive", addr)
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining <= 0 {
|
||||||
|
return fmt.Errorf("timeout waiting for TCP unreachable on %s", addr)
|
||||||
|
}
|
||||||
|
dialTimeout := min(interval, remaining)
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
time.Sleep(min(interval, time.Until(deadline)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForTCPReady polls a TCP address until a connection succeeds or timeout.
|
||||||
|
func WaitForTCPReady(addr string, timeout time.Duration) error {
|
||||||
|
if timeout <= 0 {
|
||||||
|
return fmt.Errorf("invalid timeout for TCP readiness on %s: timeout must be positive", addr)
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
var lastErr error
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, 100*time.Millisecond)
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if lastErr == nil {
|
||||||
|
return fmt.Errorf("timeout waiting for TCP readiness on %s before any dial attempt", addr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("timeout waiting for TCP readiness on %s: %w", addr, lastErr)
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
|
|||||||
clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n")
|
clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n")
|
||||||
}
|
}
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf.String()})
|
f.RunProcesses(serverConf, []string{clientConf.String()})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
framework.NewRequestExpect(f).
|
framework.NewRequestExpect(f).
|
||||||
@@ -152,7 +152,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
|
|||||||
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n")
|
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n")
|
||||||
}
|
}
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf.String()})
|
f.RunProcesses(serverConf, []string{clientConf.String()})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
for domain := range strings.SplitSeq(test.customDomains, ",") {
|
for domain := range strings.SplitSeq(test.customDomains, ",") {
|
||||||
@@ -235,7 +235,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
|
|||||||
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n")
|
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n")
|
||||||
}
|
}
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf.String()})
|
f.RunProcesses(serverConf, []string{clientConf.String()})
|
||||||
|
|
||||||
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||||
framework.ExpectNoError(err)
|
framework.ExpectNoError(err)
|
||||||
@@ -419,7 +419,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()})
|
f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
timeout := time.Second
|
timeout := time.Second
|
||||||
@@ -497,7 +497,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf.String()})
|
f.RunProcesses(serverConf, []string{clientConf.String()})
|
||||||
|
|
||||||
// Request without HTTP connect should get error
|
// Request without HTTP connect should get error
|
||||||
framework.NewRequestExpect(f).
|
framework.NewRequestExpect(f).
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
|
|||||||
framework.TCPEchoServerPort, p2Port,
|
framework.TCPEchoServerPort, p2Port,
|
||||||
framework.TCPEchoServerPort, p3Port)
|
framework.TCPEchoServerPort, p3Port)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(p1Port).Ensure()
|
framework.NewRequestExpect(f).Port(p1Port).Ensure()
|
||||||
framework.NewRequestExpect(f).Port(p2Port).Ensure()
|
framework.NewRequestExpect(f).Port(p2Port).Ensure()
|
||||||
@@ -90,7 +90,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
|
|||||||
admin_pwd = admin
|
admin_pwd = admin
|
||||||
`, dashboardPort)
|
`, dashboardPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.HTTP().HTTPPath("/healthz")
|
r.HTTP().HTTPPath("/healthz")
|
||||||
@@ -116,7 +116,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
|
|||||||
remote_port = %d
|
remote_port = %d
|
||||||
`, adminPort, framework.TCPEchoServerPort, testPort)
|
`, adminPort, framework.TCPEchoServerPort, testPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(testPort).Ensure()
|
framework.NewRequestExpect(f).Port(testPort).Ensure()
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur
|
|||||||
clientConfs = append(clientConfs, client2Conf)
|
clientConfs = append(clientConfs, client2Conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, clientConfs)
|
f.RunProcesses(serverConf, clientConfs)
|
||||||
|
|
||||||
if configures.testDelay > 0 {
|
if configures.testDelay > 0 {
|
||||||
time.Sleep(configures.testDelay)
|
time.Sleep(configures.testDelay)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() {
|
|||||||
`, "`", "`", framework.TCPEchoServerPort, portName)
|
`, "`", "`", framework.TCPEchoServerPort, portName)
|
||||||
|
|
||||||
f.SetEnvs([]string{"FRP_TOKEN=123"})
|
f.SetEnvs([]string{"FRP_TOKEN=123"})
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).PortName(portName).Ensure()
|
framework.NewRequestExpect(f).PortName(portName).Ensure()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
locations = /bar
|
locations = /bar
|
||||||
`, fooPort, barPort)
|
`, fooPort, barPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
path string
|
path string
|
||||||
@@ -111,7 +111,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
custom_domains = normal.example.com
|
custom_domains = normal.example.com
|
||||||
`, fooPort, barPort, otherPort)
|
`, fooPort, barPort, otherPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// user1
|
// user1
|
||||||
framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
|
||||||
@@ -152,7 +152,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
http_pwd = test
|
http_pwd = test
|
||||||
`, framework.HTTPSimpleServerPort)
|
`, framework.HTTPSimpleServerPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// not set auth header
|
// not set auth header
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||||
@@ -188,7 +188,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
custom_domains = *.example.com
|
custom_domains = *.example.com
|
||||||
`, framework.HTTPSimpleServerPort)
|
`, framework.HTTPSimpleServerPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// not match host
|
// not match host
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||||
@@ -238,7 +238,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
subdomain = bar
|
subdomain = bar
|
||||||
`, fooPort, barPort)
|
`, fooPort, barPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// foo
|
// foo
|
||||||
framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort).
|
||||||
@@ -279,7 +279,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
header_X-From-Where = frp
|
header_X-From-Where = frp
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// not set auth header
|
// not set auth header
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||||
@@ -312,7 +312,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
host_header_rewrite = rewrite.example.com
|
host_header_rewrite = rewrite.example.com
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||||
RequestModify(func(r *request.Request) {
|
RequestModify(func(r *request.Request) {
|
||||||
@@ -360,7 +360,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
custom_domains = 127.0.0.1
|
custom_domains = 127.0.0.1
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)}
|
u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)}
|
||||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
ginkgo.It("Ports Whitelist", func() {
|
ginkgo.It("Ports Whitelist", func() {
|
||||||
serverConf := consts.LegacyDefaultServerConfig
|
serverConf := consts.LegacyDefaultServerConfig
|
||||||
clientConf := consts.LegacyDefaultClientConfig
|
clientConf := consts.LegacyDefaultClientConfig
|
||||||
|
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
|
||||||
serverConf += `
|
serverConf += `
|
||||||
allow_ports = 10000-11000,11002,12000-13000
|
allow_ports = 10000-11000,11002,12000-13000
|
||||||
@@ -37,8 +39,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
[tcp-port-not-allowed]
|
[tcp-port-not-allowed]
|
||||||
type = tcp
|
type = tcp
|
||||||
local_port = {{ .%s }}
|
local_port = {{ .%s }}
|
||||||
remote_port = 11001
|
remote_port = %d
|
||||||
`, framework.TCPEchoServerPort)
|
`, framework.TCPEchoServerPort, tcpPortNotAllowed)
|
||||||
clientConf += fmt.Sprintf(`
|
clientConf += fmt.Sprintf(`
|
||||||
[tcp-port-unavailable]
|
[tcp-port-unavailable]
|
||||||
type = tcp
|
type = tcp
|
||||||
@@ -55,17 +57,17 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
[udp-port-not-allowed]
|
[udp-port-not-allowed]
|
||||||
type = udp
|
type = udp
|
||||||
local_port = {{ .%s }}
|
local_port = {{ .%s }}
|
||||||
remote_port = 11003
|
remote_port = %d
|
||||||
`, framework.UDPEchoServerPort)
|
`, framework.UDPEchoServerPort, udpPortNotAllowed)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// TCP
|
// TCP
|
||||||
// Allowed in range
|
// Allowed in range
|
||||||
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure()
|
||||||
|
|
||||||
// Unavailable, already bind by frps
|
// Unavailable, already bind by frps
|
||||||
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
||||||
@@ -76,7 +78,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.UDP().Port(11003)
|
r.UDP().Port(udpPortNotAllowed)
|
||||||
}).ExpectError(true).Ensure()
|
}).ExpectError(true).Ensure()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
local_port = {{ .%s }}
|
local_port = {{ .%s }}
|
||||||
`, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort)
|
`, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
client := f.APIClientForFrpc(adminPort)
|
client := f.APIClientForFrpc(adminPort)
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
custom_domains = example.com
|
custom_domains = example.com
|
||||||
`, framework.HTTPSimpleServerPort)
|
`, framework.HTTPSimpleServerPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.HTTP().HTTPHost("example.com")
|
r.HTTP().HTTPHost("example.com")
|
||||||
@@ -165,7 +167,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
custom_domains = example.com
|
custom_domains = example.com
|
||||||
`, framework.HTTPSimpleServerPort)
|
`, framework.HTTPSimpleServerPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.HTTP().HTTPPath("/healthz")
|
r.HTTP().HTTPPath("/healthz")
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
|
|||||||
custom_domains = normal.example.com
|
custom_domains = normal.example.com
|
||||||
`, fooPort, barPort, otherPort)
|
`, fooPort, barPort, otherPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// user1
|
// user1
|
||||||
framework.NewRequestExpect(f).Explain("user1").
|
framework.NewRequestExpect(f).Explain("user1").
|
||||||
@@ -121,7 +121,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
|
|||||||
http_pwd = test
|
http_pwd = test
|
||||||
`, fooPort)
|
`, fooPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// not set auth header
|
// not set auth header
|
||||||
framework.NewRequestExpect(f).Explain("no auth").
|
framework.NewRequestExpect(f).Explain("no auth").
|
||||||
@@ -204,7 +204,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
|
|||||||
custom_domains = normal.example.com
|
custom_domains = normal.example.com
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).
|
framework.NewRequestExpect(f).
|
||||||
RequestModify(func(r *request.Request) {
|
RequestModify(func(r *request.Request) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() {
|
|||||||
fallback_timeout_ms = 200
|
fallback_timeout_ms = 200
|
||||||
`, framework.TCPEchoServerPort, bindPortName)
|
`, framework.TCPEchoServerPort, bindPortName)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
framework.NewRequestExpect(f).
|
framework.NewRequestExpect(f).
|
||||||
RequestModify(func(r *request.Request) {
|
RequestModify(func(r *request.Request) {
|
||||||
r.Timeout(time.Second)
|
r.Timeout(time.Second)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
|
|||||||
bandwidth_limit = 10KB
|
bandwidth_limit = 10KB
|
||||||
`, localPort, remotePort)
|
`, localPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
content := strings.Repeat("a", 50*1024) // 5KB
|
content := strings.Repeat("a", 50*1024) // 5KB
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -89,7 +89,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
|
|||||||
remote_port = %d
|
remote_port = %d
|
||||||
`, localPort, remotePort)
|
`, localPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
content := strings.Repeat("a", 50*1024) // 5KB
|
content := strings.Repeat("a", 50*1024) // 5KB
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
|
|||||||
group_key = 123
|
group_key = 123
|
||||||
`, fooPort, remotePort, barPort, remotePort)
|
`, fooPort, remotePort, barPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
fooCount := 0
|
fooCount := 0
|
||||||
barCount := 0
|
barCount := 0
|
||||||
@@ -144,7 +144,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
|
|||||||
health_check_interval_s = 1
|
health_check_interval_s = 1
|
||||||
`, fooPort, remotePort, barPort, remotePort)
|
`, fooPort, remotePort, barPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// check foo and bar is ok
|
// check foo and bar is ok
|
||||||
results := []string{}
|
results := []string{}
|
||||||
@@ -213,7 +213,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
|
|||||||
health_check_url = /healthz
|
health_check_url = /healthz
|
||||||
`, fooPort, barPort)
|
`, fooPort, barPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// send first HTTP request
|
// send first HTTP request
|
||||||
var contents []string
|
var contents []string
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ var _ = ginkgo.Describe("[Feature: Heartbeat]", func() {
|
|||||||
`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
||||||
|
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
|
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() {
|
|||||||
remote_port = %d
|
remote_port = %d
|
||||||
`, framework.TCPEchoServerPort, remotePort)
|
`, framework.TCPEchoServerPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
|
|||||||
custom_domains = normal.example.com
|
custom_domains = normal.example.com
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||||
RequestModify(func(r *request.Request) {
|
RequestModify(func(r *request.Request) {
|
||||||
@@ -90,7 +90,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
|
|||||||
proxy_protocol_version = v2
|
proxy_protocol_version = v2
|
||||||
`, localPort, remotePort)
|
`, localPort, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool {
|
framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool {
|
||||||
log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content))
|
log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content))
|
||||||
@@ -136,7 +136,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
|
|||||||
proxy_protocol_version = v2
|
proxy_protocol_version = v2
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) {
|
||||||
r.HTTP().HTTPHost("normal.example.com")
|
r.HTTP().HTTPHost("normal.example.com")
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n")
|
clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n")
|
||||||
}
|
}
|
||||||
// run frps and frpc
|
// run frps and frpc
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf.String()})
|
f.RunProcesses(serverConf, []string{clientConf.String()})
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure()
|
framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure()
|
||||||
@@ -92,7 +92,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_http_passwd = 123
|
plugin_http_passwd = 123
|
||||||
`, remotePort)
|
`, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// http proxy, no auth info
|
// http proxy, no auth info
|
||||||
framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) {
|
||||||
@@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_passwd = 123
|
plugin_passwd = 123
|
||||||
`, remotePort)
|
`, remotePort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// http proxy, no auth info
|
// http proxy, no auth info
|
||||||
framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) {
|
||||||
@@ -168,7 +168,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_http_passwd = 123
|
plugin_http_passwd = 123
|
||||||
`, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory)
|
`, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
// from tcp proxy
|
// from tcp proxy
|
||||||
framework.NewRequestExpect(f).Request(
|
framework.NewRequestExpect(f).Request(
|
||||||
@@ -202,7 +202,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_local_addr = 127.0.0.1:%d
|
plugin_local_addr = 127.0.0.1:%d
|
||||||
`, localPort)
|
`, localPort)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||||
framework.ExpectNoError(err)
|
framework.ExpectNoError(err)
|
||||||
@@ -246,7 +246,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_key_path = %s
|
plugin_key_path = %s
|
||||||
`, localPort, crtPath, keyPath)
|
`, localPort, crtPath, keyPath)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
localServer := httpserver.New(
|
localServer := httpserver.New(
|
||||||
httpserver.WithBindPort(localPort),
|
httpserver.WithBindPort(localPort),
|
||||||
@@ -290,7 +290,7 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
|
|||||||
plugin_key_path = %s
|
plugin_key_path = %s
|
||||||
`, localPort, crtPath, keyPath)
|
`, localPort, crtPath, keyPath)
|
||||||
|
|
||||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||||
framework.ExpectNoError(err)
|
framework.ExpectNoError(err)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user