Compare commits

..

1 Commits

Author SHA1 Message Date
fatedier
443b9bca66 server: introduce SessionContext to encapsulate NewControl parameters
Replace 10 positional parameters in NewControl() with a single
SessionContext struct, matching the client-side pattern. This also
eliminates the post-construction mutation of clientRegistry and
removes two TODO comments.
2026-03-07 18:26:56 +08:00
208 changed files with 14164 additions and 10198 deletions

View File

@@ -19,15 +19,15 @@ jobs:
steps: steps:
# environment # environment
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: '0' fetch-depth: '0'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
# 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@v4 uses: docker/login-action@v3
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@v4 uses: docker/login-action@v3
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@v7 uses: docker/build-push-action@v5
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@v7 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./dockerfiles/Dockerfile-for-frps file: ./dockerfiles/Dockerfile-for-frps

View File

@@ -14,12 +14,12 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version: '1.25' go-version: '1.25'
cache: false cache: false
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
- name: Build web assets (frps) - name: Build web assets (frps)

View File

@@ -8,15 +8,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: '1.25' go-version: '1.25'
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
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@v7 uses: goreleaser/goreleaser-action@v6
with: with:
version: latest version: latest
args: release --clean --release-notes=./Release.md args: release --clean --release-notes=./Release.md

View File

@@ -19,7 +19,7 @@ jobs:
actions: write actions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v9
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
View File

@@ -25,12 +25,10 @@ dist/
client.crt client.crt
client.key client.key
node_modules/
# Cache # Cache
*.swp *.swp
# AI # AI
.claude/ CLAUDE.md
AGENTS.md
.sisyphus/ .sisyphus/
.superpowers/

View File

@@ -18,7 +18,6 @@ linters:
- lll - lll
- makezero - makezero
- misspell - misspell
- modernize
- prealloc - prealloc
- predeclared - predeclared
- revive - revive
@@ -48,9 +47,6 @@ linters:
ignore-rules: ignore-rules:
- cancelled - cancelled
- marshalled - marshalled
modernize:
disable:
- omitzero
unparam: unparam:
check-exported: false check-exported: false
exclusions: exclusions:
@@ -90,7 +86,6 @@ linters:
- third_party$ - third_party$
- builtin$ - builtin$
- examples$ - examples$
- node_modules
formatters: formatters:
enable: enable:
- gci - gci
@@ -113,7 +108,6 @@ 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

View File

@@ -1,39 +0,0 @@
# 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

View File

@@ -1 +0,0 @@
AGENTS.md

View File

@@ -13,16 +13,6 @@ 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
@@ -50,6 +40,16 @@ 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,7 +81,6 @@ 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)
@@ -150,9 +149,7 @@ We sincerely appreciate your support for frp.
## Architecture ## Architecture
<p align="center"> ![architecture](/doc/pic/architecture.png)
<img src="/doc/pic/architecture.jpg" alt="architecture" width="760">
</p>
## Example Usage ## Example Usage
@@ -596,7 +593,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 and proxies. The Client Admin UI helps you check and manage frpc's configuration.
Configure an address for admin UI to enable this feature: Configure an address for admin UI to enable this feature:
@@ -609,19 +606,6 @@ 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.

View File

@@ -15,16 +15,6 @@ 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
@@ -52,6 +42,16 @@ 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

View File

@@ -1,3 +1,9 @@
## Fixes ## Features
* Fixed a configuration-dependent authentication bypass in `type = "http"` proxies when `routeByHTTPUser` is used together with `httpUser` / `httpPassword`. This affected proxy-style requests. Proxy-style authentication failures now return `407 Proxy Authentication Required`. * Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
## Improvements
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.

View File

@@ -38,8 +38,6 @@ 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)

View File

@@ -80,48 +80,6 @@ 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

View File

@@ -26,9 +26,6 @@ type ConfigManager interface {
IsStoreProxyEnabled(name string) bool IsStoreProxyEnabled(name string) bool
StoreEnabled() bool StoreEnabled() bool
GetProxyConfig(name string) (v1.ProxyConfigurer, bool)
GetVisitorConfig(name string) (v1.VisitorConfigurer, bool)
ListStoreProxies() ([]v1.ProxyConfigurer, error) ListStoreProxies() ([]v1.ProxyConfigurer, error)
GetStoreProxy(name string) (v1.ProxyConfigurer, error) GetStoreProxy(name string) (v1.ProxyConfigurer, error)
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)

View File

@@ -119,7 +119,6 @@ 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

View File

@@ -162,44 +162,6 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.Pro
return psr return psr
} }
// GetProxyConfig handles GET /api/proxy/{name}/config
func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) {
name := ctx.Param("name")
if name == "" {
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
}
cfg, ok := c.manager.GetProxyConfig(name)
if !ok {
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name))
}
payload, err := model.ProxyDefinitionFromConfigurer(cfg)
if err != nil {
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
}
return payload, nil
}
// GetVisitorConfig handles GET /api/visitor/{name}/config
func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) {
name := ctx.Param("name")
if name == "" {
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
}
cfg, ok := c.manager.GetVisitorConfig(name)
if !ok {
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name))
}
payload, err := model.VisitorDefinitionFromConfigurer(cfg)
if err != nil {
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
}
return payload, nil
}
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) { func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
proxies, err := c.manager.ListStoreProxies() proxies, err := c.manager.ListStoreProxies()
if err != nil { if err != nil {

View File

@@ -26,8 +26,6 @@ type fakeConfigManager struct {
getProxyStatusFn func() []*proxy.WorkingStatus getProxyStatusFn func() []*proxy.WorkingStatus
isStoreProxyEnabledFn func(name string) bool isStoreProxyEnabledFn func(name string) bool
storeEnabledFn func() bool storeEnabledFn func() bool
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error) listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error) getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
@@ -84,20 +82,6 @@ func (m *fakeConfigManager) StoreEnabled() bool {
return false return false
} }
func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
if m.getProxyConfigFn != nil {
return m.getProxyConfigFn(name)
}
return nil, false
}
func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
if m.getVisitorConfigFn != nil {
return m.getVisitorConfigFn(name)
}
return nil, false
}
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
if m.listStoreProxiesFn != nil { if m.listStoreProxiesFn != nil {
return m.listStoreProxiesFn() return m.listStoreProxiesFn()
@@ -545,118 +529,3 @@ func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
t.Fatalf("unexpected response payload: %#v", payload) t.Fatalf("unexpected response payload: %#v", payload)
} }
} }
func TestGetProxyConfigFromManager(t *testing.T) {
controller := &Controller{
manager: &fakeConfigManager{
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
if name == "ssh" {
cfg := &v1.TCPProxyConfig{
ProxyBaseConfig: v1.ProxyBaseConfig{
Name: "ssh",
Type: "tcp",
ProxyBackend: v1.ProxyBackend{
LocalPort: 22,
},
},
}
return cfg, true
}
return nil, false
},
},
}
req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil)
req = mux.SetURLVars(req, map[string]string{"name": "ssh"})
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
resp, err := controller.GetProxyConfig(ctx)
if err != nil {
t.Fatalf("get proxy config: %v", err)
}
payload, ok := resp.(model.ProxyDefinition)
if !ok {
t.Fatalf("unexpected response type: %T", resp)
}
if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil {
t.Fatalf("unexpected payload: %#v", payload)
}
}
func TestGetProxyConfigNotFound(t *testing.T) {
controller := &Controller{
manager: &fakeConfigManager{
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
return nil, false
},
},
}
req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil)
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
_, err := controller.GetProxyConfig(ctx)
if err == nil {
t.Fatal("expected error")
}
assertHTTPCode(t, err, http.StatusNotFound)
}
func TestGetVisitorConfigFromManager(t *testing.T) {
controller := &Controller{
manager: &fakeConfigManager{
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
if name == "my-stcp" {
cfg := &v1.STCPVisitorConfig{
VisitorBaseConfig: v1.VisitorBaseConfig{
Name: "my-stcp",
Type: "stcp",
ServerName: "server1",
BindPort: 9000,
},
}
return cfg, true
}
return nil, false
},
},
}
req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil)
req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"})
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
resp, err := controller.GetVisitorConfig(ctx)
if err != nil {
t.Fatalf("get visitor config: %v", err)
}
payload, ok := resp.(model.VisitorDefinition)
if !ok {
t.Fatalf("unexpected response type: %T", resp)
}
if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil {
t.Fatalf("unexpected payload: %#v", payload)
}
}
func TestGetVisitorConfigNotFound(t *testing.T) {
controller := &Controller{
manager: &fakeConfigManager{
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
return nil, false
},
},
}
req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil)
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
_, err := controller.GetVisitorConfig(ctx)
if err == nil {
t.Fatal("expected error")
}
assertHTTPCode(t, err, http.StatusNotFound)
}

View File

