mirror of
https://github.com/fatedier/frp.git
synced 2026-03-31 06:09:16 +08:00
Compare commits
2 Commits
dev
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7993225be1 | ||
|
|
17b27d8d96 |
14
.github/workflows/build-and-push-image.yml
vendored
14
.github/workflows/build-and-push-image.yml
vendored
@@ -19,15 +19,15 @@ jobs:
|
||||
steps:
|
||||
# environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# get image tag name
|
||||
- name: Get Image Tag Name
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to the GPR
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push frpc
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./dockerfiles/Dockerfile-for-frpc
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
${{ env.TAG_FRPC_GPR }}
|
||||
|
||||
- name: Build and push frps
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./dockerfiles/Dockerfile-for-frps
|
||||
|
||||
6
.github/workflows/golangci-lint.yml
vendored
6
.github/workflows/golangci-lint.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: false
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
|
||||
8
.github/workflows/goreleaser.yml
vendored
8
.github/workflows/goreleaser.yml
vendored
@@ -8,15 +8,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
./package.sh
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --release-notes=./Release.md
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
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."
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,12 +25,10 @@ dist/
|
||||
client.crt
|
||||
client.key
|
||||
|
||||
node_modules/
|
||||
|
||||
# Cache
|
||||
*.swp
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
.sisyphus/
|
||||
.superpowers/
|
||||
|
||||
@@ -18,7 +18,6 @@ linters:
|
||||
- lll
|
||||
- makezero
|
||||
- misspell
|
||||
- modernize
|
||||
- prealloc
|
||||
- predeclared
|
||||
- revive
|
||||
@@ -48,9 +47,6 @@ linters:
|
||||
ignore-rules:
|
||||
- cancelled
|
||||
- marshalled
|
||||
modernize:
|
||||
disable:
|
||||
- omitzero
|
||||
unparam:
|
||||
check-exported: false
|
||||
exclusions:
|
||||
@@ -90,7 +86,6 @@ linters:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- node_modules
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
@@ -113,7 +108,6 @@ formatters:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- node_modules
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
39
AGENTS.md
39
AGENTS.md
@@ -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
|
||||
40
README.md
40
README.md
@@ -13,16 +13,6 @@ frp is an open source project with its ongoing development made possible entirel
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
<br>
|
||||
<b>The sovereign cloud that puts you in control</b>
|
||||
<br>
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
@@ -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>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
<br>
|
||||
<b>The sovereign cloud that puts you in control</b>
|
||||
<br>
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## What is frp?
|
||||
@@ -81,7 +81,6 @@ frp also offers a P2P connect mode.
|
||||
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
||||
* [Server Dashboard](#server-dashboard)
|
||||
* [Client Admin UI](#client-admin-ui)
|
||||
* [Dynamic Proxy Management (Store)](#dynamic-proxy-management-store)
|
||||
* [Monitor](#monitor)
|
||||
* [Prometheus](#prometheus)
|
||||
* [Authenticating the Client](#authenticating-the-client)
|
||||
@@ -150,9 +149,7 @@ We sincerely appreciate your support for frp.
|
||||
|
||||
## Architecture
|
||||
|
||||
<p align="center">
|
||||
<img src="/doc/pic/architecture.jpg" alt="architecture" width="760">
|
||||
</p>
|
||||

|
||||
|
||||
## Example Usage
|
||||
|
||||
@@ -596,7 +593,7 @@ Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS conn
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
#### 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
|
||||
|
||||
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
|
||||
|
||||
20
README_zh.md
20
README_zh.md
@@ -15,16 +15,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
<br>
|
||||
<b>The sovereign cloud that puts you in control</b>
|
||||
<br>
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
@@ -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>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
<br>
|
||||
<b>The sovereign cloud that puts you in control</b>
|
||||
<br>
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## 为什么使用 frp ?
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
## Features
|
||||
|
||||
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
|
||||
|
||||
## Improvements
|
||||
|
||||
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
|
||||
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
|
||||
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
|
||||
|
||||
@@ -38,8 +38,6 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
||||
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.PutConfig)).Methods(http.MethodPut)
|
||||
subRouter.HandleFunc("/api/proxy/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetProxyConfig)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/visitor/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetVisitorConfig)).Methods(http.MethodGet)
|
||||
|
||||
if svr.storeSource != nil {
|
||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||
|
||||
@@ -80,48 +80,6 @@ func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||
return m.svr.getAllProxyStatus()
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||
// Try running proxy manager first
|
||||
ws, ok := m.svr.getProxyStatus(name)
|
||||
if ok {
|
||||
return ws.Cfg, true
|
||||
}
|
||||
|
||||
// Fallback to store
|
||||
m.svr.reloadMu.Lock()
|
||||
storeSource := m.svr.storeSource
|
||||
m.svr.reloadMu.Unlock()
|
||||
|
||||
if storeSource != nil {
|
||||
cfg := storeSource.GetProxy(name)
|
||||
if cfg != nil {
|
||||
return cfg, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||
// Try running visitor manager first
|
||||
cfg, ok := m.svr.getVisitorCfg(name)
|
||||
if ok {
|
||||
return cfg, true
|
||||
}
|
||||
|
||||
// Fallback to store
|
||||
m.svr.reloadMu.Lock()
|
||||
storeSource := m.svr.storeSource
|
||||
m.svr.reloadMu.Unlock()
|
||||
|
||||
if storeSource != nil {
|
||||
vcfg := storeSource.GetVisitor(name)
|
||||
if vcfg != nil {
|
||||
return vcfg, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
|
||||
@@ -26,9 +26,6 @@ type ConfigManager interface {
|
||||
IsStoreProxyEnabled(name string) bool
|
||||
StoreEnabled() bool
|
||||
|
||||
GetProxyConfig(name string) (v1.ProxyConfigurer, bool)
|
||||
GetVisitorConfig(name string) (v1.VisitorConfigurer, bool)
|
||||
|
||||
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||
|
||||
@@ -119,7 +119,6 @@ func (c *defaultConnectorImpl) Open() error {
|
||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||
session, err := fmux.Client(conn, fmuxCfg)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
c.muxSession = session
|
||||
|
||||
@@ -162,44 +162,6 @@ func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.Pro
|
||||
return psr
|
||||
}
|
||||
|
||||
// GetProxyConfig handles GET /api/proxy/{name}/config
|
||||
func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
cfg, ok := c.manager.GetProxyConfig(name)
|
||||
if !ok {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name))
|
||||
}
|
||||
|
||||
payload, err := model.ProxyDefinitionFromConfigurer(cfg)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetVisitorConfig handles GET /api/visitor/{name}/config
|
||||
func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
cfg, ok := c.manager.GetVisitorConfig(name)
|
||||
if !ok {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name))
|
||||
}
|
||||
|
||||
payload, err := model.VisitorDefinitionFromConfigurer(cfg)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||
proxies, err := c.manager.ListStoreProxies()
|
||||
if err != nil {
|
||||
|
||||
@@ -26,8 +26,6 @@ type fakeConfigManager struct {
|
||||
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||
isStoreProxyEnabledFn func(name string) bool
|
||||
storeEnabledFn func() bool
|
||||
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
|
||||
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
|
||||
|
||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||
@@ -84,20 +82,6 @@ func (m *fakeConfigManager) StoreEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||
if m.getProxyConfigFn != nil {
|
||||
return m.getProxyConfigFn(name)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||
if m.getVisitorConfigFn != nil {
|
||||
return m.getVisitorConfigFn(name)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||
if m.listStoreProxiesFn != nil {
|
||||
return m.listStoreProxiesFn()
|
||||
@@ -545,118 +529,3 @@ func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
@@ -123,33 +122,6 @@ func (pxy *BaseProxy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// wrapWorkConn applies rate limiting, encryption, and compression
|
||||
// to a work connection based on the proxy's transport configuration.
|
||||
// The returned recycle function should be called when the stream is no longer in use
|
||||
// to return compression resources to the pool. It is safe to not call recycle,
|
||||
// in which case resources will be garbage collected normally.
|
||||
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.baseCfg.Transport.UseEncryption {
|
||||
var err error
|
||||
rwc, err = libio.WithEncryption(rwc, encKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
||||
}
|
||||
}
|
||||
var recycleFn func()
|
||||
if pxy.baseCfg.Transport.UseCompression {
|
||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||
}
|
||||
return rwc, recycleFn, nil
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||
pxy.inWorkConnCallback = cb
|
||||
}
|
||||
@@ -167,14 +139,30 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||
xl := pxy.xl
|
||||
baseCfg := pxy.baseCfg
|
||||
var (
|
||||
remote io.ReadWriteCloser
|
||||
err error
|
||||
)
|
||||
remote = workConn
|
||||
if pxy.limiter != nil {
|
||||
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
|
||||
return workConn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||
|
||||
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
if baseCfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, encKey)
|
||||
if err != nil {
|
||||
workConn.Close()
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
var compressionResourceRecycleFn func()
|
||||
if baseCfg.Transport.UseCompression {
|
||||
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
||||
}
|
||||
|
||||
// check if we need to send proxy protocol info
|
||||
@@ -190,6 +178,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
||||
}
|
||||
|
||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||
// Use the common proxy protocol builder function
|
||||
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||
connInfo.ProxyProtocolHeader = header
|
||||
}
|
||||
@@ -198,18 +187,12 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
||||
|
||||
if pxy.proxyPlugin != nil {
|
||||
// if plugin is set, let plugin handle connection first
|
||||
// Don't recycle compression resources here because plugins may
|
||||
// retain the connection after Handle returns.
|
||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||
xl.Debugf("handle by plugin finished")
|
||||
return
|
||||
}
|
||||
|
||||
if recycleFn != nil {
|
||||
defer recycleFn()
|
||||
}
|
||||
|
||||
localConn, err := libnet.Dial(
|
||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||
libnet.WithTimeout(10*time.Second),
|
||||
@@ -226,7 +209,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
||||
if connInfo.ProxyProtocolHeader != nil {
|
||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||
workConn.Close()
|
||||
localConn.Close()
|
||||
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -237,4 +219,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
||||
if len(errs) > 0 {
|
||||
xl.Tracef("join connections errors: %v", errs)
|
||||
}
|
||||
if compressionResourceRecycleFn != nil {
|
||||
compressionResourceRecycleFn()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -24,15 +25,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
||||
}
|
||||
|
||||
type SUDPProxy struct {
|
||||
@@ -80,13 +83,27 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
xl := pxy.xl
|
||||
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||
|
||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
var err error
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||
workConn := conn
|
||||
readCh := make(chan *msg.UDPPacket, 1024)
|
||||
sendCh := make(chan msg.Message, 1024)
|
||||
isClose := false
|
||||
|
||||
@@ -17,21 +17,24 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
||||
}
|
||||
|
||||
type UDPProxy struct {
|
||||
@@ -91,14 +94,28 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
// close resources related with old workConn
|
||||
pxy.Close()
|
||||
|
||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
xl.Errorf("wrap work connection: %v", err)
|
||||
return
|
||||
var rwc io.ReadWriteCloser = conn
|
||||
var err error
|
||||
if pxy.limiter != nil {
|
||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pxy.cfg.Transport.UseCompression {
|
||||
rwc = libio.WithCompression(rwc)
|
||||
}
|
||||
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||
|
||||
pxy.mu.Lock()
|
||||
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||
pxy.workConn = conn
|
||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||
pxy.sendCh = make(chan msg.Message, 1024)
|
||||
pxy.closed = false
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
||||
}
|
||||
|
||||
type XTCPProxy struct {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
@@ -163,6 +162,15 @@ func NewService(options ServiceOptions) (*Service, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -183,17 +191,6 @@ func NewService(options ServiceOptions) (*Service, error) {
|
||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||
|
||||
// Create the web server after all fallible steps so its listener is not
|
||||
// leaked when an earlier error causes NewService to return.
|
||||
var webServer *httppkg.Server
|
||||
if options.Common.WebServer.Port > 0 {
|
||||
ws, err := httppkg.NewServer(options.Common.WebServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webServer = ws
|
||||
}
|
||||
|
||||
s := &Service{
|
||||
ctx: context.Background(),
|
||||
auth: authRuntime,
|
||||
@@ -232,25 +229,22 @@ func (svr *Service) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if svr.vnetController != nil {
|
||||
vnetController := svr.vnetController
|
||||
if err := svr.vnetController.Init(); err != nil {
|
||||
log.Errorf("init virtual network controller error: %v", err)
|
||||
svr.stop()
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if svr.webServer != nil {
|
||||
webServer := svr.webServer
|
||||
go func() {
|
||||
log.Infof("admin server listen on %s", webServer.Address())
|
||||
if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Infof("admin server listen on %s", svr.webServer.Address())
|
||||
if err := svr.webServer.Run(); err != nil {
|
||||
log.Warnf("admin server exit with error: %v", err)
|
||||
}
|
||||
}()
|
||||
@@ -261,7 +255,6 @@ func (svr *Service) Run(ctx context.Context) error {
|
||||
if svr.ctl == nil {
|
||||
cancelCause := cancelErr{}
|
||||
_ = 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)
|
||||
}
|
||||
|
||||
@@ -504,10 +497,6 @@ func (svr *Service) stop() {
|
||||
svr.webServer.Close()
|
||||
svr.webServer = nil
|
||||
}
|
||||
if svr.vnetController != nil {
|
||||
_ = svr.vnetController.Stop()
|
||||
svr.vnetController = nil
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return &statusExporterImpl{
|
||||
getProxyStatusFunc: svr.getProxyStatus,
|
||||
|
||||
@@ -1,120 +1,14 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type failingConnector struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *failingConnector) Open() error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *failingConnector) Connect() (net.Conn, error) {
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
func (c *failingConnector) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFreeTCPPort(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen on ephemeral port: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func TestRunStopsStartedComponentsOnInitialLoginFailure(t *testing.T) {
|
||||
port := getFreeTCPPort(t)
|
||||
agg := source.NewAggregator(source.NewConfigSource())
|
||||
|
||||
svr, err := NewService(ServiceOptions{
|
||||
Common: &v1.ClientCommonConfig{
|
||||
LoginFailExit: lo.ToPtr(true),
|
||||
WebServer: v1.WebServerConfig{
|
||||
Addr: "127.0.0.1",
|
||||
Port: port,
|
||||
},
|
||||
},
|
||||
ConfigSourceAggregator: agg,
|
||||
ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector {
|
||||
return &failingConnector{err: errors.New("login boom")}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new service: %v", err)
|
||||
}
|
||||
|
||||
err = svr.Run(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected run error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "login boom") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if svr.webServer != nil {
|
||||
t.Fatal("expected web server to be cleaned up after initial login failure")
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
t.Fatalf("expected admin port to be released: %v", err)
|
||||
}
|
||||
_ = ln.Close()
|
||||
}
|
||||
|
||||
func TestNewServiceDoesNotLeakAdminListenerOnAuthBuildFailure(t *testing.T) {
|
||||
port := getFreeTCPPort(t)
|
||||
agg := source.NewAggregator(source.NewConfigSource())
|
||||
|
||||
_, err := NewService(ServiceOptions{
|
||||
Common: &v1.ClientCommonConfig{
|
||||
Auth: v1.AuthClientConfig{
|
||||
Method: v1.AuthMethodOIDC,
|
||||
OIDC: v1.AuthOIDCClientConfig{
|
||||
TokenEndpointURL: "://bad",
|
||||
},
|
||||
},
|
||||
WebServer: v1.WebServerConfig{
|
||||
Addr: "127.0.0.1",
|
||||
Port: port,
|
||||
},
|
||||
},
|
||||
ConfigSourceAggregator: agg,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected new service error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "auth.oidc.tokenEndpointURL") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
t.Fatalf("expected admin port to remain free: %v", err)
|
||||
}
|
||||
_ = ln.Close()
|
||||
}
|
||||
|
||||
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||
|
||||
@@ -15,12 +15,18 @@
|
||||
package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -36,10 +42,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
||||
go sv.worker()
|
||||
}
|
||||
|
||||
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
||||
go sv.internalConnWorker()
|
||||
|
||||
if sv.plugin != nil {
|
||||
sv.plugin.Start()
|
||||
@@ -51,10 +57,35 @@ func (sv *STCPVisitor) Close() {
|
||||
sv.BaseVisitor.Close()
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) worker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("stcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) internalConnWorker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("stcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
// If there was an error and connection supports CloseWithError, use it
|
||||
if tunnelErr != nil {
|
||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||
_ = eConn.CloseWithError(tunnelErr)
|
||||
@@ -65,21 +96,62 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
}()
|
||||
|
||||
xl.Debugf("get a new stcp user connection")
|
||||
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
if err != nil {
|
||||
xl.Warnf("dialRawVisitorConn error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
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 {
|
||||
xl.Warnf("wrapVisitorConn error: %v", err)
|
||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||
tunnelErr = err
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -16,17 +16,21 @@ package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
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/proto/udp"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
)
|
||||
|
||||
@@ -72,7 +76,6 @@ func (sv *SUDPVisitor) dispatcher() {
|
||||
|
||||
var (
|
||||
visitorConn net.Conn
|
||||
recycleFn func()
|
||||
err error
|
||||
|
||||
firstPacket *msg.UDPPacket
|
||||
@@ -90,17 +93,14 @@ func (sv *SUDPVisitor) dispatcher() {
|
||||
return
|
||||
}
|
||||
|
||||
visitorConn, recycleFn, err = sv.getNewVisitorConn()
|
||||
visitorConn, err = sv.getNewVisitorConn()
|
||||
if err != nil {
|
||||
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// visitorConn always be closed when worker done.
|
||||
func() {
|
||||
defer recycleFn()
|
||||
sv.worker(visitorConn, firstPacket)
|
||||
}()
|
||||
sv.worker(visitorConn, firstPacket)
|
||||
|
||||
select {
|
||||
case <-sv.checkCloseCh:
|
||||
@@ -198,17 +198,53 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||
xl.Infof("sudp worker is closed")
|
||||
}
|
||||
|
||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
||||
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
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 {
|
||||
rawConn.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 {
|
||||
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
}
|
||||
|
||||
var remote io.ReadWriteCloser
|
||||
remote = visitorConn
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if sv.cfg.Transport.UseCompression {
|
||||
remote = libio.WithCompression(remote)
|
||||
}
|
||||
return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
||||
}
|
||||
|
||||
func (sv *SUDPVisitor) Close() {
|
||||
|
||||
@@ -16,21 +16,13 @@ package visitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
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"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
@@ -127,18 +119,6 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
||||
return v.internalLn.PutConn(conn)
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
||||
xl := xlog.FromContextSafe(v.ctx)
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("%s listener closed", name)
|
||||
return
|
||||
}
|
||||
go handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) Close() {
|
||||
if v.l != nil {
|
||||
v.l.Close()
|
||||
@@ -150,57 +130,3 @@ func (v *BaseVisitor) Close() {
|
||||
v.plugin.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
||||
visitorConn, err := v.helper.ConnectServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to server error: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: v.helper.RunID(),
|
||||
ProxyName: targetProxyName,
|
||||
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: cfg.Transport.UseEncryption,
|
||||
UseCompression: cfg.Transport.UseCompression,
|
||||
}
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||
}
|
||||
|
||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||
if err != nil {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||
}
|
||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||
|
||||
if newVisitorConnRespMsg.Error != "" {
|
||||
visitorConn.Close()
|
||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||
}
|
||||
return visitorConn, nil
|
||||
}
|
||||
|
||||
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
||||
rwc := conn
|
||||
if cfg.Transport.UseEncryption {
|
||||
var err error
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
||||
if err != nil {
|
||||
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
||||
}
|
||||
}
|
||||
recycleFn := func() {}
|
||||
if cfg.Transport.UseCompression {
|
||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||
}
|
||||
return rwc, recycleFn, nil
|
||||
}
|
||||
|
||||
@@ -191,13 +191,6 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
||||
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 {
|
||||
connectServerFn func() (net.Conn, error)
|
||||
msgTransporter transport.MessageTransporter
|
||||
|
||||
@@ -65,10 +65,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
||||
go sv.worker()
|
||||
}
|
||||
|
||||
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
||||
go sv.internalConnWorker()
|
||||
go sv.processTunnelStartEvents()
|
||||
if sv.cfg.KeepTunnelOpen {
|
||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||
@@ -93,6 +93,30 @@ func (sv *XTCPVisitor) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) worker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.l.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("xtcp local listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) internalConnWorker() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
for {
|
||||
conn, err := sv.internalLn.Accept()
|
||||
if err != nil {
|
||||
xl.Warnf("xtcp internal listener closed")
|
||||
return
|
||||
}
|
||||
go sv.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||
for {
|
||||
select {
|
||||
@@ -182,14 +206,20 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
||||
if err != nil {
|
||||
xl.Errorf("%v", err)
|
||||
tunnelConn.Close()
|
||||
tunnelErr = err
|
||||
return
|
||||
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
|
||||
if sv.cfg.Transport.UseEncryption {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []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()
|
||||
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
||||
defer recycleFn()
|
||||
}
|
||||
defer recycleFn()
|
||||
|
||||
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||
xl.Debugf("join connections closed")
|
||||
@@ -343,7 +373,6 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
||||
}
|
||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||
if err != nil {
|
||||
lConn.Close()
|
||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ var natholeDiscoveryCmd = &cobra.Command{
|
||||
Use: "discover",
|
||||
Short: "Discover nathole information from stun server",
|
||||
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)
|
||||
if err != nil {
|
||||
cfg = &v1.ClientCommonConfig{}
|
||||
|
||||
@@ -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
BIN
doc/pic/architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -1,11 +1,8 @@
|
||||
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
|
||||
COPY web/frpc/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.25 AS building
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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
|
||||
COPY web/frps/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.25 AS building
|
||||
|
||||
32
go.mod
32
go.mod
@@ -5,7 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
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/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
@@ -13,7 +13,7 @@ require (
|
||||
github.com/onsi/ginkgo/v2 v2.23.4
|
||||
github.com/onsi/gomega v1.36.3
|
||||
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/prometheus/client_golang v1.19.1
|
||||
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/spf13/cobra v1.8.0
|
||||
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/vishvananda/netlink v1.3.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/time v0.10.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
k8s.io/apimachinery v0.28.8
|
||||
@@ -38,7 +38,7 @@ 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/cespare/xxhash/v2 v2.2.0 // 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/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/logging v0.2.2 // 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/pmezard/go-difflib v1.0.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/tjfoc/gmsm v1.4.1 // 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
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
105
go.sum
105
go.sum
@@ -1,6 +1,6 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
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 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
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/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=
|
||||
@@ -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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
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.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
||||
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/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||
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/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/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
||||
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/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
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/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.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.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.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
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/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
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/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/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/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
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-20200622213623-75b288015ac9/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.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.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/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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.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-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-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-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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
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-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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.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-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-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.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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
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.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.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.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=
|
||||
|
||||
111
pkg/auth/oidc.go
111
pkg/auth/oidc.go
@@ -23,14 +23,12 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
@@ -76,64 +74,14 @@ func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyUR
|
||||
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 {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenSource oauth2.TokenSource
|
||||
tokenGenerator *clientcredentials.Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
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)
|
||||
for k, v := range cfg.AdditionalEndpointParams {
|
||||
eps[k] = []string{v}
|
||||
@@ -151,42 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
||||
EndpointParams: eps,
|
||||
}
|
||||
|
||||
// Build the context that TokenSource will use for all future HTTP requests.
|
||||
// context.Background() is appropriate here because the token source is
|
||||
// long-lived and outlives any single request.
|
||||
ctx := context.Background()
|
||||
// Create custom HTTP client if needed
|
||||
var httpClient *http.Client
|
||||
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 {
|
||||
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{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
tokenSource: &oidcTokenSource{
|
||||
source: cachingSource,
|
||||
fallbackCfg: tokenGenerator,
|
||||
fallbackCtx: ctx,
|
||||
},
|
||||
tokenGenerator: tokenGenerator,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||
}
|
||||
@@ -269,8 +205,7 @@ type OidcAuthConsumer struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
verifier TokenVerifier
|
||||
mu sync.RWMutex
|
||||
subjectsFromLogin map[string]struct{}
|
||||
subjectsFromLogin []string
|
||||
}
|
||||
|
||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||
@@ -291,7 +226,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri
|
||||
return &OidcAuthConsumer{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
verifier: verifier,
|
||||
subjectsFromLogin: make(map[string]struct{}),
|
||||
subjectsFromLogin: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,9 +235,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||
}
|
||||
auth.mu.Lock()
|
||||
auth.subjectsFromLogin[token.Subject] = struct{}{}
|
||||
auth.mu.Unlock()
|
||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -311,13 +246,11 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||
}
|
||||
auth.mu.RLock()
|
||||
_, ok := auth.subjectsFromLogin[token.Subject]
|
||||
auth.mu.RUnlock()
|
||||
if !ok {
|
||||
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||
"original subjects: %s, "+
|
||||
"new subject: %s",
|
||||
token.Subject)
|
||||
auth.subjectsFromLogin, token.Subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -66,188 +62,3 @@ func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||
}
|
||||
|
||||
func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
var requestCount atomic.Int32
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
requestCount.Add(1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||
"access_token": "fresh-test-token",
|
||||
"token_type": "Bearer",
|
||||
})
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
provider, err := auth.NewOidcAuthSetter(
|
||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||
v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
ClientSecret: "test-secret",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
},
|
||||
)
|
||||
r.NoError(err)
|
||||
|
||||
// Constructor no longer fetches a token eagerly.
|
||||
// The first SetLogin triggers the adaptive probe.
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("fresh-test-token", loginMsg.PrivilegeKey)
|
||||
|
||||
for range 3 {
|
||||
pingMsg := &msg.Ping{}
|
||||
err = provider.SetPing(pingMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("fresh-test-token", pingMsg.PrivilegeKey)
|
||||
}
|
||||
|
||||
// 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch)
|
||||
r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
||||
}
|
||||
|
||||
func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
var requestCount atomic.Int32
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
requestCount.Add(1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||
"access_token": "cached-test-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
provider, err := auth.NewOidcAuthSetter(
|
||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||
v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
ClientSecret: "test-secret",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
},
|
||||
)
|
||||
r.NoError(err)
|
||||
|
||||
// Constructor no longer fetches eagerly; first SetLogin triggers the probe.
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
// SetLogin triggers the adaptive probe and caches the token.
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("cached-test-token", loginMsg.PrivilegeKey)
|
||||
r.Equal(int32(1), requestCount.Load())
|
||||
|
||||
// Subsequent calls should also reuse the cached token
|
||||
for range 5 {
|
||||
pingMsg := &msg.Ping{}
|
||||
err = provider.SetPing(pingMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("cached-test-token", pingMsg.PrivilegeKey)
|
||||
}
|
||||
r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused")
|
||||
}
|
||||
|
||||
func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
var requestCount atomic.Int32
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
n := requestCount.Add(1)
|
||||
// The oauth2 library retries once internally, so we need two
|
||||
// consecutive failures to surface an error to the caller.
|
||||
if n <= 2 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"error": "temporarily_unavailable",
|
||||
"error_description": "service is starting up",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||
"access_token": "retry-test-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
// Constructor succeeds even though the IdP is "down".
|
||||
provider, err := auth.NewOidcAuthSetter(
|
||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||
v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
ClientSecret: "test-secret",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
},
|
||||
)
|
||||
r.NoError(err)
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
// First SetLogin hits the IdP, which returns an error (after internal retry).
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.Error(err)
|
||||
r.Equal(int32(2), requestCount.Load())
|
||||
|
||||
// Second SetLogin retries and succeeds.
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("retry-test-token", loginMsg.PrivilegeKey)
|
||||
r.Equal(int32(3), requestCount.Load())
|
||||
|
||||
// Subsequent calls use cached token.
|
||||
pingMsg := &msg.Ping{}
|
||||
err = provider.SetPing(pingMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("retry-test-token", pingMsg.PrivilegeKey)
|
||||
r.Equal(int32(3), requestCount.Load())
|
||||
}
|
||||
|
||||
func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) {
|
||||
r := require.New(t)
|
||||
tokenServer := httptest.NewServer(http.NotFoundHandler())
|
||||
defer tokenServer.Close()
|
||||
|
||||
_, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: "://bad",
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.tokenEndpointURL")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.clientID is required")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
AdditionalEndpointParams: map[string]string{
|
||||
"scope": "profile",
|
||||
},
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
Audience: "api",
|
||||
AdditionalEndpointParams: map[string]string{"audience": "override"},
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||
}
|
||||
|
||||
@@ -171,14 +171,15 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
||||
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
||||
out := v1.HeaderOperations{}
|
||||
for k, v := range params {
|
||||
k, ok := strings.CutPrefix(k, "plugin_header_")
|
||||
if !ok || k == "" {
|
||||
if !strings.HasPrefix(k, "plugin_header_") {
|
||||
continue
|
||||
}
|
||||
if out.Set == nil {
|
||||
out.Set = make(map[string]string)
|
||||
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
|
||||
if out.Set == nil {
|
||||
out.Set = make(map[string]string)
|
||||
}
|
||||
out.Set[k] = v
|
||||
}
|
||||
out.Set[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ const (
|
||||
// Proxy
|
||||
var (
|
||||
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
||||
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](),
|
||||
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](),
|
||||
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](),
|
||||
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](),
|
||||
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](),
|
||||
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](),
|
||||
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](),
|
||||
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](),
|
||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
|
||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
|
||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
|
||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
|
||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
|
||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
|
||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
|
||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string
|
||||
m := make(map[string]string)
|
||||
|
||||
for key, value := range set {
|
||||
if trimmed, ok := strings.CutPrefix(key, prefix); ok {
|
||||
m[trimmed] = value
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
m[strings.TrimPrefix(key, prefix)] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ const (
|
||||
// Visitor
|
||||
var (
|
||||
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
||||
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](),
|
||||
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](),
|
||||
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](),
|
||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
|
||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
|
||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
@@ -99,11 +97,21 @@ func (a *Aggregator) mapsToSortedSlices(
|
||||
proxyMap map[string]v1.ProxyConfigurer,
|
||||
visitorMap map[string]v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||
proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int {
|
||||
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||
proxies := make([]v1.ProxyConfigurer, 0, len(proxyMap))
|
||||
for _, p := range proxyMap {
|
||||
proxies = append(proxies, p)
|
||||
}
|
||||
sort.Slice(proxies, func(i, j int) bool {
|
||||
return proxies[i].GetBaseConfig().Name < proxies[j].GetBaseConfig().Name
|
||||
})
|
||||
visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int {
|
||||
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
||||
|
||||
visitors := make([]v1.VisitorConfigurer, 0, len(visitorMap))
|
||||
for _, v := range visitorMap {
|
||||
visitors = append(visitors, v)
|
||||
}
|
||||
sort.Slice(visitors, func(i, j int) bool {
|
||||
return visitors[i].GetBaseConfig().Name < visitors[j].GetBaseConfig().Name
|
||||
})
|
||||
|
||||
return proxies, visitors
|
||||
}
|
||||
|
||||
@@ -196,27 +196,6 @@ func TestAggregator_VisitorMerge(t *testing.T) {
|
||||
require.Len(visitors, 2)
|
||||
}
|
||||
|
||||
func TestAggregator_Load_ReturnsSortedByName(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := newTestAggregator(t, nil)
|
||||
err := agg.ConfigSource().ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")},
|
||||
[]v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 3)
|
||||
require.Equal("alice", proxies[0].GetBaseConfig().Name)
|
||||
require.Equal("bob", proxies[1].GetBaseConfig().Name)
|
||||
require.Equal("charlie", proxies[2].GetBaseConfig().Name)
|
||||
require.Len(visitors, 2)
|
||||
require.Equal("alpha", visitors[0].GetBaseConfig().Name)
|
||||
require.Equal("zulu", visitors[1].GetBaseConfig().Name)
|
||||
}
|
||||
|
||||
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, e
|
||||
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
||||
}
|
||||
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
||||
for i := range firstRangeNumbers {
|
||||
for i := 0; i < len(firstRangeNumbers); i++ {
|
||||
pairs = append(pairs, NumberPair{
|
||||
First: firstRangeNumbers[i],
|
||||
Second: secondRangeNumbers[i],
|
||||
|
||||
@@ -70,18 +70,24 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error {
|
||||
f float64
|
||||
err error
|
||||
)
|
||||
if fstr, ok := strings.CutSuffix(s, "MB"); ok {
|
||||
switch {
|
||||
case strings.HasSuffix(s, "MB"):
|
||||
base = MB
|
||||
fstr := strings.TrimSuffix(s, "MB")
|
||||
f, err = strconv.ParseFloat(fstr, 64)
|
||||
} else if fstr, ok := strings.CutSuffix(s, "KB"); ok {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.HasSuffix(s, "KB"):
|
||||
base = KB
|
||||
fstr := strings.TrimSuffix(s, "KB")
|
||||
f, err = strconv.ParseFloat(fstr, 64)
|
||||
} else {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unit not support")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.s = s
|
||||
q.i = int64(f * float64(base))
|
||||
@@ -137,8 +143,8 @@ func (p PortsRangeSlice) String() string {
|
||||
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
||||
str = strings.TrimSpace(str)
|
||||
out := []PortsRange{}
|
||||
numRanges := strings.SplitSeq(str, ",")
|
||||
for numRangeStr := range numRanges {
|
||||
numRanges := strings.Split(str, ",")
|
||||
for _, numRangeStr := range numRanges {
|
||||
// 1000-2000 or 2001
|
||||
numArray := strings.Split(numRangeStr, "-")
|
||||
// length: only 1 or 2 is correct
|
||||
|
||||
@@ -39,31 +39,6 @@ func TestBandwidthQuantity(t *testing.T) {
|
||||
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
||||
}
|
||||
|
||||
func TestBandwidthQuantity_MB(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
var w Wrap
|
||||
err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w)
|
||||
require.NoError(err)
|
||||
require.EqualValues(2*MB, w.B.Bytes())
|
||||
|
||||
buf, err := json.Marshal(&w)
|
||||
require.NoError(err)
|
||||
require.Equal(`{"b":"2MB","int":1}`, string(buf))
|
||||
}
|
||||
|
||||
func TestBandwidthQuantity_InvalidUnit(t *testing.T) {
|
||||
var w Wrap
|
||||
err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBandwidthQuantity_InvalidNumber(t *testing.T) {
|
||||
var w Wrap
|
||||
err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPortsRangeSlice2String(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
|
||||
@@ -239,14 +239,14 @@ const (
|
||||
)
|
||||
|
||||
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
||||
ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](),
|
||||
ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](),
|
||||
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](),
|
||||
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](),
|
||||
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](),
|
||||
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](),
|
||||
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](),
|
||||
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](),
|
||||
ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}),
|
||||
ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}),
|
||||
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}),
|
||||
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}),
|
||||
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
|
||||
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}),
|
||||
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}),
|
||||
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}),
|
||||
}
|
||||
|
||||
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
||||
|
||||
@@ -37,16 +37,16 @@ const (
|
||||
)
|
||||
|
||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||
PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](),
|
||||
PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](),
|
||||
PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](),
|
||||
PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](),
|
||||
PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](),
|
||||
PluginSocks5: reflect.TypeFor[Socks5PluginOptions](),
|
||||
PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](),
|
||||
PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](),
|
||||
PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](),
|
||||
PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](),
|
||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||
}
|
||||
|
||||
type ClientPluginOptions interface {
|
||||
|
||||
@@ -88,11 +88,6 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
|
||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "; "))
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -79,9 +79,9 @@ const (
|
||||
)
|
||||
|
||||
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
||||
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](),
|
||||
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](),
|
||||
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](),
|
||||
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
|
||||
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
|
||||
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
|
||||
}
|
||||
|
||||
type TypedVisitorConfig struct {
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
)
|
||||
|
||||
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
||||
VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](),
|
||||
VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
|
||||
}
|
||||
|
||||
type VisitorPluginOptions interface {
|
||||
|
||||
@@ -143,6 +143,7 @@ func (m *serverMetrics) OpenConnection(name string, _ string) {
|
||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||
if ok {
|
||||
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]
|
||||
if ok {
|
||||
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]
|
||||
if ok {
|
||||
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]
|
||||
if ok {
|
||||
proxyStats.TrafficOut.Inc(trafficBytes)
|
||||
m.info.ProxyStatistics[name] = proxyStats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,25 +203,6 @@ func (m *serverMetrics) GetServer() *ServerStats {
|
||||
return s
|
||||
}
|
||||
|
||||
func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats {
|
||||
ps := &ProxyStats{
|
||||
Name: name,
|
||||
Type: proxyStats.ProxyType,
|
||||
User: proxyStats.User,
|
||||
ClientID: proxyStats.ClientID,
|
||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||
CurConns: int64(proxyStats.CurConns.Count()),
|
||||
}
|
||||
if !proxyStats.LastStartTime.IsZero() {
|
||||
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||
}
|
||||
if !proxyStats.LastCloseTime.IsZero() {
|
||||
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
||||
res := make([]*ProxyStats, 0)
|
||||
m.mu.Lock()
|
||||
@@ -227,7 +212,23 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
||||
if proxyStats.ProxyType != proxyType {
|
||||
continue
|
||||
}
|
||||
res = append(res, toProxyStats(name, proxyStats))
|
||||
|
||||
ps := &ProxyStats{
|
||||
Name: name,
|
||||
Type: proxyStats.ProxyType,
|
||||
User: proxyStats.User,
|
||||
ClientID: proxyStats.ClientID,
|
||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||
CurConns: int64(proxyStats.CurConns.Count()),
|
||||
}
|
||||
if !proxyStats.LastStartTime.IsZero() {
|
||||
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||
}
|
||||
if !proxyStats.LastCloseTime.IsZero() {
|
||||
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||
}
|
||||
res = append(res, ps)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -236,9 +237,31 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||
if ok && proxyStats.ProxyType == proxyType {
|
||||
res = toProxyStats(proxyName, proxyStats)
|
||||
for name, proxyStats := range m.info.ProxyStatistics {
|
||||
if proxyStats.ProxyType != proxyType {
|
||||
continue
|
||||
}
|
||||
|
||||
if name != proxyName {
|
||||
continue
|
||||
}
|
||||
|
||||
res = &ProxyStats{
|
||||
Name: name,
|
||||
Type: proxyStats.ProxyType,
|
||||
User: proxyStats.User,
|
||||
ClientID: proxyStats.ClientID,
|
||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||
CurConns: int64(proxyStats.CurConns.Count()),
|
||||
}
|
||||
if !proxyStats.LastStartTime.IsZero() {
|
||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||
}
|
||||
if !proxyStats.LastCloseTime.IsZero() {
|
||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||
}
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -249,7 +272,21 @@ func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
||||
|
||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||
if ok {
|
||||
res = toProxyStats(proxyName, proxyStats)
|
||||
res = &ProxyStats{
|
||||
Name: proxyName,
|
||||
Type: proxyStats.ProxyType,
|
||||
User: proxyStats.User,
|
||||
ClientID: proxyStats.ClientID,
|
||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||
CurConns: int64(proxyStats.CurConns.Count()),
|
||||
}
|
||||
if !proxyStats.LastStartTime.IsZero() {
|
||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||
}
|
||||
if !proxyStats.LastCloseTime.IsZero() {
|
||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{
|
||||
TypeNatHoleReport: NatHoleReport{},
|
||||
}
|
||||
|
||||
var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name()
|
||||
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
|
||||
|
||||
type ClientSpec struct {
|
||||
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
||||
|
||||
@@ -16,8 +16,9 @@ func StripUserPrefix(user, name string) string {
|
||||
if user == "" {
|
||||
return name
|
||||
}
|
||||
if trimmed, ok := strings.CutPrefix(name, user+"."); ok {
|
||||
return trimmed
|
||||
prefix := user + "."
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return strings.TrimPrefix(name, prefix)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func getBehaviorScoresByMode(mode int, defaultScore int) []*BehaviorScore {
|
||||
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
||||
behaviors := getBehaviorByMode(mode)
|
||||
scores := make([]*BehaviorScore, 0, len(behaviors))
|
||||
for i := range behaviors {
|
||||
for i := 0; i < len(behaviors); i++ {
|
||||
score := receiverScore
|
||||
if behaviors[i].A.Role == DetectRoleSender {
|
||||
score = senderScore
|
||||
|
||||
@@ -70,8 +70,12 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err
|
||||
continue
|
||||
}
|
||||
|
||||
portMax = max(portMax, portNum)
|
||||
portMin = min(portMin, portNum)
|
||||
if portNum > portMax {
|
||||
portMax = portNum
|
||||
}
|
||||
if portNum < portMin {
|
||||
portMin = portNum
|
||||
}
|
||||
if baseIP != ip {
|
||||
ipChanged = true
|
||||
}
|
||||
|
||||
@@ -152,9 +152,7 @@ func (c *Controller) GenSid() string {
|
||||
|
||||
func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
|
||||
if m.PreCheck {
|
||||
c.mu.RLock()
|
||||
cfg, ok := c.clientCfgs[m.ProxyName]
|
||||
c.mu.RUnlock()
|
||||
if !ok {
|
||||
_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
"github.com/pion/stun/v2"
|
||||
)
|
||||
|
||||
var responseTimeout = 3 * time.Second
|
||||
|
||||
@@ -298,13 +298,11 @@ func waitDetectMessage(
|
||||
n, raddr, err := conn.ReadFromUDP(buf)
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
pool.PutBuf(buf)
|
||||
return nil, err
|
||||
}
|
||||
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
|
||||
var m msg.NatHoleSid
|
||||
if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
|
||||
pool.PutBuf(buf)
|
||||
xl.Warnf("decode sid message error: %v", err)
|
||||
continue
|
||||
}
|
||||
@@ -410,7 +408,7 @@ func sendSidMessageToRandomPorts(
|
||||
xl := xlog.FromContextSafe(ctx)
|
||||
used := sets.New[int]()
|
||||
getUnusedPort := func() int {
|
||||
for range 10 {
|
||||
for i := 0; i < 10; i++ {
|
||||
port := rand.IntN(65535-1024) + 1024
|
||||
if !used.Has(port) {
|
||||
used.Insert(port)
|
||||
@@ -420,7 +418,7 @@ func sendSidMessageToRandomPorts(
|
||||
return 0
|
||||
}
|
||||
|
||||
for range count {
|
||||
for i := 0; i < count; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/fatedier/golib/crypto"
|
||||
"github.com/pion/stun/v3"
|
||||
"github.com/pion/stun/v2"
|
||||
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
)
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
stdlog "log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/pool"
|
||||
|
||||
@@ -69,7 +68,7 @@ func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
|
||||
|
||||
p.s = &http.Server{
|
||||
Handler: rp,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: 0,
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
stdlog "log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/golib/pool"
|
||||
|
||||
@@ -78,7 +77,7 @@ func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi
|
||||
|
||||
p.s = &http.Server{
|
||||
Handler: rp,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: 0,
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -62,13 +62,11 @@ func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
|
||||
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
xl.Warnf("tls handshake error: %v", err)
|
||||
tlsConn.Close()
|
||||
return
|
||||
}
|
||||
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
|
||||
if err != nil {
|
||||
xl.Warnf("dial to local addr error: %v", err)
|
||||
tlsConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -54,13 +54,10 @@ func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *Connect
|
||||
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
|
||||
if err != nil {
|
||||
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
|
||||
connInfo.Conn.Close()
|
||||
return
|
||||
}
|
||||
if connInfo.ProxyProtocolHeader != nil {
|
||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||
localConn.Close()
|
||||
connInfo.Conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
@@ -65,7 +64,12 @@ func (p *httpPlugin) Name() string {
|
||||
}
|
||||
|
||||
func (p *httpPlugin) IsSupport(op string) bool {
|
||||
return slices.Contains(p.options.Ops, op)
|
||||
for _, v := range p.options.Ops {
|
||||
if v == op {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) {
|
||||
|
||||
@@ -153,7 +153,10 @@ func (p *VirtualNetPlugin) run() {
|
||||
|
||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||
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 {
|
||||
// Reset consecutive errors on successful connection
|
||||
if p.consecutiveErrors > 0 {
|
||||
|
||||
@@ -16,7 +16,6 @@ package featuregate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -93,7 +92,10 @@ type featureGate struct {
|
||||
|
||||
// NewFeatureGate creates a new feature gate with the default features
|
||||
func NewFeatureGate() MutableFeatureGate {
|
||||
known := maps.Clone(defaultFeatures)
|
||||
known := map[Feature]FeatureSpec{}
|
||||
for k, v := range defaultFeatures {
|
||||
known[k] = v
|
||||
}
|
||||
|
||||
f := &featureGate{}
|
||||
f.known.Store(known)
|
||||
@@ -107,8 +109,14 @@ func (f *featureGate) SetFromMap(m map[string]bool) error {
|
||||
defer f.lock.Unlock()
|
||||
|
||||
// Copy existing state
|
||||
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))
|
||||
enabled := maps.Clone(f.enabled.Load().(map[Feature]bool))
|
||||
known := map[Feature]FeatureSpec{}
|
||||
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
|
||||
for k, v := range m {
|
||||
@@ -139,7 +147,10 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
for name, spec := range features {
|
||||
|
||||
@@ -85,7 +85,6 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
||||
}()
|
||||
|
||||
buf := pool.GetBuf(bufSize)
|
||||
defer pool.PutBuf(buf)
|
||||
for {
|
||||
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
n, _, err := udpConn.ReadFromUDP(buf)
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
@@ -86,9 +85,7 @@ func newCertPool(caPath string) (*x509.CertPool, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !pool.AppendCertsFromPEM(caCrt) {
|
||||
return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath)
|
||||
}
|
||||
pool.AppendCertsFromPEM(caCrt)
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
@@ -89,11 +89,11 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
before, after, found := strings.Cut(cs, ":")
|
||||
if !found {
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return before, after, true
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
|
||||
func BasicAuth(username, passwd string) string {
|
||||
|
||||
@@ -100,11 +100,7 @@ func (s *Server) Run() error {
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
err := s.hs.Close()
|
||||
if s.ln != nil {
|
||||
_ = s.ln.Close()
|
||||
}
|
||||
return err
|
||||
return s.hs.Close()
|
||||
}
|
||||
|
||||
type RouterRegisterHelper struct {
|
||||
|
||||
@@ -86,7 +86,11 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) {
|
||||
c.lastActive = time.Now()
|
||||
c.mu.Unlock()
|
||||
|
||||
n = min(len(b), len(content))
|
||||
if len(b) < len(content) {
|
||||
n = len(b)
|
||||
} else {
|
||||
n = len(content)
|
||||
}
|
||||
copy(b, content)
|
||||
return n, nil
|
||||
}
|
||||
@@ -164,15 +168,11 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) {
|
||||
return l, err
|
||||
}
|
||||
readConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
l = &UDPListener{
|
||||
addr: udpAddr,
|
||||
acceptCh: make(chan net.Conn),
|
||||
writeCh: make(chan *UDPPacket, 1000),
|
||||
readConn: readConn,
|
||||
fakeConns: make(map[string]*FakeUDPConn),
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ type WebsocketListener struct {
|
||||
// ln: tcp listener for websocket connections
|
||||
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
||||
wl = &WebsocketListener{
|
||||
ln: ln,
|
||||
acceptCh: make(chan net.Conn),
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) {
|
||||
rangeStr = strings.TrimSpace(rangeStr)
|
||||
numbers = make([]int64, 0)
|
||||
// e.g. 1000-2000,2001,2002,3000-4000
|
||||
numRanges := strings.SplitSeq(rangeStr, ",")
|
||||
for numRangeStr := range numRanges {
|
||||
numRanges := strings.Split(rangeStr, ",")
|
||||
for _, numRangeStr := range numRanges {
|
||||
// 1000-2000 or 2001
|
||||
numArray := strings.Split(numRangeStr, "-")
|
||||
// length: only 1 or 2 is correct
|
||||
|
||||
@@ -266,13 +266,31 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
|
||||
go libio.Join(remote, client)
|
||||
}
|
||||
|
||||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||||
const prefix = "Basic "
|
||||
// Case insensitive prefix match. See Issue 22736.
|
||||
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||
return
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
|
||||
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
||||
user := ""
|
||||
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
|
||||
if req.URL.Host != "" {
|
||||
proxyAuth := req.Header.Get("Proxy-Authorization")
|
||||
if proxyAuth != "" {
|
||||
user, _, _ = httppkg.ParseBasicAuth(proxyAuth)
|
||||
user, _, _ = parseBasicAuth(proxyAuth)
|
||||
}
|
||||
}
|
||||
if user == "" {
|
||||
|
||||
@@ -63,12 +63,11 @@ func (l *Logger) AddPrefix(prefix LogPrefix) *Logger {
|
||||
if prefix.Priority <= 0 {
|
||||
prefix.Priority = 10
|
||||
}
|
||||
for i, p := range l.prefixes {
|
||||
for _, p := range l.prefixes {
|
||||
if p.Name == prefix.Name {
|
||||
found = true
|
||||
l.prefixes[i].Value = prefix.Value
|
||||
l.prefixes[i].Priority = prefix.Priority
|
||||
break
|
||||
p.Value = prefix.Value
|
||||
p.Priority = prefix.Priority
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
|
||||
@@ -131,9 +131,6 @@ func (c *Controller) handlePacket(buf []byte) {
|
||||
}
|
||||
|
||||
func (c *Controller) Stop() error {
|
||||
if c.tun == nil {
|
||||
return nil
|
||||
}
|
||||
return c.tun.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -95,33 +95,20 @@ func (cm *ControlManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SessionContext encapsulates the input parameters for creating a new Control.
|
||||
type SessionContext struct {
|
||||
// all resource managers and controllers
|
||||
RC *controller.ResourceController
|
||||
// proxy manager
|
||||
PxyManager *proxy.Manager
|
||||
// plugin manager
|
||||
PluginManager *plugin.Manager
|
||||
// verifies authentication based on selected method
|
||||
AuthVerifier auth.Verifier
|
||||
// key used for connection encryption
|
||||
EncryptionKey []byte
|
||||
// control connection
|
||||
Conn net.Conn
|
||||
// indicates whether the connection is encrypted
|
||||
ConnEncrypted bool
|
||||
// login message
|
||||
LoginMsg *msg.Login
|
||||
// server configuration
|
||||
ServerCfg *v1.ServerConfig
|
||||
// client registry
|
||||
ClientRegistry *registry.ClientRegistry
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
// session context
|
||||
sessionCtx *SessionContext
|
||||
// all resource managers and controllers
|
||||
rc *controller.ResourceController
|
||||
|
||||
// proxy manager
|
||||
pxyManager *proxy.Manager
|
||||
|
||||
// plugin manager
|
||||
pluginManager *plugin.Manager
|
||||
|
||||
// verifies authentication based on selected method
|
||||
authVerifier auth.Verifier
|
||||
// key used for connection encryption
|
||||
encryptionKey []byte
|
||||
|
||||
// other components can use this to communicate with client
|
||||
msgTransporter transport.MessageTransporter
|
||||
@@ -130,6 +117,12 @@ type Control struct {
|
||||
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
|
||||
msgDispatcher *msg.Dispatcher
|
||||
|
||||
// login message
|
||||
loginMsg *msg.Login
|
||||
|
||||
// control connection
|
||||
conn net.Conn
|
||||
|
||||
// work connections
|
||||
workConnCh chan net.Conn
|
||||
|
||||
@@ -152,34 +145,61 @@ type Control struct {
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// Server configuration information
|
||||
serverCfg *v1.ServerConfig
|
||||
|
||||
clientRegistry *registry.ClientRegistry
|
||||
|
||||
xl *xlog.Logger
|
||||
ctx context.Context
|
||||
doneCh chan struct{}
|
||||
}
|
||||
|
||||
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
|
||||
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount))
|
||||
// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext.
|
||||
func NewControl(
|
||||
ctx context.Context,
|
||||
rc *controller.ResourceController,
|
||||
pxyManager *proxy.Manager,
|
||||
pluginManager *plugin.Manager,
|
||||
authVerifier auth.Verifier,
|
||||
encryptionKey []byte,
|
||||
ctlConn net.Conn,
|
||||
ctlConnEncrypted bool,
|
||||
loginMsg *msg.Login,
|
||||
serverCfg *v1.ServerConfig,
|
||||
) (*Control, error) {
|
||||
poolCount := loginMsg.PoolCount
|
||||
if poolCount > int(serverCfg.Transport.MaxPoolCount) {
|
||||
poolCount = int(serverCfg.Transport.MaxPoolCount)
|
||||
}
|
||||
ctl := &Control{
|
||||
sessionCtx: sessionCtx,
|
||||
workConnCh: make(chan net.Conn, poolCount+10),
|
||||
proxies: make(map[string]proxy.Proxy),
|
||||
poolCount: poolCount,
|
||||
portsUsedNum: 0,
|
||||
runID: sessionCtx.LoginMsg.RunID,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
ctx: ctx,
|
||||
doneCh: make(chan struct{}),
|
||||
rc: rc,
|
||||
pxyManager: pxyManager,
|
||||
pluginManager: pluginManager,
|
||||
authVerifier: authVerifier,
|
||||
encryptionKey: encryptionKey,
|
||||
conn: ctlConn,
|
||||
loginMsg: loginMsg,
|
||||
workConnCh: make(chan net.Conn, poolCount+10),
|
||||
proxies: make(map[string]proxy.Proxy),
|
||||
poolCount: poolCount,
|
||||
portsUsedNum: 0,
|
||||
runID: loginMsg.RunID,
|
||||
serverCfg: serverCfg,
|
||||
xl: xlog.FromContextSafe(ctx),
|
||||
ctx: ctx,
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
ctl.lastPing.Store(time.Now())
|
||||
|
||||
if sessionCtx.ConnEncrypted {
|
||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.EncryptionKey)
|
||||
if ctlConnEncrypted {
|
||||
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
||||
} else {
|
||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
||||
}
|
||||
ctl.registerMsgHandlers()
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||
@@ -193,7 +213,7 @@ func (ctl *Control) Start() {
|
||||
RunID: ctl.runID,
|
||||
Error: "",
|
||||
}
|
||||
_ = msg.WriteMsg(ctl.sessionCtx.Conn, loginRespMsg)
|
||||
_ = msg.WriteMsg(ctl.conn, loginRespMsg)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < ctl.poolCount; i++ {
|
||||
@@ -205,7 +225,7 @@ func (ctl *Control) Start() {
|
||||
}
|
||||
|
||||
func (ctl *Control) Close() error {
|
||||
ctl.sessionCtx.Conn.Close()
|
||||
ctl.conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -213,7 +233,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
|
||||
xl := ctl.xl
|
||||
xl.Infof("replaced by client [%s]", newCtl.runID)
|
||||
ctl.runID = ""
|
||||
ctl.sessionCtx.Conn.Close()
|
||||
ctl.conn.Close()
|
||||
}
|
||||
|
||||
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
|
||||
@@ -271,7 +291,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
case <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second):
|
||||
case <-time.After(time.Duration(ctl.serverCfg.UserConnTimeout) * time.Second):
|
||||
err = fmt.Errorf("timeout trying to get work connection")
|
||||
xl.Warnf("%v", err)
|
||||
return
|
||||
@@ -284,15 +304,15 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
||||
}
|
||||
|
||||
func (ctl *Control) heartbeatWorker() {
|
||||
if ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout <= 0 {
|
||||
if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
xl := ctl.xl
|
||||
go wait.Until(func() {
|
||||
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout)*time.Second {
|
||||
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warnf("heartbeat timeout")
|
||||
ctl.sessionCtx.Conn.Close()
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
}, time.Second, ctl.doneCh)
|
||||
@@ -303,30 +323,6 @@ func (ctl *Control) WaitClosed() {
|
||||
<-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() {
|
||||
xl := ctl.xl
|
||||
|
||||
@@ -334,23 +330,38 @@ func (ctl *Control) worker() {
|
||||
go ctl.msgDispatcher.Run()
|
||||
|
||||
<-ctl.msgDispatcher.Done()
|
||||
ctl.sessionCtx.Conn.Close()
|
||||
ctl.conn.Close()
|
||||
|
||||
ctl.mu.Lock()
|
||||
defer ctl.mu.Unlock()
|
||||
|
||||
close(ctl.workConnCh)
|
||||
for workConn := range ctl.workConnCh {
|
||||
workConn.Close()
|
||||
}
|
||||
proxies := ctl.proxies
|
||||
ctl.proxies = make(map[string]proxy.Proxy)
|
||||
ctl.mu.Unlock()
|
||||
|
||||
for _, pxy := range proxies {
|
||||
ctl.closeProxy(pxy)
|
||||
for _, pxy := range ctl.proxies {
|
||||
pxy.Close()
|
||||
ctl.pxyManager.Del(pxy.GetName())
|
||||
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||
|
||||
notifyContent := &plugin.CloseProxyContent{
|
||||
User: plugin.UserInfo{
|
||||
User: ctl.loginMsg.User,
|
||||
Metas: ctl.loginMsg.Metas,
|
||||
RunID: ctl.loginMsg.RunID,
|
||||
},
|
||||
CloseProxy: msg.CloseProxy{
|
||||
ProxyName: pxy.GetName(),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
_ = ctl.pluginManager.CloseProxy(notifyContent)
|
||||
}()
|
||||
}
|
||||
|
||||
metrics.Server.CloseClient()
|
||||
ctl.sessionCtx.ClientRegistry.MarkOfflineByRunID(ctl.runID)
|
||||
ctl.clientRegistry.MarkOfflineByRunID(ctl.runID)
|
||||
xl.Infof("client exit success")
|
||||
close(ctl.doneCh)
|
||||
}
|
||||
@@ -369,11 +380,15 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
||||
inMsg := m.(*msg.NewProxy)
|
||||
|
||||
content := &plugin.NewProxyContent{
|
||||
User: ctl.loginUserInfo(),
|
||||
User: plugin.UserInfo{
|
||||
User: ctl.loginMsg.User,
|
||||
Metas: ctl.loginMsg.Metas,
|
||||
RunID: ctl.loginMsg.RunID,
|
||||
},
|
||||
NewProxy: *inMsg,
|
||||
}
|
||||
var remoteAddr string
|
||||
retContent, err := ctl.sessionCtx.PluginManager.NewProxy(content)
|
||||
retContent, err := ctl.pluginManager.NewProxy(content)
|
||||
if err == nil {
|
||||
inMsg = &retContent.NewProxy
|
||||
remoteAddr, err = ctl.RegisterProxy(inMsg)
|
||||
@@ -386,15 +401,15 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
||||
if err != nil {
|
||||
xl.Warnf("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
|
||||
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
|
||||
err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient))
|
||||
err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
|
||||
} else {
|
||||
resp.RemoteAddr = remoteAddr
|
||||
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
||||
clientID := ctl.sessionCtx.LoginMsg.ClientID
|
||||
clientID := ctl.loginMsg.ClientID
|
||||
if clientID == "" {
|
||||
clientID = ctl.sessionCtx.LoginMsg.RunID
|
||||
clientID = ctl.loginMsg.RunID
|
||||
}
|
||||
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.sessionCtx.LoginMsg.User, clientID)
|
||||
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.loginMsg.User, clientID)
|
||||
}
|
||||
_ = ctl.msgDispatcher.Send(resp)
|
||||
}
|
||||
@@ -404,18 +419,22 @@ func (ctl *Control) handlePing(m msg.Message) {
|
||||
inMsg := m.(*msg.Ping)
|
||||
|
||||
content := &plugin.PingContent{
|
||||
User: ctl.loginUserInfo(),
|
||||
User: plugin.UserInfo{
|
||||
User: ctl.loginMsg.User,
|
||||
Metas: ctl.loginMsg.Metas,
|
||||
RunID: ctl.loginMsg.RunID,
|
||||
},
|
||||
Ping: *inMsg,
|
||||
}
|
||||
retContent, err := ctl.sessionCtx.PluginManager.Ping(content)
|
||||
retContent, err := ctl.pluginManager.Ping(content)
|
||||
if err == nil {
|
||||
inMsg = &retContent.Ping
|
||||
err = ctl.sessionCtx.AuthVerifier.VerifyPing(inMsg)
|
||||
err = ctl.authVerifier.VerifyPing(inMsg)
|
||||
}
|
||||
if err != nil {
|
||||
xl.Warnf("received invalid ping: %v", err)
|
||||
_ = ctl.msgDispatcher.Send(&msg.Pong{
|
||||
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)),
|
||||
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -426,17 +445,17 @@ func (ctl *Control) handlePing(m msg.Message) {
|
||||
|
||||
func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
|
||||
inMsg := m.(*msg.NatHoleVisitor)
|
||||
ctl.sessionCtx.RC.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.sessionCtx.LoginMsg.User)
|
||||
ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User)
|
||||
}
|
||||
|
||||
func (ctl *Control) handleNatHoleClient(m msg.Message) {
|
||||
inMsg := m.(*msg.NatHoleClient)
|
||||
ctl.sessionCtx.RC.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
|
||||
ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
|
||||
}
|
||||
|
||||
func (ctl *Control) handleNatHoleReport(m msg.Message) {
|
||||
inMsg := m.(*msg.NatHoleReport)
|
||||
ctl.sessionCtx.RC.NatHoleController.HandleReport(inMsg)
|
||||
ctl.rc.NatHoleController.HandleReport(inMsg)
|
||||
}
|
||||
|
||||
func (ctl *Control) handleCloseProxy(m msg.Message) {
|
||||
@@ -449,15 +468,15 @@ func (ctl *Control) handleCloseProxy(m msg.Message) {
|
||||
func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
|
||||
var pxyConf v1.ProxyConfigurer
|
||||
// Load configures from NewProxy message and validate.
|
||||
pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.sessionCtx.ServerCfg)
|
||||
pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.serverCfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// User info
|
||||
userInfo := plugin.UserInfo{
|
||||
User: ctl.sessionCtx.LoginMsg.User,
|
||||
Metas: ctl.sessionCtx.LoginMsg.Metas,
|
||||
User: ctl.loginMsg.User,
|
||||
Metas: ctl.loginMsg.Metas,
|
||||
RunID: ctl.runID,
|
||||
}
|
||||
|
||||
@@ -465,22 +484,22 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
||||
// In fact, it creates different proxies based on the proxy type. We just call run() here.
|
||||
pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{
|
||||
UserInfo: userInfo,
|
||||
LoginMsg: ctl.sessionCtx.LoginMsg,
|
||||
LoginMsg: ctl.loginMsg,
|
||||
PoolCount: ctl.poolCount,
|
||||
ResourceController: ctl.sessionCtx.RC,
|
||||
ResourceController: ctl.rc,
|
||||
GetWorkConnFn: ctl.GetWorkConn,
|
||||
Configurer: pxyConf,
|
||||
ServerCfg: ctl.sessionCtx.ServerCfg,
|
||||
EncryptionKey: ctl.sessionCtx.EncryptionKey,
|
||||
ServerCfg: ctl.serverCfg,
|
||||
EncryptionKey: ctl.encryptionKey,
|
||||
})
|
||||
if err != nil {
|
||||
return remoteAddr, err
|
||||
}
|
||||
|
||||
// Check ports used number in each client
|
||||
if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 {
|
||||
if ctl.serverCfg.MaxPortsPerClient > 0 {
|
||||
ctl.mu.Lock()
|
||||
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.sessionCtx.ServerCfg.MaxPortsPerClient) {
|
||||
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.serverCfg.MaxPortsPerClient) {
|
||||
ctl.mu.Unlock()
|
||||
err = fmt.Errorf("exceed the max_ports_per_client")
|
||||
return
|
||||
@@ -497,7 +516,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
||||
}()
|
||||
}
|
||||
|
||||
if ctl.sessionCtx.PxyManager.Exist(pxyMsg.ProxyName) {
|
||||
if ctl.pxyManager.Exist(pxyMsg.ProxyName) {
|
||||
err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName)
|
||||
return
|
||||
}
|
||||
@@ -512,7 +531,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
||||
}
|
||||
}()
|
||||
|
||||
err = ctl.sessionCtx.PxyManager.Add(pxyMsg.ProxyName, pxy)
|
||||
err = ctl.pxyManager.Add(pxyMsg.ProxyName, pxy)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -531,12 +550,28 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 {
|
||||
if ctl.serverCfg.MaxPortsPerClient > 0 {
|
||||
ctl.portsUsedNum -= pxy.GetUsedPortsNum()
|
||||
}
|
||||
pxy.Close()
|
||||
ctl.pxyManager.Del(pxy.GetName())
|
||||
delete(ctl.proxies, closeMsg.ProxyName)
|
||||
ctl.mu.Unlock()
|
||||
|
||||
ctl.closeProxy(pxy)
|
||||
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||
|
||||
notifyContent := &plugin.CloseProxyContent{
|
||||
User: plugin.UserInfo{
|
||||
User: ctl.loginMsg.User,
|
||||
Metas: ctl.loginMsg.Metas,
|
||||
RunID: ctl.loginMsg.RunID,
|
||||
},
|
||||
CloseProxy: msg.CloseProxy{
|
||||
ProxyName: pxy.GetName(),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
_ = ctl.pluginManager.CloseProxy(notifyContent)
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -24,6 +24,4 @@ var (
|
||||
ErrListenerClosed = errors.New("group listener closed")
|
||||
ErrGroupDifferentPort = errors.New("group should have same remote port")
|
||||
ErrProxyRepeated = errors.New("group proxy repeated")
|
||||
|
||||
errGroupStale = errors.New("stale group reference")
|
||||
)
|
||||
|
||||
@@ -9,42 +9,53 @@ import (
|
||||
"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 {
|
||||
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
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
|
||||
return &HTTPGroupController{
|
||||
groupRegistry: newGroupRegistry[*HTTPGroup](),
|
||||
vhostRouter: vhostRouter,
|
||||
groups: make(map[string]*HTTPGroup),
|
||||
vhostRouter: vhostRouter,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctl *HTTPGroupController) Register(
|
||||
proxyName, group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) error {
|
||||
for {
|
||||
g := ctl.getOrCreate(group, func() *HTTPGroup {
|
||||
return NewHTTPGroup(ctl)
|
||||
})
|
||||
err := g.Register(proxyName, group, groupKey, routeConfig)
|
||||
if err == errGroupStale {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
) (err error) {
|
||||
indexKey := group
|
||||
ctl.mu.Lock()
|
||||
g, ok := ctl.groups[indexKey]
|
||||
if !ok {
|
||||
g = NewHTTPGroup(ctl)
|
||||
ctl.groups[indexKey] = g
|
||||
}
|
||||
ctl.mu.Unlock()
|
||||
|
||||
return g.Register(proxyName, group, groupKey, 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 {
|
||||
return
|
||||
}
|
||||
g.UnRegister(proxyName)
|
||||
|
||||
isEmpty := g.UnRegister(proxyName)
|
||||
if isEmpty {
|
||||
delete(ctl.groups, indexKey)
|
||||
}
|
||||
}
|
||||
|
||||
type HTTPGroup struct {
|
||||
@@ -76,9 +87,6 @@ func (g *HTTPGroup) Register(
|
||||
) (err error) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if !g.ctl.isCurrent(group, func(cur *HTTPGroup) bool { return cur == g }) {
|
||||
return errGroupStale
|
||||
}
|
||||
if len(g.createFuncs) == 0 {
|
||||
// the first proxy in this group
|
||||
tmp := routeConfig // copy object
|
||||
@@ -115,7 +123,7 @@ func (g *HTTPGroup) Register(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *HTTPGroup) UnRegister(proxyName string) {
|
||||
func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
delete(g.createFuncs, proxyName)
|
||||
@@ -127,11 +135,10 @@ func (g *HTTPGroup) UnRegister(proxyName string) {
|
||||
}
|
||||
|
||||
if len(g.createFuncs) == 0 {
|
||||
isEmpty = true
|
||||
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) {
|
||||
@@ -144,7 +151,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
|
||||
location := g.location
|
||||
routeByHTTPUser := g.routeByHTTPUser
|
||||
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]
|
||||
}
|
||||
g.mu.RUnlock()
|
||||
@@ -167,7 +174,7 @@ func (g *HTTPGroup) chooseEndpoint() (string, error) {
|
||||
location := g.location
|
||||
routeByHTTPUser := g.routeByHTTPUser
|
||||
if len(g.pxyNames) > 0 {
|
||||
name = g.pxyNames[newIndex%uint64(len(g.pxyNames))]
|
||||
name = g.pxyNames[int(newIndex)%len(g.pxyNames)]
|
||||
}
|
||||
g.mu.RUnlock()
|
||||
|
||||
|
||||
@@ -17,19 +17,25 @@ package group
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
gerr "github.com/fatedier/golib/errors"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/vhost"
|
||||
)
|
||||
|
||||
type HTTPSGroupController struct {
|
||||
groupRegistry[*HTTPSGroup]
|
||||
groups map[string]*HTTPSGroup
|
||||
|
||||
httpsMuxer *vhost.HTTPSMuxer
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
|
||||
return &HTTPSGroupController{
|
||||
groupRegistry: newGroupRegistry[*HTTPSGroup](),
|
||||
httpsMuxer: httpsMuxer,
|
||||
groups: make(map[string]*HTTPSGroup),
|
||||
httpsMuxer: httpsMuxer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,28 +44,41 @@ func (ctl *HTTPSGroupController) Listen(
|
||||
group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (l net.Listener, err error) {
|
||||
for {
|
||||
g := ctl.getOrCreate(group, func() *HTTPSGroup {
|
||||
return NewHTTPSGroup(ctl)
|
||||
})
|
||||
l, err = g.Listen(ctx, group, groupKey, routeConfig)
|
||||
if err == errGroupStale {
|
||||
continue
|
||||
}
|
||||
return
|
||||
indexKey := group
|
||||
ctl.mu.Lock()
|
||||
g, ok := ctl.groups[indexKey]
|
||||
if !ok {
|
||||
g = NewHTTPSGroup(ctl)
|
||||
ctl.groups[indexKey] = g
|
||||
}
|
||||
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 {
|
||||
baseGroup
|
||||
group string
|
||||
groupKey string
|
||||
domain string
|
||||
|
||||
domain string
|
||||
ctl *HTTPSGroupController
|
||||
acceptCh chan net.Conn
|
||||
httpsLn *vhost.Listener
|
||||
lns []*HTTPSGroupListener
|
||||
ctl *HTTPSGroupController
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewHTTPSGroup(ctl *HTTPSGroupController) *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,
|
||||
group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (ln *Listener, err error) {
|
||||
) (ln *HTTPSGroupListener, err error) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if !g.ctl.isCurrent(group, func(cur *HTTPSGroup) bool { return cur == g }) {
|
||||
return nil, errGroupStale
|
||||
}
|
||||
if len(g.lns) == 0 {
|
||||
// the first listener, listen on the real address
|
||||
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
|
||||
if errRet != nil {
|
||||
return nil, errRet
|
||||
}
|
||||
ln = newHTTPSGroupListener(group, g, httpsLn.Addr())
|
||||
|
||||
g.group = group
|
||||
g.groupKey = groupKey
|
||||
g.domain = routeConfig.Domain
|
||||
g.initBase(group, groupKey, httpsLn, func() {
|
||||
g.ctl.removeIf(g.group, func(cur *HTTPSGroup) bool {
|
||||
return cur == g
|
||||
})
|
||||
})
|
||||
ln = g.newListener(httpsLn.Addr())
|
||||
go g.worker(httpsLn, g.acceptCh)
|
||||
g.httpsLn = httpsLn
|
||||
g.lns = append(g.lns, ln)
|
||||
go g.worker()
|
||||
} else {
|
||||
// route config in the same group must be equal
|
||||
if g.group != group || g.domain != routeConfig.Domain {
|
||||
@@ -96,7 +111,87 @@ func (g *HTTPSGroup) Listen(
|
||||
if g.groupKey != groupKey {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -17,91 +17,107 @@ package group
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
gerr "github.com/fatedier/golib/errors"
|
||||
|
||||
"github.com/fatedier/frp/server/ports"
|
||||
)
|
||||
|
||||
// TCPGroupCtl manages all TCPGroups.
|
||||
// TCPGroupCtl manage all TCPGroups
|
||||
type TCPGroupCtl struct {
|
||||
groupRegistry[*TCPGroup]
|
||||
groups map[string]*TCPGroup
|
||||
|
||||
// portManager is used to manage port
|
||||
portManager *ports.Manager
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTCPGroupCtl returns a new TCPGroupCtl.
|
||||
// NewTCPGroupCtl return a new TcpGroupCtl
|
||||
func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl {
|
||||
return &TCPGroupCtl{
|
||||
groupRegistry: newGroupRegistry[*TCPGroup](),
|
||||
portManager: portManager,
|
||||
groups: make(map[string]*TCPGroup),
|
||||
portManager: portManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Listen is the wrapper for TCPGroup's Listen.
|
||||
// If there is no group, one will be created.
|
||||
// Listen is the wrapper for TCPGroup's Listen
|
||||
// If there are no group, we will create one here
|
||||
func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string,
|
||||
addr string, port int,
|
||||
) (l net.Listener, realPort int, err error) {
|
||||
for {
|
||||
tcpGroup := tgc.getOrCreate(group, func() *TCPGroup {
|
||||
return NewTCPGroup(tgc)
|
||||
})
|
||||
l, realPort, err = tcpGroup.Listen(proxyName, group, groupKey, addr, port)
|
||||
if err == errGroupStale {
|
||||
continue
|
||||
}
|
||||
return
|
||||
tgc.mu.Lock()
|
||||
tcpGroup, ok := tgc.groups[group]
|
||||
if !ok {
|
||||
tcpGroup = NewTCPGroup(tgc)
|
||||
tgc.groups[group] = tcpGroup
|
||||
}
|
||||
tgc.mu.Unlock()
|
||||
|
||||
return tcpGroup.Listen(proxyName, group, groupKey, addr, port)
|
||||
}
|
||||
|
||||
// TCPGroup routes connections to different proxies.
|
||||
type TCPGroup struct {
|
||||
baseGroup
|
||||
// RemoveGroup remove TCPGroup from controller
|
||||
func (tgc *TCPGroupCtl) RemoveGroup(group string) {
|
||||
tgc.mu.Lock()
|
||||
defer tgc.mu.Unlock()
|
||||
delete(tgc.groups, group)
|
||||
}
|
||||
|
||||
// TCPGroup route connections to different proxies
|
||||
type TCPGroup struct {
|
||||
group string
|
||||
groupKey string
|
||||
addr string
|
||||
port int
|
||||
realPort int
|
||||
|
||||
acceptCh chan net.Conn
|
||||
tcpLn net.Listener
|
||||
lns []*TCPGroupListener
|
||||
ctl *TCPGroupCtl
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTCPGroup returns a new TCPGroup.
|
||||
// NewTCPGroup return a new TCPGroup
|
||||
func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup {
|
||||
return &TCPGroup{
|
||||
ctl: ctl,
|
||||
lns: make([]*TCPGroupListener, 0),
|
||||
ctl: ctl,
|
||||
acceptCh: make(chan net.Conn),
|
||||
}
|
||||
}
|
||||
|
||||
// Listen will return a new Listener.
|
||||
// If TCPGroup already has a listener, just add a new Listener to the queues,
|
||||
// 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) {
|
||||
// Listen will return a new TCPGroupListener
|
||||
// if TCPGroup already has a listener, just add a new TCPGroupListener to the queues
|
||||
// otherwise, listen on the real address
|
||||
func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *TCPGroupListener, realPort int, err error) {
|
||||
tg.mu.Lock()
|
||||
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 {
|
||||
// the first listener, listen on the real address
|
||||
realPort, err = tg.ctl.portManager.Acquire(proxyName, port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(realPort)))
|
||||
tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(port)))
|
||||
if errRet != nil {
|
||||
tg.ctl.portManager.Release(realPort)
|
||||
err = errRet
|
||||
return
|
||||
}
|
||||
ln = newTCPGroupListener(group, tg, tcpLn.Addr())
|
||||
|
||||
tg.group = group
|
||||
tg.groupKey = groupKey
|
||||
tg.addr = addr
|
||||
tg.port = port
|
||||
tg.realPort = realPort
|
||||
tg.initBase(group, groupKey, tcpLn, func() {
|
||||
tg.ctl.portManager.Release(tg.realPort)
|
||||
tg.ctl.removeIf(tg.group, func(cur *TCPGroup) bool {
|
||||
return cur == tg
|
||||
})
|
||||
})
|
||||
ln = tg.newListener(tcpLn.Addr())
|
||||
go tg.worker(tcpLn, tg.acceptCh)
|
||||
tg.tcpLn = tcpLn
|
||||
tg.lns = append(tg.lns, ln)
|
||||
if tg.acceptCh == nil {
|
||||
tg.acceptCh = make(chan net.Conn)
|
||||
}
|
||||
go tg.worker()
|
||||
} else {
|
||||
// address and port in the same group must be equal
|
||||
if tg.group != group || tg.addr != addr {
|
||||
@@ -116,8 +132,92 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
|
||||
err = ErrGroupAuthFailed
|
||||
return
|
||||
}
|
||||
ln = tg.newListener(tg.lns[0].Addr())
|
||||
ln = newTCPGroupListener(group, tg, tg.lns[0].Addr())
|
||||
realPort = tg.realPort
|
||||
tg.lns = append(tg.lns, ln)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// worker is called when the real tcp listener has been created
|
||||
func (tg *TCPGroup) worker() {
|
||||
for {
|
||||
c, err := tg.tcpLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gerr.PanicToError(func() {
|
||||
tg.acceptCh <- c
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tg *TCPGroup) Accept() <-chan net.Conn {
|
||||
return tg.acceptCh
|
||||
}
|
||||
|
||||
// CloseListener remove the TCPGroupListener from the TCPGroup
|
||||
func (tg *TCPGroup) CloseListener(ln *TCPGroupListener) {
|
||||
tg.mu.Lock()
|
||||
defer tg.mu.Unlock()
|
||||
for i, tmpLn := range tg.lns {
|
||||
if tmpLn == ln {
|
||||
tg.lns = append(tg.lns[:i], tg.lns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(tg.lns) == 0 {
|
||||
close(tg.acceptCh)
|
||||
tg.tcpLn.Close()
|
||||
tg.ctl.portManager.Release(tg.realPort)
|
||||
tg.ctl.RemoveGroup(tg.group)
|
||||
}
|
||||
}
|
||||
|
||||
// TCPGroupListener
|
||||
type TCPGroupListener struct {
|
||||
groupName string
|
||||
group *TCPGroup
|
||||
|
||||
addr net.Addr
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func newTCPGroupListener(name string, group *TCPGroup, addr net.Addr) *TCPGroupListener {
|
||||
return &TCPGroupListener{
|
||||
groupName: name,
|
||||
group: group,
|
||||
addr: addr,
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Accept will accept connections from TCPGroup
|
||||
func (ln *TCPGroupListener) Accept() (c net.Conn, err error) {
|
||||
var ok bool
|
||||
select {
|
||||
case <-ln.closeCh:
|
||||
return nil, ErrListenerClosed
|
||||
case c, ok = <-ln.group.Accept():
|
||||
if !ok {
|
||||
return nil, ErrListenerClosed
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ln *TCPGroupListener) Addr() net.Addr {
|
||||
return ln.addr
|
||||
}
|
||||
|
||||
// Close close the listener
|
||||
func (ln *TCPGroupListener) Close() (err error) {
|
||||
close(ln.closeCh)
|
||||
|
||||
// remove self from TcpGroup
|
||||
ln.group.CloseListener(ln)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,100 +18,118 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
gerr "github.com/fatedier/golib/errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/tcpmux"
|
||||
"github.com/fatedier/frp/pkg/util/vhost"
|
||||
)
|
||||
|
||||
// TCPMuxGroupCtl manages all TCPMuxGroups.
|
||||
// TCPMuxGroupCtl manage all TCPMuxGroups
|
||||
type TCPMuxGroupCtl struct {
|
||||
groupRegistry[*TCPMuxGroup]
|
||||
groups map[string]*TCPMuxGroup
|
||||
|
||||
// portManager is used to manage port
|
||||
tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTCPMuxGroupCtl returns a new TCPMuxGroupCtl.
|
||||
// NewTCPMuxGroupCtl return a new TCPMuxGroupCtl
|
||||
func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl {
|
||||
return &TCPMuxGroupCtl{
|
||||
groupRegistry: newGroupRegistry[*TCPMuxGroup](),
|
||||
groups: make(map[string]*TCPMuxGroup),
|
||||
tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer,
|
||||
}
|
||||
}
|
||||
|
||||
// Listen is the wrapper for TCPMuxGroup's Listen.
|
||||
// If there is no group, one will be created.
|
||||
// Listen is the wrapper for TCPMuxGroup's Listen
|
||||
// If there are no group, we will create one here
|
||||
func (tmgc *TCPMuxGroupCtl) Listen(
|
||||
ctx context.Context,
|
||||
multiplexer, group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (l net.Listener, err error) {
|
||||
for {
|
||||
tcpMuxGroup := tmgc.getOrCreate(group, func() *TCPMuxGroup {
|
||||
return NewTCPMuxGroup(tmgc)
|
||||
})
|
||||
tmgc.mu.Lock()
|
||||
tcpMuxGroup, ok := tmgc.groups[group]
|
||||
if !ok {
|
||||
tcpMuxGroup = NewTCPMuxGroup(tmgc)
|
||||
tmgc.groups[group] = tcpMuxGroup
|
||||
}
|
||||
tmgc.mu.Unlock()
|
||||
|
||||
switch v1.TCPMultiplexerType(multiplexer) {
|
||||
case v1.TCPMultiplexerHTTPConnect:
|
||||
l, err = tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
|
||||
if err == errGroupStale {
|
||||
continue
|
||||
}
|
||||
return
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown multiplexer [%s]", multiplexer)
|
||||
}
|
||||
switch v1.TCPMultiplexerType(multiplexer) {
|
||||
case v1.TCPMultiplexerHTTPConnect:
|
||||
return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
|
||||
default:
|
||||
err = fmt.Errorf("unknown multiplexer [%s]", multiplexer)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TCPMuxGroup routes connections to different proxies.
|
||||
type TCPMuxGroup struct {
|
||||
baseGroup
|
||||
// RemoveGroup remove TCPMuxGroup from controller
|
||||
func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) {
|
||||
tmgc.mu.Lock()
|
||||
defer tmgc.mu.Unlock()
|
||||
delete(tmgc.groups, group)
|
||||
}
|
||||
|
||||
// TCPMuxGroup route connections to different proxies
|
||||
type TCPMuxGroup struct {
|
||||
group string
|
||||
groupKey string
|
||||
domain string
|
||||
routeByHTTPUser string
|
||||
username 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 {
|
||||
return &TCPMuxGroup{
|
||||
ctl: ctl,
|
||||
lns: make([]*TCPMuxGroupListener, 0),
|
||||
ctl: ctl,
|
||||
acceptCh: make(chan net.Conn),
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPConnectListen will return a new Listener.
|
||||
// If TCPMuxGroup already has a listener, just add a new Listener to the queues,
|
||||
// otherwise listen on the real address.
|
||||
// Listen will return a new TCPMuxGroupListener
|
||||
// if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues
|
||||
// otherwise, listen on the real address
|
||||
func (tmg *TCPMuxGroup) HTTPConnectListen(
|
||||
ctx context.Context,
|
||||
group, groupKey string,
|
||||
routeConfig vhost.RouteConfig,
|
||||
) (ln *Listener, err error) {
|
||||
) (ln *TCPMuxGroupListener, err error) {
|
||||
tmg.mu.Lock()
|
||||
defer tmg.mu.Unlock()
|
||||
if !tmg.ctl.isCurrent(group, func(cur *TCPMuxGroup) bool { return cur == tmg }) {
|
||||
return nil, errGroupStale
|
||||
}
|
||||
if len(tmg.lns) == 0 {
|
||||
// the first listener, listen on the real address
|
||||
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
|
||||
if errRet != nil {
|
||||
return nil, errRet
|
||||
}
|
||||
ln = newTCPMuxGroupListener(group, tmg, tcpMuxLn.Addr())
|
||||
|
||||
tmg.group = group
|
||||
tmg.groupKey = groupKey
|
||||
tmg.domain = routeConfig.Domain
|
||||
tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
|
||||
tmg.username = routeConfig.Username
|
||||
tmg.password = routeConfig.Password
|
||||
tmg.initBase(group, groupKey, tcpMuxLn, func() {
|
||||
tmg.ctl.removeIf(tmg.group, func(cur *TCPMuxGroup) bool {
|
||||
return cur == tmg
|
||||
})
|
||||
})
|
||||
ln = tmg.newListener(tcpMuxLn.Addr())
|
||||
go tmg.worker(tcpMuxLn, tmg.acceptCh)
|
||||
tmg.tcpMuxLn = tcpMuxLn
|
||||
tmg.lns = append(tmg.lns, ln)
|
||||
if tmg.acceptCh == nil {
|
||||
tmg.acceptCh = make(chan net.Conn)
|
||||
}
|
||||
go tmg.worker()
|
||||
} else {
|
||||
// route config in the same group must be equal
|
||||
if tmg.group != group || tmg.domain != routeConfig.Domain ||
|
||||
@@ -123,7 +141,90 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(
|
||||
if tmg.groupKey != groupKey {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.HTTPProxyConfig](), NewHTTPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.HTTPProxyConfig{}), NewHTTPProxy)
|
||||
}
|
||||
|
||||
type HTTPProxy struct {
|
||||
@@ -75,13 +75,16 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain)
|
||||
|
||||
addrs := make([]string, 0)
|
||||
for _, domain := range domains {
|
||||
for _, domain := range pxy.cfg.CustomDomains {
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
routeConfig.Domain = domain
|
||||
for _, location := range locations {
|
||||
routeConfig.Location = location
|
||||
|
||||
tmpRouteConfig := routeConfig
|
||||
|
||||
// handle group
|
||||
@@ -90,6 +93,40 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pxy.closeFuncs = append(pxy.closeFuncs, func() {
|
||||
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
|
||||
})
|
||||
} else {
|
||||
// no group
|
||||
err = pxy.rc.HTTPReverseProxy.Register(routeConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pxy.closeFuncs = append(pxy.closeFuncs, func() {
|
||||
pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
|
||||
})
|
||||
}
|
||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort))
|
||||
xl.Infof("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
|
||||
routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser)
|
||||
}
|
||||
}
|
||||
|
||||
if pxy.cfg.SubDomain != "" {
|
||||
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||
for _, location := range locations {
|
||||
routeConfig.Location = location
|
||||
|
||||
tmpRouteConfig := routeConfig
|
||||
|
||||
// handle group
|
||||
if pxy.cfg.LoadBalancer.Group != "" {
|
||||
err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pxy.closeFuncs = append(pxy.closeFuncs, func() {
|
||||
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
|
||||
})
|
||||
@@ -102,7 +139,8 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
|
||||
pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
|
||||
})
|
||||
}
|
||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort))
|
||||
addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort))
|
||||
|
||||
xl.Infof("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
|
||||
routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser)
|
||||
}
|
||||
@@ -130,7 +168,6 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tmpConn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.HTTPSProxyConfig](), NewHTTPSProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.HTTPSProxyConfig{}), NewHTTPSProxy)
|
||||
}
|
||||
|
||||
type HTTPSProxy struct {
|
||||
@@ -53,10 +53,23 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||
pxy.Close()
|
||||
}
|
||||
}()
|
||||
domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain)
|
||||
|
||||
addrs := make([]string, 0)
|
||||
for _, domain := range domains {
|
||||
for _, domain := range pxy.cfg.CustomDomains {
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pxy.listeners = append(pxy.listeners, l)
|
||||
addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort))
|
||||
xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group)
|
||||
}
|
||||
|
||||
if pxy.cfg.SubDomain != "" {
|
||||
domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -150,7 +150,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
||||
dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
|
||||
dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16)
|
||||
}
|
||||
err = msg.WriteMsg(workConn, &msg.StartWorkConn{
|
||||
err := msg.WriteMsg(workConn, &msg.StartWorkConn{
|
||||
ProxyName: pxy.GetName(),
|
||||
SrcAddr: srcAddr,
|
||||
SrcPort: uint16(srcPort),
|
||||
@@ -161,7 +161,6 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
||||
if err != nil {
|
||||
xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i)
|
||||
workConn.Close()
|
||||
workConn = nil
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -174,36 +173,6 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
|
||||
return
|
||||
}
|
||||
|
||||
// startVisitorListener sets up a VisitorManager listener for visitor-based proxies (STCP, SUDP).
|
||||
func (pxy *BaseProxy) startVisitorListener(secretKey string, allowUsers []string, proxyType string) error {
|
||||
// if allowUsers is empty, only allow same user from proxy
|
||||
if len(allowUsers) == 0 {
|
||||
allowUsers = []string{pxy.GetUserInfo().User}
|
||||
}
|
||||
listener, err := pxy.rc.VisitorManager.Listen(pxy.GetName(), secretKey, allowUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pxy.listeners = append(pxy.listeners, listener)
|
||||
pxy.xl.Infof("%s proxy custom listen success", proxyType)
|
||||
pxy.startCommonTCPListenersHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildDomains constructs a list of domains from custom domains and subdomain configuration.
|
||||
func (pxy *BaseProxy) buildDomains(customDomains []string, subDomain string) []string {
|
||||
domains := make([]string, 0, len(customDomains)+1)
|
||||
for _, d := range customDomains {
|
||||
if d != "" {
|
||||
domains = append(domains, d)
|
||||
}
|
||||
}
|
||||
if subDomain != "" {
|
||||
domains = append(domains, subDomain+"."+pxy.serverCfg.SubDomainHost)
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
// startCommonTCPListenersHandler start a goroutine handler for each listener.
|
||||
func (pxy *BaseProxy) startCommonTCPListenersHandler() {
|
||||
xl := xlog.FromContextSafe(pxy.ctx)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.STCPProxyConfig](), NewSTCPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.STCPProxyConfig{}), NewSTCPProxy)
|
||||
}
|
||||
|
||||
type STCPProxy struct {
|
||||
@@ -41,7 +41,21 @@ func NewSTCPProxy(baseProxy *BaseProxy) Proxy {
|
||||
}
|
||||
|
||||
func (pxy *STCPProxy) Run() (remoteAddr string, err error) {
|
||||
err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "stcp")
|
||||
xl := pxy.xl
|
||||
allowUsers := pxy.cfg.AllowUsers
|
||||
// if allowUsers is empty, only allow same user from proxy
|
||||
if len(allowUsers) == 0 {
|
||||
allowUsers = []string{pxy.GetUserInfo().User}
|
||||
}
|
||||
listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers)
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
}
|
||||
pxy.listeners = append(pxy.listeners, listener)
|
||||
xl.Infof("stcp proxy custom listen success")
|
||||
|
||||
pxy.startCommonTCPListenersHandler()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
||||
}
|
||||
|
||||
type SUDPProxy struct {
|
||||
@@ -41,7 +41,21 @@ func NewSUDPProxy(baseProxy *BaseProxy) Proxy {
|
||||
}
|
||||
|
||||
func (pxy *SUDPProxy) Run() (remoteAddr string, err error) {
|
||||
err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "sudp")
|
||||
xl := pxy.xl
|
||||
allowUsers := pxy.cfg.AllowUsers
|
||||
// if allowUsers is empty, only allow same user from proxy
|
||||
if len(allowUsers) == 0 {
|
||||
allowUsers = []string{pxy.GetUserInfo().User}
|
||||
}
|
||||
listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers)
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
}
|
||||
pxy.listeners = append(pxy.listeners, listener)
|
||||
xl.Infof("sudp proxy custom listen success")
|
||||
|
||||
pxy.startCommonTCPListenersHandler()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.TCPProxyConfig](), NewTCPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.TCPProxyConfig{}), NewTCPProxy)
|
||||
}
|
||||
|
||||
type TCPProxy struct {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.TCPMuxProxyConfig](), NewTCPMuxProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.TCPMuxProxyConfig{}), NewTCPMuxProxy)
|
||||
}
|
||||
|
||||
type TCPMuxProxy struct {
|
||||
@@ -72,16 +72,26 @@ func (pxy *TCPMuxProxy) httpConnectListen(
|
||||
}
|
||||
|
||||
func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
|
||||
domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain)
|
||||
|
||||
addrs := make([]string, 0)
|
||||
for _, domain := range domains {
|
||||
for _, domain := range pxy.cfg.CustomDomains {
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if pxy.cfg.SubDomain != "" {
|
||||
addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost,
|
||||
pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
pxy.startCommonTCPListenersHandler()
|
||||
remoteAddr = strings.Join(addrs, ",")
|
||||
return remoteAddr, err
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
||||
}
|
||||
|
||||
type UDPProxy struct {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user