feat: grpc and http on one port

This commit is contained in:
VaalaCat
2025-04-13 07:18:52 +00:00
committed by VaalaCat
parent 6b3694a9a5
commit 60abe81523
55 changed files with 2395 additions and 133 deletions

88
.github/workflows/wiki.workflow.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Deploy VitePress site to Pages
on:
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
# Step 1: Check out the repository
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
# Step 2: Setup Node.js and pnpm
- name: Setup Node.js and pnpm
uses: actions/setup-node@v4
with:
node-version: "20" # Use Node.js version 20
cache: "pnpm" # Enable pnpm cache
cache-dependency-path: "docs"
# Step 3: Install pnpm
- name: Install pnpm
run: npm install -g pnpm
# Step 4: Restore cache for pnpm and VitePress build
- name: Restore cache
uses: actions/cache@v4
with:
# Cache pnpm store and .vitepress cache
path: |
~/.pnpm-store
docs/.vitepress/cache
# Generate a cache key based on lock file and source files
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# Fallback to a partial cache if lock file changes
restore-keys: |
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
# Step 5: Install dependencies using pnpm
- name: Install dependencies
working-directory: ./docs
run: pnpm install
# Step 6: Build the VitePress site
- name: Build with VitePress
working-directory: ./docs
run: pnpm vitepress build
# Step 7: Upload artifact for deployment
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
# Step 1: Deploy to GitHub Pages
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

21
.gitignore vendored
View File

@@ -7,4 +7,23 @@ frp-panel
out
cmd/out
frp-panel*
dist/
dist/
/coverage
/src/client/shared.ts
/src/node/shared.ts
*.log
*.tgz
.DS_Store
.idea
.temp
.vite_opt_cache
.vscode
dist
cache
temp
examples-temp
node_modules
pnpm-global
TODOs.md
*.timestamp-*.mjs

View File