@@ -32,7 +32,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
} }
type SUDPProxy struct { type SUDPProxy struct {

View File

@@ -31,7 +31,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
} }
type UDPProxy struct { type UDPProxy struct {

View File

@@ -34,7 +34,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
} }
type XTCPProxy struct { type XTCPProxy struct {

View File

@@ -19,7 +19,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"os" "os"
"runtime" "runtime"
"sync" "sync"
@@ -163,6 +162,15 @@ 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
@@ -183,17 +191,6 @@ 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,
@@ -232,25 +229,22 @@ 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 := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) { if err := svr.vnetController.Run(); err != nil {
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", webServer.Address()) log.Infof("admin server listen on %s", svr.webServer.Address())
if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := svr.webServer.Run(); err != nil {
log.Warnf("admin server exit with error: %v", err) log.Warnf("admin server exit with error: %v", err)
} }
}() }()
@@ -261,7 +255,6 @@ 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)
} }
@@ -504,10 +497,6 @@ 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) {
@@ -521,17 +510,6 @@ 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,

View File

@@ -1,120 +1,14 @@
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"}

View File

@@ -15,12 +15,18 @@
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"
) )
@@ -55,6 +61,7 @@ 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)
@@ -65,21 +72,62 @@ 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.dialRawVisitorConn(sv.cfg.GetBaseConfig()) visitorConn, err := sv.helper.ConnectServer()
if err != nil { if err != nil {
xl.Warnf("dialRawVisitorConn error: %v", err)
tunnelErr = err tunnelErr = err
return return
} }
defer visitorConn.Close() defer visitorConn.Close()
remote, recycleFn, err := wrapVisitorConn(visitorConn, 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 {
xl.Warnf("wrapVisitorConn error: %v", err) xl.Warnf("send newVisitorConnMsg to server 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)
} }

View File

@@ -16,17 +16,21 @@ 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"
) )
@@ -72,7 +76,6 @@ func (sv *SUDPVisitor) dispatcher() {
var ( var (
visitorConn net.Conn visitorConn net.Conn
recycleFn func()
err error err error
firstPacket *msg.UDPPacket firstPacket *msg.UDPPacket
@@ -90,17 +93,14 @@ func (sv *SUDPVisitor) dispatcher() {
return return
} }
visitorConn, recycleFn, err = sv.getNewVisitorConn() visitorConn, err = sv.getNewVisitorConn()
if err != nil { if err != nil {
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err) xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
continue continue
} }
// visitorConn always be closed when worker done. // visitorConn always be closed when worker done.
func() { sv.worker(visitorConn, firstPacket)
defer recycleFn()
sv.worker(visitorConn, firstPacket)
}()
select { select {
case <-sv.checkCloseCh: case <-sv.checkCloseCh:
@@ -198,17 +198,57 @@ 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, func(), error) { func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig()) xl := xlog.FromContextSafe(sv.ctx)
visitorConn, err := sv.helper.ConnectServer()
if err != nil { if err != nil {
return nil, func() {}, err return nil, fmt.Errorf("frpc connect frps error: %v", 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 {
rawConn.Close() visitorConn.Close()
return nil, func() {}, err return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", 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 {
visitorConn.Close()
return nil, fmt.Errorf("frpc 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)
}
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)
visitorConn.Close()
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() {

View File

@@ -16,21 +16,13 @@ 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"
) )
@@ -150,57 +142,3 @@ 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
}

View File

@@ -191,13 +191,6 @@ 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

View File

@@ -182,14 +182,21 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
return return
} }
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig()) var muxConnRWCloser io.ReadWriteCloser = tunnelConn
if err != nil { if sv.cfg.Transport.UseEncryption {
xl.Errorf("%v", err) muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
tunnelConn.Close() if err != nil {
tunnelErr = err xl.Errorf("create encryption stream error: %v", err)
return tunnelConn.Close()
tunnelErr = err
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")

View File

@@ -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 parameters // ignore error here, because we can use command line pameters
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
cfg = &v1.ClientCommonConfig{} cfg = &v1.ClientCommonConfig{}

View File

@@ -1,80 +0,0 @@
# 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`)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

BIN
doc/pic/architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,11 +1,8 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
COPY web/package.json /web/package.json
COPY web/shared/ /web/shared/
COPY web/frpc/ /web/frpc/
WORKDIR /web
RUN npm install
WORKDIR /web/frpc WORKDIR /web/frpc
COPY web/frpc/ ./
RUN npm install
RUN npm run build RUN npm run build
FROM golang:1.25 AS building FROM golang:1.25 AS building

View File

@@ -1,11 +1,8 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
COPY web/package.json /web/package.json
COPY web/shared/ /web/shared/
COPY web/frps/ /web/frps/
WORKDIR /web
RUN npm install
WORKDIR /web/frps WORKDIR /web/frps
COPY web/frps/ ./
RUN npm install
RUN npm run build RUN npm run build
FROM golang:1.25 AS building FROM golang:1.25 AS building

32
go.mod
View File

@@ -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.6.0 github.com/fatedier/golib v0.5.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
@@ -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/v3 v3.1.1 github.com/pion/stun/v2 v2.0.0
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.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.11.1 github.com/stretchr/testify v1.10.0
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.49.0 golang.org/x/crypto v0.41.0
golang.org/x/net v0.52.0 golang.org/x/net v0.43.0
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.16.0
golang.org/x/time v0.10.0 golang.org/x/time v0.5.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.1.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // 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,9 +51,10 @@ 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/v3 v3.0.10 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/transport/v3 v3.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/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
@@ -65,12 +66,11 @@ 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.33.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/tools v0.36.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
View File

@@ -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.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/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.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww= github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 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,14 +78,16 @@ 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/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0 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=
@@ -126,10 +128,11 @@ 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= github.com/templexxx/cpu v0.1.1 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=
@@ -146,12 +149,11 @@ 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=
@@ -159,54 +161,89 @@ 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.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=

View File

@@ -30,7 +30,6 @@ import (
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
) )
@@ -76,64 +75,14 @@ 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
tokenSource oauth2.TokenSource tokenGenerator *clientcredentials.Config
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}
@@ -151,42 +100,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
EndpointParams: eps, EndpointParams: eps,
} }
// Build the context that TokenSource will use for all future HTTP requests. // Create custom HTTP client if needed
// context.Background() is appropriate here because the token source is var httpClient *http.Client
// long-lived and outlives any single request.
ctx := context.Background()
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" { if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) var err error
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,
tokenSource: &oidcTokenSource{ tokenGenerator: tokenGenerator,
source: cachingSource, httpClient: httpClient,
fallbackCfg: tokenGenerator,
fallbackCtx: ctx,
},
}, nil }, nil
} }
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
tokenObj, err := auth.tokenSource.Token() ctx := context.Background()
if auth.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
}
tokenObj, err := auth.tokenGenerator.Token(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
} }

View File

@@ -2,10 +2,6 @@ package auth_test
import ( import (
"context" "context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -66,188 +62,3 @@ 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")
}

View File

@@ -39,14 +39,14 @@ const (
// Proxy // Proxy
var ( var (
proxyConfTypeMap = map[ProxyType]reflect.Type{ proxyConfTypeMap = map[ProxyType]reflect.Type{
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](), ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](), ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](), ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](), ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](), ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](), ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](), ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](), ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
} }
) )

View File

@@ -32,9 +32,9 @@ const (
// Visitor // Visitor
var ( var (
visitorConfTypeMap = map[VisitorType]reflect.Type{ visitorConfTypeMap = map[VisitorType]reflect.Type{
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](), VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](), VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](), VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
} }
) )

View File

@@ -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 := range firstRangeNumbers { for i := 0; i < len(firstRangeNumbers); i++ {
pairs = append(pairs, NumberPair{ pairs = append(pairs, NumberPair{
First: firstRangeNumbers[i], First: firstRangeNumbers[i],
Second: secondRangeNumbers[i], Second: secondRangeNumbers[i],

View File

@@ -137,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.SplitSeq(str, ",") numRanges := strings.Split(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

View File

@@ -239,14 +239,14 @@ const (
) )
var proxyConfigTypeMap = map[ProxyType]reflect.Type{ var proxyConfigTypeMap = map[ProxyType]reflect.Type{
ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](), ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}),
ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](), ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}),
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](), ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}),
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](), ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}),
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](), ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](), ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}),
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](), ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}),
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](), ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}),
} }
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer { func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {

View File

@@ -37,16 +37,16 @@ const (
) )
var clientPluginOptionsTypeMap = map[string]reflect.Type{ var clientPluginOptionsTypeMap = map[string]reflect.Type{
PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](), PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](), PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](), PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](), PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](), PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
PluginSocks5: reflect.TypeFor[Socks5PluginOptions](), PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](), PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](), PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](), PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](), PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
} }
type ClientPluginOptions interface { type ClientPluginOptions interface {

View File

@@ -88,11 +88,6 @@ 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
} }

View File

@@ -1,57 +0,0 @@
// 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, "; "))
}

View File

@@ -1,78 +0,0 @@
// 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")
})
}

View File

@@ -79,9 +79,9 @@ const (
) )
var visitorConfigTypeMap = map[VisitorType]reflect.Type{ var visitorConfigTypeMap = map[VisitorType]reflect.Type{
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](), VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](), VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](), VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
} }
type TypedVisitorConfig struct { type TypedVisitorConfig struct {

View File

@@ -25,7 +25,7 @@ const (
) )
var visitorPluginOptionsTypeMap = map[string]reflect.Type{ var visitorPluginOptionsTypeMap = map[string]reflect.Type{
VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](), VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
} }
type VisitorPluginOptions interface { type VisitorPluginOptions interface {

View File

@@ -143,6 +143,7 @@ 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
} }
} }
@@ -154,6 +155,7 @@ 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
} }
} }
@@ -166,6 +168,7 @@ 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
} }
} }
@@ -178,6 +181,7 @@ 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
} }
} }
@@ -236,9 +240,15 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
proxyStats, ok := m.info.ProxyStatistics[proxyName] for name, proxyStats := range m.info.ProxyStatistics {
if ok && proxyStats.ProxyType == proxyType { if proxyStats.ProxyType != proxyType {
res = toProxyStats(proxyName, proxyStats) continue
}
if name != proxyName {
continue
}
res = toProxyStats(name, proxyStats)
break
} }
return return
} }

View File

@@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{
TypeNatHoleReport: NatHoleReport{}, TypeNatHoleReport: NatHoleReport{},
} }
var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name() var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().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

View File

