Compare commits

...

7 Commits

12 changed files with 246 additions and 117 deletions

View File

@@ -18,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

View File

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

View File

@@ -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
@@ -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
@@ -55,10 +55,10 @@ jobs:
- 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 if [ -z "$(ls -A release_files/)" ]; then
echo "No archives found, looking for directories to package..." echo "No archives found, looking for directories to package..."
@@ -76,7 +76,7 @@ jobs:
fi fi
done done
fi fi
echo "Files in release_files:" echo "Files in release_files:"
ls -lh release_files/ ls -lh release_files/
@@ -100,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": [
{ {
@@ -134,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:
@@ -169,4 +205,4 @@ jobs:
files: | files: |
release_files/* release_files/*
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

View File

@@ -43,16 +43,18 @@ import (
) )
var ( var (
cfgFile string cfgFiles []string
cfgDir string cfgDir string
showVersion bool showVersion bool
strictConfigMode bool strictConfigMode bool
allowUnsafe []string allowUnsafe []string
authTokens []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")
@@ -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 != "" {
@@ -242,9 +273,9 @@ func runClientWithTokens(tokens []string, unsafeFeatures *security.UnsafeFeature
tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID) tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID)
} }
// Verify all tokens use the same token value // If we have multiple different tokens, start one service for each token group
if len(tokenToIDs) > 1 { if len(tokenToIDs) > 1 {
return fmt.Errorf("different tokens are not supported, all ids must use the same token") return runMultipleClientsWithTokens(tokenToIDs, unsafeFeatures)
} }
// Get the single token and all its IDs // Get the single token and all its IDs
@@ -298,6 +329,45 @@ func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *securi
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())

View File

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

View File

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

View File

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

View File

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

View File

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