Compare commits

...

28 Commits

Author SHA1 Message Date
655dc3cb2a fix(version): update version to LoliaFRP-CLI 0.66.2 2026-01-13 00:10:24 +08:00
9894342f46 fix(auth): support multiple authentication tokens for frpc 2026-01-13 00:04:50 +08:00
e7cc706c86 fix(vhost): correct heading text for unbound domain page 2026-01-11 14:37:17 +08:00
92ac2b9153 fix(workflow): enhance artifact handling in release process [skip ci] 2026-01-11 14:08:20 +08:00
1ed369e962 fix(workflow): remove tags specification from push event [skip ci] 2026-01-11 14:04:37 +08:00
b74a8d0232 fix(workflow): remove branch specification from build-all workflow 2026-01-11 14:01:35 +08:00
d2180081a0 fix(workflow): correct path to build-all workflow 2026-01-11 13:58:22 +08:00
51f4e065b5 fix(workflow): correct path to build-all workflow 2026-01-11 13:55:26 +08:00
e58f774086 fix(workflow): specify branch for build-all workflow 2026-01-11 13:54:26 +08:00
178e381a26 fix(workflow): update build-all workflow path to absolute 2026-01-11 13:53:33 +08:00
26b93ae3a3 chore(version): update version to LoliaFRP-CLI 0.66.1 2026-01-11 13:52:10 +08:00
a2aeee28e4 feat(vhost): enhance Not Found page with Chinese localization 2026-01-11 13:51:37 +08:00
0416caef71 feat(workflow): enable workflow_call for build-all workflow [skip ci] 2026-01-11 13:36:14 +08:00
1004473e42 feat(workflow): add release workflow for FRP binaries 2026-01-11 13:30:40 +08:00
f386996928 fix(api): update comment for URL construction security 2026-01-11 04:05:27 +08:00
4eb4b202c5 fix(controller): include session ID in analysis error log
style(log): use octal notation for file permissions
2026-01-11 04:00:52 +08:00
ac5bdad507 feat(client): add access messages for proxy services
feat(client): translate log messages to Chinese
feat(cmd): add authentication token support for API config
feat(log): implement rotating file logger with custom styles
feat(banner): add banner display function
fix(version): update version string for CLI
2026-01-11 03:53:52 +08:00
42f4ea7f87 feat(build): enhance android-arm build process and output 2026-01-11 02:00:52 +08:00
36e5ac094b feat(build): add android-arm build process and dependencies 2026-01-11 01:56:30 +08:00
72f79d3357 feat(api): add endpoint to close proxy by name 2025-12-26 01:10:43 +08:00
e1f905f63f chore: remove obsolete build-all workflow and update architectures in package script 2025-12-25 22:19:53 +08:00
eb58f09268 chore: remove funding and pull request template files 2025-12-25 22:19:50 +08:00
46955ffc80 feat(api): update kick proxy endpoint to use name only 2025-12-25 22:19:00 +08:00
2d63296576 feat(api): add endpoint to retrieve all proxies 2025-12-25 22:18:38 +08:00
a76ba823ee feat(api): add endpoint to kick proxy by name 2025-12-25 22:16:41 +08:00
fatedier
ef96481f58 update version and release notes (#5106) 2025-12-25 10:15:40 +08:00
fatedier
7526d7a69a refactor: separate auth config from runtime and defer token resolution (#5105) 2025-12-25 00:53:08 +08:00
fatedier
2bdf25bae6 rotate gold sponsor order periodically (#5094) 2025-12-11 12:48:08 +08:00
43 changed files with 1205 additions and 305 deletions

4
.github/FUNDING.yml vendored
View File

@@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [fatedier]
custom: ["https://afdian.com/a/fatedier"]

View File

@@ -1,3 +0,0 @@
### WHY
<!-- author to complete -->

185
.github/workflows/build-all.yaml vendored Normal file
View File

@@ -0,0 +1,185 @@
name: Build FRP Binaries
on:
push:
branches:
- '**'
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Build FRP ${{ matrix.goos }}-${{ matrix.goarch }}
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin, freebsd, openbsd, android]
goarch: [amd64, arm, arm64]
exclude:
- goos: darwin
goarch: arm
- goos: freebsd
goarch: arm
- goos: openbsd
goarch: arm
- goos: android
goarch: amd64
# 排除 Android ARM 32位,在单独的 job 中处理
- goos: android
goarch: arm
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y zip tar make gcc g++ upx
- name: Build FRP for ${{ matrix.goos }}-${{ matrix.goarch }}
run: |
mkdir -p release/packages
echo "Building for ${{ matrix.goos }}-${{ matrix.goarch }}"
# 构建版本号
make
version=$(./bin/frps --version)
echo "Detected version: $version"
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
export CGO_ENABLED=0
# 构建可执行文件
make frpc frps
if [ "${{ matrix.goos }}" = "windows" ]; then
if [ -f "./bin/frpc" ]; then mv ./bin/frpc ./bin/frpc.exe; fi
if [ -f "./bin/frps" ]; then mv ./bin/frps ./bin/frps.exe; fi
fi
out_dir="release/packages/frp_${version}_${{ matrix.goos }}_${{ matrix.goarch }}"
mkdir -p "$out_dir"
if [ "${{ matrix.goos }}" = "windows" ]; then
mv ./bin/frpc.exe "$out_dir/frpc.exe"
mv ./bin/frps.exe "$out_dir/frps.exe"
else
mv ./bin/frpc "$out_dir/frpc"
mv ./bin/frps "$out_dir/frps"
fi
cp LICENSE "$out_dir"
cp -f conf/frpc.toml "$out_dir"
cp -f conf/frps.toml "$out_dir"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: LoliaFrp_${{ matrix.goos }}_${{ matrix.goarch }}
path: |
release/packages/frp_*
retention-days: 7
build-android-arm:
name: Build FRP android-arm
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install dependencies and Android NDK
run: |
sudo apt-get update -y
sudo apt-get install -y zip tar make gcc g++ upx wget unzip
# 下载并安装 Android NDK
echo "Downloading Android NDK..."
wget -q https://dl.google.com/android/repository/android-ndk-r26c-linux.zip
echo "Extracting Android NDK..."
unzip -q android-ndk-r26c-linux.zip
echo "NDK installed at: $PWD/android-ndk-r26c"
- name: Build FRP for android-arm
run: |
mkdir -p release/packages
mkdir -p bin
echo "Building for android-arm with CGO"
# 首先构建一次获取版本号
CGO_ENABLED=0 make
version=$(./bin/frps --version)
echo "Detected version: $version"
# 清理之前的构建
rm -rf ./bin/*
# 设置 Android ARM 交叉编译环境
export GOOS=android
export GOARCH=arm
export GOARM=7
export CGO_ENABLED=1
export CC=$PWD/android-ndk-r26c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang
export CXX=$PWD/android-ndk-r26c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang++
echo "Environment:"
echo "GOOS=$GOOS"
echo "GOARCH=$GOARCH"
echo "GOARM=$GOARM"
echo "CGO_ENABLED=$CGO_ENABLED"
echo "CC=$CC"
# 直接使用 go build 命令,不通过 Makefile防止 CGO_ENABLED 被覆盖
echo "Building frps..."
go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
echo "Building frpc..."
go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
# 验证文件已生成
ls -lh ./bin/
file ./bin/frpc
file ./bin/frps
out_dir="release/packages/frp_${version}_android_arm"
mkdir -p "$out_dir"
mv ./bin/frpc "$out_dir/frpc"
mv ./bin/frps "$out_dir/frps"
cp LICENSE "$out_dir"
cp -f conf/frpc.toml "$out_dir"
cp -f conf/frps.toml "$out_dir"
echo "Build completed for android-arm"
ls -lh "$out_dir"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: LoliaFrp_android_arm
path: |
release/packages/frp_*
retention-days: 7

View File

@@ -23,4 +23,4 @@ jobs:
uses: golangci/golangci-lint-action@v8
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v2.3
version: v2.3

172
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,172 @@
name: Release FRP Binaries
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., v1.0.0)'
required: true
type: string
permissions:
contents: write
jobs:
# 调用 build-all workflow
build:
uses: ./.github/workflows/build-all.yaml
permissions:
contents: read
# 创建 release
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get tag name
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display artifact structure
run: |
echo "Artifact structure:"
ls -R artifacts/
- name: Organize release files
run: |
mkdir -p release_files
# 查找并复制所有压缩包
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" \) -exec cp {} release_files/ \;
# 如果没有压缩包,尝试查找二进制文件并打包
if [ -z "$(ls -A release_files/)" ]; then
echo "No archives found, looking for directories to package..."
for dir in artifacts/*/; do
if [ -d "$dir" ]; then
artifact_name=$(basename "$dir")
echo "Packaging $artifact_name"
# 检查是否是 Windows 构建
if echo "$artifact_name" | grep -q "windows"; then
(cd "$dir" && zip -r "../../release_files/${artifact_name}.zip" .)
else
tar -czf "release_files/${artifact_name}.tar.gz" -C "$dir" .
fi
fi
done
fi
echo "Files in release_files:"
ls -lh release_files/
- name: Generate checksums
run: |
cd release_files
if [ -n "$(ls -A .)" ]; then
sha256sum * > sha256sum.txt
cat sha256sum.txt
else
echo "No files to generate checksums for!"
exit 1
fi
- name: Save checksums for changelog
id: checksums
run: |
{
echo 'content<<EOF'
cat release_files/sha256sum.txt
echo EOF
} >> $GITHUB_OUTPUT
- name: Build Changelog
id: changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: |
{
"categories": [
{
"title": "## 🚀 Features",
"labels": ["feature", "feat", "enhancement"]
},
{
"title": "## 🐛 Bug Fixes",
"labels": ["fix", "bug", "bugfix"]
},
{
"title": "## 📝 Documentation",
"labels": ["docs", "documentation"]
},
{
"title": "## 🔧 Maintenance",
"labels": ["chore", "refactor", "perf"]
},
{
"title": "## 📦 Dependencies",
"labels": ["dependencies", "deps"]
},
{
"title": "## 🔀 Other Changes",
"labels": []
}
],
"template": "#{{CHANGELOG}}\n\n## 📥 Download\n\n### Checksums (SHA256)\n\n```\n${{ steps.checksums.outputs.content }}\n```\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
"pr_template": "- #{{TITLE}} by @#{{AUTHOR}} in ##{{NUMBER}}",
"empty_template": "- No changes",
"label_extractor": [
{
"pattern": "^(feat|feature)(\\(.+\\))?:",
"target": "feature"
},
{
"pattern": "^fix(\\(.+\\))?:",
"target": "fix"
},
{
"pattern": "^docs(\\(.+\\))?:",
"target": "docs"
},
{
"pattern": "^(chore|refactor|perf)(\\(.+\\))?:",
"target": "chore"
}
]
}
toTag: ${{ steps.tag.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: Release ${{ steps.tag.outputs.tag }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
files: |
release_files/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,6 +13,14 @@ 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://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/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -50,20 +58,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
<sub>Available for macOS, Linux and Windows</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">
<br>
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
</a>
</p>
<!--gold sponsors end-->
## What is frp?

View File

@@ -15,6 +15,14 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start-->
<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/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -51,20 +59,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
<sub>Available for macOS, Linux and Windows</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">
<br>
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
</a>
</p>
<!--gold sponsors end-->
## 为什么使用 frp

View File

@@ -2,6 +2,7 @@
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
* Individual frpc proxies and visitors now accept an `enabled` flag (defaults to true), letting you disable specific entries without relying on the global `start` list—disabled blocks are skipped when client configs load.
* OIDC authentication now supports a `tokenSource` field to dynamically obtain tokens from external sources. You can use `type = "file"` to read a token from a file, or `type = "exec"` to run an external command (e.g., a cloud CLI or secrets manager) and capture its stdout as the token. The `exec` type requires the `--allow-unsafe=TokenSourceExec` CLI flag for security reasons.
## Improvements

View File

@@ -16,7 +16,9 @@ package client
import (
"context"
"fmt"
"net"
"strings"
"sync/atomic"
"time"
@@ -43,8 +45,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 +93,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
}
@@ -102,7 +104,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
ctl.registerMsgHandlers()
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 +135,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
@@ -167,9 +169,44 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
// Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
if err != nil {
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
xl.Warnf("[%s] 启动失败: %v", inMsg.ProxyName, err)
} else {
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
xl.Infof("[%s] 成功启动隧道", inMsg.ProxyName)
if inMsg.RemoteAddr != "" {
// Get proxy type to format access message
if status, ok := ctl.pm.GetProxyStatus(inMsg.ProxyName); ok {
proxyType := status.Type
remoteAddr := inMsg.RemoteAddr
var accessMsg string
switch proxyType {
case "tcp", "udp", "stcp", "xtcp", "sudp", "tcpmux":
// If remoteAddr only contains port (e.g., ":8080"), prepend server address
if strings.HasPrefix(remoteAddr, ":") {
serverAddr := ctl.sessionCtx.Common.ServerAddr
remoteAddr = serverAddr + remoteAddr
}
accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr)
case "http", "https":
// Format as URL with protocol
protocol := proxyType
addr := remoteAddr
// Remove standard ports for cleaner URL
if proxyType == "http" && strings.HasSuffix(addr, ":80") {
addr = strings.TrimSuffix(addr, ":80")
} else if proxyType == "https" && strings.HasSuffix(addr, ":443") {
addr = strings.TrimSuffix(addr, ":443")
}
accessMsg = fmt.Sprintf("您可通过 %s://%s 访问您的服务", protocol, addr)
default:
accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr)
}
xl.Infof("[%s] %s", inMsg.ProxyName, accessMsg)
} else {
xl.Infof("[%s] 您可通过 %s 访问您的服务", inMsg.ProxyName, inMsg.RemoteAddr)
}
}
}
}
@@ -243,7 +280,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
}

View File

@@ -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.

View File

@@ -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,
}
@@ -156,14 +159,14 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
}
}
if len(delPxyNames) > 0 {
xl.Infof("proxy removed: %s", delPxyNames)
xl.Infof("隧道移除: %s", delPxyNames)
}
addPxyNames := make([]string, 0)
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)
}
@@ -174,6 +177,6 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
}
}
if len(addPxyNames) > 0 {
xl.Infof("proxy added: %s", addPxyNames)
xl.Infof("添加隧道: %s", addPxyNames)
}
}