@@ -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 := range behaviors { for i := 0; i < len(behaviors); i++ {
score := receiverScore score := receiverScore
if behaviors[i].A.Role == DetectRoleSender { if behaviors[i].A.Role == DetectRoleSender {
score = senderScore score = senderScore

View File

@@ -19,7 +19,7 @@ import (
"net" "net"
"time" "time"
"github.com/pion/stun/v3" "github.com/pion/stun/v2"
) )
var responseTimeout = 3 * time.Second var responseTimeout = 3 * time.Second

View File

@@ -410,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 range 10 { for i := 0; i < 10; i++ {
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)
@@ -420,7 +420,7 @@ func sendSidMessageToRandomPorts(
return 0 return 0
} }
for range count { for i := 0; i < count; i++ {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return

View File

@@ -21,7 +21,7 @@ import (
"strconv" "strconv"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
"github.com/pion/stun/v3" "github.com/pion/stun/v2"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
) )

View File

@@ -153,7 +153,10 @@ 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 = min(baseDelay*time.Duration(1<<uint(p.consecutiveErrors-1)), 300*time.Second) reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
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 {

View File

@@ -16,7 +16,6 @@ package featuregate
import ( import (
"fmt" "fmt"
"maps"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -93,7 +92,10 @@ 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 := maps.Clone(defaultFeatures) known := map[Feature]FeatureSpec{}
for k, v := range defaultFeatures {
known[k] = v
}
f := &featureGate{} f := &featureGate{}
f.known.Store(known) f.known.Store(known)
@@ -107,8 +109,14 @@ func (f *featureGate) SetFromMap(m map[string]bool) error {
defer f.lock.Unlock() defer f.lock.Unlock()
// Copy existing state // Copy existing state
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) known := map[Feature]FeatureSpec{}
enabled := maps.Clone(f.enabled.Load().(map[Feature]bool)) for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
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 {
@@ -139,7 +147,10 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
} }
// Copy existing state // Copy existing state
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) known := 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 {

View File

@@ -89,11 +89,11 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) {
return return
} }
cs := string(c) cs := string(c)
before, after, found := strings.Cut(cs, ":") s := strings.IndexByte(cs, ':')
if !found { if s < 0 {
return return
} }
return before, after, true return cs[:s], cs[s+1:], true
} }
func BasicAuth(username, passwd string) string { func BasicAuth(username, passwd string) string {

View File

@@ -100,11 +100,7 @@ func (s *Server) Run() error {
} }
func (s *Server) Close() error { func (s *Server) Close() error {
err := s.hs.Close() return s.hs.Close()
if s.ln != nil {
_ = s.ln.Close()
}
return err
} }
type RouterRegisterHelper struct { type RouterRegisterHelper struct {

View File

@@ -86,7 +86,11 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) {
c.lastActive = time.Now() c.lastActive = time.Now()
c.mu.Unlock() c.mu.Unlock()
n = min(len(b), len(content)) if len(b) < len(content) {
n = len(b)
} else {
n = len(content)
}
copy(b, content) copy(b, content)
return n, nil return n, nil
} }

View File

@@ -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.SplitSeq(rangeStr, ",") numRanges := strings.Split(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

View File

@@ -14,7 +14,7 @@
package version package version
var version = "0.68.1" var version = "0.68.0"
func Full() string { func Full() string {
return version return version

View File

@@ -187,25 +187,16 @@ func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byE
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser) return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
} }
func checkRouteAuthByRequest(req *http.Request, rc *RouteConfig) bool { func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
if rc == nil { vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
return true if ok {
} checkUser := vr.payload.(*RouteConfig).Username
if rc.Username == "" && rc.Password == "" { checkPasswd := vr.payload.(*RouteConfig).Password
return true if (checkUser != "" || checkPasswd != "") && (checkUser != user || checkPasswd != passwd) {
}
if req.URL.Host != "" {
proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth == "" {
return false return false
} }
user, passwd, ok := httppkg.ParseBasicAuth(proxyAuth)
return ok && user == rc.Username && passwd == rc.Password
} }
return true
user, passwd, ok := req.BasicAuth()
return ok && user == rc.Username && passwd == rc.Password
} }
// getVhost tries to get vhost router by route policy. // getVhost tries to get vhost router by route policy.
@@ -275,25 +266,18 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
go libio.Join(remote, client) go libio.Join(remote, client)
} }
func getRequestRouteUser(req *http.Request) string { func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
user := ""
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
if req.URL.Host != "" { if req.URL.Host != "" {
proxyAuth := req.Header.Get("Proxy-Authorization") proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth == "" { if proxyAuth != "" {
// Preserve legacy proxy-mode routing when clients send only Authorization, user, _, _ = httppkg.ParseBasicAuth(proxyAuth)
// so requests still hit the matched route and return 407 instead of 404.
// Auth validation intentionally does not share this fallback.
user, _, _ := req.BasicAuth()
return user
} }
user, _, _ := httppkg.ParseBasicAuth(proxyAuth)
return user
} }
user, _, _ := req.BasicAuth() if user == "" {
return user user, _, _ = req.BasicAuth()
} }
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
user := getRequestRouteUser(req)
reqRouteInfo := &RequestRouteInfo{ reqRouteInfo := &RequestRouteInfo{
URL: req.URL.Path, URL: req.URL.Path,
@@ -313,19 +297,16 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
} }
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
newreq := rp.injectRequestInfoToCtx(req) domain, _ := httppkg.CanonicalHost(req.Host)
rc := newreq.Context().Value(RouteConfigKey).(*RouteConfig) location := req.URL.Path
if !checkRouteAuthByRequest(req, rc) { user, passwd, _ := req.BasicAuth()
if req.URL.Host != "" { if !rp.CheckAuth(domain, location, user, user, passwd) {
rw.Header().Set("Proxy-Authenticate", `Basic realm="Restricted"`) rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(http.StatusProxyAuthRequired), http.StatusProxyAuthRequired) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
} else {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
return return
} }
newreq := rp.injectRequestInfoToCtx(req)
if req.Method == http.MethodConnect { if req.Method == http.MethodConnect {
rp.connectHandler(rw, newreq) rp.connectHandler(rw, newreq)
} else { } else {

View File

@@ -1,102 +0,0 @@
package vhost
import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
httppkg "github.com/fatedier/frp/pkg/util/http"
)
func TestCheckRouteAuthByRequest(t *testing.T) {
rc := &RouteConfig{
Username: "alice",
Password: "secret",
}
t.Run("accepts nil route config", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, nil))
})
t.Run("accepts route without credentials", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, &RouteConfig{}))
})
t.Run("accepts authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.SetBasicAuth("alice", "secret")
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("accepts proxy authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects authorization fallback for proxy request", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects when neither header matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "wrong")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects proxy authorization on direct request", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
}
func TestGetRequestRouteUser(t *testing.T) {
t.Run("proxy request uses proxy authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("connect request keeps proxy authorization routing", func(t *testing.T) {
req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil)
req.Host = "target.example.com:443"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("direct request uses authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "direct-user", getRequestRouteUser(req))
})
t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", "Basic !!!")
req.SetBasicAuth("direct-user", "direct-pass")
require.Empty(t, getRequestRouteUser(req))
})
}

View File

@@ -131,9 +131,6 @@ 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()
} }

View File

@@ -158,7 +158,10 @@ type Control struct {
} }
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount)) poolCount := sessionCtx.LoginMsg.PoolCount
if poolCount > int(sessionCtx.ServerCfg.Transport.MaxPoolCount) {
poolCount = int(sessionCtx.ServerCfg.Transport.MaxPoolCount)
}
ctl := &Control{ ctl := &Control{
sessionCtx: sessionCtx, sessionCtx: sessionCtx,
workConnCh: make(chan net.Conn, poolCount+10), workConnCh: make(chan net.Conn, poolCount+10),
@@ -303,30 +306,6 @@ 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
@@ -337,16 +316,31 @@ func (ctl *Control) worker() {
ctl.sessionCtx.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 proxies { for _, pxy := range ctl.proxies {
ctl.closeProxy(pxy) pxy.Close()
ctl.sessionCtx.PxyManager.Del(pxy.GetName())
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
notifyContent := &plugin.CloseProxyContent{
User: plugin.UserInfo{
User: ctl.sessionCtx.LoginMsg.User,
Metas: ctl.sessionCtx.LoginMsg.Metas,
RunID: ctl.sessionCtx.LoginMsg.RunID,
},
CloseProxy: msg.CloseProxy{
ProxyName: pxy.GetName(),
},
}
go func() {
_ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent)
}()
} }
metrics.Server.CloseClient() metrics.Server.CloseClient()
@@ -369,7 +363,11 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
inMsg := m.(*msg.NewProxy) inMsg := m.(*msg.NewProxy)
content := &plugin.NewProxyContent{ content := &plugin.NewProxyContent{
User: ctl.loginUserInfo(), User: plugin.UserInfo{
User: ctl.sessionCtx.LoginMsg.User,
Metas: ctl.sessionCtx.LoginMsg.Metas,
RunID: ctl.sessionCtx.LoginMsg.RunID,
},
NewProxy: *inMsg, NewProxy: *inMsg,
} }
var remoteAddr string var remoteAddr string
@@ -404,7 +402,11 @@ func (ctl *Control) handlePing(m msg.Message) {
inMsg := m.(*msg.Ping) inMsg := m.(*msg.Ping)
content := &plugin.PingContent{ content := &plugin.PingContent{
User: ctl.loginUserInfo(), User: plugin.UserInfo{
User: ctl.sessionCtx.LoginMsg.User,
Metas: ctl.sessionCtx.LoginMsg.Metas,
RunID: ctl.sessionCtx.LoginMsg.RunID,
},
Ping: *inMsg, Ping: *inMsg,
} }
retContent, err := ctl.sessionCtx.PluginManager.Ping(content) retContent, err := ctl.sessionCtx.PluginManager.Ping(content)
@@ -534,9 +536,25 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 {
ctl.portsUsedNum -= pxy.GetUsedPortsNum() ctl.portsUsedNum -= pxy.GetUsedPortsNum()
} }
pxy.Close()
ctl.sessionCtx.PxyManager.Del(pxy.GetName())
delete(ctl.proxies, closeMsg.ProxyName) delete(ctl.proxies, closeMsg.ProxyName)
ctl.mu.Unlock() ctl.mu.Unlock()
ctl.closeProxy(pxy) metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
notifyContent := &plugin.CloseProxyContent{
User: plugin.UserInfo{
User: ctl.sessionCtx.LoginMsg.User,
Metas: ctl.sessionCtx.LoginMsg.Metas,
RunID: ctl.sessionCtx.LoginMsg.RunID,
},
CloseProxy: msg.CloseProxy{
ProxyName: pxy.GetName(),
},
}
go func() {
_ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent)
}()
return return
} }

View File

@@ -1,77 +0,0 @@
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()
}
}

View File

@@ -1,169 +0,0 @@
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)
}

View File

@@ -24,6 +24,4 @@ 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")
) )

View File

