mirror of
https://github.com/fatedier/frp.git
synced 2026-03-08 10:59:11 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0416caef71
|
|||
|
1004473e42
|
|||
|
f386996928
|
|||
|
4eb4b202c5
|
|||
|
ac5bdad507
|
|||
|
42f4ea7f87
|
|||
|
36e5ac094b
|
|||
|
72f79d3357
|
|||
|
e1f905f63f
|
|||
|
eb58f09268
|
|||
|
46955ffc80
|
|||
|
2d63296576
|
|||
|
a76ba823ee
|
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [fatedier]
|
||||
custom: ["https://afdian.com/a/fatedier"]
|
||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@@ -1,3 +0,0 @@
|
||||
### WHY
|
||||
|
||||
<!-- author to complete -->
|
||||
187
.github/workflows/build-all.yaml
vendored
Normal file
187
.github/workflows/build-all.yaml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
name: Build FRP Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '**'
|
||||
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
|
||||
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -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
|
||||
140
.github/workflows/release.yaml
vendored
Normal file
140
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
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.yml
|
||||
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: Organize release files
|
||||
run: |
|
||||
mkdir -p release_files
|
||||
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" \) -exec cp {} release_files/ \;
|
||||
ls -lh release_files/
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd release_files
|
||||
sha256sum * > sha256sum.txt
|
||||
cat sha256sum.txt
|
||||
|
||||
- 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 }}
|
||||
@@ -16,7 +16,9 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ 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)
|
||||
@@ -177,6 +177,6 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
||||
}
|
||||
}
|
||||
if len(addPxyNames) > 0 {
|
||||
xl.Infof("proxy added: %s", addPxyNames)
|
||||
xl.Infof("添加隧道: %s", addPxyNames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -92,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)
|
||||
@@ -123,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)
|
||||
|
||||
@@ -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
|
||||
authToken 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().StringVarP(&authToken, "token", "t", "", "authentication token of frpc (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 authToken is provided, fetch config from API
|
||||
if authToken != "" {
|
||||
err := runClientWithToken(authToken, 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 != "" {
|
||||
@@ -143,7 +158,7 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro
|
||||
return err
|
||||
}
|
||||
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "")
|
||||
}
|
||||
|
||||
func startService(
|
||||
@@ -152,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,
|
||||
@@ -177,3 +204,105 @@ 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"`
|
||||
}
|
||||
|
||||
func runClientWithToken(token string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
// Get API server address from environment variable
|
||||
apiServer := os.Getenv("LOLIA_API")
|
||||
if apiServer == "" {
|
||||
apiServer = "https://api.lolia.link"
|
||||
}
|
||||
|
||||
// Fetch config from API
|
||||
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config/%s", apiServer, token)
|
||||
// #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)
|
||||
}
|
||||
|
||||
15
go.mod
15
go.mod
@@ -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
31
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
12
pkg/util/banner/banner.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package banner
|
||||
|
||||
import "fmt"
|
||||
|
||||
func DisplayBanner() {
|
||||
fmt.Println(" __ ___ __________ ____ ________ ____")
|
||||
fmt.Println(" / / ____ / (_)___ _/ ____/ __ \\/ __ \\ / ____/ / / _/")
|
||||
fmt.Println(" / / / __ \\/ / / __ `/ /_ / /_/ / /_/ /_____/ / / / / / ")
|
||||
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
|
||||
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
|
||||
fmt.Println(" ")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
package version
|
||||
|
||||
var version = "0.66.0"
|
||||
var version = "LoliaFRP-CLI 0.66.0"
|
||||
|
||||
func Full() string {
|
||||
return version
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user