forked from Mxmilu666/frp
Compare commits
19 Commits
v0.66.0
...
72d147dfa5
| Author | SHA1 | Date | |
|---|---|---|---|
|
72d147dfa5
|
|||
|
481121a6c2
|
|||
|
be252de683
|
|||
|
0a99c1071b
|
|||
|
dd37b2e199
|
|||
|
803e548f42
|
|||
|
2dac44ac2e
|
|||
|
655dc3cb2a
|
|||
|
9894342f46
|
|||
|
e7cc706c86
|
|||
|
92ac2b9153
|
|||
|
1ed369e962
|
|||
|
b74a8d0232
|
|||
|
d2180081a0
|
|||
|
51f4e065b5
|
|||
|
e58f774086
|
|||
|
178e381a26
|
|||
|
26b93ae3a3
|
|||
|
a2aeee28e4
|
8
.github/workflows/build-all.yaml
vendored
8
.github/workflows/build-all.yaml
vendored
@@ -4,8 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
tags:
|
|
||||||
- '**'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
@@ -20,16 +18,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
goos: [linux, windows, darwin, freebsd, openbsd, android]
|
goos: [linux, windows, darwin, freebsd, openbsd, android]
|
||||||
goarch: [amd64, arm, arm64]
|
goarch: [amd64, 386, arm, arm64]
|
||||||
exclude:
|
exclude:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm
|
goarch: arm
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
|
- goos: android
|
||||||
|
goarch: 386
|
||||||
# 排除 Android ARM 32位,在单独的 job 中处理
|
# 排除 Android ARM 32位,在单独的 job 中处理
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm
|
goarch: arm
|
||||||
|
|||||||
83
.github/workflows/build-and-push-image.yml
vendored
83
.github/workflows/build-and-push-image.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: Build Image and Publish to Dockerhub & GPR
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Image tag'
|
|
||||||
required: true
|
|
||||||
default: 'test'
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
image:
|
|
||||||
name: Build Image from Dockerfile and binaries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# environment
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# get image tag name
|
|
||||||
- name: Get Image Tag Name
|
|
||||||
run: |
|
|
||||||
if [ x${{ github.event.inputs.tag }} == x"" ]; then
|
|
||||||
echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Login to the GPR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GPR_TOKEN }}
|
|
||||||
|
|
||||||
# prepare image tags
|
|
||||||
- name: Prepare Image Tags
|
|
||||||
run: |
|
|
||||||
echo "DOCKERFILE_FRPC_PATH=dockerfiles/Dockerfile-for-frpc" >> $GITHUB_ENV
|
|
||||||
echo "DOCKERFILE_FRPS_PATH=dockerfiles/Dockerfile-for-frps" >> $GITHUB_ENV
|
|
||||||
echo "TAG_FRPC=fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
|
||||||
echo "TAG_FRPS=fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
|
||||||
echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
|
||||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build and push frpc
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./dockerfiles/Dockerfile-for-frpc
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.TAG_FRPC }}
|
|
||||||
${{ env.TAG_FRPC_GPR }}
|
|
||||||
|
|
||||||
- name: Build and push frps
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./dockerfiles/Dockerfile-for-frps
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.TAG_FRPS }}
|
|
||||||
${{ env.TAG_FRPS_GPR }}
|
|
||||||
82
.github/workflows/docker-build.yml
vendored
Normal file
82
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: frps
|
||||||
|
dockerfile: dockerfiles/Dockerfile-for-frps
|
||||||
|
image_name: ghcr.io/${{ github.repository_owner }}/loliacli-frps
|
||||||
|
- name: frpc
|
||||||
|
dockerfile: dockerfiles/Dockerfile-for-frpc
|
||||||
|
image_name: ghcr.io/${{ github.repository_owner }}/loliacli-frpc
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ matrix.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
- name: Generate image digest
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
echo "Image pushed to: ${{ matrix.image_name }}"
|
||||||
|
echo "Tags: ${{ steps.meta.outputs.tags }}"
|
||||||
98
.github/workflows/release.yaml
vendored
98
.github/workflows/release.yaml
vendored
@@ -3,11 +3,11 @@ name: Release FRP Binaries
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Tag to release (e.g., v1.0.0)'
|
description: "Tag to release (e.g., v1.0.0)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
# 调用 build-all workflow
|
# 调用 build-all workflow
|
||||||
build:
|
build:
|
||||||
uses: ./.github/workflows/build-all.yml
|
uses: ./.github/workflows/build-all.yaml
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
name: Create Release
|
name: Create Release
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -47,17 +47,49 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Display artifact structure
|
||||||
|
run: |
|
||||||
|
echo "Artifact structure:"
|
||||||
|
ls -R artifacts/
|
||||||
|
|
||||||
- name: Organize release files
|
- name: Organize release files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release_files
|
mkdir -p release_files
|
||||||
|
|
||||||
|
# 查找并复制所有压缩包
|
||||||
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" \) -exec cp {} 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/
|
ls -lh release_files/
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
cd release_files
|
cd release_files
|
||||||
sha256sum * > sha256sum.txt
|
if [ -n "$(ls -A .)" ]; then
|
||||||
cat sha256sum.txt
|
sha256sum * > sha256sum.txt
|
||||||
|
cat sha256sum.txt
|
||||||
|
else
|
||||||
|
echo "No files to generate checksums for!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Save checksums for changelog
|
- name: Save checksums for changelog
|
||||||
id: checksums
|
id: checksums
|
||||||
@@ -68,11 +100,26 @@ jobs:
|
|||||||
echo EOF
|
echo EOF
|
||||||
} >> $GITHUB_OUTPUT
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Debug - Check tags and commits
|
||||||
|
run: |
|
||||||
|
echo "Current tag: ${{ steps.tag.outputs.tag }}"
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 ${{ steps.tag.outputs.tag }}^ 2>/dev/null || echo "none")
|
||||||
|
echo "Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Commits between tags:"
|
||||||
|
if [ "$PREV_TAG" != "none" ]; then
|
||||||
|
git log --oneline $PREV_TAG..${{ steps.tag.outputs.tag }}
|
||||||
|
else
|
||||||
|
echo "First tag, showing all commits:"
|
||||||
|
git log --oneline ${{ steps.tag.outputs.tag }}
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build Changelog
|
- name: Build Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
uses: mikepenz/release-changelog-builder-action@v4
|
uses: mikepenz/release-changelog-builder-action@v4
|
||||||
with:
|
with:
|
||||||
configuration: |
|
configurationJson: |
|
||||||
{
|
{
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
@@ -102,30 +149,51 @@ jobs:
|
|||||||
],
|
],
|
||||||
"template": "#{{CHANGELOG}}\n\n## 📥 Download\n\n### Checksums (SHA256)\n\n```\n${{ steps.checksums.outputs.content }}\n```\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
|
"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}}",
|
"pr_template": "- #{{TITLE}} by @#{{AUTHOR}} in ##{{NUMBER}}",
|
||||||
"empty_template": "- No changes",
|
"commit_template": "- #{{TITLE}} by @#{{AUTHOR}}",
|
||||||
|
"empty_template": "- No changes in this release",
|
||||||
|
"mode": "COMMIT",
|
||||||
|
"sort": {
|
||||||
|
"order": "ASC",
|
||||||
|
"on_property": "mergedAt"
|
||||||
|
},
|
||||||
|
"max_tags_to_fetch": 200,
|
||||||
|
"max_pull_requests": 200,
|
||||||
|
"max_back_track_time_days": 365,
|
||||||
"label_extractor": [
|
"label_extractor": [
|
||||||
{
|
{
|
||||||
"pattern": "^(feat|feature)(\\(.+\\))?:",
|
"pattern": "^(feat|feature)(\\(.+\\))?:",
|
||||||
"target": "feature"
|
"target": "feature",
|
||||||
|
"on_property": "title"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "^fix(\\(.+\\))?:",
|
"pattern": "^fix(\\(.+\\))?:",
|
||||||
"target": "fix"
|
"target": "fix",
|
||||||
|
"on_property": "title"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "^docs(\\(.+\\))?:",
|
"pattern": "^docs(\\(.+\\))?:",
|
||||||
"target": "docs"
|
"target": "docs",
|
||||||
|
"on_property": "title"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "^(chore|refactor|perf)(\\(.+\\))?:",
|
"pattern": "^(chore|refactor|perf)(\\(.+\\))?:",
|
||||||
"target": "chore"
|
"target": "chore",
|
||||||
|
"on_property": "title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^(deps|dependencies)(\\(.+\\))?:",
|
||||||
|
"target": "dependencies",
|
||||||
|
"on_property": "title"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ignore_labels": ["ignore", "no-changelog"],
|
||||||
|
"ignore_commits": ["skip-ci", "skip ci", "[skip ci]"]
|
||||||
}
|
}
|
||||||
|
outputFile: CHANGELOG.md
|
||||||
toTag: ${{ steps.tag.outputs.tag }}
|
toTag: ${{ steps.tag.outputs.tag }}
|
||||||
|
commitMode: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
@@ -137,4 +205,4 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
release_files/*
|
release_files/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -48,8 +48,17 @@ 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{}
|
||||||
if err := cfg.Complete(); err != nil {
|
if err := cfg.Complete(); err != nil {
|
||||||
fmt.Printf("failed to complete config: %v\n", err)
|
fmt.Printf("failed to complete config: %v\n", err)
|
||||||
|
|||||||
@@ -43,20 +43,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFile string
|
cfgFiles []string
|
||||||
cfgDir string
|
cfgDir string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
allowUnsafe []string
|
allowUnsafe []string
|
||||||
authToken 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().StringVarP(&authToken, "token", "t", "", "authentication token of frpc (LoliaFRP only)")
|
rootCmd.PersistentFlags().StringSliceVarP(&authTokens, "token", "t", []string{}, "authentication tokens in format 'id:token' (LoliaFRP only)")
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||||
}
|
}
|
||||||
@@ -72,9 +74,9 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
|
||||||
// If authToken is provided, fetch config from API
|
// If authTokens is provided, fetch config from API
|
||||||
if authToken != "" {
|
if len(authTokens) > 0 {
|
||||||
err := runClientWithToken(authToken, unsafeFeatures)
|
err := runClientWithTokens(authTokens, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -83,14 +85,19 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, unsafeFeatures)
|
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If multiple config files are specified, run one frpc service for each file
|
||||||
|
if len(cfgFiles) > 1 {
|
||||||
|
runMultipleClientsFromFiles(cfgFiles, unsafeFeatures)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Do not show command usage here.
|
// Do not show command usage here.
|
||||||
err := runClient(cfgFile, unsafeFeatures)
|
err := runClient(cfgFiles[0], unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -120,6 +127,29 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMultipleClientsFromFiles(cfgFiles []string, unsafeFeatures *security.UnsafeFeatures) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Display banner first
|
||||||
|
banner.DisplayBanner()
|
||||||
|
bannerDisplayed = true
|
||||||
|
log.Infof("检测到 %d 个配置文件,将启动多个 frpc 服务实例", len(cfgFiles))
|
||||||
|
|
||||||
|
for _, cfgFile := range cfgFiles {
|
||||||
|
wg.Add(1)
|
||||||
|
// Add a small delay to avoid log output mixing
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
go func(path string) {
|
||||||
|
defer wg.Done()
|
||||||
|
err := runClient(path, unsafeFeatures)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\n配置文件 [%s] 启动失败: %v\n", path, err)
|
||||||
|
}
|
||||||
|
}(cfgFile)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
@@ -172,10 +202,11 @@ func startService(
|
|||||||
) 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 before starting the service
|
// Display banner only once before starting the first service
|
||||||
banner.DisplayBanner()
|
if !bannerDisplayed {
|
||||||
|
banner.DisplayBanner()
|
||||||
log.Infof("Nya! %s 启动中", version.Full())
|
bannerDisplayed = true
|
||||||
|
}
|
||||||
|
|
||||||
// Display node information if available
|
// Display node information if available
|
||||||
if nodeName != "" {
|
if nodeName != "" {
|
||||||
@@ -216,15 +247,58 @@ type APIResponse struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClientWithToken(token string, unsafeFeatures *security.UnsafeFeatures) error {
|
// 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
|
// Get API server address from environment variable
|
||||||
apiServer := os.Getenv("LOLIA_API")
|
apiServer := os.Getenv("LOLIA_API")
|
||||||
if apiServer == "" {
|
if apiServer == "" {
|
||||||
apiServer = "https://api.lolia.link"
|
apiServer = "https://api.lolia.link"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch config from API
|
// Build URL with query parameters
|
||||||
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config/%s", apiServer, token)
|
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)
|
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -255,6 +329,45 @@ func runClientWithToken(token string, unsafeFeatures *security.UnsafeFeatures) e
|
|||||||
return runClientWithConfig(configBytes, unsafeFeatures, apiResp.Data.NodeName, apiResp.Data.TunnelRemark)
|
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 {
|
func runClientWithConfig(configBytes []byte, unsafeFeatures *security.UnsafeFeatures, nodeName, tunnelRemark string) error {
|
||||||
// Render template first
|
// Render template first
|
||||||
renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues())
|
renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues())
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ 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)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.24 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.24 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package banner
|
package banner
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
)
|
||||||
|
|
||||||
func DisplayBanner() {
|
func DisplayBanner() {
|
||||||
fmt.Println(" __ ___ __________ ____ ________ ____")
|
fmt.Println(" __ ___ __________ ____ ________ ____")
|
||||||
@@ -9,4 +14,5 @@ func DisplayBanner() {
|
|||||||
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
|
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
|
||||||
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
|
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
|
||||||
fmt.Println(" ")
|
fmt.Println(" ")
|
||||||
|
log.Infof("Nya! %s 启动中", version.Full())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "LoliaFRP-CLI 0.66.0"
|
var version = "LoliaFRP-CLI 0.66.3"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user