View File

@@ -92,6 +92,7 @@ func NewWrapper(
ctx context.Context,
cfg v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig,
encryptionKey []byte,
eventHandler event.Handler,
msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller,
@@ -122,7 +123,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
}

View File

@@ -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)

View File

@@ -90,7 +90,7 @@ func (pxy *UDPProxy) Close() {
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl
xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
xl.Infof("收到一条新的 UDP 代理工作连接, %s", conn.RemoteAddr().String())
// close resources related with old workConn
pxy.Close()
@@ -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)

View File

@@ -111,8 +111,8 @@ 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
@@ -155,14 +155,14 @@ func NewService(options ServiceOptions) (*Service, error) {
webServer = ws
}
authSetter, err := auth.NewAuthSetter(options.Common.Auth)
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
if err != nil {
return nil, err
}
s := &Service{
ctx: context.Background(),
authSetter: authSetter,
auth: authRuntime,
webServer: webServer,
common: options.Common,
configFilePath: options.ConfigFilePath,
@@ -219,7 +219,7 @@ func (svr *Service) Run(ctx context.Context) error {
if svr.ctl == nil {
cancelCause := cancelErr{}
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
return fmt.Errorf("登录服务器失败: %v. 启用 loginFailExit 后,将不再尝试重试", cancelCause.Err)
}
go svr.keepControllerWorking()
@@ -296,7 +296,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
}
@@ -320,7 +320,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
svr.runID = loginRespMsg.RunID
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
xl.Infof("登录服务器成功, 获取 run id [%s]", loginRespMsg.RunID)
return
}
@@ -328,10 +328,10 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
xl := xlog.FromContextSafe(svr.ctx)
loginFunc := func() (bool, error) {
xl.Infof("try to connect to server...")
xl.Infof("尝试连接到服务器...")
conn, connector, err := svr.login()
if err != nil {
xl.Warnf("connect to server error: %v", err)
xl.Warnf("连接服务器错误: %v", err)
if firstLoginExit {
svr.cancel(cancelErr{Err: err})
}
@@ -350,7 +350,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,
}

View File

@@ -80,7 +80,8 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
validator := validation.NewConfigValidator(unsafeFeatures)
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
@@ -91,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
fmt.Println(err)
os.Exit(1)
}
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "", "", "")
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -110,7 +111,8 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
os.Exit(1)
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
validator := validation.NewConfigValidator(unsafeFeatures)
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
@@ -121,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
fmt.Println(err)
os.Exit(1)
}
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "", "", "")
if err != nil {
fmt.Println(err)
os.Exit(1)

View File

@@ -16,8 +16,11 @@ package sub
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"os/signal"
"path/filepath"
@@ -34,6 +37,7 @@ import (
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/banner"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
@@ -44,6 +48,7 @@ var (
showVersion bool
strictConfigMode bool
allowUnsafe []string
authTokens []string
)
func init() {
@@ -51,7 +56,7 @@ 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(&authTokens, "token", "t", []string{}, "authentication tokens in format 'id:token' (LoliaFRP only)")
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
}
@@ -67,6 +72,16 @@ var rootCmd = &cobra.Command{
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
// If authTokens is provided, fetch config from API
if len(authTokens) > 0 {
err := runClientWithTokens(authTokens, unsafeFeatures)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
}
// 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 != "" {
@@ -142,7 +157,8 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro
if err != nil {
return err
}
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "")
}
func startService(
@@ -151,12 +167,24 @@ func startService(
visitorCfgs []v1.VisitorConfigurer,
unsafeFeatures *security.UnsafeFeatures,
cfgFile string,
nodeName string,
tunnelRemark string,
) error {
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
// Display banner before starting the service
banner.DisplayBanner()
log.Infof("Nya! %s 启动中", version.Full())
// Display node information if available
if nodeName != "" {
log.Info("已获取到配置文件", "隧道名称", tunnelRemark, "使用节点", nodeName)
}
if cfgFile != "" {
log.Infof("start frpc service for config file [%s]", cfgFile)
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
log.Infof("启动 frpc 服务 [%s]", cfgFile)
defer log.Infof("frpc 服务 [%s] 已停止", cfgFile)
}
svr, err := client.NewService(client.ServiceOptions{
Common: cfg,
@@ -176,3 +204,148 @@ func startService(
}
return svr.Run(context.Background())
}
// APIResponse represents the response from LoliaFRP API
type APIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Config string `json:"config"`
NodeName string `json:"node_name"`
TunnelRemark string `json:"tunnel_remark"`
} `json:"data"`
}
// TokenInfo stores parsed id and token from the -t parameter
type TokenInfo struct {
ID string
Token string
}
func runClientWithTokens(tokens []string, unsafeFeatures *security.UnsafeFeatures) error {
// Parse all tokens (format: id:token)
tokenInfos := make([]TokenInfo, 0, len(tokens))
for _, t := range tokens {
parts := strings.SplitN(t, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid token format '%s', expected 'id:token'", t)
}
tokenInfos = append(tokenInfos, TokenInfo{
ID: strings.TrimSpace(parts[0]),
Token: strings.TrimSpace(parts[1]),
})
}
// Group tokens by token value (same token can have multiple IDs)
tokenToIDs := make(map[string][]string)
for _, ti := range tokenInfos {
tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID)
}
// Verify all tokens use the same token value
if len(tokenToIDs) > 1 {
return fmt.Errorf("different tokens are not supported, all ids must use the same token")
}
// Get the single token and all its IDs
var token string
var ids []string
for t, idList := range tokenToIDs {
token = t
ids = idList
break
}
return runClientWithTokenAndIDs(token, ids, unsafeFeatures)
}
func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *security.UnsafeFeatures) error {
// Get API server address from environment variable
apiServer := os.Getenv("LOLIA_API")
if apiServer == "" {
apiServer = "https://api.lolia.link"
}
// Build URL with query parameters
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config?token=%s&id=%s", apiServer, token, strings.Join(ids, ","))
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch config from API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return fmt.Errorf("failed to decode API response: %v", err)
}
if apiResp.Code != 200 {
return fmt.Errorf("API error: %s", apiResp.Msg)
}
// Decode base64 config
configBytes, err := base64.StdEncoding.DecodeString(apiResp.Data.Config)
if err != nil {
return fmt.Errorf("failed to decode base64 config: %v", err)
}
// Load config directly from bytes
return runClientWithConfig(configBytes, unsafeFeatures, apiResp.Data.NodeName, apiResp.Data.TunnelRemark)
}
func runClientWithConfig(configBytes []byte, unsafeFeatures *security.UnsafeFeatures, nodeName, tunnelRemark string) error {
// Render template first
renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues())
if err != nil {
return fmt.Errorf("failed to render template: %v", err)
}
var allCfg v1.ClientConfig
if err := config.LoadConfigure(renderedBytes, &allCfg, strictConfigMode); err != nil {
return fmt.Errorf("failed to parse config: %v", err)
}
cfg := &allCfg.ClientCommonConfig
proxyCfgs := make([]v1.ProxyConfigurer, 0, len(allCfg.Proxies))
for _, c := range allCfg.Proxies {
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
}
visitorCfgs := make([]v1.VisitorConfigurer, 0, len(allCfg.Visitors))
for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
}
// Call Complete to fill in default values
if err := cfg.Complete(); err != nil {
return fmt.Errorf("failed to complete config: %v", err)
}
// Call Complete for all proxies to add name prefix (e.g., user.tunnel_name)
for _, c := range proxyCfgs {
c.Complete(cfg.User)
}
for _, c := range visitorCfgs {
c.Complete(cfg)
}
if len(cfg.FeatureGates) > 0 {
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
return err
}
}
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
return err
}
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, "", nodeName, tunnelRemark)
}