@@ -9,42 +9,53 @@ import (
"github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/pkg/util/vhost"
) )
// HTTPGroupController manages HTTP groups that use round-robin
// callback routing (fundamentally different from listener-based groups).
type HTTPGroupController struct { type HTTPGroupController struct {
groupRegistry[*HTTPGroup] // groups indexed by group name
groups map[string]*HTTPGroup
// register createConn for each group to vhostRouter.
// createConn will get a connection from one proxy of the group
vhostRouter *vhost.Routers vhostRouter *vhost.Routers
mu sync.Mutex
} }
func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController { func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
return &HTTPGroupController{ return &HTTPGroupController{
groupRegistry: newGroupRegistry[*HTTPGroup](), groups: make(map[string]*HTTPGroup),
vhostRouter: vhostRouter, vhostRouter: vhostRouter,
} }
} }
func (ctl *HTTPGroupController) Register( func (ctl *HTTPGroupController) Register(
proxyName, group, groupKey string, proxyName, group, groupKey string,
routeConfig vhost.RouteConfig, routeConfig vhost.RouteConfig,
) error { ) (err error) {
for { indexKey := group
g := ctl.getOrCreate(group, func() *HTTPGroup { ctl.mu.Lock()
return NewHTTPGroup(ctl) g, ok := ctl.groups[indexKey]
}) if !ok {
err := g.Register(proxyName, group, groupKey, routeConfig) g = NewHTTPGroup(ctl)
if err == errGroupStale { ctl.groups[indexKey] = g
continue
}
return err
} }
ctl.mu.Unlock()
return g.Register(proxyName, group, groupKey, routeConfig)
} }
func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) { func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) {
g, ok := ctl.get(group) indexKey := group
ctl.mu.Lock()
defer ctl.mu.Unlock()
g, ok := ctl.groups[indexKey]
if !ok { if !ok {
return return
} }
g.UnRegister(proxyName)
isEmpty := g.UnRegister(proxyName)
if isEmpty {
delete(ctl.groups, indexKey)
}
} }
type HTTPGroup struct { type HTTPGroup struct {
@@ -76,9 +87,6 @@ func (g *HTTPGroup) Register(
) (err error) { ) (err error) {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
if !g.ctl.isCurrent(group, func(cur *HTTPGroup) bool { return cur == g }) {
return errGroupStale
}
if len(g.createFuncs) == 0 { if len(g.createFuncs) == 0 {
// the first proxy in this group // the first proxy in this group
tmp := routeConfig // copy object tmp := routeConfig // copy object
@@ -115,7 +123,7 @@ func (g *HTTPGroup) Register(
return nil return nil
} }
func (g *HTTPGroup) UnRegister(proxyName string) { func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
delete(g.createFuncs, proxyName) delete(g.createFuncs, proxyName)
@@ -127,11 +135,10 @@ func (g *HTTPGroup) UnRegister(proxyName string) {
} }
if len(g.createFuncs) == 0 { if len(g.createFuncs) == 0 {
isEmpty = true
g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser) g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser)
g.ctl.removeIf(g.group, func(cur *HTTPGroup) bool {
return cur == g
})
} }
return
} }
func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
@@ -144,7 +151,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
location := g.location location := g.location
routeByHTTPUser := g.routeByHTTPUser routeByHTTPUser := g.routeByHTTPUser
if len(g.pxyNames) > 0 { if len(g.pxyNames) > 0 {
name := g.pxyNames[newIndex%uint64(len(g.pxyNames))] name := g.pxyNames[int(newIndex)%len(g.pxyNames)]
f = g.createFuncs[name] f = g.createFuncs[name]
} }
g.mu.RUnlock() g.mu.RUnlock()
@@ -167,7 +174,7 @@ func (g *HTTPGroup) chooseEndpoint() (string, error) {
location := g.location location := g.location
routeByHTTPUser := g.routeByHTTPUser routeByHTTPUser := g.routeByHTTPUser
if len(g.pxyNames) > 0 { if len(g.pxyNames) > 0 {
name = g.pxyNames[newIndex%uint64(len(g.pxyNames))] name = g.pxyNames[int(newIndex)%len(g.pxyNames)]
} }
g.mu.RUnlock() g.mu.RUnlock()

View File

@@ -17,19 +17,25 @@ package group
import ( import (
"context" "context"
"net" "net"
"sync"
gerr "github.com/fatedier/golib/errors"
"github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/pkg/util/vhost"
) )
type HTTPSGroupController struct { type HTTPSGroupController struct {
groupRegistry[*HTTPSGroup] groups map[string]*HTTPSGroup
httpsMuxer *vhost.HTTPSMuxer httpsMuxer *vhost.HTTPSMuxer
mu sync.Mutex
} }
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController { func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
return &HTTPSGroupController{ return &HTTPSGroupController{
groupRegistry: newGroupRegistry[*HTTPSGroup](), groups: make(map[string]*HTTPSGroup),
httpsMuxer: httpsMuxer, httpsMuxer: httpsMuxer,
} }
} }
@@ -38,28 +44,41 @@ func (ctl *HTTPSGroupController) Listen(
group, groupKey string, group, groupKey string,
routeConfig vhost.RouteConfig, routeConfig vhost.RouteConfig,
) (l net.Listener, err error) { ) (l net.Listener, err error) {
for { indexKey := group
g := ctl.getOrCreate(group, func() *HTTPSGroup { ctl.mu.Lock()
return NewHTTPSGroup(ctl) g, ok := ctl.groups[indexKey]
}) if !ok {
l, err = g.Listen(ctx, group, groupKey, routeConfig) g = NewHTTPSGroup(ctl)
if err == errGroupStale { ctl.groups[indexKey] = g
continue
}
return
} }
ctl.mu.Unlock()
return g.Listen(ctx, group, groupKey, routeConfig)
}
func (ctl *HTTPSGroupController) RemoveGroup(group string) {
ctl.mu.Lock()
defer ctl.mu.Unlock()
delete(ctl.groups, group)
} }
type HTTPSGroup struct { type HTTPSGroup struct {
baseGroup group string
groupKey string
domain string
domain string acceptCh chan net.Conn
ctl *HTTPSGroupController httpsLn *vhost.Listener
lns []*HTTPSGroupListener
ctl *HTTPSGroupController
mu sync.Mutex
} }
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup { func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
return &HTTPSGroup{ return &HTTPSGroup{
ctl: ctl, lns: make([]*HTTPSGroupListener, 0),
ctl: ctl,
acceptCh: make(chan net.Conn),
} }
} }
@@ -67,27 +86,23 @@ func (g *HTTPSGroup) Listen(
ctx context.Context, ctx context.Context,
group, groupKey string, group, groupKey string,
routeConfig vhost.RouteConfig, routeConfig vhost.RouteConfig,
) (ln *Listener, err error) { ) (ln *HTTPSGroupListener, err error) {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
if !g.ctl.isCurrent(group, func(cur *HTTPSGroup) bool { return cur == g }) {
return nil, errGroupStale
}
if len(g.lns) == 0 { if len(g.lns) == 0 {
// the first listener, listen on the real address // the first listener, listen on the real address
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig) httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
if errRet != nil { if errRet != nil {
return nil, errRet return nil, errRet
} }
ln = newHTTPSGroupListener(group, g, httpsLn.Addr())
g.group = group
g.groupKey = groupKey
g.domain = routeConfig.Domain g.domain = routeConfig.Domain
g.initBase(group, groupKey, httpsLn, func() { g.httpsLn = httpsLn
g.ctl.removeIf(g.group, func(cur *HTTPSGroup) bool { g.lns = append(g.lns, ln)
return cur == g go g.worker()
})
})
ln = g.newListener(httpsLn.Addr())
go g.worker(httpsLn, g.acceptCh)
} else { } else {
// route config in the same group must be equal // route config in the same group must be equal
if g.group != group || g.domain != routeConfig.Domain { if g.group != group || g.domain != routeConfig.Domain {
@@ -96,7 +111,87 @@ func (g *HTTPSGroup) Listen(
if g.groupKey != groupKey { if g.groupKey != groupKey {
return nil, ErrGroupAuthFailed return nil, ErrGroupAuthFailed
} }
ln = g.newListener(g.lns[0].Addr()) ln = newHTTPSGroupListener(group, g, g.lns[0].Addr())
g.lns = append(g.lns, ln)
} }
return return
} }
func (g *HTTPSGroup) worker() {
for {
c, err := g.httpsLn.Accept()
if err != nil {
return
}
err = gerr.PanicToError(func() {
g.acceptCh <- c
})
if err != nil {
return
}
}
}
func (g *HTTPSGroup) Accept() <-chan net.Conn {
return g.acceptCh
}
func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) {
g.mu.Lock()
defer g.mu.Unlock()
for i, tmpLn := range g.lns {
if tmpLn == ln {
g.lns = append(g.lns[:i], g.lns[i+1:]...)
break
}
}
if len(g.lns) == 0 {
close(g.acceptCh)
if g.httpsLn != nil {
g.httpsLn.Close()
}
g.ctl.RemoveGroup(g.group)
}
}
type HTTPSGroupListener struct {
groupName string
group *HTTPSGroup
addr net.Addr
closeCh chan struct{}
}
func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener {
return &HTTPSGroupListener{
groupName: name,
group: group,
addr: addr,
closeCh: make(chan struct{}),
}
}
func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) {
var ok bool
select {
case <-ln.closeCh:
return nil, ErrListenerClosed
case c, ok = <-ln.group.Accept():
if !ok {
return nil, ErrListenerClosed
}
return c, nil
}
}
func (ln *HTTPSGroupListener) Addr() net.Addr {
return ln.addr
}
func (ln *HTTPSGroupListener) Close() (err error) {
close(ln.closeCh)
// remove self from HTTPSGroup
ln.group.CloseListener(ln)
return
}

View File

@@ -1,49 +0,0 @@
package group
import (
"net"
"sync"
)
// Listener is a per-proxy virtual listener that receives connections
// from a shared group. It implements net.Listener.
type Listener struct {
acceptCh <-chan net.Conn
addr net.Addr
closeCh chan struct{}
onClose func(*Listener)
once sync.Once
}
func newListener(acceptCh <-chan net.Conn, addr net.Addr, onClose func(*Listener)) *Listener {
return &Listener{
acceptCh: acceptCh,
addr: addr,
closeCh: make(chan struct{}),
onClose: onClose,
}
}
func (ln *Listener) Accept() (net.Conn, error) {
select {
case <-ln.closeCh:
return nil, ErrListenerClosed
case c, ok := <-ln.acceptCh:
if !ok {
return nil, ErrListenerClosed
}
return c, nil
}
}
func (ln *Listener) Addr() net.Addr {
return ln.addr
}
func (ln *Listener) Close() error {
ln.once.Do(func() {
close(ln.closeCh)
ln.onClose(ln)
})
return nil
}

View File

@@ -1,68 +0,0 @@
package group
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListener_Accept(t *testing.T) {
acceptCh := make(chan net.Conn, 1)
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
acceptCh <- c1
got, err := ln.Accept()
require.NoError(t, err)
assert.Equal(t, c1, got)
}
func TestListener_AcceptAfterChannelClose(t *testing.T) {
acceptCh := make(chan net.Conn)
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
close(acceptCh)
_, err := ln.Accept()
assert.ErrorIs(t, err, ErrListenerClosed)
}
func TestListener_AcceptAfterListenerClose(t *testing.T) {
acceptCh := make(chan net.Conn) // open, not closed
ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {})
ln.Close()
_, err := ln.Accept()
assert.ErrorIs(t, err, ErrListenerClosed)
}
func TestListener_DoubleClose(t *testing.T) {
closeCalls := 0
ln := newListener(
make(chan net.Conn),
fakeAddr("127.0.0.1:1234"),
func(*Listener) { closeCalls++ },
)
assert.NotPanics(t, func() {
ln.Close()
ln.Close()
})
assert.Equal(t, 1, closeCalls, "onClose should be called exactly once")
}
func TestListener_Addr(t *testing.T) {
addr := fakeAddr("10.0.0.1:5555")
ln := newListener(make(chan net.Conn), addr, func(*Listener) {})
assert.Equal(t, addr, ln.Addr())
}
// fakeAddr implements net.Addr for testing.
type fakeAddr string
func (a fakeAddr) Network() string { return "tcp" }
func (a fakeAddr) String() string { return string(a) }

View File

@@ -1,59 +0,0 @@
package group
import (
"sync"
)
// groupRegistry is a concurrent map of named groups with
// automatic creation on first access.
type groupRegistry[G any] struct {
groups map[string]G
mu sync.Mutex
}
func newGroupRegistry[G any]() groupRegistry[G] {
return groupRegistry[G]{
groups: make(map[string]G),
}
}
func (r *groupRegistry[G]) getOrCreate(key string, newFn func() G) G {
r.mu.Lock()
defer r.mu.Unlock()
g, ok := r.groups[key]
if !ok {
g = newFn()
r.groups[key] = g
}
return g
}
func (r *groupRegistry[G]) get(key string) (G, bool) {
r.mu.Lock()
defer r.mu.Unlock()
g, ok := r.groups[key]
return g, ok
}
// isCurrent returns true if key exists in the registry and matchFn
// returns true for the stored value.
func (r *groupRegistry[G]) isCurrent(key string, matchFn func(G) bool) bool {
r.mu.Lock()
defer r.mu.Unlock()
g, ok := r.groups[key]
return ok && matchFn(g)
}
// removeIf atomically looks up the group for key, calls fn on it,
// and removes the entry if fn returns true.
func (r *groupRegistry[G]) removeIf(key string, fn func(G) bool) {
r.mu.Lock()
defer r.mu.Unlock()
g, ok := r.groups[key]
if !ok {
return
}
if fn(g) {
delete(r.groups, key)
}
}

View File

