Compare commits

...

73 Commits

Author SHA1 Message Date
803e548f42 fix(version): update version to LoliaFRP-CLI 0.66.3 2026-01-14 00:03:50 +08:00
2dac44ac2e fix(config): support multiple configuration files for frpc 2026-01-14 00:03:18 +08:00
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
fatedier
0fe8f7a0b6 refactor: reorganize security policy into dedicated packag (#5088) 2025-12-05 16:26:09 +08:00
fatedier
2e2802ea13 refactor: use MessageSender interface for message transporter (#5083) 2025-12-02 11:22:48 +08:00
fatedier
c3821202b1 docs: remove zsxq section (#5077) 2025-11-26 23:55:54 +08:00
fatedier
15fd19a16d fix lint (#5068) 2025-11-18 01:11:44 +08:00
Krzysztof Bogacki
66973a03db Add exec value source type (#5050)
* config: introduce ExecSource value source

* auth: introduce OidcTokenSourceAuthProvider

* auth: use OidcTokenSourceAuthProvider if tokenSource config is present on the client

* cmd: allow exec token source only if CLI flag was passed
2025-11-18 00:20:21 +08:00
fatedier
f736d171ac rotate gold sponsor order periodically (#5067) 2025-11-18 00:09:37 +08:00
fatedier
b27b846971 config: add enabled field for individual proxy and visitor (#5048) 2025-11-06 14:05:03 +08:00
fatedier
e025843d3c vnet: add exponential backoff for failed reconnections (#5035) 2025-10-29 01:08:48 +08:00
fatedier
a75320ef2f update quic-go dependency from v0.53.0 to v0.55.0 (#5033) 2025-10-28 17:52:34 +08:00
fatedier
1cf325bb0c https: add load balancing group support (#5032) 2025-10-28 17:37:18 +08:00
fatedier
469097a549 update sponsor pic (#5031) 2025-10-28 16:08:29 +08:00
fatedier
2def23bb0b update sponsor (#5030) 2025-10-28 15:44:03 +08:00
Zachary Whaley
ee3cc4b14e Fix CloseNotifyConn.Close function (#5022)
The CloseNotifyConn.Close() function calls itself if the closeFlag is equal to 0 which would mean it would immediately swap the closeFlag value to 1 and never call closeFn.  It is unclear what the intent of this call to cc.Close() was but I assume it was meant to be a call to close the Conn object instead.
2025-10-17 10:53:43 +08:00
fatedier
e382676659 update README (#5001) 2025-09-26 12:26:08 +08:00
fatedier
b5e90c03a1 bump version to v0.65.0 and update release notes (#4998) 2025-09-25 20:11:17 +08:00
fatedier
b642a6323c update sponsors info (#4997) 2025-09-25 16:50:14 +08:00
juejinyuxitu
6561107945 chore: fix struct field name in comment (#4993)
Signed-off-by: juejinyuxitu <juejinyuxitu@outlook.com>
2025-09-25 16:47:33 +08:00
fatedier
abf4942e8a auth: enhance OIDC client with TLS and proxy configuration options (#4990) 2025-09-25 10:19:19 +08:00
Charlie Blevins
7cfa546b55 add proxy name label to the proxy_count prometheus metric (#4985)
* add proxy name label to the proxy_count metric

* undo label addition in favor of a new metric - this change should not break existing queries

* also register this new metric

* add type label to proxy_counts_detailed

* Update pkg/metrics/prometheus/server.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 00:18:49 +08:00
fatedier
0a798a7a69 update go version to 1.24 (#4960) 2025-08-27 15:10:36 +08:00
fatedier
604700cea5 update README (#4957) 2025-08-27 11:07:18 +08:00
fatedier
610e5ed479 improve yamux logging (#4952) 2025-08-25 17:52:58 +08:00
fatedier
80d3f332e1 xtcp: add configuration to disable assisted addresses in NAT traversal (#4951) 2025-08-25 15:52:52 +08:00
immomo808
14253afe2f remove quotes (#4938) 2025-08-15 16:11:06 +08:00
fatedier
024c334d9d Merge pull request #4928 from fatedier/xtcp
improve context and polling logic in xtcp visitor
2025-08-12 01:48:26 +08:00
fatedier
f795950742 bump version to v0.64.0 (#4924) 2025-08-10 23:11:50 +08:00
fatedier
024e4f5f1d improve random TLS certificate generation (#4923) 2025-08-10 22:59:28 +08:00
fatedier
dc3bc9182c update sponsor info (#4917) 2025-08-08 22:28:17 +08:00
fatedier
e6dacf3a67 Fix SSH tunnel gateway binding address issue #4900 (#4902)
- Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr
- This caused external connections to fail when proxyBindAddr was set to 127.0.0.1
- SSH tunnel gateway now correctly binds to bindAddr for external accessibility
- Update Release.md with bug fix description

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-28 15:19:56 +08:00
fatedier
7fe295f4f4 update golangci-lint version (#4897) 2025-07-25 17:10:32 +08:00
maguowei
c3bf952d8f fix webserver port not being released on frpc svr.Close() (#4896) 2025-07-24 10:16:44 +08:00
fatedier
f9065a6a78 add tokenSource support for auth configuration (#4865) 2025-07-03 13:17:21 +08:00
fatedier
61330d4d79 Update quic-go dependency from v0.48.2 to v0.53.0 (#4862)
- Update go.mod to use github.com/quic-go/quic-go v0.53.0
- Replace quic.Connection interface with *quic.Conn struct
- Replace quic.Stream interface with *quic.Stream struct
- Update all affected files to use new API:
  - pkg/util/net/conn.go: Update QuicStreamToNetConn function and wrapQuicStream struct
  - server/service.go: Update HandleQUICListener function parameter
  - client/visitor/xtcp.go: Update QUICTunnelSession struct field
  - client/connector.go: Update defaultConnectorImpl struct field

Fixes #4852

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-01 18:56:46 +08:00
fatedier
c777891f75 update .golangci.yml (#4848) 2025-06-25 11:40:23 +08:00
fatedier
43cf1688e4 update golangci-lint version (#4817) 2025-06-25 11:40:23 +08:00
fatedier
720c09c06b update test package (#4814) 2025-06-25 11:40:23 +08:00
fatedier
3fa76b72f3 add proxy protocol support for UDP proxies (#4810) 2025-06-25 11:40:23 +08:00
fatedier
8eb525a648 feat: support YAML merge in strict configuration mode (#4809) 2025-06-25 11:40:23 +08:00
scientificworld
077ba80ba3 fix: type error in server_plugin doc (#4799) 2025-06-25 11:40:23 +08:00
CrynTox
c99986fa28 build: add x64 openbsd (#4780)
Co-authored-by: CrynTox <>
2025-06-25 11:40:23 +08:00
fatedier
b41d8f8e40 update release notes (#4772) 2025-04-27 15:46:22 +08:00
fatedier
3c8d648ddc vnet: fix issues (#4771) 2025-04-27 15:46:22 +08:00
fatedier
27f66baf54 update feature gates doc (#4755) 2025-04-27 15:46:22 +08:00
122 changed files with 4027 additions and 814 deletions

View File

@@ -2,7 +2,7 @@ version: 2
jobs: jobs:
go-version-latest: go-version-latest:
docker: docker:
- image: cimg/go:1.23-node - image: cimg/go:1.24-node
resource_class: large resource_class: large
steps: steps:
- checkout - checkout

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

@@ -17,26 +17,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
cache: false cache: false
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v8
with: with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.61 version: v2.3
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
# skip-cache: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Make All - name: Make All
run: | run: |

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

@@ -1,4 +1,4 @@
name: "Close stale issues" name: "Close stale issues and PRs"
on: on:
schedule: schedule:
- cron: "20 0 * * *" - cron: "20 0 * * *"

3
.gitignore vendored
View File

@@ -39,3 +39,6 @@ client.key
# Cache # Cache
*.swp *.swp
# AI
CLAUDE.md

View File

@@ -1,139 +1,116 @@
service: version: "2"
golangci-lint-version: 1.61.x # use the fixed version to not introduce new linters unexpectedly
run: run:
concurrency: 4 concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 20m timeout: 20m
build-tags: build-tags:
- integ - integ
- integfuzz - integfuzz
linters: linters:
disable-all: true default: none
enable: enable:
- unused - asciicheck
- errcheck
- copyloopvar - copyloopvar
- errcheck
- gocritic - gocritic
- gofumpt - gosec
- goimports
- revive
- gosimple
- govet - govet
- ineffassign - ineffassign
- lll - lll
- makezero
- misspell - misspell
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- gci
- gosec
- asciicheck
- prealloc - prealloc
- predeclared - predeclared
- makezero - revive
fast: false - staticcheck
- unconvert
linters-settings: - unparam
errcheck: - unused
# report about not checking of errors in type assetions: `a := b.(MyStruct)`; settings:
# default is false: such cases aren't reported by default. errcheck:
check-type-assertions: false check-type-assertions: false
check-blank: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; gocritic:
# default is false: such cases aren't reported by default. disabled-checks:
check-blank: false - exitAfterDefer
govet: gosec:
# report about shadowed variables excludes:
disable: - G401
- shadow - G402
maligned: - G404
# print struct with more effective memory layout or not, false by default - G501
suggest-new: true - G115
misspell: - G204
# Correct spellings using locale preferences for US or UK. severity: low
# Default is to use a neutral variety of English. confidence: low
# Setting locale to US will correct the British spelling of 'colour' to 'color'. govet:
locale: US disable:
ignore-words: - shadow
- cancelled lll:
- marshalled line-length: 160
lll: tab-width: 1
# max line length, lines longer will be reported. Default is 120. misspell:
# '\t' is counted as 1 character by default, and can be changed with the tab-width option locale: US
line-length: 160 ignore-rules:
# tab width in spaces. Default to 1. - cancelled
tab-width: 1 - marshalled
gocritic: unparam:
disabled-checks: check-exported: false
- exitAfterDefer exclusions:
unused: generated: lax
check-exported: false presets:
unparam: - comments
# Inspect exported functions, default is false. Set to true if no external program/library imports your code. - common-false-positives
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - legacy
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations - std-error-handling
# with golangci-lint call it on a directory with the changed file. rules:
check-exported: false - linters:
gci: - errcheck
sections: - maligned
- standard path: _test\.go$|^tests/|^samples/
- default - linters:
- prefix(github.com/fatedier/frp/) - revive
gosec: - staticcheck
severity: "low" text: use underscores in Go names
confidence: "low" - linters:
excludes: - revive
- G401 text: unused-parameter
- G402 - linters:
- G404 - revive
- G501 text: "avoid meaningless package names"
- G115 # integer overflow conversion - linters:
- unparam
text: is always false
paths:
- .*\.pb\.go
- .*\.gen\.go
- genfiles$
- vendor$
- bin$
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/fatedier/frp/)
exclusions:
generated: lax
paths:
- .*\.pb\.go
- .*\.gen\.go
- genfiles$
- vendor$
- bin$
- third_party$
- builtin$
- examples$
issues: issues:
# List of regexps of issue texts to exclude, empty list by default. max-issues-per-linter: 0
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
# exclude:
# - composite literal uses unkeyed fields
exclude-rules:
# Exclude some linters from running on test files.
- path: _test\.go$|^tests/|^samples/
linters:
- errcheck
- maligned
- linters:
- revive
- stylecheck
text: "use underscores in Go names"
- linters:
- revive
text: "unused-parameter"
- linters:
- unparam
text: "is always false"
exclude-dirs:
- genfiles$
- vendor$
- bin$
exclude-files:
- ".*\\.pb\\.go"
- ".*\\.gen\\.go"
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0 max-same-issues: 0

View File

@@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on export GO111MODULE=on
LDFLAGS := -s -w LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64 os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
all: build all: build

View File

@@ -16,16 +16,46 @@ frp is an open source project with its ongoing development made possible entirel
<p align="center"> <p align="center">
<a href="https://jb.gg/frp" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg"> <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> </a>
</p> </p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center"> <p align="center">
<a href="https://github.com/beclab/Olares" target="_blank"> <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"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<div align="center">
## Recall.ai - API for meeting recordings
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<p align="center">
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
<br>
<b>Requestly - Free & Open-Source alternative to Postman</b>
<br>
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
</a>
</p>
<p align="center">
<a href="https://go.warp.dev/frp" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
<br>
<b>Warp, built for collaborating with AI Agents</b>
<br>
<sub>Available for macOS, Linux and Windows</sub>
</a> </a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@@ -502,7 +532,7 @@ name = "ssh"
type = "tcp" type = "tcp"
localIP = "127.0.0.1" localIP = "127.0.0.1"
localPort = 22 localPort = 22
remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}" remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }}
``` ```
With the config above, variables can be passed into `frpc` program like this: With the config above, variables can be passed into `frpc` program like this:
@@ -612,6 +642,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b
Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation
##### Token Source
frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported.
**File-based token source:**
```toml
# frpc.toml
auth.method = "token"
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "/path/to/token/file"
```
The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons.
#### OIDC Authentication #### OIDC Authentication
When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used.
@@ -1025,7 +1070,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`.
#### Proxy Protocol #### Proxy Protocol
frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP. frp supports Proxy Protocol to send user's real IP to local services.
Here is an example for https service: Here is an example for https service:
@@ -1283,9 +1328,7 @@ frp supports feature gates to enable or disable experimental features. This allo
To enable an experimental feature, add the feature gate to your configuration: To enable an experimental feature, add the feature gate to your configuration:
```toml ```toml
featureGates = { featureGates = { VirtualNet = true }
VirtualNet = true
}
``` ```
### Feature Lifecycle ### Feature Lifecycle

View File

@@ -18,16 +18,45 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<p align="center"> <p align="center">
<a href="https://jb.gg/frp" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg"> <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> </a>
</p> </p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center"> <p align="center">
<a href="https://github.com/beclab/Olares" target="_blank"> <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"> <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<div align="center">
## Recall.ai - API for meeting recordings
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<p align="center">
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
<br>
<b>Requestly - Free & Open-Source alternative to Postman</b>
<br>
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
</a>
</p>
<p align="center">
<a href="https://go.warp.dev/frp" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
<br>
<b>Warp, built for collaborating with AI Agents</b>
<br>
<sub>Available for macOS, Linux and Windows</sub>
</a> </a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@@ -102,9 +131,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
### 知识星球
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg)

View File

@@ -1,8 +1,13 @@
### Notes ## Features
* **Feature Gates Introduced:** This version introduces a new experimental mechanism called Feature Gates. This allows users to enable or disable specific experimental features before they become generally available. Feature gates can be configured in the `featureGates` map within the configuration file. * 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.
* **VirtualNet Feature Gate:** The first available feature gate is `VirtualNet`, which enables the experimental Virtual Network functionality (currently in Alpha stage). * 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.
### Features ## Improvements
* **Virtual Network (VirtualNet):** Introduce experimental virtual network capabilities (Alpha). This allows creating a TUN device managed by frp, enabling Layer 3 connectivity between different clients within the frp network. Requires root/admin privileges and is currently supported on Linux and macOS. Configuration is done via the `virtualNet` section and the `virtual_net` plugin. Enable with feature gate `VirtualNet`. **Note: As an Alpha feature, configuration details may change in future releases.** * **VirtualNet**: Implemented intelligent reconnection with exponential backoff. When connection errors occur repeatedly, the reconnect interval increases from 60s to 300s (max), reducing unnecessary reconnection attempts. Normal disconnections still reconnect quickly at 10s intervals.
## Fixes
* Fix deadlock issue when TCP connection is closed. Previously, sending messages could block forever if the connection handler had already stopped.

View File

@@ -92,7 +92,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
log.Warnf("reload frpc proxy config error: %s", res.Msg) log.Warnf("reload frpc proxy config error: %s", res.Msg)
return return
} }
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil { if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warnf("reload frpc proxy config error: %s", res.Msg) log.Warnf("reload frpc proxy config error: %s", res.Msg)
@@ -165,9 +165,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
res StatusResp = make(map[string][]ProxyStatusResp) res StatusResp = make(map[string][]ProxyStatusResp)
) )
log.Infof("Http request [/api/status]") log.Infof("http request [/api/status]")
defer func() { defer func() {
log.Infof("Http response [/api/status]") log.Infof("http response [/api/status]")
buf, _ = json.Marshal(&res) buf, _ = json.Marshal(&res)
_, _ = w.Write(buf) _, _ = w.Write(buf)
}() }()
@@ -198,9 +198,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
log.Infof("Http get request [/api/config]") log.Infof("http get request [/api/config]")
defer func() { defer func() {
log.Infof("Http get response [/api/config], code [%d]", res.Code) log.Infof("http get response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))
@@ -228,9 +228,9 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
log.Infof("Http put request [/api/config]") log.Infof("http put request [/api/config]")
defer func() { defer func() {
log.Infof("Http put response [/api/config], code [%d]", res.Code) log.Infof("http put response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code) w.WriteHeader(res.Code)
if len(res.Msg) > 0 { if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg)) _, _ = w.Write([]byte(res.Msg))

View File

@@ -17,7 +17,6 @@ package client
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"io"
"net" "net"
"strconv" "strconv"
"strings" "strings"
@@ -48,7 +47,7 @@ type defaultConnectorImpl struct {
cfg *v1.ClientCommonConfig cfg *v1.ClientCommonConfig
muxSession *fmux.Session muxSession *fmux.Session
quicConn quic.Connection quicConn *quic.Conn
closeOnce sync.Once closeOnce sync.Once
} }
@@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard // Use trace level for yamux logs
fmuxCfg.LogOutput = xlog.NewTraceWriter(xl)
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Client(conn, fmuxCfg) session, err := fmux.Client(conn, fmuxCfg)
if err != nil { if err != nil {

View File

@@ -16,7 +16,9 @@ package client
import ( import (
"context" "context"
"fmt"
"net" "net"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -43,8 +45,8 @@ type SessionContext struct {
Conn net.Conn Conn net.Conn
// Indicates whether the connection is encrypted. // Indicates whether the connection is encrypted.
ConnEncrypted bool ConnEncrypted bool
// Sets authentication based on selected method // Auth runtime used for login, heartbeats, and encryption.
AuthSetter auth.Setter Auth *auth.ClientAuth
// Connector is used to create new connections, which could be real TCP connections or virtual streams. // Connector is used to create new connections, which could be real TCP connections or virtual streams.
Connector Connector Connector Connector
// Virtual net controller // Virtual net controller
@@ -91,7 +93,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
ctl.lastPong.Store(time.Now()) ctl.lastPong.Store(time.Now())
if sessionCtx.ConnEncrypted { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -100,9 +102,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
} }
ctl.registerMsgHandlers() ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController) ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController) ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
return ctl, nil return ctl, nil
@@ -133,7 +135,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
m := &msg.NewWorkConn{ m := &msg.NewWorkConn{
RunID: ctl.sessionCtx.RunID, 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) xl.Warnf("error during NewWorkConn authentication: %v", err)
workConn.Close() workConn.Close()
return return
@@ -167,9 +169,44 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
// Start a new proxy handler if no error got // Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
if err != nil { if err != nil {
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err) xl.Warnf("[%s] 启动失败: %v", inMsg.ProxyName, err)
} else { } 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)
}
}
} }
} }
@@ -189,7 +226,7 @@ func (ctl *Control) handlePong(m msg.Message) {
inMsg := m.(*msg.Pong) inMsg := m.(*msg.Pong)
if inMsg.Error != "" { if inMsg.Error != "" {
xl.Errorf("Pong message contains error: %s", inMsg.Error) xl.Errorf("pong message contains error: %s", inMsg.Error)
ctl.closeSession() ctl.closeSession()
return return
} }
@@ -243,7 +280,7 @@ func (ctl *Control) heartbeatWorker() {
sendHeartBeat := func() (bool, error) { sendHeartBeat := func() (bool, error) {
xl.Debugf("send heartbeat to server") xl.Debugf("send heartbeat to server")
pingMsg := &msg.Ping{} 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) xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
return false, err return false, err
} }
@@ -276,10 +313,12 @@ func (ctl *Control) heartbeatWorker() {
} }
func (ctl *Control) worker() { func (ctl *Control) worker() {
xl := ctl.xl
go ctl.heartbeatWorker() go ctl.heartbeatWorker()
go ctl.msgDispatcher.Run() go ctl.msgDispatcher.Run()
<-ctl.msgDispatcher.Done() <-ctl.msgDispatcher.Done()
xl.Debugf("control message dispatcher exited")
ctl.closeSession() ctl.closeSession()
ctl.pm.Close() ctl.pm.Close()

View File

@@ -20,13 +20,11 @@ import (
"net" "net"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
libnet "github.com/fatedier/golib/net" libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/config/types"
@@ -35,6 +33,7 @@ import (
plugin "github.com/fatedier/frp/pkg/plugin/client" plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet" "github.com/fatedier/frp/pkg/vnet"
) )
@@ -58,6 +57,7 @@ func NewProxy(
ctx context.Context, ctx context.Context,
pxyConf v1.ProxyConfigurer, pxyConf v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
encryptionKey []byte,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller, vnetController *vnet.Controller,
) (pxy Proxy) { ) (pxy Proxy) {
@@ -70,6 +70,7 @@ func NewProxy(
baseProxy := BaseProxy{ baseProxy := BaseProxy{
baseCfg: pxyConf.GetBaseConfig(), baseCfg: pxyConf.GetBaseConfig(),
clientCfg: clientCfg, clientCfg: clientCfg,
encryptionKey: encryptionKey,
limiter: limiter, limiter: limiter,
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController, vnetController: vnetController,
@@ -87,6 +88,7 @@ func NewProxy(
type BaseProxy struct { type BaseProxy struct {
baseCfg *v1.ProxyBaseConfig baseCfg *v1.ProxyBaseConfig
clientCfg *v1.ClientCommonConfig clientCfg *v1.ClientCommonConfig
encryptionKey []byte
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
vnetController *vnet.Controller vnetController *vnet.Controller
limiter *rate.Limiter limiter *rate.Limiter
@@ -130,7 +132,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
return return
} }
} }
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token)) pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
} }
// Common handler for tcp work connections. // Common handler for tcp work connections.
@@ -176,24 +178,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
} }
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
h := &pp.Header{ // Use the common proxy protocol builder function
Command: pp.PROXY, header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
SourceAddr: connInfo.SrcAddr, connInfo.ProxyProtocolHeader = header
DestinationAddr: connInfo.DstAddr,
}
if strings.Contains(m.SrcAddr, ".") {
h.TransportProtocol = pp.TCPv4
} else {
h.TransportProtocol = pp.TCPv6
}
if baseCfg.Transport.ProxyProtocolVersion == "v1" {
h.Version = 1
} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
h.Version = 2
}
connInfo.ProxyProtocolHeader = h
} }
connInfo.Conn = remote connInfo.Conn = remote
connInfo.UnderlyingConn = workConn connInfo.UnderlyingConn = workConn

View File

@@ -40,7 +40,8 @@ type Manager struct {
closed bool closed bool
mu sync.RWMutex mu sync.RWMutex
clientCfg *v1.ClientCommonConfig encryptionKey []byte
clientCfg *v1.ClientCommonConfig
ctx context.Context ctx context.Context
} }
@@ -48,6 +49,7 @@ type Manager struct {
func NewManager( func NewManager(
ctx context.Context, ctx context.Context,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
encryptionKey []byte,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller, vnetController *vnet.Controller,
) *Manager { ) *Manager {
@@ -56,6 +58,7 @@ func NewManager(
msgTransporter: msgTransporter, msgTransporter: msgTransporter,
vnetController: vnetController, vnetController: vnetController,
closed: false, closed: false,
encryptionKey: encryptionKey,
clientCfg: clientCfg, clientCfg: clientCfg,
ctx: ctx, ctx: ctx,
} }
@@ -156,14 +159,14 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
} }
} }
if len(delPxyNames) > 0 { if len(delPxyNames) > 0 {
xl.Infof("proxy removed: %s", delPxyNames) xl.Infof("隧道移除: %s", delPxyNames)
} }
addPxyNames := make([]string, 0) addPxyNames := make([]string, 0)
for _, cfg := range proxyCfgs { for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok { 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 { if pm.inWorkConnCallback != nil {
pxy.SetInWorkConnCallback(pm.inWorkConnCallback) pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
} }
@@ -174,6 +177,6 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
} }
} }
if len(addPxyNames) > 0 { 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, ctx context.Context,
cfg v1.ProxyConfigurer, cfg v1.ProxyConfigurer,
clientCfg *v1.ClientCommonConfig, clientCfg *v1.ClientCommonConfig,
encryptionKey []byte,
eventHandler event.Handler, eventHandler event.Handler,
msgTransporter transport.MessageTransporter, msgTransporter transport.MessageTransporter,
vnetController *vnet.Controller, vnetController *vnet.Controller,
@@ -122,7 +123,7 @@ func NewWrapper(
xl.Tracef("enable health check monitor") 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 return pw
} }

View File

@@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
}) })
} }
if pxy.cfg.Transport.UseEncryption { 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 { if err != nil {
conn.Close() conn.Close()
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
@@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnReaderFn(workConn, readCh) go workConnReaderFn(workConn, readCh)
go heartbeatFn(sendCh) go heartbeatFn(sendCh)
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize)) udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
} }

View File

@@ -90,7 +90,7 @@ func (pxy *UDPProxy) Close() {
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl 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 // close resources related with old workConn
pxy.Close() pxy.Close()
@@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
}) })
} }
if pxy.cfg.Transport.UseEncryption { 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 { if err != nil {
conn.Close() conn.Close()
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
@@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
go workConnSenderFn(pxy.workConn, pxy.sendCh) go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh) go workConnReaderFn(pxy.workConn, pxy.readCh)
go heartbeatFn(pxy.sendCh) go heartbeatFn(pxy.sendCh)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize))
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
} }

View File

@@ -64,11 +64,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
} }
xl.Tracef("nathole prepare start") xl.Tracef("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
// Prepare NAT traversal options
var opts nathole.PrepareOptions
if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs {
opts.DisableAssistedAddrs = true
}
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts)
if err != nil { if err != nil {
xl.Warnf("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
defer prepareResult.ListenConn.Close() defer prepareResult.ListenConn.Close()

View File

@@ -31,6 +31,7 @@ import (
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/policy/security"
httppkg "github.com/fatedier/frp/pkg/util/http" httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
@@ -64,6 +65,8 @@ type ServiceOptions struct {
ProxyCfgs []v1.ProxyConfigurer ProxyCfgs []v1.ProxyConfigurer
VisitorCfgs []v1.VisitorConfigurer VisitorCfgs []v1.VisitorConfigurer
UnsafeFeatures *security.UnsafeFeatures
// ConfigFilePath is the path to the configuration file used to initialize. // ConfigFilePath is the path to the configuration file used to initialize.
// If it is empty, it means that the configuration file is not used for initialization. // If it is empty, it means that the configuration file is not used for initialization.
// It may be initialized using command line parameters or called directly. // It may be initialized using command line parameters or called directly.
@@ -88,13 +91,16 @@ type ServiceOptions struct {
} }
// setServiceOptionsDefault sets the default values for ServiceOptions. // setServiceOptionsDefault sets the default values for ServiceOptions.
func setServiceOptionsDefault(options *ServiceOptions) { func setServiceOptionsDefault(options *ServiceOptions) error {
if options.Common != nil { if options.Common != nil {
options.Common.Complete() if err := options.Common.Complete(); err != nil {
return err
}
} }
if options.ConnectorCreator == nil { if options.ConnectorCreator == nil {
options.ConnectorCreator = NewConnector options.ConnectorCreator = NewConnector
} }
return nil
} }
// Service is the client service that connects to frps and provides proxy services. // Service is the client service that connects to frps and provides proxy services.
@@ -105,8 +111,8 @@ type Service struct {
// Uniq id got from frps, it will be attached to loginMsg. // Uniq id got from frps, it will be attached to loginMsg.
runID string runID string
// Sets authentication based on selected method // Auth runtime and encryption materials
authSetter auth.Setter auth *auth.ClientAuth
// web server for admin UI and apis // web server for admin UI and apis
webServer *httppkg.Server webServer *httppkg.Server
@@ -119,6 +125,8 @@ type Service struct {
visitorCfgs []v1.VisitorConfigurer visitorCfgs []v1.VisitorConfigurer
clientSpec *msg.ClientSpec clientSpec *msg.ClientSpec
unsafeFeatures *security.UnsafeFeatures
// The configuration file used to initialize this client, or an empty // The configuration file used to initialize this client, or an empty
// string if no configuration file was used. // string if no configuration file was used.
configFilePath string configFilePath string
@@ -134,7 +142,9 @@ type Service struct {
} }
func NewService(options ServiceOptions) (*Service, error) { func NewService(options ServiceOptions) (*Service, error) {
setServiceOptionsDefault(&options) if err := setServiceOptionsDefault(&options); err != nil {
return nil, err
}
var webServer *httppkg.Server var webServer *httppkg.Server
if options.Common.WebServer.Port > 0 { if options.Common.WebServer.Port > 0 {
@@ -144,12 +154,19 @@ func NewService(options ServiceOptions) (*Service, error) {
} }
webServer = ws webServer = ws
} }
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
if err != nil {
return nil, err
}
s := &Service{ s := &Service{
ctx: context.Background(), ctx: context.Background(),
authSetter: auth.NewAuthSetter(options.Common.Auth), auth: authRuntime,
webServer: webServer, webServer: webServer,
common: options.Common, common: options.Common,
configFilePath: options.ConfigFilePath, configFilePath: options.ConfigFilePath,
unsafeFeatures: options.UnsafeFeatures,
proxyCfgs: options.ProxyCfgs, proxyCfgs: options.ProxyCfgs,
visitorCfgs: options.VisitorCfgs, visitorCfgs: options.VisitorCfgs,
clientSpec: options.ClientSpec, clientSpec: options.ClientSpec,
@@ -202,7 +219,7 @@ func (svr *Service) Run(ctx context.Context) error {
if svr.ctl == nil { if svr.ctl == nil {
cancelCause := cancelErr{} cancelCause := cancelErr{}
_ = errors.As(context.Cause(svr.ctx), &cancelCause) _ = errors.As(context.Cause(svr.ctx), &cancelCause)
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() go svr.keepControllerWorking()
@@ -279,7 +296,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
} }
// Add auth // Add auth
if err = svr.authSetter.SetLogin(loginMsg); err != nil { if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
return return
} }
@@ -303,7 +320,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
svr.runID = loginRespMsg.RunID svr.runID = loginRespMsg.RunID
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.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 return
} }
@@ -311,10 +328,10 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
xl := xlog.FromContextSafe(svr.ctx) xl := xlog.FromContextSafe(svr.ctx)
loginFunc := func() (bool, error) { loginFunc := func() (bool, error) {
xl.Infof("try to connect to server...") xl.Infof("尝试连接到服务器...")
conn, connector, err := svr.login() conn, connector, err := svr.login()
if err != nil { if err != nil {
xl.Warnf("connect to server error: %v", err) xl.Warnf("连接服务器错误: %v", err)
if firstLoginExit { if firstLoginExit {
svr.cancel(cancelErr{Err: err}) svr.cancel(cancelErr{Err: err})
} }
@@ -325,23 +342,22 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
proxyCfgs := svr.proxyCfgs proxyCfgs := svr.proxyCfgs
visitorCfgs := svr.visitorCfgs visitorCfgs := svr.visitorCfgs
svr.cfgMu.RUnlock() svr.cfgMu.RUnlock()
connEncrypted := true
if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" { connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
connEncrypted = false
}
sessionCtx := &SessionContext{ sessionCtx := &SessionContext{
Common: svr.common, Common: svr.common,
RunID: svr.runID, RunID: svr.runID,
Conn: conn, Conn: conn,
ConnEncrypted: connEncrypted, ConnEncrypted: connEncrypted,
AuthSetter: svr.authSetter, Auth: svr.auth,
Connector: connector, Connector: connector,
VnetController: svr.vnetController, VnetController: svr.vnetController,
} }
ctl, err := NewControl(svr.ctx, sessionCtx) ctl, err := NewControl(svr.ctx, sessionCtx)
if err != nil { if err != nil {
conn.Close() conn.Close()
xl.Errorf("NewControl error: %v", err) xl.Errorf("new control error: %v", err)
return false, err return false, err
} }
ctl.SetInWorkConnCallback(svr.handleWorkConnCb) ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
@@ -399,6 +415,10 @@ func (svr *Service) stop() {
svr.ctl.GracefulClose(svr.gracefulShutdownDuration) svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
svr.ctl = nil svr.ctl = nil
} }
if svr.webServer != nil {
svr.webServer.Close()
svr.webServer = nil
}
} }
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {

View File

@@ -15,6 +15,7 @@
package visitor package visitor
import ( import (
"fmt"
"io" "io"
"net" "net"
"strconv" "strconv"
@@ -81,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() {
func (sv *STCPVisitor) handleConn(userConn net.Conn) { func (sv *STCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
defer userConn.Close() var tunnelErr error
defer func() {
// If there was an error and connection supports CloseWithError, use it
if tunnelErr != nil {
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
_ = eConn.CloseWithError(tunnelErr)
return
}
}
userConn.Close()
}()
xl.Debugf("get a new stcp user connection") xl.Debugf("get a new stcp user connection")
visitorConn, err := sv.helper.ConnectServer() visitorConn, err := sv.helper.ConnectServer()
if err != nil { if err != nil {
tunnelErr = err
return return
} }
defer visitorConn.Close() defer visitorConn.Close()
@@ -102,6 +114,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
err = msg.WriteMsg(visitorConn, newVisitorConnMsg) err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil { if err != nil {
xl.Warnf("send newVisitorConnMsg to server error: %v", err) xl.Warnf("send newVisitorConnMsg to server error: %v", err)
tunnelErr = err
return return
} }
@@ -110,12 +123,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil { if err != nil {
xl.Warnf("get newVisitorConnRespMsg error: %v", err) xl.Warnf("get newVisitorConnRespMsg error: %v", err)
tunnelErr = err
return return
} }
_ = visitorConn.SetReadDeadline(time.Time{}) _ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" { if newVisitorConnRespMsg.Error != "" {
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
return return
} }
@@ -125,6 +140,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
tunnelErr = err
return return
} }
} }

View File

@@ -71,7 +71,7 @@ func NewVisitor(
Name: cfg.GetBaseConfig().Name, Name: cfg.GetBaseConfig().Name,
Ctx: ctx, Ctx: ctx,
VnetController: helper.VNetController(), VnetController: helper.VNetController(),
HandleConn: func(conn net.Conn) { SendConnToVisitor: func(conn net.Conn) {
_ = baseVisitor.AcceptConn(conn) _ = baseVisitor.AcceptConn(conn)
}, },
}, },

View File

@@ -145,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
return return
case <-ticker.C: case <-ticker.C:
xl.Debugf("keepTunnelOpenWorker try to check tunnel...") xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
conn, err := sv.getTunnelConn() conn, err := sv.getTunnelConn(sv.ctx)
if err != nil { if err != nil {
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err) xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
_ = sv.retryLimiter.Wait(sv.ctx) _ = sv.retryLimiter.Wait(sv.ctx)
@@ -161,9 +161,17 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
func (sv *XTCPVisitor) handleConn(userConn net.Conn) { func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
isConnTransfered := false isConnTransferred := false
var tunnelErr error
defer func() { defer func() {
if !isConnTransfered { if !isConnTransferred {
// If there was an error and connection supports CloseWithError, use it
if tunnelErr != nil {
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
_ = eConn.CloseWithError(tunnelErr)
return
}
}
userConn.Close() userConn.Close()
} }
}() }()
@@ -172,7 +180,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
// Open a tunnel connection to the server. If there is already a successful hole-punching connection, // Open a tunnel connection to the server. If there is already a successful hole-punching connection,
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout. // it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
ctx := context.Background() ctx := sv.ctx
if sv.cfg.FallbackTo != "" { if sv.cfg.FallbackTo != "" {
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond) timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
defer cancel() defer cancel()
@@ -181,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
tunnelConn, err := sv.openTunnel(ctx) tunnelConn, err := sv.openTunnel(ctx)
if err != nil { if err != nil {
xl.Errorf("open tunnel error: %v", err) xl.Errorf("open tunnel error: %v", err)
tunnelErr = err
// no fallback, just return // no fallback, just return
if sv.cfg.FallbackTo == "" { if sv.cfg.FallbackTo == "" {
return return
@@ -191,7 +201,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
return return
} }
isConnTransfered = true isConnTransferred = true
return return
} }
@@ -200,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
tunnelErr = err
return return
} }
} }
@@ -219,40 +230,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
// openTunnel will open a tunnel connection to the target server. // openTunnel will open a tunnel connection to the target server.
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) { func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
xl := xlog.FromContextSafe(sv.ctx) xl := xlog.FromContextSafe(sv.ctx)
ticker := time.NewTicker(500 * time.Millisecond) ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer ticker.Stop() defer cancel()
timeoutC := time.After(20 * time.Second) timer := time.NewTimer(0)
immediateTrigger := make(chan struct{}, 1) defer timer.Stop()
defer close(immediateTrigger)
immediateTrigger <- struct{}{}
for { for {
select { select {
case <-sv.ctx.Done(): case <-sv.ctx.Done():
return nil, sv.ctx.Err() return nil, sv.ctx.Err()
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() if errors.Is(ctx.Err(), context.DeadlineExceeded) {
case <-immediateTrigger: return nil, fmt.Errorf("open tunnel timeout")
conn, err = sv.getTunnelConn()
case <-ticker.C:
conn, err = sv.getTunnelConn()
case <-timeoutC:
return nil, fmt.Errorf("open tunnel timeout")
}
if err != nil {
if err != ErrNoTunnelSession {
xl.Warnf("get tunnel connection error: %v", err)
} }
continue return nil, ctx.Err()
case <-timer.C:
conn, err = sv.getTunnelConn(ctx)
if err != nil {
if !errors.Is(err, ErrNoTunnelSession) {
xl.Warnf("get tunnel connection error: %v", err)
}
timer.Reset(500 * time.Millisecond)
continue
}
return conn, nil
} }
return conn, nil
} }
} }
func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) { func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
conn, err := sv.session.OpenConn(sv.ctx) conn, err := sv.session.OpenConn(ctx)
if err == nil { if err == nil {
return conn, nil return conn, nil
} }
@@ -279,11 +287,19 @@ func (sv *XTCPVisitor) makeNatHole() {
} }
xl.Tracef("nathole prepare start") xl.Tracef("nathole prepare start")
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
// Prepare NAT traversal options
var opts nathole.PrepareOptions
if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs {
opts.DisableAssistedAddrs = true
}
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts)
if err != nil { if err != nil {
xl.Warnf("nathole prepare error: %v", err) xl.Warnf("nathole prepare error: %v", err)
return return
} }
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
@@ -398,7 +414,7 @@ func (ks *KCPTunnelSession) Close() {
} }
type QUICTunnelSession struct { type QUICTunnelSession struct {
session quic.Connection session *quic.Conn
listenConn *net.UDPConn listenConn *net.UDPConn
mu sync.RWMutex mu sync.RWMutex

View File

@@ -54,7 +54,11 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
Use: name, Use: name,
Short: short, Short: short,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if len(cfgFiles) == 0 || cfgFiles[0] == "" {
fmt.Println("frpc: the configuration file is not specified")
os.Exit(1)
}
cfg, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -48,10 +48,22 @@ var natholeDiscoveryCmd = &cobra.Command{
Short: "Discover nathole information from stun server", Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters // ignore error here, because we can use command line pameters
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) var cfg *v1.ClientCommonConfig
if err != nil { if len(cfgFiles) > 0 && cfgFiles[0] != "" {
_, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode)
if err != nil {
cfg = &v1.ClientCommonConfig{}
if err := cfg.Complete(); err != nil {
fmt.Printf("failed to complete config: %v\n", err)
os.Exit(1)
}
}
} else {
cfg = &v1.ClientCommonConfig{} cfg = &v1.ClientCommonConfig{}
cfg.Complete() if err := cfg.Complete(); err != nil {
fmt.Printf("failed to complete config: %v\n", err)
os.Exit(1)
}
} }
if natHoleSTUNServer != "" { if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer cfg.NatHoleSTUNServer = natHoleSTUNServer

View File

@@ -24,6 +24,7 @@ import (
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
) )
var proxyTypes = []v1.ProxyType{ var proxyTypes = []v1.ProxyType{
@@ -73,8 +74,14 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
Use: name, Use: name,
Short: fmt.Sprintf("Run frpc with a single %s proxy", name), Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
clientCfg.Complete() if err := clientCfg.Complete(); err != nil {
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err)
os.Exit(1)
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
validator := validation.NewConfigValidator(unsafeFeatures)
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -85,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "") err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "", "", "")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@@ -99,8 +106,13 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
Use: "visitor", Use: "visitor",
Short: fmt.Sprintf("Run frpc with a single %s visitor", name), Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
clientCfg.Complete() if err := clientCfg.Complete(); err != nil {
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err)
os.Exit(1)
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
validator := validation.NewConfigValidator(unsafeFeatures)
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -111,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "") err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "", "", "")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@@ -16,11 +16,15 @@ package sub
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -31,23 +35,32 @@ import (
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/featuregate" "github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/banner"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
) )
var ( var (
cfgFile string cfgFiles []string
cfgDir string cfgDir string
showVersion bool showVersion bool
strictConfigMode bool strictConfigMode bool
allowUnsafe []string
authTokens []string
bannerDisplayed bool
) )
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") rootCmd.PersistentFlags().StringSliceVarP(&cfgFiles, "config", "c", []string{"./frpc.ini"}, "config files of frpc (support multiple files)")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") 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(&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().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, ", ")))
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -59,15 +72,32 @@ var rootCmd = &cobra.Command{
return nil return nil
} }
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. // 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 != "" { if cfgDir != "" {
_ = runMultipleClients(cfgDir) _ = runMultipleClients(cfgDir, unsafeFeatures)
return nil
}
// If multiple config files are specified, run one frpc service for each file
if len(cfgFiles) > 1 {
_ = runMultipleClientsFromFiles(cfgFiles, unsafeFeatures)
return nil return nil
} }
// Do not show command usage here. // Do not show command usage here.
err := runClient(cfgFile) err := runClient(cfgFiles[0], unsafeFeatures)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@@ -76,7 +106,7 @@ var rootCmd = &cobra.Command{
}, },
} }
func runMultipleClients(cfgDir string) error { func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
var wg sync.WaitGroup var wg sync.WaitGroup
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error { err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() { if err != nil || d.IsDir() {
@@ -86,7 +116,7 @@ func runMultipleClients(cfgDir string) error {
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
go func() { go func() {
defer wg.Done() defer wg.Done()
err := runClient(path) err := runClient(path, unsafeFeatures)
if err != nil { if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path) fmt.Printf("frpc service error for config file [%s]\n", path)
} }
@@ -97,6 +127,30 @@ func runMultipleClients(cfgDir string) error {
return err return err
} }
func runMultipleClientsFromFiles(cfgFiles []string, unsafeFeatures *security.UnsafeFeatures) error {
var wg sync.WaitGroup
// Display banner first
banner.DisplayBanner()
bannerDisplayed = true
log.Infof("检测到 %d 个配置文件,将启动多个 frpc 服务实例", len(cfgFiles))
for i, cfgFile := range cfgFiles {
wg.Add(1)
// Add a small delay to avoid log output mixing
time.Sleep(100 * time.Millisecond)
go func(index int, path string) {
defer wg.Done()
err := runClient(path, unsafeFeatures)
if err != nil {
fmt.Printf("\n配置文件 [%s] 启动失败: %v\n", path, err)
}
}(i, cfgFile)
}
wg.Wait()
return nil
}
func Execute() { func Execute() {
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc) rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
@@ -111,7 +165,7 @@ func handleTermSignal(svr *client.Service) {
svr.GracefulClose(500 * time.Millisecond) svr.GracefulClose(500 * time.Millisecond)
} }
func runClient(cfgFilePath string) error { func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
if err != nil { if err != nil {
return err return err
@@ -127,32 +181,48 @@ func runClient(cfgFilePath string) error {
} }
} }
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs) warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }
if err != nil { if err != nil {
return err return err
} }
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "")
} }
func startService( func startService(
cfg *v1.ClientCommonConfig, cfg *v1.ClientCommonConfig,
proxyCfgs []v1.ProxyConfigurer, proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer, visitorCfgs []v1.VisitorConfigurer,
unsafeFeatures *security.UnsafeFeatures,
cfgFile string, cfgFile string,
nodeName string,
tunnelRemark string,
) error { ) error {
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
// Display banner only once before starting the first service
if !bannerDisplayed {
banner.DisplayBanner()
bannerDisplayed = true
}
// Display node information if available
if nodeName != "" {
log.Info("已获取到配置文件", "隧道名称", tunnelRemark, "使用节点", nodeName)
}
if cfgFile != "" { if cfgFile != "" {
log.Infof("start frpc service for config file [%s]", cfgFile) log.Infof("启动 frpc 服务 [%s]", cfgFile)
defer log.Infof("frpc service for config file [%s] stopped", cfgFile) defer log.Infof("frpc 服务 [%s] 已停止", cfgFile)
} }
svr, err := client.NewService(client.ServiceOptions{ svr, err := client.NewService(client.ServiceOptions{
Common: cfg, Common: cfg,
ProxyCfgs: proxyCfgs, ProxyCfgs: proxyCfgs,
VisitorCfgs: visitorCfgs, VisitorCfgs: visitorCfgs,
UnsafeFeatures: unsafeFeatures,
ConfigFilePath: cfgFile, ConfigFilePath: cfgFile,
}) })
if err != nil { if err != nil {
@@ -166,3 +236,187 @@ func startService(
} }
return svr.Run(context.Background()) 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)
}
// If we have multiple different tokens, start one service for each token group
if len(tokenToIDs) > 1 {
return runMultipleClientsWithTokens(tokenToIDs, unsafeFeatures)
}
// 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 runMultipleClientsWithTokens(tokenToIDs map[string][]string, unsafeFeatures *security.UnsafeFeatures) error {
var wg sync.WaitGroup
// Display banner first
banner.DisplayBanner()
bannerDisplayed = true
log.Infof("检测到 %d 个不同的 token将并行启动多个 frpc 服务实例", len(tokenToIDs))
index := 0
for token, ids := range tokenToIDs {
wg.Add(1)
currentIndex := index
currentToken := token
currentIDs := ids
totalCount := len(tokenToIDs)
// Add a small delay to avoid log output mixing
time.Sleep(100 * time.Millisecond)
go func() {
defer wg.Done()
maskedToken := currentToken
if len(maskedToken) > 6 {
maskedToken = maskedToken[:3] + "***" + maskedToken[len(maskedToken)-3:]
} else {
maskedToken = "***"
}
log.Infof("[%d/%d] 启动 token: %s (IDs: %v)", currentIndex+1, totalCount, maskedToken, currentIDs)
err := runClientWithTokenAndIDs(currentToken, currentIDs, unsafeFeatures)
if err != nil {
fmt.Printf("\nToken [%s] 启动失败: %v\n", maskedToken, err)
}
}()
index++
}
wg.Wait()
return nil
}
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

@@ -22,6 +22,7 @@ import (
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
) )
func init() { func init() {
@@ -32,17 +33,19 @@ var verifyCmd = &cobra.Command{
Use: "verify", Use: "verify",
Short: "Verify that the configures is valid", Short: "Verify that the configures is valid",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if cfgFile == "" { if len(cfgFiles) == 0 || cfgFiles[0] == "" {
fmt.Println("frpc: the configuration file is not specified") fmt.Println("frpc: the configuration file is not specified")
return nil return nil
} }
cfgFile := cfgFiles[0]
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs) unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }

View File

@@ -18,12 +18,14 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/server" "github.com/fatedier/frp/server"
@@ -33,6 +35,7 @@ var (
cfgFile string cfgFile string
showVersion bool showVersion bool
strictConfigMode bool strictConfigMode bool
allowUnsafe []string
serverCfg v1.ServerConfig serverCfg v1.ServerConfig
) )
@@ -41,6 +44,8 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version 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().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) config.RegisterServerConfigFlags(rootCmd, &serverCfg)
} }
@@ -70,11 +75,16 @@ var rootCmd = &cobra.Command{
"please use yaml/json/toml format instead!\n") "please use yaml/json/toml format instead!\n")
} }
} else { } else {
serverCfg.Complete() if err := serverCfg.Complete(); err != nil {
fmt.Printf("failed to complete server config: %v\n", err)
os.Exit(1)
}
svrCfg = &serverCfg svrCfg = &serverCfg
} }
warning, err := validation.ValidateServerConfig(svrCfg) unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
validator := validation.NewConfigValidator(unsafeFeatures)
warning, err := validator.ValidateServerConfig(svrCfg)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) 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"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
) )
func init() { func init() {
@@ -42,7 +43,9 @@ var verifyCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
warning, err := validation.ValidateServerConfig(svrCfg) unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
validator := validation.NewConfigValidator(unsafeFeatures)
warning, err := validator.ValidateServerConfig(svrCfg)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }

View File

@@ -32,6 +32,11 @@ auth.method = "token"
# auth token # auth token
auth.token = "12345678" auth.token = "12345678"
# alternatively, you can use tokenSource to load the token from a file
# this is mutually exclusive with auth.token
# auth.tokenSource.type = "file"
# auth.tokenSource.file.path = "/etc/frp/token"
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication. # oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
# auth.oidc.clientID = "" # auth.oidc.clientID = ""
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication. # oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
@@ -50,6 +55,20 @@ auth.token = "12345678"
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" # auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
# auth.oidc.additionalEndpointParams.var1 = "foobar" # auth.oidc.additionalEndpointParams.var1 = "foobar"
# OIDC TLS and proxy configuration
# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate.
# This is useful when the OIDC provider uses a self-signed certificate or a custom CA.
# auth.oidc.trustedCaFile = "/path/to/ca.crt"
# Skip TLS certificate verification for the OIDC token endpoint.
# INSECURE: Only use this for debugging purposes, not recommended for production.
# auth.oidc.insecureSkipVerify = false
# Specify a proxy server for OIDC token endpoint connections.
# Supports http, https, socks5, and socks5h proxy protocols.
# If not specified, no proxy is used for OIDC connections.
# auth.oidc.proxyURL = "http://proxy.example.com:8080"
# Set admin address for control frpc's action by http api such as reload # Set admin address for control frpc's action by http api such as reload
webServer.addr = "127.0.0.1" webServer.addr = "127.0.0.1"
webServer.port = 7400 webServer.port = 7400
@@ -124,6 +143,11 @@ transport.tls.enable = true
# Default is empty, means all proxies. # Default is empty, means all proxies.
# start = ["ssh", "dns"] # start = ["ssh", "dns"]
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
# Set 'enabled = false' in a proxy configuration to disable it.
# If 'enabled' is not set or set to true, the proxy is enabled by default.
# The 'enabled' field provides more granular control and is recommended over 'start'.
# Specify udp packet size, unit is byte. If not set, the default value is 1500. # Specify udp packet size, unit is byte. If not set, the default value is 1500.
# This parameter should be same between client and server. # This parameter should be same between client and server.
# It affects the udp and sudp proxy. # It affects the udp and sudp proxy.
@@ -150,6 +174,8 @@ metadatas.var2 = "123"
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' # If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
# Enable or disable this proxy. true or omit this field to enable, false to disable.
# enabled = true
localIP = "127.0.0.1" localIP = "127.0.0.1"
localPort = 22 localPort = 22
# Limit bandwidth for this proxy, unit is KB and MB # Limit bandwidth for this proxy, unit is KB and MB
@@ -234,6 +260,8 @@ healthCheck.httpHeaders=[
[[proxies]] [[proxies]]
name = "web02" name = "web02"
type = "https" type = "https"
# Disable this proxy by setting enabled to false
# enabled = false
localIP = "127.0.0.1" localIP = "127.0.0.1"
localPort = 8000 localPort = 8000
subdomain = "web02" subdomain = "web02"
@@ -367,6 +395,14 @@ localPort = 22
# Otherwise, visitors from same user can connect. '*' means allow all users. # Otherwise, visitors from same user can connect. '*' means allow all users.
allowUsers = ["user1", "user2"] allowUsers = ["user1", "user2"]
# NAT traversal configuration (optional)
[proxies.natTraversal]
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
# When enabled, only STUN-discovered public addresses will be used.
# This can improve performance when you have slow VPN connections.
# Default: false
disableAssistedAddrs = false
[[proxies]] [[proxies]]
name = "vnet-server" name = "vnet-server"
type = "stcp" type = "stcp"
@@ -406,6 +442,13 @@ minRetryInterval = 90
# fallbackTo = "stcp_visitor" # fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500 # fallbackTimeoutMs = 500
# NAT traversal configuration (optional)
[visitors.natTraversal]
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
# When enabled, only STUN-discovered public addresses will be used.
# Default: false
disableAssistedAddrs = false
[[visitors]] [[visitors]]
name = "vnet-visitor" name = "vnet-visitor"
type = "stcp" type = "stcp"

View File

@@ -105,6 +105,11 @@ auth.method = "token"
# auth token # auth token
auth.token = "12345678" auth.token = "12345678"
# alternatively, you can use tokenSource to load the token from a file
# this is mutually exclusive with auth.token
# auth.tokenSource.type = "file"
# auth.tokenSource.file.path = "/etc/frp/token"
# oidc issuer specifies the issuer to verify OIDC tokens with. # oidc issuer specifies the issuer to verify OIDC tokens with.
auth.oidc.issuer = "" auth.oidc.issuer = ""
# oidc audience specifies the audience OIDC tokens should contain when validated. # oidc audience specifies the audience OIDC tokens should contain when validated.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -121,7 +121,7 @@ Create new proxy
// http and https only // http and https only
"custom_domains": []<string>, "custom_domains": []<string>,
"subdomain": <string>, "subdomain": <string>,
"locations": <string>, "locations": []<string>,
"http_user": <string>, "http_user": <string>,
"http_pwd": <string>, "http_pwd": <string>,
"host_header_rewrite": <string>, "host_header_rewrite": <string>,

View File

@@ -49,9 +49,7 @@ type = "virtual_net"
# frpc.toml (client side) # frpc.toml (client side)
serverAddr = "x.x.x.x" serverAddr = "x.x.x.x"
serverPort = 7000 serverPort = 7000
featureGates = { featureGates = { VirtualNet = true }
VirtualNet = true
}
# Configure the virtual network interface # Configure the virtual network interface
virtualNet.address = "100.86.0.2/24" virtualNet.address = "100.86.0.2/24"

View File

@@ -1,4 +1,4 @@
FROM golang:1.23 AS building FROM golang:1.24 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building

View File

@@ -1,4 +1,4 @@
FROM golang:1.23 AS building FROM golang:1.24 AS building
COPY . /building COPY . /building
WORKDIR /building WORKDIR /building

49
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/fatedier/frp module github.com/fatedier/frp
go 1.23.0 go 1.24.0
require ( require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
@@ -10,13 +10,13 @@ require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1 github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.34.2 github.com/onsi/gomega v1.36.3
github.com/pelletier/go-toml/v2 v2.2.0 github.com/pelletier/go-toml/v2 v2.2.0
github.com/pion/stun/v2 v2.0.0 github.com/pion/stun/v2 v2.0.0
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.48.2 github.com/quic-go/quic-go v0.55.0
github.com/rodaine/table v1.2.0 github.com/rodaine/table v1.2.0
github.com/samber/lo v1.47.0 github.com/samber/lo v1.47.0
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
@@ -26,10 +26,10 @@ require (
github.com/tidwall/gjson v1.17.1 github.com/tidwall/gjson v1.17.1
github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13 github.com/xtaci/kcp-go/v5 v5.6.13
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.41.0
golang.org/x/net v0.39.0 golang.org/x/net v0.43.0
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.13.0 golang.org/x/sync v0.16.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
@@ -39,19 +39,30 @@ require (
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 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/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/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/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // 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-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/kr/text v0.2.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/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/transport/v2 v2.2.1 // indirect
@@ -61,20 +72,22 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.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/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
go.uber.org/mock v0.5.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
@@ -83,4 +96,4 @@ require (
) )
// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6

101
go.sum
View File

@@ -4,17 +4,30 @@ 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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/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/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= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,10 +36,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -50,10 +65,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/pprof v0.0.0-20241206021119-61a79c692802/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -70,12 +86,20 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= 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=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
@@ -94,6 +118,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -103,10 +129,12 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= 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= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -147,31 +175,35 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/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 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -185,8 +217,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
@@ -195,8 +227,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -207,28 +239,29 @@ 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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -239,8 +272,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
@@ -261,8 +294,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -3,10 +3,10 @@
SCRIPT=$(readlink -f "$0") SCRIPT=$(readlink -f "$0")
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
ginkgo_command=$(which ginkgo 2>/dev/null) # Check if ginkgo is available
if [ -z "$ginkgo_command" ]; then if ! command -v ginkgo >/dev/null 2>&1; then
echo "ginkgo not found, try to install..." echo "ginkgo not found, try to install..."
go install github.com/onsi/ginkgo/v2/ginkgo@v2.17.1 go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
fi fi
debug=false debug=false

View File

@@ -17,8 +17,8 @@ make -f ./Makefile.cross-compiles
rm -rf ./release/packages rm -rf ./release/packages
mkdir -p ./release/packages mkdir -p ./release/packages
os_all='linux windows darwin freebsd android' 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' extra_all='_ hf'
cd ./release cd ./release

View File

@@ -15,6 +15,7 @@
package auth package auth
import ( import (
"context"
"fmt" "fmt"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -27,16 +28,56 @@ type Setter interface {
SetNewWorkConn(*msg.NewWorkConn) error SetNewWorkConn(*msg.NewWorkConn) error
} }
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { type ClientAuth struct {
Setter Setter
key []byte
}
func (a *ClientAuth) EncryptionKey() []byte {
return a.key
}
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
// Caller must run validation before calling this function.
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
if cfg == nil {
return nil, fmt.Errorf("auth config is nil")
}
resolved := *cfg
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
token, err := resolved.TokenSource.Resolve(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
}
resolved.Token = token
}
setter, err := NewAuthSetter(resolved)
if err != nil {
return nil, err
}
return &ClientAuth{
Setter: setter,
key: []byte(resolved.Token),
}, nil
}
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
switch cfg.Method { switch cfg.Method {
case v1.AuthMethodToken: case v1.AuthMethodToken:
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC: case v1.AuthMethodOIDC:
authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) if cfg.OIDC.TokenSource != nil {
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
} else {
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
if err != nil {
return nil, err
}
}
default: default:
panic(fmt.Sprintf("wrong method: '%s'", cfg.Method)) return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
} }
return authProvider return authProvider, nil
} }
type Verifier interface { type Verifier interface {
@@ -45,6 +86,35 @@ type Verifier interface {
VerifyNewWorkConn(*msg.NewWorkConn) error 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) { func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
switch cfg.Method { switch cfg.Method {
case v1.AuthMethodToken: case v1.AuthMethodToken:

View File

@@ -16,23 +16,72 @@ package auth
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net/http"
"net/url"
"os"
"slices" "slices"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
) )
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) {
// Clone the default transport to get all reasonable defaults
transport := http.DefaultTransport.(*http.Transport).Clone()
// Configure TLS settings
if trustedCAFile != "" || insecureSkipVerify {
tlsConfig := &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
}
if trustedCAFile != "" && !insecureSkipVerify {
caCert, err := os.ReadFile(trustedCAFile)
if err != nil {
return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile)
}
tlsConfig.RootCAs = caCertPool
}
transport.TLSClientConfig = tlsConfig
}
// Configure proxy settings
if proxyURL != "" {
parsedURL, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err)
}
transport.Proxy = http.ProxyURL(parsedURL)
} else {
// Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment
transport.Proxy = nil
}
return &http.Client{Transport: transport}, nil
}
type OidcAuthProvider struct { type OidcAuthProvider struct {
additionalAuthScopes []v1.AuthScope additionalAuthScopes []v1.AuthScope
tokenGenerator *clientcredentials.Config tokenGenerator *clientcredentials.Config
httpClient *http.Client
} }
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider { func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
eps := make(map[string][]string) eps := make(map[string][]string)
for k, v := range cfg.AdditionalEndpointParams { for k, v := range cfg.AdditionalEndpointParams {
eps[k] = []string{v} eps[k] = []string{v}
@@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
EndpointParams: eps, EndpointParams: eps,
} }
// Create custom HTTP client if needed
var httpClient *http.Client
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
var err error
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
}
}
return &OidcAuthProvider{ return &OidcAuthProvider{
additionalAuthScopes: additionalAuthScopes, additionalAuthScopes: additionalAuthScopes,
tokenGenerator: tokenGenerator, tokenGenerator: tokenGenerator,
} httpClient: httpClient,
}, nil
} }
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
tokenObj, err := auth.tokenGenerator.Token(context.Background()) ctx := context.Background()
if auth.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
}
tokenObj, err := auth.tokenGenerator.Token(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
} }
@@ -87,6 +152,51 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
return err return err
} }
type OidcTokenSourceAuthProvider struct {
additionalAuthScopes []v1.AuthScope
valueSource *v1.ValueSource
}
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
return &OidcTokenSourceAuthProvider{
additionalAuthScopes: additionalAuthScopes,
valueSource: valueSource,
}
}
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
ctx := context.Background()
accessToken, err = auth.valueSource.Resolve(ctx)
if err != nil {
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
}
return
}
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
return err
}
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
return nil
}
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
return err
}
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
return nil
}
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
return err
}
type TokenVerifier interface { type TokenVerifier interface {
Verify(context.Context, string) (*oidc.IDToken, error) Verify(context.Context, string) (*oidc.IDToken, error)
} }

View File

@@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) {
} }
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_") common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
return common, nil return common, nil
} }
@@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni(
startProxy[s] = struct{}{} startProxy[s] = struct{}{}
} }
startAll := true startAll := len(startProxy) == 0
if len(startProxy) > 0 {
startAll = false
}
// Build template sections from range section And append to ini.File. // Build template sections from range section And append to ini.File.
rangeSections := make([]*ini.Section, 0) rangeSections := make([]*ini.Section, 0)

View File

@@ -26,20 +26,20 @@ import (
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig { func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
out := &v1.ClientCommonConfig{} out := &v1.ClientCommonConfig{}
out.User = conf.User out.User = conf.User
out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod) out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
out.Auth.Token = conf.ClientConfig.Token out.Auth.Token = conf.Token
if conf.ClientConfig.AuthenticateHeartBeats { if conf.AuthenticateHeartBeats {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
} }
if conf.ClientConfig.AuthenticateNewWorkConns { if conf.AuthenticateNewWorkConns {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
} }
out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID out.Auth.OIDC.ClientID = conf.OidcClientID
out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret out.Auth.OIDC.ClientSecret = conf.OidcClientSecret
out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience out.Auth.OIDC.Audience = conf.OidcAudience
out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope out.Auth.OIDC.Scope = conf.OidcScope
out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL
out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams
out.ServerAddr = conf.ServerAddr out.ServerAddr = conf.ServerAddr
out.ServerPort = conf.ServerPort out.ServerPort = conf.ServerPort
@@ -59,10 +59,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable) out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte) out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile out.Transport.TLS.CertFile = conf.TLSCertFile
out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile out.Transport.TLS.KeyFile = conf.TLSKeyFile
out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName out.Transport.TLS.ServerName = conf.TLSServerName
out.Log.To = conf.LogFile out.Log.To = conf.LogFile
out.Log.Level = conf.LogLevel out.Log.Level = conf.LogLevel
@@ -87,18 +87,18 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
out := &v1.ServerConfig{} out := &v1.ServerConfig{}
out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod) out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod)
out.Auth.Token = conf.ServerConfig.Token out.Auth.Token = conf.Token
if conf.ServerConfig.AuthenticateHeartBeats { if conf.AuthenticateHeartBeats {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
} }
if conf.ServerConfig.AuthenticateNewWorkConns { if conf.AuthenticateNewWorkConns {
out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
} }
out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience out.Auth.OIDC.Audience = conf.OidcAudience
out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer out.Auth.OIDC.Issuer = conf.OidcIssuer
out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck
out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck
out.BindAddr = conf.BindAddr out.BindAddr = conf.BindAddr
out.BindPort = conf.BindPort out.BindPort = conf.BindPort

View File

@@ -206,7 +206,7 @@ func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section)
} }
// plugin_xxx // plugin_xxx
cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
return nil return nil
} }

View File

@@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
return LoadConfigure(content, c, strict) return LoadConfigure(content, c, strict)
} }
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
// This function handles both cases efficiently: with or without dot fields
func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
var temp any
if err := yaml.Unmarshal(content, &temp); err != nil {
return err
}
// Remove dot fields if it's a map
if tempMap, ok := temp.(map[string]any); ok {
for key := range tempMap {
if strings.HasPrefix(key, ".") {
delete(tempMap, key)
}
}
}
// Convert to JSON and decode with strict validation
jsonBytes, err := json.Marshal(temp)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
// LoadConfigure loads configuration from bytes and unmarshal into c. // LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format. // Now it supports json, yaml and toml format.
func LoadConfigure(b []byte, c any, strict bool) error { func LoadConfigure(b []byte, c any, strict bool) error {
@@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error {
} }
return decoder.Decode(c) return decoder.Decode(c)
} }
// It wasn't JSON. Unmarshal as YAML.
// Handle YAML content
if strict { if strict {
return yaml.UnmarshalStrict(b, c) // In strict mode, always use our custom handler to support YAML merge
return parseYAMLWithDotFieldsHandling(b, c)
} }
// Non-strict mode, parse normally
return yaml.Unmarshal(b, c) return yaml.Unmarshal(b, c)
} }
@@ -182,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
} }
} }
if svrCfg != nil { if svrCfg != nil {
svrCfg.Complete() if err := svrCfg.Complete(); err != nil {
return nil, isLegacyFormat, err
}
} }
return svrCfg, isLegacyFormat, nil return svrCfg, isLegacyFormat, nil
} }
@@ -249,8 +281,21 @@ func LoadClientConfig(path string, strict bool) (
}) })
} }
// Filter by enabled field in each proxy
// nil or true means enabled, false means disabled
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
enabled := c.GetBaseConfig().Enabled
return enabled == nil || *enabled
})
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
enabled := c.GetBaseConfig().Enabled
return enabled == nil || *enabled
})
if cliCfg != nil { if cliCfg != nil {
cliCfg.Complete() if err := cliCfg.Complete(); err != nil {
return nil, nil, nil, isLegacyFormat, err
}
} }
for _, c := range proxyCfgs { for _, c := range proxyCfgs {
c.Complete(cliCfg.User) c.Complete(cliCfg.User)

View File

@@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock"
err = LoadConfigure([]byte(pluginStr), &clientCfg, true) err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.Error(err) require.Error(err)
} }
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
// even in strict mode by properly handling dot-prefixed fields
func TestYAMLMergeInStrictMode(t *testing.T) {
require := require.New(t)
yamlContent := `
serverAddr: "127.0.0.1"
serverPort: 7000
.common: &common
type: stcp
secretKey: "test-secret"
localIP: 127.0.0.1
transport:
useEncryption: true
useCompression: true
proxies:
- name: ssh
localPort: 22
<<: *common
- name: web
localPort: 80
<<: *common
`
clientCfg := v1.ClientConfig{}
// This should work in strict mode
err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
require.NoError(err)
// Verify the merge worked correctly
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
require.Len(clientCfg.Proxies, 2)
// Check first proxy
sshProxy := clientCfg.Proxies[0].ProxyConfigurer
require.Equal("ssh", sshProxy.GetBaseConfig().Name)
require.Equal("stcp", sshProxy.GetBaseConfig().Type)
// Check second proxy
webProxy := clientCfg.Proxies[1].ProxyConfigurer
require.Equal("web", webProxy.GetBaseConfig().Name)
require.Equal("stcp", webProxy.GetBaseConfig().Type)
}
// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
func TestOptimizedYAMLProcessing(t *testing.T) {
require := require.New(t)
yamlWithDotFields := []byte(`
serverAddr: "127.0.0.1"
.common: &common
type: stcp
proxies:
- name: test
<<: *common
`)
yamlWithoutDotFields := []byte(`
serverAddr: "127.0.0.1"
proxies:
- name: test
type: tcp
localPort: 22
`)
// Test that YAML without dot fields works in strict mode
clientCfg := v1.ClientConfig{}
err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
// Test that YAML with dot fields still works in strict mode
err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Len(clientCfg.Proxies, 1)
require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
}
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
func TestYAMLEdgeCases(t *testing.T) {
require := require.New(t)
// Test array at root (should fail for frp config)
arrayYAML := []byte(`
- item1
- item2
`)
clientCfg := v1.ClientConfig{}
err := LoadConfigure(arrayYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test scalar at root (should fail for frp config)
scalarYAML := []byte(`"just a string"`)
err = LoadConfigure(scalarYAML, &clientCfg, true)
require.Error(err) // Should fail because ClientConfig expects an object
// Test empty object (should work)
emptyYAML := []byte(`{}`)
err = LoadConfigure(emptyYAML, &clientCfg, true)
require.NoError(err)
// Test nested structure without dots (should work)
nestedYAML := []byte(`
serverAddr: "127.0.0.1"
serverPort: 7000
`)
err = LoadConfigure(nestedYAML, &clientCfg, true)
require.NoError(err)
require.Equal("127.0.0.1", clientCfg.ServerAddr)
require.Equal(7000, clientCfg.ServerPort)
}

View File

@@ -77,18 +77,21 @@ type ClientCommonConfig struct {
IncludeConfigFiles []string `json:"includes,omitempty"` IncludeConfigFiles []string `json:"includes,omitempty"`
} }
func (c *ClientCommonConfig) Complete() { func (c *ClientCommonConfig) Complete() error {
c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0") c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
c.ServerPort = util.EmptyOr(c.ServerPort, 7000) c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true)) c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478") c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478")
c.Auth.Complete() if err := c.Auth.Complete(); err != nil {
return err
}
c.Log.Complete() c.Log.Complete()
c.Transport.Complete() c.Transport.Complete()
c.WebServer.Complete() c.WebServer.Complete()
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
return nil
} }
type ClientTransportConfig struct { type ClientTransportConfig struct {
@@ -184,12 +187,16 @@ type AuthClientConfig struct {
// Token specifies the authorization token used to create keys to be sent // Token specifies the authorization token used to create keys to be sent
// to the server. The server must have a matching token for authorization // to the server. The server must have a matching token for authorization
// to succeed. By default, this value is "". // to succeed. By default, this value is "".
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` // TokenSource specifies a dynamic source for the authorization token.
// This is mutually exclusive with Token field.
TokenSource *ValueSource `json:"tokenSource,omitempty"`
OIDC AuthOIDCClientConfig `json:"oidc,omitempty"`
} }
func (c *AuthClientConfig) Complete() { func (c *AuthClientConfig) Complete() error {
c.Method = util.EmptyOr(c.Method, "token") c.Method = util.EmptyOr(c.Method, "token")
return nil
} }
type AuthOIDCClientConfig struct { type AuthOIDCClientConfig struct {
@@ -208,6 +215,21 @@ type AuthOIDCClientConfig struct {
// AdditionalEndpointParams specifies additional parameters to be sent // AdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator. // this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
// TrustedCaFile specifies the path to a custom CA certificate file
// for verifying the OIDC token endpoint's TLS certificate.
TrustedCaFile string `json:"trustedCaFile,omitempty"`
// InsecureSkipVerify disables TLS certificate verification for the
// OIDC token endpoint. Only use this for debugging, not recommended for production.
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
// ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint.
// Supports http, https, socks5, and socks5h proxy protocols.
// If empty, no proxy is used for OIDC connections.
ProxyURL string `json:"proxyURL,omitempty"`
// TokenSource specifies a custom dynamic source for the authorization token.
// This is mutually exclusive with every other field of this structure.
TokenSource *ValueSource `json:"tokenSource,omitempty"`
} }
type VirtualNetConfig struct { type VirtualNetConfig struct {

View File

@@ -24,7 +24,8 @@ import (
func TestClientConfigComplete(t *testing.T) { func TestClientConfigComplete(t *testing.T) {
require := require.New(t) require := require.New(t)
c := &ClientConfig{} c := &ClientConfig{}
c.Complete() err := c.Complete()
require.NoError(err)
require.EqualValues("token", c.Auth.Method) require.EqualValues("token", c.Auth.Method)
require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
@@ -33,3 +34,11 @@ func TestClientConfigComplete(t *testing.T) {
require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte)) require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
require.NotEmpty(c.NatHoleSTUNServer) require.NotEmpty(c.NatHoleSTUNServer)
} }
func TestAuthClientConfig_Complete(t *testing.T) {
require := require.New(t)
cfg := &AuthClientConfig{}
err := cfg.Complete()
require.NoError(err)
require.EqualValues("token", cfg.Method)
}

View File

@@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() {
} }
type TLSConfig struct { type TLSConfig struct {
// CertPath specifies the path of the cert file that client will load. // CertFile specifies the path of the cert file that client will load.
CertFile string `json:"certFile,omitempty"` CertFile string `json:"certFile,omitempty"`
// KeyPath specifies the path of the secret key file that client will load. // KeyFile specifies the path of the secret key file that client will load.
KeyFile string `json:"keyFile,omitempty"` KeyFile string `json:"keyFile,omitempty"`
// TrustedCaFile specifies the path of the trusted ca file that will load. // TrustedCaFile specifies the path of the trusted ca file that will load.
TrustedCaFile string `json:"trustedCaFile,omitempty"` TrustedCaFile string `json:"trustedCaFile,omitempty"`
@@ -96,6 +96,14 @@ type TLSConfig struct {
ServerName string `json:"serverName,omitempty"` ServerName string `json:"serverName,omitempty"`
} }
// NatTraversalConfig defines configuration options for NAT traversal
type NatTraversalConfig struct {
// DisableAssistedAddrs disables the use of local network interfaces
// for assisted connections during NAT traversal. When enabled,
// only STUN-discovered public addresses will be used.
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
}
type LogConfig struct { type LogConfig struct {
// This is destination where frp should write the logs. // This is destination where frp should write the logs.
// If "console" is used, logs will be printed to stdout, otherwise, // If "console" is used, logs will be printed to stdout, otherwise,

View File

@@ -108,8 +108,11 @@ type DomainConfig struct {
} }
type ProxyBaseConfig struct { type ProxyBaseConfig struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
// This allows individual control over each proxy, complementing the global "start" field.
Enabled *bool `json:"enabled,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"` Annotations map[string]string `json:"annotations,omitempty"`
Transport ProxyTransport `json:"transport,omitempty"` Transport ProxyTransport `json:"transport,omitempty"`
// metadata info for each proxy // metadata info for each proxy
@@ -129,7 +132,7 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
if c.Plugin.ClientPluginOptions != nil { if c.Plugin.ClientPluginOptions != nil {
c.Plugin.ClientPluginOptions.Complete() c.Plugin.Complete()
} }
} }
@@ -422,6 +425,9 @@ type XTCPProxyConfig struct {
Secretkey string `json:"secretKey,omitempty"` Secretkey string `json:"secretKey,omitempty"`
AllowUsers []string `json:"allowUsers,omitempty"` AllowUsers []string `json:"allowUsers,omitempty"`
// NatTraversal configuration for NAT traversal
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
} }
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {

View File

@@ -98,8 +98,10 @@ type ServerConfig struct {
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"` HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
} }
func (c *ServerConfig) Complete() { func (c *ServerConfig) Complete() error {
c.Auth.Complete() if err := c.Auth.Complete(); err != nil {
return err
}
c.Log.Complete() c.Log.Complete()
c.Transport.Complete() c.Transport.Complete()
c.WebServer.Complete() c.WebServer.Complete()
@@ -120,17 +122,20 @@ func (c *ServerConfig) Complete() {
c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24) c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
return nil
} }
type AuthServerConfig struct { type AuthServerConfig struct {
Method AuthMethod `json:"method,omitempty"` Method AuthMethod `json:"method,omitempty"`
AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
TokenSource *ValueSource `json:"tokenSource,omitempty"`
OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` OIDC AuthOIDCServerConfig `json:"oidc,omitempty"`
} }
func (c *AuthServerConfig) Complete() { func (c *AuthServerConfig) Complete() error {
c.Method = util.EmptyOr(c.Method, "token") c.Method = util.EmptyOr(c.Method, "token")
return nil
} }
type AuthOIDCServerConfig struct { type AuthOIDCServerConfig struct {

View File

@@ -24,9 +24,18 @@ import (
func TestServerConfigComplete(t *testing.T) { func TestServerConfigComplete(t *testing.T) {
require := require.New(t) require := require.New(t)
c := &ServerConfig{} c := &ServerConfig{}
c.Complete() err := c.Complete()
require.NoError(err)
require.EqualValues("token", c.Auth.Method) require.EqualValues("token", c.Auth.Method)
require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient)) require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
} }
func TestAuthServerConfig_Complete(t *testing.T) {
require := require.New(t)
cfg := &AuthServerConfig{}
err := cfg.Complete()
require.NoError(err)
require.EqualValues("token", cfg.Method)
}

View File

@@ -23,43 +23,109 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/featuregate" "github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
) )
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
var ( var (
warnings Warning warnings Warning
errs error errs error
) )
// validate feature gates
if c.VirtualNet.Address != "" { validators := []func() (Warning, error){
if !featuregate.Enabled(featuregate.VirtualNet) { func() (Warning, error) { return validateFeatureGates(c) },
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag") func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
} func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
} }
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) { for _, validator := range validators {
w, err := validator()
warnings = AppendError(warnings, w)
errs = AppendError(errs, err)
}
return warnings, errs
}
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
if c.VirtualNet.Address != "" {
if !featuregate.Enabled(featuregate.VirtualNet) {
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
}
}
return nil, nil
}
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
var errs error
if !slices.Contains(SupportedAuthMethods, c.Method) {
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
} }
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
} }
if err := validateLogConfig(&c.Log); err != nil { // Validate token/tokenSource mutual exclusivity
errs = AppendError(errs, err) if c.Token != "" && c.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
} }
if err := validateWebServerConfig(&c.WebServer); err != nil { // Validate tokenSource if specified
errs = AppendError(errs, err) if c.TokenSource != nil {
if c.TokenSource.Type == "exec" {
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
} }
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 { if err := v.validateOIDCConfig(&c.OIDC); err != nil {
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval { errs = AppendError(errs, err)
}
return nil, errs
}
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
if c.TokenSource == nil {
return nil
}
var errs error
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
}
if c.TokenSource.Type == "exec" {
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
}
return errs
}
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
var (
warnings Warning
errs error
)
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
if c.HeartbeatTimeout < c.HeartbeatInterval {
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval")) errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
} }
} }
if !lo.FromPtr(c.Transport.TLS.Enable) { if !lo.FromPtr(c.TLS.Enable) {
checkTLSConfig := func(name string, value string) Warning { checkTLSConfig := func(name string, value string) Warning {
if value != "" { if value != "" {
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name) return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
@@ -67,16 +133,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
return nil return nil
} }
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
} }
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) { if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols)) errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
} }
return warnings, errs
}
for _, f := range c.IncludeConfigFiles { func validateIncludeFiles(files []string) (Warning, error) {
var errs error
for _, f := range files {
absDir, err := filepath.Abs(filepath.Dir(f)) absDir, err := filepath.Abs(filepath.Dir(f))
if err != nil { if err != nil {
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err)) errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
@@ -86,13 +156,19 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f)) errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
} }
} }
return warnings, errs return nil, errs
} }
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { func ValidateAllClientConfig(
c *v1.ClientCommonConfig,
proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
unsafeFeatures *security.UnsafeFeatures,
) (Warning, error) {
validator := NewConfigValidator(unsafeFeatures)
var warnings Warning var warnings Warning
if c != nil { if c != nil {
warning, err := ValidateClientCommonConfig(c) warning, err := validator.ValidateClientCommonConfig(c)
warnings = AppendError(warnings, warning) warnings = AppendError(warnings, warning)
if err != nil { if err != nil {
return warnings, err return warnings, err

View File

@@ -21,9 +21,10 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1" 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 ( var (
warnings Warning warnings Warning
errs error errs error
@@ -35,6 +36,23 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
} }
// Validate token/tokenSource mutual exclusivity
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
// 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))
}
}
if err := validateLogConfig(&c.Log); err != nil { if err := validateLogConfig(&c.Log); err != nil {
errs = AppendError(errs, err) errs = AppendError(errs, 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

@@ -0,0 +1,158 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
// ValueSource provides a way to dynamically resolve configuration values
// from various sources like files, environment variables, or external services.
type ValueSource struct {
Type string `json:"type"`
File *FileSource `json:"file,omitempty"`
Exec *ExecSource `json:"exec,omitempty"`
}
// FileSource specifies how to load a value from a file.
type FileSource struct {
Path string `json:"path"`
}
// ExecSource specifies how to get a value from another program launched as subprocess.
type ExecSource struct {
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env []ExecEnvVar `json:"env,omitempty"`
}
type ExecEnvVar struct {
Name string `json:"name"`
Value string `json:"value"`
}
// Validate validates the ValueSource configuration.
func (v *ValueSource) Validate() error {
if v == nil {
return errors.New("valueSource cannot be nil")
}
switch v.Type {
case "file":
if v.File == nil {
return errors.New("file configuration is required when type is 'file'")
}
return v.File.Validate()
case "exec":
if v.Exec == nil {
return errors.New("exec configuration is required when type is 'exec'")
}
return v.Exec.Validate()
default:
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type)
}
}
// Resolve resolves the value from the configured source.
func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
if err := v.Validate(); err != nil {
return "", err
}
switch v.Type {
case "file":
return v.File.Resolve(ctx)
case "exec":
return v.Exec.Resolve(ctx)
default:
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
}
}
// Validate validates the FileSource configuration.
func (f *FileSource) Validate() error {
if f == nil {
return errors.New("fileSource cannot be nil")
}
if f.Path == "" {
return errors.New("file path cannot be empty")
}
return nil
}
// Resolve reads and returns the content from the specified file.
func (f *FileSource) Resolve(_ context.Context) (string, error) {
if err := f.Validate(); err != nil {
return "", err
}
content, err := os.ReadFile(f.Path)
if err != nil {
return "", fmt.Errorf("failed to read file %s: %v", f.Path, err)
}
// Trim whitespace, which is important for file-based tokens
return strings.TrimSpace(string(content)), nil
}
// Validate validates the ExecSource configuration.
func (e *ExecSource) Validate() error {
if e == nil {
return errors.New("execSource cannot be nil")
}
if e.Command == "" {
return errors.New("exec command cannot be empty")
}
for _, env := range e.Env {
if env.Name == "" {
return errors.New("exec env name cannot be empty")
}
if strings.Contains(env.Name, "=") {
return errors.New("exec env name cannot contain '='")
}
}
return nil
}
// Resolve reads and returns the content captured from stdout of launched subprocess.
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
if err := e.Validate(); err != nil {
return "", err
}
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
if len(e.Env) != 0 {
cmd.Env = os.Environ()
for _, env := range e.Env {
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
}
}
content, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
}
// Trim whitespace, which is important for exec-based tokens
return strings.TrimSpace(string(content)), nil
}

View File

@@ -0,0 +1,246 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestValueSource_Validate(t *testing.T) {
tests := []struct {
name string
vs *ValueSource
wantErr bool
}{
{
name: "nil valueSource",
vs: nil,
wantErr: true,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
wantErr: true,
},
{
name: "file type without file config",
vs: &ValueSource{
Type: "file",
File: nil,
},
wantErr: true,
},
{
name: "valid file type with absolute path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "/tmp/test",
},
},
wantErr: false,
},
{
name: "valid file type with relative path",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "configs/token",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.vs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Validate(t *testing.T) {
tests := []struct {
name string
fs *FileSource
wantErr bool
}{
{
name: "nil fileSource",
fs: nil,
wantErr: true,
},
{
name: "empty path",
fs: &FileSource{
Path: "",
},
wantErr: true,
},
{
name: "relative path (allowed)",
fs: &FileSource{
Path: "relative/path",
},
wantErr: false,
},
{
name: "absolute path",
fs: &FileSource{
Path: "/absolute/path",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.fs.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFileSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value\n\t "
expectedContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
fs *FileSource
want string
wantErr bool
}{
{
name: "valid file path",
fs: &FileSource{
Path: testFile,
},
want: expectedContent,
wantErr: false,
},
{
name: "non-existent file",
fs: &FileSource{
Path: "/non/existent/file",
},
want: "",
wantErr: true,
},
{
name: "path traversal attempt (should fail validation)",
fs: &FileSource{
Path: "../../../etc/passwd",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fs.Resolve(context.Background())
if (err != nil) != tt.wantErr {
t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}
func TestValueSource_Resolve(t *testing.T) {
// Create a temporary file for testing
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test_token")
testContent := "test-token-value"
err := os.WriteFile(testFile, []byte(testContent), 0o600)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
tests := []struct {
name string
vs *ValueSource
want string
wantErr bool
}{
{
name: "valid file type",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: testFile,
},
},
want: testContent,
wantErr: false,
},
{
name: "unsupported type",
vs: &ValueSource{
Type: "unsupported",
},
want: "",
wantErr: true,
},
{
name: "file type with path traversal",
vs: &ValueSource{
Type: "file",
File: &FileSource{
Path: "../../../etc/passwd",
},
},
want: "",
wantErr: true,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.vs.Resolve(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -32,8 +32,11 @@ type VisitorTransport struct {
} }
type VisitorBaseConfig struct { type VisitorBaseConfig struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
// This allows individual control over each visitor, complementing the global "start" field.
Enabled *bool `json:"enabled,omitempty"`
Transport VisitorTransport `json:"transport,omitempty"` Transport VisitorTransport `json:"transport,omitempty"`
SecretKey string `json:"secretKey,omitempty"` SecretKey string `json:"secretKey,omitempty"`
// if the server user is not set, it defaults to the current user // if the server user is not set, it defaults to the current user
@@ -160,6 +163,9 @@ type XTCPVisitorConfig struct {
MinRetryInterval int `json:"minRetryInterval,omitempty"` MinRetryInterval int `json:"minRetryInterval,omitempty"`
FallbackTo string `json:"fallbackTo,omitempty"` FallbackTo string `json:"fallbackTo,omitempty"`
FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"`
// NatTraversal configuration for NAT traversal
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
} }
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {

View File

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

View File

@@ -14,11 +14,12 @@ const (
var ServerMetrics metrics.ServerMetrics = newServerMetrics() var ServerMetrics metrics.ServerMetrics = newServerMetrics()
type serverMetrics struct { type serverMetrics struct {
clientCount prometheus.Gauge clientCount prometheus.Gauge
proxyCount *prometheus.GaugeVec proxyCount *prometheus.GaugeVec
connectionCount *prometheus.GaugeVec proxyCountDetailed *prometheus.GaugeVec
trafficIn *prometheus.CounterVec connectionCount *prometheus.GaugeVec
trafficOut *prometheus.CounterVec trafficIn *prometheus.CounterVec
trafficOut *prometheus.CounterVec
} }
func (m *serverMetrics) NewClient() { func (m *serverMetrics) NewClient() {
@@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() {
m.clientCount.Dec() m.clientCount.Dec()
} }
func (m *serverMetrics) NewProxy(_ string, proxyType string) { func (m *serverMetrics) NewProxy(name string, proxyType string) {
m.proxyCount.WithLabelValues(proxyType).Inc() m.proxyCount.WithLabelValues(proxyType).Inc()
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
} }
func (m *serverMetrics) CloseProxy(_ string, proxyType string) { func (m *serverMetrics) CloseProxy(name string, proxyType string) {
m.proxyCount.WithLabelValues(proxyType).Dec() m.proxyCount.WithLabelValues(proxyType).Dec()
m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec()
} }
func (m *serverMetrics) OpenConnection(name string, proxyType string) { func (m *serverMetrics) OpenConnection(name string, proxyType string) {
@@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics {
Name: "proxy_counts", Name: "proxy_counts",
Help: "The current proxy counts", Help: "The current proxy counts",
}, []string{"type"}), }, []string{"type"}),
proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: serverSubsystem,
Name: "proxy_counts_detailed",
Help: "The current number of proxies grouped by type and name",
}, []string{"type", "name"}),
connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace, Namespace: namespace,
Subsystem: serverSubsystem, Subsystem: serverSubsystem,
@@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics {
} }
prometheus.MustRegister(m.clientCount) prometheus.MustRegister(m.clientCount)
prometheus.MustRegister(m.proxyCount) prometheus.MustRegister(m.proxyCount)
prometheus.MustRegister(m.proxyCountDetailed)
prometheus.MustRegister(m.connectionCount) prometheus.MustRegister(m.connectionCount)
prometheus.MustRegister(m.trafficIn) prometheus.MustRegister(m.trafficIn)
prometheus.MustRegister(m.trafficOut) prometheus.MustRegister(m.trafficOut)

View File

@@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error {
} }
} }
func (d *Dispatcher) SendChannel() chan Message {
return d.sendCh
}
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) { func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
d.msgHandlers[reflect.TypeOf(msg)] = handler d.msgHandlers[reflect.TypeOf(msg)] = handler
} }

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. // Make hole-punching decisions based on the NAT information of the client and visitor.
vResp, cResp, err := c.analysis(session) vResp, cResp, err := c.analysis(session)
if err != nil { 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()) vResp = c.GenNatHoleResponse(session.visitorMsg.TransactionID, nil, err.Error())
cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error()) cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error())
} }

View File

@@ -68,6 +68,13 @@ var (
DetectRoleReceiver = "receiver" DetectRoleReceiver = "receiver"
) )
// PrepareOptions defines options for NAT traversal preparation
type PrepareOptions struct {
// DisableAssistedAddrs disables the use of local network interfaces
// for assisted connections during NAT traversal
DisableAssistedAddrs bool
}
type PrepareResult struct { type PrepareResult struct {
Addrs []string Addrs []string
AssistedAddrs []string AssistedAddrs []string
@@ -108,7 +115,7 @@ func PreCheck(
} }
// Prepare is used to do some preparation work before penetration. // Prepare is used to do some preparation work before penetration.
func Prepare(stunServers []string) (*PrepareResult, error) { func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) {
// discover for Nat type // discover for Nat type
addrs, localAddr, err := Discover(stunServers, "") addrs, localAddr, err := Discover(stunServers, "")
if err != nil { if err != nil {
@@ -133,9 +140,13 @@ func Prepare(stunServers []string) (*PrepareResult, error) {
return nil, fmt.Errorf("listen local udp addr error: %v", err) return nil, fmt.Errorf("listen local udp addr error: %v", err)
} }
assistedAddrs := make([]string, 0, len(localIPs)) // Apply NAT traversal options
for _, ip := range localIPs { var assistedAddrs []string
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) if !opts.DisableAssistedAddrs {
assistedAddrs = make([]string, 0, len(localIPs))
for _, ip := range localIPs {
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port)))
}
} }
return &PrepareResult{ return &PrepareResult{
Addrs: addrs, Addrs: addrs,

View File

@@ -18,9 +18,10 @@ package client
import ( import (
"context" "context"
"io"
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
) )
func init() { func init() {
@@ -30,6 +31,8 @@ func init() {
type VirtualNetPlugin struct { type VirtualNetPlugin struct {
pluginCtx PluginContext pluginCtx PluginContext
opts *v1.VirtualNetPluginOptions opts *v1.VirtualNetPluginOptions
mu sync.Mutex
conns map[io.ReadWriteCloser]struct{}
} }
func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) { func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
@@ -43,19 +46,32 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions
} }
func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
xl := xlog.FromContextSafe(ctx)
// Verify if virtual network controller is available // Verify if virtual network controller is available
if p.pluginCtx.VnetController == nil { if p.pluginCtx.VnetController == nil {
return return
} }
// Register the connection with the controller // Add the connection before starting the read loop to avoid race condition
routeName := p.pluginCtx.Name // where RemoveConn might be called before the connection is added.
err := p.pluginCtx.VnetController.RegisterServerConn(ctx, routeName, connInfo.Conn) p.mu.Lock()
if err != nil { if p.conns == nil {
xl.Errorf("virtual net failed to register server connection: %v", err) p.conns = make(map[io.ReadWriteCloser]struct{})
return }
p.conns[connInfo.Conn] = struct{}{}
p.mu.Unlock()
// Register the connection with the controller and pass the cleanup function
p.pluginCtx.VnetController.StartServerConnReadLoop(ctx, connInfo.Conn, func() {
p.RemoveConn(connInfo.Conn)
})
}
func (p *VirtualNetPlugin) RemoveConn(conn io.ReadWriteCloser) {
p.mu.Lock()
defer p.mu.Unlock()
// Check if the map exists, as Close might have set it to nil concurrently
if p.conns != nil {
delete(p.conns, conn)
} }
} }
@@ -64,8 +80,13 @@ func (p *VirtualNetPlugin) Name() string {
} }
func (p *VirtualNetPlugin) Close() error { func (p *VirtualNetPlugin) Close() error {
if p.pluginCtx.VnetController != nil { p.mu.Lock()
p.pluginCtx.VnetController.UnregisterServerConn(p.pluginCtx.Name) defer p.mu.Unlock()
// Close any remaining connections
for conn := range p.conns {
_ = conn.Close()
} }
p.conns = nil
return nil return nil
} }

View File

@@ -23,11 +23,20 @@ import (
"github.com/fatedier/frp/pkg/vnet" "github.com/fatedier/frp/pkg/vnet"
) )
// PluginContext provides the necessary context and callbacks for visitor plugins.
type PluginContext struct { type PluginContext struct {
Name string // Name is the unique identifier for this visitor, used for logging and routing.
Ctx context.Context Name string
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
Ctx context.Context
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
VnetController *vnet.Controller VnetController *vnet.Controller
HandleConn func(net.Conn)
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
// Does not return error; failures are handled by closing the connection.
SendConnToVisitor func(net.Conn)
} }
// Creators is used for create plugins to handle connections. // Creators is used for create plugins to handle connections.

View File

@@ -42,6 +42,8 @@ type VirtualNetPlugin struct {
controllerConn net.Conn controllerConn net.Conn
closeSignal chan struct{} closeSignal chan struct{}
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -60,7 +62,7 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOption
return nil, errors.New("destinationIP is required") return nil, errors.New("destinationIP is required")
} }
// Parse DestinationIP as a single IP and create a host route // Parse DestinationIP and create a host route.
ip := net.ParseIP(opts.DestinationIP) ip := net.ParseIP(opts.DestinationIP)
if ip == nil { if ip == nil {
return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP) return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP)
@@ -91,20 +93,17 @@ func (p *VirtualNetPlugin) Start() {
if len(p.routes) > 0 { if len(p.routes) > 0 {
routeStr = p.routes[0].String() routeStr = p.routes[0].String()
} }
xl.Infof("Starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr) xl.Infof("starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr)
go p.run() go p.run()
} }
func (p *VirtualNetPlugin) run() { func (p *VirtualNetPlugin) run() {
xl := xlog.FromContextSafe(p.ctx) xl := xlog.FromContextSafe(p.ctx)
reconnectDelay := 10 * time.Second
for { for {
// Create a signal channel for this connection attempt
currentCloseSignal := make(chan struct{}) currentCloseSignal := make(chan struct{})
// Store the signal channel under lock
p.mu.Lock() p.mu.Lock()
p.closeSignal = currentCloseSignal p.closeSignal = currentCloseSignal
p.mu.Unlock() p.mu.Unlock()
@@ -112,7 +111,6 @@ func (p *VirtualNetPlugin) run() {
select { select {
case <-p.ctx.Done(): case <-p.ctx.Done():
xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name) xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name)
// Ensure controllerConn from previous loop is cleaned up if necessary
p.cleanupControllerConn(xl) p.cleanupControllerConn(xl)
return return
default: default:
@@ -120,65 +118,70 @@ func (p *VirtualNetPlugin) run() {
controllerConn, pluginConn := net.Pipe() controllerConn, pluginConn := net.Pipe()
// Store controllerConn under lock for cleanup purposes
p.mu.Lock() p.mu.Lock()
p.controllerConn = controllerConn p.controllerConn = controllerConn
p.mu.Unlock() p.mu.Unlock()
// Wrap pluginConn using CloseNotifyConn // Wrap with CloseNotifyConn which supports both close notification and error recording
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() { var closeErr error
close(currentCloseSignal) // Signal the run loop pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
closeErr = err
close(currentCloseSignal) // Signal the run loop on close.
}) })
xl.Infof("Attempting to register client route for visitor [%s]", p.pluginCtx.Name) xl.Infof("attempting to register client route for visitor [%s]", p.pluginCtx.Name)
err := p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn) p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
if err != nil { xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
xl.Errorf("Failed to register client route for visitor [%s]: %v. Retrying after %v", p.pluginCtx.Name, err, reconnectDelay)
p.cleanupPipePair(xl, controllerConn, pluginConn) // Close both ends on registration failure
// Wait before retrying registration, unless context is cancelled // Pass the CloseNotifyConn to the visitor for handling.
select { // The visitor can call CloseWithError to record the failure reason.
case <-time.After(reconnectDelay): p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
continue // Retry the loop
case <-p.ctx.Done():
xl.Infof("VirtualNetPlugin registration retry wait interrupted for visitor [%s]", p.pluginCtx.Name)
return // Exit loop if context is cancelled during wait
}
}
xl.Infof("Successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name) // Wait for context cancellation or connection close.
// Pass the CloseNotifyConn to HandleConn.
// HandleConn is responsible for calling Close() on pluginNotifyConn.
p.pluginCtx.HandleConn(pluginNotifyConn)
// Wait for either the plugin context to be cancelled or the wrapper's Close() to be called via the signal channel.
select { select {
case <-p.ctx.Done(): case <-p.ctx.Done():
xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name) xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name)
// Context cancelled, ensure controller side is closed if HandleConn didn't close its side yet.
p.cleanupControllerConn(xl) p.cleanupControllerConn(xl)
return return
case <-currentCloseSignal: case <-currentCloseSignal:
xl.Infof("Detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name) // Determine reconnect delay based on error with exponential backoff
// HandleConn closed the plugin side (pluginNotifyConn). The closeFn was called, closing currentCloseSignal. var reconnectDelay time.Duration
// We still need to close the controller side. if closeErr != nil {
p.consecutiveErrors++
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
baseDelay := 60 * time.Second
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
if reconnectDelay > 300*time.Second {
reconnectDelay = 300 * time.Second
}
} else {
// Reset consecutive errors on successful connection
if p.consecutiveErrors > 0 {
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
p.pluginCtx.Name, p.consecutiveErrors)
p.consecutiveErrors = 0
} else {
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
}
reconnectDelay = 10 * time.Second
}
// The visitor closed the plugin side. Close the controller side.
p.cleanupControllerConn(xl) p.cleanupControllerConn(xl)
// Add a delay before attempting to reconnect, respecting context cancellation. xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
xl.Infof("Waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
select { select {
case <-time.After(reconnectDelay): case <-time.After(reconnectDelay):
// Delay completed, loop will continue.
case <-p.ctx.Done(): case <-p.ctx.Done():
xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name) xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name)
return // Exit loop if context is cancelled during wait return
} }
// Loop will continue to reconnect.
} }
// Loop will restart, context check at the beginning of the loop is sufficient. xl.Infof("re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name)
xl.Infof("Re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name)
} }
} }
@@ -187,46 +190,31 @@ func (p *VirtualNetPlugin) cleanupControllerConn(xl *xlog.Logger) {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
if p.controllerConn != nil { if p.controllerConn != nil {
xl.Debugf("Cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name) xl.Debugf("cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name)
p.controllerConn.Close() p.controllerConn.Close()
p.controllerConn = nil p.controllerConn = nil
} }
// Also clear the closeSignal reference for the completed/cancelled connection attempt
p.closeSignal = nil p.closeSignal = nil
} }
// cleanupPipePair closes both ends of a pipe, used typically when registration fails.
func (p *VirtualNetPlugin) cleanupPipePair(xl *xlog.Logger, controllerConn, pluginConn net.Conn) {
xl.Debugf("Cleaning up pipe pair for visitor [%s] after registration failure", p.pluginCtx.Name)
controllerConn.Close()
pluginConn.Close()
p.mu.Lock()
p.controllerConn = nil // Ensure field is nil if it was briefly set
p.closeSignal = nil // Ensure field is nil if it was briefly set
p.mu.Unlock()
}
// Close initiates the plugin shutdown. // Close initiates the plugin shutdown.
func (p *VirtualNetPlugin) Close() error { func (p *VirtualNetPlugin) Close() error {
xl := xlog.FromContextSafe(p.pluginCtx.Ctx) // Use base context for close logging xl := xlog.FromContextSafe(p.pluginCtx.Ctx)
xl.Infof("Closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name) xl.Infof("closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name)
// 1. Signal the run loop goroutine to stop via context cancellation. // Signal the run loop goroutine to stop.
p.cancel() p.cancel()
// 2. Unregister the route from the controller. // Unregister the route from the controller.
// This might implicitly cause the VnetController to close its end of the pipe (controllerConn).
if p.pluginCtx.VnetController != nil { if p.pluginCtx.VnetController != nil {
p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name) p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name)
xl.Infof("Unregistered client route for visitor [%s]", p.pluginCtx.Name) xl.Infof("unregistered client route for visitor [%s]", p.pluginCtx.Name)
} else {
xl.Warnf("VnetController is nil during close for visitor [%s], cannot unregister route", p.pluginCtx.Name)
} }
// 3. Explicitly close the controller side of the pipe managed by this plugin. // Explicitly close the controller side of the pipe.
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end. // This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
p.cleanupControllerConn(xl) p.cleanupControllerConn(xl)
xl.Infof("Finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
return nil return nil
} }

View File

@@ -0,0 +1,34 @@
package security
const (
TokenSourceExec = "TokenSourceExec"
)
var (
ClientUnsafeFeatures = []string{
TokenSourceExec,
}
ServerUnsafeFeatures = []string{
TokenSourceExec,
}
)
type UnsafeFeatures struct {
features map[string]bool
}
func NewUnsafeFeatures(allowed []string) *UnsafeFeatures {
features := make(map[string]bool)
for _, f := range allowed {
features[f] = true
}
return &UnsafeFeatures{features: features}
}
func (u *UnsafeFeatures) IsEnabled(feature string) bool {
if u == nil {
return false
}
return u.features[feature]
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/fatedier/golib/pool" "github.com/fatedier/golib/pool"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket { func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
@@ -69,7 +70,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
} }
} }
func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) { func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) {
var mu sync.RWMutex var mu sync.RWMutex
udpConnMap := make(map[string]*net.UDPConn) udpConnMap := make(map[string]*net.UDPConn)
@@ -110,6 +111,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
if err != nil { if err != nil {
continue continue
} }
mu.Lock() mu.Lock()
udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()] udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()]
if !ok { if !ok {
@@ -122,6 +124,18 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
} }
mu.Unlock() mu.Unlock()
// Add proxy protocol header if configured
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
if err == nil {
// Prepend proxy protocol header to the UDP payload
finalBuf := make([]byte, len(ppBuf)+len(buf))
copy(finalBuf, ppBuf)
copy(finalBuf[len(ppBuf):], buf)
buf = finalBuf
}
}
_, err = udpConn.Write(buf) _, err = udpConn.Write(buf)
if err != nil { if err != nil {
udpConn.Close() udpConn.Close()

View File

@@ -3,16 +3,16 @@ package udp
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestUdpPacket(t *testing.T) { func TestUdpPacket(t *testing.T) {
assert := assert.New(t) require := require.New(t)
buf := []byte("hello world") buf := []byte("hello world")
udpMsg := NewUDPPacket(buf, nil, nil) udpMsg := NewUDPPacket(buf, nil, nil)
newBuf, err := GetContent(udpMsg) newBuf, err := GetContent(udpMsg)
assert.NoError(err) require.NoError(err)
assert.EqualValues(buf, newBuf) require.EqualValues(buf, newBuf)
} }

View File

@@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error {
s.writeToClient(err.Error()) s.writeToClient(err.Error())
return fmt.Errorf("parse flags from ssh client error: %v", err) return fmt.Errorf("parse flags from ssh client error: %v", err)
} }
clientCfg.Complete() if err := clientCfg.Complete(); err != nil {
s.writeToClient(fmt.Sprintf("failed to complete client config: %v", err))
return fmt.Errorf("complete client config error: %v", err)
}
if sshConn.Permissions != nil { if sshConn.Permissions != nil {
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
} }

View File

@@ -35,15 +35,19 @@ type MessageTransporter interface {
DispatchWithType(m msg.Message, msgType, laneKey string) bool DispatchWithType(m msg.Message, msgType, laneKey string) bool
} }
func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter { type MessageSender interface {
Send(msg.Message) error
}
func NewMessageTransporter(sender MessageSender) MessageTransporter {
return &transporterImpl{ return &transporterImpl{
sendCh: sendCh, sender: sender,
registry: make(map[string]map[string]chan msg.Message), registry: make(map[string]map[string]chan msg.Message),
} }
} }
type transporterImpl struct { type transporterImpl struct {
sendCh chan msg.Message sender MessageSender
// First key is message type and second key is lane key. // First key is message type and second key is lane key.
// Dispatch will dispatch message to related channel by its message type // Dispatch will dispatch message to related channel by its message type
@@ -53,9 +57,7 @@ type transporterImpl struct {
} }
func (impl *transporterImpl) Send(m msg.Message) error { func (impl *transporterImpl) Send(m msg.Message) error {
return errors.PanicToError(func() { return impl.sender.Send(m)
impl.sendCh <- m
})
} }
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) { func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {

View File

@@ -22,6 +22,7 @@ import (
"encoding/pem" "encoding/pem"
"math/big" "math/big"
"os" "os"
"time"
) )
func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) {
@@ -32,12 +33,30 @@ func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) {
return &tlsCert, nil return &tlsCert, nil
} }
func newRandomTLSKeyPair() *tls.Certificate { func newRandomTLSKeyPair() (*tls.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048) key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
panic(err) return nil, err
} }
template := x509.Certificate{SerialNumber: big.NewInt(1)}
// Generate a random positive serial number with 128 bits of entropy.
// RFC 5280 requires serial numbers to be positive integers (not zero).
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}
// Ensure serial number is positive (not zero)
if serialNumber.Sign() == 0 {
serialNumber = big.NewInt(1)
}
template := x509.Certificate{
SerialNumber: serialNumber,
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(365 * 24 * time.Hour * 10),
}
certDER, err := x509.CreateCertificate( certDER, err := x509.CreateCertificate(
rand.Reader, rand.Reader,
&template, &template,
@@ -45,16 +64,16 @@ func newRandomTLSKeyPair() *tls.Certificate {
&key.PublicKey, &key.PublicKey,
key) key)
if err != nil { if err != nil {
panic(err) return nil, err
} }
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil { if err != nil {
panic(err) return nil, err
} }
return &tlsCert return &tlsCert, nil
} }
// Only support one ca file to add // Only support one ca file to add
@@ -76,7 +95,10 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) {
if certPath == "" || keyPath == "" { if certPath == "" || keyPath == "" {
// server will generate tls conf by itself // server will generate tls conf by itself
cert := newRandomTLSKeyPair() cert, err := newRandomTLSKeyPair()
if err != nil {
return nil, err
}
base.Certificates = []tls.Certificate{*cert} base.Certificates = []tls.Certificate{*cert}
} else { } else {
cert, err := newCustomTLSKeyPair(certPath, keyPath) cert, err := newCustomTLSKeyPair(certPath, keyPath)

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

@@ -0,0 +1,18 @@
package banner
import (
"fmt"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
func DisplayBanner() {
fmt.Println(" __ ___ __________ ____ ________ ____")
fmt.Println(" / / ____ / (_)___ _/ ____/ __ \\/ __ \\ / ____/ / / _/")
fmt.Println(" / / / __ \\/ / / __ `/ /_ / /_/ / /_/ /_____/ / / / / / ")
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
fmt.Println(" ")
log.Infof("Nya! %s 启动中", version.Full())
}

View File

@@ -16,13 +16,18 @@ package log
import ( import (
"bytes" "bytes"
"io"
"os" "os"
"path/filepath"
"strings"
"time"
"github.com/fatedier/golib/log" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
) )
var ( var (
TraceLevel = log.TraceLevel TraceLevel = log.DebugLevel
DebugLevel = log.DebugLevel DebugLevel = log.DebugLevel
InfoLevel = log.InfoLevel InfoLevel = log.InfoLevel
WarnLevel = log.WarnLevel WarnLevel = log.WarnLevel
@@ -32,39 +37,158 @@ var (
var Logger *log.Logger var Logger *log.Logger
func init() { func init() {
Logger = log.New( Logger = log.NewWithOptions(os.Stderr, log.Options{
log.WithCaller(true), ReportCaller: true,
log.AddCallerSkip(1), ReportTimestamp: true,
log.WithLevel(log.InfoLevel), 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) { func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) {
options := []log.Option{} var output io.Writer
var err error
if logPath == "console" { if logPath == "console" {
if !disableLogColor { output = os.Stdout
options = append(options,
log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{
Colorful: true,
}, os.Stdout)),
)
}
} else { } else {
writer := log.NewRotateFileWriter(log.RotateFileConfig{ // Use rotating file writer
FileName: logPath, output, err = NewRotateFileWriter(logPath, maxDays)
Mode: log.RotateFileModeDaily, if err != nil {
MaxDays: maxDays, // Fallback to console if file creation fails
}) output = os.Stdout
writer.Init() }
options = append(options, log.WithOutput(writer))
} }
level, err := log.ParseLevel(levelStr) level, err := log.ParseLevel(levelStr)
if err != nil { if err != nil {
level = log.InfoLevel 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) { func Errorf(format string, v ...any) {
@@ -75,6 +199,10 @@ func Warnf(format string, v ...any) {
Logger.Warnf(format, v...) Logger.Warnf(format, v...)
} }
func Info(format string, v ...any) {
Logger.Info(format, v...)
}
func Infof(format string, v ...any) { func Infof(format string, v ...any) {
Logger.Infof(format, v...) Logger.Infof(format, v...)
} }
@@ -84,11 +212,12 @@ func Debugf(format string, v ...any) {
} }
func Tracef(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) { 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 { 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) { 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 return len(p), nil
} }

View File

@@ -3,21 +3,21 @@ package metric
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestCounter(t *testing.T) { func TestCounter(t *testing.T) {
assert := assert.New(t) require := require.New(t)
c := NewCounter() c := NewCounter()
c.Inc(10) c.Inc(10)
assert.EqualValues(10, c.Count()) require.EqualValues(10, c.Count())
c.Dec(5) c.Dec(5)
assert.EqualValues(5, c.Count()) require.EqualValues(5, c.Count())
cTmp := c.Snapshot() cTmp := c.Snapshot()
assert.EqualValues(5, cTmp.Count()) require.EqualValues(5, cTmp.Count())
c.Clear() c.Clear()
assert.EqualValues(0, c.Count()) require.EqualValues(0, c.Count())
} }

View File

@@ -3,25 +3,25 @@ package metric
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestDateCounter(t *testing.T) { func TestDateCounter(t *testing.T) {
assert := assert.New(t) require := require.New(t)
dc := NewDateCounter(3) dc := NewDateCounter(3)
dc.Inc(10) dc.Inc(10)
assert.EqualValues(10, dc.TodayCount()) require.EqualValues(10, dc.TodayCount())
dc.Dec(5) dc.Dec(5)
assert.EqualValues(5, dc.TodayCount()) require.EqualValues(5, dc.TodayCount())
counts := dc.GetLastDaysCount(3) counts := dc.GetLastDaysCount(3)
assert.EqualValues(3, len(counts)) require.EqualValues(3, len(counts))
assert.EqualValues(5, counts[0]) require.EqualValues(5, counts[0])
assert.EqualValues(0, counts[1]) require.EqualValues(0, counts[1])
assert.EqualValues(0, counts[2]) require.EqualValues(0, counts[2])
dcTmp := dc.Snapshot() dcTmp := dc.Snapshot()
assert.EqualValues(5, dcTmp.TodayCount()) require.EqualValues(5, dcTmp.TodayCount())
} }

View File

@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
// 1 means closed // 1 means closed
closeFlag int32 closeFlag int32
closeFn func() closeFn func(error)
} }
// closeFn will be only called once // closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn { func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
return &CloseNotifyConn{ return &CloseNotifyConn{
Conn: c, Conn: c,
closeFn: closeFn, closeFn: closeFn,
@@ -149,14 +149,27 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
func (cc *CloseNotifyConn) Close() (err error) { func (cc *CloseNotifyConn) Close() (err error) {
pflag := atomic.SwapInt32(&cc.closeFlag, 1) pflag := atomic.SwapInt32(&cc.closeFlag, 1)
if pflag == 0 { if pflag == 0 {
err = cc.Close() err = cc.Conn.Close()
if cc.closeFn != nil { if cc.closeFn != nil {
cc.closeFn() cc.closeFn(nil)
} }
} }
return return
} }
// CloseWithError closes the connection and passes the error to the close callback.
func (cc *CloseNotifyConn) CloseWithError(err error) error {
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
if pflag == 0 {
closeErr := cc.Conn.Close()
if cc.closeFn != nil {
cc.closeFn(err)
}
return closeErr
}
return nil
}
type StatsConn struct { type StatsConn struct {
net.Conn net.Conn
@@ -197,11 +210,11 @@ func (statsConn *StatsConn) Close() (err error) {
} }
type wrapQuicStream struct { type wrapQuicStream struct {
quic.Stream *quic.Stream
c quic.Connection c *quic.Conn
} }
func QuicStreamToNetConn(s quic.Stream, c quic.Connection) net.Conn { func QuicStreamToNetConn(s *quic.Stream, c *quic.Conn) net.Conn {
return &wrapQuicStream{ return &wrapQuicStream{
Stream: s, Stream: s,
c: c, c: c,
@@ -223,7 +236,7 @@ func (conn *wrapQuicStream) RemoteAddr() net.Addr {
} }
func (conn *wrapQuicStream) Close() error { func (conn *wrapQuicStream) Close() error {
conn.Stream.CancelRead(0) conn.CancelRead(0)
return conn.Stream.Close() return conn.Stream.Close()
} }

View File

@@ -0,0 +1,45 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net
import (
"bytes"
"fmt"
"net"
pp "github.com/pires/go-proxyproto"
)
func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header {
var versionByte byte
if version == "v1" {
versionByte = 1
} else {
versionByte = 2 // default to v2
}
return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr)
}
func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) {
h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version)
// Convert header to bytes using a buffer
var buf bytes.Buffer
_, err := h.WriteTo(&buf)
if err != nil {
return nil, fmt.Errorf("failed to write proxy protocol header: %v", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,178 @@
package net
import (
"net"
"testing"
pp "github.com/pires/go-proxyproto"
"github.com/stretchr/testify/require"
)
func TestBuildProxyProtocolHeader(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectError bool
}{
{
name: "UDP IPv4 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv4 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "UDP IPv6 v2",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "TCP IPv6 v1",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "v1",
expectError: false,
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectError: false,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectError: false,
},
}
for _, tt := range tests {
header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version)
if tt.expectError {
require.Error(err, "test case: %s", tt.name)
continue
}
require.NoError(err, "test case: %s", tt.name)
require.NotEmpty(header, "test case: %s", tt.name)
}
}
func TestBuildProxyProtocolHeaderStruct(t *testing.T) {
require := require.New(t)
tests := []struct {
name string
srcAddr net.Addr
dstAddr net.Addr
version string
expectedProtocol pp.AddressFamilyAndProtocol
expectedVersion byte
expectedCommand pp.ProtocolVersionAndCommand
expectedSourceAddr net.Addr
expectedDestAddr net.Addr
}{
{
name: "TCP IPv4 v2",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
version: "v2",
expectedProtocol: pp.TCPv4,
expectedVersion: 2,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80},
},
{
name: "UDP IPv6 v1",
srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
version: "v1",
expectedProtocol: pp.UDPv6,
expectedVersion: 1,
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345},
expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306},
},
{
name: "TCP IPv6 default version",
srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
version: "",
expectedProtocol: pp.TCPv6,
expectedVersion: 2, // default to v2
expectedCommand: pp.PROXY,
expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345},
expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80},
},
{
name: "nil source address",
srcAddr: nil,
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil
expectedDestAddr: nil,
},
{
name: "nil destination address",
srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345},
dstAddr: nil,
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil
expectedDestAddr: nil,
},
{
name: "unsupported address type",
srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"},
dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306},
version: "v2",
expectedProtocol: pp.UNSPEC,
expectedVersion: 2,
expectedCommand: pp.LOCAL,
expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types
expectedDestAddr: nil,
},
}
for _, tt := range tests {
header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version)
require.NotNil(header, "test case: %s", tt.name)
require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name)
require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name)
require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name)
require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name)
require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name)
}
}

View File

@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
muxer := http.NewServeMux() muxer := http.NewServeMux()
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) { muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
notifyCh := make(chan struct{}) notifyCh := make(chan struct{})
conn := WrapCloseNotifyConn(c, func() { conn := WrapCloseNotifyConn(c, func(_ error) {
close(notifyCh) close(notifyCh)
}) })
wl.acceptCh <- conn wl.acceptCh <- conn

View File

@@ -3,45 +3,41 @@ package util
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestRandId(t *testing.T) { func TestRandId(t *testing.T) {
assert := assert.New(t) require := require.New(t)
id, err := RandID() id, err := RandID()
assert.NoError(err) require.NoError(err)
t.Log(id) t.Log(id)
assert.Equal(16, len(id)) require.Equal(16, len(id))
} }
func TestGetAuthKey(t *testing.T) { func TestGetAuthKey(t *testing.T) {
assert := assert.New(t) require := require.New(t)
key := GetAuthKey("1234", 1488720000) key := GetAuthKey("1234", 1488720000)
assert.Equal("6df41a43725f0c770fd56379e12acf8c", key) require.Equal("6df41a43725f0c770fd56379e12acf8c", key)
} }
func TestParseRangeNumbers(t *testing.T) { func TestParseRangeNumbers(t *testing.T) {
assert := assert.New(t) require := require.New(t)
numbers, err := ParseRangeNumbers("2-5") numbers, err := ParseRangeNumbers("2-5")
if assert.NoError(err) { require.NoError(err)
assert.Equal([]int64{2, 3, 4, 5}, numbers) require.Equal([]int64{2, 3, 4, 5}, numbers)
}
numbers, err = ParseRangeNumbers("1") numbers, err = ParseRangeNumbers("1")
if assert.NoError(err) { require.NoError(err)
assert.Equal([]int64{1}, numbers) require.Equal([]int64{1}, numbers)
}
numbers, err = ParseRangeNumbers("3-5,8") numbers, err = ParseRangeNumbers("3-5,8")
if assert.NoError(err) { require.NoError(err)
assert.Equal([]int64{3, 4, 5, 8}, numbers) require.Equal([]int64{3, 4, 5, 8}, numbers)
}
numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ") numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ")
if assert.NoError(err) { require.NoError(err)
assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers) require.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers)
}
_, err = ParseRangeNumbers("3-a") _, err = ParseRangeNumbers("3-a")
assert.Error(err) require.Error(err)
} }

View File

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

View File

@@ -162,7 +162,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig { func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser) vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok { if ok {
log.Debugf("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) log.Debugf("get new http request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
return vr.payload.(*RouteConfig) return vr.payload.(*RouteConfig)
} }
return nil return nil
@@ -225,11 +225,7 @@ func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (
// *.example.com // *.example.com
// *.com // *.com
domainSplit := strings.Split(domain, ".") domainSplit := strings.Split(domain, ".")
for { for len(domainSplit) >= 3 {
if len(domainSplit) < 3 {
break
}
domainSplit[0] = "*" domainSplit[0] = "*"
domain = strings.Join(domainSplit, ".") domain = strings.Join(domainSplit, ".")
vr, ok = findRouter(domain, location, routeByHTTPUser) vr, ok = findRouter(domain, location, routeByHTTPUser)

View File

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

View File

@@ -169,11 +169,7 @@ func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) {
} }
domainSplit := strings.Split(name, ".") domainSplit := strings.Split(name, ".")
for { for len(domainSplit) >= 3 {
if len(domainSplit) < 3 {
break
}
domainSplit[0] = "*" domainSplit[0] = "*"
name = strings.Join(domainSplit, ".") name = strings.Join(domainSplit, ".")
@@ -275,7 +271,7 @@ func (l *Listener) Accept() (net.Conn, error) {
xl := xlog.FromContextSafe(l.ctx) xl := xlog.FromContextSafe(l.ctx)
conn, ok := <-l.accept conn, ok := <-l.accept
if !ok { if !ok {
return nil, fmt.Errorf("Listener closed") return nil, fmt.Errorf("listener closed")
} }
// if rewriteHost func is exist // if rewriteHost func is exist

View File

@@ -0,0 +1,65 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package xlog
import "strings"
// LogWriter forwards writes to frp's logger at configurable level.
// It is safe for concurrent use as long as the underlying Logger is thread-safe.
type LogWriter struct {
xl *Logger
logFunc func(string)
}
func (w LogWriter) Write(p []byte) (n int, err error) {
msg := strings.TrimSpace(string(p))
w.logFunc(msg)
return len(p), nil
}
func NewTraceWriter(xl *Logger) LogWriter {
return LogWriter{
xl: xl,
logFunc: func(msg string) { xl.Tracef("%s", msg) },
}
}
func NewDebugWriter(xl *Logger) LogWriter {
return LogWriter{
xl: xl,
logFunc: func(msg string) { xl.Debugf("%s", msg) },
}
}
func NewInfoWriter(xl *Logger) LogWriter {
return LogWriter{
xl: xl,
logFunc: func(msg string) { xl.Infof("%s", msg) },
}
}
func NewWarnWriter(xl *Logger) LogWriter {
return LogWriter{
xl: xl,
logFunc: func(msg string) { xl.Warnf("%s", msg) },
}
}
func NewErrorWriter(xl *Logger) LogWriter {
return LogWriter{
xl: xl,
logFunc: func(msg string) { xl.Errorf("%s", msg) },
}
}

View File

@@ -111,5 +111,5 @@ func (l *Logger) Debugf(format string, v ...any) {
} }
func (l *Logger) Tracef(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

@@ -37,7 +37,9 @@ type Client struct {
func NewClient(options ClientOptions) (*Client, error) { func NewClient(options ClientOptions) (*Client, error) {
if options.Common != nil { if options.Common != nil {
options.Common.Complete() if err := options.Common.Complete(); err != nil {
return nil, err
}
} }
ln := netpkg.NewInternalListener() ln := netpkg.NewInternalListener()

View File

@@ -87,7 +87,7 @@ func (c *Controller) handlePacket(buf []byte) {
case waterutil.IsIPv4(buf): case waterutil.IsIPv4(buf):
header, err := ipv4.ParseHeader(buf) header, err := ipv4.ParseHeader(buf)
if err != nil { if err != nil {
log.Warnf("parse ipv4 header error:", err) log.Warnf("parse ipv4 header error: %v", err)
return return
} }
src = header.Src src = header.Src
@@ -98,7 +98,7 @@ func (c *Controller) handlePacket(buf []byte) {
case waterutil.IsIPv6(buf): case waterutil.IsIPv6(buf):
header, err := ipv6.ParseHeader(buf) header, err := ipv6.ParseHeader(buf)
if err != nil { if err != nil {
log.Warnf("parse ipv6 header error:", err) log.Warnf("parse ipv6 header error: %v", err)
return return
} }
src = header.Src src = header.Src
@@ -137,6 +137,12 @@ func (c *Controller) Stop() error {
// Client connection read loop // Client connection read loop
func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) { func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) {
xl := xlog.FromContextSafe(ctx) xl := xlog.FromContextSafe(ctx)
defer func() {
// Remove the route when read loop ends (connection closed)
c.clientRouter.removeConnRoute(conn)
conn.Close()
}()
for { for {
data, err := ReadMessage(conn) data, err := ReadMessage(conn)
if err != nil { if err != nil {
@@ -181,8 +187,18 @@ func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser
} }
// Server connection read loop // Server connection read loop
func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser) { func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser, onClose func()) {
xl := xlog.FromContextSafe(ctx) xl := xlog.FromContextSafe(ctx)
defer func() {
// Clean up all IP mappings associated with this connection when it closes
c.serverRouter.cleanupConnIPs(conn)
// Call the provided callback upon closure
if onClose != nil {
onClose()
}
conn.Close()
}()
for { for {
data, err := ReadMessage(conn) data, err := ReadMessage(conn)
if err != nil { if err != nil {
@@ -220,27 +236,11 @@ func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser
} }
} }
// RegisterClientRoute Register client route (based on destination IP CIDR) // RegisterClientRoute registers a client route (based on destination IP CIDR)
func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) error { // and starts the read loop
if err := c.clientRouter.addRoute(name, routes, conn); err != nil { func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) {
return err c.clientRouter.addRoute(name, routes, conn)
}
go c.readLoopClient(ctx, conn) go c.readLoopClient(ctx, conn)
return nil
}
// RegisterServerConn Register server connection (dynamically associates with source IPs)
func (c *Controller) RegisterServerConn(ctx context.Context, name string, conn io.ReadWriteCloser) error {
if err := c.serverRouter.addConn(name, conn); err != nil {
return err
}
go c.readLoopServer(ctx, conn)
return nil
}
// UnregisterServerConn Remove server connection from routing table
func (c *Controller) UnregisterServerConn(name string) {
c.serverRouter.delConn(name)
} }
// UnregisterClientRoute Remove client route from routing table // UnregisterClientRoute Remove client route from routing table
@@ -248,6 +248,12 @@ func (c *Controller) UnregisterClientRoute(name string) {
c.clientRouter.delRoute(name) c.clientRouter.delRoute(name)
} }
// StartServerConnReadLoop starts the read loop for a server connection
// (dynamically associates with source IPs)
func (c *Controller) StartServerConnReadLoop(ctx context.Context, conn io.ReadWriteCloser, onClose func()) {
go c.readLoopServer(ctx, conn, onClose)
}
// ParseRoutes Convert route strings to IPNet objects // ParseRoutes Convert route strings to IPNet objects
func ParseRoutes(routeStrings []string) ([]net.IPNet, error) { func ParseRoutes(routeStrings []string) ([]net.IPNet, error) {
routes := make([]net.IPNet, 0, len(routeStrings)) routes := make([]net.IPNet, 0, len(routeStrings))
@@ -273,7 +279,7 @@ func newClientRouter() *clientRouter {
} }
} }
func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error { func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
r.routes[name] = &routeElement{ r.routes[name] = &routeElement{
@@ -281,7 +287,6 @@ func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWri
routes: routes, routes: routes,
conn: conn, conn: conn,
} }
return nil
} }
func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) { func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) {
@@ -303,32 +308,29 @@ func (r *clientRouter) delRoute(name string) {
delete(r.routes, name) delete(r.routes, name)
} }
// Server router (based on source IP routing) func (r *clientRouter) removeConnRoute(conn io.Writer) {
r.mu.Lock()
defer r.mu.Unlock()
for name, re := range r.routes {
if re.conn == conn {
delete(r.routes, name)
return
}
}
}
// Server router (based solely on source IP routing)
type serverRouter struct { type serverRouter struct {
namedConns map[string]io.ReadWriteCloser // Name to connection mapping srcIPConns map[string]io.Writer // Source IP string to connection mapping
srcIPConns map[string]io.Writer // Source IP string to connection mapping
mu sync.RWMutex mu sync.RWMutex
} }
func newServerRouter() *serverRouter { func newServerRouter() *serverRouter {
return &serverRouter{ return &serverRouter{
namedConns: make(map[string]io.ReadWriteCloser),
srcIPConns: make(map[string]io.Writer), srcIPConns: make(map[string]io.Writer),
} }
} }
func (r *serverRouter) addConn(name string, conn io.ReadWriteCloser) error {
r.mu.Lock()
original, ok := r.namedConns[name]
r.namedConns[name] = conn
r.mu.Unlock()
if ok {
// Close the original connection if it exists
_ = original.Close()
}
return nil
}
func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) { func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@@ -340,17 +342,41 @@ func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) {
} }
func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) { func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) {
key := src.String()
r.mu.RLock()
existingConn, ok := r.srcIPConns[key]
r.mu.RUnlock()
// If the entry exists and the connection is the same, no need to do anything.
if ok && existingConn == conn {
return
}
// Acquire write lock to update the map.
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
r.srcIPConns[src.String()] = conn
// Double-check after acquiring the write lock to handle potential race conditions.
existingConn, ok = r.srcIPConns[key]
if ok && existingConn == conn {
return
}
r.srcIPConns[key] = conn
} }
func (r *serverRouter) delConn(name string) { // cleanupConnIPs removes all IP mappings associated with the specified connection
func (r *serverRouter) cleanupConnIPs(conn io.Writer) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
delete(r.namedConns, name)
// Note: We don't delete mappings from srcIPConns because we don't know which source IPs are associated with this connection // Find and delete all IP mappings pointing to this connection
// This might cause dangling references, but they will be overwritten on new connections or restart for ip, mappedConn := range r.srcIPConns {
if mappedConn == conn {
delete(r.srcIPConns, ip)
}
}
} }
type routeElement struct { type routeElement struct {

View File

@@ -33,7 +33,7 @@ func ReadMessage(r io.Reader) ([]byte, error) {
var length uint32 var length uint32
err := binary.Read(r, binary.LittleEndian, &length) err := binary.Read(r, binary.LittleEndian, &length)
if err != nil { if err != nil {
return nil, fmt.Errorf("read message length error: %v", err) return nil, fmt.Errorf("read message length error: %w", err)
} }
// Check length to prevent DoS // Check length to prevent DoS
@@ -48,7 +48,7 @@ func ReadMessage(r io.Reader) ([]byte, error) {
data := make([]byte, length) data := make([]byte, length)
_, err = io.ReadFull(r, data) _, err = io.ReadFull(r, data)
if err != nil { if err != nil {
return nil, fmt.Errorf("read message data error: %v", err) return nil, fmt.Errorf("read message data error: %w", err)
} }
return data, nil return data, nil
@@ -68,13 +68,13 @@ func WriteMessage(w io.Writer, data []byte) error {
// Write length // Write length
err := binary.Write(w, binary.LittleEndian, length) err := binary.Write(w, binary.LittleEndian, length)
if err != nil { if err != nil {
return fmt.Errorf("write message length error: %v", err) return fmt.Errorf("write message length error: %w", err)
} }
// Write message data // Write message data
_, err = w.Write(data) _, err = w.Write(data)
if err != nil { if err != nil {
return fmt.Errorf("write message data error: %v", err) return fmt.Errorf("write message data error: %w", err)
} }
return nil return nil

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