View File

@@ -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)
}

View File

@@ -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)
}

15
go.mod
View File

@@ -39,10 +39,18 @@ require (
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/log v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -51,6 +59,10 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
@@ -60,13 +72,16 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // 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

31
go.sum
View File

@@ -4,11 +4,25 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
@@ -26,6 +40,8 @@ github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMB
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-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -70,8 +86,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
@@ -109,6 +133,8 @@ github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9M
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -149,6 +175,8 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
@@ -167,6 +195,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
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=
@@ -209,6 +239,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -18,7 +18,7 @@ rm -rf ./release/packages
mkdir -p ./release/packages
os_all='linux windows darwin freebsd openbsd android'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
arch_all='amd64 arm arm64'
extra_all='_ hf'
cd ./release

View File

@@ -15,6 +15,7 @@
package auth
import (
"context"
"fmt"
v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -27,6 +28,39 @@ type Setter interface {
SetNewWorkConn(*msg.NewWorkConn) error
}
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:
@@ -52,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:

View File

@@ -15,8 +15,6 @@
package v1
import (
"context"
"fmt"
"os"
"github.com/samber/lo"
@@ -198,17 +196,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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -27,7 +27,7 @@ import (
"github.com/fatedier/frp/pkg/policy/security"
)
func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
var (
warnings Warning
errs error
@@ -35,15 +35,15 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *securi
validators := []func() (Warning, error){
func() (Warning, error) { return validateFeatureGates(c) },
func() (Warning, error) { return validateAuthConfig(&c.Auth, unsafeFeatures) },
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) },
}
for _, v := range validators {
w, err := v()
for _, validator := range validators {
w, err := validator()
warnings = AppendError(warnings, w)
errs = AppendError(errs, err)
}
@@ -59,7 +59,7 @@ func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
return nil, nil
}
func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
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))
@@ -76,9 +76,8 @@ func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeF
// Validate tokenSource if specified
if c.TokenSource != nil {
if c.TokenSource.Type == "exec" {
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.TokenSource.Validate(); err != nil {
@@ -86,13 +85,13 @@ func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeF
}
}
if err := validateOIDCConfig(&c.OIDC, unsafeFeatures); err != nil {
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
errs = AppendError(errs, err)
}
return nil, errs
}
func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.UnsafeFeatures) error {
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
if c.TokenSource == nil {
return nil
}
@@ -104,9 +103,8 @@ func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.Uns
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
}
if c.TokenSource.Type == "exec" {
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.TokenSource.Validate(); err != nil {
@@ -167,9 +165,10 @@ func ValidateAllClientConfig(
visitorCfgs []v1.VisitorConfigurer,
unsafeFeatures *security.UnsafeFeatures,
) (Warning, error) {
validator := NewConfigValidator(unsafeFeatures)
var warnings Warning
if c != nil {
warning, err := ValidateClientCommonConfig(c, unsafeFeatures)
warning, err := validator.ValidateClientCommonConfig(c)
warnings = AppendError(warnings, warning)
if err != nil {
return warnings, err

View File

@@ -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))
}

View 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
}

View File

@@ -206,8 +206,9 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
m.mu.Lock()
defer m.mu.Unlock()
filterAll := proxyType == "" || proxyType == "all"
for name, proxyStats := range m.info.ProxyStatistics {
if proxyStats.ProxyType != proxyType {
if !filterAll && proxyStats.ProxyType != proxyType {
continue
}
@@ -233,8 +234,9 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
m.mu.Lock()
defer m.mu.Unlock()
filterAll := proxyType == "" || proxyType == "all"
for name, proxyStats := range m.info.ProxyStatistics {
if proxyStats.ProxyType != proxyType {
if !filterAll && proxyStats.ProxyType != proxyType {
continue
}

View File

@@ -220,7 +220,7 @@ func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.
// Make hole-punching decisions based on the NAT information of the client and visitor.
vResp, cResp, err := c.analysis(session)
if err != nil {
log.Debugf("sid [%s] analysis error: %v", err)
log.Debugf("sid [%s] analysis error: %v", sid, err)
vResp = c.GenNatHoleResponse(session.visitorMsg.TransactionID, nil, err.Error())
cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error())
}

12
pkg/util/banner/banner.go Normal file
View File

@@ -0,0 +1,12 @@
package banner
import "fmt"
func DisplayBanner() {
fmt.Println(" __ ___ __________ ____ ________ ____")
fmt.Println(" / / ____ / (_)___ _/ ____/ __ \\/ __ \\ / ____/ / / _/")
fmt.Println(" / / / __ \\/ / / __ `/ /_ / /_/ / /_/ /_____/ / / / / / ")
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
fmt.Println(" ")
}

View File

@@ -16,13 +16,18 @@ package log
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/fatedier/golib/log"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
)
var (
TraceLevel = log.TraceLevel
TraceLevel = log.DebugLevel
DebugLevel = log.DebugLevel
InfoLevel = log.InfoLevel
WarnLevel = log.WarnLevel
@@ -32,39 +37,158 @@ var (
var Logger *log.Logger
func init() {
Logger = log.New(
log.WithCaller(true),
log.AddCallerSkip(1),
log.WithLevel(log.InfoLevel),
)
Logger = log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: true,
ReportTimestamp: true,
TimeFormat: time.Kitchen,
Prefix: "LoliaFRP-CLI",
CallerOffset: 1,
})
// 设置自定义样式以支持 Trace 级别
styles := log.DefaultStyles()
styles.Levels[TraceLevel] = lipgloss.NewStyle().
SetString("TRACE").
Bold(true).
MaxWidth(5).
Foreground(lipgloss.Color("61"))
Logger.SetStyles(styles)
}
func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) {
options := []log.Option{}
var output io.Writer
var err error
if logPath == "console" {
if !disableLogColor {
options = append(options,
log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{
Colorful: true,
}, os.Stdout)),
)
}
output = os.Stdout
} else {
writer := log.NewRotateFileWriter(log.RotateFileConfig{
FileName: logPath,
Mode: log.RotateFileModeDaily,
MaxDays: maxDays,
})
writer.Init()
options = append(options, log.WithOutput(writer))
// Use rotating file writer
output, err = NewRotateFileWriter(logPath, maxDays)
if err != nil {
// Fallback to console if file creation fails
output = os.Stdout
}
}
level, err := log.ParseLevel(levelStr)
if err != nil {
level = log.InfoLevel
}
options = append(options, log.WithLevel(level))
Logger = Logger.WithOptions(options...)
Logger = log.NewWithOptions(output, log.Options{
ReportCaller: true,
ReportTimestamp: true,
TimeFormat: time.Kitchen,
Prefix: "LoliaFRP-CLI",
CallerOffset: 1,
Level: level,
})
}
// NewRotateFileWriter creates a rotating file writer
func NewRotateFileWriter(filePath string, maxDays int) (*RotateFileWriter, error) {
w := &RotateFileWriter{
filePath: filePath,
maxDays: maxDays,
lastRotate: time.Now(),
currentDate: time.Now().Format("2006-01-02"),
}
if err := w.openFile(); err != nil {
return nil, err
}
return w, nil
}
// RotateFileWriter implements io.Writer with daily rotation
type RotateFileWriter struct {
filePath string
maxDays int
file *os.File
lastRotate time.Time
currentDate string
}
func (w *RotateFileWriter) openFile() error {
var err error
w.file, err = os.OpenFile(w.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
return err
}
func (w *RotateFileWriter) checkRotate() error {
now := time.Now()
currentDate := now.Format("2006-01-02")
if currentDate != w.currentDate {
// Close current file
if w.file != nil {
w.file.Close()
}
// Rename current file with date suffix
oldPath := w.filePath
newPath := w.filePath + "." + w.currentDate
if _, err := os.Stat(oldPath); err == nil {
if err := os.Rename(oldPath, newPath); err != nil {
return err
}
}
// Clean up old log files
w.cleanupOldLogs(now)
// Update current date and open new file
w.currentDate = currentDate
w.lastRotate = now
return w.openFile()
}
return nil
}
func (w *RotateFileWriter) cleanupOldLogs(now time.Time) {
if w.maxDays <= 0 {
return
}
cutoffDate := now.AddDate(0, 0, -w.maxDays)
// Find and remove old log files
dir := filepath.Dir(w.filePath)
base := filepath.Base(w.filePath)
files, _ := os.ReadDir(dir)
for _, f := range files {
if f.IsDir() {
continue
}
name := f.Name()
if strings.HasPrefix(name, base+".") {
// Extract date from filename (base.YYYY-MM-DD)
dateStr := strings.TrimPrefix(name, base+".")
if len(dateStr) == 10 {
fileDate, err := time.Parse("2006-01-02", dateStr)
if err == nil && fileDate.Before(cutoffDate) {
os.Remove(filepath.Join(dir, name))
}
}
}
}
}
func (w *RotateFileWriter) Write(p []byte) (n int, err error) {
if err := w.checkRotate(); err != nil {
return 0, err
}
return w.file.Write(p)
}
func (w *RotateFileWriter) Close() error {
if w.file != nil {
return w.file.Close()
}
return nil
}
func Errorf(format string, v ...any) {
@@ -75,6 +199,10 @@ func Warnf(format string, v ...any) {
Logger.Warnf(format, v...)
}
func Info(format string, v ...any) {
Logger.Info(format, v...)
}
func Infof(format string, v ...any) {
Logger.Infof(format, v...)
}
@@ -84,11 +212,12 @@ func Debugf(format string, v ...any) {
}
func Tracef(format string, v ...any) {
Logger.Tracef(format, v...)
Logger.Logf(TraceLevel, format, v...)
}
func Logf(level log.Level, offset int, format string, v ...any) {
Logger.Logf(level, offset, format, v...)
// charmbracelet/log doesn't support offset, so we ignore it
Logger.Logf(level, format, v...)
}
type WriteLogger struct {
@@ -104,6 +233,8 @@ func NewWriteLogger(level log.Level, offset int) *WriteLogger {
}
func (w *WriteLogger) Write(p []byte) (n int, err error) {
Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n")))
// charmbracelet/log doesn't support offset in Log
msg := string(bytes.TrimRight(p, "\n"))
Logger.Log(w.level, msg)
return len(p), nil
}

View File

@@ -14,7 +14,7 @@
package version
var version = "0.65.0"
var version = "LoliaFRP-CLI 0.66.2"
func Full() string {
return version

View File

@@ -28,23 +28,70 @@ var NotFoundPagePath = ""
const (
NotFound = `<!DOCTYPE html>
<html>
<html lang="zh-CN">
<head>
<title>Not Found</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 未绑定域名</title>
<style>
body {
font-family: -apple-system, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #fff;
color: #333;
}
.container {
max-width: 600px;
padding: 40px 20px;
text-align: center;
}
h1 {
font-size: 32px;
font-weight: 600;
margin-bottom: 20px;
}
p {
line-height: 1.8;
color: #666;
margin: 10px 0;
}
ul {
text-align: left;
margin: 20px auto;
max-width: 400px;
}
li {
margin: 8px 0;
color: #666;
}
a {
color: #0066cc;
text-decoration: none;
}
a:hover { text-decoration: underline; }
.footer {
margin-top: 40px;
font-size: 14px;
color: #999;
}
</style>
</head>
<body>
<h1>The page you requested was not found.</h1>
<p>Sorry, the page you are looking for is currently unavailable.<br/>
Please try again later.</p>
<p>The server is powered by <a href="https://github.com/fatedier/frp">frp</a>.</p>
<p><em>Faithfully yours, frp.</em></p>
<div class="container">
<h1>域名未绑定</h1>
<p>这个域名还没有绑定到任何隧道哦 (;д;)</p>
<p><strong>可能是这些原因:</strong></p>
<ul>
<li>域名配置不对,或者没有正确解析</li>
<li>隧道可能还没启动,或者已经停止</li>
<li>自定义域名忘记在服务端配置了</li>
</ul>
<div class="footer">由 <a href="https://lolia.link/">LoliaFRP</a> 与捐赠者们用爱发电</div>
</div>
</body>
</html>
`
@@ -69,7 +116,7 @@ func getNotFoundPageContent() []byte {
func NotFoundResponse() *http.Response {
header := make(http.Header)
header.Set("server", "frp/"+version.Full())
header.Set("server", version.Full())
header.Set("Content-Type", "text/html")
content := getNotFoundPageContent()

View File

@@ -111,5 +111,5 @@ func (l *Logger) Debugf(format string, v ...any) {
}
func (l *Logger) Tracef(format string, v ...any) {
log.Logger.Tracef(l.prefixString+format, v...)
log.Logger.Logf(log.TraceLevel, l.prefixString+format, v...)
}

View File

@@ -94,6 +94,51 @@ func (cm *ControlManager) Close() error {
return nil
}
// CloseAllProxyByName Finds the tunnel name and closes all tunnels on the same connection.
func (cm *ControlManager) CloseAllProxyByName(proxyName string) error {
cm.mu.RLock()
var target *Control
for _, ctl := range cm.ctlsByRunID {
ctl.mu.RLock()
_, ok := ctl.proxies[proxyName]
ctl.mu.RUnlock()
if ok {
target = ctl
break
}
}
cm.mu.RUnlock()
if target == nil {
return fmt.Errorf("no proxy found with name [%s]", proxyName)
}
return target.Close()
}
// KickByProxyName finds the Control that manages the given proxy (tunnel) name and closes
// Bug: The client does not display the kickout message.
func (cm *ControlManager) KickByProxyName(proxyName string) error {
cm.mu.RLock()
var target *Control
for _, ctl := range cm.ctlsByRunID {
ctl.mu.RLock()
_, ok := ctl.proxies[proxyName]
ctl.mu.RUnlock()
if ok {
target = ctl
break
}
}
cm.mu.RUnlock()
if target == nil {
return fmt.Errorf("no proxy found with name [%s]", proxyName)
}
xl := target.xl
xl.Infof("kick client with proxy [%s] by server administrator request", proxyName)
return target.Close()
}
type Control struct {
// all resource managers and controllers
rc *controller.ResourceController
@@ -106,6 +151,8 @@ type Control struct {
// verifies authentication based on selected method
authVerifier auth.Verifier
// key used for connection encryption
encryptionKey []byte
// other components can use this to communicate with client
msgTransporter transport.MessageTransporter
@@ -157,6 +204,7 @@ func NewControl(
pxyManager *proxy.Manager,
pluginManager *plugin.Manager,
authVerifier auth.Verifier,
encryptionKey []byte,
ctlConn net.Conn,
ctlConnEncrypted bool,
loginMsg *msg.Login,
@@ -171,6 +219,7 @@ func NewControl(
pxyManager: pxyManager,
pluginManager: pluginManager,
authVerifier: authVerifier,
encryptionKey: encryptionKey,
conn: ctlConn,
loginMsg: loginMsg,
workConnCh: make(chan net.Conn, poolCount+10),
@@ -186,7 +235,7 @@ func NewControl(
ctl.lastPing.Store(time.Now())
if ctlConnEncrypted {
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
if err != nil {
return nil, err
}
@@ -478,6 +527,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
GetWorkConnFn: ctl.GetWorkConn,
Configurer: pxyConf,
ServerCfg: ctl.serverCfg,
EncryptionKey: ctl.encryptionKey,
})
if err != nil {
return remoteAddr, err

View File

@@ -52,8 +52,11 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
subRouter.HandleFunc("/api/proxy/{name}/close", svr.apiCloseProxyByName).Methods("POST")
subRouter.HandleFunc("/api/proxy/{name}/kick", svr.apiKickProxyByName).Methods("POST")
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
subRouter.HandleFunc("/api/proxies", svr.apiProxiesAll).Methods("GET")
// view
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
@@ -211,6 +214,29 @@ type GetProxyInfoResp struct {
Proxies []*ProxyStatsInfo `json:"proxies"`
}
// GET /api/proxies
// Return all proxies across types (tcp, udp, http, https, stcp, xtcp, tcpmux)
func (svr *Service) apiProxiesAll(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
defer func() {
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("http request: [%s]", r.URL.Path)
proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp.Proxies = svr.getProxyStatsByType("all")
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
return cmp.Compare(a.Name, b.Name)
})
buf, _ := json.Marshal(&proxyInfoResp)
res.Msg = string(buf)
}
// /api/proxy/:type
func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
@@ -237,6 +263,7 @@ func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
}
func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
// mem.StatsCollector now supports proxyType=="all" or "" to return all proxies
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
for _, ps := range proxyStats {
@@ -308,6 +335,69 @@ func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request
res.Msg = string(buf)
}
// POST /api/proxy/:name/close
// Close the proxy with given name only. The client connection remains active.
func (svr *Service) apiCloseProxyByName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
name := params["name"]
defer func() {
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("http request: [%s]", r.URL.Path)
if name == "" {
res.Code = 400
res.Msg = "proxy name required"
return
}
if err := svr.ctlManager.CloseAllProxyByName(name); err != nil {
res.Code = 404
res.Msg = err.Error()
return
}
res.Msg = "ok"
}
// POST /api/proxy/:name/kick
// Kick the client (frpc) that owns the proxy with given name.
// This will disconnect the entire frpc client and close all its proxies.
func (svr *Service) apiKickProxyByName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
name := params["name"]
defer func() {
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("http request: [%s]", r.URL.Path)
if name == "" {
res.Code = 400
res.Msg = "proxy name required"
return
}
if err := svr.ctlManager.KickByProxyName(name); err != nil {
res.Code = 404
res.Msg = err.Error()
return
}
res.Msg = "ok"
}
func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
proxyInfo.Name = proxyName
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)

View File

@@ -165,7 +165,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
var rwc io.ReadWriteCloser = tmpConn
if pxy.cfg.Transport.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
if err != nil {
xl.Errorf("create encryption stream error: %v", err)
return

View File

@@ -68,6 +68,7 @@ type BaseProxy struct {
poolCount int
getWorkConnFn GetWorkConnFn
serverCfg *v1.ServerConfig
encryptionKey []byte
limiter *rate.Limiter
userInfo plugin.UserInfo
loginMsg *msg.Login
@@ -213,7 +214,6 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
xl := xlog.FromContextSafe(pxy.Context())
defer userConn.Close()
serverCfg := pxy.serverCfg
cfg := pxy.configurer.GetBaseConfig()
// server plugin hook
rc := pxy.GetResourceController()
@@ -240,7 +240,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
if cfg.Transport.UseEncryption {
local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token))
local, err = libio.WithEncryption(local, pxy.encryptionKey)
if err != nil {
xl.Errorf("create encryption stream error: %v", err)
return
@@ -279,6 +279,7 @@ type Options struct {
GetWorkConnFn GetWorkConnFn
Configurer v1.ProxyConfigurer
ServerCfg *v1.ServerConfig
EncryptionKey []byte
}
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
@@ -298,6 +299,7 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
poolCount: options.PoolCount,
getWorkConnFn: options.GetWorkConnFn,
serverCfg: options.ServerCfg,
encryptionKey: options.EncryptionKey,
limiter: limiter,
xl: xl,
ctx: xlog.NewContext(ctx, xl),

View File

@@ -205,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
var rwc io.ReadWriteCloser = workConn
if pxy.cfg.Transport.UseEncryption {
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
if err != nil {
xl.Errorf("create encryption stream error: %v", err)
workConn.Close()

View File

@@ -113,8 +113,8 @@ type Service struct {
sshTunnelGateway *ssh.Gateway
// Verifies authentication based on selected method
authVerifier auth.Verifier
// Auth runtime and encryption materials
auth *auth.ServerAuth
tlsConfig *tls.Config
@@ -149,6 +149,11 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
}
}
authRuntime, err := auth.BuildServerAuth(&cfg.Auth)
if err != nil {
return nil, err
}
svr := &Service{
ctlManager: NewControlManager(),
pxyManager: proxy.NewManager(),
@@ -160,7 +165,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
},
sshTunnelListener: netpkg.NewInternalListener(),
httpVhostRouter: vhost.NewRouters(),
authVerifier: auth.NewAuthVerifier(cfg.Auth),
auth: authRuntime,
webServer: webServer,
tlsConfig: tlsConfig,
cfg: cfg,
@@ -586,7 +591,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
// Check auth.
authVerifier := svr.authVerifier
authVerifier := svr.auth.Verifier
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
authVerifier = auth.AlwaysPassVerifier
}
@@ -595,7 +600,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
}
// TODO(fatedier): use SessionContext
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg)
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg)
if err != nil {
xl.Warnf("create new controller error: %v", err)
// don't return detailed errors to client