@@ -1,102 +0,0 @@
package group
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetOrCreate_New(t *testing.T) {
r := newGroupRegistry[*int]()
called := 0
v := 42
got := r.getOrCreate("k", func() *int { called++; return &v })
assert.Equal(t, 1, called)
assert.Equal(t, &v, got)
}
func TestGetOrCreate_Existing(t *testing.T) {
r := newGroupRegistry[*int]()
v := 42
r.getOrCreate("k", func() *int { return &v })
called := 0
got := r.getOrCreate("k", func() *int { called++; return nil })
assert.Equal(t, 0, called)
assert.Equal(t, &v, got)
}
func TestGet_ExistingAndMissing(t *testing.T) {
r := newGroupRegistry[*int]()
v := 1
r.getOrCreate("k", func() *int { return &v })
got, ok := r.get("k")
assert.True(t, ok)
assert.Equal(t, &v, got)
_, ok = r.get("missing")
assert.False(t, ok)
}
func TestIsCurrent(t *testing.T) {
r := newGroupRegistry[*int]()
v1 := 1
v2 := 2
r.getOrCreate("k", func() *int { return &v1 })
assert.True(t, r.isCurrent("k", func(g *int) bool { return g == &v1 }))
assert.False(t, r.isCurrent("k", func(g *int) bool { return g == &v2 }))
assert.False(t, r.isCurrent("missing", func(g *int) bool { return true }))
}
func TestRemoveIf(t *testing.T) {
t.Run("removes when fn returns true", func(t *testing.T) {
r := newGroupRegistry[*int]()
v := 1
r.getOrCreate("k", func() *int { return &v })
r.removeIf("k", func(g *int) bool { return g == &v })
_, ok := r.get("k")
assert.False(t, ok)
})
t.Run("keeps when fn returns false", func(t *testing.T) {
r := newGroupRegistry[*int]()
v := 1
r.getOrCreate("k", func() *int { return &v })
r.removeIf("k", func(g *int) bool { return false })
_, ok := r.get("k")
assert.True(t, ok)
})
t.Run("noop on missing key", func(t *testing.T) {
r := newGroupRegistry[*int]()
r.removeIf("missing", func(g *int) bool { return true }) // should not panic
})
}
func TestConcurrentGetOrCreateAndRemoveIf(t *testing.T) {
r := newGroupRegistry[*int]()
const n = 100
var wg sync.WaitGroup
wg.Add(n * 2)
for i := range n {
v := i
go func() {
defer wg.Done()
r.getOrCreate("k", func() *int { return &v })
}()
go func() {
defer wg.Done()
r.removeIf("k", func(*int) bool { return true })
}()
}
wg.Wait()
// After all goroutines finish, accessing the key must not panic.
require.NotPanics(t, func() {
_, _ = r.get("k")
})
}

View File

@@ -17,67 +17,83 @@ package group
import ( import (
"net" "net"
"strconv" "strconv"
"sync"
gerr "github.com/fatedier/golib/errors"
"github.com/fatedier/frp/server/ports" "github.com/fatedier/frp/server/ports"
) )
// TCPGroupCtl manages all TCPGroups. // TCPGroupCtl manage all TCPGroups
type TCPGroupCtl struct { type TCPGroupCtl struct {
groupRegistry[*TCPGroup] groups map[string]*TCPGroup
// portManager is used to manage port
portManager *ports.Manager portManager *ports.Manager
mu sync.Mutex
} }
// NewTCPGroupCtl returns a new TCPGroupCtl. // NewTCPGroupCtl return a new TcpGroupCtl
func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl { func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl {
return &TCPGroupCtl{ return &TCPGroupCtl{
groupRegistry: newGroupRegistry[*TCPGroup](), groups: make(map[string]*TCPGroup),
portManager: portManager, portManager: portManager,
} }
} }
// Listen is the wrapper for TCPGroup's Listen. // Listen is the wrapper for TCPGroup's Listen
// If there is no group, one will be created. // If there are no group, we will create one here
func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string, func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string,
addr string, port int, addr string, port int,
) (l net.Listener, realPort int, err error) { ) (l net.Listener, realPort int, err error) {
for { tgc.mu.Lock()
tcpGroup := tgc.getOrCreate(group, func() *TCPGroup { tcpGroup, ok := tgc.groups[group]
return NewTCPGroup(tgc) if !ok {
}) tcpGroup = NewTCPGroup(tgc)
l, realPort, err = tcpGroup.Listen(proxyName, group, groupKey, addr, port) tgc.groups[group] = tcpGroup
if err == errGroupStale {
continue
}
return
} }
tgc.mu.Unlock()
return tcpGroup.Listen(proxyName, group, groupKey, addr, port)
} }
// TCPGroup routes connections to different proxies. // RemoveGroup remove TCPGroup from controller
type TCPGroup struct { func (tgc *TCPGroupCtl) RemoveGroup(group string) {
baseGroup tgc.mu.Lock()
defer tgc.mu.Unlock()
delete(tgc.groups, group)
}
// TCPGroup route connections to different proxies
type TCPGroup struct {
group string
groupKey string
addr string addr string
port int port int
realPort int realPort int
acceptCh chan net.Conn
tcpLn net.Listener
lns []*TCPGroupListener
ctl *TCPGroupCtl ctl *TCPGroupCtl
mu sync.Mutex
} }
// NewTCPGroup returns a new TCPGroup. // NewTCPGroup return a new TCPGroup
func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup { func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup {
return &TCPGroup{ return &TCPGroup{
ctl: ctl, lns: make([]*TCPGroupListener, 0),
ctl: ctl,
acceptCh: make(chan net.Conn),
} }
} }
// Listen will return a new Listener. // Listen will return a new TCPGroupListener
// If TCPGroup already has a listener, just add a new Listener to the queues, // if TCPGroup already has a listener, just add a new TCPGroupListener to the queues
// otherwise listen on the real address. // otherwise, listen on the real address
func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *Listener, realPort int, err error) { func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *TCPGroupListener, realPort int, err error) {
tg.mu.Lock() tg.mu.Lock()
defer tg.mu.Unlock() defer tg.mu.Unlock()
if !tg.ctl.isCurrent(group, func(cur *TCPGroup) bool { return cur == tg }) {
return nil, 0, errGroupStale
}
if len(tg.lns) == 0 { if len(tg.lns) == 0 {
// the first listener, listen on the real address // the first listener, listen on the real address
realPort, err = tg.ctl.portManager.Acquire(proxyName, port) realPort, err = tg.ctl.portManager.Acquire(proxyName, port)
@@ -90,18 +106,19 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
err = errRet err = errRet
return return
} }
ln = newTCPGroupListener(group, tg, tcpLn.Addr())
tg.group = group
tg.groupKey = groupKey
tg.addr = addr tg.addr = addr
tg.port = port tg.port = port
tg.realPort = realPort tg.realPort = realPort
tg.initBase(group, groupKey, tcpLn, func() { tg.tcpLn = tcpLn
tg.ctl.portManager.Release(tg.realPort) tg.lns = append(tg.lns, ln)
tg.ctl.removeIf(tg.group, func(cur *TCPGroup) bool { if tg.acceptCh == nil {
return cur == tg tg.acceptCh = make(chan net.Conn)
}) }
}) go tg.worker()
ln = tg.newListener(tcpLn.Addr())
go tg.worker(tcpLn, tg.acceptCh)
} else { } else {
// address and port in the same group must be equal // address and port in the same group must be equal
if tg.group != group || tg.addr != addr { if tg.group != group || tg.addr != addr {
@@ -116,8 +133,92 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
err = ErrGroupAuthFailed err = ErrGroupAuthFailed
return return
} }
ln = tg.newListener(tg.lns[0].Addr()) ln = newTCPGroupListener(group, tg, tg.lns[0].Addr())
realPort = tg.realPort realPort = tg.realPort
tg.lns = append(tg.lns, ln)
} }
return return
} }
// worker is called when the real tcp listener has been created
func (tg *TCPGroup) worker() {
for {
c, err := tg.tcpLn.Accept()
if err != nil {
return
}
err = gerr.PanicToError(func() {
tg.acceptCh <- c
})
if err != nil {
return
}
}
}
func (tg *TCPGroup) Accept() <-chan net.Conn {
return tg.acceptCh
}
// CloseListener remove the TCPGroupListener from the TCPGroup
func (tg *TCPGroup) CloseListener(ln *TCPGroupListener) {
tg.mu.Lock()
defer tg.mu.Unlock()
for i, tmpLn := range tg.lns {
if tmpLn == ln {
tg.lns = append(tg.lns[:i], tg.lns[i+1:]...)
break
}
}
if len(tg.lns) == 0 {
close(tg.acceptCh)
tg.tcpLn.Close()
tg.ctl.portManager.Release(tg.realPort)
tg.ctl.RemoveGroup(tg.group)
}
}
// TCPGroupListener
type TCPGroupListener struct {
groupName string
group *TCPGroup
addr net.Addr
closeCh chan struct{}
}
func newTCPGroupListener(name string, group *TCPGroup, addr net.Addr) *TCPGroupListener {
return &TCPGroupListener{
groupName: name,
group: group,
addr: addr,
closeCh: make(chan struct{}),
}
}
// Accept will accept connections from TCPGroup
func (ln *TCPGroupListener) Accept() (c net.Conn, err error) {
var ok bool
select {
case <-ln.closeCh:
return nil, ErrListenerClosed
case c, ok = <-ln.group.Accept():
if !ok {
return nil, ErrListenerClosed
}
return c, nil
}
}
func (ln *TCPGroupListener) Addr() net.Addr {
return ln.addr
}
// Close close the listener
func (ln *TCPGroupListener) Close() (err error) {
close(ln.closeCh)
// remove self from TcpGroup
ln.group.CloseListener(ln)
return
}

View File

