Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aada7144d1 | ||
|
|
d0347325fc | ||
|
|
519368b1fd | ||
|
|
9634fd99d1 | ||
|
|
7a1c248b67 | ||
|
|
886c9c2fdb | ||
|
|
266c492b5d | ||
|
|
5dd70ace6b | ||
|
|
fb2c98e87b | ||
|
|
ed13141c56 | ||
|
|
3370bd53f5 | ||
|
|
1245f8804e | ||
|
|
479e9f50c2 | ||
|
|
a4175a2595 | ||
|
|
36718d88e4 | ||
|
|
bc378bcbec | ||
|
|
33428ab538 | ||
|
|
ef96481f58 | ||
|
|
7526d7a69a | ||
|
|
2bdf25bae6 | ||
|
|
0fe8f7a0b6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 | ||
|
|
b5e90c03a1 | ||
|
|
b642a6323c | ||
|
|
6561107945 | ||
|
|
abf4942e8a | ||
|
|
7cfa546b55 | ||
|
|
0a798a7a69 | ||
|
|
604700cea5 | ||
|
|
610e5ed479 | ||
|
|
80d3f332e1 | ||
|
|
14253afe2f | ||
|
|
024c334d9d |
@@ -2,10 +2,18 @@ version: 2
|
||||
jobs:
|
||||
go-version-latest:
|
||||
docker:
|
||||
- image: cimg/go:1.23-node
|
||||
- image: cimg/go:1.24-node
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Build web assets (frps)
|
||||
command: make install build
|
||||
working_directory: web/frps
|
||||
- run:
|
||||
name: Build web assets (frpc)
|
||||
command: make install build
|
||||
working_directory: web/frpc
|
||||
- run: make
|
||||
- run: make alltest
|
||||
|
||||
|
||||
11
.github/workflows/golangci-lint.yml
vendored
@@ -17,8 +17,17 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version: '1.24'
|
||||
cache: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
run: make build
|
||||
working-directory: web/frps
|
||||
- name: Build web assets (frpc)
|
||||
run: make build
|
||||
working-directory: web/frpc
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
|
||||
14
.github/workflows/goreleaser.yml
vendored
@@ -15,14 +15,22 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
go-version: '1.24'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build web assets (frps)
|
||||
run: make build
|
||||
working-directory: web/frps
|
||||
- name: Build web assets (frpc)
|
||||
run: make build
|
||||
working-directory: web/frpc
|
||||
- name: Make All
|
||||
run: |
|
||||
./package.sh
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --release-notes=./Release.md
|
||||
|
||||
13
.gitignore
vendored
@@ -7,18 +7,6 @@
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
@@ -42,3 +30,4 @@ client.key
|
||||
|
||||
# AI
|
||||
CLAUDE.md
|
||||
.sisyphus/
|
||||
|
||||
@@ -39,6 +39,7 @@ linters:
|
||||
- G404
|
||||
- G501
|
||||
- G115
|
||||
- G204
|
||||
severity: low
|
||||
confidence: low
|
||||
govet:
|
||||
|
||||
21
Makefile
@@ -2,19 +2,22 @@ export PATH := $(PATH):`go env GOPATH`/bin
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
|
||||
all: env fmt build
|
||||
.PHONY: web frps-web frpc-web frps frpc
|
||||
|
||||
all: env fmt web build
|
||||
|
||||
build: frps frpc
|
||||
|
||||
env:
|
||||
@go version
|
||||
|
||||
# compile assets into binary file
|
||||
file:
|
||||
rm -rf ./assets/frps/static/*
|
||||
rm -rf ./assets/frpc/static/*
|
||||
cp -rf ./web/frps/dist/* ./assets/frps/static
|
||||
cp -rf ./web/frpc/dist/* ./assets/frpc/static
|
||||
web: frps-web frpc-web
|
||||
|
||||
frps-web:
|
||||
$(MAKE) -C web/frps build
|
||||
|
||||
frpc-web:
|
||||
$(MAKE) -C web/frpc build
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
@@ -25,7 +28,7 @@ fmt-more:
|
||||
gci:
|
||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||
|
||||
vet:
|
||||
vet: web
|
||||
go vet ./...
|
||||
|
||||
frps:
|
||||
@@ -36,7 +39,7 @@ frpc:
|
||||
|
||||
test: gotest
|
||||
|
||||
gotest:
|
||||
gotest: web
|
||||
go test -v --cover ./assets/...
|
||||
go test -v --cover ./cmd/...
|
||||
go test -v --cover ./client/...
|
||||
|
||||
28
README.md
@@ -14,14 +14,15 @@ 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://go.warp.dev/frp" target="_blank">
|
||||
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Warp, the intelligent terminal</b>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>Available for macOS, Linux and Windows</sub>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
@@ -29,13 +30,7 @@ frp is an open source project with its ongoing development made possible entirel
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||
<br>
|
||||
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</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">
|
||||
@@ -45,6 +40,15 @@ frp is an open source project with its ongoing development made possible entirel
|
||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||
</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
|
||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## What is frp?
|
||||
@@ -519,7 +523,7 @@ name = "ssh"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}"
|
||||
remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }}
|
||||
```
|
||||
|
||||
With the config above, variables can be passed into `frpc` program like this:
|
||||
|
||||
37
README_zh.md
@@ -15,21 +15,42 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||
</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>
|
||||
<div align="center">
|
||||
|
||||
## Recall.ai - API for meeting recordings
|
||||
|
||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## 为什么使用 frp ?
|
||||
@@ -102,9 +123,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||
|
||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||
|
||||
### 知识星球
|
||||
|
||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
||||
|
||||

|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
## Features
|
||||
|
||||
* Support tokenSource for loading authentication tokens from files.
|
||||
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
||||
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
||||
|
||||
## Fixes
|
||||
|
||||
* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1.
|
||||
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
||||
|
||||
@@ -41,7 +41,7 @@ func Load(path string) {
|
||||
}
|
||||
|
||||
func Register(fileSystem fs.FS) {
|
||||
subFs, err := fs.Sub(fileSystem, "static")
|
||||
subFs, err := fs.Sub(fileSystem, "dist")
|
||||
if err == nil {
|
||||
content = subFs
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>frp client admin UI</title>
|
||||
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,14 +0,0 @@
|
||||
package frpc
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/fatedier/frp/assets"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var content embed.FS
|
||||
|
||||
func init() {
|
||||
assets.Register(content)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>frps dashboard</title>
|
||||
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -15,44 +15,43 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/api"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
type GeneralResponse struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||
helper.Router.HandleFunc("/healthz", svr.healthz)
|
||||
apiController := newAPIController(svr)
|
||||
|
||||
// Healthz endpoint without auth
|
||||
helper.Router.HandleFunc("/healthz", healthz)
|
||||
|
||||
// API routes and static files with auth
|
||||
subRouter := helper.Router.NewRoute().Subrouter()
|
||||
subRouter.Use(helper.AuthMiddleware)
|
||||
subRouter.Use(httppkg.NewRequestLogger)
|
||||
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||
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.Use(helper.AuthMiddleware.Middleware)
|
||||
if svr.storeSource != nil {
|
||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut)
|
||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete)
|
||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut)
|
||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
// api, see admin_api.go
|
||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||
|
||||
// view
|
||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||
subRouter.PathPrefix("/static/").Handler(
|
||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||
@@ -62,201 +61,30 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
||||
})
|
||||
}
|
||||
|
||||
// /healthz
|
||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// GET /api/reload
|
||||
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
strictConfigMode := false
|
||||
strictStr := r.URL.Query().Get("strictConfig")
|
||||
if strictStr != "" {
|
||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||
}
|
||||
|
||||
log.Infof("api request [/api/reload]")
|
||||
defer func() {
|
||||
log.Infof("api response [/api/reload], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = err.Error()
|
||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
log.Infof("success reload conf")
|
||||
func newAPIController(svr *Service) *api.Controller {
|
||||
return api.NewController(api.ControllerParams{
|
||||
GetProxyStatus: svr.getAllProxyStatus,
|
||||
ServerAddr: svr.common.ServerAddr,
|
||||
ConfigFilePath: svr.configFilePath,
|
||||
UnsafeFeatures: svr.unsafeFeatures,
|
||||
UpdateConfig: svr.UpdateConfigSource,
|
||||
ReloadFromSources: svr.reloadConfigFromSources,
|
||||
GracefulClose: svr.GracefulClose,
|
||||
StoreSource: svr.storeSource,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/stop
|
||||
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Infof("api request [/api/stop]")
|
||||
defer func() {
|
||||
log.Infof("api response [/api/stop], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
go svr.GracefulClose(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
type StatusResp map[string][]ProxyStatusResp
|
||||
|
||||
type ProxyStatusResp struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
Plugin string `json:"plugin"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
|
||||
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
|
||||
psr := ProxyStatusResp{
|
||||
Name: status.Name,
|
||||
Type: status.Type,
|
||||
Status: status.Phase,
|
||||
Err: status.Err,
|
||||
}
|
||||
baseCfg := status.Cfg.GetBaseConfig()
|
||||
if baseCfg.LocalPort != 0 {
|
||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||
}
|
||||
psr.Plugin = baseCfg.Plugin.Type
|
||||
|
||||
if status.Err == "" {
|
||||
psr.RemoteAddr = status.RemoteAddr
|
||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||
psr.RemoteAddr = serverAddr + psr.RemoteAddr
|
||||
}
|
||||
}
|
||||
return psr
|
||||
}
|
||||
|
||||
// GET /api/status
|
||||
func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
var (
|
||||
buf []byte
|
||||
res StatusResp = make(map[string][]ProxyStatusResp)
|
||||
)
|
||||
|
||||
log.Infof("http request [/api/status]")
|
||||
defer func() {
|
||||
log.Infof("http response [/api/status]")
|
||||
buf, _ = json.Marshal(&res)
|
||||
_, _ = w.Write(buf)
|
||||
}()
|
||||
|
||||
// getAllProxyStatus returns all proxy statuses.
|
||||
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||
svr.ctlMu.RLock()
|
||||
ctl := svr.ctl
|
||||
svr.ctlMu.RUnlock()
|
||||
if ctl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ps := ctl.pm.GetAllProxyStatus()
|
||||
for _, status := range ps {
|
||||
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
|
||||
}
|
||||
|
||||
for _, arrs := range res {
|
||||
if len(arrs) <= 1 {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/config
|
||||
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Infof("http get request [/api/config]")
|
||||
defer func() {
|
||||
log.Infof("http get response [/api/config], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
if svr.configFilePath == "" {
|
||||
res.Code = 400
|
||||
res.Msg = "frpc has no config file path"
|
||||
log.Warnf("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(svr.configFilePath)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = err.Error()
|
||||
log.Warnf("load frpc config file error: %s", res.Msg)
|
||||
return
|
||||
}
|
||||
res.Msg = string(content)
|
||||
}
|
||||
|
||||
// PUT /api/config
|
||||
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
|
||||
log.Infof("http put request [/api/config]")
|
||||
defer func() {
|
||||
log.Infof("http put response [/api/config], code [%d]", res.Code)
|
||||
w.WriteHeader(res.Code)
|
||||
if len(res.Msg) > 0 {
|
||||
_, _ = w.Write([]byte(res.Msg))
|
||||
}
|
||||
}()
|
||||
|
||||
// get new config content
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
res.Code = 400
|
||||
res.Msg = fmt.Sprintf("read request body error: %v", err)
|
||||
log.Warnf("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
res.Code = 400
|
||||
res.Msg = "body can't be empty"
|
||||
log.Warnf("%s", res.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
|
||||
res.Code = 500
|
||||
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
||||
log.Warnf("%s", res.Msg)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
return ctl.pm.GetAllProxyStatus()
|
||||
}
|
||||
|
||||
498
client/api/controller.go
Normal file
@@ -0,0 +1,498 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
// Controller handles HTTP API requests for frpc.
|
||||
type Controller struct {
|
||||
getProxyStatus func() []*proxy.WorkingStatus
|
||||
serverAddr string
|
||||
configFilePath string
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
updateConfig func(common *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||
reloadFromSources func() error
|
||||
gracefulClose func(d time.Duration)
|
||||
storeSource *source.StoreSource
|
||||
}
|
||||
|
||||
// ControllerParams contains parameters for creating an APIController.
|
||||
type ControllerParams struct {
|
||||
GetProxyStatus func() []*proxy.WorkingStatus
|
||||
ServerAddr string
|
||||
ConfigFilePath string
|
||||
UnsafeFeatures *security.UnsafeFeatures
|
||||
UpdateConfig func(common *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||
ReloadFromSources func() error
|
||||
GracefulClose func(d time.Duration)
|
||||
StoreSource *source.StoreSource
|
||||
}
|
||||
|
||||
func NewController(params ControllerParams) *Controller {
|
||||
return &Controller{
|
||||
getProxyStatus: params.GetProxyStatus,
|
||||
serverAddr: params.ServerAddr,
|
||||
configFilePath: params.ConfigFilePath,
|
||||
unsafeFeatures: params.UnsafeFeatures,
|
||||
updateConfig: params.UpdateConfig,
|
||||
reloadFromSources: params.ReloadFromSources,
|
||||
gracefulClose: params.GracefulClose,
|
||||
storeSource: params.StoreSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) reloadFromSourcesOrError() error {
|
||||
if err := c.reloadFromSources(); err != nil {
|
||||
return httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to apply config: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload handles GET /api/reload
|
||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||
strictConfigMode := false
|
||||
strictStr := ctx.Query("strictConfig")
|
||||
if strictStr != "" {
|
||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||
}
|
||||
|
||||
result, err := config.LoadClientConfigResult(c.configFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
proxyCfgs := result.Proxies
|
||||
visitorCfgs := result.Visitors
|
||||
|
||||
proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers(
|
||||
result.Common,
|
||||
proxyCfgs,
|
||||
visitorCfgs,
|
||||
)
|
||||
proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation)
|
||||
visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation)
|
||||
|
||||
if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, c.unsafeFeatures); err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := c.updateConfig(result.Common, proxyCfgs, visitorCfgs); err != nil {
|
||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
log.Infof("success reload conf")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Stop handles POST /api/stop
|
||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||
go c.gracefulClose(100 * time.Millisecond)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Status handles GET /api/status
|
||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||
res := make(StatusResp)
|
||||
ps := c.getProxyStatus()
|
||||
if ps == nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
for _, status := range ps {
|
||||
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||
}
|
||||
|
||||
for _, arrs := range res {
|
||||
if len(arrs) <= 1 {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetConfig handles GET /api/config
|
||||
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||
if c.configFilePath == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(c.configFilePath)
|
||||
if err != nil {
|
||||
log.Warnf("load frpc config file error: %s", err.Error())
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// PutConfig handles PUT /api/config
|
||||
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
||||
psr := ProxyStatusResp{
|
||||
Name: status.Name,
|
||||
Type: status.Type,
|
||||
Status: status.Phase,
|
||||
Err: status.Err,
|
||||
}
|
||||
baseCfg := status.Cfg.GetBaseConfig()
|
||||
if baseCfg.LocalPort != 0 {
|
||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||
}
|
||||
psr.Plugin = baseCfg.Plugin.Type
|
||||
|
||||
if status.Err == "" {
|
||||
psr.RemoteAddr = status.RemoteAddr
|
||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
// Check if proxy is from store
|
||||
if c.storeSource != nil {
|
||||
if c.storeSource.GetProxy(status.Name) != nil {
|
||||
psr.Source = "store"
|
||||
}
|
||||
}
|
||||
return psr
|
||||
}
|
||||
|
||||
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||
proxies, err := c.storeSource.GetAllProxies()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to list proxies: %v", err))
|
||||
}
|
||||
resp := ProxyListResp{Proxies: make([]ProxyConfig, 0, len(proxies))}
|
||||
|
||||
for _, p := range proxies {
|
||||
cfg, err := proxyConfigurerToMap(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Proxies = append(resp.Proxies, ProxyConfig{
|
||||
Name: p.GetBaseConfig().Name,
|
||||
Type: p.GetBaseConfig().Type,
|
||||
Config: cfg,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
p := c.storeSource.GetProxy(name)
|
||||
if p == nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name))
|
||||
}
|
||||
|
||||
cfg, err := proxyConfigurerToMap(p)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return ProxyConfig{
|
||||
Name: p.GetBaseConfig().Name,
|
||||
Type: p.GetBaseConfig().Type,
|
||||
Config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var typed v1.TypedProxyConfig
|
||||
if err := json.Unmarshal(body, &typed); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if typed.ProxyConfigurer == nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
||||
}
|
||||
|
||||
typed.Complete()
|
||||
if err := validation.ValidateProxyConfigurerForClient(typed.ProxyConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("validation error: %v", err))
|
||||
}
|
||||
|
||||
if err := c.storeSource.AddProxy(typed.ProxyConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusConflict, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: created proxy %q", typed.ProxyConfigurer.GetBaseConfig().Name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var typed v1.TypedProxyConfig
|
||||
if err := json.Unmarshal(body, &typed); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if typed.ProxyConfigurer == nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid proxy config: type is required")
|
||||
}
|
||||
|
||||
bodyName := typed.ProxyConfigurer.GetBaseConfig().Name
|
||||
if bodyName != name {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name in URL must match name in body")
|
||||
}
|
||||
|
||||
typed.Complete()
|
||||
if err := validation.ValidateProxyConfigurerForClient(typed.ProxyConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("validation error: %v", err))
|
||||
}
|
||||
|
||||
if err := c.storeSource.UpdateProxy(typed.ProxyConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: updated proxy %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||
}
|
||||
|
||||
if err := c.storeSource.RemoveProxy(name); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: deleted proxy %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
||||
visitors, err := c.storeSource.GetAllVisitors()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to list visitors: %v", err))
|
||||
}
|
||||
resp := VisitorListResp{Visitors: make([]VisitorConfig, 0, len(visitors))}
|
||||
|
||||
for _, v := range visitors {
|
||||
cfg, err := visitorConfigurerToMap(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Visitors = append(resp.Visitors, VisitorConfig{
|
||||
Name: v.GetBaseConfig().Name,
|
||||
Type: v.GetBaseConfig().Type,
|
||||
Config: cfg,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
v := c.storeSource.GetVisitor(name)
|
||||
if v == nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name))
|
||||
}
|
||||
|
||||
cfg, err := visitorConfigurerToMap(v)
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return VisitorConfig{
|
||||
Name: v.GetBaseConfig().Name,
|
||||
Type: v.GetBaseConfig().Type,
|
||||
Config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var typed v1.TypedVisitorConfig
|
||||
if err := json.Unmarshal(body, &typed); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if typed.VisitorConfigurer == nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
||||
}
|
||||
|
||||
typed.Complete()
|
||||
if err := validation.ValidateVisitorConfigurer(typed.VisitorConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("validation error: %v", err))
|
||||
}
|
||||
|
||||
if err := c.storeSource.AddVisitor(typed.VisitorConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusConflict, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: created visitor %q", typed.VisitorConfigurer.GetBaseConfig().Name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
body, err := ctx.Body()
|
||||
if err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||
}
|
||||
|
||||
var typed v1.TypedVisitorConfig
|
||||
if err := json.Unmarshal(body, &typed); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||
}
|
||||
|
||||
if typed.VisitorConfigurer == nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "invalid visitor config: type is required")
|
||||
}
|
||||
|
||||
bodyName := typed.VisitorConfigurer.GetBaseConfig().Name
|
||||
if bodyName != name {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name in URL must match name in body")
|
||||
}
|
||||
|
||||
typed.Complete()
|
||||
if err := validation.ValidateVisitorConfigurer(typed.VisitorConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("validation error: %v", err))
|
||||
}
|
||||
|
||||
if err := c.storeSource.UpdateVisitor(typed.VisitorConfigurer); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: updated visitor %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||
}
|
||||
|
||||
if err := c.storeSource.RemoveVisitor(name); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
if err := c.reloadFromSourcesOrError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("store: deleted visitor %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func proxyConfigurerToMap(p v1.ProxyConfigurer) (map[string]any, error) {
|
||||
data, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func visitorConfigurerToMap(v v1.VisitorConfigurer) (map[string]any, error) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
59
client/api/types.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
// StatusResp is the response for GET /api/status
|
||||
type StatusResp map[string][]ProxyStatusResp
|
||||
|
||||
// ProxyStatusResp contains proxy status information
|
||||
type ProxyStatusResp struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Err string `json:"err"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
Plugin string `json:"plugin"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Source string `json:"source,omitempty"` // "store" or "config"
|
||||
}
|
||||
|
||||
// ProxyConfig wraps proxy configuration for API requests/responses.
|
||||
type ProxyConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
// VisitorConfig wraps visitor configuration for API requests/responses.
|
||||
type VisitorConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
// ProxyListResp is the response for GET /api/store/proxies
|
||||
type ProxyListResp struct {
|
||||
Proxies []ProxyConfig `json:"proxies"`
|
||||
}
|
||||
|
||||
// VisitorListResp is the response for GET /api/store/visitors
|
||||
type VisitorListResp struct {
|
||||
Visitors []VisitorConfig `json:"visitors"`
|
||||
}
|
||||
|
||||
// ErrorResp represents an error response
|
||||
type ErrorResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -17,7 +17,6 @@ package client
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error {
|
||||
|
||||
fmuxCfg := fmux.DefaultConfig()
|
||||
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
||||
fmuxCfg.LogOutput = io.Discard
|
||||
// Use trace level for yamux logs
|
||||
fmuxCfg.LogOutput = xlog.NewTraceWriter(xl)
|
||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||
session, err := fmux.Client(conn, fmuxCfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"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/wait"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
@@ -43,8 +44,8 @@ type SessionContext struct {
|
||||
Conn net.Conn
|
||||
// Indicates whether the connection is encrypted.
|
||||
ConnEncrypted bool
|
||||
// Sets authentication based on selected method
|
||||
AuthSetter auth.Setter
|
||||
// Auth runtime used for login, heartbeats, and encryption.
|
||||
Auth *auth.ClientAuth
|
||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||
Connector Connector
|
||||
// Virtual net controller
|
||||
@@ -91,7 +92,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
||||
ctl.lastPong.Store(time.Now())
|
||||
|
||||
if sessionCtx.ConnEncrypted {
|
||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
|
||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -100,9 +101,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||
}
|
||||
ctl.registerMsgHandlers()
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||
|
||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
|
||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||
return ctl, nil
|
||||
@@ -133,7 +134,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
||||
m := &msg.NewWorkConn{
|
||||
RunID: ctl.sessionCtx.RunID,
|
||||
}
|
||||
if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
|
||||
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||
workConn.Close()
|
||||
return
|
||||
@@ -156,6 +157,8 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
startMsg.ProxyName = util.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName)
|
||||
|
||||
// dispatch this work connection to related proxy
|
||||
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
||||
}
|
||||
@@ -165,11 +168,12 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
||||
inMsg := m.(*msg.NewProxyResp)
|
||||
// Server will return NewProxyResp message to each NewProxy message.
|
||||
// Start a new proxy handler if no error got
|
||||
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||
proxyName := util.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName)
|
||||
err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||
if err != nil {
|
||||
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
|
||||
xl.Warnf("[%s] start error: %v", proxyName, err)
|
||||
} else {
|
||||
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
|
||||
xl.Infof("[%s] start proxy success", proxyName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +247,7 @@ func (ctl *Control) heartbeatWorker() {
|
||||
sendHeartBeat := func() (bool, error) {
|
||||
xl.Debugf("send heartbeat to server")
|
||||
pingMsg := &msg.Ping{}
|
||||
if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
|
||||
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||
return false, err
|
||||
}
|
||||
@@ -276,10 +280,12 @@ func (ctl *Control) heartbeatWorker() {
|
||||
}
|
||||
|
||||
func (ctl *Control) worker() {
|
||||
xl := ctl.xl
|
||||
go ctl.heartbeatWorker()
|
||||
go ctl.msgDispatcher.Run()
|
||||
|
||||
<-ctl.msgDispatcher.Done()
|
||||
xl.Debugf("control message dispatcher exited")
|
||||
ctl.closeSession()
|
||||
|
||||
ctl.pm.Close()
|
||||
|
||||
@@ -57,6 +57,7 @@ func NewProxy(
|
||||
ctx context.Context,
|
||||
pxyConf v1.ProxyConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) (pxy Proxy) {
|
||||
@@ -69,6 +70,7 @@ func NewProxy(
|
||||
baseProxy := BaseProxy{
|
||||
baseCfg: pxyConf.GetBaseConfig(),
|
||||
clientCfg: clientCfg,
|
||||
encryptionKey: encryptionKey,
|
||||
limiter: limiter,
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
@@ -86,6 +88,7 @@ func NewProxy(
|
||||
type BaseProxy struct {
|
||||
baseCfg *v1.ProxyBaseConfig
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
encryptionKey []byte
|
||||
msgTransporter transport.MessageTransporter
|
||||
vnetController *vnet.Controller
|
||||
limiter *rate.Limiter
|
||||
@@ -129,7 +132,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
|
||||
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
||||
}
|
||||
|
||||
// Common handler for tcp work connections.
|
||||
|
||||
@@ -40,7 +40,8 @@ type Manager struct {
|
||||
closed bool
|
||||
mu sync.RWMutex
|
||||
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
encryptionKey []byte
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
|
||||
ctx context.Context
|
||||
}
|
||||
@@ -48,6 +49,7 @@ type Manager struct {
|
||||
func NewManager(
|
||||
ctx context.Context,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
) *Manager {
|
||||
@@ -56,6 +58,7 @@ func NewManager(
|
||||
msgTransporter: msgTransporter,
|
||||
vnetController: vnetController,
|
||||
closed: false,
|
||||
encryptionKey: encryptionKey,
|
||||
clientCfg: clientCfg,
|
||||
ctx: ctx,
|
||||
}
|
||||
@@ -163,7 +166,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
||||
for _, cfg := range proxyCfgs {
|
||||
name := cfg.GetBaseConfig().Name
|
||||
if _, ok := pm.proxies[name]; !ok {
|
||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||
if pm.inWorkConnCallback != nil {
|
||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
@@ -86,12 +87,15 @@ type Wrapper struct {
|
||||
|
||||
xl *xlog.Logger
|
||||
ctx context.Context
|
||||
|
||||
wireName string
|
||||
}
|
||||
|
||||
func NewWrapper(
|
||||
ctx context.Context,
|
||||
cfg v1.ProxyConfigurer,
|
||||
clientCfg *v1.ClientCommonConfig,
|
||||
encryptionKey []byte,
|
||||
eventHandler event.Handler,
|
||||
msgTransporter transport.MessageTransporter,
|
||||
vnetController *vnet.Controller,
|
||||
@@ -112,6 +116,7 @@ func NewWrapper(
|
||||
vnetController: vnetController,
|
||||
xl: xl,
|
||||
ctx: xlog.NewContext(ctx, xl),
|
||||
wireName: util.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
||||
}
|
||||
|
||||
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||
@@ -122,7 +127,7 @@ func NewWrapper(
|
||||
xl.Tracef("enable health check monitor")
|
||||
}
|
||||
|
||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
|
||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
||||
return pw
|
||||
}
|
||||
|
||||
@@ -181,7 +186,7 @@ func (pw *Wrapper) Stop() {
|
||||
func (pw *Wrapper) close() {
|
||||
_ = pw.handler(&event.CloseProxyPayload{
|
||||
CloseProxyMsg: &msg.CloseProxy{
|
||||
ProxyName: pw.Name,
|
||||
ProxyName: pw.wireName,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -207,6 +212,7 @@ func (pw *Wrapper) checkWorker() {
|
||||
|
||||
var newProxyMsg msg.NewProxy
|
||||
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
||||
newProxyMsg.ProxyName = pw.wireName
|
||||
pw.lastSendStartMsg = now
|
||||
_ = pw.handler(&event.StartProxyPayload{
|
||||
NewProxyMsg: &newProxyMsg,
|
||||
|
||||
@@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
})
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
|
||||
@@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
})
|
||||
}
|
||||
if pxy.cfg.Transport.UseEncryption {
|
||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -64,11 +65,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
}
|
||||
|
||||
xl.Tracef("nathole prepare start")
|
||||
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
|
||||
|
||||
// Prepare NAT traversal options
|
||||
var opts nathole.PrepareOptions
|
||||
if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs {
|
||||
opts.DisableAssistedAddrs = true
|
||||
}
|
||||
|
||||
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts)
|
||||
if err != nil {
|
||||
xl.Warnf("nathole prepare error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||
defer prepareResult.ListenConn.Close()
|
||||
@@ -77,7 +86,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
transactionID := nathole.NewTransactionID()
|
||||
natHoleClientMsg := &msg.NatHoleClient{
|
||||
TransactionID: transactionID,
|
||||
ProxyName: pxy.cfg.Name,
|
||||
ProxyName: util.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name),
|
||||
Sid: natHoleSidMsg.Sid,
|
||||
MappedAddrs: prepareResult.Addrs,
|
||||
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||
|
||||
@@ -29,8 +29,11 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
@@ -60,9 +63,13 @@ func (e cancelErr) Error() string {
|
||||
|
||||
// ServiceOptions contains options for creating a new client service.
|
||||
type ServiceOptions struct {
|
||||
Common *v1.ClientCommonConfig
|
||||
ProxyCfgs []v1.ProxyConfigurer
|
||||
VisitorCfgs []v1.VisitorConfigurer
|
||||
Common *v1.ClientCommonConfig
|
||||
|
||||
// ConfigSourceAggregator manages internal config and optional store sources.
|
||||
// It is required for creating a Service.
|
||||
ConfigSourceAggregator *source.Aggregator
|
||||
|
||||
UnsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||
// If it is empty, it means that the configuration file is not used for initialization.
|
||||
@@ -108,19 +115,30 @@ type Service struct {
|
||||
// Uniq id got from frps, it will be attached to loginMsg.
|
||||
runID string
|
||||
|
||||
// Sets authentication based on selected method
|
||||
authSetter auth.Setter
|
||||
// Auth runtime and encryption materials
|
||||
auth *auth.ClientAuth
|
||||
|
||||
// web server for admin UI and apis
|
||||
webServer *httppkg.Server
|
||||
|
||||
vnetController *vnet.Controller
|
||||
|
||||
cfgMu sync.RWMutex
|
||||
common *v1.ClientCommonConfig
|
||||
proxyCfgs []v1.ProxyConfigurer
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
clientSpec *msg.ClientSpec
|
||||
cfgMu sync.RWMutex
|
||||
common *v1.ClientCommonConfig
|
||||
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
||||
// It can be updated by /api/reload without mutating startup-only common behavior.
|
||||
reloadCommon *v1.ClientCommonConfig
|
||||
proxyCfgs []v1.ProxyConfigurer
|
||||
visitorCfgs []v1.VisitorConfigurer
|
||||
clientSpec *msg.ClientSpec
|
||||
|
||||
// aggregator manages multiple configuration sources.
|
||||
// When set, the service watches for config changes and reloads automatically.
|
||||
aggregator *source.Aggregator
|
||||
configSource *source.ConfigSource
|
||||
storeSource *source.StoreSource
|
||||
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
|
||||
// The configuration file used to initialize this client, or an empty
|
||||
// string if no configuration file was used.
|
||||
@@ -149,18 +167,45 @@ func NewService(options ServiceOptions) (*Service, error) {
|
||||
}
|
||||
webServer = ws
|
||||
}
|
||||
|
||||
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.ConfigSourceAggregator == nil {
|
||||
return nil, fmt.Errorf("config source aggregator is required")
|
||||
}
|
||||
|
||||
configSource := options.ConfigSourceAggregator.ConfigSource()
|
||||
storeSource := options.ConfigSourceAggregator.StoreSource()
|
||||
|
||||
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
||||
if loadErr != nil {
|
||||
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
||||
}
|
||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||
|
||||
s := &Service{
|
||||
ctx: context.Background(),
|
||||
authSetter: auth.NewAuthSetter(options.Common.Auth),
|
||||
auth: authRuntime,
|
||||
webServer: webServer,
|
||||
common: options.Common,
|
||||
reloadCommon: options.Common,
|
||||
configFilePath: options.ConfigFilePath,
|
||||
proxyCfgs: options.ProxyCfgs,
|
||||
visitorCfgs: options.VisitorCfgs,
|
||||
unsafeFeatures: options.UnsafeFeatures,
|
||||
proxyCfgs: proxyCfgs,
|
||||
visitorCfgs: visitorCfgs,
|
||||
clientSpec: options.ClientSpec,
|
||||
aggregator: options.ConfigSourceAggregator,
|
||||
configSource: configSource,
|
||||
storeSource: storeSource,
|
||||
connectorCreator: options.ConnectorCreator,
|
||||
handleWorkConnCb: options.HandleWorkConnCb,
|
||||
}
|
||||
|
||||
if webServer != nil {
|
||||
webServer.RouteRegister(s.registerRouteHandlers)
|
||||
}
|
||||
@@ -269,11 +314,15 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
loginMsg := &msg.Login{
|
||||
Arch: runtime.GOARCH,
|
||||
Os: runtime.GOOS,
|
||||
Hostname: hostname,
|
||||
PoolCount: svr.common.Transport.PoolCount,
|
||||
User: svr.common.User,
|
||||
ClientID: svr.common.ClientID,
|
||||
Version: version.Full(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
RunID: svr.runID,
|
||||
@@ -284,7 +333,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
||||
}
|
||||
|
||||
// Add auth
|
||||
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
||||
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,7 +387,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
||||
RunID: svr.runID,
|
||||
Conn: conn,
|
||||
ConnEncrypted: connEncrypted,
|
||||
AuthSetter: svr.authSetter,
|
||||
Auth: svr.auth,
|
||||
Connector: connector,
|
||||
VnetController: svr.vnetController,
|
||||
}
|
||||
@@ -387,6 +436,33 @@ func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svr *Service) UpdateConfigSource(
|
||||
common *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
) error {
|
||||
cfgSource := svr.configSource
|
||||
if cfgSource == nil {
|
||||
return fmt.Errorf("config source is not available")
|
||||
}
|
||||
|
||||
// Update reloadCommon before ReplaceAll so the subsequent reload uses the
|
||||
// same common config as /api/reload validation.
|
||||
svr.cfgMu.Lock()
|
||||
prevReloadCommon := svr.reloadCommon
|
||||
svr.reloadCommon = common
|
||||
svr.cfgMu.Unlock()
|
||||
|
||||
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||
svr.cfgMu.Lock()
|
||||
svr.reloadCommon = prevReloadCommon
|
||||
svr.cfgMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
return svr.reloadConfigFromSources()
|
||||
}
|
||||
|
||||
func (svr *Service) Close() {
|
||||
svr.GracefulClose(time.Duration(0))
|
||||
}
|
||||
@@ -407,6 +483,11 @@ func (svr *Service) stop() {
|
||||
svr.webServer.Close()
|
||||
svr.webServer = nil
|
||||
}
|
||||
if svr.aggregator != nil {
|
||||
svr.aggregator = nil
|
||||
}
|
||||
svr.configSource = nil
|
||||
svr.storeSource = nil
|
||||
}
|
||||
|
||||
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||
@@ -437,3 +518,28 @@ type statusExporterImpl struct {
|
||||
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||
return s.getProxyStatusFunc(name)
|
||||
}
|
||||
|
||||
func (svr *Service) reloadConfigFromSources() error {
|
||||
if svr.aggregator == nil {
|
||||
return errors.New("config aggregator is not initialized")
|
||||
}
|
||||
|
||||
svr.cfgMu.RLock()
|
||||
reloadCommon := svr.reloadCommon
|
||||
svr.cfgMu.RUnlock()
|
||||
|
||||
proxies, visitors, err := svr.aggregator.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload config from sources failed: %w", err)
|
||||
}
|
||||
|
||||
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
||||
proxies = config.CompleteProxyConfigurers(proxies)
|
||||
visitors = config.CompleteVisitorConfigurers(visitors)
|
||||
|
||||
// Atomically replace the entire configuration
|
||||
if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package visitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
@@ -81,19 +82,31 @@ func (sv *STCPVisitor) internalConnWorker() {
|
||||
|
||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
defer userConn.Close()
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}()
|
||||
|
||||
xl.Debugf("get a new stcp user connection")
|
||||
visitorConn, err := sv.helper.ConnectServer()
|
||||
if err != nil {
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
defer visitorConn.Close()
|
||||
|
||||
now := time.Now().Unix()
|
||||
targetProxyName := util.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
ProxyName: targetProxyName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
@@ -102,6 +115,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||
if err != nil {
|
||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,12 +124,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -125,6 +141,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +205,10 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
targetProxyName := util.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||
RunID: sv.helper.RunID(),
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
ProxyName: targetProxyName,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||
|
||||
@@ -71,7 +71,7 @@ func NewVisitor(
|
||||
Name: cfg.GetBaseConfig().Name,
|
||||
Ctx: ctx,
|
||||
VnetController: helper.VNetController(),
|
||||
HandleConn: func(conn net.Conn) {
|
||||
SendConnToVisitor: func(conn net.Conn) {
|
||||
_ = baseVisitor.AcceptConn(conn)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -145,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||
return
|
||||
case <-ticker.C:
|
||||
xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
|
||||
conn, err := sv.getTunnelConn()
|
||||
conn, err := sv.getTunnelConn(sv.ctx)
|
||||
if err != nil {
|
||||
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
||||
_ = sv.retryLimiter.Wait(sv.ctx)
|
||||
@@ -161,9 +161,17 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||
|
||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
isConnTransfered := false
|
||||
isConnTransferred := false
|
||||
var tunnelErr error
|
||||
defer func() {
|
||||
if !isConnTransfered {
|
||||
if !isConnTransferred {
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
}
|
||||
userConn.Close()
|
||||
}
|
||||
}()
|
||||
@@ -172,7 +180,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
|
||||
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
|
||||
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
|
||||
ctx := context.Background()
|
||||
ctx := sv.ctx
|
||||
if sv.cfg.FallbackTo != "" {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
@@ -181,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
tunnelConn, err := sv.openTunnel(ctx)
|
||||
if err != nil {
|
||||
xl.Errorf("open tunnel error: %v", err)
|
||||
tunnelErr = err
|
||||
|
||||
// no fallback, just return
|
||||
if sv.cfg.FallbackTo == "" {
|
||||
return
|
||||
@@ -191,7 +201,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
||||
return
|
||||
}
|
||||
isConnTransfered = true
|
||||
isConnTransferred = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||
if err != nil {
|
||||
xl.Errorf("create encryption stream error: %v", err)
|
||||
tunnelErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -219,40 +230,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||
// openTunnel will open a tunnel connection to the target server.
|
||||
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
timeoutC := time.After(20 * time.Second)
|
||||
immediateTrigger := make(chan struct{}, 1)
|
||||
defer close(immediateTrigger)
|
||||
immediateTrigger <- struct{}{}
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sv.ctx.Done():
|
||||
return nil, sv.ctx.Err()
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-immediateTrigger:
|
||||
conn, err = sv.getTunnelConn()
|
||||
case <-ticker.C:
|
||||
conn, err = sv.getTunnelConn()
|
||||
case <-timeoutC:
|
||||
return nil, fmt.Errorf("open tunnel timeout")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err != ErrNoTunnelSession {
|
||||
xl.Warnf("get tunnel connection error: %v", err)
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, fmt.Errorf("open tunnel timeout")
|
||||
}
|
||||
continue
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
conn, err = sv.getTunnelConn(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrNoTunnelSession) {
|
||||
xl.Warnf("get tunnel connection error: %v", err)
|
||||
}
|
||||
timer.Reset(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
|
||||
conn, err := sv.session.OpenConn(sv.ctx)
|
||||
func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
||||
conn, err := sv.session.OpenConn(ctx)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
@@ -272,18 +280,27 @@ func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
|
||||
// 4. Create a tunnel session using an underlying UDP connection.
|
||||
func (sv *XTCPVisitor) makeNatHole() {
|
||||
xl := xlog.FromContextSafe(sv.ctx)
|
||||
targetProxyName := util.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||
xl.Tracef("makeNatHole start")
|
||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
|
||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil {
|
||||
xl.Warnf("nathole precheck error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Tracef("nathole prepare start")
|
||||
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
|
||||
|
||||
// Prepare NAT traversal options
|
||||
var opts nathole.PrepareOptions
|
||||
if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs {
|
||||
opts.DisableAssistedAddrs = true
|
||||
}
|
||||
|
||||
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts)
|
||||
if err != nil {
|
||||
xl.Warnf("nathole prepare error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||
|
||||
@@ -294,7 +311,7 @@ func (sv *XTCPVisitor) makeNatHole() {
|
||||
transactionID := nathole.NewTransactionID()
|
||||
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
||||
TransactionID: transactionID,
|
||||
ProxyName: sv.cfg.ServerName,
|
||||
ProxyName: targetProxyName,
|
||||
Protocol: sv.cfg.Protocol,
|
||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||
Timestamp: now,
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/fatedier/frp/assets/frpc"
|
||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
var proxyTypes = []v1.ProxyType{
|
||||
@@ -77,18 +79,22 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Complete(clientCfg.User)
|
||||
c.GetBaseConfig().Type = name
|
||||
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
|
||||
c.Complete()
|
||||
proxyCfg := c
|
||||
if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
||||
err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -106,18 +112,21 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Complete(clientCfg)
|
||||
c.GetBaseConfig().Type = name
|
||||
if err := validation.ValidateVisitorConfigurer(c); err != nil {
|
||||
c.Complete()
|
||||
visitorCfg := c
|
||||
if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -125,3 +134,18 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func startService(
|
||||
cfg *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) error {
|
||||
configSource := source.NewConfigSource()
|
||||
if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||
return fmt.Errorf("failed to set config source: %w", err)
|
||||
}
|
||||
aggregator := source.NewAggregator(configSource)
|
||||
return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -29,9 +30,11 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
)
|
||||
@@ -41,6 +44,7 @@ var (
|
||||
cfgDir string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -48,6 +52,9 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -59,15 +66,17 @@ var rootCmd = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
|
||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||
if cfgDir != "" {
|
||||
_ = runMultipleClients(cfgDir)
|
||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not show command usage here.
|
||||
err := runClient(cfgFile)
|
||||
err := runClient(cfgFile, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
@@ -76,7 +85,7 @@ var rootCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runMultipleClients(cfgDir string) error {
|
||||
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
var wg sync.WaitGroup
|
||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
@@ -86,7 +95,7 @@ func runMultipleClients(cfgDir string) error {
|
||||
time.Sleep(time.Millisecond)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := runClient(path)
|
||||
err := runClient(path, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||
}
|
||||
@@ -111,56 +120,98 @@ func handleTermSignal(svr *client.Service) {
|
||||
svr.GracefulClose(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func runClient(cfgFilePath string) error {
|
||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
// Load configuration
|
||||
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isLegacyFormat {
|
||||
if result.IsLegacyFormat {
|
||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
}
|
||||
|
||||
if len(cfg.FeatureGates) > 0 {
|
||||
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
||||
if len(result.Common.FeatureGates) > 0 {
|
||||
if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
|
||||
return runClientWithAggregator(result, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
// runClientWithAggregator runs the client using the internal source aggregator.
|
||||
func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error {
|
||||
configSource := source.NewConfigSource()
|
||||
if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
||||
return fmt.Errorf("failed to set config source: %w", err)
|
||||
}
|
||||
|
||||
var storeSource *source.StoreSource
|
||||
|
||||
if result.Common.Store.IsEnabled() {
|
||||
storePath := result.Common.Store.Path
|
||||
if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) {
|
||||
storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath)
|
||||
}
|
||||
|
||||
s, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||
Path: storePath,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create store source: %w", err)
|
||||
}
|
||||
storeSource = s
|
||||
}
|
||||
|
||||
aggregator := source.NewAggregator(configSource)
|
||||
if storeSource != nil {
|
||||
aggregator.SetStoreSource(storeSource)
|
||||
}
|
||||
|
||||
proxyCfgs, visitorCfgs, err := aggregator.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config from sources: %w", err)
|
||||
}
|
||||
|
||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||
|
||||
warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
|
||||
|
||||
return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath)
|
||||
}
|
||||
|
||||
func startService(
|
||||
func startServiceWithAggregator(
|
||||
cfg *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
aggregator *source.Aggregator,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
cfgFile string,
|
||||
) error {
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
log.Infof("start frpc service for config file [%s]", cfgFile)
|
||||
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
|
||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||
}
|
||||
svr, err := client.NewService(client.ServiceOptions{
|
||||
Common: cfg,
|
||||
ProxyCfgs: proxyCfgs,
|
||||
VisitorCfgs: visitorCfgs,
|
||||
ConfigFilePath: cfgFile,
|
||||
Common: cfg,
|
||||
ConfigSourceAggregator: aggregator,
|
||||
UnsafeFeatures: unsafeFeatures,
|
||||
ConfigFilePath: cfgFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
||||
// Capture the exit signal if we use kcp or quic.
|
||||
if shouldGracefulClose {
|
||||
go handleTermSignal(svr)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -42,7 +43,8 @@ var verifyCmd = &cobra.Command{
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/fatedier/frp/assets/frps"
|
||||
_ "github.com/fatedier/frp/pkg/metrics"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/server"
|
||||
@@ -33,6 +35,7 @@ var (
|
||||
cfgFile string
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
|
||||
serverCfg v1.ServerConfig
|
||||
)
|
||||
@@ -41,6 +44,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
||||
|
||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||
}
|
||||
@@ -77,7 +82,9 @@ var rootCmd = &cobra.Command{
|
||||
svrCfg = &serverCfg
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -42,7 +43,9 @@ var verifyCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||
if warning != nil {
|
||||
fmt.Printf("WARNING: %v\n", warning)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||
|
||||
# Optional unique identifier for this frpc instance.
|
||||
clientID = "your_client_id"
|
||||
# your proxy name will be changed to {user}.{proxy}
|
||||
user = "your_name"
|
||||
|
||||
@@ -55,6 +57,20 @@ auth.token = "12345678"
|
||||
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
||||
# auth.oidc.additionalEndpointParams.var1 = "foobar"
|
||||
|
||||
# OIDC TLS and proxy configuration
|
||||
# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate.
|
||||
# This is useful when the OIDC provider uses a self-signed certificate or a custom CA.
|
||||
# auth.oidc.trustedCaFile = "/path/to/ca.crt"
|
||||
|
||||
# Skip TLS certificate verification for the OIDC token endpoint.
|
||||
# INSECURE: Only use this for debugging purposes, not recommended for production.
|
||||
# auth.oidc.insecureSkipVerify = false
|
||||
|
||||
# Specify a proxy server for OIDC token endpoint connections.
|
||||
# Supports http, https, socks5, and socks5h proxy protocols.
|
||||
# If not specified, no proxy is used for OIDC connections.
|
||||
# auth.oidc.proxyURL = "http://proxy.example.com:8080"
|
||||
|
||||
# Set admin address for control frpc's action by http api such as reload
|
||||
webServer.addr = "127.0.0.1"
|
||||
webServer.port = 7400
|
||||
@@ -129,6 +145,11 @@ transport.tls.enable = true
|
||||
# Default is empty, means all proxies.
|
||||
# start = ["ssh", "dns"]
|
||||
|
||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||
# Set 'enabled = false' in a proxy configuration to disable it.
|
||||
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
||||
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
||||
|
||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||
# This parameter should be same between client and server.
|
||||
# It affects the udp and sudp proxy.
|
||||
@@ -155,6 +176,8 @@ metadatas.var2 = "123"
|
||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||
name = "ssh"
|
||||
type = "tcp"
|
||||
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||
# enabled = true
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
# Limit bandwidth for this proxy, unit is KB and MB
|
||||
@@ -239,6 +262,8 @@ healthCheck.httpHeaders=[
|
||||
[[proxies]]
|
||||
name = "web02"
|
||||
type = "https"
|
||||
# Disable this proxy by setting enabled to false
|
||||
# enabled = false
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 8000
|
||||
subdomain = "web02"
|
||||
@@ -372,6 +397,14 @@ localPort = 22
|
||||
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||
allowUsers = ["user1", "user2"]
|
||||
|
||||
# NAT traversal configuration (optional)
|
||||
[proxies.natTraversal]
|
||||
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||
# When enabled, only STUN-discovered public addresses will be used.
|
||||
# This can improve performance when you have slow VPN connections.
|
||||
# Default: false
|
||||
disableAssistedAddrs = false
|
||||
|
||||
[[proxies]]
|
||||
name = "vnet-server"
|
||||
type = "stcp"
|
||||
@@ -411,6 +444,13 @@ minRetryInterval = 90
|
||||
# fallbackTo = "stcp_visitor"
|
||||
# fallbackTimeoutMs = 500
|
||||
|
||||
# NAT traversal configuration (optional)
|
||||
[visitors.natTraversal]
|
||||
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||
# When enabled, only STUN-discovered public addresses will be used.
|
||||
# Default: false
|
||||
disableAssistedAddrs = false
|
||||
|
||||
[[visitors]]
|
||||
name = "vnet-visitor"
|
||||
type = "stcp"
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 37 KiB |
BIN
doc/pic/zsxq.jpg
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,9 +1,17 @@
|
||||
FROM golang:1.23 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frpc
|
||||
COPY web/frpc/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
||||
WORKDIR /building
|
||||
|
||||
RUN make frpc
|
||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
FROM golang:1.23 AS building
|
||||
FROM node:22 AS web-builder
|
||||
|
||||
WORKDIR /web/frps
|
||||
COPY web/frps/ ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.24 AS building
|
||||
|
||||
COPY . /building
|
||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
||||
WORKDIR /building
|
||||
|
||||
RUN make frps
|
||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
|
||||
21
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/fatedier/frp
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
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.53.0
|
||||
github.com/quic-go/quic-go v0.55.0
|
||||
github.com/rodaine/table v1.2.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
@@ -26,10 +26,10 @@ require (
|
||||
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.37.0
|
||||
golang.org/x/net v0.39.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.13.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
|
||||
@@ -67,11 +67,10 @@ require (
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.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
|
||||
@@ -82,4 +81,4 @@ require (
|
||||
)
|
||||
|
||||
// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
|
||||
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d
|
||||
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6
|
||||
|
||||
44
go.sum
@@ -22,8 +22,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
||||
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
|
||||
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
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=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
@@ -105,8 +105,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
|
||||
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
||||
@@ -156,24 +156,24 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2
|
||||
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.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
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.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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
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.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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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=
|
||||
@@ -187,8 +187,8 @@ 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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
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=
|
||||
@@ -197,8 +197,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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=
|
||||
@@ -213,24 +213,24 @@ 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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
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.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
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=
|
||||
@@ -241,8 +241,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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=
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
@@ -27,16 +28,56 @@ type Setter interface {
|
||||
SetNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
|
||||
type ClientAuth struct {
|
||||
Setter Setter
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (a *ClientAuth) EncryptionKey() []byte {
|
||||
return a.key
|
||||
}
|
||||
|
||||
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||
// Caller must run validation before calling this function.
|
||||
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("auth config is nil")
|
||||
}
|
||||
resolved := *cfg
|
||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
resolved.Token = token
|
||||
}
|
||||
setter, err := NewAuthSetter(resolved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ClientAuth{
|
||||
Setter: setter,
|
||||
key: []byte(resolved.Token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
||||
switch cfg.Method {
|
||||
case v1.AuthMethodToken:
|
||||
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||
case v1.AuthMethodOIDC:
|
||||
authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||
if cfg.OIDC.TokenSource != nil {
|
||||
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
|
||||
} else {
|
||||
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
|
||||
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
||||
}
|
||||
return authProvider
|
||||
return authProvider, nil
|
||||
}
|
||||
|
||||
type Verifier interface {
|
||||
@@ -45,6 +86,35 @@ type Verifier interface {
|
||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||
}
|
||||
|
||||
type ServerAuth struct {
|
||||
Verifier Verifier
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (a *ServerAuth) EncryptionKey() []byte {
|
||||
return a.key
|
||||
}
|
||||
|
||||
// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||
// Caller must run validation before calling this function.
|
||||
func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("auth config is nil")
|
||||
}
|
||||
resolved := *cfg
|
||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
resolved.Token = token
|
||||
}
|
||||
return &ServerAuth{
|
||||
Verifier: NewAuthVerifier(resolved),
|
||||
key: []byte(resolved.Token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||
switch cfg.Method {
|
||||
case v1.AuthMethodToken:
|
||||
|
||||
116
pkg/auth/oidc.go
@@ -16,23 +16,72 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"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/msg"
|
||||
)
|
||||
|
||||
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
|
||||
func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) {
|
||||
// Clone the default transport to get all reasonable defaults
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// Configure TLS settings
|
||||
if trustedCAFile != "" || insecureSkipVerify {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
if trustedCAFile != "" && !insecureSkipVerify {
|
||||
caCert, err := os.ReadFile(trustedCAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile)
|
||||
}
|
||||
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
// Configure proxy settings
|
||||
if proxyURL != "" {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err)
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(parsedURL)
|
||||
} else {
|
||||
// Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment
|
||||
transport.Proxy = nil
|
||||
}
|
||||
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}
|
||||
|
||||
type OidcAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenGenerator *clientcredentials.Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||
eps := make(map[string][]string)
|
||||
for k, v := range cfg.AdditionalEndpointParams {
|
||||
eps[k] = []string{v}
|
||||
@@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
||||
EndpointParams: eps,
|
||||
}
|
||||
|
||||
// Create custom HTTP client if needed
|
||||
var httpClient *http.Client
|
||||
if 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)
|
||||
}
|
||||
}
|
||||
|
||||
return &OidcAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
tokenGenerator: tokenGenerator,
|
||||
}
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||
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)
|
||||
}
|
||||
@@ -87,6 +152,51 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
||||
return err
|
||||
}
|
||||
|
||||
type OidcTokenSourceAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
valueSource *v1.ValueSource
|
||||
}
|
||||
|
||||
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
|
||||
return &OidcTokenSourceAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
valueSource: valueSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
ctx := context.Background()
|
||||
accessToken, err = auth.valueSource.Resolve(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||
return nil
|
||||
}
|
||||
|
||||
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
type TokenVerifier interface {
|
||||
Verify(context.Context, string) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||
}
|
||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
|
||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
|
||||
}
|
||||
|
||||
configurer.UnmarshalFromMsg(m)
|
||||
configurer.Complete("")
|
||||
configurer.Complete()
|
||||
|
||||
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
||||
return nil, err
|
||||
@@ -219,60 +219,131 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
|
||||
return svrCfg, isLegacyFormat, nil
|
||||
}
|
||||
|
||||
// ClientConfigLoadResult contains the result of loading a client configuration file.
|
||||
type ClientConfigLoadResult struct {
|
||||
// Common contains the common client configuration.
|
||||
Common *v1.ClientCommonConfig
|
||||
|
||||
// Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles.
|
||||
// These are NOT completed (user prefix not added).
|
||||
Proxies []v1.ProxyConfigurer
|
||||
|
||||
// Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles.
|
||||
// These are NOT completed.
|
||||
Visitors []v1.VisitorConfigurer
|
||||
|
||||
// IsLegacyFormat indicates whether the config file is in legacy INI format.
|
||||
IsLegacyFormat bool
|
||||
}
|
||||
|
||||
// LoadClientConfigResult loads and parses a client configuration file.
|
||||
// It returns the raw configuration without completing proxies/visitors.
|
||||
// The caller should call Complete on the configs manually for legacy behavior.
|
||||
func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) {
|
||||
result := &ClientConfigLoadResult{
|
||||
Proxies: make([]v1.ProxyConfigurer, 0),
|
||||
Visitors: make([]v1.VisitorConfigurer, 0),
|
||||
}
|
||||
|
||||
if DetectLegacyINIFormatFromFile(path) {
|
||||
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||
for _, c := range legacyProxyCfgs {
|
||||
result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c))
|
||||
}
|
||||
for _, c := range legacyVisitorCfgs {
|
||||
result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c))
|
||||
}
|
||||
result.IsLegacyFormat = true
|
||||
} else {
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Common = &allCfg.ClientCommonConfig
|
||||
for _, c := range allCfg.Proxies {
|
||||
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
||||
}
|
||||
for _, c := range allCfg.Visitors {
|
||||
result.Visitors = append(result.Visitors, c.VisitorConfigurer)
|
||||
}
|
||||
}
|
||||
|
||||
// Load additional config from includes.
|
||||
// legacy ini format already handle this in ParseClientConfig.
|
||||
if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat {
|
||||
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Proxies = append(result.Proxies, extProxyCfgs...)
|
||||
result.Visitors = append(result.Visitors, extVisitorCfgs...)
|
||||
}
|
||||
|
||||
// Complete the common config
|
||||
if result.Common != nil {
|
||||
if err := result.Common.Complete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func LoadClientConfig(path string, strict bool) (
|
||||
*v1.ClientCommonConfig,
|
||||
[]v1.ProxyConfigurer,
|
||||
[]v1.VisitorConfigurer,
|
||||
bool, error,
|
||||
) {
|
||||
var (
|
||||
cliCfg *v1.ClientCommonConfig
|
||||
proxyCfgs = make([]v1.ProxyConfigurer, 0)
|
||||
visitorCfgs = make([]v1.VisitorConfigurer, 0)
|
||||
isLegacyFormat bool
|
||||
)
|
||||
|
||||
if DetectLegacyINIFormatFromFile(path) {
|
||||
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, true, err
|
||||
}
|
||||
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||
for _, c := range legacyProxyCfgs {
|
||||
proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
|
||||
}
|
||||
for _, c := range legacyVisitorCfgs {
|
||||
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
|
||||
}
|
||||
isLegacyFormat = true
|
||||
} else {
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
cliCfg = &allCfg.ClientCommonConfig
|
||||
for _, c := range allCfg.Proxies {
|
||||
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
||||
}
|
||||
for _, c := range allCfg.Visitors {
|
||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
||||
}
|
||||
result, err := LoadClientConfigResult(path, strict)
|
||||
if err != nil {
|
||||
return nil, nil, nil, result != nil && result.IsLegacyFormat, err
|
||||
}
|
||||
|
||||
// Load additional config from includes.
|
||||
// legacy ini format already handle this in ParseClientConfig.
|
||||
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
|
||||
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
|
||||
if err != nil {
|
||||
return nil, nil, nil, isLegacyFormat, err
|
||||
}
|
||||
proxyCfgs = append(proxyCfgs, extProxyCfgs...)
|
||||
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
|
||||
proxyCfgs := result.Proxies
|
||||
visitorCfgs := result.Visitors
|
||||
|
||||
proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||
proxyCfgs = CompleteProxyConfigurers(proxyCfgs)
|
||||
visitorCfgs = CompleteVisitorConfigurers(visitorCfgs)
|
||||
return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil
|
||||
}
|
||||
|
||||
func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer {
|
||||
proxyCfgs := proxies
|
||||
for _, c := range proxyCfgs {
|
||||
c.Complete()
|
||||
}
|
||||
return proxyCfgs
|
||||
}
|
||||
|
||||
func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer {
|
||||
visitorCfgs := visitors
|
||||
for _, c := range visitorCfgs {
|
||||
c.Complete()
|
||||
}
|
||||
return visitorCfgs
|
||||
}
|
||||
|
||||
func FilterClientConfigurers(
|
||||
common *v1.ClientCommonConfig,
|
||||
proxies []v1.ProxyConfigurer,
|
||||
visitors []v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||
if common == nil {
|
||||
common = &v1.ClientCommonConfig{}
|
||||
}
|
||||
|
||||
proxyCfgs := proxies
|
||||
visitorCfgs := visitors
|
||||
|
||||
// Filter by start
|
||||
if len(cliCfg.Start) > 0 {
|
||||
startSet := sets.New(cliCfg.Start...)
|
||||
if len(common.Start) > 0 {
|
||||
startSet := sets.New(common.Start...)
|
||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
return startSet.Has(c.GetBaseConfig().Name)
|
||||
})
|
||||
@@ -281,18 +352,17 @@ func LoadClientConfig(path string, strict bool) (
|
||||
})
|
||||
}
|
||||
|
||||
if cliCfg != nil {
|
||||
if err := cliCfg.Complete(); err != nil {
|
||||
return nil, nil, nil, isLegacyFormat, err
|
||||
}
|
||||
}
|
||||
for _, c := range proxyCfgs {
|
||||
c.Complete(cliCfg.User)
|
||||
}
|
||||
for _, c := range visitorCfgs {
|
||||
c.Complete(cliCfg)
|
||||
}
|
||||
return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
|
||||
// Filter by enabled field in each proxy
|
||||
// nil or true means enabled, false means disabled
|
||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
||||
enabled := c.GetBaseConfig().Enabled
|
||||
return enabled == nil || *enabled
|
||||
})
|
||||
return proxyCfgs, visitorCfgs
|
||||
}
|
||||
|
||||
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -273,6 +274,169 @@ proxies:
|
||||
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
||||
}
|
||||
|
||||
func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
enabled := true
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy-raw"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
proxyCfg.Enabled = &enabled
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor-raw"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server-raw"
|
||||
visitorCfg.FallbackTo = "fallback-raw"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
visitorCfg.Enabled = &enabled
|
||||
|
||||
common := &v1.ClientCommonConfig{
|
||||
User: "alice",
|
||||
}
|
||||
|
||||
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 1)
|
||||
|
||||
p := proxies[0].GetBaseConfig()
|
||||
require.Equal("proxy-raw", p.Name)
|
||||
require.Empty(p.LocalIP)
|
||||
|
||||
v := visitors[0].GetBaseConfig()
|
||||
require.Equal("visitor-raw", v.Name)
|
||||
require.Equal("server-raw", v.ServerName)
|
||||
require.Empty(v.BindAddr)
|
||||
|
||||
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
||||
require.Equal("fallback-raw", xtcp.FallbackTo)
|
||||
require.Empty(xtcp.Protocol)
|
||||
}
|
||||
|
||||
func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
enabled := true
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy-raw"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
proxyCfg.Enabled = &enabled
|
||||
|
||||
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
||||
require.Len(proxies, 1)
|
||||
|
||||
p := proxies[0].GetBaseConfig()
|
||||
require.Equal("proxy-raw", p.Name)
|
||||
require.Equal("127.0.0.1", p.LocalIP)
|
||||
}
|
||||
|
||||
func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
enabled := true
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor-raw"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server-raw"
|
||||
visitorCfg.FallbackTo = "fallback-raw"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
visitorCfg.Enabled = &enabled
|
||||
|
||||
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
||||
require.Len(visitors, 1)
|
||||
|
||||
v := visitors[0].GetBaseConfig()
|
||||
require.Equal("visitor-raw", v.Name)
|
||||
require.Equal("server-raw", v.ServerName)
|
||||
require.Equal("127.0.0.1", v.BindAddr)
|
||||
|
||||
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
||||
require.Equal("fallback-raw", xtcp.FallbackTo)
|
||||
require.Equal("quic", xtcp.Protocol)
|
||||
}
|
||||
|
||||
func TestCompleteProxyConfigurers_Idempotent(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
||||
firstProxyJSON, err := json.Marshal(proxies[0])
|
||||
require.NoError(err)
|
||||
|
||||
proxies = CompleteProxyConfigurers(proxies)
|
||||
secondProxyJSON, err := json.Marshal(proxies[0])
|
||||
require.NoError(err)
|
||||
|
||||
require.Equal(string(firstProxyJSON), string(secondProxyJSON))
|
||||
}
|
||||
|
||||
func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
||||
firstVisitorJSON, err := json.Marshal(visitors[0])
|
||||
require.NoError(err)
|
||||
|
||||
visitors = CompleteVisitorConfigurers(visitors)
|
||||
secondVisitorJSON, err := json.Marshal(visitors[0])
|
||||
require.NoError(err)
|
||||
|
||||
require.Equal(string(firstVisitorJSON), string(secondVisitorJSON))
|
||||
}
|
||||
|
||||
func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
enabled := true
|
||||
disabled := false
|
||||
|
||||
proxyKeep := &v1.TCPProxyConfig{}
|
||||
proxyKeep.Name = "keep"
|
||||
proxyKeep.Type = "tcp"
|
||||
proxyKeep.LocalPort = 10080
|
||||
proxyKeep.Enabled = &enabled
|
||||
|
||||
proxyDropByStart := &v1.TCPProxyConfig{}
|
||||
proxyDropByStart.Name = "drop-by-start"
|
||||
proxyDropByStart.Type = "tcp"
|
||||
proxyDropByStart.LocalPort = 10081
|
||||
proxyDropByStart.Enabled = &enabled
|
||||
|
||||
proxyDropByEnabled := &v1.TCPProxyConfig{}
|
||||
proxyDropByEnabled.Name = "drop-by-enabled"
|
||||
proxyDropByEnabled.Type = "tcp"
|
||||
proxyDropByEnabled.LocalPort = 10082
|
||||
proxyDropByEnabled.Enabled = &disabled
|
||||
|
||||
common := &v1.ClientCommonConfig{
|
||||
Start: []string{"keep"},
|
||||
}
|
||||
|
||||
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{
|
||||
proxyKeep,
|
||||
proxyDropByStart,
|
||||
proxyDropByEnabled,
|
||||
}, nil)
|
||||
require.Len(visitors, 0)
|
||||
require.Len(proxies, 1)
|
||||
require.Equal("keep", proxies[0].GetBaseConfig().Name)
|
||||
}
|
||||
|
||||
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
||||
func TestYAMLEdgeCases(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
125
pkg/config/source/aggregator.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type sourceEntry struct {
|
||||
source Source
|
||||
}
|
||||
|
||||
type Aggregator struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
configSource *ConfigSource
|
||||
storeSource *StoreSource
|
||||
}
|
||||
|
||||
func NewAggregator(configSource *ConfigSource) *Aggregator {
|
||||
if configSource == nil {
|
||||
configSource = NewConfigSource()
|
||||
}
|
||||
return &Aggregator{
|
||||
configSource: configSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Aggregator) SetStoreSource(storeSource *StoreSource) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.storeSource = storeSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) ConfigSource() *ConfigSource {
|
||||
return a.configSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) StoreSource() *StoreSource {
|
||||
return a.storeSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) getSourcesLocked() []sourceEntry {
|
||||
sources := make([]sourceEntry, 0, 2)
|
||||
if a.configSource != nil {
|
||||
sources = append(sources, sourceEntry{
|
||||
source: a.configSource,
|
||||
})
|
||||
}
|
||||
if a.storeSource != nil {
|
||||
sources = append(sources, sourceEntry{
|
||||
source: a.storeSource,
|
||||
})
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
a.mu.RLock()
|
||||
entries := a.getSourcesLocked()
|
||||
a.mu.RUnlock()
|
||||
|
||||
if len(entries) == 0 {
|
||||
return nil, nil, errors.New("no sources configured")
|
||||
}
|
||||
|
||||
proxyMap := make(map[string]v1.ProxyConfigurer)
|
||||
visitorMap := make(map[string]v1.VisitorConfigurer)
|
||||
|
||||
for _, entry := range entries {
|
||||
proxies, visitors, err := entry.source.Load()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load source: %w", err)
|
||||
}
|
||||
for _, p := range proxies {
|
||||
proxyMap[p.GetBaseConfig().Name] = p
|
||||
}
|
||||
for _, v := range visitors {
|
||||
visitorMap[v.GetBaseConfig().Name] = v
|
||||
}
|
||||
}
|
||||
proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap)
|
||||
return proxies, visitors, nil
|
||||
}
|
||||
|
||||
func (a *Aggregator) mapsToSortedSlices(
|
||||
proxyMap map[string]v1.ProxyConfigurer,
|
||||
visitorMap map[string]v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||
proxies := 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 := 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
|
||||
}
|
||||
217
pkg/config/source/aggregator_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// mockProxy creates a TCP proxy config for testing
|
||||
func mockProxy(name string) v1.ProxyConfigurer {
|
||||
cfg := &v1.TCPProxyConfig{}
|
||||
cfg.Name = name
|
||||
cfg.Type = "tcp"
|
||||
cfg.LocalPort = 8080
|
||||
cfg.RemotePort = 9090
|
||||
return cfg
|
||||
}
|
||||
|
||||
// mockVisitor creates a STCP visitor config for testing
|
||||
func mockVisitor(name string) v1.VisitorConfigurer {
|
||||
cfg := &v1.STCPVisitorConfig{}
|
||||
cfg.Name = name
|
||||
cfg.Type = "stcp"
|
||||
cfg.ServerName = "test-server"
|
||||
return cfg
|
||||
}
|
||||
|
||||
func newTestStoreSource(t *testing.T) *StoreSource {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(t, err)
|
||||
return storeSource
|
||||
}
|
||||
|
||||
func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator {
|
||||
t.Helper()
|
||||
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
if storeSource != nil {
|
||||
agg.SetStoreSource(storeSource)
|
||||
}
|
||||
return agg
|
||||
}
|
||||
|
||||
func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := NewAggregator(nil)
|
||||
require.NotNil(agg)
|
||||
require.NotNil(agg.ConfigSource())
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestNewAggregator_WithoutStore(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
require.NotNil(agg)
|
||||
require.Same(configSource, agg.ConfigSource())
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestNewAggregator_WithStore(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
agg.SetStoreSource(storeSource)
|
||||
|
||||
require.Same(configSource, agg.ConfigSource())
|
||||
require.Same(storeSource, agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestAggregator_SetStoreSource_Overwrite(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := newTestAggregator(t, nil)
|
||||
first := newTestStoreSource(t)
|
||||
second := newTestStoreSource(t)
|
||||
|
||||
agg.SetStoreSource(first)
|
||||
require.Same(first, agg.StoreSource())
|
||||
|
||||
agg.SetStoreSource(second)
|
||||
require.Same(second, agg.StoreSource())
|
||||
|
||||
agg.SetStoreSource(nil)
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestAggregator_MergeBySourceOrder(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
|
||||
configSource := agg.ConfigSource()
|
||||
|
||||
configShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||
configShared.LocalPort = 1111
|
||||
configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig)
|
||||
configOnly.LocalPort = 1112
|
||||
|
||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
storeShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||
storeShared.LocalPort = 2222
|
||||
storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig)
|
||||
storeOnly.LocalPort = 2223
|
||||
err = storeSource.AddProxy(storeShared)
|
||||
require.NoError(err)
|
||||
err = storeSource.AddProxy(storeOnly)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(visitors, 0)
|
||||
require.Len(proxies, 3)
|
||||
|
||||
var sharedProxy *v1.TCPProxyConfig
|
||||
for _, p := range proxies {
|
||||
if p.GetBaseConfig().Name == "shared" {
|
||||
sharedProxy = p.(*v1.TCPProxyConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(sharedProxy)
|
||||
require.Equal(2222, sharedProxy.LocalPort)
|
||||
}
|
||||
|
||||
func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
configSource := agg.ConfigSource()
|
||||
|
||||
lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||
lowProxy.LocalPort = 1111
|
||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
disabled := false
|
||||
highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||
highProxy.LocalPort = 2222
|
||||
highProxy.Enabled = &disabled
|
||||
err = storeSource.AddProxy(highProxy)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 0)
|
||||
|
||||
proxy := proxies[0].(*v1.TCPProxyConfig)
|
||||
require.Equal("shared-proxy", proxy.Name)
|
||||
require.Equal(1111, proxy.LocalPort)
|
||||
}
|
||||
|
||||
func TestAggregator_VisitorMerge(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
|
||||
err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")})
|
||||
require.NoError(err)
|
||||
err = storeSource.AddVisitor(mockVisitor("visitor2"))
|
||||
require.NoError(err)
|
||||
|
||||
_, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(visitors, 2)
|
||||
}
|
||||
|
||||
func TestAggregator_Load_ReturnsSharedReferences(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := newTestAggregator(t, nil)
|
||||
err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, _, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Equal("ssh", proxies[0].GetBaseConfig().Name)
|
||||
|
||||
proxies[0].GetBaseConfig().Name = "alice.ssh"
|
||||
|
||||
proxies2, _, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies2, 1)
|
||||
require.Equal("alice.ssh", proxies2[0].GetBaseConfig().Name)
|
||||
}
|
||||
65
pkg/config/source/base_source.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// baseSource provides shared state and behavior for Source implementations.
|
||||
// It manages proxy/visitor storage.
|
||||
// Concrete types (ConfigSource, StoreSource) embed this struct.
|
||||
type baseSource struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
proxies map[string]v1.ProxyConfigurer
|
||||
visitors map[string]v1.VisitorConfigurer
|
||||
}
|
||||
|
||||
func newBaseSource() baseSource {
|
||||
return baseSource{
|
||||
proxies: make(map[string]v1.ProxyConfigurer),
|
||||
visitors: make(map[string]v1.VisitorConfigurer),
|
||||
}
|
||||
}
|
||||
|
||||
// Load returns all enabled proxy and visitor configurations.
|
||||
// Configurations with Enabled explicitly set to false are filtered out.
|
||||
func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||
for _, p := range s.proxies {
|
||||
// Filter out disabled proxies (nil or true means enabled)
|
||||
if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, p)
|
||||
}
|
||||
|
||||
visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||
for _, v := range s.visitors {
|
||||
// Filter out disabled visitors (nil or true means enabled)
|
||||
if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||
continue
|
||||
}
|
||||
visitors = append(visitors, v)
|
||||
}
|
||||
|
||||
return proxies, visitors, nil
|
||||
}
|
||||
65
pkg/config/source/config_source.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// ConfigSource implements Source for in-memory configuration.
|
||||
// All operations are thread-safe.
|
||||
type ConfigSource struct {
|
||||
baseSource
|
||||
}
|
||||
|
||||
func NewConfigSource() *ConfigSource {
|
||||
return &ConfigSource{
|
||||
baseSource: newBaseSource(),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceAll replaces all proxy and visitor configurations atomically.
|
||||
func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies))
|
||||
for _, p := range proxies {
|
||||
if p == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
name := p.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
nextProxies[name] = p
|
||||
}
|
||||
nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors))
|
||||
for _, v := range visitors {
|
||||
if v == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
name := v.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
nextVisitors[name] = v
|
||||
}
|
||||
s.proxies = nextProxies
|
||||
s.visitors = nextVisitors
|
||||
return nil
|
||||
}
|
||||
173
pkg/config/source/config_source_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestNewConfigSource(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
require.NotNil(src)
|
||||
}
|
||||
|
||||
func TestConfigSource_ReplaceAll(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2)
|
||||
require.Len(visitors, 1)
|
||||
|
||||
// ReplaceAll again should replace everything
|
||||
err = src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy3")},
|
||||
nil,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err = src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 0)
|
||||
require.Equal("proxy3", proxies[0].GetBaseConfig().Name)
|
||||
|
||||
// ReplaceAll with nil proxy should fail
|
||||
err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil)
|
||||
require.Error(err)
|
||||
|
||||
// ReplaceAll with empty name proxy should fail
|
||||
err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil)
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestConfigSource_Load(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2)
|
||||
require.Len(visitors, 1)
|
||||
}
|
||||
|
||||
// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out
|
||||
// proxies and visitors with Enabled explicitly set to false.
|
||||
func TestConfigSource_Load_FiltersDisabled(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
disabled := false
|
||||
enabled := true
|
||||
|
||||
// Create enabled proxy (nil Enabled = enabled by default)
|
||||
enabledProxy := mockProxy("enabled-proxy")
|
||||
|
||||
// Create disabled proxy
|
||||
disabledProxy := &v1.TCPProxyConfig{}
|
||||
disabledProxy.Name = "disabled-proxy"
|
||||
disabledProxy.Type = "tcp"
|
||||
disabledProxy.Enabled = &disabled
|
||||
|
||||
// Create explicitly enabled proxy
|
||||
explicitEnabledProxy := &v1.TCPProxyConfig{}
|
||||
explicitEnabledProxy.Name = "explicit-enabled-proxy"
|
||||
explicitEnabledProxy.Type = "tcp"
|
||||
explicitEnabledProxy.Enabled = &enabled
|
||||
|
||||
// Create enabled visitor (nil Enabled = enabled by default)
|
||||
enabledVisitor := mockVisitor("enabled-visitor")
|
||||
|
||||
// Create disabled visitor
|
||||
disabledVisitor := &v1.STCPVisitorConfig{}
|
||||
disabledVisitor.Name = "disabled-visitor"
|
||||
disabledVisitor.Type = "stcp"
|
||||
disabledVisitor.Enabled = &disabled
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy},
|
||||
[]v1.VisitorConfigurer{enabledVisitor, disabledVisitor},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// Load should filter out disabled configs
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2, "Should have 2 enabled proxies")
|
||||
require.Len(visitors, 1, "Should have 1 enabled visitor")
|
||||
|
||||
// Verify the correct proxies are returned
|
||||
proxyNames := make([]string, 0, len(proxies))
|
||||
for _, p := range proxies {
|
||||
proxyNames = append(proxyNames, p.GetBaseConfig().Name)
|
||||
}
|
||||
require.Contains(proxyNames, "enabled-proxy")
|
||||
require.Contains(proxyNames, "explicit-enabled-proxy")
|
||||
require.NotContains(proxyNames, "disabled-proxy")
|
||||
|
||||
// Verify the correct visitor is returned
|
||||
require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name)
|
||||
}
|
||||
|
||||
func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 1)
|
||||
require.Empty(proxies[0].GetBaseConfig().LocalIP)
|
||||
require.Empty(visitors[0].GetBaseConfig().BindAddr)
|
||||
require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
37
pkg/config/source/source.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// Source is the interface for configuration sources.
|
||||
// A Source provides proxy and visitor configurations from various backends.
|
||||
// Aggregator currently uses the built-in config source as base and an optional
|
||||
// store source as higher-priority overlay.
|
||||
type Source interface {
|
||||
// Load loads the proxy and visitor configurations from this source.
|
||||
// Returns the loaded configurations and any error encountered.
|
||||
// A disabled entry in one source is source-local filtering, not a cross-source
|
||||
// tombstone for entries from lower-priority sources.
|
||||
//
|
||||
// Error handling contract with Aggregator:
|
||||
// - When err is nil, returned slices are consumed.
|
||||
// - When err is non-nil, Aggregator aborts the merge and returns the error.
|
||||
// - To publish best-effort or partial results, return those results with
|
||||
// err set to nil.
|
||||
Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error)
|
||||
}
|
||||
351
pkg/config/source/store.go
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type StoreSourceConfig struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type storeData struct {
|
||||
Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"`
|
||||
Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"`
|
||||
}
|
||||
|
||||
type StoreSource struct {
|
||||
baseSource
|
||||
config StoreSourceConfig
|
||||
}
|
||||
|
||||
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
|
||||
if cfg.Path == "" {
|
||||
return nil, fmt.Errorf("path is required")
|
||||
}
|
||||
|
||||
s := &StoreSource{
|
||||
baseSource: newBaseSource(),
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
if err := s.loadFromFile(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to load existing data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) loadFromFile() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.loadFromFileUnlocked()
|
||||
}
|
||||
|
||||
func (s *StoreSource) loadFromFileUnlocked() error {
|
||||
data, err := os.ReadFile(s.config.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stored storeData
|
||||
if err := json.Unmarshal(data, &stored); err != nil {
|
||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||
|
||||
for _, tp := range stored.Proxies {
|
||||
if tp.ProxyConfigurer != nil {
|
||||
proxyCfg := tp.ProxyConfigurer
|
||||
name := proxyCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
s.proxies[name] = proxyCfg
|
||||
}
|
||||
}
|
||||
|
||||
for _, tv := range stored.Visitors {
|
||||
if tv.VisitorConfigurer != nil {
|
||||
visitorCfg := tv.VisitorConfigurer
|
||||
name := visitorCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
s.visitors[name] = visitorCfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) saveToFileUnlocked() error {
|
||||
stored := storeData{
|
||||
Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)),
|
||||
Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)),
|
||||
}
|
||||
|
||||
for _, p := range s.proxies {
|
||||
stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p})
|
||||
}
|
||||
for _, v := range s.visitors {
|
||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(stored, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(s.config.Path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := s.config.Path + ".tmp"
|
||||
|
||||
f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to sync temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, s.config.Path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
|
||||
name := proxy.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.proxies[name]; exists {
|
||||
return fmt.Errorf("proxy %q already exists", name)
|
||||
}
|
||||
|
||||
s.proxies[name] = proxy
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
delete(s.proxies, name)
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
|
||||
name := proxy.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldProxy, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("proxy %q does not exist", name)
|
||||
}
|
||||
|
||||
s.proxies[name] = proxy
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.proxies[name] = oldProxy
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) RemoveProxy(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldProxy, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("proxy %q does not exist", name)
|
||||
}
|
||||
|
||||
delete(s.proxies, name)
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.proxies[name] = oldProxy
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
p, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
|
||||
name := visitor.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.visitors[name]; exists {
|
||||
return fmt.Errorf("visitor %q already exists", name)
|
||||
}
|
||||
|
||||
s.visitors[name] = visitor
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
delete(s.visitors, name)
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
|
||||
name := visitor.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldVisitor, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("visitor %q does not exist", name)
|
||||
}
|
||||
|
||||
s.visitors[name] = visitor
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.visitors[name] = oldVisitor
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) RemoveVisitor(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldVisitor, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("visitor %q does not exist", name)
|
||||
}
|
||||
|
||||
delete(s.visitors, name)
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.visitors[name] = oldVisitor
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
v, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||
for _, p := range s.proxies {
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||
for _, v := range s.visitors {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
99
pkg/config/source/store_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
err = storeSource.AddProxy(proxyCfg)
|
||||
require.NoError(err)
|
||||
err = storeSource.AddVisitor(visitorCfg)
|
||||
require.NoError(err)
|
||||
|
||||
gotProxy := storeSource.GetProxy("proxy1")
|
||||
require.NotNil(gotProxy)
|
||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||
|
||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||
require.NotNil(gotVisitor)
|
||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
|
||||
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
stored := storeData{
|
||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||
}
|
||||
data, err := json.Marshal(stored)
|
||||
require.NoError(err)
|
||||
err = os.WriteFile(path, data, 0o600)
|
||||
require.NoError(err)
|
||||
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
gotProxy := storeSource.GetProxy("proxy1")
|
||||
require.NotNil(gotProxy)
|
||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||
|
||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||
require.NotNil(gotVisitor)
|
||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/samber/lo"
|
||||
@@ -39,6 +37,8 @@ type ClientCommonConfig struct {
|
||||
// clients. If this value is not "", proxy names will automatically be
|
||||
// changed to "{user}.{proxy_name}".
|
||||
User string `json:"user,omitempty"`
|
||||
// ClientID uniquely identifies this frpc instance.
|
||||
ClientID string `json:"clientID,omitempty"`
|
||||
|
||||
// ServerAddr specifies the address of the server to connect to. By
|
||||
// default, this value is "0.0.0.0".
|
||||
@@ -77,6 +77,9 @@ type ClientCommonConfig struct {
|
||||
|
||||
// Include other config files for proxies.
|
||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
||||
|
||||
// Store config enables the built-in store source (not configurable via sources list).
|
||||
Store StoreConfig `json:"store,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ClientCommonConfig) Complete() error {
|
||||
@@ -198,17 +201,6 @@ type AuthClientConfig struct {
|
||||
|
||||
func (c *AuthClientConfig) Complete() error {
|
||||
c.Method = util.EmptyOr(c.Method, "token")
|
||||
|
||||
// Resolve tokenSource during configuration loading
|
||||
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||
token, err := c.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
// Move the resolved token to the Token field and clear TokenSource
|
||||
c.Token = token
|
||||
c.TokenSource = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -228,6 +220,21 @@ type AuthOIDCClientConfig struct {
|
||||
// AdditionalEndpointParams specifies additional parameters to be sent
|
||||
// this field will be transfer to map[string][]string in OIDC token generator.
|
||||
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
|
||||
|
||||
// TrustedCaFile specifies the path to a custom CA certificate file
|
||||
// for verifying the OIDC token endpoint's TLS certificate.
|
||||
TrustedCaFile string `json:"trustedCaFile,omitempty"`
|
||||
// InsecureSkipVerify disables TLS certificate verification for the
|
||||
// OIDC token endpoint. Only use this for debugging, not recommended for production.
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||
// ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint.
|
||||
// Supports http, https, socks5, and socks5h proxy protocols.
|
||||
// If empty, no proxy is used for OIDC connections.
|
||||
ProxyURL string `json:"proxyURL,omitempty"`
|
||||
|
||||
// TokenSource specifies a custom dynamic source for the authorization token.
|
||||
// This is mutually exclusive with every other field of this structure.
|
||||
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||
}
|
||||
|
||||
type VirtualNetConfig struct {
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
@@ -38,68 +36,9 @@ func TestClientConfigComplete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthClientConfig_Complete(t *testing.T) {
|
||||
// Create a temporary file for testing
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test_token")
|
||||
testContent := "client-token-value"
|
||||
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AuthClientConfig
|
||||
expectToken string
|
||||
expectPanic bool
|
||||
}{
|
||||
{
|
||||
name: "tokenSource resolved to token",
|
||||
config: AuthClientConfig{
|
||||
Method: AuthMethodToken,
|
||||
TokenSource: &ValueSource{
|
||||
Type: "file",
|
||||
File: &FileSource{
|
||||
Path: testFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectToken: testContent,
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "direct token unchanged",
|
||||
config: AuthClientConfig{
|
||||
Method: AuthMethodToken,
|
||||
Token: "direct-token",
|
||||
},
|
||||
expectToken: "direct-token",
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "invalid tokenSource should panic",
|
||||
config: AuthClientConfig{
|
||||
Method: AuthMethodToken,
|
||||
TokenSource: &ValueSource{
|
||||
Type: "file",
|
||||
File: &FileSource{
|
||||
Path: "/non/existent/file",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPanic: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.expectPanic {
|
||||
err := tt.config.Complete()
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
err := tt.config.Complete()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||
}
|
||||
})
|
||||
}
|
||||
require := require.New(t)
|
||||
cfg := &AuthClientConfig{}
|
||||
err := cfg.Complete()
|
||||
require.NoError(err)
|
||||
require.EqualValues("token", cfg.Method)
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() {
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
// CertPath specifies the path of the cert file that client will load.
|
||||
// CertFile specifies the path of the cert file that client will load.
|
||||
CertFile string `json:"certFile,omitempty"`
|
||||
// KeyPath specifies the path of the secret key file that client will load.
|
||||
// KeyFile specifies the path of the secret key file that client will load.
|
||||
KeyFile string `json:"keyFile,omitempty"`
|
||||
// TrustedCaFile specifies the path of the trusted ca file that will load.
|
||||
TrustedCaFile string `json:"trustedCaFile,omitempty"`
|
||||
@@ -96,6 +96,14 @@ type TLSConfig struct {
|
||||
ServerName string `json:"serverName,omitempty"`
|
||||
}
|
||||
|
||||
// NatTraversalConfig defines configuration options for NAT traversal
|
||||
type NatTraversalConfig struct {
|
||||
// DisableAssistedAddrs disables the use of local network interfaces
|
||||
// for assisted connections during NAT traversal. When enabled,
|
||||
// only STUN-discovered public addresses will be used.
|
||||
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
// This is destination where frp should write the logs.
|
||||
// If "console" is used, logs will be printed to stdout, otherwise,
|
||||
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
@@ -108,8 +106,11 @@ type DomainConfig struct {
|
||||
}
|
||||
|
||||
type ProxyBaseConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
||||
// This allows individual control over each proxy, complementing the global "start" field.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Transport ProxyTransport `json:"transport,omitempty"`
|
||||
// metadata info for each proxy
|
||||
@@ -123,8 +124,7 @@ func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ProxyBaseConfig) Complete(namePrefix string) {
|
||||
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
|
||||
func (c *ProxyBaseConfig) Complete() {
|
||||
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
|
||||
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
|
||||
|
||||
@@ -204,7 +204,7 @@ func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
type ProxyConfigurer interface {
|
||||
Complete(namePrefix string)
|
||||
Complete()
|
||||
GetBaseConfig() *ProxyBaseConfig
|
||||
// MarshalToMsg marshals this config into a msg.NewProxy message. This
|
||||
// function will be called on the frpc side.
|
||||
@@ -422,6 +422,9 @@ type XTCPProxyConfig struct {
|
||||
|
||||
Secretkey string `json:"secretKey,omitempty"`
|
||||
AllowUsers []string `json:"allowUsers,omitempty"`
|
||||
|
||||
// NatTraversal configuration for NAT traversal
|
||||
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||
}
|
||||
|
||||
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
@@ -138,17 +135,6 @@ type AuthServerConfig struct {
|
||||
|
||||
func (c *AuthServerConfig) Complete() error {
|
||||
c.Method = util.EmptyOr(c.Method, "token")
|
||||
|
||||
// Resolve tokenSource during configuration loading
|
||||
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||
token, err := c.TokenSource.Resolve(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||
}
|
||||
// Move the resolved token to the Token field and clear TokenSource
|
||||
c.Token = token
|
||||
c.TokenSource = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
@@ -35,68 +33,9 @@ func TestServerConfigComplete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthServerConfig_Complete(t *testing.T) {
|
||||
// Create a temporary file for testing
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test_token")
|
||||
testContent := "file-token-value"
|
||||
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AuthServerConfig
|
||||
expectToken string
|
||||
expectPanic bool
|
||||
}{
|
||||
{
|
||||
name: "tokenSource resolved to token",
|
||||
config: AuthServerConfig{
|
||||
Method: AuthMethodToken,
|
||||
TokenSource: &ValueSource{
|
||||
Type: "file",
|
||||
File: &FileSource{
|
||||
Path: testFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectToken: testContent,
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "direct token unchanged",
|
||||
config: AuthServerConfig{
|
||||
Method: AuthMethodToken,
|
||||
Token: "direct-token",
|
||||
},
|
||||
expectToken: "direct-token",
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "invalid tokenSource should panic",
|
||||
config: AuthServerConfig{
|
||||
Method: AuthMethodToken,
|
||||
TokenSource: &ValueSource{
|
||||
Type: "file",
|
||||
File: &FileSource{
|
||||
Path: "/non/existent/file",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPanic: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.expectPanic {
|
||||
err := tt.config.Complete()
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
err := tt.config.Complete()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||
}
|
||||
})
|
||||
}
|
||||
require := require.New(t)
|
||||
cfg := &AuthServerConfig{}
|
||||
err := cfg.Complete()
|
||||
require.NoError(err)
|
||||
require.EqualValues("token", cfg.Method)
|
||||
}
|
||||
|
||||
26
pkg/config/v1/store.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
// StoreConfig configures the built-in store source.
|
||||
type StoreConfig struct {
|
||||
// Path is the store file path.
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// IsEnabled returns true if the store is configured with a valid path.
|
||||
func (c *StoreConfig) IsEnabled() bool {
|
||||
return c.Path != ""
|
||||
}
|
||||
@@ -23,55 +23,109 @@ import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
var (
|
||||
warnings Warning
|
||||
errs error
|
||||
)
|
||||
// validate feature gates
|
||||
if c.VirtualNet.Address != "" {
|
||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||
}
|
||||
|
||||
validators := []func() (Warning, error){
|
||||
func() (Warning, error) { return validateFeatureGates(c) },
|
||||
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
||||
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
|
||||
for _, validator := range validators {
|
||||
w, err := validator()
|
||||
warnings = AppendError(warnings, w)
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
if c.VirtualNet.Address != "" {
|
||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
|
||||
var errs error
|
||||
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||
}
|
||||
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
|
||||
if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||
}
|
||||
|
||||
// Validate token/tokenSource mutual exclusivity
|
||||
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
|
||||
if c.Token != "" && c.TokenSource != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||
}
|
||||
|
||||
// Validate tokenSource if specified
|
||||
if c.Auth.TokenSource != nil {
|
||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||
if c.TokenSource != nil {
|
||||
if c.TokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := c.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateLogConfig(&c.Log); err != nil {
|
||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
||||
if c.TokenSource == nil {
|
||||
return nil
|
||||
}
|
||||
var errs error
|
||||
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
|
||||
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
|
||||
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
|
||||
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
||||
}
|
||||
if c.TokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := c.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
|
||||
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
|
||||
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
||||
var (
|
||||
warnings Warning
|
||||
errs error
|
||||
)
|
||||
|
||||
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
|
||||
if c.HeartbeatTimeout < c.HeartbeatInterval {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
||||
}
|
||||
}
|
||||
|
||||
if !lo.FromPtr(c.Transport.TLS.Enable) {
|
||||
if !lo.FromPtr(c.TLS.Enable) {
|
||||
checkTLSConfig := func(name string, value string) Warning {
|
||||
if value != "" {
|
||||
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
||||
@@ -79,16 +133,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
|
||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
|
||||
}
|
||||
|
||||
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
|
||||
if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||
}
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
for _, f := range c.IncludeConfigFiles {
|
||||
func validateIncludeFiles(files []string) (Warning, error) {
|
||||
var errs error
|
||||
for _, f := range files {
|
||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||
if err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
||||
@@ -98,13 +156,19 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
||||
}
|
||||
}
|
||||
return warnings, errs
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
|
||||
func ValidateAllClientConfig(
|
||||
c *v1.ClientCommonConfig,
|
||||
proxyCfgs []v1.ProxyConfigurer,
|
||||
visitorCfgs []v1.VisitorConfigurer,
|
||||
unsafeFeatures *security.UnsafeFeatures,
|
||||
) (Warning, error) {
|
||||
validator := NewConfigValidator(unsafeFeatures)
|
||||
var warnings Warning
|
||||
if c != nil {
|
||||
warning, err := ValidateClientCommonConfig(c)
|
||||
warning, err := validator.ValidateClientCommonConfig(c)
|
||||
warnings = AppendError(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
|
||||
@@ -81,7 +81,7 @@ func validateDomainConfigForClient(c *v1.DomainConfig) error {
|
||||
func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
|
||||
for _, domain := range c.CustomDomains {
|
||||
if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
|
||||
if strings.Contains(domain, s.SubDomainHost) {
|
||||
if strings.HasSuffix(domain, "."+s.SubDomainHost) {
|
||||
return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||
var (
|
||||
warnings Warning
|
||||
errs error
|
||||
@@ -42,6 +43,11 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||
|
||||
// Validate tokenSource if specified
|
||||
if c.Auth.TokenSource != nil {
|
||||
if c.Auth.TokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
|
||||
28
pkg/config/v1/validation/validator.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
)
|
||||
|
||||
// ConfigValidator holds the context dependencies for configuration validation.
|
||||
type ConfigValidator struct {
|
||||
unsafeFeatures *security.UnsafeFeatures
|
||||
}
|
||||
|
||||
// NewConfigValidator creates a new ConfigValidator instance.
|
||||
func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator {
|
||||
return &ConfigValidator{
|
||||
unsafeFeatures: unsafeFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateUnsafeFeature checks if a specific unsafe feature is enabled.
|
||||
func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error {
|
||||
if !v.unsafeFeatures.IsEnabled(feature) {
|
||||
return fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||
"To enable it, ensure it is allowed in the configuration or command line flags", feature)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
type ValueSource struct {
|
||||
Type string `json:"type"`
|
||||
File *FileSource `json:"file,omitempty"`
|
||||
Exec *ExecSource `json:"exec,omitempty"`
|
||||
}
|
||||
|
||||
// FileSource specifies how to load a value from a file.
|
||||
@@ -34,6 +36,18 @@ type FileSource struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// ExecSource specifies how to get a value from another program launched as subprocess.
|
||||
type ExecSource struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
Env []ExecEnvVar `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
type ExecEnvVar struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Validate validates the ValueSource configuration.
|
||||
func (v *ValueSource) Validate() error {
|
||||
if v == nil {
|
||||
@@ -46,8 +60,13 @@ func (v *ValueSource) Validate() error {
|
||||
return errors.New("file configuration is required when type is 'file'")
|
||||
}
|
||||
return v.File.Validate()
|
||||
case "exec":
|
||||
if v.Exec == nil {
|
||||
return errors.New("exec configuration is required when type is 'exec'")
|
||||
}
|
||||
return v.Exec.Validate()
|
||||
default:
|
||||
return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
|
||||
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +79,8 @@ func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
|
||||
switch v.Type {
|
||||
case "file":
|
||||
return v.File.Resolve(ctx)
|
||||
case "exec":
|
||||
return v.Exec.Resolve(ctx)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
||||
}
|
||||
@@ -91,3 +112,47 @@ func (f *FileSource) Resolve(_ context.Context) (string, error) {
|
||||
// Trim whitespace, which is important for file-based tokens
|
||||
return strings.TrimSpace(string(content)), nil
|
||||
}
|
||||
|
||||
// Validate validates the ExecSource configuration.
|
||||
func (e *ExecSource) Validate() error {
|
||||
if e == nil {
|
||||
return errors.New("execSource cannot be nil")
|
||||
}
|
||||
|
||||
if e.Command == "" {
|
||||
return errors.New("exec command cannot be empty")
|
||||
}
|
||||
|
||||
for _, env := range e.Env {
|
||||
if env.Name == "" {
|
||||
return errors.New("exec env name cannot be empty")
|
||||
}
|
||||
if strings.Contains(env.Name, "=") {
|
||||
return errors.New("exec env name cannot contain '='")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve reads and returns the content captured from stdout of launched subprocess.
|
||||
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
|
||||
if err := e.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
|
||||
if len(e.Env) != 0 {
|
||||
cmd.Env = os.Environ()
|
||||
for _, env := range e.Env {
|
||||
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
|
||||
}
|
||||
|
||||
// Trim whitespace, which is important for exec-based tokens
|
||||
return strings.TrimSpace(string(content)), nil
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
)
|
||||
|
||||
@@ -32,8 +30,11 @@ type VisitorTransport struct {
|
||||
}
|
||||
|
||||
type VisitorBaseConfig struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
||||
// This allows individual control over each visitor, complementing the global "start" field.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Transport VisitorTransport `json:"transport,omitempty"`
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
// if the server user is not set, it defaults to the current user
|
||||
@@ -53,26 +54,14 @@ func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) {
|
||||
func (c *VisitorBaseConfig) Complete() {
|
||||
if c.BindAddr == "" {
|
||||
c.BindAddr = "127.0.0.1"
|
||||
}
|
||||
|
||||
namePrefix := ""
|
||||
if g.User != "" {
|
||||
namePrefix = g.User + "."
|
||||
}
|
||||
c.Name = namePrefix + c.Name
|
||||
|
||||
if c.ServerUser != "" {
|
||||
c.ServerName = c.ServerUser + "." + c.ServerName
|
||||
} else {
|
||||
c.ServerName = namePrefix + c.ServerName
|
||||
}
|
||||
}
|
||||
|
||||
type VisitorConfigurer interface {
|
||||
Complete(*ClientCommonConfig)
|
||||
Complete()
|
||||
GetBaseConfig() *VisitorBaseConfig
|
||||
}
|
||||
|
||||
@@ -160,17 +149,16 @@ type XTCPVisitorConfig struct {
|
||||
MinRetryInterval int `json:"minRetryInterval,omitempty"`
|
||||
FallbackTo string `json:"fallbackTo,omitempty"`
|
||||
FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"`
|
||||
|
||||
// NatTraversal configuration for NAT traversal
|
||||
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||
}
|
||||
|
||||
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
|
||||
c.VisitorBaseConfig.Complete(g)
|
||||
func (c *XTCPVisitorConfig) Complete() {
|
||||
c.VisitorBaseConfig.Complete()
|
||||
|
||||
c.Protocol = util.EmptyOr(c.Protocol, "quic")
|
||||
c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
|
||||
c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
|
||||
c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
|
||||
|
||||
if c.FallbackTo != "" {
|
||||
c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
||||
for _, v := range m.ms {
|
||||
v.NewProxy(name, proxyType)
|
||||
v.NewProxy(name, proxyType, user, clientID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (m *serverMetrics) CloseClient() {
|
||||
m.info.ClientCounts.Dec(1)
|
||||
}
|
||||
|
||||
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
||||
@@ -119,6 +119,8 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||
}
|
||||
m.info.ProxyStatistics[name] = proxyStats
|
||||
}
|
||||
proxyStats.User = user
|
||||
proxyStats.ClientID = clientID
|
||||
proxyStats.LastStartTime = time.Now()
|
||||
}
|
||||
|
||||
@@ -214,6 +216,8 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*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()),
|
||||
@@ -245,6 +249,8 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
||||
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()),
|
||||
@@ -260,6 +266,31 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
||||
return
|
||||
}
|
||||
|
||||
func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||
if ok {
|
||||
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
|
||||
}
|
||||
|
||||
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -35,6 +35,8 @@ type ServerStats struct {
|
||||
type ProxyStats struct {
|
||||
Name string
|
||||
Type string
|
||||
User string
|
||||
ClientID string
|
||||
TodayTrafficIn int64
|
||||
TodayTrafficOut int64
|
||||
LastStartTime string
|
||||
@@ -51,6 +53,8 @@ type ProxyTrafficInfo struct {
|
||||
type ProxyStatistics struct {
|
||||
Name string
|
||||
ProxyType string
|
||||
User string
|
||||
ClientID string
|
||||
TrafficIn metric.DateCounter
|
||||
TrafficOut metric.DateCounter
|
||||
CurConns metric.Counter
|
||||
@@ -78,6 +82,7 @@ type Collector interface {
|
||||
GetServer() *ServerStats
|
||||
GetProxiesByType(proxyType string) []*ProxyStats
|
||||
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
||||
GetProxyByName(proxyName string) *ProxyStats
|
||||
GetProxyTraffic(name string) *ProxyTrafficInfo
|
||||
ClearOfflineProxies() (int, int)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ const (
|
||||
var ServerMetrics metrics.ServerMetrics = newServerMetrics()
|
||||
|
||||
type serverMetrics struct {
|
||||
clientCount prometheus.Gauge
|
||||
proxyCount *prometheus.GaugeVec
|
||||
connectionCount *prometheus.GaugeVec
|
||||
trafficIn *prometheus.CounterVec
|
||||
trafficOut *prometheus.CounterVec
|
||||
clientCount prometheus.Gauge
|
||||
proxyCount *prometheus.GaugeVec
|
||||
proxyCountDetailed *prometheus.GaugeVec
|
||||
connectionCount *prometheus.GaugeVec
|
||||
trafficIn *prometheus.CounterVec
|
||||
trafficOut *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func (m *serverMetrics) NewClient() {
|
||||
@@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() {
|
||||
m.clientCount.Dec()
|
||||
}
|
||||
|
||||
func (m *serverMetrics) NewProxy(_ string, proxyType string) {
|
||||
func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) {
|
||||
m.proxyCount.WithLabelValues(proxyType).Inc()
|
||||
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
||||
}
|
||||
|
||||
func (m *serverMetrics) CloseProxy(_ string, proxyType string) {
|
||||
func (m *serverMetrics) CloseProxy(name string, proxyType string) {
|
||||
m.proxyCount.WithLabelValues(proxyType).Dec()
|
||||
m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec()
|
||||
}
|
||||
|
||||
func (m *serverMetrics) OpenConnection(name string, proxyType string) {
|
||||
@@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics {
|
||||
Name: "proxy_counts",
|
||||
Help: "The current proxy counts",
|
||||
}, []string{"type"}),
|
||||
proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: serverSubsystem,
|
||||
Name: "proxy_counts_detailed",
|
||||
Help: "The current number of proxies grouped by type and name",
|
||||
}, []string{"type", "name"}),
|
||||
connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: serverSubsystem,
|
||||
@@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics {
|
||||
}
|
||||
prometheus.MustRegister(m.clientCount)
|
||||
prometheus.MustRegister(m.proxyCount)
|
||||
prometheus.MustRegister(m.proxyCountDetailed)
|
||||
prometheus.MustRegister(m.connectionCount)
|
||||
prometheus.MustRegister(m.trafficIn)
|
||||
prometheus.MustRegister(m.trafficOut)
|
||||
|
||||
@@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) SendChannel() chan Message {
|
||||
return d.sendCh
|
||||
}
|
||||
|
||||
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
||||
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ type Login struct {
|
||||
PrivilegeKey string `json:"privilege_key,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
RunID string `json:"run_id,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
Metas map[string]string `json:"metas,omitempty"`
|
||||
|
||||
// Currently only effective for VirtualClient.
|
||||
|
||||
@@ -68,6 +68,13 @@ var (
|
||||
DetectRoleReceiver = "receiver"
|
||||
)
|
||||
|
||||
// PrepareOptions defines options for NAT traversal preparation
|
||||
type PrepareOptions struct {
|
||||
// DisableAssistedAddrs disables the use of local network interfaces
|
||||
// for assisted connections during NAT traversal
|
||||
DisableAssistedAddrs bool
|
||||
}
|
||||
|
||||
type PrepareResult struct {
|
||||
Addrs []string
|
||||
AssistedAddrs []string
|
||||
@@ -108,7 +115,7 @@ func PreCheck(
|
||||
}
|
||||
|
||||
// Prepare is used to do some preparation work before penetration.
|
||||
func Prepare(stunServers []string) (*PrepareResult, error) {
|
||||
func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) {
|
||||
// discover for Nat type
|
||||
addrs, localAddr, err := Discover(stunServers, "")
|
||||
if err != nil {
|
||||
@@ -133,9 +140,13 @@ func Prepare(stunServers []string) (*PrepareResult, error) {
|
||||
return nil, fmt.Errorf("listen local udp addr error: %v", err)
|
||||
}
|
||||
|
||||
assistedAddrs := make([]string, 0, len(localIPs))
|
||||
for _, ip := range localIPs {
|
||||
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port)))
|
||||
// Apply NAT traversal options
|
||||
var assistedAddrs []string
|
||||
if !opts.DisableAssistedAddrs {
|
||||
assistedAddrs = make([]string, 0, len(localIPs))
|
||||
for _, ip := range localIPs {
|
||||
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port)))
|
||||
}
|
||||
}
|
||||
return &PrepareResult{
|
||||
Addrs: addrs,
|
||||
|
||||
@@ -23,11 +23,20 @@ import (
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
)
|
||||
|
||||
// PluginContext provides the necessary context and callbacks for visitor plugins.
|
||||
type PluginContext struct {
|
||||
Name string
|
||||
Ctx context.Context
|
||||
// Name is the unique identifier for this visitor, used for logging and routing.
|
||||
Name string
|
||||
|
||||
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
|
||||
Ctx context.Context
|
||||
|
||||
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
|
||||
VnetController *vnet.Controller
|
||||
HandleConn func(net.Conn)
|
||||
|
||||
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
|
||||
// Does not return error; failures are handled by closing the connection.
|
||||
SendConnToVisitor func(net.Conn)
|
||||
}
|
||||
|
||||
// Creators is used for create plugins to handle connections.
|
||||
|
||||
@@ -42,6 +42,8 @@ type VirtualNetPlugin struct {
|
||||
controllerConn net.Conn
|
||||
closeSignal chan struct{}
|
||||
|
||||
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -98,7 +100,6 @@ func (p *VirtualNetPlugin) Start() {
|
||||
|
||||
func (p *VirtualNetPlugin) run() {
|
||||
xl := xlog.FromContextSafe(p.ctx)
|
||||
reconnectDelay := 10 * time.Second
|
||||
|
||||
for {
|
||||
currentCloseSignal := make(chan struct{})
|
||||
@@ -121,7 +122,10 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.controllerConn = controllerConn
|
||||
p.mu.Unlock()
|
||||
|
||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
|
||||
// Wrap with CloseNotifyConn which supports both close notification and error recording
|
||||
var closeErr error
|
||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
|
||||
closeErr = err
|
||||
close(currentCloseSignal) // Signal the run loop on close.
|
||||
})
|
||||
|
||||
@@ -129,9 +133,9 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
||||
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
||||
|
||||
// Pass the CloseNotifyConn to HandleConn.
|
||||
// HandleConn is responsible for calling Close() on pluginNotifyConn.
|
||||
p.pluginCtx.HandleConn(pluginNotifyConn)
|
||||
// Pass the CloseNotifyConn to the visitor for handling.
|
||||
// The visitor can call CloseWithError to record the failure reason.
|
||||
p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
|
||||
|
||||
// Wait for context cancellation or connection close.
|
||||
select {
|
||||
@@ -140,8 +144,32 @@ func (p *VirtualNetPlugin) run() {
|
||||
p.cleanupControllerConn(xl)
|
||||
return
|
||||
case <-currentCloseSignal:
|
||||
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
|
||||
// HandleConn closed the plugin side. Close the controller side.
|
||||
// Determine reconnect delay based on error with exponential backoff
|
||||
var reconnectDelay time.Duration
|
||||
if closeErr != nil {
|
||||
p.consecutiveErrors++
|
||||
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
|
||||
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
|
||||
|
||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||
baseDelay := 60 * 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 {
|
||||
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
|
||||
p.pluginCtx.Name, p.consecutiveErrors)
|
||||
p.consecutiveErrors = 0
|
||||
} else {
|
||||
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
|
||||
}
|
||||
reconnectDelay = 10 * time.Second
|
||||
}
|
||||
|
||||
// The visitor closed the plugin side. Close the controller side.
|
||||
p.cleanupControllerConn(xl)
|
||||
|
||||
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
||||
@@ -184,7 +212,7 @@ func (p *VirtualNetPlugin) Close() error {
|
||||
}
|
||||
|
||||
// Explicitly close the controller side of the pipe.
|
||||
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
|
||||
// This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
|
||||
p.cleanupControllerConn(xl)
|
||||
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
||||
|
||||
|
||||
34
pkg/policy/security/unsafe.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package security
|
||||
|
||||
const (
|
||||
TokenSourceExec = "TokenSourceExec"
|
||||
)
|
||||
|
||||
var (
|
||||
ClientUnsafeFeatures = []string{
|
||||
TokenSourceExec,
|
||||
}
|
||||
|
||||
ServerUnsafeFeatures = []string{
|
||||
TokenSourceExec,
|
||||
}
|
||||
)
|
||||
|
||||
type UnsafeFeatures struct {
|
||||
features map[string]bool
|
||||
}
|
||||
|
||||
func NewUnsafeFeatures(allowed []string) *UnsafeFeatures {
|
||||
features := make(map[string]bool)
|
||||
for _, f := range allowed {
|
||||
features[f] = true
|
||||
}
|
||||
return &UnsafeFeatures{features: features}
|
||||
}
|
||||
|
||||
func (u *UnsafeFeatures) IsEnabled(feature string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
return u.features[feature]
|
||||
}
|
||||
@@ -124,8 +124,8 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Add proxy protocol header if configured
|
||||
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
||||
// Add proxy protocol header if configured (only for the first packet of a new connection)
|
||||
if !ok && proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
||||
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
|
||||
if err == nil {
|
||||
// Prepend proxy protocol header to the UDP payload
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"github.com/fatedier/frp/client/api"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
||||
c.authPwd = pwd
|
||||
}
|
||||
|
||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
|
||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allStatus := make(client.StatusResp)
|
||||
allStatus := make(api.StatusResp)
|
||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
|
||||
return nil, fmt.Errorf("no proxy status found")
|
||||
}
|
||||
|
||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
|
||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allStatus := make(client.StatusResp)
|
||||
allStatus := make(api.StatusResp)
|
||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (s *TunnelServer) Run() error {
|
||||
if sshConn.Permissions != nil {
|
||||
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
|
||||
}
|
||||
pc.Complete(clientCfg.User)
|
||||
pc.Complete()
|
||||
|
||||
vc, err := virtual.NewClient(virtual.ClientOptions{
|
||||
Common: clientCfg,
|
||||
|
||||
@@ -35,15 +35,19 @@ type MessageTransporter interface {
|
||||
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
||||
}
|
||||
|
||||
func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter {
|
||||
type MessageSender interface {
|
||||
Send(msg.Message) error
|
||||
}
|
||||
|
||||
func NewMessageTransporter(sender MessageSender) MessageTransporter {
|
||||
return &transporterImpl{
|
||||
sendCh: sendCh,
|
||||
sender: sender,
|
||||
registry: make(map[string]map[string]chan msg.Message),
|
||||
}
|
||||
}
|
||||
|
||||
type transporterImpl struct {
|
||||
sendCh chan msg.Message
|
||||
sender MessageSender
|
||||
|
||||
// First key is message type and second key is lane key.
|
||||
// Dispatch will dispatch message to related channel by its message type
|
||||
@@ -53,9 +57,7 @@ type transporterImpl struct {
|
||||
}
|
||||
|
||||
func (impl *transporterImpl) Send(m msg.Message) error {
|
||||
return errors.PanicToError(func() {
|
||||
impl.sendCh <- m
|
||||
})
|
||||
return impl.sender.Send(m)
|
||||
}
|
||||
|
||||
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
||||
|
||||
57
pkg/util/http/context.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Req *http.Request
|
||||
Resp http.ResponseWriter
|
||||
vars map[string]string
|
||||
}
|
||||
|
||||
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
|
||||
return &Context{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
vars: mux.Vars(r),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) Param(key string) string {
|
||||
return c.vars[key]
|
||||
}
|
||||
|
||||
func (c *Context) Query(key string) string {
|
||||
return c.Req.URL.Query().Get(key)
|
||||
}
|
||||
|
||||
func (c *Context) BindJSON(obj any) error {
|
||||
body, err := io.ReadAll(c.Req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, obj)
|
||||
}
|
||||
|
||||
func (c *Context) Body() ([]byte, error) {
|
||||
return io.ReadAll(c.Req.Body)
|
||||
}
|
||||
33
pkg/util/http/error.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Error struct {
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func NewError(code int, msg string) *Error {
|
||||
return &Error{
|
||||
Code: code,
|
||||
Err: fmt.Errorf("%s", msg),
|
||||
}
|
||||
}
|
||||
66
pkg/util/http/handler.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
type GeneralResponse struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
// APIHandler is a handler function that returns a response object or an error.
|
||||
type APIHandler func(ctx *Context) (any, error)
|
||||
|
||||
// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc.
|
||||
func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := NewContext(w, r)
|
||||
res, err := handler(ctx)
|
||||
if err != nil {
|
||||
log.Warnf("http response [%s]: error: %v", r.URL.Path, err)
|
||||
code := http.StatusInternalServerError
|
||||
if e, ok := err.(*Error); ok {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
switch v := res.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(v)
|
||||
case string:
|
||||
_, _ = w.Write([]byte(v))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
pkg/util/http/middleware.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
code int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.code = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func NewRequestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("http request: [%s]", r.URL.Path)
|
||||
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
|
||||
next.ServeHTTP(rw, r)
|
||||
log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code)
|
||||
})
|
||||
}
|
||||
@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
|
||||
// 1 means closed
|
||||
closeFlag int32
|
||||
|
||||
closeFn func()
|
||||
closeFn func(error)
|
||||
}
|
||||
|
||||
// closeFn will be only called once
|
||||
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||
// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
|
||||
func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
||||
return &CloseNotifyConn{
|
||||
Conn: c,
|
||||
closeFn: closeFn,
|
||||
@@ -149,14 +149,27 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||
func (cc *CloseNotifyConn) Close() (err error) {
|
||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||
if pflag == 0 {
|
||||
err = cc.Close()
|
||||
err = cc.Conn.Close()
|
||||
if cc.closeFn != nil {
|
||||
cc.closeFn()
|
||||
cc.closeFn(nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CloseWithError closes the connection and passes the error to the close callback.
|
||||
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||
if pflag == 0 {
|
||||
closeErr := cc.Conn.Close()
|
||||
if cc.closeFn != nil {
|
||||
cc.closeFn(err)
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StatsConn struct {
|
||||
net.Conn
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
||||
muxer := http.NewServeMux()
|
||||
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
||||
notifyCh := make(chan struct{})
|
||||
conn := WrapCloseNotifyConn(c, func() {
|
||||
conn := WrapCloseNotifyConn(c, func(_ error) {
|
||||
close(notifyCh)
|
||||
})
|
||||
wl.acceptCh <- conn
|
||||
|
||||
33
pkg/util/util/names.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package util
|
||||
|
||||
import "strings"
|
||||
|
||||
// AddUserPrefix builds the wire-level proxy name for frps by prefixing user.
|
||||
func AddUserPrefix(user, name string) string {
|
||||
if user == "" {
|
||||
return name
|
||||
}
|
||||
return user + "." + name
|
||||
}
|
||||
|
||||
// StripUserPrefix converts a wire-level proxy name to an internal raw name.
|
||||
// It strips only one exact "{user}." prefix.
|
||||
func StripUserPrefix(user, name string) string {
|
||||
if user == "" {
|
||||
return name
|
||||
}
|
||||
prefix := user + "."
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return strings.TrimPrefix(name, prefix)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// BuildTargetServerProxyName resolves visitor target proxy name for wire-level
|
||||
// protocol messages. serverUser overrides local user when set.
|
||||
func BuildTargetServerProxyName(localUser, serverUser, serverName string) string {
|
||||
if serverUser != "" {
|
||||
return AddUserPrefix(serverUser, serverName)
|
||||
}
|
||||
return AddUserPrefix(localUser, serverName)
|
||||
}
|
||||
@@ -41,3 +41,23 @@ func TestParseRangeNumbers(t *testing.T) {
|
||||
_, err = ParseRangeNumbers("3-a")
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestAddUserPrefix(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.Equal("test", AddUserPrefix("", "test"))
|
||||
require.Equal("alice.test", AddUserPrefix("alice", "test"))
|
||||
}
|
||||
|
||||
func TestStripUserPrefix(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.Equal("test", StripUserPrefix("", "test"))
|
||||
require.Equal("test", StripUserPrefix("alice", "alice.test"))
|
||||
require.Equal("alice.test", StripUserPrefix("alice", "alice.alice.test"))
|
||||
require.Equal("bob.test", StripUserPrefix("alice", "bob.test"))
|
||||
}
|
||||
|
||||
func TestBuildTargetServerProxyName(t *testing.T) {
|
||||
require := require.New(t)
|
||||
require.Equal("alice.test", BuildTargetServerProxyName("alice", "", "test"))
|
||||
require.Equal("bob.test", BuildTargetServerProxyName("alice", "bob", "test"))
|
||||
}
|
||||
|
||||