mirror of
https://github.com/fatedier/frp.git
synced 2026-04-09 18:49:17 +08:00
Compare commits
47 Commits
381245a439
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d667be7a0a | ||
|
|
31c3deb4f7 | ||
|
|
31e271939b | ||
|
|
061c141756 | ||
|
|
98ee1adb13 | ||
|
|
76abeff881 | ||
|
|
c694b1f6a9 | ||
|
|
5ed02275da | ||
|
|
60c4f5d4bd | ||
|
|
d20e384bf1 | ||
|
|
c95dc9d88a | ||
|
|
38a71a6803 | ||
|
|
6cdef90113 | ||
|
|
85e8e2c830 | ||
|
|
ff4ad2f907 | ||
|
|
94a631fe9c | ||
|
|
6b1be922e1 | ||
|
|
4f584f81d0 | ||
|
|
9669e1ca0c | ||
|
|
48e8901466 | ||
|
|
bcd2424c24 | ||
|
|
c7ac12ea0f | ||
|
|
eeb0dacfc1 | ||
|
|
535eb3db35 | ||
|
|
605f3bdece | ||
|
|
764a626b6e | ||
|
|
c2454e7114 | ||
|
|
017d71717f | ||
|
|
bd200b1a3b | ||
|
|
c70ceff370 | ||
|
|
bb3d0e7140 | ||
|
|
cf396563f8 | ||
|
|
0b4f83cd04 | ||
|
|
e9f7a1a9f2 | ||
|
|
d644593342 | ||
|
|
427c4ca3ae | ||
|
|
f2d1f3739a | ||
|
|
c23894f156 | ||
|
|
cb459b02b6 | ||
|
|
8f633fe363 | ||
|
|
c62a1da161 | ||
|
|
f22f7d539c | ||
|
|
462c987f6d | ||
|
|
541878af4d | ||
|
|
b7435967b0 | ||
|
|
774478d071 | ||
|
|
fbeb6ca43a |
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
|
||||||
|
|||||||
6
.github/workflows/golangci-lint.yml
vendored
6
.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)
|
||||||
|
|||||||
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."
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,10 +25,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/
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ linters:
|
|||||||
- lll
|
- lll
|
||||||
- makezero
|
- makezero
|
||||||
- misspell
|
- misspell
|
||||||
|
- modernize
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- revive
|
- revive
|
||||||
@@ -47,6 +48,9 @@ linters:
|
|||||||
ignore-rules:
|
ignore-rules:
|
||||||
- cancelled
|
- cancelled
|
||||||
- marshalled
|
- marshalled
|
||||||
|
modernize:
|
||||||
|
disable:
|
||||||
|
- omitzero
|
||||||
unparam:
|
unparam:
|
||||||
check-exported: false
|
check-exported: false
|
||||||
exclusions:
|
exclusions:
|
||||||
@@ -86,6 +90,7 @@ linters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gci
|
- gci
|
||||||
@@ -108,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
|
||||||
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 ?
|
||||||
|
|||||||
@@ -1,8 +1 @@
|
|||||||
## 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.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
@@ -1,390 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeConfigManager struct {
|
|
||||||
reloadFromFileFn func(strict bool) error
|
|
||||||
readConfigFileFn func() (string, error)
|
|
||||||
writeConfigFileFn func(content []byte) error
|
|
||||||
getProxyStatusFn func() []*proxy.WorkingStatus
|
|
||||||
isStoreProxyEnabledFn func(name string) bool
|
|
||||||
storeEnabledFn func() bool
|
|
||||||
|
|
||||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
|
||||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
|
||||||
createStoreProxyFn func(cfg v1.ProxyConfigurer) error
|
|
||||||
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) error
|
|
||||||
deleteStoreProxyFn func(name string) error
|
|
||||||
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
|
||||||
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
|
||||||
createStoreVisitFn func(cfg v1.VisitorConfigurer) error
|
|
||||||
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) error
|
|
||||||
deleteStoreVisitFn func(name string) error
|
|
||||||
gracefulCloseFn func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
|
||||||
if m.reloadFromFileFn != nil {
|
|
||||||
return m.reloadFromFileFn(strict)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
|
||||||
if m.readConfigFileFn != nil {
|
|
||||||
return m.readConfigFileFn()
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
|
||||||
if m.writeConfigFileFn != nil {
|
|
||||||
return m.writeConfigFileFn(content)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
|
||||||
if m.getProxyStatusFn != nil {
|
|
||||||
return m.getProxyStatusFn()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
|
||||||
if m.isStoreProxyEnabledFn != nil {
|
|
||||||
return m.isStoreProxyEnabledFn(name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) StoreEnabled() bool {
|
|
||||||
if m.storeEnabledFn != nil {
|
|
||||||
return m.storeEnabledFn()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
|
||||||
if m.listStoreProxiesFn != nil {
|
|
||||||
return m.listStoreProxiesFn()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
|
||||||
if m.getStoreProxyFn != nil {
|
|
||||||
return m.getStoreProxyFn(name)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
|
||||||
if m.createStoreProxyFn != nil {
|
|
||||||
return m.createStoreProxyFn(cfg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error {
|
|
||||||
if m.updateStoreProxyFn != nil {
|
|
||||||
return m.updateStoreProxyFn(name, cfg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
|
||||||
if m.deleteStoreProxyFn != nil {
|
|
||||||
return m.deleteStoreProxyFn(name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
|
||||||
if m.listStoreVisitorsFn != nil {
|
|
||||||
return m.listStoreVisitorsFn()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
|
||||||
if m.getStoreVisitorFn != nil {
|
|
||||||
return m.getStoreVisitorFn(name)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error {
|
|
||||||
if m.createStoreVisitFn != nil {
|
|
||||||
return m.createStoreVisitFn(cfg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error {
|
|
||||||
if m.updateStoreVisitFn != nil {
|
|
||||||
return m.updateStoreVisitFn(name, cfg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
|
||||||
if m.deleteStoreVisitFn != nil {
|
|
||||||
return m.deleteStoreVisitFn(name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
|
||||||
if m.gracefulCloseFn != nil {
|
|
||||||
m.gracefulCloseFn(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDisallowUnknownFieldsForTest(t *testing.T, value bool) func() {
|
|
||||||
t.Helper()
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := v1.DisallowUnknownFields
|
|
||||||
v1.DisallowUnknownFields = value
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return func() {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
v1.DisallowUnknownFields = prev
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDisallowUnknownFieldsForTest() bool {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return v1.DisallowUnknownFields
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
|
||||||
return &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: name,
|
|
||||||
Type: "tcp",
|
|
||||||
ProxyBackend: v1.ProxyBackend{
|
|
||||||
LocalPort: 10080,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRawXTCPVisitorConfig(name string) *v1.XTCPVisitorConfig {
|
|
||||||
return &v1.XTCPVisitorConfig{
|
|
||||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
|
||||||
Name: name,
|
|
||||||
Type: "xtcp",
|
|
||||||
ServerName: "server",
|
|
||||||
BindPort: 10081,
|
|
||||||
SecretKey: "secret",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
|
||||||
status := &proxy.WorkingStatus{
|
|
||||||
Name: "shared-proxy",
|
|
||||||
Type: "tcp",
|
|
||||||
Phase: proxy.ProxyPhaseRunning,
|
|
||||||
RemoteAddr: ":8080",
|
|
||||||
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
|
||||||
}
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
serverAddr: "127.0.0.1",
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
isStoreProxyEnabledFn: func(name string) bool {
|
|
||||||
return name == "shared-proxy"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := controller.buildProxyStatusResp(status)
|
|
||||||
if resp.Source != "store" {
|
|
||||||
t.Fatalf("unexpected source: %q", resp.Source)
|
|
||||||
}
|
|
||||||
if resp.RemoteAddr != "127.0.0.1:8080" {
|
|
||||||
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReloadErrorMapping(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedCode int
|
|
||||||
}{
|
|
||||||
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
|
||||||
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
|
||||||
}
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
|
||||||
_, err := controller.Reload(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, tc.expectedCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreProxyErrorMapping(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedCode int
|
|
||||||
}{
|
|
||||||
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
|
||||||
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
|
||||||
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
body, err := json.Marshal(newRawTCPProxyConfig("shared-proxy"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) error { return tc.err },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = controller.UpdateStoreProxy(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, tc.expectedCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreVisitorErrorMapping(t *testing.T) {
|
|
||||||
body, err := json.Marshal(newRawXTCPVisitorConfig("shared-visitor"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
deleteStoreVisitFn: func(string) error {
|
|
||||||
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = controller.DeleteStoreVisitor(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreProxy_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
|
||||||
restore := setDisallowUnknownFieldsForTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
var gotName string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) error {
|
|
||||||
gotName = cfg.GetBaseConfig().Name
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{"name":"raw-proxy","type":"tcp","localPort":10080,"unexpected":"value"}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
_, err := controller.CreateStoreProxy(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store proxy: %v", err)
|
|
||||||
}
|
|
||||||
if gotName != "raw-proxy" {
|
|
||||||
t.Fatalf("unexpected proxy name: %q", gotName)
|
|
||||||
}
|
|
||||||
if !getDisallowUnknownFieldsForTest() {
|
|
||||||
t.Fatal("global strictness flag was not restored")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreVisitor_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
|
||||||
restore := setDisallowUnknownFieldsForTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
var gotName string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) error {
|
|
||||||
gotName = cfg.GetBaseConfig().Name
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{"name":"raw-visitor","type":"xtcp","serverName":"server","bindPort":10081,"secretKey":"secret","unexpected":"value"}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
_, err := controller.CreateStoreVisitor(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store visitor: %v", err)
|
|
||||||
}
|
|
||||||
if gotName != "raw-visitor" {
|
|
||||||
t.Fatalf("unexpected visitor name: %q", gotName)
|
|
||||||
}
|
|
||||||
if !getDisallowUnknownFieldsForTest() {
|
|
||||||
t.Fatal("global strictness flag was not restored")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fmtError(sentinel error, msg string) error {
|
|
||||||
return fmt.Errorf("%w: %s", sentinel, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertHTTPCode(t *testing.T, err error, expected int) {
|
|
||||||
t.Helper()
|
|
||||||
var httpErr *httppkg.Error
|
|
||||||
if !errors.As(err, &httpErr) {
|
|
||||||
t.Fatalf("unexpected error type: %T", err)
|
|
||||||
}
|
|
||||||
if httpErr.Code != expected {
|
|
||||||
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
adminapi "github.com/fatedier/frp/client/http"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -38,6 +38,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)
|
||||||
@@ -65,9 +67,9 @@ func healthz(w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *api.Controller {
|
func newAPIController(svr *Service) *adminapi.Controller {
|
||||||
manager := newServiceConfigManager(svr)
|
manager := newServiceConfigManager(svr)
|
||||||
return api.NewController(api.ControllerParams{
|
return adminapi.NewController(adminapi.ControllerParams{
|
||||||
ServerAddr: svr.common.ServerAddr,
|
ServerAddr: svr.common.ServerAddr,
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
})
|
})
|
||||||
@@ -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
|
||||||
@@ -133,12 +175,13 @@ func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, e
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.AddProxy(cfg); err != nil {
|
if err := storeSource.AddProxy(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
@@ -146,30 +189,30 @@ func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Infof("store: created proxy %q", name)
|
||||||
log.Infof("store: created proxy %q", cfg.GetBaseConfig().Name)
|
return persisted, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
if bodyName != name {
|
if bodyName != name {
|
||||||
return fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.UpdateProxy(cfg); err != nil {
|
if err := storeSource.UpdateProxy(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
@@ -177,12 +220,13 @@ func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigu
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: updated proxy %q", name)
|
log.Infof("store: updated proxy %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
||||||
@@ -231,12 +275,13 @@ func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigure
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) error {
|
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.AddVisitor(cfg); err != nil {
|
if err := storeSource.AddVisitor(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
@@ -244,30 +289,31 @@ func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: created visitor %q", cfg.GetBaseConfig().Name)
|
log.Infof("store: created visitor %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error {
|
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
if bodyName != name {
|
if bodyName != name {
|
||||||
return fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
return fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
@@ -275,12 +321,13 @@ func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorCon
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("store: updated visitor %q", name)
|
log.Infof("store: updated visitor %q", name)
|
||||||
return nil
|
return persisted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
@@ -340,6 +387,58 @@ func (m *serviceConfigManager) withStoreMutationAndReload(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.ProxyConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetProxy(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.VisitorConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetVisitor(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return fmt.Errorf("invalid proxy config")
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected conflict error")
|
t.Fatal("expected conflict error")
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testin
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected apply config error")
|
t.Fatal("expected apply config error")
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected store disabled error")
|
t.Fatal("expected store disabled error")
|
||||||
}
|
}
|
||||||
@@ -116,10 +116,13 @@ func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *te
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create store proxy: %v", err)
|
t.Fatalf("create store proxy: %v", err)
|
||||||
}
|
}
|
||||||
|
if persisted == nil {
|
||||||
|
t.Fatal("expected persisted proxy to be returned")
|
||||||
|
}
|
||||||
|
|
||||||
got := storeSource.GetProxy("raw-proxy")
|
got := storeSource.GetProxy("raw-proxy")
|
||||||
if got == nil {
|
if got == nil {
|
||||||
|
|||||||
@@ -26,16 +26,19 @@ 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) error
|
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) error
|
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
DeleteStoreProxy(name string) error
|
DeleteStoreProxy(name string) error
|
||||||
|
|
||||||
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
||||||
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
||||||
CreateStoreVisitor(cfg v1.VisitorConfigurer) error
|
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) error
|
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
DeleteStoreVisitor(name string) error
|
DeleteStoreVisitor(name string) error
|
||||||
|
|
||||||
GracefulClose(d time.Duration)
|
GracefulClose(d time.Duration)
|
||||||
|
|||||||
@@ -119,6 +119,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
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -26,9 +25,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller handles HTTP API requests for frpc.
|
// Controller handles HTTP API requests for frpc.
|
||||||
@@ -67,15 +67,6 @@ func (c *Controller) toHTTPError(err error) error {
|
|||||||
return httppkg.NewError(code, err.Error())
|
return httppkg.NewError(code, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): Remove this lock wrapper after migrating typed config
|
|
||||||
// decoding to encoding/json/v2 with per-call options.
|
|
||||||
// TypedProxyConfig/TypedVisitorConfig currently read global strictness state.
|
|
||||||
func unmarshalTypedConfig[T any](body []byte, out *T) error {
|
|
||||||
return v1.WithDisallowUnknownFields(false, func() error {
|
|
||||||
return json.Unmarshal(body, out)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload handles GET /api/reload
|
// Reload handles GET /api/reload
|
||||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
strictConfigMode := false
|
strictConfigMode := false
|
||||||
@@ -98,7 +89,7 @@ func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
|||||||
|
|
||||||
// Status handles GET /api/status
|
// Status handles GET /api/status
|
||||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
res := make(StatusResp)
|
res := make(model.StatusResp)
|
||||||
ps := c.manager.GetProxyStatus()
|
ps := c.manager.GetProxyStatus()
|
||||||
if ps == nil {
|
if ps == nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -112,7 +103,7 @@ func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
|||||||
if len(arrs) <= 1 {
|
if len(arrs) <= 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int {
|
||||||
return cmp.Compare(a.Name, b.Name)
|
return cmp.Compare(a.Name, b.Name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -145,8 +136,8 @@ func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
||||||
psr := ProxyStatusResp{
|
psr := model.ProxyStatusResp{
|
||||||
Name: status.Name,
|
Name: status.Name,
|
||||||
Type: status.Type,
|
Type: status.Type,
|
||||||
Status: status.Phase,
|
Status: status.Phase,
|
||||||
@@ -166,29 +157,66 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.manager.IsStoreProxyEnabled(status.Name) {
|
if c.manager.IsStoreProxyEnabled(status.Name) {
|
||||||
psr.Source = SourceStore
|
psr.Source = model.SourceStore
|
||||||
}
|
}
|
||||||
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 {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := ProxyListResp{Proxies: make([]ProxyConfig, 0, len(proxies))}
|
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
||||||
for _, p := range proxies {
|
for _, p := range proxies {
|
||||||
cfg, err := configurerToMap(p)
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
resp.Proxies = append(resp.Proxies, ProxyConfig{
|
resp.Proxies = append(resp.Proxies, payload)
|
||||||
Name: p.GetBaseConfig().Name,
|
|
||||||
Type: p.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,16 +231,12 @@ func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := configurerToMap(p)
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProxyConfig{
|
return payload, nil
|
||||||
Name: p.GetBaseConfig().Name,
|
|
||||||
Type: p.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -221,19 +245,28 @@ func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedProxyConfig
|
var payload model.ProxyDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.ProxyConfigurer == nil {
|
if err := payload.Validate("", false); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.CreateStoreProxy(typed.ProxyConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreProxy(cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -247,19 +280,28 @@ func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedProxyConfig
|
var payload model.ProxyDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.ProxyConfigurer == nil {
|
if err := payload.Validate(name, true); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.UpdateStoreProxy(name, typed.ProxyConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -280,18 +322,17 @@ func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := VisitorListResp{Visitors: make([]VisitorConfig, 0, len(visitors))}
|
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
||||||
for _, v := range visitors {
|
for _, v := range visitors {
|
||||||
cfg, err := configurerToMap(v)
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
resp.Visitors = append(resp.Visitors, VisitorConfig{
|
resp.Visitors = append(resp.Visitors, payload)
|
||||||
Name: v.GetBaseConfig().Name,
|
|
||||||
Type: v.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,16 +347,12 @@ func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := configurerToMap(v)
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisitorConfig{
|
return payload, nil
|
||||||
Name: v.GetBaseConfig().Name,
|
|
||||||
Type: v.GetBaseConfig().Type,
|
|
||||||
Config: cfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -324,19 +361,28 @@ func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedVisitorConfig
|
var payload model.VisitorDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.VisitorConfigurer == nil {
|
if err := payload.Validate("", false); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.CreateStoreVisitor(typed.VisitorConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreVisitor(cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -350,19 +396,28 @@ func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var typed v1.TypedVisitorConfig
|
var payload model.VisitorDefinition
|
||||||
if err := unmarshalTypedConfig(body, &typed); err != nil {
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed.VisitorConfigurer == nil {
|
if err := payload.Validate(name, true); err != nil {
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
if err := c.manager.UpdateStoreVisitor(name, typed.VisitorConfigurer); err != nil {
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
||||||
|
if err != nil {
|
||||||
return nil, c.toHTTPError(err)
|
return nil, c.toHTTPError(err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
@@ -376,15 +431,3 @@ func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configurerToMap(v any) (map[string]any, error) {
|
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m map[string]any
|
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
662
client/http/controller_test.go
Normal file
662
client/http/controller_test.go
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeConfigManager struct {
|
||||||
|
reloadFromFileFn func(strict bool) error
|
||||||
|
readConfigFileFn func() (string, error)
|
||||||
|
writeConfigFileFn func(content []byte) error
|
||||||
|
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||||
|
isStoreProxyEnabledFn func(name string) bool
|
||||||
|
storeEnabledFn func() bool
|
||||||
|
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
|
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||||
|
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||||
|
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
deleteStoreProxyFn func(name string) error
|
||||||
|
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
||||||
|
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
||||||
|
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
deleteStoreVisitFn func(name string) error
|
||||||
|
gracefulCloseFn func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
||||||
|
if m.reloadFromFileFn != nil {
|
||||||
|
return m.reloadFromFileFn(strict)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
||||||
|
if m.readConfigFileFn != nil {
|
||||||
|
return m.readConfigFileFn()
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
||||||
|
if m.writeConfigFileFn != nil {
|
||||||
|
return m.writeConfigFileFn(content)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
if m.getProxyStatusFn != nil {
|
||||||
|
return m.getProxyStatusFn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
|
if m.isStoreProxyEnabledFn != nil {
|
||||||
|
return m.isStoreProxyEnabledFn(name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) StoreEnabled() bool {
|
||||||
|
if m.storeEnabledFn != nil {
|
||||||
|
return m.storeEnabledFn()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if m.getProxyConfigFn != nil {
|
||||||
|
return m.getProxyConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if m.getVisitorConfigFn != nil {
|
||||||
|
return m.getVisitorConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
if m.listStoreProxiesFn != nil {
|
||||||
|
return m.listStoreProxiesFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.getStoreProxyFn != nil {
|
||||||
|
return m.getStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.createStoreProxyFn != nil {
|
||||||
|
return m.createStoreProxyFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.updateStoreProxyFn != nil {
|
||||||
|
return m.updateStoreProxyFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
||||||
|
if m.deleteStoreProxyFn != nil {
|
||||||
|
return m.deleteStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
if m.listStoreVisitorsFn != nil {
|
||||||
|
return m.listStoreVisitorsFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.getStoreVisitorFn != nil {
|
||||||
|
return m.getStoreVisitorFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.createStoreVisitFn != nil {
|
||||||
|
return m.createStoreVisitFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.updateStoreVisitFn != nil {
|
||||||
|
return m.updateStoreVisitFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
|
if m.deleteStoreVisitFn != nil {
|
||||||
|
return m.deleteStoreVisitFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
||||||
|
if m.gracefulCloseFn != nil {
|
||||||
|
m.gracefulCloseFn(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
|
return &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 10080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
||||||
|
status := &proxy.WorkingStatus{
|
||||||
|
Name: "shared-proxy",
|
||||||
|
Type: "tcp",
|
||||||
|
Phase: proxy.ProxyPhaseRunning,
|
||||||
|
RemoteAddr: ":8080",
|
||||||
|
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
||||||
|
}
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
serverAddr: "127.0.0.1",
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
isStoreProxyEnabledFn: func(name string) bool {
|
||||||
|
return name == "shared-proxy"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := controller.buildProxyStatusResp(status)
|
||||||
|
if resp.Source != "store" {
|
||||||
|
t.Fatalf("unexpected source: %q", resp.Source)
|
||||||
|
}
|
||||||
|
if resp.RemoteAddr != "127.0.0.1:8080" {
|
||||||
|
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
||||||
|
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
||||||
|
_, err := controller.Reload(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreProxyErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
||||||
|
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
||||||
|
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return nil, tc.err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreVisitorErrorMapping(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
deleteStoreVisitFn: func(string) error {
|
||||||
|
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.DeleteStoreVisitor(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-proxy" {
|
||||||
|
t.Fatalf("unexpected proxy name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
||||||
|
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-visitor" {
|
||||||
|
t.Fatalf("unexpected visitor name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "xtcp" || payload.XTCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.TCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"plugin-visitor","type":"stcp",
|
||||||
|
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.STCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
||||||
|
b := newRawTCPProxyConfig("b")
|
||||||
|
a := newRawTCPProxyConfig("a")
|
||||||
|
return []v1.ProxyConfigurer{b, a}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
||||||
|
|
||||||
|
resp, err := controller.ListStoreProxies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list store proxies: %v", err)
|
||||||
|
}
|
||||||
|
out, ok := resp.(model.ProxyListResp)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if len(out.Proxies) != 2 {
|
||||||
|
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
||||||
|
}
|
||||||
|
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
||||||
|
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtError(sentinel error, msg string) error {
|
||||||
|
return fmt.Errorf("%w: %s", sentinel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertHTTPCode(t *testing.T, err error, expected int) {
|
||||||
|
t.Helper()
|
||||||
|
var httpErr *httppkg.Error
|
||||||
|
if !errors.As(err, &httpErr) {
|
||||||
|
t.Fatalf("unexpected error type: %T", err)
|
||||||
|
}
|
||||||
|
if httpErr.Code != expected {
|
||||||
|
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"name": "shared-proxy",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localPort": 10080,
|
||||||
|
"remotePort": 7000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("update store proxy: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if name == "ssh" {
|
||||||
|
cfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "ssh",
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "ssh"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get proxy config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if name == "my-stcp" {
|
||||||
|
cfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "my-stcp",
|
||||||
|
Type: "stcp",
|
||||||
|
ServerName: "server1",
|
||||||
|
BindPort: 9000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get visitor config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
148
client/http/model/proxy_definition.go
Normal file
148
client/http/model/proxy_definition.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
||||||
|
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
||||||
|
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
||||||
|
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
||||||
|
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
||||||
|
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("proxy name is required")
|
||||||
|
}
|
||||||
|
if !IsProxyType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("proxy name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := ProxyDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.TCPProxyConfig:
|
||||||
|
payload.TCP = c
|
||||||
|
case *v1.UDPProxyConfig:
|
||||||
|
payload.UDP = c
|
||||||
|
case *v1.HTTPProxyConfig:
|
||||||
|
payload.HTTP = c
|
||||||
|
case *v1.HTTPSProxyConfig:
|
||||||
|
payload.HTTPS = c
|
||||||
|
case *v1.TCPMuxProxyConfig:
|
||||||
|
payload.TCPMux = c
|
||||||
|
case *v1.STCPProxyConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPProxyConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPProxyConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.ProxyConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.TCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCP
|
||||||
|
blockType = "tcp"
|
||||||
|
}
|
||||||
|
if p.UDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.UDP
|
||||||
|
blockType = "udp"
|
||||||
|
}
|
||||||
|
if p.HTTP != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTP
|
||||||
|
blockType = "http"
|
||||||
|
}
|
||||||
|
if p.HTTPS != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTPS
|
||||||
|
blockType = "https"
|
||||||
|
}
|
||||||
|
if p.TCPMux != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCPMux
|
||||||
|
blockType = "tcpmux"
|
||||||
|
}
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProxyType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package model
|
||||||
|
|
||||||
const SourceStore = "store"
|
const SourceStore = "store"
|
||||||
|
|
||||||
@@ -31,26 +31,12 @@ type ProxyStatusResp struct {
|
|||||||
Source string `json:"source,omitempty"` // "store" or "config"
|
Source string `json:"source,omitempty"` // "store" or "config"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig wraps proxy configuration for API requests/responses.
|
|
||||||
type ProxyConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Config map[string]any `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitorConfig wraps visitor configuration for API requests/responses.
|
|
||||||
type VisitorConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Config map[string]any `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProxyListResp is the response for GET /api/store/proxies
|
// ProxyListResp is the response for GET /api/store/proxies
|
||||||
type ProxyListResp struct {
|
type ProxyListResp struct {
|
||||||
Proxies []ProxyConfig `json:"proxies"`
|
Proxies []ProxyDefinition `json:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VisitorListResp is the response for GET /api/store/visitors
|
// VisitorListResp is the response for GET /api/store/visitors
|
||||||
type VisitorListResp struct {
|
type VisitorListResp struct {
|
||||||
Visitors []VisitorConfig `json:"visitors"`
|
Visitors []VisitorDefinition `json:"visitors"`
|
||||||
}
|
}
|
||||||
107
client/http/model/visitor_definition.go
Normal file
107
client/http/model/visitor_definition.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VisitorDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("visitor name is required")
|
||||||
|
}
|
||||||
|
if !IsVisitorType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("visitor name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := VisitorDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.STCPVisitorConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPVisitorConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPVisitorConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.VisitorConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsVisitorType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -122,6 +123,33 @@ func (pxy *BaseProxy) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapWorkConn applies rate limiting, encryption, and compression
|
||||||
|
// to a work connection based on the proxy's transport configuration.
|
||||||
|
// The returned recycle function should be called when the stream is no longer in use
|
||||||
|
// to return compression resources to the pool. It is safe to not call recycle,
|
||||||
|
// in which case resources will be garbage collected normally.
|
||||||
|
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
||||||
|
var rwc io.ReadWriteCloser = conn
|
||||||
|
if pxy.limiter != nil {
|
||||||
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||||
|
return conn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if pxy.baseCfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, encKey)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var recycleFn func()
|
||||||
|
if pxy.baseCfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
pxy.inWorkConnCallback = cb
|
pxy.inWorkConnCallback = cb
|
||||||
}
|
}
|
||||||
@@ -139,30 +167,14 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
baseCfg := pxy.baseCfg
|
baseCfg := pxy.baseCfg
|
||||||
var (
|
|
||||||
remote io.ReadWriteCloser
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
remote = workConn
|
|
||||||
if pxy.limiter != nil {
|
|
||||||
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
|
|
||||||
return workConn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||||
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||||
if baseCfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, encKey)
|
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
workConn.Close()
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var compressionResourceRecycleFn func()
|
|
||||||
if baseCfg.Transport.UseCompression {
|
|
||||||
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we need to send proxy protocol info
|
// check if we need to send proxy protocol info
|
||||||
@@ -178,7 +190,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
// Use the common proxy protocol builder function
|
|
||||||
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||||
connInfo.ProxyProtocolHeader = header
|
connInfo.ProxyProtocolHeader = header
|
||||||
}
|
}
|
||||||
@@ -187,12 +198,18 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
|
|
||||||
if pxy.proxyPlugin != nil {
|
if pxy.proxyPlugin != nil {
|
||||||
// if plugin is set, let plugin handle connection first
|
// if plugin is set, let plugin handle connection first
|
||||||
|
// Don't recycle compression resources here because plugins may
|
||||||
|
// retain the connection after Handle returns.
|
||||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||||
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||||
xl.Debugf("handle by plugin finished")
|
xl.Debugf("handle by plugin finished")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recycleFn != nil {
|
||||||
|
defer recycleFn()
|
||||||
|
}
|
||||||
|
|
||||||
localConn, err := libnet.Dial(
|
localConn, err := libnet.Dial(
|
||||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||||
libnet.WithTimeout(10*time.Second),
|
libnet.WithTimeout(10*time.Second),
|
||||||
@@ -209,6 +226,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if connInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
|
localConn.Close()
|
||||||
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -219,7 +237,4 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
xl.Tracef("join connections errors: %v", errs)
|
xl.Tracef("join connections errors: %v", errs)
|
||||||
}
|
}
|
||||||
if compressionResourceRecycleFn != nil {
|
|
||||||
compressionResourceRecycleFn()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -25,17 +24,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
"github.com/fatedier/frp/pkg/util/limit"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SUDPProxy struct {
|
type SUDPProxy struct {
|
||||||
@@ -83,27 +80,13 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||||
|
|
||||||
var rwc io.ReadWriteCloser = conn
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
var err error
|
if err != nil {
|
||||||
if pxy.limiter != nil {
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
return
|
||||||
return conn.Close()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseCompression {
|
|
||||||
rwc = libio.WithCompression(rwc)
|
|
||||||
}
|
|
||||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
|
||||||
|
|
||||||
workConn := conn
|
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
readCh := make(chan *msg.UDPPacket, 1024)
|
readCh := make(chan *msg.UDPPacket, 1024)
|
||||||
sendCh := make(chan msg.Message, 1024)
|
sendCh := make(chan msg.Message, 1024)
|
||||||
isClose := false
|
isClose := false
|
||||||
|
|||||||
@@ -17,24 +17,21 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
"github.com/fatedier/frp/pkg/util/limit"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UDPProxy struct {
|
type UDPProxy struct {
|
||||||
@@ -94,28 +91,14 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
// close resources related with old workConn
|
// close resources related with old workConn
|
||||||
pxy.Close()
|
pxy.Close()
|
||||||
|
|
||||||
var rwc io.ReadWriteCloser = conn
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
var err error
|
if err != nil {
|
||||||
if pxy.limiter != nil {
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
return
|
||||||
return conn.Close()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pxy.cfg.Transport.UseCompression {
|
|
||||||
rwc = libio.WithCompression(rwc)
|
|
||||||
}
|
|
||||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
|
||||||
|
|
||||||
pxy.mu.Lock()
|
pxy.mu.Lock()
|
||||||
pxy.workConn = conn
|
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||||
pxy.sendCh = make(chan msg.Message, 1024)
|
pxy.sendCh = make(chan msg.Message, 1024)
|
||||||
pxy.closed = false
|
pxy.closed = false
|
||||||
@@ -129,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
xl.Tracef("get udp package from workConn: %s", udpMsg.Content)
|
xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content))
|
||||||
readCh <- &udpMsg
|
readCh <- &udpMsg
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
||||||
@@ -145,7 +128,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
for rawMsg := range sendCh {
|
for rawMsg := range sendCh {
|
||||||
switch m := rawMsg.(type) {
|
switch m := rawMsg.(type) {
|
||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
xl.Tracef("send udp package to workConn: %s", m.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(m.Content))
|
||||||
case *msg.Ping:
|
case *msg.Ping:
|
||||||
xl.Tracef("send ping message to udp workConn")
|
xl.Tracef("send ping message to udp workConn")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type XTCPProxy struct {
|
type XTCPProxy struct {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -162,15 +163,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 +183,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 +232,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 +261,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +504,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 +521,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"}
|
||||||
|
|||||||
@@ -15,18 +15,12 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
libio "github.com/fatedier/golib/io"
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
|
||||||
"github.com/fatedier/frp/pkg/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,10 +36,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.worker()
|
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.internalConnWorker()
|
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
||||||
|
|
||||||
if sv.plugin != nil {
|
if sv.plugin != nil {
|
||||||
sv.plugin.Start()
|
sv.plugin.Start()
|
||||||
@@ -57,35 +51,10 @@ func (sv *STCPVisitor) Close() {
|
|||||||
sv.BaseVisitor.Close()
|
sv.BaseVisitor.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *STCPVisitor) worker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("stcp local listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *STCPVisitor) internalConnWorker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.internalLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("stcp internal listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
var tunnelErr error
|
var tunnelErr error
|
||||||
defer func() {
|
defer func() {
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
if tunnelErr != nil {
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
@@ -96,62 +65,21 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
xl.Warnf("dialRawVisitorConn error: %v", err)
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
|
|
||||||
now := time.Now().Unix()
|
remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig())
|
||||||
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
|
||||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
|
||||||
RunID: sv.helper.RunID(),
|
|
||||||
ProxyName: targetProxyName,
|
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
|
||||||
Timestamp: now,
|
|
||||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
|
||||||
UseCompression: sv.cfg.Transport.UseCompression,
|
|
||||||
}
|
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("wrapVisitorConn error: %v", err)
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer recycleFn()
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
|
||||||
tunnelErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
|
||||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote io.ReadWriteCloser
|
|
||||||
remote = visitorConn
|
|
||||||
if sv.cfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
tunnelErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
var recycleFn func()
|
|
||||||
remote, recycleFn = libio.WithCompressionFromPool(remote)
|
|
||||||
defer recycleFn()
|
|
||||||
}
|
|
||||||
|
|
||||||
libio.Join(userConn, remote)
|
libio.Join(userConn, remote)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,21 +16,17 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,6 +72,7 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
visitorConn net.Conn
|
visitorConn net.Conn
|
||||||
|
recycleFn func()
|
||||||
err error
|
err error
|
||||||
|
|
||||||
firstPacket *msg.UDPPacket
|
firstPacket *msg.UDPPacket
|
||||||
@@ -93,14 +90,17 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
visitorConn, err = sv.getNewVisitorConn()
|
visitorConn, recycleFn, err = sv.getNewVisitorConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitorConn always be closed when worker done.
|
// visitorConn always be closed when worker done.
|
||||||
sv.worker(visitorConn, firstPacket)
|
func() {
|
||||||
|
defer recycleFn()
|
||||||
|
sv.worker(visitorConn, firstPacket)
|
||||||
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sv.checkCloseCh:
|
case <-sv.checkCloseCh:
|
||||||
@@ -147,7 +147,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
sv.readCh <- m
|
sv.readCh <- m
|
||||||
xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content)
|
xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content))
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed")
|
xl.Infof("reader goroutine for udp work connection closed")
|
||||||
return
|
return
|
||||||
@@ -169,7 +169,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn: %s", firstPacket.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content))
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -184,7 +184,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn: %s", udpMsg.Content)
|
xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content))
|
||||||
case <-closeCh:
|
case <-closeCh:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -198,53 +198,17 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Infof("sudp worker is closed")
|
xl.Infof("sudp worker is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("frpc connect frps error: %v", err)
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig())
|
||||||
now := time.Now().Unix()
|
|
||||||
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
|
||||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
|
||||||
RunID: sv.helper.RunID(),
|
|
||||||
ProxyName: targetProxyName,
|
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
|
||||||
Timestamp: now,
|
|
||||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
|
||||||
UseCompression: sv.cfg.Transport.UseCompression,
|
|
||||||
}
|
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
|
rawConn.Close()
|
||||||
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
|
|
||||||
}
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
|
||||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote io.ReadWriteCloser
|
|
||||||
remote = visitorConn
|
|
||||||
if sv.cfg.Transport.UseEncryption {
|
|
||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
remote = libio.WithCompression(remote)
|
|
||||||
}
|
|
||||||
return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) Close() {
|
func (sv *SUDPVisitor) Close() {
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
@@ -119,6 +127,18 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
|||||||
return v.internalLn.PutConn(conn)
|
return v.internalLn.PutConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
||||||
|
xl := xlog.FromContextSafe(v.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("%s listener closed", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *BaseVisitor) Close() {
|
func (v *BaseVisitor) Close() {
|
||||||
if v.l != nil {
|
if v.l != nil {
|
||||||
v.l.Close()
|
v.l.Close()
|
||||||
@@ -130,3 +150,57 @@ func (v *BaseVisitor) Close() {
|
|||||||
v.plugin.Close()
|
v.plugin.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
||||||
|
visitorConn, err := v.helper.ConnectServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
||||||
|
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||||
|
RunID: v.helper.RunID(),
|
||||||
|
ProxyName: targetProxyName,
|
||||||
|
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
UseEncryption: cfg.Transport.UseEncryption,
|
||||||
|
UseCompression: cfg.Transport.UseCompression,
|
||||||
|
}
|
||||||
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||||
|
}
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
}
|
||||||
|
return visitorConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
||||||
|
rwc := conn
|
||||||
|
if cfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recycleFn := func() {}
|
||||||
|
if cfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -191,6 +191,13 @@ 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() (net.Conn, error)
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.worker()
|
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.internalConnWorker()
|
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
||||||
go sv.processTunnelStartEvents()
|
go sv.processTunnelStartEvents()
|
||||||
if sv.cfg.KeepTunnelOpen {
|
if sv.cfg.KeepTunnelOpen {
|
||||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||||
@@ -93,30 +93,6 @@ func (sv *XTCPVisitor) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *XTCPVisitor) worker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("xtcp local listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *XTCPVisitor) internalConnWorker() {
|
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := sv.internalLn.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("xtcp internal listener closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go sv.handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -206,20 +182,14 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
|
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
||||||
if sv.cfg.Transport.UseEncryption {
|
if err != nil {
|
||||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
xl.Errorf("%v", err)
|
||||||
if err != nil {
|
tunnelConn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
tunnelErr = err
|
||||||
tunnelErr = err
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sv.cfg.Transport.UseCompression {
|
|
||||||
var recycleFn func()
|
|
||||||
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
|
||||||
defer recycleFn()
|
|
||||||
}
|
}
|
||||||
|
defer recycleFn()
|
||||||
|
|
||||||
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||||
xl.Debugf("join connections closed")
|
xl.Debugf("join connections closed")
|
||||||
@@ -373,6 +343,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
|||||||
}
|
}
|
||||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
lConn.Close()
|
||||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
Use: "discover",
|
Use: "discover",
|
||||||
Short: "Discover nathole information from stun server",
|
Short: "Discover nathole information from stun server",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// ignore error here, because we can use command line pameters
|
// ignore error here, because we can use command line parameters
|
||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg = &v1.ClientCommonConfig{}
|
cfg = &v1.ClientCommonConfig{}
|
||||||
|
|||||||
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`)
|
||||||
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=
|
||||||
|
|||||||
111
pkg/auth/oidc.go
111
pkg/auth/oidc.go
@@ -23,12 +23,14 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/clientcredentials"
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,14 +76,64 @@ func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyUR
|
|||||||
return &http.Client{Transport: transport}, nil
|
return &http.Client{Transport: transport}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nonCachingTokenSource wraps a clientcredentials.Config to fetch a fresh
|
||||||
|
// token on every call. This is used as a fallback when the OIDC provider
|
||||||
|
// does not return expires_in, which would cause a caching TokenSource to
|
||||||
|
// hold onto a stale token forever.
|
||||||
|
type nonCachingTokenSource struct {
|
||||||
|
cfg *clientcredentials.Config
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
return s.cfg.Token(s.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oidcTokenSource wraps a caching oauth2.TokenSource and, on the first
|
||||||
|
// successful Token() call, checks whether the provider returns an expiry.
|
||||||
|
// If not, it permanently switches to nonCachingTokenSource so that a fresh
|
||||||
|
// token is fetched every time. This avoids an eager network call at
|
||||||
|
// construction time, letting the login retry loop handle transient IdP
|
||||||
|
// outages.
|
||||||
|
type oidcTokenSource struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
initialized bool
|
||||||
|
source oauth2.TokenSource
|
||||||
|
fallbackCfg *clientcredentials.Config
|
||||||
|
fallbackCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *oidcTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if !s.initialized {
|
||||||
|
token, err := s.source.Token()
|
||||||
|
if err != nil {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token.Expiry.IsZero() {
|
||||||
|
s.source = &nonCachingTokenSource{cfg: s.fallbackCfg, ctx: s.fallbackCtx}
|
||||||
|
}
|
||||||
|
s.initialized = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
source := s.source
|
||||||
|
s.mu.Unlock()
|
||||||
|
return source.Token()
|
||||||
|
}
|
||||||
|
|
||||||
type OidcAuthProvider struct {
|
type OidcAuthProvider struct {
|
||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
tokenGenerator *clientcredentials.Config
|
tokenSource oauth2.TokenSource
|
||||||
httpClient *http.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||||
|
if err := validation.ValidateOIDCClientCredentialsConfig(&cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
eps := make(map[string][]string)
|
eps := make(map[string][]string)
|
||||||
for k, v := range cfg.AdditionalEndpointParams {
|
for k, v := range cfg.AdditionalEndpointParams {
|
||||||
eps[k] = []string{v}
|
eps[k] = []string{v}
|
||||||
@@ -99,30 +151,42 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
|||||||
EndpointParams: eps,
|
EndpointParams: eps,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create custom HTTP client if needed
|
// Build the context that TokenSource will use for all future HTTP requests.
|
||||||
var httpClient *http.Client
|
// context.Background() is appropriate here because the token source is
|
||||||
|
// long-lived and outlives any single request.
|
||||||
|
ctx := context.Background()
|
||||||
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
||||||
var err error
|
httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
||||||
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
||||||
}
|
}
|
||||||
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a persistent TokenSource that caches the token and refreshes
|
||||||
|
// it before expiry. This avoids making a new HTTP request to the OIDC
|
||||||
|
// provider on every heartbeat/ping.
|
||||||
|
//
|
||||||
|
// We wrap it in an oidcTokenSource so that the first Token() call
|
||||||
|
// (deferred to SetLogin inside the login retry loop) probes whether the
|
||||||
|
// provider returns expires_in. If not, it switches to a non-caching
|
||||||
|
// source. This avoids an eager network call at construction time, which
|
||||||
|
// would prevent loopLoginUntilSuccess from retrying on transient IdP
|
||||||
|
// outages.
|
||||||
|
cachingSource := tokenGenerator.TokenSource(ctx)
|
||||||
|
|
||||||
return &OidcAuthProvider{
|
return &OidcAuthProvider{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
tokenGenerator: tokenGenerator,
|
tokenSource: &oidcTokenSource{
|
||||||
httpClient: httpClient,
|
source: cachingSource,
|
||||||
|
fallbackCfg: tokenGenerator,
|
||||||
|
fallbackCtx: ctx,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||||
ctx := context.Background()
|
tokenObj, err := auth.tokenSource.Token()
|
||||||
if auth.httpClient != nil {
|
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenObj, err := auth.tokenGenerator.Token(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||||
}
|
}
|
||||||
@@ -205,7 +269,8 @@ type OidcAuthConsumer struct {
|
|||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
verifier TokenVerifier
|
verifier TokenVerifier
|
||||||
subjectsFromLogin []string
|
mu sync.RWMutex
|
||||||
|
subjectsFromLogin map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||||
@@ -226,7 +291,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri
|
|||||||
return &OidcAuthConsumer{
|
return &OidcAuthConsumer{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
subjectsFromLogin: []string{},
|
subjectsFromLogin: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +300,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||||
}
|
}
|
||||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
auth.mu.Lock()
|
||||||
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
auth.subjectsFromLogin[token.Subject] = struct{}{}
|
||||||
}
|
auth.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +311,13 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||||
}
|
}
|
||||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
auth.mu.RLock()
|
||||||
|
_, ok := auth.subjectsFromLogin[token.Subject]
|
||||||
|
auth.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||||
"original subjects: %s, "+
|
|
||||||
"new subject: %s",
|
"new subject: %s",
|
||||||
auth.subjectsFromLogin, token.Subject)
|
token.Subject)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package auth_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -62,3 +66,188 @@ func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
|||||||
r.Error(err)
|
r.Error(err)
|
||||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
requestCount.Add(1)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "fresh-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// Constructor no longer fetches a token eagerly.
|
||||||
|
// The first SetLogin triggers the adaptive probe.
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("fresh-test-token", loginMsg.PrivilegeKey)
|
||||||
|
|
||||||
|
for range 3 {
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("fresh-test-token", pingMsg.PrivilegeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch)
|
||||||
|
r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
requestCount.Add(1)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "cached-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// Constructor no longer fetches eagerly; first SetLogin triggers the probe.
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
// SetLogin triggers the adaptive probe and caches the token.
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("cached-test-token", loginMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(1), requestCount.Load())
|
||||||
|
|
||||||
|
// Subsequent calls should also reuse the cached token
|
||||||
|
for range 5 {
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("cached-test-token", pingMsg.PrivilegeKey)
|
||||||
|
}
|
||||||
|
r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
var requestCount atomic.Int32
|
||||||
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
n := requestCount.Add(1)
|
||||||
|
// The oauth2 library retries once internally, so we need two
|
||||||
|
// consecutive failures to surface an error to the caller.
|
||||||
|
if n <= 2 {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"error": "temporarily_unavailable",
|
||||||
|
"error_description": "service is starting up",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||||
|
"access_token": "retry-test-token",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
// Constructor succeeds even though the IdP is "down".
|
||||||
|
provider, err := auth.NewOidcAuthSetter(
|
||||||
|
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||||
|
v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal(int32(0), requestCount.Load())
|
||||||
|
|
||||||
|
// First SetLogin hits the IdP, which returns an error (after internal retry).
|
||||||
|
loginMsg := &msg.Login{}
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.Error(err)
|
||||||
|
r.Equal(int32(2), requestCount.Load())
|
||||||
|
|
||||||
|
// Second SetLogin retries and succeeds.
|
||||||
|
err = provider.SetLogin(loginMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("retry-test-token", loginMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(3), requestCount.Load())
|
||||||
|
|
||||||
|
// Subsequent calls use cached token.
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
err = provider.SetPing(pingMsg)
|
||||||
|
r.NoError(err)
|
||||||
|
r.Equal("retry-test-token", pingMsg.PrivilegeKey)
|
||||||
|
r.Equal(int32(3), requestCount.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
tokenServer := httptest.NewServer(http.NotFoundHandler())
|
||||||
|
defer tokenServer.Close()
|
||||||
|
|
||||||
|
_, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: "://bad",
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.tokenEndpointURL")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.clientID is required")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
AdditionalEndpointParams: map[string]string{
|
||||||
|
"scope": "profile",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||||
|
|
||||||
|
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||||
|
ClientID: "test-client",
|
||||||
|
TokenEndpointURL: tokenServer.URL,
|
||||||
|
Audience: "api",
|
||||||
|
AdditionalEndpointParams: map[string]string{"audience": "override"},
|
||||||
|
})
|
||||||
|
r.Error(err)
|
||||||
|
r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,15 +171,14 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
|||||||
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
||||||
out := v1.HeaderOperations{}
|
out := v1.HeaderOperations{}
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
if !strings.HasPrefix(k, "plugin_header_") {
|
k, ok := strings.CutPrefix(k, "plugin_header_")
|
||||||
|
if !ok || k == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
|
if out.Set == nil {
|
||||||
if out.Set == nil {
|
out.Set = make(map[string]string)
|
||||||
out.Set = make(map[string]string)
|
|
||||||
}
|
|
||||||
out.Set[k] = v
|
|
||||||
}
|
}
|
||||||
|
out.Set[k] = v
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ const (
|
|||||||
// Proxy
|
// Proxy
|
||||||
var (
|
var (
|
||||||
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
|
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](),
|
||||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
|
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](),
|
||||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
|
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](),
|
||||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
|
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](),
|
||||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
|
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](),
|
||||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
|
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](),
|
||||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
|
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](),
|
||||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
|
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string
|
|||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
|
|
||||||
for key, value := range set {
|
for key, value := range set {
|
||||||
if strings.HasPrefix(key, prefix) {
|
if trimmed, ok := strings.CutPrefix(key, prefix); ok {
|
||||||
m[strings.TrimPrefix(key, prefix)] = value
|
m[trimmed] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const (
|
|||||||
// Visitor
|
// Visitor
|
||||||
var (
|
var (
|
||||||
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
|
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](),
|
||||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
|
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](),
|
||||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
|
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -33,6 +34,7 @@ import (
|
|||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +110,21 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return LoadConfigure(content, c, strict)
|
return LoadConfigure(content, c, strict, detectFormatFromPath(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectFormatFromPath returns a format hint based on the file extension.
|
||||||
|
func detectFormatFromPath(path string) string {
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".toml":
|
||||||
|
return "toml"
|
||||||
|
case ".yaml", ".yml":
|
||||||
|
return "yaml"
|
||||||
|
case ".json":
|
||||||
|
return "json"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
||||||
@@ -129,45 +145,134 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON and decode with strict validation
|
// Convert to JSON and decode with strict validation
|
||||||
jsonBytes, err := json.Marshal(temp)
|
jsonBytes, err := jsonx.Marshal(temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
return decodeJSONContent(jsonBytes, target, true)
|
||||||
decoder.DisallowUnknownFields()
|
}
|
||||||
return decoder.Decode(target)
|
|
||||||
|
func decodeJSONContent(content []byte, target any, strict bool) error {
|
||||||
|
if clientCfg, ok := target.(*v1.ClientConfig); ok {
|
||||||
|
decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{
|
||||||
|
DisallowUnknownFields: strict,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*clientCfg = decoded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: strict,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
||||||
// Now it supports json, yaml and toml format.
|
// Now it supports json, yaml and toml format.
|
||||||
func LoadConfigure(b []byte, c any, strict bool) error {
|
// An optional format hint (e.g. "toml", "yaml", "json") can be provided
|
||||||
return v1.WithDisallowUnknownFields(strict, func() error {
|
// to enable better error messages with line number information.
|
||||||
var tomlObj any
|
func LoadConfigure(b []byte, c any, strict bool, formats ...string) error {
|
||||||
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
format := ""
|
||||||
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
if len(formats) > 0 {
|
||||||
var err error
|
format = formats[0]
|
||||||
b, err = json.Marshal(&tomlObj)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
|
||||||
if yaml.IsJSONBuffer(b) {
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if strict {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
return decoder.Decode(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle YAML content
|
originalBytes := b
|
||||||
if strict {
|
parsedFromTOML := false
|
||||||
// In strict mode, always use our custom handler to support YAML merge
|
|
||||||
return parseYAMLWithDotFieldsHandling(b, c)
|
var tomlObj any
|
||||||
|
tomlErr := toml.Unmarshal(b, &tomlObj)
|
||||||
|
if tomlErr == nil {
|
||||||
|
parsedFromTOML = true
|
||||||
|
var err error
|
||||||
|
b, err = jsonx.Marshal(&tomlObj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
// Non-strict mode, parse normally
|
} else if format == "toml" {
|
||||||
return yaml.Unmarshal(b, c)
|
// File is known to be TOML but has syntax errors.
|
||||||
})
|
return formatTOMLError(tomlErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
||||||
|
if yaml.IsJSONBuffer(b) {
|
||||||
|
if err := decodeJSONContent(b, c, strict); err != nil {
|
||||||
|
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle YAML content
|
||||||
|
if strict {
|
||||||
|
// In strict mode, always use our custom handler to support YAML merge
|
||||||
|
if err := parseYAMLWithDotFieldsHandling(b, c); err != nil {
|
||||||
|
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Non-strict mode, parse normally
|
||||||
|
return yaml.Unmarshal(b, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTOMLError extracts line/column information from TOML decode errors.
|
||||||
|
func formatTOMLError(err error) error {
|
||||||
|
var decErr *toml.DecodeError
|
||||||
|
if errors.As(err, &decErr) {
|
||||||
|
row, col := decErr.Position()
|
||||||
|
return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error())
|
||||||
|
}
|
||||||
|
var strictErr *toml.StrictMissingError
|
||||||
|
if errors.As(err, &strictErr) {
|
||||||
|
return strictErr
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors.
|
||||||
|
func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error {
|
||||||
|
var typeErr *json.UnmarshalTypeError
|
||||||
|
if errors.As(err, &typeErr) && typeErr.Field != "" {
|
||||||
|
if includeLine {
|
||||||
|
line := findFieldLineInContent(originalContent, typeErr.Field)
|
||||||
|
if line > 0 {
|
||||||
|
return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// findFieldLineInContent searches the original config content for a field name
|
||||||
|
// and returns the 1-indexed line number where it appears, or 0 if not found.
|
||||||
|
func findFieldLineInContent(content []byte, fieldPath string) int {
|
||||||
|
if fieldPath == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the last component of the field path (e.g. "proxies" from "proxies" or
|
||||||
|
// "protocol" from "transport.protocol").
|
||||||
|
parts := strings.Split(fieldPath, ".")
|
||||||
|
searchKey := parts[len(parts)-1]
|
||||||
|
|
||||||
|
lines := bytes.Split(content, []byte("\n"))
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := bytes.TrimSpace(line)
|
||||||
|
// Match TOML key assignments like: key = ...
|
||||||
|
if bytes.HasPrefix(trimmed, []byte(searchKey)) {
|
||||||
|
rest := bytes.TrimSpace(trimmed[len(searchKey):])
|
||||||
|
if len(rest) > 0 && rest[0] == '=' {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match TOML table array headers like: [[proxies]]
|
||||||
|
if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||||
|
|||||||
@@ -189,6 +189,31 @@ unixPath = "/tmp/uds.sock"
|
|||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = 6000
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2https"
|
||||||
|
localAddr = "127.0.0.1:8080"
|
||||||
|
unknownInPlugin = "value"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
err = LoadConfigure([]byte(content), &clientCfg, true)
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
||||||
// even in strict mode by properly handling dot-prefixed fields
|
// even in strict mode by properly handling dot-prefixed fields
|
||||||
func TestYAMLMergeInStrictMode(t *testing.T) {
|
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||||
@@ -470,3 +495,111 @@ serverPort: 7000
|
|||||||
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
require.Equal(7000, clientCfg.ServerPort)
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTOMLSyntaxErrorWithPosition(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// TOML with syntax error (unclosed table array header)
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]
|
||||||
|
name = "test"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.Error(err)
|
||||||
|
require.Contains(err.Error(), "toml")
|
||||||
|
require.Contains(err.Error(), "line")
|
||||||
|
require.Contains(err.Error(), "column")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// TOML with wrong type: proxies should be a table array, not a string
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
proxies = "this should be a table array"
|
||||||
|
`
|
||||||
|
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.Error(err)
|
||||||
|
// The error should contain field info
|
||||||
|
errMsg := err.Error()
|
||||||
|
require.Contains(errMsg, "proxies")
|
||||||
|
require.NotContains(errMsg, "line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindFieldLineInContent(t *testing.T) {
|
||||||
|
content := []byte(`serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6000
|
||||||
|
`)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
fieldPath string
|
||||||
|
wantLine int
|
||||||
|
}{
|
||||||
|
{"serverAddr", 1},
|
||||||
|
{"serverPort", 2},
|
||||||
|
{"name", 5},
|
||||||
|
{"type", 6},
|
||||||
|
{"remotePort", 7},
|
||||||
|
{"nonexistent", 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.fieldPath, func(t *testing.T) {
|
||||||
|
got := findFieldLineInContent(content, tt.fieldPath)
|
||||||
|
require.Equal(t, tt.wantLine, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatDetection(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
format string
|
||||||
|
}{
|
||||||
|
{"config.toml", "toml"},
|
||||||
|
{"config.TOML", "toml"},
|
||||||
|
{"config.yaml", "yaml"},
|
||||||
|
{"config.yml", "yaml"},
|
||||||
|
{"config.json", "json"},
|
||||||
|
{"config.ini", ""},
|
||||||
|
{"config", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
require.Equal(t, tt.format, detectFormatFromPath(tt.path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidTOMLStillWorks(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// Valid TOML with format hint should work fine
|
||||||
|
content := `serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6000
|
||||||
|
`
|
||||||
|
clientCfg := v1.ClientConfig{}
|
||||||
|
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
|
require.Len(clientCfg.Proxies, 1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"maps"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -97,21 +99,11 @@ func (a *Aggregator) mapsToSortedSlices(
|
|||||||
proxyMap map[string]v1.ProxyConfigurer,
|
proxyMap map[string]v1.ProxyConfigurer,
|
||||||
visitorMap map[string]v1.VisitorConfigurer,
|
visitorMap map[string]v1.VisitorConfigurer,
|
||||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||||
proxies := make([]v1.ProxyConfigurer, 0, len(proxyMap))
|
proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int {
|
||||||
for _, p := range proxyMap {
|
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||||
proxies = append(proxies, p)
|
|
||||||
}
|
|
||||||
sort.Slice(proxies, func(i, j int) bool {
|
|
||||||
return proxies[i].GetBaseConfig().Name < proxies[j].GetBaseConfig().Name
|
|
||||||
})
|
})
|
||||||
|
visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int {
|
||||||
visitors := make([]v1.VisitorConfigurer, 0, len(visitorMap))
|
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||||
for _, v := range visitorMap {
|
|
||||||
visitors = append(visitors, v)
|
|
||||||
}
|
|
||||||
sort.Slice(visitors, func(i, j int) bool {
|
|
||||||
return visitors[i].GetBaseConfig().Name < visitors[j].GetBaseConfig().Name
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return proxies, visitors
|
return proxies, visitors
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,27 @@ func TestAggregator_VisitorMerge(t *testing.T) {
|
|||||||
require.Len(visitors, 2)
|
require.Len(visitors, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAggregator_Load_ReturnsSortedByName(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
agg := newTestAggregator(t, nil)
|
||||||
|
err := agg.ConfigSource().ReplaceAll(
|
||||||
|
[]v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")},
|
||||||
|
[]v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")},
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
proxies, visitors, err := agg.Load()
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(proxies, 3)
|
||||||
|
require.Equal("alice", proxies[0].GetBaseConfig().Name)
|
||||||
|
require.Equal("bob", proxies[1].GetBaseConfig().Name)
|
||||||
|
require.Equal("charlie", proxies[2].GetBaseConfig().Name)
|
||||||
|
require.Len(visitors, 2)
|
||||||
|
require.Equal("alpha", visitors[0].GetBaseConfig().Name)
|
||||||
|
require.Equal("zulu", visitors[1].GetBaseConfig().Name)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StoreSourceConfig struct {
|
type StoreSourceConfig struct {
|
||||||
@@ -74,36 +74,44 @@ func (s *StoreSource) loadFromFileUnlocked() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var stored storeData
|
type rawStoreData struct {
|
||||||
if err := v1.WithDisallowUnknownFields(false, func() error {
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
return json.Unmarshal(data, &stored)
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
}); err != nil {
|
}
|
||||||
|
stored := rawStoreData{}
|
||||||
|
if err := jsonx.Unmarshal(data, &stored); err != nil {
|
||||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||||
|
|
||||||
for _, tp := range stored.Proxies {
|
for i, proxyData := range stored.Proxies {
|
||||||
if tp.ProxyConfigurer != nil {
|
proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{
|
||||||
proxyCfg := tp.ProxyConfigurer
|
DisallowUnknownFields: false,
|
||||||
name := proxyCfg.GetBaseConfig().Name
|
})
|
||||||
if name == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
return fmt.Errorf("failed to decode proxy at index %d: %w", i, err)
|
||||||
}
|
|
||||||
s.proxies[name] = proxyCfg
|
|
||||||
}
|
}
|
||||||
|
name := proxyCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("proxy name cannot be empty")
|
||||||
|
}
|
||||||
|
s.proxies[name] = proxyCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tv := range stored.Visitors {
|
for i, visitorData := range stored.Visitors {
|
||||||
if tv.VisitorConfigurer != nil {
|
visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{
|
||||||
visitorCfg := tv.VisitorConfigurer
|
DisallowUnknownFields: false,
|
||||||
name := visitorCfg.GetBaseConfig().Name
|
})
|
||||||
if name == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
return fmt.Errorf("failed to decode visitor at index %d: %w", i, err)
|
||||||
}
|
|
||||||
s.visitors[name] = visitorCfg
|
|
||||||
}
|
}
|
||||||
|
name := visitorCfg.GetBaseConfig().Name
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("visitor name cannot be empty")
|
||||||
|
}
|
||||||
|
s.visitors[name] = visitorCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -122,7 +130,7 @@ func (s *StoreSource) saveToFileUnlocked() error {
|
|||||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(stored, "", " ")
|
data, err := jsonx.MarshalIndent(stored, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -23,27 +22,9 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setDisallowUnknownFieldsForStoreTest(t *testing.T, value bool) func() {
|
|
||||||
t.Helper()
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := v1.DisallowUnknownFields
|
|
||||||
v1.DisallowUnknownFields = value
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return func() {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
v1.DisallowUnknownFields = prev
|
|
||||||
v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDisallowUnknownFieldsForStoreTest() bool {
|
|
||||||
v1.DisallowUnknownFieldsMu.Lock()
|
|
||||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
|
||||||
return v1.DisallowUnknownFields
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
@@ -99,7 +80,7 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|||||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(stored)
|
data, err := jsonx.Marshal(stored)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
err = os.WriteFile(path, data, 0o600)
|
err = os.WriteFile(path, data, 0o600)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
@@ -117,12 +98,9 @@ func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|||||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
restore := setDisallowUnknownFieldsForStoreTest(t, true)
|
|
||||||
t.Cleanup(restore)
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
path := filepath.Join(t.TempDir(), "store.json")
|
||||||
raw := []byte(`{
|
raw := []byte(`{
|
||||||
"proxies": [
|
"proxies": [
|
||||||
@@ -140,5 +118,4 @@ func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t
|
|||||||
|
|
||||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||||
require.True(getDisallowUnknownFieldsForStoreTest())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, e
|
|||||||
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
||||||
}
|
}
|
||||||
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
||||||
for i := 0; i < len(firstRangeNumbers); i++ {
|
for i := range firstRangeNumbers {
|
||||||
pairs = append(pairs, NumberPair{
|
pairs = append(pairs, NumberPair{
|
||||||
First: firstRangeNumbers[i],
|
First: firstRangeNumbers[i],
|
||||||
Second: secondRangeNumbers[i],
|
Second: secondRangeNumbers[i],
|
||||||
|
|||||||
@@ -70,24 +70,18 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error {
|
|||||||
f float64
|
f float64
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
switch {
|
if fstr, ok := strings.CutSuffix(s, "MB"); ok {
|
||||||
case strings.HasSuffix(s, "MB"):
|
|
||||||
base = MB
|
base = MB
|
||||||
fstr := strings.TrimSuffix(s, "MB")
|
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
if err != nil {
|
} else if fstr, ok := strings.CutSuffix(s, "KB"); ok {
|
||||||
return err
|
|
||||||
}
|
|
||||||
case strings.HasSuffix(s, "KB"):
|
|
||||||
base = KB
|
base = KB
|
||||||
fstr := strings.TrimSuffix(s, "KB")
|
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
if err != nil {
|
} else {
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("unit not support")
|
return errors.New("unit not support")
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
q.s = s
|
q.s = s
|
||||||
q.i = int64(f * float64(base))
|
q.i = int64(f * float64(base))
|
||||||
@@ -143,8 +137,8 @@ func (p PortsRangeSlice) String() string {
|
|||||||
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
out := []PortsRange{}
|
out := []PortsRange{}
|
||||||
numRanges := strings.Split(str, ",")
|
numRanges := strings.SplitSeq(str, ",")
|
||||||
for _, numRangeStr := range numRanges {
|
for numRangeStr := range numRanges {
|
||||||
// 1000-2000 or 2001
|
// 1000-2000 or 2001
|
||||||
numArray := strings.Split(numRangeStr, "-")
|
numArray := strings.Split(numRangeStr, "-")
|
||||||
// length: only 1 or 2 is correct
|
// length: only 1 or 2 is correct
|
||||||
|
|||||||
@@ -39,6 +39,31 @@ func TestBandwidthQuantity(t *testing.T) {
|
|||||||
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_MB(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w)
|
||||||
|
require.NoError(err)
|
||||||
|
require.EqualValues(2*MB, w.B.Bytes())
|
||||||
|
|
||||||
|
buf, err := json.Marshal(&w)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(`{"b":"2MB","int":1}`, string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_InvalidUnit(t *testing.T) {
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBandwidthQuantity_InvalidNumber(t *testing.T) {
|
||||||
|
var w Wrap
|
||||||
|
err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPortsRangeSlice2String(t *testing.T) {
|
func TestPortsRangeSlice2String(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -16,35 +16,10 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(fatedier): Migrate typed config decoding to encoding/json/v2 when it is stable for production use.
|
|
||||||
// The current encoding/json(v1) path cannot propagate DisallowUnknownFields into custom UnmarshalJSON
|
|
||||||
// methods, so we temporarily keep this global strictness flag protected by a mutex.
|
|
||||||
//
|
|
||||||
// https://github.com/golang/go/issues/41144
|
|
||||||
// https://github.com/golang/go/discussions/63397
|
|
||||||
var (
|
|
||||||
DisallowUnknownFields = false
|
|
||||||
DisallowUnknownFieldsMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithDisallowUnknownFields temporarily overrides typed config JSON strictness.
|
|
||||||
// It restores the previous value before returning.
|
|
||||||
func WithDisallowUnknownFields(disallow bool, fn func() error) error {
|
|
||||||
DisallowUnknownFieldsMu.Lock()
|
|
||||||
prev := DisallowUnknownFields
|
|
||||||
DisallowUnknownFields = disallow
|
|
||||||
defer func() {
|
|
||||||
DisallowUnknownFields = prev
|
|
||||||
DisallowUnknownFieldsMu.Unlock()
|
|
||||||
}()
|
|
||||||
return fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthScope string
|
type AuthScope string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
195
pkg/config/v1/decode.go
Normal file
195
pkg/config/v1/decode.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DecodeOptions struct {
|
||||||
|
DisallowUnknownFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error {
|
||||||
|
return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{
|
||||||
|
RejectUnknownMembers: options.DisallowUnknownFields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONNull(b []byte) bool {
|
||||||
|
return len(b) == 0 || string(b) == "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
type typedEnvelope struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Plugin jsonx.RawMessage `json:"plugin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewProxyConfigurerByType(ProxyType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown proxy type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return nil, errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurer := NewVisitorConfigurerByType(VisitorType(env.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return nil, fmt.Errorf("unknown visitor type: %s", env.Type)
|
||||||
|
}
|
||||||
|
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
||||||
|
plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err)
|
||||||
|
}
|
||||||
|
configurer.GetBaseConfig().Plugin = plugin
|
||||||
|
}
|
||||||
|
return configurer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedClientPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedClientPluginOptions{}, errors.New("plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := clientPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedClientPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
ClientPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
|
||||||
|
if isJSONNull(b) {
|
||||||
|
return TypedVisitorPluginOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var env typedEnvelope
|
||||||
|
if err := jsonx.Unmarshal(b, &env); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, err
|
||||||
|
}
|
||||||
|
if env.Type == "" {
|
||||||
|
return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := visitorPluginOptionsTypeMap[env.Type]
|
||||||
|
if !ok {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type)
|
||||||
|
}
|
||||||
|
optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)
|
||||||
|
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
||||||
|
return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
return TypedVisitorPluginOptions{
|
||||||
|
Type: env.Type,
|
||||||
|
VisitorPluginOptions: optionsStruct,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
|
||||||
|
type rawClientConfig struct {
|
||||||
|
ClientCommonConfig
|
||||||
|
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
||||||
|
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := rawClientConfig{}
|
||||||
|
if err := decodeJSONWithOptions(b, &raw, options); err != nil {
|
||||||
|
return ClientConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := ClientConfig{
|
||||||
|
ClientCommonConfig: raw.ClientCommonConfig,
|
||||||
|
Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)),
|
||||||
|
Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, proxyData := range raw.Proxies {
|
||||||
|
proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{
|
||||||
|
Type: proxyCfg.GetBaseConfig().Type,
|
||||||
|
ProxyConfigurer: proxyCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, visitorData := range raw.Visitors {
|
||||||
|
visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options)
|
||||||
|
if err != nil {
|
||||||
|
return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{
|
||||||
|
Type: visitorCfg.GetBaseConfig().Type,
|
||||||
|
VisitorConfigurer: visitorCfg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
86
pkg/config/v1/decode_test.go
Normal file
86
pkg/config/v1/decode_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2026 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"plugin":{
|
||||||
|
"type":"http2https",
|
||||||
|
"localAddr":"127.0.0.1:8080",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"name":"v1",
|
||||||
|
"type":"stcp",
|
||||||
|
"serverName":"server",
|
||||||
|
"bindPort":10081,
|
||||||
|
"plugin":{
|
||||||
|
"type":"virtual_net",
|
||||||
|
"destinationIP":"10.0.0.1",
|
||||||
|
"unknownInPlugin":"value"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownInPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
data := []byte(`{
|
||||||
|
"serverPort":7000,
|
||||||
|
"proxies":[
|
||||||
|
{
|
||||||
|
"name":"p1",
|
||||||
|
"type":"tcp",
|
||||||
|
"localPort":10080,
|
||||||
|
"unknownField":"value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
||||||
|
require.ErrorContains(err, "unknownField")
|
||||||
|
}
|
||||||
@@ -15,16 +15,13 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
"maps"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,35 +199,18 @@ type TypedProxyConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.ProxyConfigurer = configurer
|
c.ProxyConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ProxyConfigurer)
|
return jsonx.Marshal(c.ProxyConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyConfigurer interface {
|
type ProxyConfigurer interface {
|
||||||
@@ -259,14 +239,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}),
|
ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](),
|
||||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}),
|
ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](),
|
||||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}),
|
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](),
|
||||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}),
|
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](),
|
||||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
|
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](),
|
||||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}),
|
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](),
|
||||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}),
|
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](),
|
||||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}),
|
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](),
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
||||||
|
|||||||
@@ -15,14 +15,11 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,16 +37,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](),
|
||||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](),
|
||||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](),
|
||||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](),
|
||||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](),
|
||||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
PluginSocks5: reflect.TypeFor[Socks5PluginOptions](),
|
||||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](),
|
||||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](),
|
||||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](),
|
||||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientPluginOptions interface {
|
type ClientPluginOptions interface {
|
||||||
@@ -71,42 +68,16 @@ func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{})
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
c.Type = typeStruct.Type
|
|
||||||
if c.Type == "" {
|
|
||||||
return errors.New("plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
options := reflect.New(v).Interface().(ClientPluginOptions)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := decoder.Decode(options); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
c.ClientPluginOptions = options
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.ClientPluginOptions)
|
return jsonx.Marshal(c.ClientPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2HTTPSPluginOptions struct {
|
type HTTP2HTTPSPluginOptions struct {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,12 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,9 +79,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
|
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](),
|
||||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
|
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](),
|
||||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
|
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedVisitorConfig struct {
|
type TypedVisitorConfig struct {
|
||||||
@@ -93,35 +90,18 @@ type TypedVisitorConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{})
|
||||||
return errors.New("type is required")
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = typeStruct.Type
|
c.Type = configurer.GetBaseConfig().Type
|
||||||
configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(configurer); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
|
||||||
}
|
|
||||||
c.VisitorConfigurer = configurer
|
c.VisitorConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorConfigurer)
|
return jsonx.Marshal(c.VisitorConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,7 +25,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
|
VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](),
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorPluginOptions interface {
|
type VisitorPluginOptions interface {
|
||||||
@@ -49,42 +47,16 @@ func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
if len(b) == 4 && string(b) == "null" {
|
decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{})
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
typeStruct := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*c = decoded
|
||||||
c.Type = typeStruct.Type
|
|
||||||
if c.Type == "" {
|
|
||||||
return errors.New("visitor plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := visitorPluginOptionsTypeMap[typeStruct.Type]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type)
|
|
||||||
}
|
|
||||||
options := reflect.New(v).Interface().(VisitorPluginOptions)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
if DisallowUnknownFields {
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := decoder.Decode(options); err != nil {
|
|
||||||
return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
c.VisitorPluginOptions = options
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(c.VisitorPluginOptions)
|
return jsonx.Marshal(c.VisitorPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetVisitorPluginOptions struct {
|
type VirtualNetVisitorPluginOptions struct {
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ func (m *serverMetrics) OpenConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Inc(1)
|
proxyStats.CurConns.Inc(1)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +154,6 @@ func (m *serverMetrics) CloseConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Dec(1)
|
proxyStats.CurConns.Dec(1)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +166,6 @@ func (m *serverMetrics) AddTrafficIn(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficIn.Inc(trafficBytes)
|
proxyStats.TrafficIn.Inc(trafficBytes)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +178,6 @@ func (m *serverMetrics) AddTrafficOut(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficOut.Inc(trafficBytes)
|
proxyStats.TrafficOut.Inc(trafficBytes)
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +199,25 @@ func (m *serverMetrics) GetServer() *ServerStats {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats {
|
||||||
|
ps := &ProxyStats{
|
||||||
|
Name: name,
|
||||||
|
Type: proxyStats.ProxyType,
|
||||||
|
User: proxyStats.User,
|
||||||
|
ClientID: proxyStats.ClientID,
|
||||||
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
|
}
|
||||||
|
if !proxyStats.LastStartTime.IsZero() {
|
||||||
|
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
if !proxyStats.LastCloseTime.IsZero() {
|
||||||
|
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
||||||
res := make([]*ProxyStats, 0)
|
res := make([]*ProxyStats, 0)
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -212,23 +227,7 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
if proxyStats.ProxyType != proxyType {
|
if proxyStats.ProxyType != proxyType {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
res = append(res, toProxyStats(name, proxyStats))
|
||||||
ps := &ProxyStats{
|
|
||||||
Name: name,
|
|
||||||
Type: proxyStats.ProxyType,
|
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
|
||||||
}
|
|
||||||
if !proxyStats.LastStartTime.IsZero() {
|
|
||||||
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
res = append(res, ps)
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -237,31 +236,9 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
for name, proxyStats := range m.info.ProxyStatistics {
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
if proxyStats.ProxyType != proxyType {
|
if ok && proxyStats.ProxyType == proxyType {
|
||||||
continue
|
res = toProxyStats(proxyName, proxyStats)
|
||||||
}
|
|
||||||
|
|
||||||
if name != proxyName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
res = &ProxyStats{
|
|
||||||
Name: name,
|
|
||||||
Type: proxyStats.ProxyType,
|
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
|
||||||
}
|
|
||||||
if !proxyStats.LastStartTime.IsZero() {
|
|
||||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -272,21 +249,7 @@ func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
|||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
if ok {
|
if ok {
|
||||||
res = &ProxyStats{
|
res = toProxyStats(proxyName, proxyStats)
|
||||||
Name: proxyName,
|
|
||||||
Type: proxyStats.ProxyType,
|
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
|
||||||
}
|
|
||||||
if !proxyStats.LastStartTime.IsZero() {
|
|
||||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{
|
|||||||
TypeNatHoleReport: NatHoleReport{},
|
TypeNatHoleReport: NatHoleReport{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
|
var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name()
|
||||||
|
|
||||||
type ClientSpec struct {
|
type ClientSpec struct {
|
||||||
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
||||||
@@ -184,7 +184,7 @@ type Pong struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPPacket struct {
|
type UDPPacket struct {
|
||||||
Content string `json:"c,omitempty"`
|
Content []byte `json:"c,omitempty"`
|
||||||
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
||||||
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ func StripUserPrefix(user, name string) string {
|
|||||||
if user == "" {
|
if user == "" {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
prefix := user + "."
|
if trimmed, ok := strings.CutPrefix(name, user+"."); ok {
|
||||||
if strings.HasPrefix(name, prefix) {
|
return trimmed
|
||||||
return strings.TrimPrefix(name, prefix)
|
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func getBehaviorScoresByMode(mode int, defaultScore int) []*BehaviorScore {
|
|||||||
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
||||||
behaviors := getBehaviorByMode(mode)
|
behaviors := getBehaviorByMode(mode)
|
||||||
scores := make([]*BehaviorScore, 0, len(behaviors))
|
scores := make([]*BehaviorScore, 0, len(behaviors))
|
||||||
for i := 0; i < len(behaviors); i++ {
|
for i := range behaviors {
|
||||||
score := receiverScore
|
score := receiverScore
|
||||||
if behaviors[i].A.Role == DetectRoleSender {
|
if behaviors[i].A.Role == DetectRoleSender {
|
||||||
score = senderScore
|
score = senderScore
|
||||||
|
|||||||
@@ -70,12 +70,8 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if portNum > portMax {
|
portMax = max(portMax, portNum)
|
||||||
portMax = portNum
|
portMin = min(portMin, portNum)
|
||||||
}
|
|
||||||
if portNum < portMin {
|
|
||||||
portMin = portNum
|
|
||||||
}
|
|
||||||
if baseIP != ip {
|
if baseIP != ip {
|
||||||
ipChanged = true
|
ipChanged = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,9 @@ func (c *Controller) GenSid() string {
|
|||||||
|
|
||||||
func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
|
func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
|
||||||
if m.PreCheck {
|
if m.PreCheck {
|
||||||
|
c.mu.RLock()
|
||||||
cfg, ok := c.clientCfgs[m.ProxyName]
|
cfg, ok := c.clientCfgs[m.ProxyName]
|
||||||
|
c.mu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
|
_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -298,11 +298,13 @@ func waitDetectMessage(
|
|||||||
n, raddr, err := conn.ReadFromUDP(buf)
|
n, raddr, err := conn.ReadFromUDP(buf)
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
pool.PutBuf(buf)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
|
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
|
||||||
var m msg.NatHoleSid
|
var m msg.NatHoleSid
|
||||||
if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
|
if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
|
||||||
|
pool.PutBuf(buf)
|
||||||
xl.Warnf("decode sid message error: %v", err)
|
xl.Warnf("decode sid message error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -408,7 +410,7 @@ func sendSidMessageToRandomPorts(
|
|||||||
xl := xlog.FromContextSafe(ctx)
|
xl := xlog.FromContextSafe(ctx)
|
||||||
used := sets.New[int]()
|
used := sets.New[int]()
|
||||||
getUnusedPort := func() int {
|
getUnusedPort := func() int {
|
||||||
for i := 0; i < 10; i++ {
|
for range 10 {
|
||||||
port := rand.IntN(65535-1024) + 1024
|
port := rand.IntN(65535-1024) + 1024
|
||||||
if !used.Has(port) {
|
if !used.Has(port) {
|
||||||
used.Insert(port)
|
used.Insert(port)
|
||||||
@@ -418,7 +420,7 @@ func sendSidMessageToRandomPorts(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for range count {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
stdlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/pool"
|
"github.com/fatedier/golib/pool"
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
|
|||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
Handler: rp,
|
Handler: rp,
|
||||||
ReadHeaderTimeout: 0,
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
stdlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/pool"
|
"github.com/fatedier/golib/pool"
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi
|
|||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
Handler: rp,
|
Handler: rp,
|
||||||
ReadHeaderTimeout: 0,
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
|
|||||||
|
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
xl.Warnf("tls handshake error: %v", err)
|
xl.Warnf("tls handshake error: %v", err)
|
||||||
|
tlsConn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
|
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("dial to local addr error: %v", err)
|
xl.Warnf("dial to local addr error: %v", err)
|
||||||
|
tlsConn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *Connect
|
|||||||
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
|
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
|
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
|
||||||
|
connInfo.Conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if connInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
|
localConn.Close()
|
||||||
|
connInfo.Conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -64,12 +65,7 @@ func (p *httpPlugin) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *httpPlugin) IsSupport(op string) bool {
|
func (p *httpPlugin) IsSupport(op string) bool {
|
||||||
for _, v := range p.options.Ops {
|
return slices.Contains(p.options.Ops, op)
|
||||||
if v == op {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) {
|
func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) {
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
|
|
||||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||||
baseDelay := 60 * time.Second
|
baseDelay := 60 * time.Second
|
||||||
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
reconnectDelay = min(baseDelay*time.Duration(1<<uint(p.consecutiveErrors-1)), 300*time.Second)
|
||||||
if reconnectDelay > 300*time.Second {
|
|
||||||
reconnectDelay = 300 * time.Second
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Reset consecutive errors on successful connection
|
// Reset consecutive errors on successful connection
|
||||||
if p.consecutiveErrors > 0 {
|
if p.consecutiveErrors > 0 {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package featuregate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -92,10 +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)
|
||||||
for k, v := range defaultFeatures {
|
|
||||||
known[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
f := &featureGate{}
|
f := &featureGate{}
|
||||||
f.known.Store(known)
|
f.known.Store(known)
|
||||||
@@ -109,14 +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))
|
||||||
for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
|
enabled := maps.Clone(f.enabled.Load().(map[Feature]bool))
|
||||||
known[k] = v
|
|
||||||
}
|
|
||||||
enabled := map[Feature]bool{}
|
|
||||||
for k, v := range f.enabled.Load().(map[Feature]bool) {
|
|
||||||
enabled[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the new settings
|
// Apply the new settings
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@@ -147,10 +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))
|
||||||
for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
|
|
||||||
known[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new features
|
// Add new features
|
||||||
for name, spec := range features {
|
for name, spec := range features {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package udp
|
package udp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,16 +27,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
|
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
|
||||||
|
content := make([]byte, len(buf))
|
||||||
|
copy(content, buf)
|
||||||
return &msg.UDPPacket{
|
return &msg.UDPPacket{
|
||||||
Content: base64.StdEncoding.EncodeToString(buf),
|
Content: content,
|
||||||
LocalAddr: laddr,
|
LocalAddr: laddr,
|
||||||
RemoteAddr: raddr,
|
RemoteAddr: raddr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContent(m *msg.UDPPacket) (buf []byte, err error) {
|
func GetContent(m *msg.UDPPacket) (buf []byte, err error) {
|
||||||
buf, err = base64.StdEncoding.DecodeString(m.Content)
|
return m.Content, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) {
|
func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) {
|
||||||
@@ -60,7 +60,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// buf[:n] will be encoded to string, so the bytes can be reused
|
// NewUDPPacket copies buf[:n], so the read buffer can be reused
|
||||||
udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr)
|
udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -85,6 +85,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
buf := pool.GetBuf(bufSize)
|
buf := pool.GetBuf(bufSize)
|
||||||
|
defer pool.PutBuf(buf)
|
||||||
for {
|
for {
|
||||||
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
n, _, err := udpConn.ReadFromUDP(buf)
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
"github.com/fatedier/frp/client/http/model"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
|||||||
c.authPwd = pwd
|
c.authPwd = pwd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*model.ProxyStatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(model.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (model.StatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(model.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -85,7 +86,9 @@ func newCertPool(caPath string) (*x509.CertPool, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.AppendCertsFromPEM(caCrt)
|
if !pool.AppendCertsFromPEM(caCrt) {
|
||||||
|
return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath)
|
||||||
|
}
|
||||||
|
|
||||||
return pool, nil
|
return pool, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cs := string(c)
|
cs := string(c)
|
||||||
s := strings.IndexByte(cs, ':')
|
before, after, found := strings.Cut(cs, ":")
|
||||||
if s < 0 {
|
if !found {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return cs[:s], cs[s+1:], true
|
return before, after, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func BasicAuth(username, passwd string) string {
|
func BasicAuth(username, passwd string) string {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
45
pkg/util/jsonx/json_v1.go
Normal file
45
pkg/util/jsonx/json_v1.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DecodeOptions struct {
|
||||||
|
RejectUnknownMembers bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||||
|
return json.MarshalIndent(v, prefix, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(data []byte, out any) error {
|
||||||
|
return json.Unmarshal(data, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalWithOptions(data []byte, out any, options DecodeOptions) error {
|
||||||
|
if !options.RejectUnknownMembers {
|
||||||
|
return json.Unmarshal(data, out)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
return decoder.Decode(out)
|
||||||
|
}
|
||||||
36
pkg/util/jsonx/raw_message.go
Normal file
36
pkg/util/jsonx/raw_message.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// 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 jsonx
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// RawMessage stores a raw encoded JSON value.
|
||||||
|
// It is equivalent to encoding/json.RawMessage behavior.
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("jsonx.RawMessage: UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -86,11 +86,7 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) {
|
|||||||
c.lastActive = time.Now()
|
c.lastActive = time.Now()
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if len(b) < len(content) {
|
n = min(len(b), len(content))
|
||||||
n = len(b)
|
|
||||||
} else {
|
|
||||||
n = len(content)
|
|
||||||
}
|
|
||||||
copy(b, content)
|
copy(b, content)
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -168,11 +164,15 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) {
|
|||||||
return l, err
|
return l, err
|
||||||
}
|
}
|
||||||
readConn, err := net.ListenUDP("udp", udpAddr)
|
readConn, err := net.ListenUDP("udp", udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
l = &UDPListener{
|
l = &UDPListener{
|
||||||
addr: udpAddr,
|
addr: udpAddr,
|
||||||
acceptCh: make(chan net.Conn),
|
acceptCh: make(chan net.Conn),
|
||||||
writeCh: make(chan *UDPPacket, 1000),
|
writeCh: make(chan *UDPPacket, 1000),
|
||||||
|
readConn: readConn,
|
||||||
fakeConns: make(map[string]*FakeUDPConn),
|
fakeConns: make(map[string]*FakeUDPConn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type WebsocketListener struct {
|
|||||||
// ln: tcp listener for websocket connections
|
// ln: tcp listener for websocket connections
|
||||||
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
||||||
wl = &WebsocketListener{
|
wl = &WebsocketListener{
|
||||||
|
ln: ln,
|
||||||
acceptCh: make(chan net.Conn),
|
acceptCh: make(chan net.Conn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) {
|
|||||||
rangeStr = strings.TrimSpace(rangeStr)
|
rangeStr = strings.TrimSpace(rangeStr)
|
||||||
numbers = make([]int64, 0)
|
numbers = make([]int64, 0)
|
||||||
// e.g. 1000-2000,2001,2002,3000-4000
|
// e.g. 1000-2000,2001,2002,3000-4000
|
||||||
numRanges := strings.Split(rangeStr, ",")
|
numRanges := strings.SplitSeq(rangeStr, ",")
|
||||||
for _, numRangeStr := range numRanges {
|
for numRangeStr := range numRanges {
|
||||||
// 1000-2000 or 2001
|
// 1000-2000 or 2001
|
||||||
numArray := strings.Split(numRangeStr, "-")
|
numArray := strings.Split(numRangeStr, "-")
|
||||||
// length: only 1 or 2 is correct
|
// length: only 1 or 2 is correct
|
||||||
|
|||||||
@@ -266,31 +266,13 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
|
|||||||
go libio.Join(remote, client)
|
go libio.Join(remote, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
|
||||||
const prefix = "Basic "
|
|
||||||
// Case insensitive prefix match. See Issue 22736.
|
|
||||||
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cs := string(c)
|
|
||||||
s := strings.IndexByte(cs, ':')
|
|
||||||
if s < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return cs[:s], cs[s+1:], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
||||||
user := ""
|
user := ""
|
||||||
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
|
// 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, _, _ = parseBasicAuth(proxyAuth)
|
user, _, _ = httppkg.ParseBasicAuth(proxyAuth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if user == "" {
|
if user == "" {
|
||||||
|
|||||||
@@ -63,11 +63,12 @@ func (l *Logger) AddPrefix(prefix LogPrefix) *Logger {
|
|||||||
if prefix.Priority <= 0 {
|
if prefix.Priority <= 0 {
|
||||||
prefix.Priority = 10
|
prefix.Priority = 10
|
||||||
}
|
}
|
||||||
for _, p := range l.prefixes {
|
for i, p := range l.prefixes {
|
||||||
if p.Name == prefix.Name {
|
if p.Name == prefix.Name {
|
||||||
found = true
|
found = true
|
||||||
p.Value = prefix.Value
|
l.prefixes[i].Value = prefix.Value
|
||||||
p.Priority = prefix.Priority
|
l.prefixes[i].Priority = prefix.Priority
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
server/api_router.go
Normal file
64
server/api_router.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
adminapi "github.com/fatedier/frp/server/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
|
helper.Router.HandleFunc("/healthz", healthz)
|
||||||
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
|
||||||
|
subRouter.Use(helper.AuthMiddleware)
|
||||||
|
subRouter.Use(httppkg.NewRequestLogger)
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
if svr.cfg.EnablePrometheus {
|
||||||
|
subRouter.Handle("/metrics", promhttp.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
apiController := adminapi.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
|
||||||
|
|
||||||
|
// apis
|
||||||
|
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE")
|
||||||
|
|
||||||
|
// view
|
||||||
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
|
).Methods("GET")
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
@@ -95,20 +95,33 @@ func (cm *ControlManager) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Control struct {
|
// SessionContext encapsulates the input parameters for creating a new Control.
|
||||||
|
type SessionContext struct {
|
||||||
// all resource managers and controllers
|
// all resource managers and controllers
|
||||||
rc *controller.ResourceController
|
RC *controller.ResourceController
|
||||||
|
|
||||||
// proxy manager
|
// proxy manager
|
||||||
pxyManager *proxy.Manager
|
PxyManager *proxy.Manager
|
||||||
|
|
||||||
// plugin manager
|
// plugin manager
|
||||||
pluginManager *plugin.Manager
|
PluginManager *plugin.Manager
|
||||||
|
|
||||||
// verifies authentication based on selected method
|
// verifies authentication based on selected method
|
||||||
authVerifier auth.Verifier
|
AuthVerifier auth.Verifier
|
||||||
// key used for connection encryption
|
// key used for connection encryption
|
||||||
encryptionKey []byte
|
EncryptionKey []byte
|
||||||
|
// control connection
|
||||||
|
Conn net.Conn
|
||||||
|
// indicates whether the connection is encrypted
|
||||||
|
ConnEncrypted bool
|
||||||
|
// login message
|
||||||
|
LoginMsg *msg.Login
|
||||||
|
// server configuration
|
||||||
|
ServerCfg *v1.ServerConfig
|
||||||
|
// client registry
|
||||||
|
ClientRegistry *registry.ClientRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
type Control struct {
|
||||||
|
// session context
|
||||||
|
sessionCtx *SessionContext
|
||||||
|
|
||||||
// other components can use this to communicate with client
|
// other components can use this to communicate with client
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
@@ -117,12 +130,6 @@ type Control struct {
|
|||||||
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
|
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
|
||||||
msgDispatcher *msg.Dispatcher
|
msgDispatcher *msg.Dispatcher
|
||||||
|
|
||||||
// login message
|
|
||||||
loginMsg *msg.Login
|
|
||||||
|
|
||||||
// control connection
|
|
||||||
conn net.Conn
|
|
||||||
|
|
||||||
// work connections
|
// work connections
|
||||||
workConnCh chan net.Conn
|
workConnCh chan net.Conn
|
||||||
|
|
||||||
@@ -145,61 +152,34 @@ type Control struct {
|
|||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
// Server configuration information
|
|
||||||
serverCfg *v1.ServerConfig
|
|
||||||
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext.
|
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
|
||||||
func NewControl(
|
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount))
|
||||||
ctx context.Context,
|
|
||||||
rc *controller.ResourceController,
|
|
||||||
pxyManager *proxy.Manager,
|
|
||||||
pluginManager *plugin.Manager,
|
|
||||||
authVerifier auth.Verifier,
|
|
||||||
encryptionKey []byte,
|
|
||||||
ctlConn net.Conn,
|
|
||||||
ctlConnEncrypted bool,
|
|
||||||
loginMsg *msg.Login,
|
|
||||||
serverCfg *v1.ServerConfig,
|
|
||||||
) (*Control, error) {
|
|
||||||
poolCount := loginMsg.PoolCount
|
|
||||||
if poolCount > int(serverCfg.Transport.MaxPoolCount) {
|
|
||||||
poolCount = int(serverCfg.Transport.MaxPoolCount)
|
|
||||||
}
|
|
||||||
ctl := &Control{
|
ctl := &Control{
|
||||||
rc: rc,
|
sessionCtx: sessionCtx,
|
||||||
pxyManager: pxyManager,
|
workConnCh: make(chan net.Conn, poolCount+10),
|
||||||
pluginManager: pluginManager,
|
proxies: make(map[string]proxy.Proxy),
|
||||||
authVerifier: authVerifier,
|
poolCount: poolCount,
|
||||||
encryptionKey: encryptionKey,
|
portsUsedNum: 0,
|
||||||
conn: ctlConn,
|
runID: sessionCtx.LoginMsg.RunID,
|
||||||
loginMsg: loginMsg,
|
xl: xlog.FromContextSafe(ctx),
|
||||||
workConnCh: make(chan net.Conn, poolCount+10),
|
ctx: ctx,
|
||||||
proxies: make(map[string]proxy.Proxy),
|
doneCh: make(chan struct{}),
|
||||||
poolCount: poolCount,
|
|
||||||
portsUsedNum: 0,
|
|
||||||
runID: loginMsg.RunID,
|
|
||||||
serverCfg: serverCfg,
|
|
||||||
xl: xlog.FromContextSafe(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
doneCh: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
ctl.lastPing.Store(time.Now())
|
ctl.lastPing.Store(time.Now())
|
||||||
|
|
||||||
if ctlConnEncrypted {
|
if sessionCtx.ConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.EncryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
||||||
} else {
|
} else {
|
||||||
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
@@ -213,7 +193,7 @@ func (ctl *Control) Start() {
|
|||||||
RunID: ctl.runID,
|
RunID: ctl.runID,
|
||||||
Error: "",
|
Error: "",
|
||||||
}
|
}
|
||||||
_ = msg.WriteMsg(ctl.conn, loginRespMsg)
|
_ = msg.WriteMsg(ctl.sessionCtx.Conn, loginRespMsg)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for i := 0; i < ctl.poolCount; i++ {
|
for i := 0; i < ctl.poolCount; i++ {
|
||||||
@@ -225,7 +205,7 @@ func (ctl *Control) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) Close() error {
|
func (ctl *Control) Close() error {
|
||||||
ctl.conn.Close()
|
ctl.sessionCtx.Conn.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +213,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
|
|||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
xl.Infof("replaced by client [%s]", newCtl.runID)
|
xl.Infof("replaced by client [%s]", newCtl.runID)
|
||||||
ctl.runID = ""
|
ctl.runID = ""
|
||||||
ctl.conn.Close()
|
ctl.sessionCtx.Conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
|
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
|
||||||
@@ -291,7 +271,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(time.Duration(ctl.serverCfg.UserConnTimeout) * time.Second):
|
case <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second):
|
||||||
err = fmt.Errorf("timeout trying to get work connection")
|
err = fmt.Errorf("timeout trying to get work connection")
|
||||||
xl.Warnf("%v", err)
|
xl.Warnf("%v", err)
|
||||||
return
|
return
|
||||||
@@ -304,15 +284,15 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) heartbeatWorker() {
|
func (ctl *Control) heartbeatWorker() {
|
||||||
if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 {
|
if ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
go wait.Until(func() {
|
go wait.Until(func() {
|
||||||
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
|
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout)*time.Second {
|
||||||
xl.Warnf("heartbeat timeout")
|
xl.Warnf("heartbeat timeout")
|
||||||
ctl.conn.Close()
|
ctl.sessionCtx.Conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}, time.Second, ctl.doneCh)
|
}, time.Second, ctl.doneCh)
|
||||||
@@ -323,6 +303,30 @@ func (ctl *Control) WaitClosed() {
|
|||||||
<-ctl.doneCh
|
<-ctl.doneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) loginUserInfo() plugin.UserInfo {
|
||||||
|
return plugin.UserInfo{
|
||||||
|
User: ctl.sessionCtx.LoginMsg.User,
|
||||||
|
Metas: ctl.sessionCtx.LoginMsg.Metas,
|
||||||
|
RunID: ctl.sessionCtx.LoginMsg.RunID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) closeProxy(pxy proxy.Proxy) {
|
||||||
|
pxy.Close()
|
||||||
|
ctl.sessionCtx.PxyManager.Del(pxy.GetName())
|
||||||
|
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||||
|
|
||||||
|
notifyContent := &plugin.CloseProxyContent{
|
||||||
|
User: ctl.loginUserInfo(),
|
||||||
|
CloseProxy: msg.CloseProxy{
|
||||||
|
ProxyName: pxy.GetName(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
_ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (ctl *Control) worker() {
|
func (ctl *Control) worker() {
|
||||||
xl := ctl.xl
|
xl := ctl.xl
|
||||||
|
|
||||||
@@ -330,38 +334,23 @@ func (ctl *Control) worker() {
|
|||||||
go ctl.msgDispatcher.Run()
|
go ctl.msgDispatcher.Run()
|
||||||
|
|
||||||
<-ctl.msgDispatcher.Done()
|
<-ctl.msgDispatcher.Done()
|
||||||
ctl.conn.Close()
|
ctl.sessionCtx.Conn.Close()
|
||||||
|
|
||||||
ctl.mu.Lock()
|
ctl.mu.Lock()
|
||||||
defer ctl.mu.Unlock()
|
|
||||||
|
|
||||||
close(ctl.workConnCh)
|
close(ctl.workConnCh)
|
||||||
for workConn := range ctl.workConnCh {
|
for workConn := range ctl.workConnCh {
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
}
|
}
|
||||||
|
proxies := ctl.proxies
|
||||||
|
ctl.proxies = make(map[string]proxy.Proxy)
|
||||||
|
ctl.mu.Unlock()
|
||||||
|
|
||||||
for _, pxy := range ctl.proxies {
|
for _, pxy := range proxies {
|
||||||
pxy.Close()
|
ctl.closeProxy(pxy)
|
||||||
ctl.pxyManager.Del(pxy.GetName())
|
|
||||||
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
|
||||||
|
|
||||||
notifyContent := &plugin.CloseProxyContent{
|
|
||||||
User: plugin.UserInfo{
|
|
||||||
User: ctl.loginMsg.User,
|
|
||||||
Metas: ctl.loginMsg.Metas,
|
|
||||||
RunID: ctl.loginMsg.RunID,
|
|
||||||
},
|
|
||||||
CloseProxy: msg.CloseProxy{
|
|
||||||
ProxyName: pxy.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
_ = ctl.pluginManager.CloseProxy(notifyContent)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.Server.CloseClient()
|
metrics.Server.CloseClient()
|
||||||
ctl.clientRegistry.MarkOfflineByRunID(ctl.runID)
|
ctl.sessionCtx.ClientRegistry.MarkOfflineByRunID(ctl.runID)
|
||||||
xl.Infof("client exit success")
|
xl.Infof("client exit success")
|
||||||
close(ctl.doneCh)
|
close(ctl.doneCh)
|
||||||
}
|
}
|
||||||
@@ -380,15 +369,11 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
|||||||
inMsg := m.(*msg.NewProxy)
|
inMsg := m.(*msg.NewProxy)
|
||||||
|
|
||||||
content := &plugin.NewProxyContent{
|
content := &plugin.NewProxyContent{
|
||||||
User: plugin.UserInfo{
|
User: ctl.loginUserInfo(),
|
||||||
User: ctl.loginMsg.User,
|
|
||||||
Metas: ctl.loginMsg.Metas,
|
|
||||||
RunID: ctl.loginMsg.RunID,
|
|
||||||
},
|
|
||||||
NewProxy: *inMsg,
|
NewProxy: *inMsg,
|
||||||
}
|
}
|
||||||
var remoteAddr string
|
var remoteAddr string
|
||||||
retContent, err := ctl.pluginManager.NewProxy(content)
|
retContent, err := ctl.sessionCtx.PluginManager.NewProxy(content)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
inMsg = &retContent.NewProxy
|
inMsg = &retContent.NewProxy
|
||||||
remoteAddr, err = ctl.RegisterProxy(inMsg)
|
remoteAddr, err = ctl.RegisterProxy(inMsg)
|
||||||
@@ -401,15 +386,15 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
|
xl.Warnf("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
|
||||||
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
|
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
|
||||||
err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
|
err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient))
|
||||||
} else {
|
} else {
|
||||||
resp.RemoteAddr = remoteAddr
|
resp.RemoteAddr = remoteAddr
|
||||||
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
||||||
clientID := ctl.loginMsg.ClientID
|
clientID := ctl.sessionCtx.LoginMsg.ClientID
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
clientID = ctl.loginMsg.RunID
|
clientID = ctl.sessionCtx.LoginMsg.RunID
|
||||||
}
|
}
|
||||||
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.loginMsg.User, clientID)
|
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.sessionCtx.LoginMsg.User, clientID)
|
||||||
}
|
}
|
||||||
_ = ctl.msgDispatcher.Send(resp)
|
_ = ctl.msgDispatcher.Send(resp)
|
||||||
}
|
}
|
||||||
@@ -419,22 +404,18 @@ func (ctl *Control) handlePing(m msg.Message) {
|
|||||||
inMsg := m.(*msg.Ping)
|
inMsg := m.(*msg.Ping)
|
||||||
|
|
||||||
content := &plugin.PingContent{
|
content := &plugin.PingContent{
|
||||||
User: plugin.UserInfo{
|
User: ctl.loginUserInfo(),
|
||||||
User: ctl.loginMsg.User,
|
|
||||||
Metas: ctl.loginMsg.Metas,
|
|
||||||
RunID: ctl.loginMsg.RunID,
|
|
||||||
},
|
|
||||||
Ping: *inMsg,
|
Ping: *inMsg,
|
||||||
}
|
}
|
||||||
retContent, err := ctl.pluginManager.Ping(content)
|
retContent, err := ctl.sessionCtx.PluginManager.Ping(content)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
inMsg = &retContent.Ping
|
inMsg = &retContent.Ping
|
||||||
err = ctl.authVerifier.VerifyPing(inMsg)
|
err = ctl.sessionCtx.AuthVerifier.VerifyPing(inMsg)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("received invalid ping: %v", err)
|
xl.Warnf("received invalid ping: %v", err)
|
||||||
_ = ctl.msgDispatcher.Send(&msg.Pong{
|
_ = ctl.msgDispatcher.Send(&msg.Pong{
|
||||||
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
|
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -445,17 +426,17 @@ func (ctl *Control) handlePing(m msg.Message) {
|
|||||||
|
|
||||||
func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
|
func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
|
||||||
inMsg := m.(*msg.NatHoleVisitor)
|
inMsg := m.(*msg.NatHoleVisitor)
|
||||||
ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User)
|
ctl.sessionCtx.RC.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.sessionCtx.LoginMsg.User)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) handleNatHoleClient(m msg.Message) {
|
func (ctl *Control) handleNatHoleClient(m msg.Message) {
|
||||||
inMsg := m.(*msg.NatHoleClient)
|
inMsg := m.(*msg.NatHoleClient)
|
||||||
ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
|
ctl.sessionCtx.RC.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) handleNatHoleReport(m msg.Message) {
|
func (ctl *Control) handleNatHoleReport(m msg.Message) {
|
||||||
inMsg := m.(*msg.NatHoleReport)
|
inMsg := m.(*msg.NatHoleReport)
|
||||||
ctl.rc.NatHoleController.HandleReport(inMsg)
|
ctl.sessionCtx.RC.NatHoleController.HandleReport(inMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Control) handleCloseProxy(m msg.Message) {
|
func (ctl *Control) handleCloseProxy(m msg.Message) {
|
||||||
@@ -468,15 +449,15 @@ func (ctl *Control) handleCloseProxy(m msg.Message) {
|
|||||||
func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
|
func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
|
||||||
var pxyConf v1.ProxyConfigurer
|
var pxyConf v1.ProxyConfigurer
|
||||||
// Load configures from NewProxy message and validate.
|
// Load configures from NewProxy message and validate.
|
||||||
pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.serverCfg)
|
pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.sessionCtx.ServerCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// User info
|
// User info
|
||||||
userInfo := plugin.UserInfo{
|
userInfo := plugin.UserInfo{
|
||||||
User: ctl.loginMsg.User,
|
User: ctl.sessionCtx.LoginMsg.User,
|
||||||
Metas: ctl.loginMsg.Metas,
|
Metas: ctl.sessionCtx.LoginMsg.Metas,
|
||||||
RunID: ctl.runID,
|
RunID: ctl.runID,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,22 +465,22 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
// In fact, it creates different proxies based on the proxy type. We just call run() here.
|
// In fact, it creates different proxies based on the proxy type. We just call run() here.
|
||||||
pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{
|
pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{
|
||||||
UserInfo: userInfo,
|
UserInfo: userInfo,
|
||||||
LoginMsg: ctl.loginMsg,
|
LoginMsg: ctl.sessionCtx.LoginMsg,
|
||||||
PoolCount: ctl.poolCount,
|
PoolCount: ctl.poolCount,
|
||||||
ResourceController: ctl.rc,
|
ResourceController: ctl.sessionCtx.RC,
|
||||||
GetWorkConnFn: ctl.GetWorkConn,
|
GetWorkConnFn: ctl.GetWorkConn,
|
||||||
Configurer: pxyConf,
|
Configurer: pxyConf,
|
||||||
ServerCfg: ctl.serverCfg,
|
ServerCfg: ctl.sessionCtx.ServerCfg,
|
||||||
EncryptionKey: ctl.encryptionKey,
|
EncryptionKey: ctl.sessionCtx.EncryptionKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return remoteAddr, err
|
return remoteAddr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ports used number in each client
|
// Check ports used number in each client
|
||||||
if ctl.serverCfg.MaxPortsPerClient > 0 {
|
if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 {
|
||||||
ctl.mu.Lock()
|
ctl.mu.Lock()
|
||||||
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.serverCfg.MaxPortsPerClient) {
|
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.sessionCtx.ServerCfg.MaxPortsPerClient) {
|
||||||
ctl.mu.Unlock()
|
ctl.mu.Unlock()
|
||||||
err = fmt.Errorf("exceed the max_ports_per_client")
|
err = fmt.Errorf("exceed the max_ports_per_client")
|
||||||
return
|
return
|
||||||
@@ -516,7 +497,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctl.pxyManager.Exist(pxyMsg.ProxyName) {
|
if ctl.sessionCtx.PxyManager.Exist(pxyMsg.ProxyName) {
|
||||||
err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName)
|
err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -531,7 +512,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = ctl.pxyManager.Add(pxyMsg.ProxyName, pxy)
|
err = ctl.sessionCtx.PxyManager.Add(pxyMsg.ProxyName, pxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -550,28 +531,12 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctl.serverCfg.MaxPortsPerClient > 0 {
|
if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 {
|
||||||
ctl.portsUsedNum -= pxy.GetUsedPortsNum()
|
ctl.portsUsedNum -= pxy.GetUsedPortsNum()
|
||||||
}
|
}
|
||||||
pxy.Close()
|
|
||||||
ctl.pxyManager.Del(pxy.GetName())
|
|
||||||
delete(ctl.proxies, closeMsg.ProxyName)
|
delete(ctl.proxies, closeMsg.ProxyName)
|
||||||
ctl.mu.Unlock()
|
ctl.mu.Unlock()
|
||||||
|
|
||||||
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
ctl.closeProxy(pxy)
|
||||||
|
|
||||||
notifyContent := &plugin.CloseProxyContent{
|
|
||||||
User: plugin.UserInfo{
|
|
||||||
User: ctl.loginMsg.User,
|
|
||||||
Metas: ctl.loginMsg.Metas,
|
|
||||||
RunID: ctl.loginMsg.RunID,
|
|
||||||
},
|
|
||||||
CloseProxy: msg.CloseProxy{
|
|
||||||
ProxyName: pxy.GetName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
_ = ctl.pluginManager.CloseProxy(notifyContent)
|
|
||||||
}()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
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")
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user