@@ -18,100 +18,118 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"sync"
gerr "github.com/fatedier/golib/errors"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/tcpmux"
"github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/pkg/util/vhost"
) )
// TCPMuxGroupCtl manages all TCPMuxGroups. // TCPMuxGroupCtl manage all TCPMuxGroups
type TCPMuxGroupCtl struct { type TCPMuxGroupCtl struct {
groupRegistry[*TCPMuxGroup] groups map[string]*TCPMuxGroup
// portManager is used to manage port
tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer
mu sync.Mutex
} }
// NewTCPMuxGroupCtl returns a new TCPMuxGroupCtl. // NewTCPMuxGroupCtl return a new TCPMuxGroupCtl
func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl { func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl {
return &TCPMuxGroupCtl{ return &TCPMuxGroupCtl{
groupRegistry: newGroupRegistry[*TCPMuxGroup](), groups: make(map[string]*TCPMuxGroup),
tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer, tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer,
} }
} }
// Listen is the wrapper for TCPMuxGroup's Listen. // Listen is the wrapper for TCPMuxGroup's Listen
// If there is no group, one will be created. // If there are no group, we will create one here
func (tmgc *TCPMuxGroupCtl) Listen( func (tmgc *TCPMuxGroupCtl) Listen(
ctx context.Context, ctx context.Context,
multiplexer, group, groupKey string, multiplexer, group, groupKey string,
routeConfig vhost.RouteConfig, routeConfig vhost.RouteConfig,
) (l net.Listener, err error) { ) (l net.Listener, err error) {
for { tmgc.mu.Lock()
tcpMuxGroup := tmgc.getOrCreate(group, func() *TCPMuxGroup { tcpMuxGroup, ok := tmgc.groups[group]
return NewTCPMuxGroup(tmgc) if !ok {
}) tcpMuxGroup = NewTCPMuxGroup(tmgc)
tmgc.groups[group] = tcpMuxGroup
}
tmgc.mu.Unlock()
switch v1.TCPMultiplexerType(multiplexer) { switch v1.TCPMultiplexerType(multiplexer) {
case v1.TCPMultiplexerHTTPConnect: case v1.TCPMultiplexerHTTPConnect:
l, err = tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
if err == errGroupStale { default:
continue err = fmt.Errorf("unknown multiplexer [%s]", multiplexer)
} return
return
default:
return nil, fmt.Errorf("unknown multiplexer [%s]", multiplexer)
}
} }
} }
// TCPMuxGroup routes connections to different proxies. // RemoveGroup remove TCPMuxGroup from controller
type TCPMuxGroup struct { func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) {
baseGroup tmgc.mu.Lock()
defer tmgc.mu.Unlock()
delete(tmgc.groups, group)
}
// TCPMuxGroup route connections to different proxies
type TCPMuxGroup struct {
group string
groupKey string
domain string domain string
routeByHTTPUser string routeByHTTPUser string
username string username string
password string password string
ctl *TCPMuxGroupCtl
acceptCh chan net.Conn
tcpMuxLn net.Listener
lns []*TCPMuxGroupListener
ctl *TCPMuxGroupCtl
mu sync.Mutex
} }
// NewTCPMuxGroup returns a new TCPMuxGroup. // NewTCPMuxGroup return a new TCPMuxGroup
func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup { func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup {
return &TCPMuxGroup{ return &TCPMuxGroup{
ctl: ctl, lns: make([]*TCPMuxGroupListener, 0),
ctl: ctl,
acceptCh: make(chan net.Conn),
} }
} }
// HTTPConnectListen will return a new Listener. // Listen will return a new TCPMuxGroupListener
// If TCPMuxGroup already has a listener, just add a new Listener to the queues, // if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues
// otherwise listen on the real address. // otherwise, listen on the real address
func (tmg *TCPMuxGroup) HTTPConnectListen( func (tmg *TCPMuxGroup) HTTPConnectListen(
ctx context.Context, ctx context.Context,
group, groupKey string, group, groupKey string,
routeConfig vhost.RouteConfig, routeConfig vhost.RouteConfig,
) (ln *Listener, err error) { ) (ln *TCPMuxGroupListener, err error) {
tmg.mu.Lock() tmg.mu.Lock()
defer tmg.mu.Unlock() defer tmg.mu.Unlock()
if !tmg.ctl.isCurrent(group, func(cur *TCPMuxGroup) bool { return cur == tmg }) {
return nil, errGroupStale
}
if len(tmg.lns) == 0 { if len(tmg.lns) == 0 {
// the first listener, listen on the real address // the first listener, listen on the real address
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig) tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
if errRet != nil { if errRet != nil {
return nil, errRet return nil, errRet
} }
ln = newTCPMuxGroupListener(group, tmg, tcpMuxLn.Addr())
tmg.group = group
tmg.groupKey = groupKey
tmg.domain = routeConfig.Domain tmg.domain = routeConfig.Domain
tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
tmg.username = routeConfig.Username tmg.username = routeConfig.Username
tmg.password = routeConfig.Password tmg.password = routeConfig.Password
tmg.initBase(group, groupKey, tcpMuxLn, func() { tmg.tcpMuxLn = tcpMuxLn
tmg.ctl.removeIf(tmg.group, func(cur *TCPMuxGroup) bool { tmg.lns = append(tmg.lns, ln)
return cur == tmg if tmg.acceptCh == nil {
}) tmg.acceptCh = make(chan net.Conn)
}) }
ln = tmg.newListener(tcpMuxLn.Addr()) go tmg.worker()
go tmg.worker(tcpMuxLn, tmg.acceptCh)
} else { } else {
// route config in the same group must be equal // route config in the same group must be equal
if tmg.group != group || tmg.domain != routeConfig.Domain || if tmg.group != group || tmg.domain != routeConfig.Domain ||
@@ -123,7 +141,90 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(
if tmg.groupKey != groupKey { if tmg.groupKey != groupKey {
return nil, ErrGroupAuthFailed return nil, ErrGroupAuthFailed
} }
ln = tmg.newListener(tmg.lns[0].Addr()) ln = newTCPMuxGroupListener(group, tmg, tmg.lns[0].Addr())
tmg.lns = append(tmg.lns, ln)
} }
return return
} }
// worker is called when the real TCP listener has been created
func (tmg *TCPMuxGroup) worker() {
for {
c, err := tmg.tcpMuxLn.Accept()
if err != nil {
return
}
err = gerr.PanicToError(func() {
tmg.acceptCh <- c
})
if err != nil {
return
}
}
}
func (tmg *TCPMuxGroup) Accept() <-chan net.Conn {
return tmg.acceptCh
}
// CloseListener remove the TCPMuxGroupListener from the TCPMuxGroup
func (tmg *TCPMuxGroup) CloseListener(ln *TCPMuxGroupListener) {
tmg.mu.Lock()
defer tmg.mu.Unlock()
for i, tmpLn := range tmg.lns {
if tmpLn == ln {
tmg.lns = append(tmg.lns[:i], tmg.lns[i+1:]...)
break
}
}
if len(tmg.lns) == 0 {
close(tmg.acceptCh)
tmg.tcpMuxLn.Close()
tmg.ctl.RemoveGroup(tmg.group)
}
}
// TCPMuxGroupListener
type TCPMuxGroupListener struct {
groupName string
group *TCPMuxGroup
addr net.Addr
closeCh chan struct{}
}
func newTCPMuxGroupListener(name string, group *TCPMuxGroup, addr net.Addr) *TCPMuxGroupListener {
return &TCPMuxGroupListener{
groupName: name,
group: group,
addr: addr,
closeCh: make(chan struct{}),
}
}
// Accept will accept connections from TCPMuxGroup
func (ln *TCPMuxGroupListener) Accept() (c net.Conn, err error) {
var ok bool
select {
case <-ln.closeCh:
return nil, ErrListenerClosed
case c, ok = <-ln.group.Accept():
if !ok {
return nil, ErrListenerClosed
}
return c, nil
}
}
func (ln *TCPMuxGroupListener) Addr() net.Addr {
return ln.addr
}
// Close close the listener
func (ln *TCPMuxGroupListener) Close() (err error) {
close(ln.closeCh)
// remove self from TcpMuxGroup
ln.group.CloseListener(ln)
return
}

View File

@@ -31,7 +31,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.HTTPProxyConfig](), NewHTTPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.HTTPProxyConfig{}), NewHTTPProxy)
} }
type HTTPProxy struct { type HTTPProxy struct {

View File

@@ -25,7 +25,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.HTTPSProxyConfig](), NewHTTPSProxy) RegisterProxyFactory(reflect.TypeOf(&v1.HTTPSProxyConfig{}), NewHTTPSProxy)
} }
type HTTPSProxy struct { type HTTPSProxy struct {

View File

@@ -21,7 +21,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.STCPProxyConfig](), NewSTCPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.STCPProxyConfig{}), NewSTCPProxy)
} }
type STCPProxy struct { type STCPProxy struct {

View File

@@ -21,7 +21,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
} }
type SUDPProxy struct { type SUDPProxy struct {

View File

@@ -24,7 +24,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.TCPProxyConfig](), NewTCPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.TCPProxyConfig{}), NewTCPProxy)
} }
type TCPProxy struct { type TCPProxy struct {

View File

@@ -26,7 +26,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.TCPMuxProxyConfig](), NewTCPMuxProxy) RegisterProxyFactory(reflect.TypeOf(&v1.TCPMuxProxyConfig{}), NewTCPMuxProxy)
} }
type TCPMuxProxy struct { type TCPMuxProxy struct {

View File

@@ -35,7 +35,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
} }
type UDPProxy struct { type UDPProxy struct {

View File

@@ -24,7 +24,7 @@ import (
) )
func init() { func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
} }
type XTCPProxy struct { type XTCPProxy struct {

View File

@@ -193,7 +193,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err)
} }
log.Infof("tcpmux httpconnect multiplexer listen on %s, passthrough: %v", address, cfg.TCPMuxPassthrough) log.Infof("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough)
} }
// Init all plugins // Init all plugins

View File

@@ -26,7 +26,7 @@ var _ = ginkgo.Describe("[Feature: Example]", func() {
remotePort = %d remotePort = %d
`, framework.TCPEchoServerPort, remotePort) `, framework.TCPEchoServerPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort).Ensure()
}) })

View File

@@ -241,31 +241,6 @@ func (f *Framework) AllocPort() int {
return port return port
} }
func (f *Framework) AllocPortExcludingRanges(ranges ...[2]int) int {
for range 1000 {
port := f.portAllocator.Get()
ExpectTrue(port > 0, "alloc port failed")
inExcludedRange := false
for _, portRange := range ranges {
if port >= portRange[0] && port <= portRange[1] {
inExcludedRange = true
break
}
}
if inExcludedRange {
f.portAllocator.Release(port)
continue
}
f.allocatedPorts = append(f.allocatedPorts, port)
return port
}
Failf("alloc port outside excluded ranges failed")
return 0
}
func (f *Framework) ReleasePort(port int) { func (f *Framework) ReleasePort(port int) {
f.portAllocator.Release(port) f.portAllocator.Release(port)
} }

View File