@@ -1,5 +1,6 @@
> Detailed blog post: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/)
> You can refer to the blog for instructions, or scroll down to the end.
> FRP-Panel WIKI[FRP-Panel WiKi](https://vaala.cat/frp-panel)
> Development blog: [https://vaala.cat/posts/frp-panel-doc/](https://vaala.cat/posts/frp-panel-doc/)
> You can refer to the wiki for instructions, or scroll down read simple doc.
# FRP-Panel
@@ -12,9 +13,9 @@ Our goal is to create a more powerful and comprehensive frp that provides:
- Visual configuration interface
- Simplified configuration required for running
- demo Video: [demo Video](doc/frp-panel-demo.mp4)
- demo Video: [demo Video](docs/public/images/frp-panel-demo.mp4)
![](./doc/frp-panel-demo.gif)
![](./docs/public/images/frp-panel-demo.gif)
## Project Usage Instructions
@@ -165,7 +166,7 @@ Next, we have frpc and frps. The original version requires writing configuration
In addition, we also need to consider the compatibility with the original version. The client/server of frp-panel must be able to connect to the official frpc/frps service. In this way, both configuration file and non-configuration file modes can work perfectly.
Overall, the architecture is quite simple.
![arch](doc/arch.png)
![arch](docs/public/images/arch.png)
### Development
@@ -231,7 +232,7 @@ The project configuration file will read the .env file in the current folder by
Detailed architecture call diagram:
![structure](doc/callvis.svg)
![structure](docs/public/images/callvis.svg)
### Core Configuration Explanation
@@ -241,31 +242,31 @@ This file contains detailed explanations of the configuration parameters. Please
## Screenshots
### Index Page
![Index Page](doc/en_index.png)
![Index Page](docs/public/images/en_index.png)
### Server List
![Server List](doc/en_server_list.png)
![Server List](docs/public/images/en_server_list.png)
### Server Edit
![Server Edit](doc/en_server_edit.png)
![Server Edit](docs/public/images/en_server_edit.png)
### Server Edit Advanced
![Server Edit Advanced](doc/en_server_edit_adv.png)
![Server Edit Advanced](docs/public/images/en_server_edit_adv.png)
### Client List
![Client List](doc/en_client_list.png)
![Client List](docs/public/images/en_client_list.png)
### Client Edit
![Client Edit](doc/en_client_edit.png)
![Client Edit](docs/public/images/en_client_edit.png)
### Client Edit Advanced
![Client Edit Advanced](doc/en_client_edit_adv.png)
![Client Edit Advanced](docs/public/images/en_client_edit_adv.png)
### Client Stats
![Client Stats](doc/en_client_stats.png)
![Client Stats](docs/public/images/en_client_stats.png)
### Realtime Log
![Realtime Log](doc/en_realtime_log.png)
![Realtime Log](docs/public/images/en_realtime_log.png)
### Remote Console
![Remote Console](doc/en_remote_console.png)
![Remote Console](docs/public/images/en_remote_console.png)

View File

@@ -1,5 +1,7 @@
> 详细博客地址: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/)
> 使用说明可以看博客,也可以直接滑到最后
> 详细使用文档:[FRP-Panel WiKi](https://vaala.cat/frp-panel)
> 博客开发记录: [https://vaala.cat/posts/frp-panel-doc/](https://vaala.cat/posts/frp-panel-doc/)
QQ交流群: 830620423
# FRP-Panel
@@ -14,9 +16,9 @@
的更强更完善的 frp
- demo Video: [demo Video](doc/frp-panel-demo.mp4)
- demo Video: [demo Video](docs/public/images/frp-panel-demo.mp4)
![](./doc/frp-panel-demo.gif)
![](./docs/public/images/frp-panel-demo.gif)
## 项目使用说明
@@ -199,7 +201,7 @@ C:/frpp/frpp.exe uninstall
其次还要考虑到与原版的兼容问题frp-panel 的客户端/服务端都必须要能连上官方 frpc/frps 服务。这样的话就可以做到配置文件/不要配置文件都能完美工作了。
总的说来架构还是很简单的。
![arch](doc/arch.png)
![arch](docs/public/images/arch.png)
### 开发
@@ -266,7 +268,7 @@ C:/frpp/frpp.exe uninstall
详细架构调用图
![structure](doc/callvis.svg)
![structure](docs/public/images/callvis.svg)
### 本体配置说明
@@ -276,31 +278,31 @@ C:/frpp/frpp.exe uninstall
## 截图展示
### 首页
![首页](doc/cn_index.png)
![首页](docs/public/images/cn_index.png)
### 服务器列表
![服务器列表](doc/cn_server_list.png)
![服务器列表](docs/public/images/cn_server_list.png)
### 服务器编辑
![服务器编辑](doc/cn_server_edit.png)
![服务器编辑](docs/public/images/cn_server_edit.png)
### 服务器高级编辑
![服务器高级编辑](doc/cn_server_edit_adv.png)
![服务器高级编辑](docs/public/images/cn_server_edit_adv.png)
### 客户端列表
![客户端列表](doc/cn_client_list.png)
![客户端列表](docs/public/images/cn_client_list.png)
### 客户端编辑
![客户端编辑](doc/cn_client_edit.png)
![客户端编辑](docs/public/images/cn_client_edit.png)
### 客户端高级编辑
![客户端高级编辑](doc/cn_client_edit_adv.png)
![客户端高级编辑](docs/public/images/cn_client_edit_adv.png)
### 客户端统计
![客户端统计](doc/cn_client_stats.png)
![客户端统计](docs/public/images/cn_client_stats.png)
### 实时日志
![实时日志](doc/cn_realtime_log.png)
![实时日志](docs/public/images/cn_realtime_log.png)
### 远程控制台
![远程控制台](doc/cn_remote_console.png)
![远程控制台](docs/public/images/cn_remote_console.png)

View File

@@ -151,7 +151,7 @@ done
# Move to current directory if current enabled
if [[ "$CURRENT" == "true" ]]; then
mv dist/frp* ./frp-panel
cp dist/frp* ./frp-panel
fi
echo "Build Done!"
echo "Build Done!"

View File

@@ -41,6 +41,7 @@ func initCmdWithFlag() []*cobra.Command {
clientSecret string
clientID string
rpcHost string
apiHost string
appSecret string
rpcPort int
apiPort int
@@ -49,11 +50,11 @@ func initCmdWithFlag() []*cobra.Command {
)
clientCmd = &cobra.Command{
Use: "client [-s client secret] [-i client id] [-a app secret] [-r rpc host] [-c rpc port] [-p api port]",
Use: "client [-s client secret] [-i client id] [-a app secret] [-t api host] [-r rpc host] [-c rpc port] [-p api port]",
Short: "run managed frpc",
Run: func(cmd *cobra.Command, args []string) {
run := func() {
patchConfig(rpcHost, appSecret,
patchConfig(apiHost, rpcHost, appSecret,
clientID, clientSecret,
apiScheme, rpcPort, apiPort)
runClient()
@@ -71,7 +72,7 @@ func initCmdWithFlag() []*cobra.Command {
Short: "run managed frps",
Run: func(cmd *cobra.Command, args []string) {
run := func() {
patchConfig(rpcHost, appSecret,
patchConfig(apiHost, rpcHost, appSecret,
clientID, clientSecret,
apiScheme, rpcPort, apiPort)
runServer()
@@ -88,7 +89,7 @@ func initCmdWithFlag() []*cobra.Command {
Use: "join [-j join token] [-r rpc host] [-p api port] [-e api scheme]",
Short: "join to master with token, save param to config",
Run: func(cmd *cobra.Command, args []string) {
pullRunConfig(joinToken, appSecret, rpcHost, apiScheme, rpcPort, apiPort, clientID)
pullRunConfig(joinToken, appSecret, rpcHost, apiScheme, rpcPort, apiPort, clientID, apiHost)
},
}
@@ -98,6 +99,10 @@ func initCmdWithFlag() []*cobra.Command {
serverCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
clientCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain")
serverCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain")
clientCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain")
serverCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain")
clientCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret")
serverCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret")
@@ -114,6 +119,7 @@ func initCmdWithFlag() []*cobra.Command {
joinCmd.Flags().StringVarP(&appSecret, "app", "a", "", "app secret")
joinCmd.Flags().StringVarP(&joinToken, "join-token", "j", "", "your token from master")
joinCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "rpc host, canbe ip or domain")
joinCmd.Flags().StringVarP(&apiHost, "api", "t", "", "api host, canbe ip or domain")
joinCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "api scheme, master api scheme, scheme is http/https")
joinCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
@@ -206,12 +212,17 @@ func initLogger() {
logger.Instance().SetReportCaller(true)
}
func patchConfig(host, secret, clientID, clientSecret, apiScheme string, rpcPort, apiPort int) {
func patchConfig(apiHost, rpcHost, secret, clientID, clientSecret, apiScheme string, rpcPort, apiPort int) {
c := context.Background()
if len(host) != 0 {
conf.Get().Master.RPCHost = host
conf.Get().Master.APIHost = host
if len(rpcHost) != 0 {
conf.Get().Master.RPCHost = rpcHost
conf.Get().Master.APIHost = rpcHost
}
if len(apiHost) != 0 {
conf.Get().Master.APIHost = apiHost
}
if len(secret) != 0 {
conf.Get().App.Secret = secret
}
@@ -244,9 +255,9 @@ func setMasterCommandIfNonePresent() {
}
}
func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, apiPort int, clientID string) {
func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, apiPort int, clientID, apiHost string) {
c := context.Background()
if err := checkPullParams(joinToken, rpcHost, apiScheme, apiPort); err != nil {
if err := checkPullParams(joinToken, apiHost, apiScheme, apiPort); err != nil {
logger.Logger(c).Errorf("check pull params failed: %s", err.Error())
return
}
@@ -261,7 +272,7 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api
}
clientID = utils.MakeClientIDPermited(clientID)
patchConfig(rpcHost, appSecret, "", "", apiScheme, rpcPort, apiPort)
patchConfig(apiHost, rpcHost, appSecret, "", "", apiScheme, rpcPort, apiPort)
initResp, err := rpc.InitClient(clientID, joinToken)
if err != nil {
@@ -309,7 +320,7 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api
envMap[common.EnvClientSecret] = client.GetSecret()
envMap[common.EnvMasterRPCHost] = rpcHost
envMap[common.EnvMasterRPCPort] = cast.ToString(rpcPort)
envMap[common.EnvMasterAPIHost] = rpcHost
envMap[common.EnvMasterAPIHost] = apiHost
envMap[common.EnvMasterAPIPort] = cast.ToString(apiPort)
envMap[common.EnvMasterAPIScheme] = apiScheme
@@ -320,12 +331,12 @@ func pullRunConfig(joinToken, appSecret, rpcHost, apiScheme string, rpcPort, api
logger.Logger(c).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]", common.SysEnvPath, envMap)
}
func checkPullParams(joinToken, rpcHost, apiScheme string, apiPort int) error {
func checkPullParams(joinToken, apiHost, apiScheme string, apiPort int) error {
if len(joinToken) == 0 {
return errors.New("join token is empty")
}
if len(rpcHost) == 0 {
return errors.New("rpc host is empty")
if len(apiHost) == 0 {
return errors.New("api host is empty")
}
if len(apiScheme) == 0 {
return errors.New("api scheme is empty")

View File

@@ -18,8 +18,8 @@ import (
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/rpc"
"github.com/VaalaCat/frp-panel/services/api"
"github.com/VaalaCat/frp-panel/services/master"
"github.com/VaalaCat/frp-panel/services/mux"
"github.com/VaalaCat/frp-panel/services/rpcclient"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/watcher"
@@ -27,6 +27,8 @@ import (
"github.com/glebarez/sqlite"
"github.com/sirupsen/logrus"
"github.com/sourcegraph/conc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
@@ -46,28 +48,31 @@ func runMaster() {
auth.InitAuth()
creds := dao.InitCert(conf.GetCertTemplate())
master.MustInitMasterService(creds)
router := bizmaster.NewRouter(fs)
api.MustInitApiService(conf.MasterAPIListenAddr(), router)
logger.Logger(c).Infof("start to run master")
m := master.GetMasterSerivce()
a := api.GetAPIService()
lisOpt := conf.GetListener(c)
r, w := initDefaultInternalServer()
defer w.Stop()
defer r.Stop()
masterService := master.NewMasterService(credentials.NewTLS(creds))
server := masterService.GetServer()
muxServer := mux.NewMux(server, router, lisOpt.MuxLis, creds)
masterH2CService := master.NewMasterService(insecure.NewCredentials())
serverH2c := masterH2CService.GetServer()
httpMuxServer := mux.NewMux(serverH2c, router, lisOpt.ApiLis, nil)
tasks := watcher.NewClient()
tasks.AddCronTask("0 0 3 * * *", proxy.CollectDailyStats)
defer tasks.Stop()
var wg conc.WaitGroup
wg.Go(w.Run)
wg.Go(r.Run)
wg.Go(m.Run)
wg.Go(a.Run)
logger.Logger(c).Infof("start to run master")
wg.Go(runDefaultInternalServer)
wg.Go(muxServer.Run)
wg.Go(httpMuxServer.Run)
wg.Go(tasks.Run)
wg.Wait()
}
@@ -114,7 +119,7 @@ func initDatabase(c context.Context) {
models.GetDBManager().Init()
}
func initDefaultInternalServer() (rpcclient.ClientRPCHandler, watcher.Client) {
func runDefaultInternalServer() {
dao.InitDefaultServer(conf.Get().Master.APIHost)
defaultServer, err := dao.GetDefaultServer()
if err != nil {
@@ -143,5 +148,13 @@ func initDefaultInternalServer() (rpcclient.ClientRPCHandler, watcher.Client) {
w.AddDurationTask(common.PushProxyInfoDuration, bizserver.PushProxyInfo, defaultServer.ServerID, defaultServer.ConnectSecret)
go initServerOnce(defaultServer.ServerID, defaultServer.ConnectSecret)
return r, w
var wg conc.WaitGroup
defer w.Stop()
defer r.Stop()
wg.Go(w.Run)
wg.Go(r.Run)
wg.Wait()
}

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"net"
bizserver "github.com/VaalaCat/frp-panel/biz/server"
"github.com/VaalaCat/frp-panel/common"
@@ -31,10 +32,13 @@ func runServer() {
logrus.Fatal("client id cannot be empty")
}
router := bizserver.NewRouter()
api.MustInitApiService(conf.ServerAPIListenAddr(), router)
l, err := net.Listen("tcp", conf.ServerAPIListenAddr())
if err != nil {
logger.Logger(c).WithError(err).Fatalf("failed to listen addr: %v", conf.ServerAPIListenAddr())
return
}
a := api.GetAPIService()
a := api.NewApiService(l, bizserver.NewRouter(), true)
defer a.Stop()
cred, err := utils.TLSClientCertNoValidate(rpc.GetClientCert(clientID, clientSecret, pb.ClientType_CLIENT_TYPE_FRPS))

View File

@@ -112,10 +112,12 @@ func GetCertTemplate() *x509.Certificate {
return &x509.Certificate{
SerialNumber: big.NewInt(now.Unix()),
Subject: pkix.Name{
CommonName: cfg.Master.APIHost,
Country: []string{"CN"},
Organization: []string{"frp-panel"},
OrganizationalUnit: []string{"frp-panel"},
},
SignatureAlgorithm: x509.SHA512WithRSA,
DNSNames: []string{cfg.Master.APIHost},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
NotBefore: now,
@@ -123,8 +125,43 @@ func GetCertTemplate() *x509.Certificate {
SubjectKeyId: []byte{102, 114, 112, 45, 112, 97, 110, 101, 108},
BasicConstraintsValid: true,
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageKeyAgreement |
x509.KeyUsageDataEncipherment,
}
}
type LisOpt struct {
MuxLis net.Listener
ApiLis net.Listener
RunAPI bool
}
func GetListener(c context.Context) LisOpt {
runAPI := RPCListenAddr() != MasterAPIListenAddr()
muxLis, err := net.Listen("tcp", RPCListenAddr())
if err != nil {
logger.Logger(c).WithError(err).Fatalf("failed to listen: %v", RPCListenAddr())
}
opt := LisOpt{
MuxLis: muxLis,
RunAPI: runAPI,
}
if runAPI {
apiLis, err := net.Listen("tcp", MasterAPIListenAddr())
if err != nil {
logger.Logger(c).WithError(err).Warnf("failed to listen: %v, but mux server can handle http api", MasterAPIListenAddr())
}
opt.ApiLis = apiLis
}
if !runAPI {
opt.ApiLis = nil
}
return opt
}

View File

@@ -49,6 +49,7 @@ type Config struct {
Client struct {
ID string `env:"ID" env-description:"client id"`
Secret string `env:"SECRET" env-description:"client secret"`
TLSRpc bool `env:"TLS_RPC" env-default:"true" env-description:"use tls for rpc connection"`
} `env-prefix:"CLIENT_"`
IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"`
}

View File

@@ -2,19 +2,18 @@ package dao
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/utils"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/credentials"
)
func InitCert(template *x509.Certificate) credentials.TransportCredentials {
func InitCert(template *x509.Certificate) *tls.Config {
var (
certPem []byte
keyPem []byte
@@ -52,7 +51,12 @@ func InitCert(template *x509.Certificate) credentials.TransportCredentials {
func GenX509Info(template *x509.Certificate) (certPem []byte, keyPem []byte, err error) {
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
// priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
// if err != nil {
// return nil, nil, err
// }
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vitepress'
import { zhConfig } from './config/zh'
import { enConfig } from './config/en'
// https://vitepress.dev/reference/site-config
export default defineConfig({
base: '/frp-panel/',
locales: {
root: {
label: '简体中文',
lang: 'zh',
...zhConfig
},
en: {
label: 'English',
lang: 'en',
...enConfig
}
},
title: "Frp-Panel WIKI",
description: "Wiki of vaalacat's wonderful frp-panel",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
}
})

View File

@@ -0,0 +1,6 @@
import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
themeConfig: {
}
}

View File

@@ -0,0 +1,27 @@
import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '源码', link: 'https://github.com/vaalacat/frp-panel' }
],
sidebar: [
{
text: '快速开始',
collapsed: true,
link: '/zh/quick-start',
items: [
{ text: 'Master 部署', link: '/zh/deploy-master' },
{ text: 'Server 部署', link: '/zh/deploy-server' },
{ text: 'Client 部署', link: '/zh/deploy-client' },
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/vaalacat/frp-panel' }
]
}
}

22
docs/en/index.md Normal file
View File

@@ -0,0 +1,22 @@
---
layout: home
hero:
name: "Frp-Panel Docs"
text: "Multi-Node, Centralized & Visual FRP Management Platform"
tagline: Secure, modern open-source alternative to Cloudflare Tunnel/Tailscale Funnel/Ngrok platforms and proxies <br/> Cloudflare Tunnel/Tailscale Funnel/Ngrok platform and agent open source alternative
actions:
- theme: alt
text: Quick Start
link: /en
features:
- title: 🚀 One-Click Deployment
details: Rapid deployment via Docker Compose or single-line commands, supports multiple architectures (x86/ARM), with built-in batch node startup commands and auto-config generation
- title: 📦 One-Click Installation
details: Fully automated install script with system dependency detection, supports Debian/Ubuntu/CentOS Linux distributions / Windows 10+ / macOS
- title: 🌐 Centralized Web Management
details: Visual dashboard for centralized frpc/frps node management, real-time traffic metrics, use HTTPS/GRPC/WSS protocol, with live log analysis and web-based interactive terminal
- title: 🔒 Enterprise-Grade Security
details: RPC port enforces TLS encryption by default. Both API and RPC ports support TLS via reverse proxy configurations
---

27
docs/index.md Normal file
View File

@@ -0,0 +1,27 @@
---
layout: home
hero:
name: "Frp-Panel 文档"
text: "多节点、中心化、可视化 的 frp 管理平台"
tagline: Cloudflare Tunnel / Tailscale Funnel / Ngork / nps 平台和代理的安全、现代化开源替代品
actions:
- theme: alt
text: 快速开始
link: /zh/quick-start
- theme: alt
text: Quick Start
link: /en
features:
- title: 🚀 一键启动
details: 通过 Docker Compose 或单行命令快速部署,支持多种架构 (x86/ARM),内置批量节点启动命令和自动配置生成功能
- title: 📦 一键安装
details: 提供全自动安装脚本,自动检测系统依赖,支持 Debian/Ubuntu/CentOS 等主流 Linux 发行版 / Windows10 以上 / MacOS 等
- title: 🌐 中心化网页管理
details: 可视化仪表盘支持多节点集中管控frpc/frps实时流量统计支持 HTTPS/WSS 协议,提供实时日志分析和实时网页交互终端
- title: 🔒 企业级安全
details: RPC端口默认开启 TLS 加密通信API 可使用反向代理支持 TLS。RPC 端口和 API 均支持以反向代理支持 TLS
---
QQ交流群: `830620423`

10
docs/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"devDependencies": {
"vitepress": "^1.6.3"
},
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
}
}

1561
docs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View File

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

View File

Before

Width:  |  Height:  |  Size: 491 KiB

After

Width:  |  Height:  |  Size: 491 KiB

View File

Before

Width:  |  Height:  |  Size: 683 KiB

After

Width:  |  Height:  |  Size: 683 KiB

View File

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 306 KiB

View File

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 357 KiB

View File

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

View File

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 281 KiB

View File

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View File

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 248 KiB

View File

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 252 KiB

View File

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View File

Before

Width:  |  Height:  |  Size: 759 KiB

After

Width:  |  Height:  |  Size: 759 KiB

View File

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 674 KiB

View File

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 351 KiB

View File

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 267 KiB

60
docs/zh/deploy-client.md Normal file
View File

@@ -0,0 +1,60 @@
# Client 部署
Client 推荐使用 docker 部署,直接部署在客户机中,可以通过远程终端直接在客户机以 root 权限执行命令,方便升级和管理。
## 准备
打开 Master 的 webui 并登录,如果没有账号,请直接注册,第一个用户即为管理员
在侧边栏跳转到 `服务端`,点击上方的 `新建` 并输入 服务端 的唯一识别ID和 服务端 能够被公网访问的 IP/域名,然后点击保存
![](../public/images/cn_client_list.png)
刷新后,新的客户端会出现在列表中。
## 直接部署
点击对应客户端的 `ID (点击查看安装命令)` 一列,弹出不同系统的安装命令,粘贴到对应终端即可安装,这里以 Linux 为例
```
curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
如果你在国内可以增加github加速到安装脚本前
```
curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
注意,如果你使用 反向代理 TLS需要修改这行命令类似如下
```bash
curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
## Docker Compose 部署
点击对应客户端的 `密钥 (点击查看启动命令)` 一列中的隐藏字段,复制类似的启动命令如下备用:
```bash
frp-panel client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
注意,如果你使用 反向代理 TLS需要修改这行命令类似如下
```bash
frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
docker-compose.yaml
```yaml
version: '3'
services:
frp-panel-client:
image: vaalacat/frp-panel
container_name: frp-panel-client
network_mode: host
restart: unless-stopped
command: client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```

218
docs/zh/deploy-master.md Normal file
View File

@@ -0,0 +1,218 @@
# Master 部署
Master 推荐使用 docker 部署!不推荐直接安装到服务器中
会给出三种部署方式,任选一种即可
部署后没有默认用户,注册的第一个用户即为管理员,为了安全,默认不开启多用户注册
## 前期准备
### 服务器开放公网端口:
- **WEBUI 端口**: 默认 `TCP 9000`
- **RPC 端口**: 默认 `TCP 9001`
- **frps 的API端口**:没有默认,请随意预留,例子使用 `TCP/UDP 7000`
- **frps 对外开放的服务端口**:没有默认,请随意预留,例子使用 `TCP/UDP 26999-27050`
如果使用反向代理,请忽略 WEBUI 和 RPC 端口,放行 80/443 即可
WEBUI 端口也可以处理 h2c 格式的 RPC 连接
RPC 端口也可以处理自签名 HTTPS 的 API 连接
二者都可使用反向代理服务器连接并提供TLS
> 测试端口是否开放的方法以8080为例在服务器上运行
> ```shell
> python3 -m http.server 8080
> ```
> 然后在另一台电脑/服务器上执行:
> ```shell
> curl http://服务器公网IP/域名:8080 -I
> ```
> 成功的话,输出类似
> ```
> HTTP/1.0 200 OK
> Server: SimpleHTTP/0.6 Python/3.11.0
> Date: Sat, 12 Apr 2025 17:12:15 GMT
> Content-type: text/html; charset=utf-8
> Content-Length: 8225
> ```
## 方式一Docker Compose 部署
服务器需要安装docker和docker compose
首先创建一个`docker-compose.yaml`文件,写入以下内容
```yaml
version: "3"
services:
frpp-master:
image: vaalacat/frp-panel:latest
environment:
APP_GLOBAL_SECRET: your_secret
MASTER_RPC_HOST: 1.2.3.4 #服务器的外部IP或域名
MASTER_RPC_PORT: 9001
MASTER_API_HOST: 1.2.3.4 #服务器的外部IP或域名
MASTER_API_PORT: 9000
MASTER_API_SCHEME: http
volumes:
- ./data:/data
restart: unless-stopped
command: master
```
## 方式二Docker 命令部署
服务器需要安装 docker我们推荐使用 host 网络模式部署 `Master`
```bash
# 推荐
# MASTER_RPC_HOST要改成你服务器的外部IP
# APP_GLOBAL_SECRET注意不要泄漏客户端和服务端的是通过Master生成的
docker run -d \
--network=host \
--restart=unless-stopped \
-v /opt/frp-panel:/data \
-e APP_GLOBAL_SECRET=your_secret \
-e MASTER_RPC_HOST=0.0.0.0 \
vaalacat/frp-panel
```
如果你不想使用 host 网络模式,请参考使用下面的命令修改
```bash
# 或者
# 运行时记得删除命令中的中文
docker run -d -p 9000:9000 \ # API控制台端口
-p 9001:9001 \ # rpc端口
-p 7000:7000 \ # frps 端口
-p 27000-27050:27000-27050 \ # 给frps预留的端口
--restart=unless-stopped \
-v /opt/frp-panel:/data \ # 数据存储位置
-e APP_GLOBAL_SECRET=your_secret \ # Master的secret注意不要泄漏客户端和服务端的是通过Master生成的
-e MASTER_RPC_HOST=0.0.0.0 \ # 这里要改成你服务器的外部IP
vaalacat/frp-panel
```
## 方式三:使用 docker 反向代理 TLS 加密部署
这里我们以 [Traefik](https://traefik.io/traefik/) 为例
> `Traefik` 可以实时自动识别 Docker 容器的端口并热更新配置,非常适合 Docker 服务的反向代理
首先创建一个名为`traefik`的反向代理专用网络
```bash
docker network create traefik
```
然后启动反向代理和 Master 服务
- `docker-compose.yaml`
```yaml
version: '3'
services:
traefk-reverse-proxy:
image: traefik:v3.3
restart: unless-stopped
networks:
- traefik
command:
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.websecure.http2.maxConcurrentStreams=250
- --providers.docker
- --providers.docker.network=traefik
- --api.insecure # 在生产环境请删除这一行
# 这下面使用 80 端口做ACME HTTP DNS证书验证
- --certificatesresolvers.le.acme.email=me@example.com
- --certificatesresolvers.le.acme.storage=/etc/traefik/conf/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
ports:
# 反向代理的 HTTP 端口
- "80:80"
# 反向代理的 HTTPS 端口
- "443:443"
# Traefik 的 Web UI (--api.insecure=true 会使用这个端口)
# 生产环境请删除这个端口
- "8080:8080"
volumes:
# 挂载 docker.sock这样 Traefik 可以自动识别主机上所有 docker 容器反向代理配置
- /var/run/docker.sock:/var/run/docker.sock
# 保存 Traefik 申请的证书
- ./conf:/etc/traefik/conf
frpp-master:
image: vaalacat/frp-panel:latest # 这里换成你想使用的版本
environment:
APP_GLOBAL_SECRET: your_secret
# 因为 api 和 rpc 使用的协议不一样
# 我们需要对 api 和 rpc 使用两个域名
# 以便反向代理正确识别需要转发的协议
MASTER_RPC_HOST: frpp.example.com
MASTER_API_PORT: 443
MASTER_API_HOST: frpp-rpc.example.com
MASTER_API_SCHEME: https
networks:
- traefik
volumes:
- ./data:/data
ports:
# 无需为 master 预留 api 和 rpc 端口
# 预留frps api端口
- 7000:7000
- 7000:7000/udp
# 预留frps的业务端口
# 26999 端口是留给 frps 的http代理端口
- 26999-27050:26999-27050
- 26999-27050:26999-27050/udp
restart: unless-stopped
command: master
labels:
# API
- traefik.http.routers.frp-panel-api.rule=Host(`frpp.example.com`)
- traefik.http.routers.frp-panel-api.tls=true
- traefik.http.routers.frp-panel-api.tls.certresolver=le
- traefik.http.routers.frp-panel-api.entrypoints=websecure
- traefik.http.routers.frp-panel-api.service=frp-panel-api
- traefik.http.services.frp-panel-api.loadbalancer.server.port=9000
- traefik.http.services.frp-panel-api.loadbalancer.server.scheme=http
# RPC
- traefik.http.routers.frp-panel-rpc.rule=Host(`frpp-rpc.example.com`)
- traefik.http.routers.frp-panel-rpc.tls=true
- traefik.http.routers.frp-panel-rpc.tls.certresolver=le
- traefik.http.routers.frp-panel-rpc.entrypoints=websecure
- traefik.http.routers.frp-panel-rpc.service=frp-panel-rpc
- traefik.http.services.frp-panel-rpc.loadbalancer.server.port=9000
- traefik.http.services.frp-panel-rpc.loadbalancer.server.scheme=h2c
# 下方如果你用不到 frps 的http代理可以不要
# 需要配置域名 *.frpp.example.com 泛解析到你服务器的公网IP
# 这样可以实现使用 .frpp.example.com 结束的域名,在 443 端口,转发多个服务到多个 frpc
- traefik.http.routers.frp-panel-tunnel.rule=HostRegexp(`.*.frpp.example.com`)
- traefik.http.routers.frp-panel-tunnel.tls.domains[0].sans=*.frpp.example.com
- traefik.http.routers.frp-panel-tunnel.tls=true
- traefik.http.routers.frp-panel-tunnel.tls.certresolver=le
- traefik.http.routers.frp-panel-tunnel.entrypoints=websecure
- traefik.http.routers.frp-panel-tunnel.service=frp-panel-tunnel
- traefik.http.services.frp-panel-tunnel.loadbalancer.server.port=26999
- traefik.http.services.frp-panel-tunnel.loadbalancer.server.scheme=http
networks:
traefik:
external: true
name: traefik
```
上方的 `docker-compose.yaml` 部署完成后,可以访问 `服务器公网IP/域名:8080` 查看反向代理状态
随后配置 default server 即可实现 frp 子域名转发:
| 配置项 | 值 |
|----|-----|
| FRPs 监听端口 | 7000 |
| FRPs 监听地址 | 0.0.0.0 |
| 代理监听地址 | 0.0.0.0 |
| HTTP 监听端口 | 26999 |
| 域名后缀 | frpp.example.com |

44
docs/zh/deploy-server.md Normal file
View File

@@ -0,0 +1,44 @@
# Server 部署
Server 推荐使用 docker 部署!不推荐直接安装到服务器中
> 如果只有一台公网服务器需要管理,那么使用 `master` 自带的 `default server` 即可,无需单独部署 `server`
## 准备
打开 Master 的 webui 并登录,如果没有账号,请直接注册,第一个用户即为管理员
在侧边栏跳转到 `服务端`,点击上方的 `新建` 并输入 服务端 的唯一识别ID和 服务端 能够被公网访问的 IP/域名,然后点击保存
![](../public/images/cn_server_list.png)
刷新后,新的服务端会出现在列表中。点击对应服务端的`密钥 (点击查看启动命令)`一列中的隐藏字段,复制类似的启动命令如下备用:
```bash
frp-panel server -s abc -i user.s.server1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
注意,如果你使用 反向代理 TLS需要修改这行命令类似如下
```bash
frp-panel server -s abc -i user.s.server1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
## Docker Compose 部署
docker-compose.yaml
```yaml
version: '3'
services:
frp-panel-server:
image: vaalacat/frp-panel
container_name: frp-panel-server
network_mode: host
restart: unless-stopped
command: server -s abc -i user.s.server1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
## 直接部署
如果你想要直接部署,请参考 client 部署的步骤

19
docs/zh/quick-start.md Normal file
View File

@@ -0,0 +1,19 @@
# 快速开始
## 开始前必读
`frp-panel` 有三个模块:
1. `master`: 中控模块,负责分发配置文件和控制所有其他模块
2. `server`: 对应`frps`,负责提供流量接入点
3. `client`: 对应`frpc`,可以将本地的服务暴露给`server`上某一个接入点
> 部署`master`时,`master`会启动一个默认的`default server` 供`client`连接,因此`master`一般不会独立存在,但你可以选择不使用
部署时,我们一般从 `master` 开始。`master` 负责管理的 `server``client` 部署时,需要用到成功部署后的 `master` 控制页面中自动生成的内容。
对于 `frp-panel` 我们**推荐所有的组件都使用 `docker` 部署**,并且**使用 `host` 网络**模式,除非你需要远程终端控制远端的机器时才使用服务安装到客户机
## 架构图
![](../public/images/arch.svg)

2
go.mod
View File

@@ -30,6 +30,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/tiendc/go-deepcopy v1.2.0
golang.org/x/crypto v0.31.0
golang.org/x/net v0.33.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.35.2
gorm.io/driver/mysql v1.5.2
@@ -122,7 +123,6 @@ require (
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect

View File

@@ -2,6 +2,7 @@ package rpc
import (
"context"
"crypto/tls"
"errors"
"github.com/VaalaCat/frp-panel/common"
@@ -10,6 +11,7 @@ import (
"github.com/imroc/req/v3"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/proto"
)
@@ -18,8 +20,16 @@ var (
)
func newMasterCli() {
conn, err := grpc.NewClient(conf.RPCCallAddr(),
grpc.WithTransportCredentials(conf.ClientCred))
opt := []grpc.DialOption{}
if conf.Get().Client.TLSRpc {
logrus.Infof("use tls rpc")
opt = append(opt, grpc.WithTransportCredentials(conf.ClientCred))
} else {
logrus.Infof("use insecure rpc")
opt = append(opt, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
conn, err := grpc.NewClient(conf.RPCCallAddr(), opt...)
if err != nil {
logrus.Fatalf("did not connect: %v", err)
}
@@ -34,9 +44,16 @@ func MasterCli(c context.Context) (pb.MasterClient, error) {
return masterCli, nil
}
func httpCli() *req.Client {
c := req.C()
c.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
return c
}
func GetClientCert(clientID, clientSecret string, clientType pb.ClientType) []byte {
apiEndpoint := conf.GetAPIURL()
c := req.C()
c := httpCli()
rawReq, err := proto.Marshal(&pb.GetClientCertRequest{
ClientId: clientID,
ClientSecret: clientSecret,
@@ -61,7 +78,9 @@ func GetClientCert(clientID, clientSecret string, clientType pb.ClientType) []by
func InitClient(clientID, joinToken string) (*pb.InitClientResponse, error) {
apiEndpoint := conf.GetAPIURL()
c := req.C()
c := httpCli()
rawReq, err := proto.Marshal(&pb.InitClientRequest{
ClientId: &clientID,
})
@@ -86,7 +105,8 @@ func InitClient(clientID, joinToken string) (*pb.InitClientResponse, error) {
func GetClient(clientID, joinToken string) (*pb.GetClientResponse, error) {
apiEndpoint := conf.GetAPIURL()
c := req.C()
c := httpCli()
rawReq, err := proto.Marshal(&pb.GetClientRequest{
ClientId: &clientID,
})

View File

@@ -1,6 +1,8 @@
package api
import (
"net"
"github.com/gin-gonic/gin"
)
@@ -10,32 +12,29 @@ type ApiService interface {
}
type server struct {
srv *gin.Engine
addr string
srv *gin.Engine
addr net.Listener
enable bool
}
var (
_ ApiService = (*server)(nil)
apiService *server
_ ApiService = (*server)(nil)
)
func NewApiService(listenAddr string, router *gin.Engine) *server {
func NewApiService(listen net.Listener, router *gin.Engine, enable bool) *server {
return &server{
srv: router,
addr: listenAddr,
srv: router,
addr: listen,
enable: enable,
}
}
func MustInitApiService(listenAddr string, router *gin.Engine) {
apiService = NewApiService(listenAddr, router)
}
func GetAPIService() ApiService {
return apiService
}
func (s *server) Run() {
s.srv.Run(s.addr)
// 如果完全使用mux可以不启动
if !s.enable {
return
}
s.srv.RunListener(s.addr)
}
func (s *server) Stop() {

View File

@@ -25,13 +25,15 @@ type server struct {
pb.UnimplementedMasterServer
}
func NewRpcServer(creds credentials.TransportCredentials) *grpc.Server {
func newRpcServer(creds credentials.TransportCredentials) *grpc.Server {
// s := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
// s := grpc.NewServer(grpc.Creds(creds))
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterMasterServer(s, &server{})
return s
}
func RunRpcServer(s *grpc.Server) {
func runRpcServer(s *grpc.Server) {
lis, err := net.Listen("tcp", conf.RPCListenAddr())
if err != nil {
logrus.Fatalf("rpc server failed to listen: %v", err)

View File

@@ -1,54 +1,35 @@
package master
import (
"context"
"github.com/VaalaCat/frp-panel/logger"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type MasterHandler interface {
type MasterService interface {
Run()
Stop()
GetServer() *grpc.Server
}
type Master struct {
type master struct {
grpcServer *grpc.Server
}
var (
cli *Master
)
func MustInitMasterService(creds credentials.TransportCredentials) {
ctx := context.Background()
if cli != nil {
logger.Logger(ctx).Warn("server has been initialized")
return
}
cli = NewMasterHandler(creds)
}
func GetMasterSerivce() MasterHandler {
if cli == nil {
logrus.Panic("server has not been initialized")
}
return cli
}
func NewMasterHandler(creds credentials.TransportCredentials) *Master {
s := NewRpcServer(creds)
return &Master{
func NewMasterService(creds credentials.TransportCredentials) MasterService {
s := newRpcServer(creds)
return &master{
grpcServer: s,
}
}
func (s *Master) Run() {
RunRpcServer(s.grpcServer)
func (s *master) Run() {
runRpcServer(s.grpcServer)
}
func (s *Master) Stop() {
func (s *master) Stop() {
s.grpcServer.Stop()
}
func (s *master) GetServer() *grpc.Server {
return s.grpcServer
}

View File

@@ -0,0 +1,60 @@
package mux
import (
"crypto/tls"
"log"
"net"
"net/http"
"strings"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
type MuxServer interface {
Run()
Stop()
}
type muxImpl struct {
srv *http.Server
lis net.Listener
tls bool
}
func NewMux(grpcServer, apiServer http.Handler, lis net.Listener, creds *tls.Config) MuxServer {
tlsServer := grpcHandlerFunc(grpcServer, apiServer)
tlsServer.TLSConfig = creds
return &muxImpl{
srv: tlsServer,
lis: lis,
tls: creds != nil,
}
}
func (m *muxImpl) Run() {
if m.tls {
if err := m.srv.ServeTLS(m.lis, "", ""); err != nil {
log.Fatal(err)
}
} else {
if err := m.srv.Serve(m.lis); err != nil {
log.Fatal(err)
}
}
}
func (m *muxImpl) Stop() {
}
func grpcHandlerFunc(grpcServer http.Handler, httpHandler http.Handler) *http.Server {
return &http.Server{Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// fmt.Printf("proto major: %d, %s , %s\n", r.ProtoMajor, r.RequestURI, r.Header.Get("Content-Type"))
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
httpHandler.ServeHTTP(w, r)
}
}), &http2.Server{}), ReadHeaderTimeout: time.Second * 30}
}

View File

@@ -37,7 +37,7 @@ func PemBlockForPrivKey(priv interface{}) *pem.Block {
}
}
func TLSServerCert(certPem, keyPem []byte) (credentials.TransportCredentials, error) {
func TLSServerCert(certPem, keyPem []byte) (*tls.Config, error) {
cert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
return nil, err
@@ -46,7 +46,7 @@ func TLSServerCert(certPem, keyPem []byte) (credentials.TransportCredentials, er
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
}
return credentials.NewTLS(config), nil
return config, nil
}
func TLSClientCert(caPem []byte) (credentials.TransportCredentials, error) {
@@ -62,6 +62,7 @@ func TLSClientCertNoValidate(caPem []byte) (credentials.TransportCredentials, er
config := &tls.Config{
RootCAs: certpool,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
}
return credentials.NewTLS(config), nil