@@ -2,85 +2,70 @@ package framework
import ( import (
"fmt" "fmt"
"maps"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/fatedier/frp/pkg/config"
flog "github.com/fatedier/frp/pkg/util/log" flog "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/pkg/process" "github.com/fatedier/frp/test/e2e/pkg/process"
) )
// RunProcesses starts one frps and zero or more frpc processes from templates. // RunProcesses run multiple processes from templates.
func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string) (*process.Process, []*process.Process) { // The first template should always be frps.
templates := append([]string{serverTemplate}, clientTemplates...) func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) {
templates := make([]string, 0, len(serverTemplates)+len(clientTemplates))
templates = append(templates, serverTemplates...)
templates = append(templates, clientTemplates...)
outs, ports, err := f.RenderTemplates(templates) outs, ports, err := f.RenderTemplates(templates)
ExpectNoError(err) ExpectNoError(err)
ExpectTrue(len(templates) > 0)
maps.Copy(f.usedPorts, ports) for name, port := range ports {
f.usedPorts[name] = port
// Start frps.
serverPath := filepath.Join(f.TempDirectory, "frp-e2e-server-0")
err = os.WriteFile(serverPath, []byte(outs[0]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
flog.Debugf("[%s] %s", serverPath, outs[0])
} }
serverProcess := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", serverPath}, f.osEnvs) currentServerProcesses := make([]*process.Process, 0, len(serverTemplates))
f.serverConfPaths = append(f.serverConfPaths, serverPath) for i := range serverTemplates {
f.serverProcesses = append(f.serverProcesses, serverProcess) path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
err = serverProcess.Start() err = os.WriteFile(path, []byte(outs[i]), 0o600)
ExpectNoError(err)
if port, ok := ports[consts.PortServerName]; ok {
ExpectNoError(WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 5*time.Second))
} else {
time.Sleep(2 * time.Second)
}
// Start frpc(s).
clientProcesses := make([]*process.Process, 0, len(clientTemplates))
for i := range clientTemplates {
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
err = os.WriteFile(path, []byte(outs[1+i]), 0o600)
ExpectNoError(err) ExpectNoError(err)
if TestContext.Debug { if TestContext.Debug {
flog.Debugf("[%s] %s", path, outs[1+i]) flog.Debugf("[%s] %s", path, outs[i])
}
p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs)
f.serverConfPaths = append(f.serverConfPaths, path)
f.serverProcesses = append(f.serverProcesses, p)
currentServerProcesses = append(currentServerProcesses, p)
err = p.Start()
ExpectNoError(err)
time.Sleep(500 * time.Millisecond)
}
time.Sleep(2 * time.Second)
currentClientProcesses := make([]*process.Process, 0, len(clientTemplates))
for i := range clientTemplates {
index := i + len(serverTemplates)
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
err = os.WriteFile(path, []byte(outs[index]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
flog.Debugf("[%s] %s", path, outs[index])
} }
p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs) p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs)
f.clientConfPaths = append(f.clientConfPaths, path) f.clientConfPaths = append(f.clientConfPaths, path)
f.clientProcesses = append(f.clientProcesses, p) f.clientProcesses = append(f.clientProcesses, p)
clientProcesses = append(clientProcesses, p) currentClientProcesses = append(currentClientProcesses, p)
err = p.Start() err = p.Start()
ExpectNoError(err) ExpectNoError(err)
time.Sleep(500 * time.Millisecond)
} }
// Wait for each client's proxies to register with frps. time.Sleep(3 * time.Second)
// If any client has no proxies (e.g. visitor-only), fall back to sleep
// for the remaining time since visitors have no deterministic readiness signal.
allConfirmed := len(clientProcesses) > 0
start := time.Now()
for i, p := range clientProcesses {
configPath := f.clientConfPaths[len(f.clientConfPaths)-len(clientProcesses)+i]
if !waitForClientProxyReady(configPath, p, 5*time.Second) {
allConfirmed = false
}
}
if len(clientProcesses) > 0 && !allConfirmed {
remaining := 1500*time.Millisecond - time.Since(start)
if remaining > 0 {
time.Sleep(remaining)
}
}
return serverProcess, clientProcesses return currentServerProcesses, currentClientProcesses
} }
func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
@@ -88,13 +73,11 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
f.serverProcesses = append(f.serverProcesses, p) f.serverProcesses = append(f.serverProcesses, p)
err := p.Start() err := p.Start()
if err != nil { if err != nil {
return p, p.Output(), err return p, p.StdOutput(), err
} }
select { // Give frps extra time to finish binding ports before proceeding.
case <-p.Done(): time.Sleep(4 * time.Second)
case <-time.After(2 * time.Second): return p, p.StdOutput(), nil
}
return p, p.Output(), nil
} }
func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
@@ -102,13 +85,10 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
f.clientProcesses = append(f.clientProcesses, p) f.clientProcesses = append(f.clientProcesses, p)
err := p.Start() err := p.Start()
if err != nil { if err != nil {
return p, p.Output(), err return p, p.StdOutput(), err
} }
select { time.Sleep(2 * time.Second)
case <-p.Done(): return p, p.StdOutput(), nil
case <-time.After(1500 * time.Millisecond):
}
return p, p.Output(), nil
} }
func (f *Framework) GenerateConfigFile(content string) string { func (f *Framework) GenerateConfigFile(content string) string {
@@ -118,74 +98,3 @@ func (f *Framework) GenerateConfigFile(content string) string {
ExpectNoError(err) ExpectNoError(err)
return path return path
} }
// waitForClientProxyReady parses the client config to extract proxy names,
// then waits for each proxy's "start proxy success" log in the process output.
// Returns true only if proxies were expected and all registered successfully.
func waitForClientProxyReady(configPath string, p *process.Process, timeout time.Duration) bool {
_, proxyCfgs, _, _, err := config.LoadClientConfig(configPath, false)
if err != nil || len(proxyCfgs) == 0 {
return false
}
// Use a single deadline so the total wait across all proxies does not exceed timeout.
deadline := time.Now().Add(timeout)
for _, cfg := range proxyCfgs {
remaining := time.Until(deadline)
if remaining <= 0 {
return false
}
name := cfg.GetBaseConfig().Name
pattern := fmt.Sprintf("[%s] start proxy success", name)
if err := p.WaitForOutput(pattern, 1, remaining); err != nil {
return false
}
}
return true
}
// WaitForTCPUnreachable polls a TCP address until a connection fails or timeout.
func WaitForTCPUnreachable(addr string, interval, timeout time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid interval for TCP unreachable on %s: interval must be positive", addr)
}
if timeout <= 0 {
return fmt.Errorf("invalid timeout for TCP unreachable on %s: timeout must be positive", addr)
}
deadline := time.Now().Add(timeout)
for {
remaining := time.Until(deadline)
if remaining <= 0 {
return fmt.Errorf("timeout waiting for TCP unreachable on %s", addr)
}
dialTimeout := min(interval, remaining)
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil
}
conn.Close()
time.Sleep(min(interval, time.Until(deadline)))
}
}
// WaitForTCPReady polls a TCP address until a connection succeeds or timeout.
func WaitForTCPReady(addr string, timeout time.Duration) error {
if timeout <= 0 {
return fmt.Errorf("invalid timeout for TCP readiness on %s: timeout must be positive", addr)
}
deadline := time.Now().Add(timeout)
var lastErr error
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", addr, 100*time.Millisecond)
if err == nil {
conn.Close()
return nil
}
lastErr = err
time.Sleep(50 * time.Millisecond)
}
if lastErr == nil {
return fmt.Errorf("timeout waiting for TCP readiness on %s before any dial attempt", addr)
}
return fmt.Errorf("timeout waiting for TCP readiness on %s: %w", addr, lastErr)
}

View File

@@ -26,8 +26,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
proxyType := t proxyType := t
ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() {
serverConf := consts.LegacyDefaultServerConfig serverConf := consts.LegacyDefaultServerConfig
var clientConf strings.Builder clientConf := consts.LegacyDefaultClientConfig
clientConf.WriteString(consts.LegacyDefaultClientConfig)
localPortName := "" localPortName := ""
protocol := "tcp" protocol := "tcp"
@@ -79,10 +78,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
// build all client config // build all client config
for _, test := range tests { for _, test := range tests {
clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n"
} }
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientConf.String()}) f.RunProcesses([]string{serverConf}, []string{clientConf})
for _, test := range tests { for _, test := range tests {
framework.NewRequestExpect(f). framework.NewRequestExpect(f).
@@ -103,8 +102,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
vhost_http_port = %d vhost_http_port = %d
`, vhostHTTPPort) `, vhostHTTPPort)
var clientConf strings.Builder clientConf := consts.LegacyDefaultClientConfig
clientConf.WriteString(consts.LegacyDefaultClientConfig)
getProxyConf := func(proxyName string, customDomains string, extra string) string { getProxyConf := func(proxyName string, customDomains string, extra string) string {
return fmt.Sprintf(` return fmt.Sprintf(`
@@ -149,13 +147,13 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
if tests[i].customDomains == "" { if tests[i].customDomains == "" {
tests[i].customDomains = test.proxyName + ".example.com" tests[i].customDomains = test.proxyName + ".example.com"
} }
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n"
} }
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientConf.String()}) f.RunProcesses([]string{serverConf}, []string{clientConf})
for _, test := range tests { for _, test := range tests {
for domain := range strings.SplitSeq(test.customDomains, ",") { for _, domain := range strings.Split(test.customDomains, ",") {
domain = strings.TrimSpace(domain) domain = strings.TrimSpace(domain)
framework.NewRequestExpect(f). framework.NewRequestExpect(f).
Explain(test.proxyName + "-" + domain). Explain(test.proxyName + "-" + domain).
@@ -187,8 +185,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
`, vhostHTTPSPort) `, vhostHTTPSPort)
localPort := f.AllocPort() localPort := f.AllocPort()
var clientConf strings.Builder clientConf := consts.LegacyDefaultClientConfig
clientConf.WriteString(consts.LegacyDefaultClientConfig)
getProxyConf := func(proxyName string, customDomains string, extra string) string { getProxyConf := func(proxyName string, customDomains string, extra string) string {
return fmt.Sprintf(` return fmt.Sprintf(`
[%s] [%s]
@@ -232,10 +229,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
if tests[i].customDomains == "" { if tests[i].customDomains == "" {
tests[i].customDomains = test.proxyName + ".example.com" tests[i].customDomains = test.proxyName + ".example.com"
} }
clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n"
} }
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientConf.String()}) f.RunProcesses([]string{serverConf}, []string{clientConf})
tlsConfig, err := transport.NewServerTLSConfig("", "", "") tlsConfig, err := transport.NewServerTLSConfig("", "", "")
framework.ExpectNoError(err) framework.ExpectNoError(err)
@@ -247,7 +244,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
f.RunServer("", localServer) f.RunServer("", localServer)
for _, test := range tests { for _, test := range tests {
for domain := range strings.SplitSeq(test.customDomains, ",") { for _, domain := range strings.Split(test.customDomains, ",") {
domain = strings.TrimSpace(domain) domain = strings.TrimSpace(domain)
framework.NewRequestExpect(f). framework.NewRequestExpect(f).
Explain(test.proxyName + "-" + domain). Explain(test.proxyName + "-" + domain).
@@ -285,12 +282,9 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
proxyType := t proxyType := t
ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() {
serverConf := consts.LegacyDefaultServerConfig serverConf := consts.LegacyDefaultServerConfig
var clientServerConf strings.Builder clientServerConf := consts.LegacyDefaultClientConfig + "\nuser = user1"
clientServerConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1") clientVisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user1"
var clientVisitorConf strings.Builder clientUser2VisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user2"
clientVisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1")
var clientUser2VisitorConf strings.Builder
clientUser2VisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user2")
localPortName := "" localPortName := ""
protocol := "tcp" protocol := "tcp"
@@ -406,20 +400,20 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
// build all client config // build all client config
for _, test := range tests { for _, test := range tests {
clientServerConf.WriteString(getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n") clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n"
} }
for _, test := range tests { for _, test := range tests {
config := getProxyVisitorConf( config := getProxyVisitorConf(
test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig,
) + "\n" ) + "\n"
if test.deployUser2Client { if test.deployUser2Client {
clientUser2VisitorConf.WriteString(config) clientUser2VisitorConf += config
} else { } else {
clientVisitorConf.WriteString(config) clientVisitorConf += config
} }
} }
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf})
for _, test := range tests { for _, test := range tests {
timeout := time.Second timeout := time.Second
@@ -446,8 +440,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
ginkgo.Describe("TCPMUX", func() { ginkgo.Describe("TCPMUX", func() {
ginkgo.It("Type tcpmux", func() { ginkgo.It("Type tcpmux", func() {
serverConf := consts.LegacyDefaultServerConfig serverConf := consts.LegacyDefaultServerConfig
var clientConf strings.Builder clientConf := consts.LegacyDefaultClientConfig
clientConf.WriteString(consts.LegacyDefaultClientConfig)
tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") tcpmuxHTTPConnectPortName := port.GenName("TCPMUX")
serverConf += fmt.Sprintf(` serverConf += fmt.Sprintf(`
@@ -490,14 +483,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
// build all client config // build all client config
for _, test := range tests { for _, test := range tests {
clientConf.WriteString(getProxyConf(test.proxyName, test.extraConfig) + "\n") clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n"
localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName)))
f.RunServer(port.GenName(test.proxyName), localServer) f.RunServer(port.GenName(test.proxyName), localServer)
} }
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientConf.String()}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// Request without HTTP connect should get error // Request without HTTP connect should get error
framework.NewRequestExpect(f). framework.NewRequestExpect(f).

View File

@@ -48,7 +48,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
framework.TCPEchoServerPort, p2Port, framework.TCPEchoServerPort, p2Port,
framework.TCPEchoServerPort, p3Port) framework.TCPEchoServerPort, p3Port)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(p1Port).Ensure() framework.NewRequestExpect(f).Port(p1Port).Ensure()
framework.NewRequestExpect(f).Port(p2Port).Ensure() framework.NewRequestExpect(f).Port(p2Port).Ensure()
@@ -90,7 +90,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
admin_pwd = admin admin_pwd = admin
`, dashboardPort) `, dashboardPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.HTTP().HTTPPath("/healthz") r.HTTP().HTTPPath("/healthz")
@@ -116,7 +116,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
remote_port = %d remote_port = %d
`, adminPort, framework.TCPEchoServerPort, testPort) `, adminPort, framework.TCPEchoServerPort, testPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(testPort).Ensure() framework.NewRequestExpect(f).Port(testPort).Ensure()

View File

@@ -76,7 +76,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur
clientConfs = append(clientConfs, client2Conf) clientConfs = append(clientConfs, client2Conf)
} }
f.RunProcesses(serverConf, clientConfs) f.RunProcesses([]string{serverConf}, clientConfs)
if configures.testDelay > 0 { if configures.testDelay > 0 {
time.Sleep(configures.testDelay) time.Sleep(configures.testDelay)

View File

@@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() {
`, "`", "`", framework.TCPEchoServerPort, portName) `, "`", "`", framework.TCPEchoServerPort, portName)
f.SetEnvs([]string{"FRP_TOKEN=123"}) f.SetEnvs([]string{"FRP_TOKEN=123"})
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).PortName(portName).Ensure() framework.NewRequestExpect(f).PortName(portName).Ensure()
}) })

View File

@@ -56,7 +56,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
locations = /bar locations = /bar
`, fooPort, barPort) `, fooPort, barPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
tests := []struct { tests := []struct {
path string path string
@@ -111,7 +111,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
custom_domains = normal.example.com custom_domains = normal.example.com
`, fooPort, barPort, otherPort) `, fooPort, barPort, otherPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// user1 // user1
framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
@@ -152,7 +152,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
http_pwd = test http_pwd = test
`, framework.HTTPSimpleServerPort) `, framework.HTTPSimpleServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// not set auth header // not set auth header
framework.NewRequestExpect(f).Port(vhostHTTPPort). framework.NewRequestExpect(f).Port(vhostHTTPPort).
@@ -188,7 +188,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
custom_domains = *.example.com custom_domains = *.example.com
`, framework.HTTPSimpleServerPort) `, framework.HTTPSimpleServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// not match host // not match host
framework.NewRequestExpect(f).Port(vhostHTTPPort). framework.NewRequestExpect(f).Port(vhostHTTPPort).
@@ -238,7 +238,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
subdomain = bar subdomain = bar
`, fooPort, barPort) `, fooPort, barPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// foo // foo
framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort).
@@ -279,7 +279,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
header_X-From-Where = frp header_X-From-Where = frp
`, localPort) `, localPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// not set auth header // not set auth header
framework.NewRequestExpect(f).Port(vhostHTTPPort). framework.NewRequestExpect(f).Port(vhostHTTPPort).
@@ -312,7 +312,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
host_header_rewrite = rewrite.example.com host_header_rewrite = rewrite.example.com
`, localPort) `, localPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(vhostHTTPPort). framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) { RequestModify(func(r *request.Request) {
@@ -360,7 +360,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
custom_domains = 127.0.0.1 custom_domains = 127.0.0.1
`, localPort) `, localPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)

View File

@@ -20,8 +20,6 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
ginkgo.It("Ports Whitelist", func() { ginkgo.It("Ports Whitelist", func() {
serverConf := consts.LegacyDefaultServerConfig serverConf := consts.LegacyDefaultServerConfig
clientConf := consts.LegacyDefaultClientConfig clientConf := consts.LegacyDefaultClientConfig
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
serverConf += ` serverConf += `
allow_ports = 10000-11000,11002,12000-13000 allow_ports = 10000-11000,11002,12000-13000
@@ -30,7 +28,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000)) tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000))
udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000)) udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000))
clientConf += fmt.Sprintf(` clientConf += fmt.Sprintf(`
[tcp-allowed-in-range] [tcp-allowded-in-range]
type = tcp type = tcp
local_port = {{ .%s }} local_port = {{ .%s }}
remote_port = {{ .%s }} remote_port = {{ .%s }}
@@ -39,8 +37,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
[tcp-port-not-allowed] [tcp-port-not-allowed]
type = tcp type = tcp
local_port = {{ .%s }} local_port = {{ .%s }}
remote_port = %d remote_port = 11001
`, framework.TCPEchoServerPort, tcpPortNotAllowed) `, framework.TCPEchoServerPort)
clientConf += fmt.Sprintf(` clientConf += fmt.Sprintf(`
[tcp-port-unavailable] [tcp-port-unavailable]
type = tcp type = tcp
@@ -57,17 +55,17 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
[udp-port-not-allowed] [udp-port-not-allowed]
type = udp type = udp
local_port = {{ .%s }} local_port = {{ .%s }}
remote_port = %d remote_port = 11003
`, framework.UDPEchoServerPort, udpPortNotAllowed) `, framework.UDPEchoServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// TCP // TCP
// Allowed in range // Allowed in range
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure() framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
// Unavailable, already bind by frps // Unavailable, already bind by frps
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
@@ -78,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.UDP().Port(udpPortNotAllowed) r.UDP().Port(11003)
}).ExpectError(true).Ensure() }).ExpectError(true).Ensure()
}) })
@@ -99,7 +97,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
local_port = {{ .%s }} local_port = {{ .%s }}
`, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
client := f.APIClientForFrpc(adminPort) client := f.APIClientForFrpc(adminPort)
@@ -140,7 +138,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
custom_domains = example.com custom_domains = example.com
`, framework.HTTPSimpleServerPort) `, framework.HTTPSimpleServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("example.com") r.HTTP().HTTPHost("example.com")
@@ -167,7 +165,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
custom_domains = example.com custom_domains = example.com
`, framework.HTTPSimpleServerPort) `, framework.HTTPSimpleServerPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.HTTP().HTTPPath("/healthz") r.HTTP().HTTPPath("/healthz")

View File

@@ -76,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
custom_domains = normal.example.com custom_domains = normal.example.com
`, fooPort, barPort, otherPort) `, fooPort, barPort, otherPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// user1 // user1
framework.NewRequestExpect(f).Explain("user1"). framework.NewRequestExpect(f).Explain("user1").
@@ -121,7 +121,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
http_pwd = test http_pwd = test
`, fooPort) `, fooPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// not set auth header // not set auth header
framework.NewRequestExpect(f).Explain("no auth"). framework.NewRequestExpect(f).Explain("no auth").
@@ -204,7 +204,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
custom_domains = normal.example.com custom_domains = normal.example.com
`, localPort) `, localPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f). framework.NewRequestExpect(f).
RequestModify(func(r *request.Request) { RequestModify(func(r *request.Request) {

View File

@@ -41,7 +41,7 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() {
fallback_timeout_ms = 200 fallback_timeout_ms = 200
`, framework.TCPEchoServerPort, bindPortName) `, framework.TCPEchoServerPort, bindPortName)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f). framework.NewRequestExpect(f).
RequestModify(func(r *request.Request) { RequestModify(func(r *request.Request) {
r.Timeout(time.Second) r.Timeout(time.Second)

View File

@@ -35,7 +35,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
bandwidth_limit = 10KB bandwidth_limit = 10KB
`, localPort, remotePort) `, localPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
content := strings.Repeat("a", 50*1024) // 5KB content := strings.Repeat("a", 50*1024) // 5KB
start := time.Now() start := time.Now()
@@ -89,7 +89,7 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
remote_port = %d remote_port = %d
`, localPort, remotePort) `, localPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
content := strings.Repeat("a", 50*1024) // 5KB content := strings.Repeat("a", 50*1024) // 5KB
start := time.Now() start := time.Now()

View File

@@ -48,10 +48,12 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
return true return true
}) })
} }
for range 10 { for i := 0; i < 10; i++ {
wait.Go(func() { wait.Add(1)
go func() {
defer wait.Done()
expectFn() expectFn()
}) }()
} }
wait.Wait() wait.Wait()
@@ -88,11 +90,11 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
group_key = 123 group_key = 123
`, fooPort, remotePort, barPort, remotePort) `, fooPort, remotePort, barPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
fooCount := 0 fooCount := 0
barCount := 0 barCount := 0
for i := range 10 { for i := 0; i < 10; i++ {
framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool {
switch string(resp.Content) { switch string(resp.Content) {
case "foo": case "foo":
@@ -144,11 +146,11 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
health_check_interval_s = 1 health_check_interval_s = 1
`, fooPort, remotePort, barPort, remotePort) `, fooPort, remotePort, barPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// check foo and bar is ok // check foo and bar is ok
results := []string{} results := []string{}
for range 10 { for i := 0; i < 10; i++ {
framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool {
results = append(results, string(resp.Content)) results = append(results, string(resp.Content))
return true return true
@@ -159,7 +161,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
// close bar server, check foo is ok // close bar server, check foo is ok
barServer.Close() barServer.Close()
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
for range 10 { for i := 0; i < 10; i++ {
framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure()
} }
@@ -167,7 +169,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
f.RunServer("", barServer) f.RunServer("", barServer)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
results = []string{} results = []string{}
for range 10 { for i := 0; i < 10; i++ {
framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool {
results = append(results, string(resp.Content)) results = append(results, string(resp.Content))
return true return true
@@ -213,7 +215,7 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
health_check_url = /healthz health_check_url = /healthz
`, fooPort, barPort) `, fooPort, barPort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// send first HTTP request // send first HTTP request
var contents []string var contents []string

View File

@@ -38,7 +38,7 @@ var _ = ginkgo.Describe("[Feature: Heartbeat]", func() {
`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
// run frps and frpc // run frps and frpc
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()

View File

@@ -33,7 +33,7 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() {
remote_port = %d remote_port = %d
`, framework.TCPEchoServerPort, remotePort) `, framework.TCPEchoServerPort, remotePort)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort).Ensure()
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)

Some files were not shown because too many files have changed in this diff Show More