feat: support wireguard

This commit is contained in:
VaalaCat
2025-10-06 15:27:09 +00:00
parent c0eb0c07f2
commit 55dbc54a1a
167 changed files with 22822 additions and 879 deletions
+52 -52
View File
@@ -1,53 +1,53 @@
# name: Master Release
# on:
# push:
# branches: [main]
# pull_request:
# branches: [main]
# workflow_dispatch:
name: Master Release
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# jobs:
# build-static:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: npm setup
# uses: actions/setup-node@v4
# with:
# node-version: 20
# - name: Setup Go
# uses: actions/setup-go@v4
# with:
# go-version: "1.24.x"
# - name: npm install and build
# run: |
# cd www
# npm install && npm install -g pnpm
# - name: Install dependencies
# run: |
# go mod tidy
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# - name: Install Protoc
# uses: arduino/setup-protoc@v3
# - name: Compile server
# run: bash ./build.sh
# - uses: "marvinpinto/action-automatic-releases@latest"
# with:
# repo_token: "${{ secrets.GITHUB_TOKEN }}"
# prerelease: true
# automatic_release_tag: latest
# files: |
# dist/*
# - name: Setup ko
# uses: ko-build/setup-ko@v0.9
# env:
# KO_DOCKER_REPO: docker.io/vaalacat/frp-panel
# - env:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# run: |
# echo "${password}" | ko login docker.io --username ${username} --password-stdin
# ko build ./cmd/frpp --sbom=none --bare
jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: npm setup
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.24.x"
- name: npm install and build
run: |
cd www
npm install && npm install -g pnpm
- name: Install dependencies
run: |
go mod tidy
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Compile server
run: bash ./build.sh
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: true
automatic_release_tag: alpha
files: |
dist/*
- name: Setup ko
uses: ko-build/setup-ko@v0.9
env:
KO_DOCKER_REPO: docker.io/vaalacat/frp-panel
- env:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
echo "${password}" | ko login docker.io --username ${username} --password-stdin
ko build ./cmd/frpp --sbom=none --bare -t alpha
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: true
prerelease: false
automatic_release_tag: latest
files: |
dist/*
+1 -1
View File
@@ -2,7 +2,7 @@
__debug*
.env
tmp
*.db
*.db*
frp-panel
out
cmd/out
+31
View File
@@ -0,0 +1,31 @@
package client
import (
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
)
func CreateWireGuard(ctx *app.Context, req *pb.CreateWireGuardRequest) (*pb.CreateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "CreateWireGuard")
cfg := defs.WireGuardConfig{WireGuardConfig: req.GetWireguardConfig()}
log.Debugf("create wireguard service, cfg: %s", cfg.String())
wgSvc, err := ctx.GetApp().GetWireGuardManager().CreateService(&cfg)
if err != nil {
log.WithError(err).Errorf("create wireguard service failed")
return nil, err
}
err = wgSvc.Start()
if err != nil {
log.WithError(err).Errorf("start wireguard service failed")
return nil, err
}
log.Debugf("start wireguard service success")
return &pb.CreateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
+42
View File
@@ -0,0 +1,42 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
)
func GetWireGuardRuntimeInfo(ctx *app.Context, req *pb.GetWireGuardRuntimeInfoRequest) (*pb.GetWireGuardRuntimeInfoResponse, error) {
var (
interfaceName = req.GetInterfaceName()
log = ctx.Logger().WithField("op", "GetWireGuardRuntimeInfo")
)
if interfaceName == "" {
log.Errorf("interface_name is required")
return nil, fmt.Errorf("interface_name is required")
}
wgSvc, ok := ctx.GetApp().GetWireGuardManager().GetService(interfaceName)
if !ok {
log.Errorf("wireguard service not found, interface_name: %s", interfaceName)
return nil, fmt.Errorf("wireguard service not found, interface_name: %s", interfaceName)
}
runtimeInfo, err := wgSvc.GetWGRuntimeInfo()
if err != nil {
log.WithError(err).Errorf("get wireguard runtime info failed")
return nil, fmt.Errorf("get wireguard runtime info failed: %v", err)
}
log.Debugf("get wireguard runtime info with interface_name: %s, runtimeInfo: %s", interfaceName, runtimeInfo.String())
return &pb.GetWireGuardRuntimeInfoResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "success",
},
WgDeviceRuntimeInfo: runtimeInfo,
}, nil
}
+22
View File
@@ -0,0 +1,22 @@
package client
import (
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
)
func DeleteWireGuard(ctx *app.Context, req *pb.DeleteWireGuardRequest) (*pb.DeleteWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "DeleteWireGuard")
log.Debugf("delete wireguard service, client_id: %s, interface_name: %s", req.GetClientId(), req.GetInterfaceName())
err := ctx.GetApp().GetWireGuardManager().RemoveService(req.GetInterfaceName())
if err != nil {
log.WithError(err).Errorf("remove wireguard service failed")
return nil, err
}
log.Debugf("remove wireguard service success")
return &pb.DeleteWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
+8
View File
@@ -45,6 +45,14 @@ func HandleServerMessage(appInstance app.Application, req *pb.ServerMessage) *pb
return app.WrapperServerMsg(appInstance, req, GetWorkerStatus)
case pb.Event_EVENT_INSTALL_WORKERD:
return app.WrapperServerMsg(appInstance, req, InstallWorkerd)
case pb.Event_EVENT_CREATE_WIREGUARD:
return app.WrapperServerMsg(appInstance, req, CreateWireGuard)
case pb.Event_EVENT_DELETE_WIREGUARD:
return app.WrapperServerMsg(appInstance, req, DeleteWireGuard)
case pb.Event_EVENT_UPDATE_WIREGUARD:
return app.WrapperServerMsg(appInstance, req, UpdateWireGuard)
case pb.Event_EVENT_GET_WIREGUARD_RUNTIME_INFO:
return app.WrapperServerMsg(appInstance, req, GetWireGuardRuntimeInfo)
case pb.Event_EVENT_PING:
rawData, _ := proto.Marshal(conf.GetVersion().ToProto())
return &pb.ClientMessage{
+62
View File
@@ -0,0 +1,62 @@
package client
import (
"context"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
)
func PullWireGuards(appInstance app.Application, clientID, clientSecret string) error {
ctx := app.NewContext(context.Background(), appInstance)
log := ctx.Logger().WithField("op", "PullWireGuards")
log.Debugf("start to pull wireguards belong to client, clientID: [%s]", clientID)
cli := ctx.GetApp().GetMasterCli()
resp, err := cli.Call().ListClientWireGuards(ctx, &pb.ListClientWireGuardsRequest{
Base: &pb.ClientBase{
ClientId: clientID,
ClientSecret: clientSecret,
},
})
if err != nil {
log.WithError(err).Errorf("cannot list client wireguards, do not change anything")
return err
}
if len(resp.GetWireguardConfigs()) == 0 {
log.Debugf("client [%s] has no wireguards", clientID)
return nil
}
log.Debugf("client [%s] has [%d] wireguards, check their status", clientID, len(resp.GetWireguardConfigs()))
wgMgr := ctx.GetApp().GetWireGuardManager()
successCnt := 0
for _, wireGuard := range resp.GetWireguardConfigs() {
wgCfg := &defs.WireGuardConfig{WireGuardConfig: wireGuard}
wgSvc, ok := wgMgr.GetService(wireGuard.GetInterfaceName())
if ok {
log.Debugf("wireguard [%s] already exists, skip create, update peers if need", wireGuard.GetInterfaceName())
wgSvc.PatchPeers(wgCfg.GetParsedPeers())
continue
}
wgSvc, err := wgMgr.CreateService(&defs.WireGuardConfig{WireGuardConfig: wireGuard})
if err != nil {
log.WithError(err).Errorf("create wireguard service failed")
continue
}
err = wgSvc.Start()
if err != nil {
log.WithError(err).Errorf("start wireguard service failed")
continue
}
successCnt++
}
log.Debugf("pull wireguards belong to client success, clientID: [%s], [%d] wireguards created", clientID, successCnt)
return nil
}
@@ -0,0 +1,43 @@
package client
import (
"context"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/samber/lo"
)
func ReportWireGuardRuntimeInfo(appInstance app.Application, clientID, clientSecret string) error {
ctx := app.NewContext(context.Background(), appInstance)
log := ctx.Logger().WithField("op", "ReportWireGuardRuntimeInfo")
log.Debugf("start to report wireguard runtime info, clientID: [%s]", clientID)
cli := ctx.GetApp().GetMasterCli()
wgs := ctx.GetApp().GetWireGuardManager().GetAllServices()
for _, wg := range wgs {
runtimeInfo, err := wg.GetWGRuntimeInfo()
if err != nil {
log.WithError(err).Errorf("failed to get wireguard runtime info")
continue
}
resp, err := cli.Call().ReportWireGuardRuntimeInfo(ctx, &pb.ReportWireGuardRuntimeInfoReq{
Base: &pb.ClientBase{
ClientId: clientID,
ClientSecret: clientSecret,
},
InterfaceName: lo.ToPtr(wg.GetBaseIfceConfig().InterfaceName),
RuntimeInfo: runtimeInfo,
})
if err != nil {
log.WithError(err).Errorf("failed to report wireguard runtime info")
return err
}
log.Debugf("report wireguard runtime info success, clientID: [%s], resp: %s", clientID, resp.String())
}
return nil
}
+5
View File
@@ -21,6 +21,11 @@ func StartFRPCHandler(ctx *app.Context, req *pb.StartFRPCRequest) (*pb.StartFRPC
}
}
if err := PullWireGuards(ctx.GetApp(), req.GetClientId(), ctx.GetApp().GetConfig().Client.Secret); err != nil {
logger.Logger(ctx).WithError(err).Error("cannot pull client wireguards")
return nil, err
}
return &pb.StartFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
+9
View File
@@ -1,6 +1,8 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
@@ -16,6 +18,13 @@ func StopFRPCHandler(ctx *app.Context, req *pb.StopFRPCRequest) (*pb.StopFRPCRes
ctx.GetApp().GetWorkersManager().StopAllWorkers(ctx)
}
errs := ctx.GetApp().GetWireGuardManager().StopAllServices()
if len(errs) > 0 {
logger.Logger(ctx).
WithError(fmt.Errorf("wireguard manager stop all wireguard error, errs: %v", errs)).
Errorf("wireguard manager stop all wireguard error")
}
return &pb.StopFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
+108
View File
@@ -0,0 +1,108 @@
package client
import (
"errors"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/samber/lo"
)
func UpdateWireGuard(ctx *app.Context, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "UpdateWireGuard")
wgSvc, ok := ctx.GetApp().GetWireGuardManager().GetService(req.GetWireguardConfig().GetInterfaceName())
if !ok || wgSvc == nil {
log.Errorf("get wireguard service failed")
return nil, errors.New("wireguard service not found")
}
switch req.GetUpdateType() {
case pb.UpdateWireGuardRequest_UPDATE_TYPE_ADD_PEER:
return AddPeer(ctx, wgSvc, req)
case pb.UpdateWireGuardRequest_UPDATE_TYPE_REMOVE_PEER:
return RemovePeer(ctx, wgSvc, req)
case pb.UpdateWireGuardRequest_UPDATE_TYPE_UPDATE_PEER:
return UpdatePeer(ctx, wgSvc, req)
case pb.UpdateWireGuardRequest_UPDATE_TYPE_PATCH_PEERS:
return PatchPeers(ctx, wgSvc, req)
default:
}
return nil, errors.New("update type not found, please check the update type in the request")
}
func AddPeer(ctx *app.Context, wgSvc app.WireGuard, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "AddPeer")
log.Debugf("add peer, peer_config: %+v", req.GetWireguardConfig().GetPeers())
for _, peer := range req.GetWireguardConfig().GetPeers() {
err := wgSvc.AddPeer(&defs.WireGuardPeerConfig{WireGuardPeerConfig: peer})
if err != nil {
log.WithError(err).Errorf("add peer failed")
continue
}
}
log.Infof("add peer done")
return &pb.UpdateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
func RemovePeer(ctx *app.Context, wgSvc app.WireGuard, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "RemovePeer")
log.Debugf("remove peer, peer_config: %+v", req.GetWireguardConfig().GetPeers())
for _, peer := range req.GetWireguardConfig().GetPeers() {
err := wgSvc.RemovePeer(peer.GetPublicKey())
if err != nil {
log.WithError(err).Errorf("remove peer failed")
continue
}
}
log.Infof("remove peer done")
return &pb.UpdateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
func UpdatePeer(ctx *app.Context, wgSvc app.WireGuard, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "UpdatePeer")
log.Debugf("update peer, peer_config: %+v", req.GetWireguardConfig().GetPeers())
for _, peer := range req.GetWireguardConfig().GetPeers() {
err := wgSvc.UpdatePeer(&defs.WireGuardPeerConfig{WireGuardPeerConfig: peer})
if err != nil {
log.WithError(err).Errorf("update peer failed")
continue
}
}
log.Infof("update peer done")
return &pb.UpdateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
func PatchPeers(ctx *app.Context, wgSvc app.WireGuard, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "PatchPeers")
log.Debugf("patch peers, peer_config: %+v", req.GetWireguardConfig().GetPeers())
wgCfg := &defs.WireGuardConfig{WireGuardConfig: req.GetWireguardConfig()}
diffResp, err := wgSvc.PatchPeers(wgCfg.GetParsedPeers())
if err != nil {
log.WithError(err).Errorf("patch peers failed")
return nil, err
}
log.Debugf("patch peers done, add_peers: %+v, remove_peers: %+v",
lo.Map(diffResp.AddPeers, func(item *defs.WireGuardPeerConfig, _ int) string { return item.GetClientId() }),
lo.Map(diffResp.RemovePeers, func(item *defs.WireGuardPeerConfig, _ int) string { return item.GetClientId() }))
return &pb.UpdateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
+36
View File
@@ -15,6 +15,8 @@ import (
"github.com/VaalaCat/frp-panel/middleware"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/gin-gonic/gin"
wgHandler "github.com/VaalaCat/frp-panel/biz/master/wg"
)
func NewRouter(fs embed.FS, appInstance app.Application) *gin.Engine {
@@ -97,6 +99,40 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
workerHandler.POST("/create_ingress", app.Wrapper(appInstance, worker.CreateWorkerIngress))
workerHandler.POST("/get_ingress", app.Wrapper(appInstance, worker.GetWorkerIngress))
}
wgRouter := v1.Group("/wg")
{
// network
wgRouter.POST("/network/create", app.Wrapper(appInstance, wgHandler.CreateNetwork))
wgRouter.POST("/network/delete", app.Wrapper(appInstance, wgHandler.DeleteNetwork))
wgRouter.POST("/network/update", app.Wrapper(appInstance, wgHandler.UpdateNetwork))
wgRouter.POST("/network/get", app.Wrapper(appInstance, wgHandler.GetNetwork))
wgRouter.POST("/network/list", app.Wrapper(appInstance, wgHandler.ListNetworks))
wgRouter.POST("/network/topology", app.Wrapper(appInstance, wgHandler.GetNetworkTopology))
// endpoint
wgRouter.POST("/endpoint/create", app.Wrapper(appInstance, wgHandler.CreateEndpoint))
wgRouter.POST("/endpoint/delete", app.Wrapper(appInstance, wgHandler.DeleteEndpoint))
wgRouter.POST("/endpoint/update", app.Wrapper(appInstance, wgHandler.UpdateEndpoint))
wgRouter.POST("/endpoint/get", app.Wrapper(appInstance, wgHandler.GetEndpoint))
wgRouter.POST("/endpoint/list", app.Wrapper(appInstance, wgHandler.ListEndpoints))
// link
wgRouter.POST("/link/create", app.Wrapper(appInstance, wgHandler.CreateWireGuardLink))
wgRouter.POST("/link/delete", app.Wrapper(appInstance, wgHandler.DeleteWireGuardLink))
wgRouter.POST("/link/update", app.Wrapper(appInstance, wgHandler.UpdateWireGuardLink))
wgRouter.POST("/link/get", app.Wrapper(appInstance, wgHandler.GetWireGuardLink))
wgRouter.POST("/link/list", app.Wrapper(appInstance, wgHandler.ListWireGuardLinks))
// wireguard
wgRouter.POST("/create", app.Wrapper(appInstance, wgHandler.CreateWireGuard))
wgRouter.POST("/delete", app.Wrapper(appInstance, wgHandler.DeleteWireGuard))
wgRouter.POST("/update", app.Wrapper(appInstance, wgHandler.UpdateWireGuard))
wgRouter.POST("/get", app.Wrapper(appInstance, wgHandler.GetWireGuard))
wgRouter.POST("/list", app.Wrapper(appInstance, wgHandler.ListWireGuards))
wgRouter.POST("/runtime/get", app.Wrapper(appInstance, wgHandler.GetWireGuardRuntimeInfo))
}
v1.GET("/pty/:clientID", shell.PTYHandler(appInstance))
v1.GET("/log", streamlog.GetLogHandler(appInstance))
}
+81
View File
@@ -0,0 +1,81 @@
package wg
import (
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
wgsvc "github.com/VaalaCat/frp-panel/services/wg"
"github.com/samber/lo"
)
func ListClientWireGuards(ctx *app.Context, req *pb.ListClientWireGuardsRequest) (*pb.ListClientWireGuardsResponse, error) {
clientId := req.GetBase().GetClientId()
log := ctx.Logger().WithField("op", "ListClientWireGuards")
wgCfgs, err := dao.NewQuery(ctx).AdminListWireGuardsWithClientID(clientId)
if err != nil {
log.WithError(err).Errorf("failed to list wireguard configs with client id: %s", clientId)
return nil, err
}
networkPeers := map[uint][]*models.WireGuard{}
networkIDs := lo.Map(wgCfgs, func(wgCfg *models.WireGuard, _ int) uint {
return wgCfg.NetworkID
})
allRelatedWgCfgs, err := dao.NewQuery(ctx).AdminListWireGuardsWithNetworkIDs(networkIDs)
if err != nil {
log.WithError(err).Errorf("failed to list wireguard configs with network ids: %v", networkIDs)
return nil, err
}
allRelatedWgLinks, err := dao.NewQuery(ctx).AdminListWireGuardLinksWithNetworkIDs(networkIDs)
if err != nil {
log.WithError(err).Errorf("failed to list wireguard links with network ids: %v", networkIDs)
return nil, err
}
networkLinksMap := make(map[uint][]*models.WireGuardLink)
for _, link := range allRelatedWgLinks {
if _, ok := networkLinksMap[link.NetworkID]; !ok {
networkLinksMap[link.NetworkID] = []*models.WireGuardLink{}
}
networkLinksMap[link.NetworkID] = append(networkLinksMap[link.NetworkID], link)
}
for _, wgCfg := range allRelatedWgCfgs {
if _, ok := networkPeers[wgCfg.NetworkID]; !ok {
networkPeers[wgCfg.NetworkID] = []*models.WireGuard{}
}
networkPeers[wgCfg.NetworkID] = append(networkPeers[wgCfg.NetworkID], wgCfg)
}
resp := &pb.ListClientWireGuardsResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
WireguardConfigs: lo.Map(wgCfgs, func(wgCfg *models.WireGuard, _ int) *pb.WireGuardConfig {
if wgCfg == nil || wgCfg.Network == nil {
log.Warnf("wireguard config or network is nil, wireguard id: %d", wgCfg.ID)
return nil
}
peerConfigs, err := wgsvc.PlanAllowedIPs(
networkPeers[wgCfg.NetworkID], networkLinksMap[wgCfg.NetworkID],
wgsvc.DefaultRoutingPolicy(
wgsvc.NewACL().LoadFromPB(wgCfg.Network.ACL.Data),
ctx.GetApp().GetNetworkTopologyCache()))
if err != nil {
log.WithError(err).Errorf("failed to plan allowed ips for wireguard configs: %v", wgCfgs)
return nil
}
r := wgCfg.ToPB()
r.Peers = lo.Map(peerConfigs[wgCfg.ID], func(peerCfg *pb.WireGuardPeerConfig, _ int) *pb.WireGuardPeerConfig {
return peerCfg
})
return r
}),
}
return resp, nil
}
+33
View File
@@ -0,0 +1,33 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func ReportWireGuardRuntimeInfo(ctx *app.Context, req *pb.ReportWireGuardRuntimeInfoReq) (*pb.ReportWireGuardRuntimeInfoResp, error) {
var (
interfaceName = req.GetInterfaceName()
clientId = req.GetBase().GetClientId()
log = ctx.Logger().WithField("op", "ReportWireGuardRuntimeInfo")
)
wgIfce, err := dao.NewQuery(ctx).AdminGetWireGuardByClientIDAndInterfaceName(clientId, interfaceName)
if err != nil {
log.WithError(err).Errorf("failed to get wireguard by client id and interface name, clientId: [%s], interfaceName: [%s]", clientId, interfaceName)
return nil, errors.New("failed to get wireguard by client id and interface name")
}
networkTopologyCache := ctx.GetApp().GetNetworkTopologyCache()
networkTopologyCache.SetRuntimeInfo(uint(wgIfce.ID), req.GetRuntimeInfo())
return &pb.ReportWireGuardRuntimeInfoResp{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "success",
},
}, nil
}
+28
View File
@@ -0,0 +1,28 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func CreateEndpoint(ctx *app.Context, req *pb.CreateEndpointRequest) (*pb.CreateEndpointResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
e := req.GetEndpoint()
if e == nil || len(e.GetHost()) == 0 || e.GetPort() == 0 {
return nil, errors.New("invalid endpoint params")
}
entity := &models.EndpointEntity{Host: e.GetHost(), Port: e.GetPort(), ClientID: e.GetClientId()}
if err := dao.NewQuery(ctx).CreateEndpoint(userInfo, entity); err != nil {
return nil, err
}
return &pb.CreateEndpointResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, Endpoint: &pb.Endpoint{Id: 0, Host: entity.Host, Port: entity.Port, ClientId: entity.ClientID}}, nil
}
+25
View File
@@ -0,0 +1,25 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func DeleteEndpoint(ctx *app.Context, req *pb.DeleteEndpointRequest) (*pb.DeleteEndpointResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
if err := dao.NewQuery(ctx).DeleteEndpoint(userInfo, id); err != nil {
return nil, err
}
return &pb.DeleteEndpointResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
+54
View File
@@ -0,0 +1,54 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/samber/lo"
)
func GetEndpoint(ctx *app.Context, req *pb.GetEndpointRequest) (*pb.GetEndpointResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
edp, err := dao.NewQuery(ctx).GetEndpointByID(userInfo, id)
if err != nil {
return nil, err
}
return &pb.GetEndpointResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Endpoint: edp.ToPB(),
}, nil
}
func ListEndpoints(ctx *app.Context, req *pb.ListEndpointsRequest) (*pb.ListEndpointsResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
page, pageSize := int(req.GetPage()), int(req.GetPageSize())
keyword := req.GetKeyword()
clientID := req.GetClientId()
wireguardID := uint(req.GetWireguardId())
list, err := dao.NewQuery(ctx).ListEndpointsWithFilters(userInfo, page, pageSize, clientID, wireguardID, keyword)
if err != nil {
return nil, err
}
count, err := dao.NewQuery(ctx).CountEndpointsWithFilters(userInfo, clientID, wireguardID, keyword)
if err != nil {
return nil, err
}
resp := lo.Map(list, func(item *models.Endpoint, _ int) *pb.Endpoint {
return item.ToPB()
})
return &pb.ListEndpointsResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, Endpoints: resp, Total: lo.ToPtr(int32(count))}, nil
}
+37
View File
@@ -0,0 +1,37 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func UpdateEndpoint(ctx *app.Context, req *pb.UpdateEndpointRequest) (*pb.UpdateEndpointResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
e := req.GetEndpoint()
if e == nil || e.GetId() == 0 || len(e.GetHost()) == 0 || e.GetPort() == 0 {
return nil, errors.New("invalid endpoint params")
}
oldEndpoint, err := dao.NewQuery(ctx).GetEndpointByID(userInfo, uint(e.GetId()))
if err != nil {
return nil, err
}
// 只更新 host 和 port
oldEndpoint.Host = e.GetHost()
oldEndpoint.Port = e.GetPort()
if err := dao.NewQuery(ctx).UpdateEndpoint(userInfo, uint(e.GetId()), oldEndpoint.EndpointEntity); err != nil {
return nil, err
}
return &pb.UpdateEndpointResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Endpoint: oldEndpoint.ToPB(),
}, nil
}
+83
View File
@@ -0,0 +1,83 @@
package wg
import (
"errors"
"fmt"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/wg"
"github.com/samber/lo"
)
func GetNetworkTopology(ctx *app.Context, req *pb.GetNetworkTopologyRequest) (*pb.GetNetworkTopologyResponse, error) {
log := ctx.Logger().WithField("op", "GetNetworkTopology")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
networkID := uint(req.GetId())
if networkID == 0 {
return nil, errors.New("invalid id")
}
q := dao.NewQuery(ctx)
peers, err := q.GetWireGuardsByNetworkID(userInfo, networkID)
if err != nil {
log.WithError(err).Errorf("failed to get wireguard peers by network id: %d", networkID)
return nil, err
}
links, err := q.ListWireGuardLinksByNetwork(userInfo, networkID)
if err != nil {
log.WithError(err).Errorf("failed to get wireguard links by network id: %d", networkID)
return nil, err
}
if len(peers) == 0 {
log.Errorf("no wireguard peers found")
return nil, fmt.Errorf("no wireguard peers found")
}
policy := wg.DefaultRoutingPolicy(wg.NewACL().LoadFromPB(peers[0].Network.ACL.Data), ctx.GetApp().GetNetworkTopologyCache())
resp, err := wg.NewDijkstraAllowedIPsPlanner(policy).BuildGraph(peers, links)
if err != nil {
log.WithError(err).Errorf("failed to build graph")
return nil, err
}
adjs := make(map[uint32]*pb.WireGuardLinks)
for id, peerConfigs := range resp {
adjs[uint32(id)] = &pb.WireGuardLinks{
Links: lo.Map(peerConfigs, func(e wg.Edge, _ int) *pb.WireGuardLink {
v := e.ToPB()
v.FromWireguardId = uint32(uint(id))
return v
}),
}
}
for id, links := range adjs {
for _, link := range links.GetLinks() {
toWireguardEdges, ok := adjs[uint32(link.GetToWireguardId())]
if ok {
for _, edge := range toWireguardEdges.GetLinks() {
if edge.GetToWireguardId() == uint32(uint(id)) {
link.DownBandwidthMbps = edge.GetUpBandwidthMbps()
}
}
}
}
}
return &pb.GetNetworkTopologyResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Adjs: adjs,
}, nil
}
+40
View File
@@ -0,0 +1,40 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
)
func GetWireGuardRuntimeInfo(ctx *app.Context, req *pb.GetWireGuardRuntimeInfoRequest) (*pb.GetWireGuardRuntimeInfoResponse, error) {
log := ctx.Logger().WithField("op", "GetWireGuardRuntimeInfo")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
log.Errorf("invalid user")
return nil, errors.New("invalid user")
}
wgRecord, err := dao.NewQuery(ctx).GetWireGuardByID(userInfo, uint(req.GetId()))
if err != nil {
log.WithError(err).Errorf("get wireguard by id failed")
return nil, errors.New("get wireguard by id failed")
}
resp := &pb.GetWireGuardRuntimeInfoResponse{}
if err := rpc.CallClientWrapper(ctx, wgRecord.ClientID, pb.Event_EVENT_GET_WIREGUARD_RUNTIME_INFO, &pb.GetWireGuardRuntimeInfoRequest{
InterfaceName: &wgRecord.Name,
}, resp); err != nil {
log.WithError(err).Errorf("failed to call get wireguard runtime info with clientId: %s", wgRecord.ClientID)
return nil, errors.New("failed to call get wireguard runtime info")
}
log.Debugf("get wireguard runtime info success with clientId: %s, interfaceName: %s, runtimeInfo: %s",
wgRecord.ClientID, wgRecord.Name, resp.GetWgDeviceRuntimeInfo().String())
return resp, nil
}
+146
View File
@@ -0,0 +1,146 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/samber/lo"
)
func CreateWireGuardLink(ctx *app.Context, req *pb.CreateWireGuardLinkRequest) (*pb.CreateWireGuardLinkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
l := req.GetWireguardLink()
if l == nil || l.GetFromWireguardId() == 0 || l.GetToWireguardId() == 0 || l.GetFromWireguardId() == l.GetToWireguardId() {
return nil, errors.New("invalid link params")
}
// 校验两端属于同一 network
q := dao.NewQuery(ctx)
from, err := q.GetWireGuardByID(userInfo, uint(l.GetFromWireguardId()))
if err != nil {
return nil, err
}
to, err := q.GetWireGuardByID(userInfo, uint(l.GetToWireguardId()))
if err != nil {
return nil, err
}
if from.NetworkID == 0 || from.NetworkID != to.NetworkID {
return nil, errors.New("wireguard not in same network")
}
m := &models.WireGuardLink{}
m.FromPB(l)
m.NetworkID = from.NetworkID
reverse := &models.WireGuardLink{}
reverse.FromPB((&defs.WireGuardLink{WireGuardLink: l}).GetReverse().WireGuardLink)
reverse.NetworkID = from.NetworkID
if err := q.CreateWireGuardLinks(userInfo, m, reverse); err != nil {
return nil, err
}
return &pb.CreateWireGuardLinkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardLink: m.ToPB()}, nil
}
func UpdateWireGuardLink(ctx *app.Context, req *pb.UpdateWireGuardLinkRequest) (*pb.UpdateWireGuardLinkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
l := req.GetWireguardLink()
if l == nil || l.GetId() == 0 || l.GetFromWireguardId() == 0 || l.GetToWireguardId() == 0 || l.GetFromWireguardId() == l.GetToWireguardId() {
return nil, errors.New("invalid link params")
}
q := dao.NewQuery(ctx)
m, err := q.GetWireGuardLinkByID(userInfo, uint(l.GetId()))
if err != nil {
return nil, err
}
// 只能修改这些
m.Active = l.GetActive()
m.LatencyMs = l.GetLatencyMs()
m.UpBandwidthMbps = l.GetUpBandwidthMbps()
m.DownBandwidthMbps = l.GetDownBandwidthMbps()
if err := q.UpdateWireGuardLink(userInfo, uint(l.GetId()), m); err != nil {
return nil, err
}
return &pb.UpdateWireGuardLinkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardLink: m.ToPB()}, nil
}
func DeleteWireGuardLink(ctx *app.Context, req *pb.DeleteWireGuardLinkRequest) (*pb.DeleteWireGuardLinkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
link, err := dao.NewQuery(ctx).GetWireGuardLinkByID(userInfo, id)
if err != nil {
return nil, err
}
rev, err := dao.NewQuery(ctx).GetWireGuardLinkByClientIDs(userInfo, link.ToWireGuardID, link.FromWireGuardID)
if err != nil {
return nil, err
}
if err := dao.NewQuery(ctx).DeleteWireGuardLink(userInfo, uint(link.ID)); err != nil {
return nil, err
}
if err := dao.NewQuery(ctx).DeleteWireGuardLink(userInfo, uint(rev.ID)); err != nil {
return nil, err
}
return &pb.DeleteWireGuardLinkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
func GetWireGuardLink(ctx *app.Context, req *pb.GetWireGuardLinkRequest) (*pb.GetWireGuardLinkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
m, err := dao.NewQuery(ctx).GetWireGuardLinkByID(userInfo, id)
if err != nil {
return nil, err
}
return &pb.GetWireGuardLinkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardLink: m.ToPB()}, nil
}
func ListWireGuardLinks(ctx *app.Context, req *pb.ListWireGuardLinksRequest) (*pb.ListWireGuardLinksResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
page, pageSize := int(req.GetPage()), int(req.GetPageSize())
keyword := req.GetKeyword()
networkID := uint(req.GetNetworkId())
list, err := dao.NewQuery(ctx).ListWireGuardLinksWithFilters(userInfo, page, pageSize, networkID, keyword)
if err != nil {
return nil, err
}
total, err := dao.NewQuery(ctx).CountWireGuardLinksWithFilters(userInfo, networkID, keyword)
if err != nil {
return nil, err
}
return &pb.ListWireGuardLinksResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Total: lo.ToPtr(int32(total)),
WireguardLinks: lo.Map(list, func(x *models.WireGuardLink, _ int) *pb.WireGuardLink {
return x.ToPB()
})}, nil
}
+54
View File
@@ -0,0 +1,54 @@
package wg
import (
"errors"
"net/netip"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func CreateNetwork(ctx *app.Context, req *pb.CreateNetworkRequest) (*pb.CreateNetworkResponse, error) {
log := ctx.Logger().WithField("op", "CreateNetwork")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
log.Errorf("invalid user")
return nil, errors.New("invalid user")
}
if req.GetNetwork() == nil || len(req.GetNetwork().GetName()) == 0 || len(req.GetNetwork().GetCidr()) == 0 {
return nil, errors.New("invalid request")
}
if _, err := netip.ParsePrefix(req.GetNetwork().GetCidr()); err != nil {
log.WithError(err).Errorf("invalid cidr")
return nil, errors.New("invalid cidr")
}
entity := &models.NetworkEntity{
Name: req.GetNetwork().GetName(),
CIDR: req.GetNetwork().GetCidr(),
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
ACL: models.JSON[*pb.AclConfig]{Data: req.GetNetwork().GetAcl()},
}
if err := dao.NewQuery(ctx).CreateNetwork(userInfo, entity); err != nil {
log.WithError(err).Errorf("create network error")
return nil, err
}
log.Debugf("create network success")
return &pb.CreateNetworkResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Network: &pb.Network{
Id: 0, UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
Name: entity.Name, Cidr: entity.CIDR,
},
}, nil
}
+27
View File
@@ -0,0 +1,27 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func DeleteNetwork(ctx *app.Context, req *pb.DeleteNetworkRequest) (*pb.DeleteNetworkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
if err := dao.NewQuery(ctx).DeleteNetwork(userInfo, id); err != nil {
return nil, err
}
return &pb.DeleteNetworkResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
}, nil
}
+74
View File
@@ -0,0 +1,74 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func GetNetwork(ctx *app.Context, req *pb.GetNetworkRequest) (*pb.GetNetworkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
net, err := dao.NewQuery(ctx).GetNetworkByID(userInfo, id)
if err != nil {
return nil, err
}
logger.Logger(ctx).Infof("get network: %+v", net)
return &pb.GetNetworkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Network: net.ToPB(),
}, nil
}
func ListNetworks(ctx *app.Context, req *pb.ListNetworksRequest) (*pb.ListNetworksResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
page, pageSize := int(req.GetPage()), int(req.GetPageSize())
keyword := req.GetKeyword()
var (
list []*models.Network
err error
total int64
)
if len(keyword) > 0 {
list, err = dao.NewQuery(ctx).ListNetworksWithKeyword(userInfo, page, pageSize, keyword)
if err != nil {
return nil, err
}
total, err = dao.NewQuery(ctx).CountNetworksWithKeyword(userInfo, keyword)
if err != nil {
return nil, err
}
} else {
list, err = dao.NewQuery(ctx).ListNetworks(userInfo, page, pageSize)
if err != nil {
return nil, err
}
total, err = dao.NewQuery(ctx).CountNetworks(userInfo)
if err != nil {
return nil, err
}
}
resp := lo.Map(list, func(item *models.Network, _ int) *pb.Network {
return item.ToPB()
})
return &pb.ListNetworksResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Networks: resp,
Total: lo.ToPtr(int32(total)),
}, nil
}
+31
View File
@@ -0,0 +1,31 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
)
func UpdateNetwork(ctx *app.Context, req *pb.UpdateNetworkRequest) (*pb.UpdateNetworkResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
n := req.GetNetwork()
if n == nil || n.GetId() == 0 || len(n.GetName()) == 0 || len(n.GetCidr()) == 0 {
return nil, errors.New("invalid network")
}
entity := &models.NetworkEntity{Name: n.GetName(), CIDR: n.GetCidr(), ACL: models.JSON[*pb.AclConfig]{Data: n.GetAcl()}}
if err := dao.NewQuery(ctx).UpdateNetwork(userInfo, uint(n.GetId()), entity); err != nil {
return nil, err
}
e := &models.Network{NetworkEntity: entity}
return &pb.UpdateNetworkResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"},
Network: e.ToPB(),
}, nil
}
+231
View File
@@ -0,0 +1,231 @@
package wg
import (
"errors"
"net/netip"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
wgsvc "github.com/VaalaCat/frp-panel/services/wg"
"github.com/VaalaCat/frp-panel/utils"
"github.com/samber/lo"
)
// Create/Update/Get/List WireGuard 基于 pb.WireGuardConfig
// 将 pb 映射到 models.WireGuard + models.Endpoint 列表
func CreateWireGuard(ctx *app.Context, req *pb.CreateWireGuardRequest) (*pb.CreateWireGuardResponse, error) {
log := ctx.Logger().WithField("op", "CreateWireGuard")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
cfg := req.GetWireguardConfig()
if cfg == nil || len(cfg.GetClientId()) == 0 || len(cfg.GetInterfaceName()) == 0 || len(cfg.GetLocalAddress()) == 0 {
return nil, errors.New("invalid wireguard config")
}
ips, err := dao.NewQuery(ctx).GetWireGuardLocalAddressesByNetworkID(userInfo, uint(cfg.GetNetworkId()))
if err != nil {
log.WithError(err).Errorf("get wireguard local addresses by network id failed")
return nil, err
}
network, err := dao.NewQuery(ctx).GetNetworkByID(userInfo, uint(cfg.GetNetworkId()))
if err != nil {
log.WithError(err).Errorf("get network by id failed")
return nil, err
}
newIpStr, err := utils.AllocateIP(network.CIDR, ips, cfg.GetLocalAddress())
if err != nil {
log.WithError(err).Errorf("allocate ip failed")
return nil, err
}
newIp, err := netip.ParseAddr(newIpStr)
if err != nil {
log.WithError(err).Errorf("parse ip failed")
return nil, err
}
networkCidr, err := netip.ParsePrefix(network.CIDR)
if err != nil {
log.WithError(err).Errorf("parse network cidr failed")
return nil, err
}
newIpCidr := netip.PrefixFrom(newIp, networkCidr.Bits())
keys := wgsvc.GenerateKeys()
wgModel := &models.WireGuard{
WireGuardEntity: &models.WireGuardEntity{
Name: cfg.GetInterfaceName(),
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
PrivateKey: keys.PrivateKeyBase64,
LocalAddress: newIpCidr.String(),
ListenPort: cfg.GetListenPort(),
InterfaceMtu: cfg.GetInterfaceMtu(),
DnsServers: models.GormArray[string](cfg.GetDnsServers()),
ClientID: cfg.GetClientId(),
NetworkID: uint(cfg.GetNetworkId()),
Tags: models.GormArray[string](cfg.GetTags()),
},
}
log.Debugf("create wireguard with config: %+v", wgModel)
if err := dao.NewQuery(ctx).CreateWireGuard(userInfo, wgModel); err != nil {
return nil, err
}
// 处理端点:优先复用已存在的 endpoint(通过 id),否则创建新端点并绑定到该 WireGuard
for _, ep := range cfg.GetAdvertisedEndpoints() {
if ep == nil {
continue
}
if ep.GetId() > 0 {
// 复用现有 endpoint,要求归属同一 client
exist, err := dao.NewQuery(ctx).GetEndpointByID(userInfo, uint(ep.GetId()))
if err != nil {
return nil, err
}
if exist.ClientID != cfg.GetClientId() {
return nil, errors.New("endpoint client mismatch")
}
entity := &models.EndpointEntity{Host: exist.Host, Port: exist.Port, ClientID: exist.ClientID, WireGuardID: wgModel.ID}
if err := dao.NewQuery(ctx).UpdateEndpoint(userInfo, uint(exist.ID), entity); err != nil {
return nil, err
}
} else {
// 创建并绑定新端点
entity := &models.EndpointEntity{Host: ep.GetHost(), Port: ep.GetPort(), ClientID: cfg.GetClientId(), WireGuardID: wgModel.ID}
if err := dao.NewQuery(ctx).CreateEndpoint(userInfo, entity); err != nil {
return nil, err
}
}
}
go func() {
peers, err := dao.NewQuery(ctx).GetWireGuardsByNetworkID(userInfo, uint(cfg.GetNetworkId()))
if err != nil {
log.WithError(err).Errorf("get wireguards by network id failed")
return
}
links, err := dao.NewQuery(ctx).ListWireGuardLinksByNetwork(userInfo, uint(cfg.GetNetworkId()))
if err != nil {
log.WithError(err).Errorf("get wireguard links by network id failed")
return
}
peerConfigs, err := wgsvc.PlanAllowedIPs(
peers,
links,
wgsvc.DefaultRoutingPolicy(wgsvc.NewACL().LoadFromPB(network.ACL.Data), ctx.GetApp().GetNetworkTopologyCache()))
if err != nil {
log.WithError(err).Errorf("build peer configs for network failed")
return
}
for _, peer := range peers {
if peer.ClientID == cfg.GetClientId() {
if err := emitCreateWireGuardEventToClient(ctx, peer, peerConfigs[peer.ID]); err != nil {
log.WithError(err).Errorf("update config to client failed")
}
continue
}
if err := emitPatchWireGuardEventToClient(ctx, peer, peerConfigs[peer.ID]); err != nil {
log.WithError(err).Errorf("add wireguard event send to client error")
continue
}
log.Debugf("update config to client success, client id: [%s], wireguard interface: [%s]", peer.ClientID, peer.Name)
}
}()
return &pb.CreateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardConfig: cfg}, nil
}
func emitCreateWireGuardEventToClient(ctx *app.Context, peer *models.WireGuard, peerConfigs []*pb.WireGuardPeerConfig) error {
log := ctx.Logger().WithField("op", "updateConfigToClient")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return errors.New("invalid user")
}
resp := &pb.CreateWireGuardResponse{}
err := rpc.CallClientWrapper(ctx, peer.ClientID, pb.Event_EVENT_CREATE_WIREGUARD, &pb.CreateWireGuardRequest{
WireguardConfig: &pb.WireGuardConfig{
ClientId: peer.ClientID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
InterfaceName: peer.Name,
PrivateKey: peer.PrivateKey,
LocalAddress: peer.LocalAddress,
ListenPort: peer.ListenPort,
InterfaceMtu: peer.InterfaceMtu,
DnsServers: peer.DnsServers,
AdvertisedEndpoints: lo.Map(peer.AdvertisedEndpoints, func(e *models.Endpoint, _ int) *pb.Endpoint {
return &pb.Endpoint{
Host: e.Host,
Port: e.Port,
}
}),
NetworkId: uint32(peer.NetworkID),
Peers: peerConfigs,
Tags: peer.Tags,
},
}, resp)
if err != nil {
log.WithError(err).Errorf("create wireguard event send to client error")
return err
}
log.Infof("create wireguard event send to client success, client id: [%s], wireguard interface: [%s]",
peer.ClientID, peer.Name)
return nil
}
func emitPatchWireGuardEventToClient(ctx *app.Context, peer *models.WireGuard, peerConfigs []*pb.WireGuardPeerConfig) error {
log := ctx.Logger().WithField("op", "patchWireGuardToClient")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return errors.New("invalid user")
}
resp := &pb.UpdateWireGuardResponse{}
err := rpc.CallClientWrapper(ctx, peer.ClientID, pb.Event_EVENT_UPDATE_WIREGUARD, &pb.UpdateWireGuardRequest{
WireguardConfig: &pb.WireGuardConfig{
ClientId: peer.ClientID,
InterfaceName: peer.Name,
PrivateKey: peer.PrivateKey,
LocalAddress: peer.LocalAddress,
ListenPort: peer.ListenPort,
InterfaceMtu: peer.InterfaceMtu,
DnsServers: peer.DnsServers,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
Peers: peerConfigs,
Tags: peer.Tags,
},
UpdateType: pb.UpdateWireGuardRequest_UPDATE_TYPE_PATCH_PEERS.Enum(),
}, resp)
if err != nil {
log.WithError(err).Errorf("add wireguard event send to client error")
return err
}
log.Infof("add wireguard event send to client success, client id: [%s], wireguard interface: [%s]",
peer.ClientID, peer.Name)
return nil
}
+118
View File
@@ -0,0 +1,118 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
wgsvc "github.com/VaalaCat/frp-panel/services/wg"
)
func DeleteWireGuard(ctx *app.Context, req *pb.DeleteWireGuardRequest) (*pb.DeleteWireGuardResponse, error) {
userInfo := common.GetUserInfo(ctx)
log := ctx.Logger().WithField("op", "DeleteWireGuard")
if !userInfo.Valid() {
return &pb.DeleteWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "invalid user"}}, nil
}
id := uint(req.GetId())
if id == 0 {
return &pb.DeleteWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "invalid id"}}, nil
}
wgToDelete, err := dao.NewQuery(ctx).GetWireGuardByID(userInfo, id)
if err != nil {
log.WithError(err).Errorf("get wireguard by id failed")
return nil, err
}
if err := dao.NewQuery(ctx).DeleteWireGuard(userInfo, id); err != nil {
log.WithError(err).Errorf("delete wireguard failed")
return nil, err
}
log.Debugf("delete wireguard success, id: %d", id)
ctxBg := ctx.Background()
go func() {
resp, err := rpc.CallClient(ctxBg, wgToDelete.ClientID, pb.Event_EVENT_DELETE_WIREGUARD, &pb.DeleteWireGuardRequest{
ClientId: &wgToDelete.ClientID,
InterfaceName: &wgToDelete.Name,
})
if err != nil {
log.WithError(err).Errorf("delete wireguard event send to client failed")
}
if resp == nil {
log.Errorf("cannot get response, client id: [%s]", wgToDelete.ClientID)
}
peers, err := dao.NewQuery(ctx).GetWireGuardsByNetworkID(userInfo, uint(wgToDelete.NetworkID))
if err != nil {
log.WithError(err).Errorf("get wireguards by network id failed")
return
}
if len(peers) == 0 {
log.Infof("no wireguards in network, network id: [%d]", wgToDelete.NetworkID)
return
}
links, err := dao.NewQuery(ctx).ListWireGuardLinksByNetwork(userInfo, uint(wgToDelete.NetworkID))
if err != nil {
log.WithError(err).Errorf("get wireguard links by network id failed")
return
}
peerConfigs, err := wgsvc.PlanAllowedIPs(peers, links,
wgsvc.DefaultRoutingPolicy(
wgsvc.NewACL().LoadFromPB(wgToDelete.Network.ACL.Data),
ctx.GetApp().GetNetworkTopologyCache(),
))
if err != nil {
log.WithError(err).Errorf("build peer configs for network failed")
return
}
for _, peer := range peers {
if err := emitPatchWireGuardEventToClient(ctx, peer, peerConfigs[peer.ID]); err != nil {
log.WithError(err).Errorf("patch wireguard event send to client error")
continue
}
log.Debugf("update config to client success, client id: [%s], wireguard interface: [%s]", peer.ClientID, peer.Name)
}
}()
return &pb.DeleteWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}}, nil
}
func emitDeleteWireGuardEventToClient(ctx *app.Context, peerNeedRemoveWg *models.WireGuard, wgToDelete *models.WireGuard) error {
log := ctx.Logger().WithField("op", "emitDeleteWireGuardEventToClient")
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return errors.New("invalid user")
}
resp := &pb.UpdateWireGuardResponse{}
err := rpc.CallClientWrapper(ctx, peerNeedRemoveWg.ClientID, pb.Event_EVENT_UPDATE_WIREGUARD, &pb.UpdateWireGuardRequest{
WireguardConfig: &pb.WireGuardConfig{
InterfaceName: peerNeedRemoveWg.Name,
Peers: []*pb.WireGuardPeerConfig{{ClientId: peerNeedRemoveWg.ClientID}},
},
UpdateType: pb.UpdateWireGuardRequest_UPDATE_TYPE_REMOVE_PEER.Enum(),
}, resp)
if err != nil {
log.WithError(err).Errorf("delete wireguard event send to client error")
return err
}
log.Infof("delete wireguard event send to client success, client id: [%s], wireguard interface: [%s]",
wgToDelete.ClientID, wgToDelete.Name)
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/samber/lo"
)
func GetWireGuard(ctx *app.Context, req *pb.GetWireGuardRequest) (*pb.GetWireGuardResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
id := uint(req.GetId())
if id == 0 {
return nil, errors.New("invalid id")
}
wg, err := dao.NewQuery(ctx).GetWireGuardByID(userInfo, id)
if err != nil {
return nil, err
}
resp := wg.ToPB()
return &pb.GetWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardConfig: resp}, nil
}
func ListWireGuards(ctx *app.Context, req *pb.ListWireGuardsRequest) (*pb.ListWireGuardsResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
page, pageSize := int(req.GetPage()), int(req.GetPageSize())
keyword := req.GetKeyword()
filter := &models.WireGuardEntity{}
if cid := req.GetClientId(); len(cid) > 0 {
filter.ClientID = cid
}
// proto 的 network_id 是 string,但模型为 uint;此处仅在非空时参与过滤,解析失败则忽略
if nid := req.GetNetworkId(); nid > 0 {
filter.NetworkID = uint(nid)
}
list, err := dao.NewQuery(ctx).ListWireGuardsWithFilters(userInfo, page, pageSize, filter, keyword)
if err != nil {
return nil, err
}
total, err := dao.NewQuery(ctx).CountWireGuardsWithFilters(userInfo, filter, keyword)
if err != nil {
return nil, err
}
resp := lo.Map(list, func(item *models.WireGuard, _ int) *pb.WireGuardConfig {
return item.ToPB()
})
return &pb.ListWireGuardsResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardConfigs: resp, Total: lo.ToPtr(int32(total))}, nil
}
+94
View File
@@ -0,0 +1,94 @@
package wg
import (
"errors"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/samber/lo"
)
func UpdateWireGuard(ctx *app.Context, req *pb.UpdateWireGuardRequest) (*pb.UpdateWireGuardResponse, error) {
userInfo := common.GetUserInfo(ctx)
if !userInfo.Valid() {
return nil, errors.New("invalid user")
}
cfg := req.GetWireguardConfig()
if cfg == nil || cfg.GetId() == 0 || len(cfg.GetClientId()) == 0 || len(cfg.GetInterfaceName()) == 0 || len(cfg.GetPrivateKey()) == 0 || len(cfg.GetLocalAddress()) == 0 {
return nil, errors.New("invalid wireguard config")
}
model := &models.WireGuard{
WireGuardEntity: &models.WireGuardEntity{
Name: cfg.GetInterfaceName(),
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
PrivateKey: cfg.GetPrivateKey(),
LocalAddress: cfg.GetLocalAddress(),
ListenPort: cfg.GetListenPort(),
InterfaceMtu: cfg.GetInterfaceMtu(),
DnsServers: models.GormArray[string](cfg.GetDnsServers()),
ClientID: cfg.GetClientId(),
NetworkID: uint(cfg.GetNetworkId()),
Tags: models.GormArray[string](cfg.GetTags()),
},
}
if err := dao.NewQuery(ctx).UpdateWireGuard(userInfo, uint(cfg.GetId()), model); err != nil {
return nil, err
}
// 端点赋值与解绑
// 1) 读取当前绑定的端点
currentList, err := dao.NewQuery(ctx).ListEndpointsWithFilters(userInfo, 1, 1000, "", uint(cfg.GetId()), "")
if err != nil {
return nil, err
}
currentSet := lo.SliceToMap(currentList, func(e *models.Endpoint) (uint, struct{}) { return uint(e.ID), struct{}{} })
// 2) 处理本次配置中的端点:存在则更新并绑定;不存在则创建绑定
newSet := map[uint]struct{}{}
for _, ep := range cfg.GetAdvertisedEndpoints() {
if ep == nil {
continue
}
if ep.GetId() > 0 {
exist, err := dao.NewQuery(ctx).GetEndpointByID(userInfo, uint(ep.GetId()))
if err != nil {
return nil, err
}
// 必须属于相同 client
if exist.ClientID != cfg.GetClientId() {
return nil, errors.New("endpoint client mismatch")
}
entity := &models.EndpointEntity{Host: ep.GetHost(), Port: ep.GetPort(), ClientID: exist.ClientID, WireGuardID: uint(cfg.GetId())}
if err := dao.NewQuery(ctx).UpdateEndpoint(userInfo, uint(exist.ID), entity); err != nil {
return nil, err
}
newSet[uint(exist.ID)] = struct{}{}
} else {
entity := &models.EndpointEntity{Host: ep.GetHost(), Port: ep.GetPort(), ClientID: cfg.GetClientId(), WireGuardID: uint(cfg.GetId())}
if err := dao.NewQuery(ctx).CreateEndpoint(userInfo, entity); err != nil {
return nil, err
}
// 无法获取新建 id,这里不加入 newSet;不影响后续解绑逻辑(仅解绑 current - new
}
}
// 3) 解绑本次未包含的端点(将 WireGuardID 置 0
for id := range currentSet {
if _, ok := newSet[id]; ok {
continue
}
exist, err := dao.NewQuery(ctx).GetEndpointByID(userInfo, id)
if err != nil {
return nil, err
}
entity := &models.EndpointEntity{Host: exist.Host, Port: exist.Port, ClientID: exist.ClientID, WireGuardID: 0}
if err := dao.NewQuery(ctx).UpdateEndpoint(userInfo, id, entity); err != nil {
return nil, err
}
}
return &pb.UpdateWireGuardResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "success"}, WireguardConfig: cfg}, nil
}
+1 -1
View File
@@ -2,7 +2,7 @@
# Function to print usage
usage() {
echo "Usage: $0 [--platform <platform>] [--bintype <bintype>] [--arch <arch>]"
echo "Usage: $0 [--platform <platform>] [--bintype <bintype>] [--arch <arch>] [--skip-frontend] [--current]"
echo "Platforms: windows, linux, darwin, all"
echo "Binary Types: full, client, all"
echo "Architectures: amd64, arm64, arm, all"
+18 -4
View File
@@ -22,10 +22,11 @@ type runClientParam struct {
Lc fx.Lifecycle
Ctx *app.Context
AppInstance app.Application
TaskManager watcher.Client `name:"clientTaskManager"`
WorkersManager app.WorkersManager
Ctx *app.Context
AppInstance app.Application
TaskManager watcher.Client `name:"clientTaskManager"`
WorkersManager app.WorkersManager
WireGuardManager app.WireGuardManager
Cfg conf.Config
}
@@ -50,6 +51,10 @@ func runClient(param runClientParam) {
bizclient.PullConfig, appInstance, clientID, clientSecret)
param.TaskManager.AddDurationTask(defs.PullClientWorkersDuration,
bizclient.PullWorkers, appInstance, clientID, clientSecret)
param.TaskManager.AddDurationTask(defs.PullClientWireGuardsDuration,
bizclient.PullWireGuards, appInstance, clientID, clientSecret)
param.TaskManager.AddDurationTask(defs.ReportWireGuardRuntimeInfoDuration,
bizclient.ReportWireGuardRuntimeInfo, appInstance, clientID, clientSecret)
var wg conc.WaitGroup
param.Lc.Append(fx.Hook{
@@ -57,6 +62,7 @@ func runClient(param runClientParam) {
appInstance.SetRPCCred(NewClientCred(appInstance))
appInstance.SetMasterCli(rpc.NewMasterCli(appInstance))
appInstance.SetClientController(tunnel.NewClientController())
appInstance.SetWireGuardManager(param.WireGuardManager)
cliRpcHandler := clientrpc.NewClientRPCHandler(
appInstance,
@@ -70,6 +76,7 @@ func runClient(param runClientParam) {
// --- init once start ---
initClientOnce(appInstance, clientID, clientSecret)
initClientWorkerOnce(appInstance, clientID, clientSecret)
initClientWireGuardOnce(appInstance, clientID, clientSecret)
// --- init once stop ----
wg.Go(cliRpcHandler.Run)
@@ -99,3 +106,10 @@ func initClientWorkerOnce(appInstance app.Application, clientID, clientSecret st
logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client workers, wait for retry")
}
}
func initClientWireGuardOnce(appInstance app.Application, clientID, clientSecret string) {
err := bizclient.PullWireGuards(appInstance, clientID, clientSecret)
if err != nil {
logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client wireguards, wait for retry")
}
}
+1
View File
@@ -16,6 +16,7 @@ var (
fx.Provide(
NewWorkerExecManager,
NewWorkersManager,
NewWireGuardManager,
fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
))
+6
View File
@@ -22,6 +22,7 @@ import (
"github.com/VaalaCat/frp-panel/services/rbac"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/services/watcher"
"github.com/VaalaCat/frp-panel/services/wg"
"github.com/VaalaCat/frp-panel/services/workerd"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
@@ -62,6 +63,7 @@ func NewBaseApp(param struct {
appInstance.SetStreamLogHookMgr(param.HookMgr)
appInstance.SetShellPTYMgr(param.PtyMgr)
appInstance.SetClientRecvMap(&sync.Map{})
appInstance.SetNetworkTopologyCache(wg.NewNetworkTopologyCache())
return appInstance
}
@@ -392,3 +394,7 @@ func NewWorkerExecManager(cfg conf.Config, appInstance app.Application) app.Work
appInstance.SetWorkerExecManager(mgr)
return mgr
}
func NewWireGuardManager(appInstance app.Application) app.WireGuardManager {
return wg.NewWireGuardManager(appInstance)
}
+1 -1
View File
@@ -3,4 +3,4 @@
PROTOC_PATH=$(whereis protoc | awk '{print $2}')
cd idl && $PROTOC_PATH *.proto --go_out=. --go-grpc_out=. && cd ..
cd www && npx $PROTOC_PATH --ts_out ./lib/pb -I ../idl --proto_path ../idl/common.proto ../idl/common.proto ../idl/api*.proto && cd ..
cd www && npx $PROTOC_PATH --ts_out ./lib/pb -I ../idl --proto_path ../idl/common.proto ../idl/common.proto ../idl/api*.proto ../idl/types*.proto && cd ..
+7 -1
View File
@@ -29,7 +29,13 @@ type ReqType interface {
pb.CreateWorkerRequest | pb.RemoveWorkerRequest | pb.RunWorkerRequest | pb.StopWorkerRequest | pb.UpdateWorkerRequest | pb.GetWorkerRequest |
pb.ListWorkersRequest | pb.CreateWorkerIngressRequest | pb.GetWorkerIngressRequest |
pb.GetWorkerStatusRequest | pb.InstallWorkerdRequest | pb.RedeployWorkerRequest |
pb.StartSteamLogRequest
pb.StartSteamLogRequest |
// wireguard api
pb.CreateNetworkRequest | pb.DeleteNetworkRequest | pb.UpdateNetworkRequest | pb.GetNetworkRequest | pb.ListNetworksRequest |
pb.CreateEndpointRequest | pb.DeleteEndpointRequest | pb.UpdateEndpointRequest | pb.GetEndpointRequest | pb.ListEndpointsRequest |
pb.CreateWireGuardRequest | pb.DeleteWireGuardRequest | pb.UpdateWireGuardRequest | pb.GetWireGuardRequest | pb.ListWireGuardsRequest |
pb.CreateWireGuardLinkRequest | pb.DeleteWireGuardLinkRequest | pb.UpdateWireGuardLinkRequest | pb.GetWireGuardLinkRequest | pb.ListWireGuardLinksRequest |
pb.GetWireGuardRuntimeInfoRequest | pb.GetNetworkTopologyRequest
}
func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {
+15 -1
View File
@@ -30,7 +30,13 @@ type RespType interface {
pb.CreateWorkerResponse | pb.RemoveWorkerResponse | pb.RunWorkerResponse | pb.StopWorkerResponse | pb.UpdateWorkerResponse | pb.GetWorkerResponse |
pb.ListWorkersResponse | pb.CreateWorkerIngressResponse | pb.GetWorkerIngressResponse |
pb.GetWorkerStatusResponse | pb.InstallWorkerdResponse | pb.RedeployWorkerResponse |
pb.StartSteamLogResponse
pb.StartSteamLogResponse |
// wireguard api
pb.CreateNetworkResponse | pb.DeleteNetworkResponse | pb.UpdateNetworkResponse | pb.GetNetworkResponse | pb.ListNetworksResponse |
pb.CreateEndpointResponse | pb.DeleteEndpointResponse | pb.UpdateEndpointResponse | pb.GetEndpointResponse | pb.ListEndpointsResponse |
pb.CreateWireGuardResponse | pb.DeleteWireGuardResponse | pb.UpdateWireGuardResponse | pb.GetWireGuardResponse | pb.ListWireGuardsResponse |
pb.CreateWireGuardLinkResponse | pb.DeleteWireGuardLinkResponse | pb.UpdateWireGuardLinkResponse | pb.GetWireGuardLinkResponse | pb.ListWireGuardLinksResponse |
pb.GetWireGuardRuntimeInfoResponse | pb.GetNetworkTopologyResponse
}
func OKResp[T RespType](c *gin.Context, origin *T) {
@@ -108,6 +114,14 @@ func getEvent(origin interface{}) (pb.Event, protoreflect.ProtoMessage, error) {
return pb.Event_EVENT_GET_WORKER_STATUS, ptr, nil
case *pb.InstallWorkerdResponse:
return pb.Event_EVENT_INSTALL_WORKERD, ptr, nil
case *pb.CreateWireGuardResponse:
return pb.Event_EVENT_CREATE_WIREGUARD, ptr, nil
case *pb.DeleteWireGuardResponse:
return pb.Event_EVENT_DELETE_WIREGUARD, ptr, nil
case *pb.UpdateWireGuardResponse:
return pb.Event_EVENT_UPDATE_WIREGUARD, ptr, nil
case *pb.GetWireGuardRuntimeInfoResponse:
return pb.Event_EVENT_GET_WIREGUARD_RUNTIME_INFO, ptr, nil
default:
return 0, nil, fmt.Errorf("cannot unmarshal unknown type: %T", origin)
}
+13 -4
View File
@@ -68,10 +68,14 @@ const (
)
const (
PullConfigDuration = 30 * time.Second
PushProxyInfoDuration = 30 * time.Second
PullClientWorkersDuration = 30 * time.Second
AppStartTimeout = 5 * time.Minute
PullConfigDuration = 30 * time.Second
PushProxyInfoDuration = 30 * time.Second
PullClientWorkersDuration = 30 * time.Second
PullClientWireGuardsDuration = 30 * time.Second
ReportWireGuardRuntimeInfoDuration = 60 * time.Second
AppStartTimeout = 5 * time.Minute
)
const (
@@ -182,6 +186,11 @@ const (
PlaceholderPeerVPNAddressCIDR = "<PEER_VPN_IP_ADDRESS/PREFIX>"
)
const (
DefaultDeviceMTU uint32 = 1400
DefaultPersistentKeepalive = 25
)
var VaalaMagicBytes = []byte("vaala-ping")
const VaalaMagicBytesCookie = uint32(1630367849)
+125
View File
@@ -0,0 +1,125 @@
package defs
import (
"bytes"
"errors"
"github.com/VaalaCat/frp-panel/pb"
"github.com/samber/lo"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type WireGuardConfig struct {
*pb.WireGuardConfig
parsedPublicKey wgtypes.Key `json:"-"`
parsedPrivKey wgtypes.Key `json:"-"`
}
func (w *WireGuardConfig) GetParsedPublicKey() wgtypes.Key {
if IsZeroKey(w.parsedPublicKey) {
w.parsedPublicKey = w.GetParsedPrivKey().PublicKey()
}
return w.parsedPublicKey
}
func (w *WireGuardConfig) GetParsedPrivKey() wgtypes.Key {
if IsZeroKey(w.parsedPrivKey) {
var err error
w.parsedPrivKey, err = wgtypes.ParseKey(w.GetPrivateKey())
if err != nil {
panic(errors.Join(errors.New("parse private key error"), err))
}
}
return w.parsedPrivKey
}
func (w *WireGuardConfig) GetParsedPeers() []*WireGuardPeerConfig {
parsedPeers := make([]*WireGuardPeerConfig, 0, len(w.GetPeers()))
for _, p := range w.GetPeers() {
parsedPeers = append(parsedPeers, &WireGuardPeerConfig{WireGuardPeerConfig: p})
}
return parsedPeers
}
type WireGuardPeerConfig struct {
*pb.WireGuardPeerConfig
parsedPublicKey wgtypes.Key `json:"-"`
parsedPresharedKey wgtypes.Key `json:"-"`
}
func (w *WireGuardPeerConfig) GetParsedPublicKey() wgtypes.Key {
if IsZeroKey(w.parsedPublicKey) {
var err error
w.parsedPublicKey, err = wgtypes.ParseKey(w.GetPublicKey())
if err != nil {
panic(errors.Join(errors.New("parse public key error"), err))
}
}
return w.parsedPublicKey
}
func (w *WireGuardPeerConfig) GetParsedPresharedKey() *wgtypes.Key {
if w.GetPresharedKey() == "" {
return nil
}
if IsZeroKey(w.parsedPresharedKey) {
var err error
w.parsedPresharedKey, err = wgtypes.ParseKey(w.GetPresharedKey())
if err != nil {
panic(errors.Join(errors.New("parse preshared key error"), err))
}
}
return lo.ToPtr(w.parsedPresharedKey)
}
func (w *WireGuardPeerConfig) Equal(other *WireGuardPeerConfig) bool {
endpointEqual := false
if w.Endpoint != nil && other.Endpoint != nil {
endpointEqual = (w.Endpoint.Host == other.Endpoint.Host && w.Endpoint.Port == other.Endpoint.Port)
} else if w.Endpoint == nil && other.Endpoint == nil {
endpointEqual = true
}
oldExtraIps, newExtraIps := lo.Difference(w.GetAllowedIps(), other.GetAllowedIps())
allowedIpsEqual := len(oldExtraIps) == 0 && len(newExtraIps) == 0
return w.Id == other.Id &&
w.ClientId == other.ClientId &&
w.UserId == other.UserId &&
w.TenantId == other.TenantId &&
w.PublicKey == other.PublicKey &&
w.PresharedKey == other.PresharedKey &&
w.PersistentKeepalive == other.PersistentKeepalive &&
endpointEqual &&
allowedIpsEqual
}
// IsZeroKey 检查一个 wgtypes.Key 是否是空。
func IsZeroKey(key wgtypes.Key) bool {
var zeroKey wgtypes.Key
return bytes.Equal(key[:], zeroKey[:])
}
type WireGuardLink struct {
*pb.WireGuardLink
}
func (w *WireGuardLink) GetReverse() *WireGuardLink {
return &WireGuardLink{
WireGuardLink: &pb.WireGuardLink{
Id: w.Id,
FromWireguardId: w.ToWireguardId,
ToWireguardId: w.FromWireguardId,
UpBandwidthMbps: w.DownBandwidthMbps,
DownBandwidthMbps: w.UpBandwidthMbps,
LatencyMs: w.LatencyMs,
Active: w.Active,
},
}
}
+15 -13
View File
@@ -1,6 +1,6 @@
module github.com/VaalaCat/frp-panel
go 1.23.1
go 1.24.0
toolchain go1.24.1
@@ -38,8 +38,9 @@ require (
github.com/tiendc/go-deepcopy v1.2.0
go.uber.org/fx v1.23.0
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
golang.org/x/crypto v0.42.0
golang.org/x/net v0.44.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5
gorm.io/driver/mysql v1.5.7
@@ -108,11 +109,12 @@ require (
github.com/pion/logging v0.2.2 // indirect
github.com/pion/stun/v2 v2.0.0 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/transport/v3 v3.0.1 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus-community/pro-bing v0.7.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
@@ -131,8 +133,9 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vishvananda/netlink v1.3.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.3 // indirect
github.com/xtaci/kcp-go/v5 v5.6.13 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
@@ -141,22 +144,21 @@ require (
go.uber.org/zap v1.26.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/plugin/dbresolver v1.5.2 // indirect
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.38.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
+30 -9
View File
@@ -259,8 +259,9 @@ github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@@ -274,6 +275,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -349,10 +352,12 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
@@ -392,6 +397,8 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
@@ -403,6 +410,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -422,6 +431,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
@@ -433,6 +444,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -458,6 +471,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -467,6 +482,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -478,6 +494,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -492,14 +510,17 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -546,8 +567,8 @@ gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII=
gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ=
+231
View File
@@ -0,0 +1,231 @@
syntax = "proto3";
package api_wireguard;
option go_package = "../pb";
import "common.proto";
import "types_wg.proto";
message CreateNetworkRequest {
optional wireguard.Network network = 1;
}
message CreateNetworkResponse {
optional common.Status status = 1;
optional wireguard.Network network = 2;
}
message DeleteNetworkRequest {
optional uint32 id = 1;
}
message DeleteNetworkResponse {
optional common.Status status = 1;
}
message UpdateNetworkRequest {
optional wireguard.Network network = 1;
}
message UpdateNetworkResponse {
optional common.Status status = 1;
optional wireguard.Network network = 2;
}
message GetNetworkRequest {
optional uint32 id = 1;
}
message GetNetworkResponse {
optional common.Status status = 1;
optional wireguard.Network network = 2;
}
message ListNetworksRequest {
optional int32 page = 1;
optional int32 page_size = 2;
optional string keyword = 3;
}
message ListNetworksResponse {
optional common.Status status = 1;
optional int32 total = 2;
repeated wireguard.Network networks = 3;
}
message GetNetworkTopologyRequest {
optional uint32 id = 1;
}
message GetNetworkTopologyResponse {
optional common.Status status = 1;
map<uint32, wireguard.WireGuardLinks> adjs = 2;
}
message CreateEndpointRequest {
optional wireguard.Endpoint endpoint = 1;
}
message CreateEndpointResponse {
optional common.Status status = 1;
optional wireguard.Endpoint endpoint = 2;
}
message DeleteEndpointRequest {
optional uint32 id = 1;
}
message DeleteEndpointResponse {
optional common.Status status = 1;
}
message UpdateEndpointRequest {
optional wireguard.Endpoint endpoint = 1;
}
message UpdateEndpointResponse {
optional common.Status status = 1;
optional wireguard.Endpoint endpoint = 2;
}
message GetEndpointRequest {
optional uint32 id = 1;
}
message GetEndpointResponse {
optional common.Status status = 1;
optional wireguard.Endpoint endpoint = 2;
}
message ListEndpointsRequest {
optional int32 page = 1;
optional int32 page_size = 2;
optional string keyword = 3;
optional string client_id = 4;
optional uint32 wireguard_id = 5;
}
message ListEndpointsResponse {
optional common.Status status = 1;
optional int32 total = 2;
repeated wireguard.Endpoint endpoints = 3;
}
message CreateWireGuardRequest {
optional wireguard.WireGuardConfig wireguard_config = 1;
}
message CreateWireGuardResponse {
optional common.Status status = 1;
optional wireguard.WireGuardConfig wireguard_config = 2;
}
message DeleteWireGuardRequest {
optional uint32 id = 1;
optional string client_id = 2;
optional string interface_name = 3;
}
message DeleteWireGuardResponse {
optional common.Status status = 1;
}
message UpdateWireGuardRequest {
optional wireguard.WireGuardConfig wireguard_config = 1;
enum UpdateType {
UPDATE_TYPE_ADD_PEER = 0;
UPDATE_TYPE_REMOVE_PEER = 1;
UPDATE_TYPE_UPDATE_PEER = 2;
UPDATE_TYPE_PATCH_PEERS = 3;
}
optional UpdateType update_type = 2;
}
message UpdateWireGuardResponse {
optional common.Status status = 1;
optional wireguard.WireGuardConfig wireguard_config = 2;
}
message GetWireGuardRequest {
optional uint32 id = 1;
optional string client_id = 2;
optional string interface_name = 3;
}
message GetWireGuardResponse {
optional common.Status status = 1;
optional wireguard.WireGuardConfig wireguard_config = 2;
}
message GetWireGuardRuntimeInfoRequest {
optional uint32 id = 1;
optional string client_id = 2;
optional string interface_name = 3;
}
message GetWireGuardRuntimeInfoResponse {
optional common.Status status = 1;
optional wireguard.WGDeviceRuntimeInfo wg_device_runtime_info = 2;
}
message ListWireGuardsRequest {
optional int32 page = 1;
optional int32 page_size = 2;
optional string keyword = 3;
optional string client_id = 4;
optional uint32 network_id = 5;
}
message ListWireGuardsResponse {
optional common.Status status = 1;
optional int32 total = 2;
repeated wireguard.WireGuardConfig wireguard_configs = 3;
}
message CreateWireGuardLinkRequest {
optional wireguard.WireGuardLink wireguard_link = 1;
}
message CreateWireGuardLinkResponse {
optional common.Status status = 1;
optional wireguard.WireGuardLink wireguard_link = 2;
}
message DeleteWireGuardLinkRequest {
optional uint32 id = 1;
}
message DeleteWireGuardLinkResponse {
optional common.Status status = 1;
}
message UpdateWireGuardLinkRequest {
optional wireguard.WireGuardLink wireguard_link = 1;
}
message UpdateWireGuardLinkResponse {
optional common.Status status = 1;
optional wireguard.WireGuardLink wireguard_link = 2;
}
message GetWireGuardLinkRequest {
optional uint32 id = 1;
}
message GetWireGuardLinkResponse {
optional common.Status status = 1;
optional wireguard.WireGuardLink wireguard_link = 2;
}
message ListWireGuardLinksRequest {
optional int32 page = 1;
optional int32 page_size = 2;
optional string keyword = 3;
optional uint32 network_id = 4;
}
message ListWireGuardLinksResponse {
optional common.Status status = 1;
optional int32 total = 2;
repeated wireguard.WireGuardLink wireguard_links = 3;
}
+26
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package master;
import "common.proto";
import "types_wg.proto";
option go_package="../pb";
enum Event {
@@ -28,6 +29,10 @@ enum Event {
EVENT_REMOVE_WORKER = 20;
EVENT_GET_WORKER_STATUS = 21;
EVENT_INSTALL_WORKERD = 22;
EVENT_CREATE_WIREGUARD = 23;
EVENT_DELETE_WIREGUARD = 24;
EVENT_UPDATE_WIREGUARD = 25;
EVENT_GET_WIREGUARD_RUNTIME_INFO = 26;
}
message ServerBase {
@@ -135,14 +140,35 @@ message ListClientWorkersResponse {
repeated common.Worker workers = 2;
}
message ListClientWireGuardsRequest {
ClientBase base = 255;
}
message ListClientWireGuardsResponse {
common.Status status = 1;
repeated wireguard.WireGuardConfig wireguard_configs = 2;
}
message ReportWireGuardRuntimeInfoReq {
optional string interface_name = 1;
optional wireguard.WGDeviceRuntimeInfo runtime_info = 2;
ClientBase base = 255;
}
message ReportWireGuardRuntimeInfoResp {
common.Status status = 1;
}
service Master {
rpc ServerSend(stream ClientMessage) returns(stream ServerMessage);
rpc PullClientConfig(PullClientConfigReq) returns(PullClientConfigResp);
rpc PullServerConfig(PullServerConfigReq) returns(PullServerConfigResp);
rpc ListClientWorkers(ListClientWorkersRequest) returns(ListClientWorkersResponse);
rpc ListClientWireGuards(ListClientWireGuardsRequest) returns(ListClientWireGuardsResponse);
rpc FRPCAuth(FRPAuthRequest) returns(FRPAuthResponse);
rpc PushProxyInfo(PushProxyInfoReq) returns(PushProxyInfoResp);
rpc PushClientStreamLog(stream PushClientStreamLogReq) returns(PushStreamLogResp);
rpc PushServerStreamLog(stream PushServerStreamLogReq) returns(PushStreamLogResp);
rpc PTYConnect(stream PTYClientMessage) returns(stream PTYServerMessage);
rpc ReportWireGuardRuntimeInfo(ReportWireGuardRuntimeInfoReq) returns(ReportWireGuardRuntimeInfoResp);
}
+109
View File
@@ -0,0 +1,109 @@
syntax = "proto3";
package wireguard;
option go_package = "../pb";
// WireGuardPeerConfig wg peer 的配置
message WireGuardPeerConfig {
uint32 id = 1;
string client_id = 2;
uint32 user_id = 3;
uint32 tenant_id = 4;
string public_key = 5; // Peer 的 wg 公钥
string preshared_key = 6; // (可选) Peer 的 WireGuard 预共享密钥
repeated string allowed_ips = 7; // 通过此 Peer 路由的 IP 地址/子网
Endpoint endpoint = 8; // (可选) Peer 的公网端点 "host:port"
uint32 persistent_keepalive = 9; // 可选
repeated string tags = 10; // 标签
}
// WireGuardConfig wg 配置
message WireGuardConfig {
uint32 id = 1;
string client_id = 2;
uint32 user_id = 3;
uint32 tenant_id = 4;
string interface_name = 5; // WireGuard 网络接口的名称
string private_key = 6; // 接口的私钥
string local_address = 7; // 虚拟接口的 CIDR
uint32 listen_port = 8; // (可选) WireGuard 监听端口,如果没有配置,则使用默认端口
uint32 interface_mtu = 9; // 可选
repeated WireGuardPeerConfig peers = 10; // Peer 列表
repeated Endpoint advertised_endpoints = 11; // (可选) 外部可连接的地址
repeated string dns_servers = 12; // (可选) DNS 服务器列表
uint32 network_id = 13; // 归属的网络 ID
repeated string tags = 14; // 标签
}
message Endpoint {
uint32 id = 1;
string host = 2;
uint32 port = 3;
string client_id = 4;
uint32 wireguard_id = 5; // 分配的 WireGuard ID
}
message WireGuardLink {
uint32 id = 1;
uint32 from_wireguard_id = 2;
uint32 to_wireguard_id = 3;
uint32 up_bandwidth_mbps = 4;
uint32 down_bandwidth_mbps = 5;
uint32 latency_ms = 6;
bool active = 7;
}
message WireGuardLinks {
repeated WireGuardLink links = 1;
}
message Network {
uint32 id = 1;
uint32 user_id = 2;
uint32 tenant_id = 3;
string name = 4;
string cidr = 5;
AclConfig acl = 6;
}
message AclConfig {
repeated AclRuleConfig acls = 1;
}
message AclRuleConfig {
string action = 1;
repeated string src = 2;
repeated string dst = 3;
}
message WGPeerRuntimeInfo {
string public_key = 1;
string preshared_key = 2;
repeated string allowed_ips = 3;
string endpoint_host = 4;
uint32 endpoint_port = 5;
uint64 tx_bytes = 6;
uint64 rx_bytes = 7;
uint32 persistent_keepalive_interval = 8;
uint64 last_handshake_time_nsec = 9;
uint64 last_handshake_time_sec = 10;
string client_id = 11;
map<string, string> extra = 100;
}
message WGDeviceRuntimeInfo {
string private_key = 1;
uint32 listen_port = 2;
repeated WGPeerRuntimeInfo peers = 3;
uint32 protocol_version = 4;
int32 errno = 5;
string client_id = 6;
map<uint32, uint32> ping_map = 7; // to peer endpoint ping
string interface_name = 8;
map<string, string> extra = 100;
}
+2
View File
@@ -14,6 +14,8 @@ import (
type Client struct {
*ClientEntity
Workers []*Worker `json:"workers,omitempty" gorm:"many2many:worker_clients;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
WireGuard []*WireGuard `json:"wireguard,omitempty" gorm:"foreignKey:ClientID;references:ClientID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type ClientEntity struct {
+12
View File
@@ -44,6 +44,18 @@ func (dbm *dbManagerImpl) Init() {
if err := db.AutoMigrate(&UserGroup{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&UserGroup{}).TableName())
}
if err := db.AutoMigrate(&WireGuard{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&WireGuard{}).TableName())
}
if err := db.AutoMigrate(&Network{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Network{}).TableName())
}
if err := db.AutoMigrate(&Endpoint{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Endpoint{}).TableName())
}
if err := db.AutoMigrate(&WireGuardLink{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&WireGuardLink{}).TableName())
}
}
}
}
+68
View File
@@ -0,0 +1,68 @@
package models
import (
"github.com/VaalaCat/frp-panel/pb"
"gorm.io/gorm"
)
// WireGuardLink 描述同一 Network 下两个 WireGuard 节点之间的有向链路与其指标。
// 语义:从 FromWireGuardID 指向 ToWireGuardID 的传输路径,
// UpBandwidthMbps 表示从 From -> To 的可用上行带宽;LatencyMs 为单向时延。
// 如需双向链路,请创建两条对向记录。
type WireGuardLink struct {
gorm.Model
*WireGuardLinkEntity
FromWireGuard *WireGuard `json:"from_wireguard,omitempty" gorm:"foreignKey:FromWireGuardID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
ToWireGuard *WireGuard `json:"to_wireguard,omitempty" gorm:"foreignKey:ToWireGuardID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
type WireGuardLinkEntity struct {
// 多租户
UserId uint32 `gorm:"index"`
TenantId uint32 `gorm:"index"`
// 归属网络
NetworkID uint `gorm:"index"`
// 有向边两端
FromWireGuardID uint `gorm:"index"`
ToWireGuardID uint `gorm:"index"`
// 链路指标
UpBandwidthMbps uint32
DownBandwidthMbps uint32
LatencyMs uint32
// 状态
Active bool `gorm:"index"`
}
func (*WireGuardLink) TableName() string {
return "wireguard_links"
}
func (w *WireGuardLink) FromPB(pbData *pb.WireGuardLink) {
w.Model = gorm.Model{}
w.WireGuardLinkEntity = &WireGuardLinkEntity{}
w.Model.ID = uint(pbData.GetId())
w.FromWireGuardID = uint(pbData.GetFromWireguardId())
w.ToWireGuardID = uint(pbData.GetToWireguardId())
w.UpBandwidthMbps = pbData.GetUpBandwidthMbps()
w.DownBandwidthMbps = pbData.GetDownBandwidthMbps()
w.LatencyMs = pbData.GetLatencyMs()
w.Active = pbData.GetActive()
}
func (w *WireGuardLink) ToPB() *pb.WireGuardLink {
return &pb.WireGuardLink{
Id: uint32(w.ID),
FromWireguardId: uint32(w.FromWireGuardID),
ToWireguardId: uint32(w.ToWireGuardID),
UpBandwidthMbps: w.UpBandwidthMbps,
DownBandwidthMbps: w.DownBandwidthMbps,
LatencyMs: w.LatencyMs,
Active: w.Active,
}
}
+236
View File
@@ -0,0 +1,236 @@
package models
import (
"errors"
"net/netip"
"github.com/VaalaCat/frp-panel/pb"
"github.com/samber/lo"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm"
)
type WireGuard struct {
gorm.Model
*WireGuardEntity
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;references:ClientID"`
Network *Network `json:"network,omitempty" gorm:"foreignKey:NetworkID;references:ID"`
AdvertisedEndpoints []*Endpoint `json:"advertised_endpoints,omitempty" gorm:"foreignKey:WireGuardID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
WireGuardLinks []*WireGuardLink `json:"wireguard_links,omitempty" gorm:"foreignKey:FromWireGuardID;references:ID"`
}
type WireGuardEntity struct {
Name string `gorm:"type:varchar(255);uniqueIndex:idx_client_id_name"`
UserId uint32 `gorm:"index"`
TenantId uint32 `gorm:"index"`
PrivateKey string `json:"private_key" gorm:"type:varchar(255)"`
LocalAddress string `json:"local_address" gorm:"type:varchar(255)"`
ListenPort uint32 `json:"listen_port" gorm:"uniqueIndex:idx_client_id_listen_port"`
InterfaceMtu uint32 `json:"interface_mtu"`
DnsServers GormArray[string] `json:"dns_servers" gorm:"type:varchar(255)"`
ClientID string `gorm:"type:varchar(64);uniqueIndex:idx_client_id_name;uniqueIndex:idx_client_id_listen_port"`
NetworkID uint `gorm:"index"`
Tags GormArray[string] `json:"tags" gorm:"type:varchar(255)"`
}
func (*WireGuard) TableName() string {
return "wireguards"
}
func (w *WireGuard) GetTags() []string {
return w.Tags
}
func (w *WireGuard) GetID() uint {
return uint(w.ID)
}
func ParseIPOrCIDRWithNetip(s string) (netip.Addr, netip.Prefix, error) {
if prefix, err := netip.ParsePrefix(s); err == nil {
return prefix.Addr(), prefix, nil
}
if addr, err := netip.ParseAddr(s); err == nil {
return addr, netip.Prefix{}, nil
}
return netip.Addr{}, netip.Prefix{}, errors.New("invalid ip or cidr")
}
func (w *WireGuard) AsBasePeerConfig() (*pb.WireGuardPeerConfig, error) {
privKey, err := wgtypes.ParseKey(w.PrivateKey)
if err != nil {
return nil, errors.Join(errors.New("parse private key error"), err)
}
_, localIPPrefix, err := ParseIPOrCIDRWithNetip(w.LocalAddress)
if err != nil {
return nil, errors.Join(errors.New("parse local address error"), err)
}
localIPPrefixAllowed := netip.PrefixFrom(localIPPrefix.Addr(), 32)
resp := &pb.WireGuardPeerConfig{
Id: uint32(w.ID),
ClientId: w.ClientID,
UserId: w.UserId,
TenantId: w.TenantId,
PublicKey: privKey.PublicKey().String(),
AllowedIps: []string{localIPPrefixAllowed.String()},
PersistentKeepalive: 20,
Tags: w.Tags,
}
if len(w.AdvertisedEndpoints) > 0 {
resp.Endpoint = &pb.Endpoint{
Host: w.AdvertisedEndpoints[0].Host,
Port: w.AdvertisedEndpoints[0].Port,
}
}
return resp, nil
}
func (w *WireGuard) FromPB(pb *pb.WireGuardConfig) {
w.Model = gorm.Model{}
w.WireGuardEntity = &WireGuardEntity{}
w.Model.ID = uint(pb.GetId())
w.Name = pb.GetInterfaceName()
w.UserId = pb.GetUserId()
w.TenantId = pb.GetTenantId()
w.PrivateKey = pb.GetPrivateKey()
w.LocalAddress = pb.GetLocalAddress()
w.ListenPort = pb.GetListenPort()
w.InterfaceMtu = pb.GetInterfaceMtu()
w.DnsServers = GormArray[string](pb.GetDnsServers())
w.ClientID = pb.GetClientId()
w.NetworkID = uint(pb.GetNetworkId())
w.Tags = GormArray[string](pb.GetTags())
w.AdvertisedEndpoints = make([]*Endpoint, 0, len(pb.GetAdvertisedEndpoints()))
for _, e := range pb.GetAdvertisedEndpoints() {
w.AdvertisedEndpoints = append(w.AdvertisedEndpoints, &Endpoint{
Model: gorm.Model{ID: uint(e.GetId())},
EndpointEntity: &EndpointEntity{
Host: e.GetHost(),
Port: e.GetPort(),
ClientID: e.GetClientId(),
WireGuardID: uint(e.GetWireguardId()),
},
})
}
}
func (w *WireGuard) ToPB() *pb.WireGuardConfig {
return &pb.WireGuardConfig{
Id: uint32(w.ID),
ClientId: w.ClientID,
UserId: uint32(w.UserId),
TenantId: uint32(w.TenantId),
InterfaceName: w.Name,
PrivateKey: w.PrivateKey,
LocalAddress: w.LocalAddress,
ListenPort: w.ListenPort,
InterfaceMtu: w.InterfaceMtu,
DnsServers: w.DnsServers,
NetworkId: uint32(w.NetworkID),
Tags: w.Tags,
AdvertisedEndpoints: lo.Map(w.AdvertisedEndpoints, func(e *Endpoint, _ int) *pb.Endpoint {
return &pb.Endpoint{
Id: uint32(e.ID),
Host: e.Host,
Port: e.Port,
ClientId: e.ClientID,
WireguardId: uint32(e.WireGuardID),
}
}),
}
}
type Network struct {
gorm.Model
*NetworkEntity
WireGuard []*WireGuard `json:"wireguard,omitempty" gorm:"foreignKey:NetworkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
func (n *Network) FromPB(pbData *pb.Network) {
n.Model = gorm.Model{}
n.NetworkEntity = &NetworkEntity{}
n.Model.ID = uint(pbData.GetId())
n.Name = pbData.GetName()
n.UserId = pbData.GetUserId()
n.TenantId = pbData.GetTenantId()
n.CIDR = pbData.GetCidr()
n.ACL = JSON[*pb.AclConfig]{Data: pbData.GetAcl()}
}
func (n *Network) ToPB() *pb.Network {
return &pb.Network{
Id: uint32(n.ID),
UserId: n.UserId,
TenantId: n.TenantId,
Name: n.Name,
Cidr: n.CIDR,
Acl: n.ACL.Data,
}
}
func (*Network) TableName() string {
return "networks"
}
type NetworkEntity struct {
Name string `gorm:"type:varchar(255);index"`
UserId uint32 `gorm:"index"`
TenantId uint32 `gorm:"index"`
CIDR string `gorm:"type:varchar(255);index"`
ACL JSON[*pb.AclConfig] `gorm:"type:text;index"`
}
type Endpoint struct {
gorm.Model
*EndpointEntity
WireGuard *WireGuard `json:"wireguard,omitempty" gorm:"foreignKey:WireGuardID;references:ID"`
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;references:ClientID"`
}
type EndpointEntity struct {
Host string `gorm:"uniqueIndex:idx_client_id_host_port"`
Port uint32 `gorm:"uniqueIndex:idx_client_id_host_port"`
WireGuardID uint `gorm:"index"`
ClientID string `gorm:"type:varchar(255);uniqueIndex:idx_client_id_host_port"`
}
func (*Endpoint) TableName() string {
return "endpoints"
}
func (e *Endpoint) ToPB() *pb.Endpoint {
return &pb.Endpoint{
Id: uint32(e.ID),
Host: e.Host,
Port: e.Port,
ClientId: e.ClientID,
WireguardId: uint32(e.WireGuardID),
}
}
func (e *Endpoint) FromPB(pbData *pb.Endpoint) {
e.Model = gorm.Model{}
e.EndpointEntity = &EndpointEntity{}
e.Model.ID = uint(pbData.GetId())
e.Host = pbData.GetHost()
e.Port = pbData.GetPort()
e.ClientID = pbData.GetClientId()
e.WireGuardID = uint(pbData.GetWireguardId())
}
+15
View File
@@ -0,0 +1,15 @@
package models_test
import (
"net/netip"
"testing"
"github.com/VaalaCat/frp-panel/models"
)
func TestParseIPOrCIDRWithNetip(t *testing.T) {
ip, cidr, _ := models.ParseIPOrCIDRWithNetip("192.168.1.1/24")
t.Errorf("ip: %v, cidr: %v", ip, cidr)
newcidr := netip.PrefixFrom(ip, 32)
t.Errorf("newcidr: %v", newcidr)
}
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: api_auth.proto
package pb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: api_client.proto
package pb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: api_master.proto
package pb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: api_server.proto
package pb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: api_user.proto
package pb
+2791
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: common.proto
package pb
+362 -113
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.30.2
// protoc-gen-go v1.36.8
// protoc v3.21.11
// source: rpc_master.proto
package pb
@@ -24,29 +24,33 @@ const (
type Event int32
const (
Event_EVENT_UNSPECIFIED Event = 0
Event_EVENT_REGISTER_CLIENT Event = 1
Event_EVENT_REGISTER_SERVER Event = 2
Event_EVENT_ERROR Event = 3
Event_EVENT_DATA Event = 4
Event_EVENT_UPDATE_FRPC Event = 5
Event_EVENT_REMOVE_FRPC Event = 6
Event_EVENT_UPDATE_FRPS Event = 7
Event_EVENT_REMOVE_FRPS Event = 8
Event_EVENT_PING Event = 9
Event_EVENT_PONG Event = 10
Event_EVENT_STOP_FRPC Event = 11
Event_EVENT_START_FRPC Event = 12
Event_EVENT_STOP_FRPS Event = 13
Event_EVENT_START_FRPS Event = 14
Event_EVENT_START_STREAM_LOG Event = 15
Event_EVENT_STOP_STREAM_LOG Event = 16
Event_EVENT_START_PTY_CONNECT Event = 17
Event_EVENT_GET_PROXY_INFO Event = 18
Event_EVENT_CREATE_WORKER Event = 19
Event_EVENT_REMOVE_WORKER Event = 20
Event_EVENT_GET_WORKER_STATUS Event = 21
Event_EVENT_INSTALL_WORKERD Event = 22
Event_EVENT_UNSPECIFIED Event = 0
Event_EVENT_REGISTER_CLIENT Event = 1
Event_EVENT_REGISTER_SERVER Event = 2
Event_EVENT_ERROR Event = 3
Event_EVENT_DATA Event = 4
Event_EVENT_UPDATE_FRPC Event = 5
Event_EVENT_REMOVE_FRPC Event = 6
Event_EVENT_UPDATE_FRPS Event = 7
Event_EVENT_REMOVE_FRPS Event = 8
Event_EVENT_PING Event = 9
Event_EVENT_PONG Event = 10
Event_EVENT_STOP_FRPC Event = 11
Event_EVENT_START_FRPC Event = 12
Event_EVENT_STOP_FRPS Event = 13
Event_EVENT_START_FRPS Event = 14
Event_EVENT_START_STREAM_LOG Event = 15
Event_EVENT_STOP_STREAM_LOG Event = 16
Event_EVENT_START_PTY_CONNECT Event = 17
Event_EVENT_GET_PROXY_INFO Event = 18
Event_EVENT_CREATE_WORKER Event = 19
Event_EVENT_REMOVE_WORKER Event = 20
Event_EVENT_GET_WORKER_STATUS Event = 21
Event_EVENT_INSTALL_WORKERD Event = 22
Event_EVENT_CREATE_WIREGUARD Event = 23
Event_EVENT_DELETE_WIREGUARD Event = 24
Event_EVENT_UPDATE_WIREGUARD Event = 25
Event_EVENT_GET_WIREGUARD_RUNTIME_INFO Event = 26
)
// Enum value maps for Event.
@@ -75,31 +79,39 @@ var (
20: "EVENT_REMOVE_WORKER",
21: "EVENT_GET_WORKER_STATUS",
22: "EVENT_INSTALL_WORKERD",
23: "EVENT_CREATE_WIREGUARD",
24: "EVENT_DELETE_WIREGUARD",
25: "EVENT_UPDATE_WIREGUARD",
26: "EVENT_GET_WIREGUARD_RUNTIME_INFO",
}
Event_value = map[string]int32{
"EVENT_UNSPECIFIED": 0,
"EVENT_REGISTER_CLIENT": 1,
"EVENT_REGISTER_SERVER": 2,
"EVENT_ERROR": 3,
"EVENT_DATA": 4,
"EVENT_UPDATE_FRPC": 5,
"EVENT_REMOVE_FRPC": 6,
"EVENT_UPDATE_FRPS": 7,
"EVENT_REMOVE_FRPS": 8,
"EVENT_PING": 9,
"EVENT_PONG": 10,
"EVENT_STOP_FRPC": 11,
"EVENT_START_FRPC": 12,
"EVENT_STOP_FRPS": 13,
"EVENT_START_FRPS": 14,
"EVENT_START_STREAM_LOG": 15,
"EVENT_STOP_STREAM_LOG": 16,
"EVENT_START_PTY_CONNECT": 17,
"EVENT_GET_PROXY_INFO": 18,
"EVENT_CREATE_WORKER": 19,
"EVENT_REMOVE_WORKER": 20,
"EVENT_GET_WORKER_STATUS": 21,
"EVENT_INSTALL_WORKERD": 22,
"EVENT_UNSPECIFIED": 0,
"EVENT_REGISTER_CLIENT": 1,
"EVENT_REGISTER_SERVER": 2,
"EVENT_ERROR": 3,
"EVENT_DATA": 4,
"EVENT_UPDATE_FRPC": 5,
"EVENT_REMOVE_FRPC": 6,
"EVENT_UPDATE_FRPS": 7,
"EVENT_REMOVE_FRPS": 8,
"EVENT_PING": 9,
"EVENT_PONG": 10,
"EVENT_STOP_FRPC": 11,
"EVENT_START_FRPC": 12,
"EVENT_STOP_FRPS": 13,
"EVENT_START_FRPS": 14,
"EVENT_START_STREAM_LOG": 15,
"EVENT_STOP_STREAM_LOG": 16,
"EVENT_START_PTY_CONNECT": 17,
"EVENT_GET_PROXY_INFO": 18,
"EVENT_CREATE_WORKER": 19,
"EVENT_REMOVE_WORKER": 20,
"EVENT_GET_WORKER_STATUS": 21,
"EVENT_INSTALL_WORKERD": 22,
"EVENT_CREATE_WIREGUARD": 23,
"EVENT_DELETE_WIREGUARD": 24,
"EVENT_UPDATE_WIREGUARD": 25,
"EVENT_GET_WIREGUARD_RUNTIME_INFO": 26,
}
)
@@ -1204,11 +1216,211 @@ func (x *ListClientWorkersResponse) GetWorkers() []*Worker {
return nil
}
type ListClientWireGuardsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *ClientBase `protobuf:"bytes,255,opt,name=base,proto3" json:"base,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListClientWireGuardsRequest) Reset() {
*x = ListClientWireGuardsRequest{}
mi := &file_rpc_master_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListClientWireGuardsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListClientWireGuardsRequest) ProtoMessage() {}
func (x *ListClientWireGuardsRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListClientWireGuardsRequest.ProtoReflect.Descriptor instead.
func (*ListClientWireGuardsRequest) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{19}
}
func (x *ListClientWireGuardsRequest) GetBase() *ClientBase {
if x != nil {
return x.Base
}
return nil
}
type ListClientWireGuardsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
WireguardConfigs []*WireGuardConfig `protobuf:"bytes,2,rep,name=wireguard_configs,json=wireguardConfigs,proto3" json:"wireguard_configs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListClientWireGuardsResponse) Reset() {
*x = ListClientWireGuardsResponse{}
mi := &file_rpc_master_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListClientWireGuardsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListClientWireGuardsResponse) ProtoMessage() {}
func (x *ListClientWireGuardsResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListClientWireGuardsResponse.ProtoReflect.Descriptor instead.
func (*ListClientWireGuardsResponse) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{20}
}
func (x *ListClientWireGuardsResponse) GetStatus() *Status {
if x != nil {
return x.Status
}
return nil
}
func (x *ListClientWireGuardsResponse) GetWireguardConfigs() []*WireGuardConfig {
if x != nil {
return x.WireguardConfigs
}
return nil
}
type ReportWireGuardRuntimeInfoReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
InterfaceName *string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3,oneof" json:"interface_name,omitempty"`
RuntimeInfo *WGDeviceRuntimeInfo `protobuf:"bytes,2,opt,name=runtime_info,json=runtimeInfo,proto3,oneof" json:"runtime_info,omitempty"`
Base *ClientBase `protobuf:"bytes,255,opt,name=base,proto3" json:"base,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReportWireGuardRuntimeInfoReq) Reset() {
*x = ReportWireGuardRuntimeInfoReq{}
mi := &file_rpc_master_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReportWireGuardRuntimeInfoReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReportWireGuardRuntimeInfoReq) ProtoMessage() {}
func (x *ReportWireGuardRuntimeInfoReq) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReportWireGuardRuntimeInfoReq.ProtoReflect.Descriptor instead.
func (*ReportWireGuardRuntimeInfoReq) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{21}
}
func (x *ReportWireGuardRuntimeInfoReq) GetInterfaceName() string {
if x != nil && x.InterfaceName != nil {
return *x.InterfaceName
}
return ""
}
func (x *ReportWireGuardRuntimeInfoReq) GetRuntimeInfo() *WGDeviceRuntimeInfo {
if x != nil {
return x.RuntimeInfo
}
return nil
}
func (x *ReportWireGuardRuntimeInfoReq) GetBase() *ClientBase {
if x != nil {
return x.Base
}
return nil
}
type ReportWireGuardRuntimeInfoResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReportWireGuardRuntimeInfoResp) Reset() {
*x = ReportWireGuardRuntimeInfoResp{}
mi := &file_rpc_master_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReportWireGuardRuntimeInfoResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReportWireGuardRuntimeInfoResp) ProtoMessage() {}
func (x *ReportWireGuardRuntimeInfoResp) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReportWireGuardRuntimeInfoResp.ProtoReflect.Descriptor instead.
func (*ReportWireGuardRuntimeInfoResp) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{22}
}
func (x *ReportWireGuardRuntimeInfoResp) GetStatus() *Status {
if x != nil {
return x.Status
}
return nil
}
var File_rpc_master_proto protoreflect.FileDescriptor
const file_rpc_master_proto_rawDesc = "" +
"\n" +
"\x10rpc_master.proto\x12\x06master\x1a\fcommon.proto\"N\n" +
"\x10rpc_master.proto\x12\x06master\x1a\fcommon.proto\x1a\x0etypes_wg.proto\"N\n" +
"\n" +
"ServerBase\x12\x1b\n" +
"\tserver_id\x18\x01 \x01(\tR\bserverId\x12#\n" +
@@ -1285,7 +1497,20 @@ const file_rpc_master_proto_rawDesc = "" +
"\x04base\x18\xff\x01 \x01(\v2\x12.master.ClientBaseR\x04base\"m\n" +
"\x19ListClientWorkersResponse\x12&\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusR\x06status\x12(\n" +
"\aworkers\x18\x02 \x03(\v2\x0e.common.WorkerR\aworkers*\x9f\x04\n" +
"\aworkers\x18\x02 \x03(\v2\x0e.common.WorkerR\aworkers\"F\n" +
"\x1bListClientWireGuardsRequest\x12'\n" +
"\x04base\x18\xff\x01 \x01(\v2\x12.master.ClientBaseR\x04base\"\x8f\x01\n" +
"\x1cListClientWireGuardsResponse\x12&\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusR\x06status\x12G\n" +
"\x11wireguard_configs\x18\x02 \x03(\v2\x1a.wireguard.WireGuardConfigR\x10wireguardConfigs\"\xe0\x01\n" +
"\x1dReportWireGuardRuntimeInfoReq\x12*\n" +
"\x0einterface_name\x18\x01 \x01(\tH\x00R\rinterfaceName\x88\x01\x01\x12F\n" +
"\fruntime_info\x18\x02 \x01(\v2\x1e.wireguard.WGDeviceRuntimeInfoH\x01R\vruntimeInfo\x88\x01\x01\x12'\n" +
"\x04base\x18\xff\x01 \x01(\v2\x12.master.ClientBaseR\x04baseB\x11\n" +
"\x0f_interface_nameB\x0f\n" +
"\r_runtime_info\"H\n" +
"\x1eReportWireGuardRuntimeInfoResp\x12&\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusR\x06status*\x99\x05\n" +
"\x05Event\x12\x15\n" +
"\x11EVENT_UNSPECIFIED\x10\x00\x12\x19\n" +
"\x15EVENT_REGISTER_CLIENT\x10\x01\x12\x19\n" +
@@ -1313,19 +1538,25 @@ const file_rpc_master_proto_rawDesc = "" +
"\x13EVENT_CREATE_WORKER\x10\x13\x12\x17\n" +
"\x13EVENT_REMOVE_WORKER\x10\x14\x12\x1b\n" +
"\x17EVENT_GET_WORKER_STATUS\x10\x15\x12\x19\n" +
"\x15EVENT_INSTALL_WORKERD\x10\x162\xb1\x05\n" +
"\x15EVENT_INSTALL_WORKERD\x10\x16\x12\x1a\n" +
"\x16EVENT_CREATE_WIREGUARD\x10\x17\x12\x1a\n" +
"\x16EVENT_DELETE_WIREGUARD\x10\x18\x12\x1a\n" +
"\x16EVENT_UPDATE_WIREGUARD\x10\x19\x12$\n" +
" EVENT_GET_WIREGUARD_RUNTIME_INFO\x10\x1a2\x81\a\n" +
"\x06Master\x12>\n" +
"\n" +
"ServerSend\x12\x15.master.ClientMessage\x1a\x15.master.ServerMessage(\x010\x01\x12M\n" +
"\x10PullClientConfig\x12\x1b.master.PullClientConfigReq\x1a\x1c.master.PullClientConfigResp\x12M\n" +
"\x10PullServerConfig\x12\x1b.master.PullServerConfigReq\x1a\x1c.master.PullServerConfigResp\x12X\n" +
"\x11ListClientWorkers\x12 .master.ListClientWorkersRequest\x1a!.master.ListClientWorkersResponse\x12;\n" +
"\x11ListClientWorkers\x12 .master.ListClientWorkersRequest\x1a!.master.ListClientWorkersResponse\x12a\n" +
"\x14ListClientWireGuards\x12#.master.ListClientWireGuardsRequest\x1a$.master.ListClientWireGuardsResponse\x12;\n" +
"\bFRPCAuth\x12\x16.master.FRPAuthRequest\x1a\x17.master.FRPAuthResponse\x12D\n" +
"\rPushProxyInfo\x12\x18.master.PushProxyInfoReq\x1a\x19.master.PushProxyInfoResp\x12R\n" +
"\x13PushClientStreamLog\x12\x1e.master.PushClientStreamLogReq\x1a\x19.master.PushStreamLogResp(\x01\x12R\n" +
"\x13PushServerStreamLog\x12\x1e.master.PushServerStreamLogReq\x1a\x19.master.PushStreamLogResp(\x01\x12D\n" +
"\n" +
"PTYConnect\x12\x18.master.PTYClientMessage\x1a\x18.master.PTYServerMessage(\x010\x01B\aZ\x05../pbb\x06proto3"
"PTYConnect\x12\x18.master.PTYClientMessage\x1a\x18.master.PTYServerMessage(\x010\x01\x12k\n" +
"\x1aReportWireGuardRuntimeInfo\x12%.master.ReportWireGuardRuntimeInfoReq\x1a&.master.ReportWireGuardRuntimeInfoRespB\aZ\x05../pbb\x06proto3"
var (
file_rpc_master_proto_rawDescOnce sync.Once
@@ -1340,79 +1571,95 @@ func file_rpc_master_proto_rawDescGZIP() []byte {
}
var file_rpc_master_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_rpc_master_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_rpc_master_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_rpc_master_proto_goTypes = []any{
(Event)(0), // 0: master.Event
(*ServerBase)(nil), // 1: master.ServerBase
(*ClientBase)(nil), // 2: master.ClientBase
(*ServerMessage)(nil), // 3: master.ServerMessage
(*ClientMessage)(nil), // 4: master.ClientMessage
(*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq
(*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp
(*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq
(*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp
(*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest
(*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse
(*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq
(*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp
(*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq
(*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq
(*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp
(*PTYClientMessage)(nil), // 16: master.PTYClientMessage
(*PTYServerMessage)(nil), // 17: master.PTYServerMessage
(*ListClientWorkersRequest)(nil), // 18: master.ListClientWorkersRequest
(*ListClientWorkersResponse)(nil), // 19: master.ListClientWorkersResponse
(*Status)(nil), // 20: common.Status
(*Client)(nil), // 21: common.Client
(*Server)(nil), // 22: common.Server
(*ProxyInfo)(nil), // 23: common.ProxyInfo
(*Worker)(nil), // 24: common.Worker
(Event)(0), // 0: master.Event
(*ServerBase)(nil), // 1: master.ServerBase
(*ClientBase)(nil), // 2: master.ClientBase
(*ServerMessage)(nil), // 3: master.ServerMessage
(*ClientMessage)(nil), // 4: master.ClientMessage
(*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq
(*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp
(*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq
(*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp
(*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest
(*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse
(*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq
(*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp
(*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq
(*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq
(*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp
(*PTYClientMessage)(nil), // 16: master.PTYClientMessage
(*PTYServerMessage)(nil), // 17: master.PTYServerMessage
(*ListClientWorkersRequest)(nil), // 18: master.ListClientWorkersRequest
(*ListClientWorkersResponse)(nil), // 19: master.ListClientWorkersResponse
(*ListClientWireGuardsRequest)(nil), // 20: master.ListClientWireGuardsRequest
(*ListClientWireGuardsResponse)(nil), // 21: master.ListClientWireGuardsResponse
(*ReportWireGuardRuntimeInfoReq)(nil), // 22: master.ReportWireGuardRuntimeInfoReq
(*ReportWireGuardRuntimeInfoResp)(nil), // 23: master.ReportWireGuardRuntimeInfoResp
(*Status)(nil), // 24: common.Status
(*Client)(nil), // 25: common.Client
(*Server)(nil), // 26: common.Server
(*ProxyInfo)(nil), // 27: common.ProxyInfo
(*Worker)(nil), // 28: common.Worker
(*WireGuardConfig)(nil), // 29: wireguard.WireGuardConfig
(*WGDeviceRuntimeInfo)(nil), // 30: wireguard.WGDeviceRuntimeInfo
}
var file_rpc_master_proto_depIdxs = []int32{
0, // 0: master.ServerMessage.event:type_name -> master.Event
0, // 1: master.ClientMessage.event:type_name -> master.Event
2, // 2: master.PullClientConfigReq.base:type_name -> master.ClientBase
20, // 3: master.PullClientConfigResp.status:type_name -> common.Status
21, // 4: master.PullClientConfigResp.client:type_name -> common.Client
24, // 3: master.PullClientConfigResp.status:type_name -> common.Status
25, // 4: master.PullClientConfigResp.client:type_name -> common.Client
1, // 5: master.PullServerConfigReq.base:type_name -> master.ServerBase
20, // 6: master.PullServerConfigResp.status:type_name -> common.Status
22, // 7: master.PullServerConfigResp.server:type_name -> common.Server
24, // 6: master.PullServerConfigResp.status:type_name -> common.Status
26, // 7: master.PullServerConfigResp.server:type_name -> common.Server
1, // 8: master.FRPAuthRequest.base:type_name -> master.ServerBase
20, // 9: master.FRPAuthResponse.status:type_name -> common.Status
24, // 9: master.FRPAuthResponse.status:type_name -> common.Status
1, // 10: master.PushProxyInfoReq.base:type_name -> master.ServerBase
23, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo
20, // 12: master.PushProxyInfoResp.status:type_name -> common.Status
27, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo
24, // 12: master.PushProxyInfoResp.status:type_name -> common.Status
1, // 13: master.PushServerStreamLogReq.base:type_name -> master.ServerBase
2, // 14: master.PushClientStreamLogReq.base:type_name -> master.ClientBase
20, // 15: master.PushStreamLogResp.status:type_name -> common.Status
24, // 15: master.PushStreamLogResp.status:type_name -> common.Status
1, // 16: master.PTYClientMessage.server_base:type_name -> master.ServerBase
2, // 17: master.PTYClientMessage.client_base:type_name -> master.ClientBase
2, // 18: master.ListClientWorkersRequest.base:type_name -> master.ClientBase
20, // 19: master.ListClientWorkersResponse.status:type_name -> common.Status
24, // 20: master.ListClientWorkersResponse.workers:type_name -> common.Worker
4, // 21: master.Master.ServerSend:input_type -> master.ClientMessage
5, // 22: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq
7, // 23: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq
18, // 24: master.Master.ListClientWorkers:input_type -> master.ListClientWorkersRequest
9, // 25: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest
11, // 26: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq
14, // 27: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq
13, // 28: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq
16, // 29: master.Master.PTYConnect:input_type -> master.PTYClientMessage
3, // 30: master.Master.ServerSend:output_type -> master.ServerMessage
6, // 31: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp
8, // 32: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp
19, // 33: master.Master.ListClientWorkers:output_type -> master.ListClientWorkersResponse
10, // 34: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse
12, // 35: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp
15, // 36: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp
15, // 37: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp
17, // 38: master.Master.PTYConnect:output_type -> master.PTYServerMessage
30, // [30:39] is the sub-list for method output_type
21, // [21:30] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
24, // 19: master.ListClientWorkersResponse.status:type_name -> common.Status
28, // 20: master.ListClientWorkersResponse.workers:type_name -> common.Worker
2, // 21: master.ListClientWireGuardsRequest.base:type_name -> master.ClientBase
24, // 22: master.ListClientWireGuardsResponse.status:type_name -> common.Status
29, // 23: master.ListClientWireGuardsResponse.wireguard_configs:type_name -> wireguard.WireGuardConfig
30, // 24: master.ReportWireGuardRuntimeInfoReq.runtime_info:type_name -> wireguard.WGDeviceRuntimeInfo
2, // 25: master.ReportWireGuardRuntimeInfoReq.base:type_name -> master.ClientBase
24, // 26: master.ReportWireGuardRuntimeInfoResp.status:type_name -> common.Status
4, // 27: master.Master.ServerSend:input_type -> master.ClientMessage
5, // 28: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq
7, // 29: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq
18, // 30: master.Master.ListClientWorkers:input_type -> master.ListClientWorkersRequest
20, // 31: master.Master.ListClientWireGuards:input_type -> master.ListClientWireGuardsRequest
9, // 32: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest
11, // 33: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq
14, // 34: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq
13, // 35: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq
16, // 36: master.Master.PTYConnect:input_type -> master.PTYClientMessage
22, // 37: master.Master.ReportWireGuardRuntimeInfo:input_type -> master.ReportWireGuardRuntimeInfoReq
3, // 38: master.Master.ServerSend:output_type -> master.ServerMessage
6, // 39: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp
8, // 40: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp
19, // 41: master.Master.ListClientWorkers:output_type -> master.ListClientWorkersResponse
21, // 42: master.Master.ListClientWireGuards:output_type -> master.ListClientWireGuardsResponse
10, // 43: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse
12, // 44: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp
15, // 45: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp
15, // 46: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp
17, // 47: master.Master.PTYConnect:output_type -> master.PTYServerMessage
23, // 48: master.Master.ReportWireGuardRuntimeInfo:output_type -> master.ReportWireGuardRuntimeInfoResp
38, // [38:49] is the sub-list for method output_type
27, // [27:38] is the sub-list for method input_type
27, // [27:27] is the sub-list for extension type_name
27, // [27:27] is the sub-list for extension extendee
0, // [0:27] is the sub-list for field type_name
}
func init() { file_rpc_master_proto_init() }
@@ -1421,18 +1668,20 @@ func file_rpc_master_proto_init() {
return
}
file_common_proto_init()
file_types_wg_proto_init()
file_rpc_master_proto_msgTypes[15].OneofWrappers = []any{
(*PTYClientMessage_ServerBase)(nil),
(*PTYClientMessage_ClientBase)(nil),
}
file_rpc_master_proto_msgTypes[16].OneofWrappers = []any{}
file_rpc_master_proto_msgTypes[21].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_master_proto_rawDesc), len(file_rpc_master_proto_rawDesc)),
NumEnums: 1,
NumMessages: 19,
NumMessages: 23,
NumExtensions: 0,
NumServices: 1,
},
+86 -10
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.30.2
// - protoc v3.21.11
// source: rpc_master.proto
package pb
@@ -19,15 +19,17 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Master_ServerSend_FullMethodName = "/master.Master/ServerSend"
Master_PullClientConfig_FullMethodName = "/master.Master/PullClientConfig"
Master_PullServerConfig_FullMethodName = "/master.Master/PullServerConfig"
Master_ListClientWorkers_FullMethodName = "/master.Master/ListClientWorkers"
Master_FRPCAuth_FullMethodName = "/master.Master/FRPCAuth"
Master_PushProxyInfo_FullMethodName = "/master.Master/PushProxyInfo"
Master_PushClientStreamLog_FullMethodName = "/master.Master/PushClientStreamLog"
Master_PushServerStreamLog_FullMethodName = "/master.Master/PushServerStreamLog"
Master_PTYConnect_FullMethodName = "/master.Master/PTYConnect"
Master_ServerSend_FullMethodName = "/master.Master/ServerSend"
Master_PullClientConfig_FullMethodName = "/master.Master/PullClientConfig"
Master_PullServerConfig_FullMethodName = "/master.Master/PullServerConfig"
Master_ListClientWorkers_FullMethodName = "/master.Master/ListClientWorkers"
Master_ListClientWireGuards_FullMethodName = "/master.Master/ListClientWireGuards"
Master_FRPCAuth_FullMethodName = "/master.Master/FRPCAuth"
Master_PushProxyInfo_FullMethodName = "/master.Master/PushProxyInfo"
Master_PushClientStreamLog_FullMethodName = "/master.Master/PushClientStreamLog"
Master_PushServerStreamLog_FullMethodName = "/master.Master/PushServerStreamLog"
Master_PTYConnect_FullMethodName = "/master.Master/PTYConnect"
Master_ReportWireGuardRuntimeInfo_FullMethodName = "/master.Master/ReportWireGuardRuntimeInfo"
)
// MasterClient is the client API for Master service.
@@ -38,11 +40,13 @@ type MasterClient interface {
PullClientConfig(ctx context.Context, in *PullClientConfigReq, opts ...grpc.CallOption) (*PullClientConfigResp, error)
PullServerConfig(ctx context.Context, in *PullServerConfigReq, opts ...grpc.CallOption) (*PullServerConfigResp, error)
ListClientWorkers(ctx context.Context, in *ListClientWorkersRequest, opts ...grpc.CallOption) (*ListClientWorkersResponse, error)
ListClientWireGuards(ctx context.Context, in *ListClientWireGuardsRequest, opts ...grpc.CallOption) (*ListClientWireGuardsResponse, error)
FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error)
PushProxyInfo(ctx context.Context, in *PushProxyInfoReq, opts ...grpc.CallOption) (*PushProxyInfoResp, error)
PushClientStreamLog(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushClientStreamLogReq, PushStreamLogResp], error)
PushServerStreamLog(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushServerStreamLogReq, PushStreamLogResp], error)
PTYConnect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PTYClientMessage, PTYServerMessage], error)
ReportWireGuardRuntimeInfo(ctx context.Context, in *ReportWireGuardRuntimeInfoReq, opts ...grpc.CallOption) (*ReportWireGuardRuntimeInfoResp, error)
}
type masterClient struct {
@@ -96,6 +100,16 @@ func (c *masterClient) ListClientWorkers(ctx context.Context, in *ListClientWork
return out, nil
}
func (c *masterClient) ListClientWireGuards(ctx context.Context, in *ListClientWireGuardsRequest, opts ...grpc.CallOption) (*ListClientWireGuardsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListClientWireGuardsResponse)
err := c.cc.Invoke(ctx, Master_ListClientWireGuards_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *masterClient) FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FRPAuthResponse)
@@ -155,6 +169,16 @@ func (c *masterClient) PTYConnect(ctx context.Context, opts ...grpc.CallOption)
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Master_PTYConnectClient = grpc.BidiStreamingClient[PTYClientMessage, PTYServerMessage]
func (c *masterClient) ReportWireGuardRuntimeInfo(ctx context.Context, in *ReportWireGuardRuntimeInfoReq, opts ...grpc.CallOption) (*ReportWireGuardRuntimeInfoResp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReportWireGuardRuntimeInfoResp)
err := c.cc.Invoke(ctx, Master_ReportWireGuardRuntimeInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MasterServer is the server API for Master service.
// All implementations must embed UnimplementedMasterServer
// for forward compatibility.
@@ -163,11 +187,13 @@ type MasterServer interface {
PullClientConfig(context.Context, *PullClientConfigReq) (*PullClientConfigResp, error)
PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error)
ListClientWorkers(context.Context, *ListClientWorkersRequest) (*ListClientWorkersResponse, error)
ListClientWireGuards(context.Context, *ListClientWireGuardsRequest) (*ListClientWireGuardsResponse, error)
FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error)
PushProxyInfo(context.Context, *PushProxyInfoReq) (*PushProxyInfoResp, error)
PushClientStreamLog(grpc.ClientStreamingServer[PushClientStreamLogReq, PushStreamLogResp]) error
PushServerStreamLog(grpc.ClientStreamingServer[PushServerStreamLogReq, PushStreamLogResp]) error
PTYConnect(grpc.BidiStreamingServer[PTYClientMessage, PTYServerMessage]) error
ReportWireGuardRuntimeInfo(context.Context, *ReportWireGuardRuntimeInfoReq) (*ReportWireGuardRuntimeInfoResp, error)
mustEmbedUnimplementedMasterServer()
}
@@ -190,6 +216,9 @@ func (UnimplementedMasterServer) PullServerConfig(context.Context, *PullServerCo
func (UnimplementedMasterServer) ListClientWorkers(context.Context, *ListClientWorkersRequest) (*ListClientWorkersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListClientWorkers not implemented")
}
func (UnimplementedMasterServer) ListClientWireGuards(context.Context, *ListClientWireGuardsRequest) (*ListClientWireGuardsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListClientWireGuards not implemented")
}
func (UnimplementedMasterServer) FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FRPCAuth not implemented")
}
@@ -205,6 +234,9 @@ func (UnimplementedMasterServer) PushServerStreamLog(grpc.ClientStreamingServer[
func (UnimplementedMasterServer) PTYConnect(grpc.BidiStreamingServer[PTYClientMessage, PTYServerMessage]) error {
return status.Errorf(codes.Unimplemented, "method PTYConnect not implemented")
}
func (UnimplementedMasterServer) ReportWireGuardRuntimeInfo(context.Context, *ReportWireGuardRuntimeInfoReq) (*ReportWireGuardRuntimeInfoResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReportWireGuardRuntimeInfo not implemented")
}
func (UnimplementedMasterServer) mustEmbedUnimplementedMasterServer() {}
func (UnimplementedMasterServer) testEmbeddedByValue() {}
@@ -287,6 +319,24 @@ func _Master_ListClientWorkers_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _Master_ListClientWireGuards_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListClientWireGuardsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MasterServer).ListClientWireGuards(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Master_ListClientWireGuards_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MasterServer).ListClientWireGuards(ctx, req.(*ListClientWireGuardsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Master_FRPCAuth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FRPAuthRequest)
if err := dec(in); err != nil {
@@ -344,6 +394,24 @@ func _Master_PTYConnect_Handler(srv interface{}, stream grpc.ServerStream) error
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Master_PTYConnectServer = grpc.BidiStreamingServer[PTYClientMessage, PTYServerMessage]
func _Master_ReportWireGuardRuntimeInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReportWireGuardRuntimeInfoReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MasterServer).ReportWireGuardRuntimeInfo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Master_ReportWireGuardRuntimeInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MasterServer).ReportWireGuardRuntimeInfo(ctx, req.(*ReportWireGuardRuntimeInfoReq))
}
return interceptor(ctx, in, info, handler)
}
// Master_ServiceDesc is the grpc.ServiceDesc for Master service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -363,6 +431,10 @@ var Master_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListClientWorkers",
Handler: _Master_ListClientWorkers_Handler,
},
{
MethodName: "ListClientWireGuards",
Handler: _Master_ListClientWireGuards_Handler,
},
{
MethodName: "FRPCAuth",
Handler: _Master_FRPCAuth_Handler,
@@ -371,6 +443,10 @@ var Master_ServiceDesc = grpc.ServiceDesc{
MethodName: "PushProxyInfo",
Handler: _Master_PushProxyInfo_Handler,
},
{
MethodName: "ReportWireGuardRuntimeInfo",
Handler: _Master_ReportWireGuardRuntimeInfo_Handler,
},
},
Streams: []grpc.StreamDesc{
{
+1107
View File
File diff suppressed because it is too large Load Diff
+38 -16
View File
@@ -16,22 +16,24 @@ type application struct {
streamLogHookMgr StreamLogHookMgr
masterCli MasterClient
shellPTYMgr ShellPTYMgr
clientLogManager ClientLogManager
clientRPCHandler ClientRPCHandler
dbManager DBManager
clientController ClientController
clientRecvMap *sync.Map
clientsManager ClientsManager
serverHandler ServerHandler
serverController ServerController
rpcCred credentials.TransportCredentials
conf conf.Config
currentRole string
permManager PermissionManager
enforcer *casbin.Enforcer
workerExecManager WorkerExecManager
workersManager WorkersManager
shellPTYMgr ShellPTYMgr
clientLogManager ClientLogManager
clientRPCHandler ClientRPCHandler
dbManager DBManager
clientController ClientController
clientRecvMap *sync.Map
clientsManager ClientsManager
serverHandler ServerHandler
serverController ServerController
rpcCred credentials.TransportCredentials
conf conf.Config
currentRole string
permManager PermissionManager
enforcer *casbin.Enforcer
workerExecManager WorkerExecManager
workersManager WorkersManager
wireGuardManager WireGuardManager
networkTopologyCache NetworkTopologyCache
loggerInstance *logrus.Logger
}
@@ -231,6 +233,16 @@ func (a *application) GetStreamLogHookMgr() StreamLogHookMgr {
return a.streamLogHookMgr
}
// GetWireGuardManager implements Application.
func (a *application) GetWireGuardManager() WireGuardManager {
return a.wireGuardManager
}
// SetWireGuardManager implements Application.
func (a *application) SetWireGuardManager(wireGuardManager WireGuardManager) {
a.wireGuardManager = wireGuardManager
}
// SetShellPTYMgr implements Application.
func (a *application) SetShellPTYMgr(shellPTYMgr ShellPTYMgr) {
a.shellPTYMgr = shellPTYMgr
@@ -240,3 +252,13 @@ func (a *application) SetShellPTYMgr(shellPTYMgr ShellPTYMgr) {
func (a *application) SetStreamLogHookMgr(streamLogHookMgr StreamLogHookMgr) {
a.streamLogHookMgr = streamLogHookMgr
}
// GetNetworkTopologyCache implements Application.
func (a *application) GetNetworkTopologyCache() NetworkTopologyCache {
return a.networkTopologyCache
}
// SetNetworkTopologyCache implements Application.
func (a *application) SetNetworkTopologyCache(networkTopologyCache NetworkTopologyCache) {
a.networkTopologyCache = networkTopologyCache
}
+4
View File
@@ -53,6 +53,10 @@ type Application interface {
Logger(ctx context.Context) *logrus.Entry
GetClientBase() *pb.ClientBase
GetServerBase() *pb.ServerBase
GetWireGuardManager() WireGuardManager
SetWireGuardManager(WireGuardManager)
GetNetworkTopologyCache() NetworkTopologyCache
SetNetworkTopologyCache(NetworkTopologyCache)
}
type Context struct {
+63
View File
@@ -194,3 +194,66 @@ type WorkersManager interface {
// install workerd bin to workerd bin path, if not specified, use default path /usr/local/bin/workerd
InstallWorkerd(ctx *Context, url string, path string) (string, error)
}
// services/wg/wireguard_manager.go
type WireGuardManager interface {
// CreateService 使用给定的配置初始化一个新的 WireGuard 并记录
// 服务创建后不会自动启动
CreateService(cfg *defs.WireGuardConfig) (WireGuard, error)
// StartService 启动之前添加的 WireGuard
StartService(interfaceName string) error
GetService(interfaceName string) (WireGuard, bool)
StopService(interfaceName string) error
GetAllServices() []WireGuard
// RemoveService 停止并移除一个 WireGuard
RemoveService(interfaceName string) error
// StopAllServices 停止所有 WireGuard
// 返回一个 interfaceName 到 error 的映射,记录停止失败的服务
StopAllServices() map[string]error
RestartService(interfaceName string) error
// Start 启动 WireGuardManager 本身
Start()
Stop()
}
type WireGuardDiffPeersResponse struct {
AddPeers []*defs.WireGuardPeerConfig
RemovePeers []*defs.WireGuardPeerConfig
}
// services/wg/wireguard.go
type WireGuard interface {
Start() error
Stop() error
// Peer相关
AddPeer(peer *defs.WireGuardPeerConfig) error
RemovePeer(peerNameOrPk string) error
GetPeer(peerNameOrPk string) (*defs.WireGuardPeerConfig, error)
UpdatePeer(peer *defs.WireGuardPeerConfig) error
ListPeers() ([]*defs.WireGuardPeerConfig, error)
PatchPeers(newPeers []*defs.WireGuardPeerConfig) (*WireGuardDiffPeersResponse, error)
// Interface相关
GetIfceConfig() (*defs.WireGuardConfig, error)
GetBaseIfceConfig() *defs.WireGuardConfig
// Config相关
GenWGConfig() (string, error) // unimplemented
GetWGRuntimeInfo() (*pb.WGDeviceRuntimeInfo, error)
}
type NetworkTopologyCache interface {
GetRuntimeInfo(wireguardId uint) (*pb.WGDeviceRuntimeInfo, bool)
SetRuntimeInfo(wireguardId uint, runtimeInfo *pb.WGDeviceRuntimeInfo)
DeleteRuntimeInfo(wireguardId uint)
GetLatencyMs(fromWGID, toWGID uint) (uint32, bool)
}
+130
View File
@@ -0,0 +1,130 @@
package dao
import (
"fmt"
"github.com/VaalaCat/frp-panel/models"
"gorm.io/gorm"
)
func (q *queryImpl) CreateEndpoint(userInfo models.UserInfo, endpoint *models.EndpointEntity) error {
if endpoint == nil {
return fmt.Errorf("invalid endpoint entity")
}
if len(endpoint.Host) == 0 || endpoint.Port == 0 {
return fmt.Errorf("invalid endpoint host or port")
}
// scope via parent wireguard/client
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Create(&models.Endpoint{EndpointEntity: endpoint}).Error
}
func (q *queryImpl) UpdateEndpoint(userInfo models.UserInfo, id uint, endpoint *models.EndpointEntity) error {
if id == 0 || endpoint == nil {
return fmt.Errorf("invalid endpoint id or entity")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Where(&models.Endpoint{
Model: gorm.Model{ID: id},
}).Save(&models.Endpoint{Model: gorm.Model{ID: id}, EndpointEntity: endpoint}).Error
}
func (q *queryImpl) DeleteEndpoint(userInfo models.UserInfo, id uint) error {
if id == 0 {
return fmt.Errorf("invalid endpoint id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Unscoped().Where(&models.Endpoint{Model: gorm.Model{ID: id}}).Delete(&models.Endpoint{}).Error
}
func (q *queryImpl) GetEndpointByID(userInfo models.UserInfo, id uint) (*models.Endpoint, error) {
if id == 0 {
return nil, fmt.Errorf("invalid endpoint id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var e models.Endpoint
if err := db.Where(&models.Endpoint{Model: gorm.Model{ID: id}}).First(&e).Error; err != nil {
return nil, err
}
return &e, nil
}
func (q *queryImpl) ListEndpoints(userInfo models.UserInfo, page, pageSize int) ([]*models.Endpoint, error) {
if page < 1 || pageSize < 1 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.Endpoint
offset := (page - 1) * pageSize
if err := db.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) CountEndpoints(userInfo models.UserInfo) (int64, error) {
var count int64
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
if err := db.Model(&models.Endpoint{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// ListEndpointsWithFilters 根据 clientID / wireguardID / keyword 过滤端点
func (q *queryImpl) ListEndpointsWithFilters(userInfo models.UserInfo, page, pageSize int, clientID string, wireguardID uint, keyword string) ([]*models.Endpoint, error) {
if page < 1 || pageSize < 1 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
// 若指定 clientID,先校验归属
if len(clientID) > 0 {
if _, err := q.GetClientByClientID(userInfo, clientID); err != nil {
return nil, err
}
}
var list []*models.Endpoint
offset := (page - 1) * pageSize
query := db.Model(&models.Endpoint{})
if len(clientID) > 0 {
query = query.Where(&models.Endpoint{EndpointEntity: &models.EndpointEntity{ClientID: clientID}})
}
if wireguardID > 0 {
query = query.Where(&models.Endpoint{EndpointEntity: &models.EndpointEntity{WireGuardID: wireguardID}})
}
if len(keyword) > 0 {
query = query.Where("host like ?", "%"+keyword+"%")
}
if err := query.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) CountEndpointsWithFilters(userInfo models.UserInfo, clientID string, wireguardID uint, keyword string) (int64, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
if len(clientID) > 0 {
if _, err := q.GetClientByClientID(userInfo, clientID); err != nil {
return 0, err
}
}
var count int64
query := db.Model(&models.Endpoint{})
if len(clientID) > 0 {
query = query.Where(&models.Endpoint{EndpointEntity: &models.EndpointEntity{ClientID: clientID}})
}
if wireguardID > 0 {
query = query.Where(&models.Endpoint{EndpointEntity: &models.EndpointEntity{WireGuardID: wireguardID}})
}
if len(keyword) > 0 {
query = query.Where("host like ?", "%"+keyword+"%")
}
if err := query.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
+180
View File
@@ -0,0 +1,180 @@
package dao
import (
"fmt"
"strconv"
"github.com/VaalaCat/frp-panel/models"
"gorm.io/gorm"
)
func (q *queryImpl) CreateWireGuardLink(userInfo models.UserInfo, link *models.WireGuardLink) error {
if link == nil {
return fmt.Errorf("invalid wg link")
}
if link.WireGuardLinkEntity == nil {
link.WireGuardLinkEntity = &models.WireGuardLinkEntity{}
}
link.UserId = uint32(userInfo.GetUserID())
link.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Create(link).Error
}
func (q *queryImpl) CreateWireGuardLinks(userInfo models.UserInfo, links ...*models.WireGuardLink) error {
if len(links) == 0 {
return fmt.Errorf("invalid wg links")
}
for _, link := range links {
link.UserId = uint32(userInfo.GetUserID())
link.TenantId = uint32(userInfo.GetTenantID())
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Create(links).Error
}
func (q *queryImpl) UpdateWireGuardLink(userInfo models.UserInfo, id uint, link *models.WireGuardLink) error {
if id == 0 || link == nil {
return fmt.Errorf("invalid wg link id or entity")
}
link.Model.ID = id
if link.WireGuardLinkEntity == nil {
link.WireGuardLinkEntity = &models.WireGuardLinkEntity{}
}
link.UserId = uint32(userInfo.GetUserID())
link.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Where(&models.WireGuardLink{
Model: link.Model,
WireGuardLinkEntity: &models.WireGuardLinkEntity{UserId: link.UserId, TenantId: link.TenantId},
}).Save(link).Error
}
func (q *queryImpl) DeleteWireGuardLink(userInfo models.UserInfo, id uint) error {
if id == 0 {
return fmt.Errorf("invalid wg link id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Unscoped().Where(&models.WireGuardLink{
Model: gorm.Model{ID: id},
WireGuardLinkEntity: &models.WireGuardLinkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Delete(&models.WireGuardLink{}).Error
}
func (q *queryImpl) ListWireGuardLinksByNetwork(userInfo models.UserInfo, networkID uint) ([]*models.WireGuardLink, error) {
if networkID == 0 {
return nil, fmt.Errorf("invalid network id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuardLink
if err := db.Where(&models.WireGuardLink{
WireGuardLinkEntity: &models.WireGuardLinkEntity{
NetworkID: networkID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
// GetWireGuardLinkByID 根据 ID 查询 Link(按租户隔离)
func (q *queryImpl) GetWireGuardLinkByID(userInfo models.UserInfo, id uint) (*models.WireGuardLink, error) {
if id == 0 {
return nil, fmt.Errorf("invalid wg link id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var m models.WireGuardLink
if err := db.Where(&models.WireGuardLink{
Model: gorm.Model{ID: id},
WireGuardLinkEntity: &models.WireGuardLinkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).First(&m).Error; err != nil {
return nil, err
}
return &m, nil
}
func (q *queryImpl) GetWireGuardLinkByClientIDs(userInfo models.UserInfo, fromClientId, toClientId uint) (*models.WireGuardLink, error) {
if fromClientId == 0 || toClientId == 0 {
return nil, fmt.Errorf("invalid from client id or to client id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var link *models.WireGuardLink
if err := db.Where(&models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
FromWireGuardID: fromClientId,
ToWireGuardID: toClientId,
}}).First(&link).Error; err != nil {
return nil, err
}
return link, nil
}
// ListWireGuardLinksWithFilters 分页查询 Link,支持按 networkID 过滤与关键字(数字时匹配 from/to id)
func (q *queryImpl) ListWireGuardLinksWithFilters(userInfo models.UserInfo, page, pageSize int, networkID uint, keyword string) ([]*models.WireGuardLink, error) {
if page < 1 || pageSize < 1 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuardLink
offset := (page - 1) * pageSize
base := db.Where(&models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}})
if networkID > 0 {
base = base.Where(&models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{NetworkID: networkID}})
}
if len(keyword) > 0 {
if v, err := strconv.ParseUint(keyword, 10, 64); err == nil {
base = base.Where("from_wire_guard_id = ? OR to_wire_guard_id = ?", uint(v), uint(v))
}
}
if err := base.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
// CountWireGuardLinksWithFilters 统计分页条件下的总数
func (q *queryImpl) CountWireGuardLinksWithFilters(userInfo models.UserInfo, networkID uint, keyword string) (int64, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var count int64
base := db.Model(&models.WireGuardLink{}).Where(&models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}})
if networkID > 0 {
base = base.Where(&models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{NetworkID: networkID}})
}
if len(keyword) > 0 {
if v, err := strconv.ParseUint(keyword, 10, 64); err == nil {
base = base.Where("from_wire_guard_id = ? OR to_wire_guard_id = ?", uint(v), uint(v))
}
}
if err := base.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (q *queryImpl) AdminListWireGuardLinksWithNetworkIDs(networkIDs []uint) ([]*models.WireGuardLink, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuardLink
if err := db.Where("network_id IN ?", networkIDs).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
+132
View File
@@ -0,0 +1,132 @@
package dao
import (
"fmt"
"github.com/VaalaCat/frp-panel/models"
"gorm.io/gorm"
)
func (q *queryImpl) CreateNetwork(userInfo models.UserInfo, network *models.NetworkEntity) error {
if network == nil {
return fmt.Errorf("invalid network entity")
}
if len(network.Name) == 0 || len(network.CIDR) == 0 {
return fmt.Errorf("invalid network name or cidr")
}
// scope
network.UserId = uint32(userInfo.GetUserID())
network.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Create(&models.Network{NetworkEntity: network}).Error
}
func (q *queryImpl) UpdateNetwork(userInfo models.UserInfo, id uint, network *models.NetworkEntity) error {
if id == 0 || network == nil {
return fmt.Errorf("invalid network id or entity")
}
// scope
network.UserId = uint32(userInfo.GetUserID())
network.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Where(&models.Network{
Model: gorm.Model{ID: id},
NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Save(&models.Network{
Model: gorm.Model{ID: id},
NetworkEntity: network,
}).Error
}
func (q *queryImpl) DeleteNetwork(userInfo models.UserInfo, id uint) error {
if id == 0 {
return fmt.Errorf("invalid network id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Unscoped().Where(&models.Network{
Model: gorm.Model{ID: id},
NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Delete(&models.Network{}).Error
}
func (q *queryImpl) GetNetworkByID(userInfo models.UserInfo, id uint) (*models.Network, error) {
if id == 0 {
return nil, fmt.Errorf("invalid network id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var n models.Network
if err := db.Where(&models.Network{
Model: gorm.Model{ID: id},
NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).First(&n).Error; err != nil {
return nil, err
}
return &n, nil
}
func (q *queryImpl) ListNetworks(userInfo models.UserInfo, page, pageSize int) ([]*models.Network, error) {
if page < 1 || pageSize < 1 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.Network
offset := (page - 1) * pageSize
if err := db.Where(&models.Network{NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) ListNetworksWithKeyword(userInfo models.UserInfo, page, pageSize int, keyword string) ([]*models.Network, error) {
if page < 1 || pageSize < 1 || len(keyword) == 0 {
return nil, fmt.Errorf("invalid page or page size or keyword")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.Network
offset := (page - 1) * pageSize
if err := db.Where(&models.Network{NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Where("name like ? OR cidr like ?", "%"+keyword+"%", "%"+keyword+"%").Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) CountNetworks(userInfo models.UserInfo) (int64, error) {
var count int64
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
if err := db.Model(&models.Network{}).Where(&models.Network{NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (q *queryImpl) CountNetworksWithKeyword(userInfo models.UserInfo, keyword string) (int64, error) {
var count int64
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
if err := db.Model(&models.Network{}).Where(&models.Network{NetworkEntity: &models.NetworkEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Where("name like ? OR cidr like ?", "%"+keyword+"%", "%"+keyword+"%").Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
+217
View File
@@ -0,0 +1,217 @@
package dao
import (
"fmt"
"github.com/VaalaCat/frp-panel/models"
"gorm.io/gorm"
)
func (q *queryImpl) CreateWireGuard(userInfo models.UserInfo, wg *models.WireGuard) error {
if wg == nil || wg.WireGuardEntity == nil {
return fmt.Errorf("invalid wireguard entity")
}
if len(wg.Name) == 0 || len(wg.PrivateKey) == 0 || len(wg.LocalAddress) == 0 {
return fmt.Errorf("invalid wireguard fields")
}
wg.UserId = uint32(userInfo.GetUserID())
wg.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Create(wg).Error
}
func (q *queryImpl) UpdateWireGuard(userInfo models.UserInfo, id uint, wg *models.WireGuard) error {
if id == 0 || wg == nil || wg.WireGuardEntity == nil {
return fmt.Errorf("invalid wireguard id or entity")
}
wg.UserId = uint32(userInfo.GetUserID())
wg.TenantId = uint32(userInfo.GetTenantID())
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
// clear endpoints and resave if provided
if wg.AdvertisedEndpoints != nil {
if err := db.Unscoped().Model(&models.WireGuard{Model: gorm.Model{ID: id}, WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Association("AdvertisedEndpoints").Unscoped().Clear(); err != nil {
return err
}
}
wg.Model = gorm.Model{ID: id}
return db.Where(&models.WireGuard{Model: gorm.Model{ID: id}, WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}}).Save(wg).Error
}
func (q *queryImpl) DeleteWireGuard(userInfo models.UserInfo, id uint) error {
if id == 0 {
return fmt.Errorf("invalid wireguard id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Unscoped().Where(&models.WireGuard{
Model: gorm.Model{ID: id},
WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Delete(&models.WireGuard{}).Error
}
func (q *queryImpl) GetWireGuardByID(userInfo models.UserInfo, id uint) (*models.WireGuard, error) {
if id == 0 {
return nil, fmt.Errorf("invalid wireguard id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var m models.WireGuard
if err := db.
Preload("AdvertisedEndpoints").
Preload("Network").
Where(&models.WireGuard{
Model: gorm.Model{ID: id},
WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).First(&m).Error; err != nil {
return nil, err
}
return &m, nil
}
func (q *queryImpl) AdminGetWireGuardByClientIDAndInterfaceName(clientID, interfaceName string) (*models.WireGuard, error) {
if clientID == "" || interfaceName == "" {
return nil, fmt.Errorf("invalid client id or interface name")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var m models.WireGuard
if err := db.Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
ClientID: clientID,
Name: interfaceName,
}}).First(&m).Error; err != nil {
return nil, err
}
return &m, nil
}
func (q *queryImpl) GetWireGuardsByNetworkID(userInfo models.UserInfo, networkID uint) ([]*models.WireGuard, error) {
if networkID == 0 {
return nil, fmt.Errorf("invalid network id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuard
if err := db.Preload("Network").
Preload("AdvertisedEndpoints").
Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
NetworkID: networkID,
}}).
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) GetWireGuardLocalAddressesByNetworkID(userInfo models.UserInfo, networkID uint) ([]string, error) {
if networkID == 0 {
return nil, fmt.Errorf("invalid network id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []string
if err := db.Model(&models.WireGuard{}).Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
NetworkID: networkID,
}}).Pluck("local_address", &list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) ListWireGuardsWithFilters(userInfo models.UserInfo, page, pageSize int, filter *models.WireGuardEntity, keyword string) ([]*models.WireGuard, error) {
if page < 1 || pageSize < 1 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuard
offset := (page - 1) * pageSize
base := db.Preload("AdvertisedEndpoints").Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}})
scoped := base
if filter != nil {
// only apply selected fields to filter
f := &models.WireGuardEntity{}
if len(filter.ClientID) > 0 {
f.ClientID = filter.ClientID
}
if filter.NetworkID != 0 {
f.NetworkID = filter.NetworkID
}
scoped = scoped.Where(&models.WireGuard{WireGuardEntity: f})
}
if len(keyword) > 0 {
scoped = scoped.Where("name like ? OR local_address like ? OR client_id like ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
if err := scoped.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) AdminListWireGuardsWithClientID(clientID string) ([]*models.WireGuard, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuard
if err := db.Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{ClientID: clientID}}).
Preload("AdvertisedEndpoints").Preload("Network").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) AdminListWireGuardsWithNetworkIDs(networkIDs []uint) ([]*models.WireGuard, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var list []*models.WireGuard
if err := db.Where("network_id IN ?", networkIDs).
Preload("AdvertisedEndpoints").Preload("Network").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (q *queryImpl) CountWireGuardsWithFilters(userInfo models.UserInfo, filter *models.WireGuardEntity, keyword string) (int64, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var count int64
base := db.Model(&models.WireGuard{}).Where(&models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
}})
if filter != nil {
f := &models.WireGuardEntity{}
if len(filter.ClientID) > 0 {
f.ClientID = filter.ClientID
}
if filter.NetworkID != 0 {
f.NetworkID = filter.NetworkID
}
base = base.Where(&models.WireGuard{WireGuardEntity: f})
}
if len(keyword) > 0 {
base = base.Where("name like ? OR local_address like ?", "%"+keyword+"%", "%"+keyword+"%")
}
if err := base.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
+49
View File
@@ -10,6 +10,7 @@ import (
masterserver "github.com/VaalaCat/frp-panel/biz/master/server"
"github.com/VaalaCat/frp-panel/biz/master/shell"
"github.com/VaalaCat/frp-panel/biz/master/streamlog"
"github.com/VaalaCat/frp-panel/biz/master/wg"
"github.com/VaalaCat/frp-panel/biz/master/worker"
"github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs"
@@ -27,6 +28,54 @@ type server struct {
appInstance app.Application
}
func (s *server) ReportWireGuardRuntimeInfo(ctx context.Context, req *pb.ReportWireGuardRuntimeInfoReq) (*pb.ReportWireGuardRuntimeInfoResp, error) {
logger.Logger(ctx).Infof("report wireguard runtime info, clientID: [%s], interfaceName: [%s]", req.GetBase().GetClientId(), req.GetInterfaceName())
appCtx := app.NewContext(ctx, s.appInstance)
if client, err := client.ValidateClientRequest(appCtx, req.GetBase()); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot validate client request")
return nil, err
} else if client.Stopped {
logger.Logger(appCtx).Infof("report wireguard runtime info, client [%s] is stopped", req.GetBase().GetClientId())
return &pb.ReportWireGuardRuntimeInfoResp{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_NOT_FOUND,
Message: "client stopped",
},
}, nil
}
logger.Logger(appCtx).Infof("validate client success, clientID: [%s], interfaceName: [%s]", req.GetBase().GetClientId(), req.GetInterfaceName())
return wg.ReportWireGuardRuntimeInfo(appCtx, req)
}
// ListClientWireGuards implements pb.MasterServer.
func (s *server) ListClientWireGuards(ctx context.Context, req *pb.ListClientWireGuardsRequest) (*pb.ListClientWireGuardsResponse, error) {
logger.Logger(ctx).Infof("list client wire guards, clientID: [%s]", req.GetBase().GetClientId())
appCtx := app.NewContext(ctx, s.appInstance)
if client, err := client.ValidateClientRequest(appCtx, req.GetBase()); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot validate client request")
return nil, err
} else if client.Stopped {
logger.Logger(appCtx).Infof("list client wire guards, client [%s] is stopped", req.GetBase().GetClientId())
return &pb.ListClientWireGuardsResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_NOT_FOUND,
Message: "client stopped",
},
}, nil
}
resp, err := wg.ListClientWireGuards(appCtx, req)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot list client wire guards")
return nil, err
}
return resp, nil
}
// ListClientWorkers implements pb.MasterServer.
func (s *server) ListClientWorkers(ctx context.Context, req *pb.ListClientWorkersRequest) (*pb.ListClientWorkersResponse, error) {
logger.Logger(ctx).Infof("list client workers, clientID: [%s]", req.GetBase().GetClientId())
+94
View File
@@ -0,0 +1,94 @@
package wg
import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/VaalaCat/frp-panel/pb"
"github.com/samber/lo"
)
type ACLEntity interface {
GetTags() []string
GetID() uint
}
type ACL struct {
*pb.AclConfig
Links map[uint]uint
mu sync.RWMutex
}
func NewACL() *ACL {
return &ACL{
AclConfig: &pb.AclConfig{},
}
}
func (a *ACL) AddRule(sourceTag, destTag string) {
a.mu.Lock()
defer a.mu.Unlock()
a.Acls = append(a.Acls, &pb.AclRuleConfig{Src: []string{sourceTag}, Dst: []string{destTag}})
}
func (a *ACL) CanConnect(src, dst ACLEntity) bool {
if a == nil || lo.IsNil(a) {
return true
}
if lo.IsNil(a.AclConfig) || len(a.AclConfig.Acls) == 0 {
return true
}
a.mu.RLock()
defer a.mu.RUnlock()
for _, sTag := range src.GetTags() {
for _, dTag := range dst.GetTags() {
if a.matchRule(sTag, dTag) {
return true
}
}
}
return false
}
func (a *ACL) matchRule(sourceTag, destTag string) bool {
for _, r := range a.Acls {
if lo.Contains(r.Src, sourceTag) && lo.Contains(r.Dst, destTag) {
return true
}
}
return false
}
func (a *ACL) LoadFromJSON(data []byte) error {
var cfg pb.AclConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("invalid ACL JSON: %w", err)
}
for _, rule := range cfg.Acls {
if strings.ToLower(rule.Action) != "accept" {
continue
}
for _, s := range rule.Src {
for _, d := range rule.Dst {
a.AddRule(s, d)
}
}
}
return nil
}
func (a *ACL) LoadFromPB(cfg *pb.AclConfig) *ACL {
a.mu.Lock()
defer a.mu.Unlock()
a.AclConfig = cfg
return a
}
type DefaultEntity struct {
ID string
}
+158
View File
@@ -0,0 +1,158 @@
package wg
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/VaalaCat/frp-panel/defs"
)
func InitAndValidateWGConfig(cfg *defs.WireGuardConfig, logger *logrus.Entry) error {
logEntry := logger.WithField("op", "initCfg").WithField("iface", cfg.InterfaceName)
if cfg.PrivateKey == "" || cfg.PrivateKey == defs.PlaceholderPrivateKey {
return errors.New("private key is required")
} else {
_, err := wgtypes.ParseKey(cfg.PrivateKey)
if err != nil {
return errors.Join(errors.New("invalid PrivateKey"), err)
}
logEntry.Debugf("Using provided PrivateKey. Public key: %s", cfg.GetParsedPublicKey().String())
}
if defs.IsZeroKey(cfg.GetParsedPrivKey()) && cfg.PrivateKey != defs.PlaceholderPrivateKey {
return errors.New("failed to parse and store private key internally")
}
if defs.IsZeroKey(cfg.GetParsedPublicKey()) && cfg.PrivateKey != defs.PlaceholderPrivateKey {
return errors.New("failed to derive and store public key internally")
}
if cfg.GetListenPort() == 0 {
return errors.New("listen port is required")
}
if cfg.GetLocalAddress() == "" {
return errors.New("local address is required")
}
// 使用默认MTU
if cfg.GetInterfaceMtu() == 0 {
cfg.InterfaceMtu = defs.DefaultDeviceMTU
logEntry.Debugf("InterfaceMTU using default: %d", cfg.GetInterfaceMtu())
}
if _, _, err := net.ParseCIDR(cfg.LocalAddress); err != nil {
return errors.Join(fmt.Errorf("invalid LocalAddress ('%s')", cfg.LocalAddress), err)
}
return nil
}
type WGKeys struct {
PrivateKeyBase64 string
PublicKeyBase64 string
PrivateKey wgtypes.Key
PublicKey wgtypes.Key
}
// GenerateKeys generates a new private key and returns it in base64 format.
// return: private key, public key, private key, public key
func GenerateKeys() WGKeys {
priv, e := wgtypes.GeneratePrivateKey()
if e != nil {
panic(fmt.Errorf("generate private key: %w", e))
}
pub := priv.PublicKey()
return WGKeys{
PrivateKeyBase64: priv.String(),
PublicKeyBase64: pub.String(),
PrivateKey: priv,
PublicKey: pub,
}
}
// parseAndValidatePeerConfigs 生成wg UAPI格式的peer配置
func parseAndValidatePeerConfigs(peerConfigs []*defs.WireGuardPeerConfig) ([]wgtypes.PeerConfig, error) {
if len(peerConfigs) == 0 {
return []wgtypes.PeerConfig{}, nil
}
wgTypedPeers := make([]wgtypes.PeerConfig, 0, len(peerConfigs))
for i, pCfg := range peerConfigs {
peerIDForLog := pCfg.ClientId
if peerIDForLog == "" {
peerIDForLog = fmt.Sprintf("index %d (PK: %s...)", i, truncate(pCfg.PublicKey, 10))
} else {
peerIDForLog = fmt.Sprintf("'%s' (PK: %s...)", pCfg.ClientId, truncate(pCfg.PublicKey, 10))
}
typedPeer, err := parseAndValidatePeerConfig(pCfg)
if err != nil {
return nil, fmt.Errorf("peer %s: %w", peerIDForLog, err)
}
wgTypedPeers = append(wgTypedPeers, typedPeer)
}
return wgTypedPeers, nil
}
// parseAndValidatePeerConfig 将frpp使用的PeerConfig转换为wgtypes.PeerConfig,用来给wg设备使用
func parseAndValidatePeerConfig(pCfg *defs.WireGuardPeerConfig) (wgtypes.PeerConfig, error) {
var typedPeer wgtypes.PeerConfig
typedPeer.PublicKey = pCfg.GetParsedPublicKey()
typedPeer.PresharedKey = pCfg.GetParsedPresharedKey()
typedPeer.AllowedIPs = make([]net.IPNet, 0, len(pCfg.GetAllowedIps()))
for _, cidrStr := range pCfg.GetAllowedIps() {
trimmedCidr := strings.TrimSpace(cidrStr)
if trimmedCidr == "" {
continue
}
_, ipNet, err := net.ParseCIDR(trimmedCidr)
if err != nil {
return wgtypes.PeerConfig{}, errors.Join(errors.New("invalid AllowedIP CIDR"), err)
}
typedPeer.AllowedIPs = append(typedPeer.AllowedIPs, *ipNet)
}
if pCfg.Endpoint != nil {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", pCfg.Endpoint.Host, pCfg.Endpoint.Port))
if err != nil {
return wgtypes.PeerConfig{}, errors.Join(errors.New("invalid endpoint address"), err)
}
typedPeer.Endpoint = addr
}
if pCfg.PersistentKeepalive <= 0 {
typedPeer.PersistentKeepaliveInterval = lo.ToPtr(time.Duration(defs.DefaultPersistentKeepalive) * time.Second)
} else {
interval := time.Duration(pCfg.PersistentKeepalive) * time.Second
typedPeer.PersistentKeepaliveInterval = &interval
}
typedPeer.ReplaceAllowedIPs = true
return typedPeer, nil
}
func truncate(s string, maxLen int) string {
if len(s) > maxLen {
return s[:maxLen] + "..."
}
return s
}
// generateUAPIConfigString implementation from previous step
func generateUAPIConfigString(cfg *defs.WireGuardConfig, wgPrivateKey wgtypes.Key, peerConfigs []wgtypes.PeerConfig, firstStart bool) string {
uapiBuilder := NewUAPIBuilder()
uapiBuilder.WithPrivateKey(wgPrivateKey).
WithListenPort(int(cfg.ListenPort)).
ReplacePeers(!firstStart).
AddPeers(peerConfigs)
return uapiBuilder.Build()
}
+31
View File
@@ -0,0 +1,31 @@
package wg_test
import (
"testing"
"github.com/VaalaCat/frp-panel/services/wg"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func TestGenerateKeys(t *testing.T) {
tests := []struct {
name string // description of this test case
want wg.WGKeys
}{
{"test", wg.WGKeys{
PrivateKeyBase64: "test",
PublicKeyBase64: "test",
PrivateKey: wgtypes.Key{},
PublicKey: wgtypes.Key{},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := wg.GenerateKeys()
// TODO: update the condition below to compare got with tt.want.
if true {
t.Errorf("GenerateKeys() = %v, want %v", got, tt.want)
}
})
}
}
+333
View File
@@ -0,0 +1,333 @@
package wg
import (
"errors"
"math"
"sort"
"github.com/samber/lo"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
)
// RoutingPolicy 决定边权重的计算方式。
// cost = LatencyWeight*latency_ms + InverseBandwidthWeight*(1/max(up_mbps,1e-6)) + HopWeight
type RoutingPolicy struct {
LatencyWeight float64
InverseBandwidthWeight float64
HopWeight float64
MinUpMbps uint32
DefaultEndpointUpMbps uint32
DefaultEndpointLatencyMs uint32
ACL *ACL
NetworkTopologyCache app.NetworkTopologyCache
}
func (p *RoutingPolicy) LoadACL(acl *ACL) *RoutingPolicy {
p.ACL = acl
return p
}
func DefaultRoutingPolicy(acl *ACL, networkTopologyCache app.NetworkTopologyCache) RoutingPolicy {
return RoutingPolicy{
LatencyWeight: 1.0,
InverseBandwidthWeight: 50.0, // 对低带宽路径给予更高惩罚
HopWeight: 1.0,
DefaultEndpointUpMbps: 50,
DefaultEndpointLatencyMs: 30,
ACL: acl,
NetworkTopologyCache: networkTopologyCache,
}
}
type AllowedIPsPlanner interface {
// Compute 基于拓扑与链路指标,计算每个节点应配置到直连邻居的 AllowedIPs,并返回节点ID->PeerConfig 列表。
// 输入的 peers 应包含同一 Network 下的所有 WireGuard 节点,links 为其有向链路。
Compute(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]*pb.WireGuardPeerConfig, error)
BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error)
}
type dijkstraAllowedIPsPlanner struct {
policy RoutingPolicy
}
func NewDijkstraAllowedIPsPlanner(policy RoutingPolicy) AllowedIPsPlanner {
return &dijkstraAllowedIPsPlanner{policy: policy}
}
func PlanAllowedIPs(peers []*models.WireGuard, links []*models.WireGuardLink, policy RoutingPolicy) (map[uint][]*pb.WireGuardPeerConfig, error) {
return NewDijkstraAllowedIPsPlanner(policy).Compute(peers, links)
}
func (p *dijkstraAllowedIPsPlanner) Compute(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]*pb.WireGuardPeerConfig, error) {
if len(peers) == 0 {
return map[uint][]*pb.WireGuardPeerConfig{}, nil
}
idToPeer, order := buildNodeIndex(peers)
adj := buildAdjacency(order, idToPeer, links, p.policy)
aggByNode := runAllPairsDijkstra(order, adj, idToPeer, p.policy)
result, err := assemblePeerConfigs(order, aggByNode, idToPeer)
if err != nil {
return nil, err
}
fillIsolates(order, result)
return result, nil
}
func (p *dijkstraAllowedIPsPlanner) BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error) {
idToPeer, order := buildNodeIndex(peers)
adj := buildAdjacency(order, idToPeer, links, p.policy)
// 填充没有链路的节点
for _, id := range order {
if _, ok := adj[id]; !ok {
adj[id] = []Edge{}
}
}
return adj, nil
}
type Edge struct {
to uint
latency uint32
upMbps uint32
}
func (e *Edge) ToPB() *pb.WireGuardLink {
return &pb.WireGuardLink{
ToWireguardId: uint32(e.to),
LatencyMs: e.latency,
UpBandwidthMbps: e.upMbps,
Active: true,
}
}
func buildNodeIndex(peers []*models.WireGuard) (map[uint]*models.WireGuard, []uint) {
idToPeer := make(map[uint]*models.WireGuard, len(peers))
order := make([]uint, 0, len(peers))
for _, p := range peers {
idToPeer[uint(p.ID)] = p
order = append(order, uint(p.ID))
}
return idToPeer, order
}
func buildAdjacency(order []uint, idToPeer map[uint]*models.WireGuard, links []*models.WireGuardLink, policy RoutingPolicy) map[uint][]Edge {
adj := make(map[uint][]Edge, len(order))
// 1) 显式链路
for _, l := range links {
if !l.Active {
continue
}
from := l.FromWireGuardID
to := l.ToWireGuardID
if _, ok := idToPeer[from]; !ok {
continue
}
if _, ok := idToPeer[to]; !ok {
continue
}
// 如果两个peer都没有endpoint,则不建立链路
if len(idToPeer[from].AdvertisedEndpoints) == 0 && len(idToPeer[to].AdvertisedEndpoints) == 0 {
continue
}
latency := l.LatencyMs
if latency == 0 { // 如果指定latency为0,则使用真实值
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
latency = latencyMs
} else {
latency = policy.DefaultEndpointLatencyMs
}
}
adj[from] = append(adj[from], Edge{to: to, latency: latency, upMbps: l.UpBandwidthMbps})
}
// 2) 若某节点具备 endpoint,则所有其他节点可直连它
edgeSet := make(map[[2]uint]struct{}, 16)
for from, edges := range adj {
for _, e := range edges { // 先拿到所有直连的节点
edgeSet[[2]uint{from, e.to}] = struct{}{}
edgeSet[[2]uint{e.to, from}] = struct{}{}
}
}
for _, to := range order {
peer := idToPeer[to]
if peer == nil || len(peer.AdvertisedEndpoints) == 0 {
continue
}
for _, from := range order {
if from == to {
continue
}
if _, ok := idToPeer[from]; !ok {
continue
}
latency := policy.DefaultEndpointLatencyMs
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
latency = latencyMs
}
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(to, from); ok {
latency = latencyMs
}
// 有 acl 限制
if policy.ACL.CanConnect(idToPeer[from], idToPeer[to]) {
key1 := [2]uint{from, to}
if _, exists := edgeSet[key1]; exists {
continue
}
adj[from] = append(adj[from], Edge{to: to, latency: latency, upMbps: policy.DefaultEndpointUpMbps})
edgeSet[key1] = struct{}{}
}
if policy.ACL.CanConnect(idToPeer[to], idToPeer[from]) {
key2 := [2]uint{to, from}
if _, exists := edgeSet[key2]; exists {
continue
}
adj[to] = append(adj[to], Edge{to: from, latency: latency, upMbps: policy.DefaultEndpointUpMbps})
edgeSet[key2] = struct{}{}
}
}
}
return adj
}
func runAllPairsDijkstra(order []uint, adj map[uint][]Edge, idToPeer map[uint]*models.WireGuard, policy RoutingPolicy) map[uint]map[uint]map[string]struct{} {
aggByNode := make(map[uint]map[uint]map[string]struct{}, len(order))
for _, src := range order {
dist, prev, visited := initSSSP(order)
dist[src] = 0
for {
u, ok := pickNext(order, dist, visited)
if !ok {
break
}
visited[u] = true
for _, e := range adj[u] {
invBw := 1.0 / math.Max(float64(e.upMbps), 1e-6)
w := policy.LatencyWeight*float64(e.latency) + policy.InverseBandwidthWeight*invBw + policy.HopWeight
alt := dist[u] + w
if alt < dist[e.to] {
dist[e.to] = alt
prev[e.to] = u
}
}
}
// 累计 nextHop -> CIDR
for _, dst := range order {
if dst == src {
continue
}
if _, ok := prev[dst]; !ok {
continue
}
next := findNextHop(src, dst, prev)
if next == 0 {
continue
}
dstPeer := idToPeer[dst]
allowed, err := dstPeer.AsBasePeerConfig()
if err != nil || len(allowed.GetAllowedIps()) == 0 {
continue
}
cidr := allowed.GetAllowedIps()[0]
if _, ok := aggByNode[src]; !ok {
aggByNode[src] = make(map[uint]map[string]struct{})
}
if _, ok := aggByNode[src][next]; !ok {
aggByNode[src][next] = map[string]struct{}{}
}
aggByNode[src][next][cidr] = struct{}{}
}
}
return aggByNode
}
func initSSSP(order []uint) (map[uint]float64, map[uint]uint, map[uint]bool) {
dist := make(map[uint]float64, len(order))
prev := make(map[uint]uint, len(order))
visited := make(map[uint]bool, len(order))
for _, vid := range order {
dist[vid] = math.Inf(1)
}
return dist, prev, visited
}
func pickNext(order []uint, dist map[uint]float64, visited map[uint]bool) (uint, bool) {
best := uint(0)
bestVal := math.Inf(1)
found := false
for _, vid := range order {
if visited[vid] {
continue
}
if dist[vid] < bestVal {
bestVal = dist[vid]
best = vid
found = true
}
}
return best, found
}
func findNextHop(src, dst uint, prev map[uint]uint) uint {
next := dst
for {
p, ok := prev[next]
if !ok {
return 0
}
if p == src {
return next
}
next = p
}
}
func assemblePeerConfigs(order []uint, aggByNode map[uint]map[uint]map[string]struct{}, idToPeer map[uint]*models.WireGuard) (map[uint][]*pb.WireGuardPeerConfig, error) {
result := make(map[uint][]*pb.WireGuardPeerConfig, len(order))
for src, nextMap := range aggByNode {
peersForSrc := make([]*pb.WireGuardPeerConfig, 0, len(nextMap))
for nextHop, cidrSet := range nextMap {
remote := idToPeer[nextHop]
base, err := remote.AsBasePeerConfig()
if err != nil {
return nil, errors.Join(errors.New("build peer base config failed"), err)
}
cidrs := make([]string, 0, len(cidrSet))
for c := range cidrSet {
cidrs = append(cidrs, c)
}
sort.Strings(cidrs)
base.AllowedIps = lo.Uniq(cidrs)
peersForSrc = append(peersForSrc, base)
}
sort.SliceStable(peersForSrc, func(i, j int) bool {
return peersForSrc[i].GetClientId() < peersForSrc[j].GetClientId()
})
result[src] = peersForSrc
}
return result, nil
}
func fillIsolates(order []uint, result map[uint][]*pb.WireGuardPeerConfig) {
for _, id := range order {
if _, ok := result[id]; !ok {
result[id] = []*pb.WireGuardPeerConfig{}
}
}
}
+143
View File
@@ -0,0 +1,143 @@
package wg
import (
"strconv"
"strings"
"github.com/VaalaCat/frp-panel/pb"
)
func ParseWGRunningInfo(raw string) (*pb.WGDeviceRuntimeInfo, error) {
lines := strings.Split(raw, "\n")
dev := &pb.WGDeviceRuntimeInfo{Peers: make([]*pb.WGPeerRuntimeInfo, 0, 8)}
var cur *pb.WGPeerRuntimeInfo
flushPeer := func() {
if cur != nil {
dev.Peers = append(dev.Peers, cur)
cur = nil
}
}
for _, ln := range lines {
ln = strings.TrimSpace(ln)
if ln == "" {
break
}
eq := strings.IndexByte(ln, '=')
if eq <= 0 {
continue
}
k := ln[:eq]
v := ln[eq+1:]
switch k {
case "private_key":
dev.PrivateKey = v
case "listen_port":
if p, err := strconv.ParseUint(v, 10, 32); err == nil {
dev.ListenPort = uint32(p)
}
case "protocol_version":
if pv, err := strconv.ParseUint(v, 10, 32); err == nil {
dev.ProtocolVersion = uint32(pv)
}
case "errno":
if e, err := strconv.ParseInt(v, 10, 32); err == nil {
dev.Errno = int32(e)
}
case "public_key":
// 新 peer 开始
flushPeer()
cur = &pb.WGPeerRuntimeInfo{PublicKey: v}
case "preshared_key":
if cur != nil {
cur.PresharedKey = v
}
case "allowed_ip":
if cur != nil {
cur.AllowedIps = append(cur.AllowedIps, v)
}
case "endpoint":
if cur != nil {
host, port := parseEndpoint(v)
cur.EndpointHost = host
cur.EndpointPort = port
}
case "tx_bytes":
if cur != nil {
if n, err := strconv.ParseUint(v, 10, 64); err == nil {
cur.TxBytes = n
}
}
case "rx_bytes":
if cur != nil {
if n, err := strconv.ParseUint(v, 10, 64); err == nil {
cur.RxBytes = n
}
}
case "persistent_keepalive_interval":
if cur != nil {
if n, err := strconv.ParseInt(v, 10, 32); err == nil {
cur.PersistentKeepaliveInterval = uint32(n)
}
}
case "last_handshake_time_nsec":
if cur != nil {
if n, err := strconv.ParseUint(v, 10, 64); err == nil {
cur.LastHandshakeTimeNsec = n
}
}
case "last_handshake_time_sec":
if cur != nil {
if n, err := strconv.ParseUint(v, 10, 64); err == nil {
cur.LastHandshakeTimeSec = n
}
}
default:
if cur != nil {
if cur.Extra == nil {
cur.Extra = make(map[string]string)
}
cur.Extra[k] = v
} else {
if dev.Extra == nil {
dev.Extra = make(map[string]string)
}
dev.Extra[k] = v
}
}
}
flushPeer()
return dev, nil
}
func parseEndpoint(v string) (string, uint32) {
v = strings.TrimSpace(v)
if v == "" {
return "", 0
}
// IPv6 带 [] 的形式
if strings.HasPrefix(v, "[") {
rb := strings.IndexByte(v, ']')
if rb > 1 && rb+1 < len(v) && v[rb+1] == ':' {
host := v[1:rb]
portStr := v[rb+2:]
if p, err := strconv.ParseUint(portStr, 10, 32); err == nil {
return host, uint32(p)
}
return host, 0
}
return v, 0
}
// IPv4 或域名 host:port
idx := strings.LastIndexByte(v, ':')
if idx <= 0 || idx+1 >= len(v) {
return v, 0
}
host := v[:idx]
portStr := v[idx+1:]
if p, err := strconv.ParseUint(portStr, 10, 32); err == nil {
return host, uint32(p)
}
return host, 0
}
+53
View File
@@ -0,0 +1,53 @@
package wg
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils"
)
var (
_ app.NetworkTopologyCache = (*networkTopologyCache)(nil)
)
// networkTopologyCache 目前只给服务端用
type networkTopologyCache struct {
wireguardRuntimeInfoMap *utils.SyncMap[uint, *pb.WGDeviceRuntimeInfo] // wireguardId -> peerRuntimeInfo
fromToLatencyMap *utils.SyncMap[string, uint32] // fromWGID::toWGID -> latencyMs
}
func NewNetworkTopologyCache() *networkTopologyCache {
return &networkTopologyCache{
wireguardRuntimeInfoMap: &utils.SyncMap[uint, *pb.WGDeviceRuntimeInfo]{},
fromToLatencyMap: &utils.SyncMap[string, uint32]{},
}
}
func (c *networkTopologyCache) GetRuntimeInfo(wireguardId uint) (*pb.WGDeviceRuntimeInfo, bool) {
return c.wireguardRuntimeInfoMap.Load(wireguardId)
}
func (c *networkTopologyCache) SetRuntimeInfo(wireguardId uint, runtimeInfo *pb.WGDeviceRuntimeInfo) {
c.wireguardRuntimeInfoMap.Store(wireguardId, runtimeInfo)
for toWireGuardId, latencyMs := range runtimeInfo.GetPingMap() {
c.fromToLatencyMap.Store(parseFromToLatencyKey(wireguardId, uint(toWireGuardId)), latencyMs)
}
}
func (c *networkTopologyCache) DeleteRuntimeInfo(wireguardId uint) {
c.wireguardRuntimeInfoMap.Delete(wireguardId)
}
func (c *networkTopologyCache) GetLatencyMs(fromWGID, toWGID uint) (uint32, bool) {
v1, ok := c.fromToLatencyMap.Load(parseFromToLatencyKey(fromWGID, toWGID))
if !ok {
return c.fromToLatencyMap.Load(parseFromToLatencyKey(toWGID, fromWGID))
}
return v1, true
}
func parseFromToLatencyKey(fromWGID, toWGID uint) string {
return fmt.Sprintf("%d::%d", fromWGID, toWGID)
}
+157
View File
@@ -0,0 +1,157 @@
package wg
import (
"encoding/hex"
"fmt"
"net"
"strings"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type UAPIBuilder struct {
// Interface-level settings
interfacePrivateKey *wgtypes.Key
listenPort *int
fwmark *int
replacePeers bool
// Accumulated peer sections (already formatted key=value lines ending with \n)
peerSections []string
}
// NewUAPIBuilder creates a new builder instance.
func NewUAPIBuilder() *UAPIBuilder {
return &UAPIBuilder{peerSections: make([]string, 0, 8)}
}
// WithPrivateKey sets the interface private key.
func (b *UAPIBuilder) WithPrivateKey(key wgtypes.Key) *UAPIBuilder {
b.interfacePrivateKey = &key
return b
}
// WithListenPort sets the interface listen port.
func (b *UAPIBuilder) WithListenPort(port int) *UAPIBuilder {
b.listenPort = &port
return b
}
// WithFwmark sets the fwmark. Passing 0 indicates removal per UAPI.
func (b *UAPIBuilder) WithFwmark(mark int) *UAPIBuilder {
b.fwmark = &mark
return b
}
// ReplacePeers controls whether subsequent peers replace existing ones instead of appending.
func (b *UAPIBuilder) ReplacePeers(replace bool) *UAPIBuilder {
b.replacePeers = replace
return b
}
// AddPeerConfig appends a peer configuration section.
func (b *UAPIBuilder) AddPeerConfig(peer wgtypes.PeerConfig) *UAPIBuilder {
b.peerSections = append(b.peerSections, buildPeerSection(peer, ""))
return b
}
func (b *UAPIBuilder) AddPeers(peers []wgtypes.PeerConfig) *UAPIBuilder {
for _, peer := range peers {
b.AddPeerConfig(peer)
}
return b
}
// UpdatePeerConfig appends a peer configuration section with update_only=true.
func (b *UAPIBuilder) UpdatePeerConfig(peer wgtypes.PeerConfig) *UAPIBuilder {
b.peerSections = append(b.peerSections, buildPeerSection(peer, "update_only=true\n"))
return b
}
// RemovePeerByKey appends a peer removal section.
func (b *UAPIBuilder) RemovePeerByKey(publicKey wgtypes.Key) *UAPIBuilder {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("public_key=%s\n", hex.EncodeToString(publicKey[:])))
sb.WriteString("remove=true\n")
b.peerSections = append(b.peerSections, sb.String())
return b
}
// RemovePeerByHexPublicKey appends a peer removal section using a hex-encoded public key.
func (b *UAPIBuilder) RemovePeerByHexPublicKey(hexPublicKey string) *UAPIBuilder {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("public_key=%s\n", strings.ToLower(strings.TrimSpace(hexPublicKey))))
sb.WriteString("remove=true\n")
b.peerSections = append(b.peerSections, sb.String())
return b
}
// Build renders the final UAPI configuration string.
// It ensures interface-level keys precede all peer-level keys and ends with a blank line.
func (b *UAPIBuilder) Build() string {
var sb strings.Builder
if b.interfacePrivateKey != nil {
sb.WriteString(fmt.Sprintf("private_key=%s\n", hex.EncodeToString(b.interfacePrivateKey[:])))
}
if b.listenPort != nil {
sb.WriteString(fmt.Sprintf("listen_port=%d\n", *b.listenPort))
}
if b.fwmark != nil {
sb.WriteString(fmt.Sprintf("fwmark=%d\n", *b.fwmark))
}
if b.replacePeers {
sb.WriteString("replace_peers=true\n")
}
for _, section := range b.peerSections {
sb.WriteString(section)
}
out := sb.String()
out = strings.TrimSuffix(out, "\n")
return out + "\n\n"
}
// buildPeerSection converts a wgtypes.PeerConfig into its UAPI text format.
func buildPeerSection(peer wgtypes.PeerConfig, extraHeader string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("public_key=%s\n", hex.EncodeToString(peer.PublicKey[:])))
if extraHeader != "" {
sb.WriteString(extraHeader)
}
if peer.PresharedKey != nil && !isZeroKey(*peer.PresharedKey) {
sb.WriteString(fmt.Sprintf("preshared_key=%s\n", hex.EncodeToString(peer.PresharedKey[:])))
}
if peer.Endpoint != nil {
sb.WriteString(fmt.Sprintf("endpoint=%s\n", normalizeEndpoint(peer.Endpoint)))
}
if peer.PersistentKeepaliveInterval != nil && *peer.PersistentKeepaliveInterval > 0 {
secs := int(peer.PersistentKeepaliveInterval.Seconds())
sb.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", secs))
}
if peer.ReplaceAllowedIPs {
sb.WriteString("replace_allowed_ips=true\n")
}
for _, allowedIP := range peer.AllowedIPs {
sb.WriteString(fmt.Sprintf("allowed_ip=%s\n", allowedIP.String()))
}
return sb.String()
}
func normalizeEndpoint(addr *net.UDPAddr) string {
// UDPAddr.String formats IPv6 as [ip%zone]:port; keep as-is to match UAPI.
return addr.String()
}
func isZeroKey(key wgtypes.Key) bool {
var zero wgtypes.Key
return key == zero
}
+482
View File
@@ -0,0 +1,482 @@
package wg
import (
"context"
"errors"
"fmt"
"os"
"sync"
"time"
probing "github.com/prometheus-community/pro-bing"
"github.com/sirupsen/logrus"
"github.com/sourcegraph/conc"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils"
)
const (
ReportInterval = time.Second * 60
)
var (
_ app.WireGuard = (*wireGuard)(nil)
)
type wireGuard struct {
sync.RWMutex
ifce *defs.WireGuardConfig
pingMap *utils.SyncMap[uint32, uint32] // ms
wgDevice *device.Device
tunDevice tun.Device
running bool
svcLogger *logrus.Entry
ctx *app.Context
cancel context.CancelFunc
}
func NewWireGuard(ctx *app.Context, ifce defs.WireGuardConfig, logger *logrus.Entry) (app.WireGuard, error) {
if logger == nil {
defaultLog := logrus.New()
logger = logrus.NewEntry(defaultLog)
}
cfg := ifce
if err := InitAndValidateWGConfig(&cfg, logger); err != nil {
return nil, errors.Join(errors.New("init and validate wg config error"), err)
}
svcCtx, cancel := ctx.CopyWithCancel()
return &wireGuard{
RWMutex: sync.RWMutex{},
ifce: &cfg,
ctx: svcCtx,
cancel: cancel,
svcLogger: logger,
pingMap: &utils.SyncMap[uint32, uint32]{},
}, nil
}
// Start implements WireGuard.
func (w *wireGuard) Start() error {
w.Lock()
defer w.Unlock()
log := w.svcLogger.WithField("op", "Start")
if w.running {
log.Warnf("wireguard is already running, skip start, ifce: %s", w.ifce.GetInterfaceName())
return nil
}
if err := w.initWGDevice(); err != nil {
return errors.Join(fmt.Errorf("init WG device failed"), err)
}
if err := w.applyPeerConfig(); err != nil {
return errors.Join(fmt.Errorf("apply peer config failed"), err)
}
if err := w.initNetwork(); err != nil {
return errors.Join(errors.New("init network failed"), err)
}
if err := w.wgDevice.Up(); err != nil {
return errors.Join(fmt.Errorf("wgDevice.Up '%s'", w.ifce.GetInterfaceName()), err)
}
log.Infof("Started service done for iface '%s'", w.ifce.GetInterfaceName())
w.running = true
go w.reportStatusTask()
return nil
}
// Stop implements WireGuard.
func (w *wireGuard) Stop() error {
w.Lock()
defer w.Unlock()
log := w.svcLogger.WithField("op", "Stop")
if !w.running {
log.Info("Service already down.")
return nil
}
log.Info("Stopping service...")
w.cleanupWGDevice()
w.cleanupNetwork()
w.cancel()
w.running = false
log.Info("Service stopped.")
return nil
}
// AddPeer implements WireGuard.
func (w *wireGuard) AddPeer(peer *defs.WireGuardPeerConfig) error {
log := w.svcLogger.WithField("op", "AddPeer")
peerCfg, err := parseAndValidatePeerConfig(peer)
if err != nil {
return errors.Join(errors.New("parse and validate peer config"), err)
}
w.Lock()
defer w.Unlock()
w.ifce.Peers = append(w.ifce.Peers, peer.WireGuardPeerConfig)
uapiBuilder := NewUAPIBuilder().AddPeerConfig(peerCfg)
log.Debugf("uapiBuilder: %s", uapiBuilder.Build())
if err = w.wgDevice.IpcSet(uapiBuilder.Build()); err != nil {
return errors.Join(errors.New("add peer IpcSet error"), err)
}
return nil
}
// GenWGConfig implements WireGuard.
func (w *wireGuard) GenWGConfig() (string, error) {
panic("unimplemented")
}
// GetIfceConfig implements WireGuard.
func (w *wireGuard) GetIfceConfig() (*defs.WireGuardConfig, error) {
w.RLock()
defer w.RUnlock()
return w.ifce, nil
}
// GetBaseIfceConfig implements WireGuard.
func (w *wireGuard) GetBaseIfceConfig() *defs.WireGuardConfig {
return w.ifce
}
// GetPeer implements WireGuard.
func (w *wireGuard) GetPeer(peerNameOrPk string) (*defs.WireGuardPeerConfig, error) {
w.RLock()
defer w.RUnlock()
for _, p := range w.ifce.Peers {
if p.ClientId == peerNameOrPk || p.PublicKey == peerNameOrPk {
return &defs.WireGuardPeerConfig{WireGuardPeerConfig: p}, nil
}
}
return nil, errors.New("peer not found")
}
// ListPeers implements WireGuard.
func (w *wireGuard) ListPeers() ([]*defs.WireGuardPeerConfig, error) {
w.RLock()
defer w.RUnlock()
return w.ifce.GetParsedPeers(), nil
}
// RemovePeer implements WireGuard.
func (w *wireGuard) RemovePeer(peerNameOrPk string) error {
log := w.svcLogger.WithField("op", "RemovePeer")
w.Lock()
defer w.Unlock()
newPeers := []*pb.WireGuardPeerConfig{}
var peerToRemove *defs.WireGuardPeerConfig
for _, p := range w.ifce.Peers {
if p.ClientId != peerNameOrPk && p.PublicKey != peerNameOrPk {
newPeers = append(newPeers, p)
continue
}
peerToRemove = &defs.WireGuardPeerConfig{WireGuardPeerConfig: p}
}
if len(newPeers) == len(w.ifce.Peers) {
return errors.New("peer not found")
}
w.ifce.Peers = newPeers
uapiBuilder := NewUAPIBuilder().RemovePeerByKey(peerToRemove.GetParsedPublicKey())
log.Debugf("uapiBuilder: %s", uapiBuilder.Build())
if err := w.wgDevice.IpcSet(uapiBuilder.Build()); err != nil {
return errors.Join(errors.New("remove peer IpcSet error"), err)
}
return nil
}
// UpdatePeer implements WireGuard.
func (w *wireGuard) UpdatePeer(peer *defs.WireGuardPeerConfig) error {
log := w.svcLogger.WithField("op", "UpdatePeer")
peerCfg, err := parseAndValidatePeerConfig(peer)
if err != nil {
return errors.Join(errors.New("parse and validate peer config"), err)
}
w.Lock()
defer w.Unlock()
newPeers := []*pb.WireGuardPeerConfig{}
for _, p := range w.ifce.Peers {
if p.ClientId != peer.ClientId && p.PublicKey != peer.PublicKey {
newPeers = append(newPeers, p)
continue
}
newPeers = append(newPeers, peer.WireGuardPeerConfig)
}
w.ifce.Peers = newPeers
uapiBuilder := NewUAPIBuilder().UpdatePeerConfig(peerCfg)
log.Debugf("uapiBuilder: %s", uapiBuilder.Build())
if err := w.wgDevice.IpcSet(uapiBuilder.Build()); err != nil {
return errors.Join(errors.New("update peer IpcSet error"), err)
}
return nil
}
func (w *wireGuard) PatchPeers(newPeers []*defs.WireGuardPeerConfig) (*app.WireGuardDiffPeersResponse, error) {
oldPeers := w.ifce.GetParsedPeers()
diffResp := utils.Diff(oldPeers, newPeers)
resp := &app.WireGuardDiffPeersResponse{
AddPeers: diffResp.NotInArr1,
RemovePeers: diffResp.NotInArr2,
}
for _, peer := range resp.RemovePeers {
w.RemovePeer(peer.GetPublicKey())
}
for _, peer := range resp.AddPeers {
w.AddPeer(peer)
}
return resp, nil
}
func (w *wireGuard) GetWGRuntimeInfo() (*pb.WGDeviceRuntimeInfo, error) {
runningInfo, err := w.wgDevice.IpcGet()
if err != nil {
return nil, errors.Join(errors.New("get WG running info error"), err)
}
runtimeInfo, err := ParseWGRunningInfo(runningInfo)
if err != nil {
return nil, err
}
runtimeInfo.PingMap = w.pingMap.Export()
runtimeInfo.InterfaceName = w.ifce.GetInterfaceName()
return runtimeInfo, nil
}
func (w *wireGuard) initWGDevice() error {
log := w.svcLogger.WithField("op", "initWGDevice")
log.Debugf("start to create TUN device '%s' (MTU %d)", w.ifce.GetInterfaceName(), w.ifce.GetInterfaceMtu())
var err error
w.tunDevice, err = tun.CreateTUN(w.ifce.GetInterfaceName(), int(w.ifce.GetInterfaceMtu()))
if err != nil {
return errors.Join(fmt.Errorf("create TUN device '%s' (MTU %d) failed", w.ifce.GetInterfaceName(), w.ifce.GetInterfaceMtu()), err)
}
log.Debugf("TUN device '%s' (MTU %d) created successfully", w.ifce.GetInterfaceName(), w.ifce.GetInterfaceMtu())
log.Debugf("start to create WireGuard device '%s'", w.ifce.GetInterfaceName())
w.wgDevice = device.NewDevice(w.tunDevice, conn.NewDefaultBind(), &device.Logger{
Verbosef: w.svcLogger.WithField("wg-dev-iface", w.ifce.GetInterfaceName()).Debugf,
Errorf: w.svcLogger.WithField("wg-dev-iface", w.ifce.GetInterfaceName()).Errorf,
})
log.Debugf("WireGuard device '%s' created successfully", w.ifce.GetInterfaceName())
return nil
}
func (w *wireGuard) applyPeerConfig() error {
log := w.svcLogger.WithField("op", "applyConfig")
log.Debugf("start to apply config to WireGuard device '%s'", w.ifce.GetInterfaceName())
if w.wgDevice == nil {
return errors.New("wgDevice is nil, please init WG device first")
}
wgTypedPeerConfigs, err := parseAndValidatePeerConfigs(w.ifce.GetParsedPeers())
if err != nil {
return errors.Join(errors.New("parse/validate peers"), err)
}
uapiConfigString := generateUAPIConfigString(w.ifce, w.ifce.GetParsedPrivKey(), wgTypedPeerConfigs, !w.running)
log.Debugf("uapiBuilder: %s", uapiConfigString)
if err = w.wgDevice.IpcSet(uapiConfigString); err != nil {
return errors.Join(errors.New("IpcSet error"), err)
}
return nil
}
func (w *wireGuard) initNetwork() error {
log := w.svcLogger.WithField("op", "initNetwork")
link, err := netlink.LinkByName(w.ifce.GetInterfaceName())
if err != nil {
return errors.Join(fmt.Errorf("get iface '%s' via netlink", w.ifce.GetInterfaceName()), err)
}
addr, err := netlink.ParseAddr(w.ifce.GetLocalAddress())
if err != nil {
return errors.Join(fmt.Errorf("parse local addr '%s' for netlink", w.ifce.GetLocalAddress()), err)
}
if err = netlink.AddrAdd(link, addr); err != nil && !os.IsExist(err) {
return errors.Join(fmt.Errorf("add IP '%s' to '%s'", w.ifce.GetLocalAddress(), w.ifce.GetInterfaceName()), err)
} else if os.IsExist(err) {
log.Infof("IP %s already on '%s'.", w.ifce.GetLocalAddress(), w.ifce.GetInterfaceName())
} else {
log.Infof("IP %s added to '%s'.", w.ifce.GetLocalAddress(), w.ifce.GetInterfaceName())
}
if err = netlink.LinkSetMTU(link, int(w.ifce.GetInterfaceMtu())); err != nil {
log.Warnf("Set MTU %d on '%s' via netlink: %v. TUN MTU is %d.",
w.ifce.GetInterfaceMtu(), w.ifce.GetInterfaceName(), err, w.ifce.GetInterfaceMtu())
} else {
log.Infof("Iface '%s' MTU %d set via netlink.", w.ifce.GetInterfaceName(), w.ifce.GetInterfaceMtu())
}
if err = netlink.LinkSetUp(link); err != nil {
return errors.Join(fmt.Errorf("bring up iface '%s' via netlink", w.ifce.GetInterfaceName()), err)
}
log.Infof("Iface '%s' up via netlink.", w.ifce.GetInterfaceName())
return nil
}
func (w *wireGuard) cleanupNetwork() {
log := w.svcLogger.WithField("op", "cleanupNetwork")
link, err := netlink.LinkByName(w.ifce.GetInterfaceName())
if err == nil {
if err := netlink.LinkSetDown(link); err != nil {
log.Warnf("Failed to LinkSetDown '%s' after wgDevice.Up() error: %v", w.ifce.GetInterfaceName(), err)
}
}
log.Debug("Cleanup network complete.")
}
func (w *wireGuard) cleanupWGDevice() {
log := w.svcLogger.WithField("op", "cleanupWGDevice")
if w.wgDevice != nil {
w.wgDevice.Close()
} else if w.tunDevice != nil {
w.tunDevice.Close()
}
w.wgDevice = nil
w.tunDevice = nil
log.Debug("Cleanup WG device complete.")
}
func (w *wireGuard) reportStatusTask() {
for {
select {
case <-w.ctx.Done():
return
default:
w.pingPeers()
time.Sleep(ReportInterval)
}
}
}
func (w *wireGuard) pingPeers() {
log := w.svcLogger.WithField("op", "pingPeers")
ifceConfig, err := w.GetIfceConfig()
if err != nil {
log.WithError(err).Errorf("failed to get interface config")
return
}
peers := ifceConfig.Peers
var waitGroup conc.WaitGroup
for _, peer := range peers {
addr := ""
if peer.Endpoint != nil && peer.Endpoint.Host != "" {
addr = peer.Endpoint.Host
}
if addr == "" {
continue
}
pinger, err := probing.NewPinger(addr)
if err != nil {
log.WithError(err).Errorf("failed to create pinger for %s", addr)
return
}
pinger.Count = 5
pinger.OnFinish = func(stats *probing.Statistics) {
// stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss
// stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt
if w.pingMap != nil {
log.Tracef("ping stats for %s: %v", addr, stats)
avgRttMs := uint32(stats.AvgRtt.Milliseconds())
if avgRttMs == 0 { // 0 means bug
avgRttMs = 1
}
w.pingMap.Store(peer.Id, avgRttMs)
}
}
pinger.OnRecv = func(pkt *probing.Packet) {
log.Tracef("recv from %s", pkt.IPAddr.String())
}
waitGroup.Go(func() {
if err := pinger.Run(); err != nil {
log.WithError(err).Errorf("failed to run pinger for %s", addr)
return
}
})
}
rcs := waitGroup.WaitAndRecover()
if rcs != nil {
log.WithError(rcs.AsError()).Errorf("failed to wait for pingers")
}
}
+142
View File
@@ -0,0 +1,142 @@
package wg
import (
"context"
"errors"
"fmt"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils"
)
var (
_ app.WireGuardManager = (*wireGuardManager)(nil)
)
type wireGuardManager struct {
services *utils.SyncMap[string, app.WireGuard]
originCtx *app.Context
svcCtx *app.Context
cancel context.CancelFunc
}
func NewWireGuardManager(appInstance app.Application) app.WireGuardManager {
ctx := app.NewContext(context.Background(), appInstance)
svcCtx, cancel := ctx.CopyWithCancel()
return &wireGuardManager{
services: &utils.SyncMap[string, app.WireGuard]{},
originCtx: ctx,
svcCtx: svcCtx,
cancel: cancel,
}
}
func (m *wireGuardManager) CreateService(cfg *defs.WireGuardConfig) (app.WireGuard, error) {
if cfg == nil {
return nil, errors.New("wireguard config is nil")
}
if cfg.GetInterfaceName() == "" {
return nil, errors.New("wireguard config interface name is empty")
}
ctx := m.svcCtx
wg, err := NewWireGuard(ctx, *cfg, ctx.Logger().WithField("interface", cfg.GetInterfaceName()))
if err != nil {
return nil, errors.Join(errors.New("wireguard manager create wireguard error"), err)
}
m.services.Store(cfg.GetInterfaceName(), wg)
return wg, nil
}
func (m *wireGuardManager) StartService(interfaceName string) error {
wg, ok := m.services.Load(interfaceName)
if !ok {
return fmt.Errorf("wireguard service not found, interfaceName: %s", interfaceName)
}
if err := wg.Start(); err != nil {
return errors.Join(errors.New("wireguard manager start wireguard error"), err)
}
return nil
}
func (m *wireGuardManager) GetService(interfaceName string) (app.WireGuard, bool) {
return m.services.Load(interfaceName)
}
func (m *wireGuardManager) GetAllServices() []app.WireGuard {
return m.services.Values()
}
func (m *wireGuardManager) StopService(interfaceName string) error {
wg, ok := m.services.Load(interfaceName)
if !ok {
return fmt.Errorf("wireguard service not found, interfaceName: %s", interfaceName)
}
if err := wg.Stop(); err != nil {
return errors.Join(errors.New("wireguard manager stop wireguard error"), err)
}
return nil
}
func (m *wireGuardManager) RemoveService(interfaceName string) error {
wg, ok := m.services.Load(interfaceName)
if !ok {
return fmt.Errorf("wireguard service not found, interfaceName: %s", interfaceName)
}
if err := wg.Stop(); err != nil {
return errors.Join(errors.New("wireguard manager remove wireguard error"), err)
}
m.services.Delete(interfaceName)
return nil
}
func (m *wireGuardManager) StopAllServices() map[string]error {
errMap := make(map[string]error)
m.services.Range(func(k string, v app.WireGuard) bool {
if err := v.Stop(); err != nil {
m.svcCtx.Logger().WithError(err).Errorf("wireguard manager stop all wireguard error, interfaceName: %s", k)
errMap[k] = err
return false
}
m.services.Delete(k)
return true
})
m.cancel()
m.svcCtx, m.cancel = m.originCtx.BackgroundWithCancel()
return errMap
}
func (m *wireGuardManager) RestartService(interfaceName string) error {
wg, ok := m.services.Load(interfaceName)
if !ok {
return fmt.Errorf("wireguard service not found, interfaceName: %s", interfaceName)
}
if err := wg.Stop(); err != nil {
return errors.Join(errors.New("wireguard manager restart wireguard error"), err)
}
if err := wg.Start(); err != nil {
return errors.Join(errors.New("wireguard manager restart wireguard error"), err)
}
return nil
}
func (m *wireGuardManager) Start() {
<-m.svcCtx.Done()
}
func (m *wireGuardManager) Stop() {
m.StopAllServices()
}
+45
View File
@@ -0,0 +1,45 @@
package utils
type Equaler[E any] interface {
Equal(E) bool
}
// DiffResult 结构体存储了两个切片比较后的差异结果。
type DiffResult[T any] struct {
NotInArr1 []T // 存在于 arr2 中但不在 arr1 中的元素 (arr2 - arr1)
NotInArr2 []T // 存在于 arr1 中但不在 arr2 中的元素 (arr1 - arr2)
InBoth []T // 同时存在于 arr1 和 arr2 中的元素 (arr1 ∩ arr2)
}
func Diff[T Equaler[T]](arr1, arr2 []T) DiffResult[T] {
result := DiffResult[T]{
NotInArr1: make([]T, 0),
NotInArr2: make([]T, 0),
InBoth: make([]T, 0),
}
matchedInArr2 := make([]bool, len(arr2))
for _, item1 := range arr1 {
foundInArr2 := false
for i, item2 := range arr2 {
if !matchedInArr2[i] && item1.Equal(item2) {
result.InBoth = append(result.InBoth, item1)
matchedInArr2[i] = true
foundInArr2 = true
break
}
}
if !foundInArr2 {
result.NotInArr2 = append(result.NotInArr2, item1)
}
}
for i, item2 := range arr2 {
if !matchedInArr2[i] {
result.NotInArr1 = append(result.NotInArr1, item2)
}
}
return result
}
+110
View File
@@ -2,7 +2,9 @@ package utils
import (
"fmt"
"math/big"
"net"
"strings"
)
// GetLocalIPv4s 返回本地所有活跃网络接口的 IPv4 地址列表。
@@ -50,3 +52,111 @@ func GetLocalIPv4s() ([]net.IP, error) {
return ips, nil
}
// nextIP returns ip + 1
func nextIP(ip net.IP) net.IP {
ip = ip.To4()
ipInt := big.NewInt(0).SetBytes(ip)
ipInt.Add(ipInt, big.NewInt(1))
return net.IP(ipInt.FillBytes(make([]byte, 4)))
}
// ipLE returns true if ip1 ≤ ip2
func ipLE(ip1, ip2 net.IP) bool {
a := big.NewInt(0).SetBytes(ip1.To4())
b := big.NewInt(0).SetBytes(ip2.To4())
return a.Cmp(b) <= 0
}
// parseNetwork parses CIDR and returns network IP and broadcast IP
func parseNetwork(cidr string) (net.IP, net.IP, error) {
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, nil, fmt.Errorf("invalid network CIDR %q: %w", cidr, err)
}
network := ip.Mask(ipnet.Mask)
ones, bits := ipnet.Mask.Size()
total := big.NewInt(1)
total.Lsh(total, uint(bits-ones))
// broadcast = network + total - 1
bcastInt := big.NewInt(0).SetBytes(network.To4())
bcastInt.Add(bcastInt, big.NewInt(0).Sub(total, big.NewInt(1)))
bcast := net.IP(bcastInt.FillBytes(make([]byte, 4)))
return network, bcast, nil
}
// buildUsedSet builds a set of used IP strings.
// If an entry is CIDR, only its base IP is used.
func buildUsedSet(allocated []string) (map[string]bool, error) {
used := make(map[string]bool)
for _, a := range allocated {
ipStr := a
if idx := strings.Index(a, "/"); idx != -1 {
ipStr = a[:idx]
}
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid allocated IP %q", a)
}
used[ip.String()] = true
}
return used, nil
}
// IsIPFree checks whether ipStr is free in the network given allocated list.
// allocated entries with CIDR ignore the mask (only base IP).
func IsIPFree(networkCidr string, allocated []string, ipStr string) (bool, error) {
network, bcast, err := parseNetwork(networkCidr)
if err != nil {
return false, nil
}
ip := net.ParseIP(ipStr).To4()
if ip == nil {
return false, fmt.Errorf("invalid IP %q", ipStr)
}
// must be inside (network, broadcast)
if !ipLE(nextIP(network), ip) || !ipLE(ip, nextIP(bcast)) {
return false, nil
}
used, err := buildUsedSet(allocated)
if err != nil {
return false, err
}
if used[ip.String()] {
return false, nil
}
return true, nil
}
// AllocateIP tries to allocate desiredStr; if not free or empty, picks the first free IP in network.
func AllocateIP(networkCidr string, allocated []string, desiredStr string) (string, error) {
network, bcast, err := parseNetwork(networkCidr)
if err != nil {
return "", err
}
used, err := buildUsedSet(allocated)
if err != nil {
return "", err
}
// try desired
if desiredStr != "" {
free, err := IsIPFree(networkCidr, allocated, desiredStr)
if err != nil {
return "", err
}
if free {
return desiredStr, nil
}
}
// scan for first free
for ip := nextIP(network); ipLE(ip, nextIP(bcast)); ip = nextIP(ip) {
if ip.Equal(bcast) {
break
}
s := ip.String()
if !used[s] {
return s, nil
}
}
return "", fmt.Errorf("no available IP")
}
+15
View File
@@ -0,0 +1,15 @@
package utils_test
import (
"testing"
"github.com/VaalaCat/frp-panel/utils"
)
func TestAllocateIP(t *testing.T) {
ip, err := utils.AllocateIP("192.168.1.0/24", []string{"192.168.1.1/24"}, "192.168.1.1")
if err != nil {
t.Errorf("AllocateIP() failed: %v", err)
}
t.Errorf("AllocateIP() = %v", ip)
}
+13
View File
@@ -144,6 +144,19 @@ func (s *SyncMap[K, V]) Range(f func(key K, value V) (shouldContinue bool)) {
s.mu.Unlock()
}
func (s *SyncMap[K, V]) Export() map[K]V {
s.mu.Lock()
defer s.mu.Unlock()
m := make(map[K]V, len(s.m))
for k, v := range s.m {
m[k] = v
}
return m
}
// Len returns the count of values in the map.
func (s *SyncMap[K, V]) Len() (l int) {
s.mu.Lock()
+146
View File
@@ -0,0 +1,146 @@
import http from '@/api/http'
import { API_PATH } from '@/lib/consts'
import {
CreateNetworkRequest,
CreateNetworkResponse,
DeleteNetworkRequest,
DeleteNetworkResponse,
UpdateNetworkRequest,
UpdateNetworkResponse,
GetNetworkRequest,
GetNetworkResponse,
ListNetworksRequest,
ListNetworksResponse,
CreateEndpointRequest,
CreateEndpointResponse,
DeleteEndpointRequest,
DeleteEndpointResponse,
UpdateEndpointRequest,
UpdateEndpointResponse,
GetEndpointRequest,
GetEndpointResponse,
ListEndpointsRequest,
ListEndpointsResponse,
CreateWireGuardRequest,
CreateWireGuardResponse,
DeleteWireGuardRequest,
DeleteWireGuardResponse,
UpdateWireGuardRequest,
UpdateWireGuardResponse,
GetWireGuardRequest,
GetWireGuardResponse,
ListWireGuardsRequest,
ListWireGuardsResponse,
GetWireGuardRuntimeInfoRequest,
GetWireGuardRuntimeInfoResponse,
GetNetworkTopologyRequest,
GetNetworkTopologyResponse,
CreateWireGuardLinkRequest,
CreateWireGuardLinkResponse,
DeleteWireGuardLinkRequest,
DeleteWireGuardLinkResponse,
UpdateWireGuardLinkRequest,
UpdateWireGuardLinkResponse,
GetWireGuardLinkRequest,
GetWireGuardLinkResponse,
ListWireGuardLinksRequest,
ListWireGuardLinksResponse,
} from '@/lib/pb/api_wg'
import { BaseResponse } from '@/types/api'
// Network
export const createNetwork = async (req: CreateNetworkRequest) => {
const res = await http.post(API_PATH + '/wg/network/create', CreateNetworkRequest.toJson(req))
return CreateNetworkResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteNetwork = async (req: DeleteNetworkRequest) => {
const res = await http.post(API_PATH + '/wg/network/delete', DeleteNetworkRequest.toJson(req))
return DeleteNetworkResponse.fromJson((res.data as BaseResponse).body)
}
export const updateNetwork = async (req: UpdateNetworkRequest) => {
const res = await http.post(API_PATH + '/wg/network/update', UpdateNetworkRequest.toJson(req))
return UpdateNetworkResponse.fromJson((res.data as BaseResponse).body)
}
export const getNetwork = async (req: GetNetworkRequest) => {
const res = await http.post(API_PATH + '/wg/network/get', GetNetworkRequest.toJson(req))
return GetNetworkResponse.fromJson((res.data as BaseResponse).body)
}
export const listNetworks = async (req: ListNetworksRequest) => {
const res = await http.post(API_PATH + '/wg/network/list', ListNetworksRequest.toJson(req))
return ListNetworksResponse.fromJson((res.data as BaseResponse).body)
}
export const getNetworkTopology = async (req: GetNetworkTopologyRequest) => {
const res = await http.post(API_PATH + '/wg/network/topology', GetNetworkTopologyRequest.toJson(req))
return GetNetworkTopologyResponse.fromJson((res.data as BaseResponse).body)
}
// Endpoint
export const createEndpoint = async (req: CreateEndpointRequest) => {
const res = await http.post(API_PATH + '/wg/endpoint/create', CreateEndpointRequest.toJson(req))
return CreateEndpointResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteEndpoint = async (req: DeleteEndpointRequest) => {
const res = await http.post(API_PATH + '/wg/endpoint/delete', DeleteEndpointRequest.toJson(req))
return DeleteEndpointResponse.fromJson((res.data as BaseResponse).body)
}
export const updateEndpoint = async (req: UpdateEndpointRequest) => {
const res = await http.post(API_PATH + '/wg/endpoint/update', UpdateEndpointRequest.toJson(req))
return UpdateEndpointResponse.fromJson((res.data as BaseResponse).body)
}
export const getEndpoint = async (req: GetEndpointRequest) => {
const res = await http.post(API_PATH + '/wg/endpoint/get', GetEndpointRequest.toJson(req))
return GetEndpointResponse.fromJson((res.data as BaseResponse).body)
}
export const listEndpoints = async (req: ListEndpointsRequest) => {
const res = await http.post(API_PATH + '/wg/endpoint/list', ListEndpointsRequest.toJson(req))
return ListEndpointsResponse.fromJson((res.data as BaseResponse).body)
}
// WireGuard
export const createWireGuard = async (req: CreateWireGuardRequest) => {
const res = await http.post(API_PATH + '/wg/create', CreateWireGuardRequest.toJson(req))
return CreateWireGuardResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteWireGuard = async (req: DeleteWireGuardRequest) => {
const res = await http.post(API_PATH + '/wg/delete', DeleteWireGuardRequest.toJson(req))
return DeleteWireGuardResponse.fromJson((res.data as BaseResponse).body)
}
export const updateWireGuard = async (req: UpdateWireGuardRequest) => {
const res = await http.post(API_PATH + '/wg/update', UpdateWireGuardRequest.toJson(req))
return UpdateWireGuardResponse.fromJson((res.data as BaseResponse).body)
}
export const getWireGuard = async (req: GetWireGuardRequest) => {
const res = await http.post(API_PATH + '/wg/get', GetWireGuardRequest.toJson(req))
return GetWireGuardResponse.fromJson((res.data as BaseResponse).body)
}
export const listWireGuards = async (req: ListWireGuardsRequest) => {
const res = await http.post(API_PATH + '/wg/list', ListWireGuardsRequest.toJson(req))
return ListWireGuardsResponse.fromJson((res.data as BaseResponse).body)
}
export const getWireGuardRuntime = async (req: GetWireGuardRuntimeInfoRequest) => {
const res = await http.post(API_PATH + '/wg/runtime/get', GetWireGuardRuntimeInfoRequest.toJson(req))
return GetWireGuardRuntimeInfoResponse.fromJson((res.data as BaseResponse).body)
}
// WireGuard Link
export const createWireGuardLink = async (req: CreateWireGuardLinkRequest) => {
const res = await http.post(API_PATH + '/wg/link/create', CreateWireGuardLinkRequest.toJson(req))
return CreateWireGuardLinkResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteWireGuardLink = async (req: DeleteWireGuardLinkRequest) => {
const res = await http.post(API_PATH + '/wg/link/delete', DeleteWireGuardLinkRequest.toJson(req))
return DeleteWireGuardLinkResponse.fromJson((res.data as BaseResponse).body)
}
export const updateWireGuardLink = async (req: UpdateWireGuardLinkRequest) => {
const res = await http.post(API_PATH + '/wg/link/update', UpdateWireGuardLinkRequest.toJson(req))
return UpdateWireGuardLinkResponse.fromJson((res.data as BaseResponse).body)
}
export const getWireGuardLink = async (req: GetWireGuardLinkRequest) => {
const res = await http.post(API_PATH + '/wg/link/get', GetWireGuardLinkRequest.toJson(req))
return GetWireGuardLinkResponse.fromJson((res.data as BaseResponse).body)
}
export const listWireGuardLinks = async (req: ListWireGuardLinksRequest) => {
const res = await http.post(API_PATH + '/wg/link/list', ListWireGuardLinksRequest.toJson(req))
return ListWireGuardLinksResponse.fromJson((res.data as BaseResponse).body)
}
+139
View File
@@ -0,0 +1,139 @@
'use client'
import React from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { listClient } from '@/api/client'
import { listServer } from '@/api/server'
import { useTranslation } from 'react-i18next'
import { Client, Server } from '@/lib/pb/common'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { useDebouncedCallback } from 'use-debounce'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { CaretSortIcon } from '@radix-ui/react-icons'
export type AgentType = 'client' | 'server'
export interface Agent {
id: string
label: string
type: AgentType
original: Client | Server
}
export interface AgentSelectorProps {
value?: Agent
onChange: (agent: Agent) => void
placeholder?: string
className?: string
}
export const AgentSelector: React.FC<AgentSelectorProps> = ({ value, onChange, placeholder, className }) => {
const { t } = useTranslation()
const [open, setOpen] = React.useState(false)
const [keyword, setKeyword] = React.useState('')
const debounced = useDebouncedCallback((v) => {
setKeyword(v as string)
}, 500)
// 获取客户端列表
const { data: clientList, refetch: refetchClients } = useQuery({
queryKey: ['listClient', keyword],
queryFn: () => {
return listClient({ page: 1, pageSize: 20, keyword: keyword })
},
placeholderData: keepPreviousData,
})
// 获取服务器列表
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ['listServer', keyword],
queryFn: () => {
return listServer({ page: 1, pageSize: 20, keyword: keyword })
},
placeholderData: keepPreviousData,
})
// 转换为统一的 Agent 格式
const clientAgents: Agent[] = React.useMemo(() => {
return (clientList?.clients || []).map((client) => ({
id: `client-${client.id}`,
label: client.id || '',
type: 'client' as AgentType,
original: client,
}))
}, [clientList])
const serverAgents: Agent[] = React.useMemo(() => {
return (serverList?.servers || []).map((server) => ({
id: `server-${server.id}`,
label: server.id || '',
type: 'server' as AgentType,
original: server,
}))
}, [serverList])
const handleSelect = (agent: Agent) => {
onChange(agent)
setOpen(false)
}
const handleOpenChange = (open: boolean) => {
setOpen(open)
if (open) {
refetchClients()
refetchServers()
}
}
const defaultPlaceholder = t('selector.common.placeholder')
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('w-full justify-between font-normal px-3', className, !value && 'text-muted-foreground')}
>
{value ? value.label : placeholder || defaultPlaceholder}
<CaretSortIcon className="h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
<Command>
<CommandInput onValueChange={(v) => debounced(v)} placeholder={placeholder || defaultPlaceholder} />
<CommandList>
<CommandEmpty>{t('selector.common.notFound')}</CommandEmpty>
{clientAgents.length > 0 && (
<CommandGroup heading={t('canvas.panel.clients')}>
{clientAgents.map((agent) => (
<CommandItem key={agent.id} value={agent.label} onSelect={() => handleSelect(agent)}>
{agent.label}
<Check className={cn('ml-auto', value?.id === agent.id ? 'opacity-100' : 'opacity-0')} />
</CommandItem>
))}
</CommandGroup>
)}
{serverAgents.length > 0 && (
<CommandGroup heading={t('canvas.panel.servers')}>
{serverAgents.map((agent) => (
<CommandItem key={agent.id} value={agent.label} onSelect={() => handleSelect(agent)}>
{agent.label}
<Check className={cn('ml-auto', value?.id === agent.id ? 'opacity-100' : 'opacity-0')} />
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
+14 -10
View File
@@ -25,21 +25,25 @@ interface DataTableProps<TData, TValue> {
data?: TData[]
filterColumnName?: string
table: TableType<TData>
toolbar?: React.ReactNode
}
export function DataTable<TData, TValue>({ columns, filterColumnName, table }: DataTableProps<TData, TValue>) {
export function DataTable<TData, TValue>({ columns, filterColumnName, table, toolbar }: DataTableProps<TData, TValue>) {
const { t } = useTranslation()
return (
<div>
{filterColumnName && (
<div className="flex flex-1 items-center py-4">
<Input
placeholder={t('table.filter.placeholder', { column: filterColumnName })}
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn(filterColumnName)?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className="space-y-4">
{(toolbar || filterColumnName) && (
<div className="flex flex-wrap items-center justify-between gap-3">
{filterColumnName ? (
<Input
placeholder={t('table.filter.placeholder', { column: filterColumnName })}
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn(filterColumnName)?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
) : null}
{toolbar && <div className="flex items-center gap-2">{toolbar}</div>}
</div>
)}
<div className="rounded-md border">
+48
View File
@@ -0,0 +1,48 @@
'use client'
import React from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { listEndpoints } from '@/api/wg'
import { Combobox } from './combobox'
import { useTranslation } from 'react-i18next'
export interface EndpointSelectorProps {
clientID: string
endpointID?: number
setEndpointID: (id?: number) => void
onOpenChange?: () => void
}
export const EndpointSelector: React.FC<EndpointSelectorProps> = ({ clientID, endpointID, setEndpointID, onOpenChange }) => {
const { t } = useTranslation()
const [keyword, setKeyword] = React.useState('')
const { data, refetch, isFetching } = useQuery({
queryKey: ['listEndpoints', clientID, keyword],
queryFn: () =>
listEndpoints({
page: 1,
pageSize: 50,
clientId: clientID,
keyword: keyword || undefined,
}),
placeholderData: keepPreviousData,
enabled: !!clientID,
})
const items = (data?.endpoints ?? []).map((e) => ({ value: String(e.id), label: `${e.host}:${e.port}` }))
return (
<Combobox
placeholder={t('wg.selector.endpoint') as string}
dataList={items}
value={endpointID ? String(endpointID) : ''}
setValue={(v) => setEndpointID(v ? Number(v) : undefined)}
onKeyWordChange={setKeyword}
onOpenChange={() => refetch()}
isLoading={isFetching}
/>
)
}
+11 -4
View File
@@ -11,6 +11,13 @@ export interface IdInputProps {
refetchTrigger?: (randStr: string) => void
}
const makeRandomKey = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}
export const IdInput: React.FC<IdInputProps> = ({ setKeyword, keyword, refetchTrigger }) => {
const { t } = useTranslation()
const [input, setInput] = useState(keyword)
@@ -18,16 +25,16 @@ export const IdInput: React.FC<IdInputProps> = ({ setKeyword, keyword, refetchTr
return (
<div className="flex flex-row gap-2 items-center">
<Input
className="text-sm"
defaultValue={keyword}
className="text-sm"
value={input}
placeholder={t('input.keyword.placeholder')}
onChange={(e) => setInput(e.target.value)}
/>
<Button
variant="outline"
variant="outline"
onClick={() => {
setKeyword(input)
refetchTrigger && refetchTrigger(JSON.stringify(Math.random()))
refetchTrigger?.(makeRandomKey())
}}
>
{t('input.search')}
+45
View File
@@ -0,0 +1,45 @@
'use client'
import React from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { listNetworks } from '@/api/wg'
import { Combobox } from './combobox'
import { useTranslation } from 'react-i18next'
export interface NetworkSelectorProps {
networkID?: number
setNetworkID: (id?: number) => void
onOpenChange?: () => void
}
export const NetworkSelector: React.FC<NetworkSelectorProps> = ({ networkID, setNetworkID, onOpenChange }) => {
const { t } = useTranslation()
const [keyword, setKeyword] = React.useState('')
const { data, refetch, isFetching } = useQuery({
queryKey: ['listNetworks', keyword],
queryFn: () =>
listNetworks({
page: 1,
pageSize: 20,
keyword: keyword || undefined,
}),
placeholderData: keepPreviousData,
})
const items = (data?.networks ?? []).map((n) => ({ value: String(n.id), label: `${n.name} (${n.cidr})` }))
return (
<Combobox
placeholder={t('wg.selector.network') as string}
dataList={items}
value={networkID ? String(networkID) : ''}
setValue={(v) => setNetworkID(v ? Number(v) : undefined)}
onKeyWordChange={setKeyword}
onOpenChange={() => refetch()}
isLoading={isFetching}
/>
)
}
+94 -86
View File
@@ -1,9 +1,9 @@
"use client"
'use client'
import { ClientStatus } from '@/lib/pb/api_master'
import { terminalWebsocketUrl } from '@/lib/terminal'
import { FitAddon } from '@xterm/addon-fit'
import { CanvasAddon } from "@xterm/addon-canvas";
import { Unicode11Addon } from "@xterm/addon-unicode11";
import { CanvasAddon } from '@xterm/addon-canvas'
import { Unicode11Addon } from '@xterm/addon-unicode11'
import { useEffect } from 'react'
import { useXTerm } from 'react-xtermjs'
@@ -11,12 +11,12 @@ export interface TerminalComponentProps {
isLoading: boolean
clientStatus?: ClientStatus
reset: number
setStatus: (status: "loading" | "success" | "error" | undefined) => void
setStatus: (status: 'loading' | 'success' | 'error' | undefined) => void
}
const TerminalComponent = ({ isLoading, clientStatus, reset, setStatus }: TerminalComponentProps) => {
const { instance: terminal, ref } = useXTerm()
const fitAddon = new FitAddon()
useEffect(() => {
@@ -27,96 +27,110 @@ const TerminalComponent = ({ isLoading, clientStatus, reset, setStatus }: Termin
useEffect(() => {
if (!terminal) {
return;
return
}
terminal.options.allowProposedApi = true
terminal.options.cursorStyle = "block"
terminal.loadAddon(new CanvasAddon());
terminal.options.allowProposedApi = true
terminal.options.cursorStyle = 'block'
terminal.loadAddon(new CanvasAddon())
terminal.loadAddon(fitAddon)
terminal.loadAddon(new Unicode11Addon());
terminal.unicode.activeVersion = "11";
terminal.loadAddon(new Unicode11Addon())
terminal.unicode.activeVersion = '11'
const handleResize = () => fitAddon.fit()
fitAddon.fit()
fitAddon.fit()
// 监听窗口大小改变
window.addEventListener('resize', handleResize)
// 监听容器大小改变
let resizeObserver: ResizeObserver | null = null
if (ref.current) {
resizeObserver = new ResizeObserver(() => {
// 使用 requestAnimationFrame 来避免性能问题
requestAnimationFrame(() => {
fitAddon.fit()
})
})
resizeObserver.observe(ref.current)
}
return () => {
window.removeEventListener('resize', handleResize)
if (resizeObserver) {
resizeObserver.disconnect()
}
}
}, [ref, terminal])
useEffect(() => {
if (!terminal) {
return;
return
}
// The terminal should be cleared on each reconnect
// because all data is re-rendered from the backend.
terminal.clear();
terminal.clear()
// Focusing on connection allows users to reload the page and start
// typing immediately.
terminal.focus();
terminal.focus()
// Disable input while we connect.
terminal.options.disableStdin = true;
terminal.options.disableStdin = true
// Show a message if we failed to find the workspace or agent.
if (isLoading) {
return;
return
}
if (!clientStatus) {
terminal.writeln(
`no client found with ID, is the program started?`,
);
setStatus("error");
return;
terminal.writeln(`no client found with ID, is the program started?`)
setStatus('error')
return
}
// Hook up terminal events to the websocket.
let websocket: WebSocket | null;
let websocket: WebSocket | null
const disposers = [
terminal.onData((data) => {
websocket?.send(
new TextEncoder().encode(JSON.stringify({ data: data })),
);
websocket?.send(new TextEncoder().encode(JSON.stringify({ data: data })))
}),
terminal.onResize((event) => {
websocket?.send(
new TextEncoder().encode(
JSON.stringify({
height: event.rows,
width: event.cols,
}),
),
);
try {
websocket?.send(
new TextEncoder().encode(
JSON.stringify({
height: event.rows,
width: event.cols,
}),
),
)
} catch (error) {
console.error('Failed to send resize event:', error)
}
}),
];
]
let disposed = false;
let disposed = false
// Open the web socket and hook it up to the terminal.
terminalWebsocketUrl(
clientStatus.clientId,
terminal.rows,
terminal.cols,
)
terminalWebsocketUrl(clientStatus.clientId, terminal.rows, terminal.cols)
.then((url) => {
if (disposed) {
return; // Unmounted while we waited for the async call.
return // Unmounted while we waited for the async call.
}
websocket = new WebSocket(url);
websocket.binaryType = "arraybuffer";
websocket.addEventListener("open", () => {
websocket = new WebSocket(url)
websocket.binaryType = 'arraybuffer'
websocket.addEventListener('open', () => {
// Now that we are connected, allow user input.
terminal.options = {
disableStdin: false,
windowsMode: clientStatus.version?.platform.includes("windows"),
};
windowsMode: clientStatus.version?.platform.includes('windows'),
}
// Send the initial size.
websocket?.send(
new TextEncoder().encode(
@@ -125,61 +139,55 @@ const TerminalComponent = ({ isLoading, clientStatus, reset, setStatus }: Termin
width: terminal.cols,
}),
),
);
});
websocket.addEventListener("error", () => {
terminal.options.disableStdin = true;
terminal.writeln(
`socket errored`,
);
setStatus("error");
});
websocket.addEventListener("close", () => {
terminal.options.disableStdin = true;
setStatus(undefined);
});
websocket.addEventListener("message", (event) => {
if (typeof event.data === "string") {
)
})
websocket.addEventListener('error', () => {
terminal.options.disableStdin = true
terminal.writeln(`socket errored`)
setStatus('error')
})
websocket.addEventListener('close', () => {
terminal.options.disableStdin = true
setStatus(undefined)
})
websocket.addEventListener('message', (event) => {
if (typeof event.data === 'string') {
// This exclusively occurs when testing.
// "jest-websocket-mock" doesn't support ArrayBuffer.
terminal.write(event.data);
terminal.write(event.data)
} else {
terminal.write(new Uint8Array(event.data));
terminal.write(new Uint8Array(event.data))
}
setStatus("success");
});
setStatus('success')
})
})
.catch((error) => {
setStatus("error");
setStatus('error')
if (disposed) {
return; // Unmounted while we waited for the async call.
return // Unmounted while we waited for the async call.
}
terminal.writeln(error.message);
});
terminal.writeln(error.message)
})
return () => {
disposed = true; // Could use AbortController instead?
disposed = true // Could use AbortController instead?
for (const d of disposers) {
d.dispose();
d.dispose()
}
websocket?.close(1000);
};
}, [
terminal,
isLoading,
setStatus,
]);
websocket?.close(1000)
}
}, [terminal, isLoading, setStatus])
return <div ref={ref} style={styles.terminal()} />
}
const styles = {
terminal: () => ({
width: "100%",
height: "100%",
overflow: "hidden",
flex: 1,
}),
terminal: () => ({
width: '100%',
height: '100%',
overflow: 'hidden',
flex: 1,
}),
}
export default TerminalComponent
export default TerminalComponent
+20 -3
View File
@@ -39,7 +39,7 @@ const LogTerminalComponent = ({ logs, reset }: { logs: string, reset: number })
}, [reset, ref, instance])
useEffect(() => {
// Load the fit addon
instance?.loadAddon(fitAddon)
@@ -47,14 +47,31 @@ const LogTerminalComponent = ({ logs, reset }: { logs: string, reset: number })
fitAddon.fit()
instance?.writeln(currentLine)
// Handle resize event
// 监听窗口大小改变
window.addEventListener('resize', handleResize)
// 监听容器大小改变
let resizeObserver: ResizeObserver | null = null
if (ref.current) {
resizeObserver = new ResizeObserver(() => {
// 使用 requestAnimationFrame 来避免性能问题
requestAnimationFrame(() => {
fitAddon.fit()
})
})
resizeObserver.observe(ref.current)
}
return () => {
window.removeEventListener('resize', handleResize)
if (resizeObserver) {
resizeObserver.disconnect()
}
}
}, [ref, instance, currentLine])
return <div ref={ref} style={{ height: '100%', width: '100%'}} />
return <div ref={ref} style={{ height: '100%', width: '100%' }} />
}
export default LogTerminalComponent
@@ -0,0 +1,71 @@
'use client'
import React, { useEffect } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { listWireGuards } from '@/api/wg'
import { Combobox } from './combobox'
import { useTranslation } from 'react-i18next'
export interface WireGuardSelectorProps {
clientID?: string
networkID?: number
wireguardID?: number
setWireguardID: (id?: number) => void
onOpenChange?: () => void
}
export const WireGuardSelector: React.FC<WireGuardSelectorProps> = ({ clientID, networkID, wireguardID, setWireguardID, onOpenChange }) => {
const { t } = useTranslation()
const [keyword, setKeyword] = React.useState('')
const [valueKey, setValueKey] = React.useState('')
const { data, refetch, isFetching } = useQuery({
queryKey: ['listWireGuards', clientID, networkID, keyword],
queryFn: () =>
listWireGuards({
page: 1,
pageSize: 50,
clientId: clientID,
networkId: networkID,
keyword: keyword || undefined,
}),
placeholderData: keepPreviousData,
})
const items = (data?.wireguardConfigs ?? []).map((w) => ({
value: `${w.id ?? ''}`,
label: `${w.clientId ?? ''} ${w.localAddress ? `(${w.localAddress})` : ''}`.trim(),
}))
useEffect(() => {
if (wireguardID && data?.wireguardConfigs) {
const target = data.wireguardConfigs.find((w) => w.id === wireguardID)
if (target) {
setValueKey(String(target.id))
}
}
}, [wireguardID, data?.wireguardConfigs])
useEffect(() => {
if (valueKey) {
setWireguardID(Number(valueKey))
} else {
setWireguardID(undefined)
}
}, [valueKey, setWireguardID])
return (
<Combobox
placeholder={t('wg.selector.clientWireguards') as string}
dataList={items}
value={valueKey}
setValue={(v) => setValueKey(v)}
onKeyWordChange={setKeyword}
onOpenChange={() => refetch()}
isLoading={isFetching}
/>
)
}
+227
View File
@@ -0,0 +1,227 @@
'use client'
import React, { useCallback, useRef } from 'react'
import {
applyNodeChanges,
Background,
Controls,
MiniMap,
ReactFlow,
NodeChange,
ReactFlowInstance,
} from '@xyflow/react'
import type { CanvasData, CanvasNode } from './types'
import ClientNode from './ClientNode'
import ServerNode from './ServerNode'
import TerminalNode from './TerminalNode'
import LogNode from './LogNode'
import { nanoid } from 'nanoid'
export interface CanvasProps {
data: CanvasData
setNodes: React.Dispatch<React.SetStateAction<CanvasNode[]>>
onDeleteNode?: (nodeId: string) => void
focusNodeId?: string
onFocusComplete?: () => void
fullscreen?: boolean
}
export default function Canvas({
data,
setNodes,
onDeleteNode,
focusNodeId,
onFocusComplete,
fullscreen = false,
}: CanvasProps) {
const reactFlowInstance = useRef<ReactFlowInstance | null>(null)
const onNodesChange = useCallback(
(changes: NodeChange[]) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot as any) as any),
[setNodes],
)
const onInit = useCallback((instance: any) => {
reactFlowInstance.current = instance
}, [])
// 聚焦到指定节点
React.useEffect(() => {
if (focusNodeId && reactFlowInstance.current) {
const node = reactFlowInstance.current.getNodes().find((n) => n.id === focusNodeId)
if (node) {
reactFlowInstance.current.setCenter(
node.position.x + (node.width || 200) / 2,
node.position.y + (node.height || 100) / 2,
)
// 高亮节点
reactFlowInstance.current.setNodes((nds: any) =>
nds.map((n: any) => ({
...n,
selected: n.id === focusNodeId,
})),
)
onFocusComplete?.()
}
}
}, [focusNodeId, onFocusComplete])
const handleOpenTerminal = useCallback(
(clientId: string, clientType: number, sourceNodeId?: string) => {
if (!reactFlowInstance.current) return
const instance = reactFlowInstance.current
// 找到源节点的位置
let position = { x: 0, y: 0 }
if (sourceNodeId) {
const sourceNode = instance.getNodes().find((n: any) => n.id === sourceNodeId)
if (sourceNode) {
// 在源节点右侧创建终端节点
position = {
x: sourceNode.position.x + (sourceNode.width || 200) + 50,
y: sourceNode.position.y,
}
}
}
// 如果找不到源节点,使用画布中心
if (position.x === 0 && position.y === 0) {
const bounds = document.querySelector('.react-flow__viewport')?.getBoundingClientRect()
const centerX = bounds ? bounds.width / 2 : 400
const centerY = bounds ? bounds.height / 2 : 300
position = instance.screenToFlowPosition({
x: centerX,
y: centerY,
})
}
const newNode = {
id: `terminal-${nanoid()}`,
type: 'terminal',
position,
dragHandle: '.drag-handle',
style: { width: 650, height: 500 },
data: {
label: `${clientId}`,
clientId,
clientType,
},
}
// 添加新的终端节点
setNodes((nds) => [...nds, newNode as any])
},
[setNodes],
)
const handleOpenLog = useCallback(
(clientId: string, clientType: number, sourceNodeId?: string) => {
if (!reactFlowInstance.current) return
const instance = reactFlowInstance.current
let position = { x: 0, y: 0 }
if (sourceNodeId) {
const sourceNode = instance.getNodes().find((n: any) => n.id === sourceNodeId)
if (sourceNode) {
position = {
x: sourceNode.position.x + (sourceNode.width || 200) + 50,
y: sourceNode.position.y + 200,
}
}
}
if (position.x === 0 && position.y === 0) {
const viewport = instance.getViewport()
const bounds = document.querySelector('.react-flow__viewport')?.getBoundingClientRect()
const centerX = bounds ? bounds.width / 2 : 400
const centerY = bounds ? bounds.height / 2 : 400
position = instance.screenToFlowPosition({
x: centerX,
y: centerY,
})
}
const newNode = {
id: `log-${nanoid()}`,
type: 'log',
position,
dragHandle: '.drag-handle',
style: { width: 650, height: 500 },
data: {
label: `Log - ${clientId}`,
clientId,
clientType,
minimized: false,
pkgs: ['all'],
},
}
setNodes((nds) => [...nds, newNode as any])
},
[setNodes],
)
const handleDeleteNode = useCallback(
(nodeId: string) => {
setNodes((nds) => nds.filter((n) => n.id !== nodeId))
onDeleteNode?.(nodeId)
},
[setNodes, onDeleteNode],
)
// 创建自定义节点类型的工厂函数,注入回调
const nodeTypes = React.useMemo(
() => ({
client: (props: any) => (
<ClientNode
{...props}
onOpenTerminal={(clientId: string, clientType: number) => handleOpenTerminal(clientId, clientType, props.id)}
onOpenLog={(clientId: string, clientType: number) => handleOpenLog(clientId, clientType, props.id)}
/>
),
server: (props: any) => (
<ServerNode
{...props}
onOpenTerminal={(clientId: string, clientType: number) => handleOpenTerminal(clientId, clientType, props.id)}
onOpenLog={(clientId: string, clientType: number) => handleOpenLog(clientId, clientType, props.id)}
/>
),
terminal: (props: any) => <TerminalNode {...props} onDelete={handleDeleteNode} />,
log: (props: any) => <LogNode {...props} onDelete={handleDeleteNode} />,
}),
[handleOpenTerminal, handleOpenLog, handleDeleteNode],
)
const canvasClassName = fullscreen ? 'h-full w-full' : 'w-full border rounded-lg overflow-hidden shadow-sm'
const canvasStyle = fullscreen ? undefined : { height: '80dvh' }
return (
<div className={canvasClassName} style={canvasStyle}>
<ReactFlow
nodes={data.nodes}
edges={[]}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onInit={onInit}
fitView
minZoom={0.1}
maxZoom={2}
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
>
<MiniMap nodeStrokeWidth={3} zoomable pannable />
<Controls />
<Background gap={12} size={1} />
</ReactFlow>
</div>
)
}
+160
View File
@@ -0,0 +1,160 @@
'use client'
import React, { useCallback, useState } from 'react'
import { Button } from '@/components/ui/button'
import Canvas from './Canvas'
import type { CanvasNode, ClientNode, ServerNode } from './types'
import { useTranslation } from 'react-i18next'
import { Maximize2, Minimize2, Plus, Trash2 } from 'lucide-react'
import { ReactFlowProvider } from '@xyflow/react'
import { AgentSelector, type Agent } from '@/components/base/agent-selector'
import { Client, Server } from '@/lib/pb/common'
export default function CanvasPanel() {
const { t } = useTranslation()
const [fullscreen, setFullscreen] = useState(false)
const [selectedAgent, setSelectedAgent] = useState<Agent | undefined>()
const [focusNodeId, setFocusNodeId] = useState<string | undefined>()
const [nodes, setNodes] = useState<CanvasNode[]>([])
const [agentNodeCount, setAgentNodeCount] = useState(0)
const handleAddAgent = useCallback(() => {
if (!selectedAgent) return
const nodeId =
selectedAgent.type === 'client'
? `client-${(selectedAgent.original as Client).id}`
: `server-${(selectedAgent.original as Server).id}`
// 检查是否已存在
if (nodes.some((n) => n.id === nodeId)) {
// 如果已存在,聚焦到该节点
setFocusNodeId(nodeId)
setSelectedAgent(undefined)
return
}
// 根据类型创建节点
if (selectedAgent.type === 'client') {
const client = selectedAgent.original as Client
const newNode: ClientNode = {
id: nodeId,
type: 'client',
dragHandle: '.drag-handle',
data: {
label: client.id || 'Client',
original: client,
},
position: {
x: 0,
y: agentNodeCount * 100 + 100,
},
}
setNodes((prev) => [...prev, newNode])
} else {
const server = selectedAgent.original as Server
const newNode: ServerNode = {
id: nodeId,
type: 'server',
dragHandle: '.drag-handle',
data: {
label: server.id || 'Server',
original: server,
status: 'offline',
},
position: {
x: 0,
y: agentNodeCount * 100 + 100,
},
}
setNodes((prev) => [...prev, newNode])
}
setAgentNodeCount(agentNodeCount + 1)
// 聚焦到新节点
setFocusNodeId(nodeId)
// 清空选择
setSelectedAgent(undefined)
}, [selectedAgent, nodes, setNodes, setFocusNodeId, setSelectedAgent, agentNodeCount, setAgentNodeCount])
const handleClearCanvas = () => {
if (confirm(t('canvas.panel.confirmClear'))) {
// 只保留 client 和 server 节点
setNodes(nodes.filter((n) => n.type === 'client' || n.type === 'server'))
setAgentNodeCount(nodes.filter((n) => n.type === 'client' || n.type === 'server').length)
}
}
return (
<ReactFlowProvider>
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold">{t('canvas.panel.title')}</h2>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleClearCanvas} title={t('canvas.panel.clear')}>
<Trash2 className="h-4 w-4 mr-2" />
{t('canvas.panel.clear')}
</Button>
<Button
variant="outline"
size="icon"
onClick={() => setFullscreen(!fullscreen)}
className="hidden md:flex"
title={fullscreen ? t('canvas.panel.exitFullscreen') : t('canvas.panel.fullscreen')}
>
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="flex flex-wrap gap-4 p-3 bg-muted/50 rounded-lg items-center">
<div className="flex items-center gap-2 flex-1 min-w-[300px] max-w-[500px]">
<AgentSelector
value={selectedAgent}
onChange={setSelectedAgent}
placeholder={t('canvas.panel.selectAgent')}
className="flex-1"
/>
<Button variant="default" size="sm" onClick={handleAddAgent} disabled={!selectedAgent}>
<Plus className="h-4 w-4 mr-2" />
{t('canvas.panel.addNode')}
</Button>
</div>
<div className="flex-1"></div>
<div className="text-sm text-muted-foreground">
{t('canvas.panel.nodeCount')}: {agentNodeCount}
</div>
</div>
<div className={fullscreen ? 'fixed inset-0 z-50 bg-background flex flex-col' : ''}>
{fullscreen && (
<div className="flex items-center justify-end p-3 border-b">
<Button
variant="outline"
size="icon"
onClick={() => setFullscreen(false)}
title={t('canvas.panel.exitFullscreen')}
>
<Minimize2 className="h-4 w-4" />
</Button>
</div>
)}
<div className={fullscreen ? 'flex-1 min-h-0' : ''}>
<Canvas
data={{ nodes }}
setNodes={setNodes}
focusNodeId={focusNodeId}
onFocusComplete={() => setFocusNodeId(undefined)}
fullscreen={fullscreen}
/>
</div>
</div>
</div>
</ReactFlowProvider>
)
}
+144
View File
@@ -0,0 +1,144 @@
'use client'
import React from 'react'
import type { NodeProps } from '@xyflow/react'
import type { ClientNode, NodeOperations } from './types'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import { getClientsStatus } from '@/api/platform'
import { ClientType } from '@/lib/pb/common'
import { ClientStatus_Status } from '@/lib/pb/api_master'
import { Badge } from '@/components/ui/badge'
import { Wifi, WifiOff, Terminal, FileText } from 'lucide-react'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
ContextMenuSeparator,
} from '@/components/ui/context-menu'
const ClientNodeComponent: React.FC<NodeProps<ClientNode> & NodeOperations> = ({
id,
data,
selected,
onOpenTerminal,
onOpenLog,
}) => {
const { t } = useTranslation()
const clientId = data.original?.id
const clientType = ClientType.FRPC
const handleOpenTerminal = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
event.preventDefault()
if (clientId) {
onOpenTerminal?.(clientId, clientType, id)
}
},
[clientId, clientType, id, onOpenTerminal],
)
const handleOpenLog = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
event.preventDefault()
if (clientId) {
onOpenLog?.(clientId, clientType, id)
}
},
[clientId, clientType, id, onOpenLog],
)
// 获取客户端状态
const { data: clientStatusData } = useQuery({
queryKey: ['clientStatus', clientId],
queryFn: async () => {
if (!clientId) return undefined
return await getClientsStatus({
clientIds: [clientId],
clientType: clientType,
})
},
enabled: !!clientId,
refetchInterval: 30000, // 30秒刷新一次
})
const clientStatus = clientStatusData?.clients[clientId || '']
const isOnline = clientStatus?.status === ClientStatus_Status.ONLINE
const ping = clientStatus?.ping
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div
className={cn('customNode', selected && 'ring-2 ring-primary/60 rounded-md')}
style={{ minWidth: 200, userSelect: 'none' }}
>
<div className="customNodeBody bg-card rounded-md border p-3 flex flex-col gap-2 text-sm shadow-md hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-2">
{isOnline ? (
<Wifi className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<WifiOff className="h-3.5 w-3.5 text-red-500 flex-shrink-0" />
)}
<span className="font-medium truncate text-blue-600" title={clientId}>
{clientId || t('canvas.client.unknown')}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
<span>FRPC</span>
{ping !== undefined && (
<Badge variant="secondary" className="text-[10px] h-4 px-1">
{ping}ms
</Badge>
)}
</div>
{data.original?.stopped && (
<Badge variant="destructive" className="text-[10px] h-4 px-1 mt-1 w-fit">
{t('canvas.client.stopped')}
</Badge>
)}
</div>
<div className="drag-handle cursor-grab active:cursor-grabbing p-1 hover:bg-muted rounded flex-shrink-0">
<div className="w-4 h-4 flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" className="text-muted-foreground">
<circle cx="2" cy="2" r="1.5" />
<circle cx="6" cy="2" r="1.5" />
<circle cx="10" cy="2" r="1.5" />
<circle cx="2" cy="6" r="1.5" />
<circle cx="6" cy="6" r="1.5" />
<circle cx="10" cy="6" r="1.5" />
<circle cx="2" cy="10" r="1.5" />
<circle cx="6" cy="10" r="1.5" />
<circle cx="10" cy="10" r="1.5" />
</svg>
</div>
</div>
</div>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuItem onClick={handleOpenTerminal} disabled={!clientId} className="gap-2">
<Terminal className="h-4 w-4" />
<span>{t('canvas.contextMenu.openTerminal')}</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleOpenLog} disabled={!clientId} className="gap-2">
<FileText className="h-4 w-4" />
<span>{t('canvas.contextMenu.openLog')}</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
export default ClientNodeComponent
+206
View File
@@ -0,0 +1,206 @@
'use client'
import React, { useState, useEffect } from 'react'
import type { NodeProps } from '@xyflow/react'
import { NodeResizer } from '@xyflow/react'
import type { LogNode, NodeOperations } from './types'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { FileText, Minimize2, Maximize2, X, PlayCircle, StopCircle, Eraser, Circle } from 'lucide-react'
import dynamic from 'next/dynamic'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ClientSelector } from '@/components/base/client-selector'
import { ServerSelector } from '@/components/base/server-selector'
import { Badge } from '@/components/ui/badge'
import { BaseSelector } from '@/components/base/selector'
import { ClientType } from '@/lib/pb/common'
import { parseStreaming } from '@/lib/stream'
import { ClientStatus_Status } from '@/lib/pb/api_master'
import { useQuery } from '@tanstack/react-query'
import { getClientsStatus } from '@/api/platform'
const LogTerminalComponent = dynamic(() => import('@/components/base/readonly-xterm'), {
ssr: false,
})
const LogNodeComponent: React.FC<NodeProps<LogNode> & NodeOperations> = ({ id, data, selected, onDelete }) => {
const { t } = useTranslation()
const [clientId, setClientId] = useState(data.clientId || '')
const [log, setLog] = useState<string | undefined>(undefined)
const [clear, setClear] = useState<number>(0)
const [enabled, setEnabled] = useState<boolean>(false)
const [status, setStatus] = useState<'loading' | 'success' | 'error' | undefined>()
const [pkgs, setPkgs] = useState<string[]>(data.pkgs || ['all'])
const isFrps = data.clientType === ClientType.FRPS
// 获取客户端状态
const { data: clientStatusData } = useQuery({
queryKey: ['clientStatus', clientId],
queryFn: async () => {
if (!clientId) return undefined
return await getClientsStatus({
clientIds: [clientId],
clientType: data.clientType,
})
},
enabled: !!clientId,
refetchInterval: 30000,
})
const clientStatus = clientStatusData?.clients[clientId]
const isOnline = clientStatus?.status === ClientStatus_Status.ONLINE
useEffect(() => {
setClear(Math.random())
setStatus(undefined)
if (!clientId || !enabled) {
return
}
const abortController = new AbortController()
setStatus('loading')
parseStreaming(
abortController,
clientId,
pkgs[0] === 'all' ? [] : pkgs,
setLog,
(status: number) => {
if (status === 200) {
setStatus('success')
} else {
setStatus('error')
}
},
() => {
setStatus('success')
},
)
return () => {
abortController.abort('unmount')
setEnabled(false)
}
}, [clientId, enabled, pkgs])
const handleDelete = () => {
onDelete?.(id)
}
const handleConnect = () => {
if (!clientId) return
setEnabled(!enabled)
}
return (
<div
className={cn('logNode', selected && 'ring-2 ring-primary/60 rounded-lg')}
style={{ userSelect: 'none', pointerEvents: 'all', width: '100%', height: '100%' }}
>
<NodeResizer isVisible={selected} minWidth={400} minHeight={320} maxWidth={1200} maxHeight={800} />
<Card className={cn('shadow-lg border-2 h-full flex flex-col', 'w-full')}>
<CardHeader
className="p-3 pb-2 flex flex-row items-center justify-between space-y-0 drag-handle"
style={{ cursor: 'move', userSelect: 'none' }}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<FileText className="h-4 w-4 flex-shrink-0" />
<CardTitle className="text-sm font-medium truncate">
{clientId ? `${clientId}` : t('canvas.log.noClient')}
</CardTitle>
{clientId && (
<Badge variant={isOnline ? 'default' : 'secondary'} className="text-[10px] h-4 px-1.5">
<Circle className={cn('h-2 w-2 mr-1', isOnline ? 'fill-green-500' : 'fill-gray-500')} />
{isOnline ? t('client.status_online') : t('client.status_offline')}
</Badge>
)}
{status && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
{status === 'success' && t('canvas.log.connected')}
{status === 'loading' && t('canvas.log.connecting')}
{status === 'error' && t('canvas.log.error')}
</Badge>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{clientId && (
<>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleConnect}
title={enabled ? t('canvas.log.stop') : t('canvas.log.start')}
>
{enabled ? (
<StopCircle className="h-3.5 w-3.5 text-red-500" />
) : (
<PlayCircle className="h-3.5 w-3.5 text-green-500" />
)}
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setClear(Math.random())}
title={t('canvas.log.clear')}
>
<Eraser className="h-3.5 w-3.5" />
</Button>
</>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive hover:text-destructive"
onClick={handleDelete}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
</CardHeader>
<CardContent className="p-3 pt-0 flex flex-col gap-2 nodrag flex-1 min-h-0">
<div className="flex gap-2 nodrag">
<div className="flex-1">
{isFrps ? (
<ServerSelector serverID={clientId} setServerID={setClientId} />
) : (
<ClientSelector clientID={clientId} setClientID={setClientId} />
)}
</div>
<div className="w-32">
<BaseSelector
dataList={[
{ value: 'all', label: 'all' },
{ value: 'frp', label: 'frp' },
{ value: 'workerd', label: 'workerd' },
]}
setValue={(value) => {
setPkgs([value])
}}
label={t('common.stream_log_pkgs_select')}
className="h-10"
/>
</div>
</div>
{clientId ? (
<div
className="flex-1 min-h-[200px] border rounded overflow-hidden bg-black nodrag"
style={{ userSelect: 'text' }}
>
<LogTerminalComponent logs={log || ''} reset={clear} />
</div>
) : (
<div className="flex-1 min-h-[200px] border rounded bg-muted flex items-center justify-center">
<p className="text-muted-foreground text-sm">{t('canvas.log.selectClient')}</p>
</div>
)}
</CardContent>
</Card>
</div>
)
}
export default LogNodeComponent
+135
View File
@@ -0,0 +1,135 @@
'use client'
import React from 'react'
import type { NodeProps } from '@xyflow/react'
import type { ServerNode, NodeOperations } from './types'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Server, Terminal, FileText } from 'lucide-react'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
ContextMenuSeparator,
} from '@/components/ui/context-menu'
import { ClientType } from '@/lib/pb/common'
import { useQuery } from '@tanstack/react-query'
import { getClientsStatus } from '@/api/platform'
import { ClientStatus_Status } from '@/lib/pb/api_master'
const ServerNodeComponent: React.FC<NodeProps<ServerNode> & NodeOperations> = ({
id,
data,
selected,
onOpenTerminal,
onOpenLog,
}) => {
const { t } = useTranslation()
const serverId = data.original?.id
const clientType = ClientType.FRPS
const handleOpenTerminal = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
event.preventDefault()
if (serverId) {
onOpenTerminal?.(serverId, clientType, id)
}
},
[serverId, clientType, id, onOpenTerminal],
)
const handleOpenLog = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
event.preventDefault()
if (serverId) {
onOpenLog?.(serverId, clientType, id)
}
},
[serverId, clientType, id, onOpenLog],
)
const { data: serverStatusData } = useQuery({
queryKey: ['serverStatus', serverId],
queryFn: async () => {
if (!serverId) return undefined
return await getClientsStatus({
clientIds: [serverId],
clientType: clientType,
})
},
enabled: !!serverId,
refetchInterval: 30000, // 30秒刷新一次
})
const serverStatus = serverStatusData?.clients[serverId || '']
const isOnline = serverStatus?.status === ClientStatus_Status.ONLINE
const ping = serverStatus?.ping
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div
className={cn('customNode', selected && 'ring-2 ring-primary/60 rounded-md')}
style={{ minWidth: 200, userSelect: 'none' }}
>
<div className="customNodeBody bg-card rounded-md border p-3 flex flex-col gap-2 text-sm shadow-md hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-2">
<Server className={cn('h-3.5 w-3.5 flex-shrink-0', isOnline ? 'text-green-500' : 'text-gray-500')} />
<span className="font-medium truncate text-purple-600" title={serverId}>
{serverId || t('canvas.server.unknown')}
</span>
</div>
{data.original?.ip && (
<span className="font-mono text-xs text-muted-foreground truncate mt-1">{data.original.ip}</span>
)}
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
<span>FRPS</span>
<Badge variant={isOnline ? 'default' : 'secondary'} className="text-[10px] h-4 px-1">
{isOnline ? t('canvas.server.online') : t('canvas.server.offline')}
</Badge>
</div>
</div>
<div className="drag-handle cursor-grab active:cursor-grabbing p-1 hover:bg-muted rounded flex-shrink-0">
<div className="w-4 h-4 flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" className="text-muted-foreground">
<circle cx="2" cy="2" r="1.5" />
<circle cx="6" cy="2" r="1.5" />
<circle cx="10" cy="2" r="1.5" />
<circle cx="2" cy="6" r="1.5" />
<circle cx="6" cy="6" r="1.5" />
<circle cx="10" cy="6" r="1.5" />
<circle cx="2" cy="10" r="1.5" />
<circle cx="6" cy="10" r="1.5" />
<circle cx="10" cy="10" r="1.5" />
</svg>
</div>
</div>
</div>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuItem onClick={handleOpenTerminal} disabled={!serverId} className="gap-2">
<Terminal className="h-4 w-4" />
<span>{t('canvas.contextMenu.openTerminal')}</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleOpenLog} disabled={!serverId} className="gap-2">
<FileText className="h-4 w-4" />
<span>{t('canvas.contextMenu.openLog')}</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
export default ServerNodeComponent
+168
View File
@@ -0,0 +1,168 @@
'use client'
import React, { useState, useEffect } from 'react'
import type { NodeProps } from '@xyflow/react'
import { NodeResizer } from '@xyflow/react'
import type { TerminalNode, NodeOperations } from './types'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Terminal as TerminalIcon, Minimize2, Maximize2, X, RefreshCcw, Eraser, Circle } from 'lucide-react'
import dynamic from 'next/dynamic'
import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ClientSelector } from '@/components/base/client-selector'
import { ServerSelector } from '@/components/base/server-selector'
import { useQuery } from '@tanstack/react-query'
import { getClientsStatus } from '@/api/platform'
import { ClientType } from '@/lib/pb/common'
import { Badge } from '@/components/ui/badge'
const TerminalComponent = dynamic(() => import('@/components/base/read-write-xterm'), {
ssr: false,
})
const TerminalNodeComponent: React.FC<NodeProps<TerminalNode> & NodeOperations> = ({
id,
data,
selected,
onDelete,
}) => {
const { t } = useTranslation()
const [clientId, setClientId] = useState(data.clientId || '')
const [enabled, setEnabled] = useState(!!data.clientId)
const [status, setStatus] = useState<'loading' | 'success' | 'error' | undefined>()
const [clear, setClear] = useState(0)
const isFrps = data.clientType === ClientType.FRPS
// 获取客户端状态
const { data: clientStatusData } = useQuery({
queryKey: ['clientStatus', clientId],
queryFn: async () => {
if (!clientId) return undefined
return await getClientsStatus({
clientIds: [clientId],
clientType: data.clientType,
})
},
enabled: !!clientId,
refetchInterval: 30000,
})
const clientStatus = clientStatusData?.clients[clientId]
const isOnline = clientStatus?.status === ClientStatus_Status.ONLINE
useEffect(() => {
if (clientId && !enabled) {
setEnabled(true)
}
}, [clientId, enabled])
const handleDelete = () => {
onDelete?.(id)
}
const handleRefresh = () => {
setClear(Math.random())
}
return (
<div
className={cn('terminalNode', selected && 'ring-2 ring-primary/60 rounded-lg')}
style={{ userSelect: 'none', pointerEvents: 'all', width: '100%', height: '100%' }}
>
<NodeResizer isVisible={selected} minWidth={400} minHeight={320} maxWidth={1200} maxHeight={800} />
<Card className={cn('shadow-lg border-2 h-full flex flex-col', 'w-full')}>
<CardHeader
className="p-3 pb-2 flex flex-row items-center justify-between space-y-0 drag-handle"
style={{ cursor: 'move', userSelect: 'none' }}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
<CardTitle className="text-sm font-medium truncate">{clientId || t('canvas.terminal.noClient')}</CardTitle>
{clientId && (
<Badge variant={isOnline ? 'default' : 'secondary'} className="text-[10px] h-4 px-1.5">
<Circle className={cn('h-2 w-2 mr-1', isOnline ? 'fill-green-500' : 'fill-gray-500')} />
{isOnline ? t('client.status_online') : t('client.status_offline')}
</Badge>
)}
{status && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
{status === 'success' && t('canvas.terminal.connected')}
{status === 'loading' && t('canvas.terminal.connecting')}
{status === 'error' && t('canvas.terminal.error')}
</Badge>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{clientId && (
<>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleRefresh}
title={t('canvas.terminal.refresh')}
>
<RefreshCcw className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setClear(Math.random())}
title={t('canvas.terminal.clear')}
>
<Eraser className="h-3.5 w-3.5" />
</Button>
</>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive hover:text-destructive"
onClick={handleDelete}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
</CardHeader>
<CardContent className="p-3 pt-0 flex flex-col gap-2 nodrag flex-1 min-h-0">
<div className="nodrag">
{isFrps ? (
<ServerSelector serverID={clientId} setServerID={setClientId} />
) : (
<ClientSelector clientID={clientId} setClientID={setClientId} />
)}
</div>
{clientId ? (
<div
className="flex-1 min-h-[200px] border rounded overflow-hidden bg-black terminal-container nodrag"
style={{ userSelect: 'text' }}
>
<TerminalComponent
setStatus={setStatus}
isLoading={!enabled}
clientStatus={
{
clientId: clientId,
clientType: data.clientType,
version: { platform: 'linux' },
} as ClientStatus
}
reset={clear}
/>
</div>
) : (
<div className="flex-1 min-h-[200px] border rounded bg-muted flex items-center justify-center">
<p className="text-muted-foreground text-sm">{t('canvas.terminal.selectClient')}</p>
</div>
)}
</CardContent>
</Card>
</div>
)
}
export default TerminalNodeComponent
+8
View File
@@ -0,0 +1,8 @@
export { default as Canvas } from './Canvas'
export { default as CanvasPanel } from './CanvasPanel'
export { default as ClientNode } from './ClientNode'
export { default as ServerNode } from './ServerNode'
export { default as TerminalNode } from './TerminalNode'
export { default as LogNode } from './LogNode'
export * from './types'
+64
View File
@@ -0,0 +1,64 @@
import type { Node as XNode } from '@xyflow/react'
import type { Client, Server } from '@/lib/pb/common'
import { ClientStatus } from '@/lib/pb/api_master'
// ==================== 节点数据类型 ====================
// Client 节点数据
export type ClientNodeData = {
label: string
original?: Client
clientStatus?: ClientStatus
}
// Server 节点数据
export type ServerNodeData = {
label: string
original?: Server
status?: 'online' | 'offline'
}
// 终端节点数据
export type TerminalNodeData = {
label: string
clientId: string
clientType: number
minimized?: boolean
}
// 日志节点数据
export type LogNodeData = {
label: string
clientId: string
clientType: number
minimized?: boolean
pkgs?: string[]
}
// ==================== 节点类型 ====================
export type ClientNode = XNode<ClientNodeData, 'client'>
export type ServerNode = XNode<ServerNodeData, 'server'>
export type TerminalNode = XNode<TerminalNodeData, 'terminal'>
export type LogNode = XNode<LogNodeData, 'log'>
export type CanvasNode = ClientNode | ServerNode | TerminalNode | LogNode
// ==================== 画布数据类型 ====================
export type CanvasData = {
nodes: CanvasNode[]
}
// ==================== 回调函数类型 ====================
export type OnNodeClick = (nodeId: string) => void
export type OnNodeDelete = (nodeId: string) => void
// ==================== 节点操作接口 ====================
export interface NodeOperations {
onOpenTerminal?: (clientId: string, clientType: number, sourceNodeId?: string) => void
onOpenLog?: (clientId: string, clientType: number, sourceNodeId?: string) => void
onDelete?: (nodeId: string) => void
}
+5 -5
View File
@@ -34,25 +34,25 @@ export function LanguageSwitcher() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
<DropdownMenuItem
onClick={() => toggleLanguage('zh')}
className={currentLanguage === 'zh' ? 'bg-accent' : ''}
>
()
</DropdownMenuItem>
<DropdownMenuItem
<DropdownMenuItem
onClick={() => toggleLanguage('zhtw')}
className={currentLanguage === 'zh-tw' ? 'bg-accent' : ''}
className={currentLanguage === 'zhtw' ? 'bg-accent' : ''}
>
()
</DropdownMenuItem>
<DropdownMenuItem
<DropdownMenuItem
onClick={() => toggleLanguage('en')}
className={currentLanguage === 'en' ? 'bg-accent' : ''}
>
English
</DropdownMenuItem>
<DropdownMenuItem
<DropdownMenuItem
onClick={() => toggleLanguage('fr')}
className={currentLanguage === 'fr' ? 'bg-accent' : ''}
>
+1 -1
View File
@@ -77,7 +77,7 @@ export function NavUser({ user }: NavUserProps) {
<DropdownMenuItem asChild>
<Link href="/platform-settings" className="w-full flex items-center space-x-2">
<SettingsIcon className="h-4 w-4" />
<span>{t('平台设置')}</span>
<span>{t('settings.header.title')}</span>
</Link>
</DropdownMenuItem>
+18 -18
View File
@@ -74,7 +74,7 @@ export function PlatformSettingsForm() {
clientRpcUrl: values.clientRpcUrl?.trim() || undefined,
}
$frontendPreference.set(pref)
toast.success(t('已更新平台设置'))
toast.success(t('settings.toast.updateSuccess'))
// 重置 initial 状态 & 清空 dirty
form.reset(values)
setInitial(values)
@@ -83,7 +83,7 @@ export function PlatformSettingsForm() {
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">{t('正在加载平台设置')}</p>
<p className="text-gray-500">{t('settings.loading.platform')}</p>
</div>
)
}
@@ -91,9 +91,9 @@ export function PlatformSettingsForm() {
return (
<Card className="max-w-lg mx-auto">
<CardHeader className="border-b">
<CardTitle>{t('平台设置')}</CardTitle>
<CardDescription>{t('修改前端平台设置')}</CardDescription>
<p className="text-xs text-muted-foreground mt-1 italic">{t('此配置仅保存在本地')}</p>
<CardTitle>{t('settings.header.title')}</CardTitle>
<CardDescription>{t('settings.header.description')}</CardDescription>
<p className="text-xs text-muted-foreground mt-1 italic">{t('settings.header.note')}</p>
</CardHeader>
<CardContent>
<Form {...form}>
@@ -105,8 +105,8 @@ export function PlatformSettingsForm() {
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>{t('使用服务器 Github 代理')}</FormLabel>
<FormDescription>{t('若开启,则使用后台配置的代理地址下载')}</FormDescription>
<FormLabel>{t('settings.form.useServerGithubLabel')}</FormLabel>
<FormDescription>{t('settings.form.useServerGithubDescription')}</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
@@ -121,9 +121,9 @@ export function PlatformSettingsForm() {
name="githubProxyUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t('自定义 Github 代理地址')}</FormLabel>
<FormLabel>{t('settings.form.githubProxyLabel')}</FormLabel>
<FormControl>
<Input placeholder={platformInfo?.githubProxyUrl || t('例如 https://ghproxy.com/')} {...field} />
<Input placeholder={platformInfo?.githubProxyUrl || t('settings.form.githubProxyPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -136,9 +136,9 @@ export function PlatformSettingsForm() {
name="clientApiUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t('自定义 API URL')}</FormLabel>
<FormLabel>{t('settings.form.clientApiLabel')}</FormLabel>
<FormControl>
<Input placeholder={platformInfo?.clientApiUrl || t('例如 https://api.example.com/')} {...field} />
<Input placeholder={platformInfo?.clientApiUrl || t('settings.form.clientApiPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -151,9 +151,9 @@ export function PlatformSettingsForm() {
name="clientRpcUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t('自定义 RPC URL')}</FormLabel>
<FormLabel>{t('settings.form.clientRpcLabel')}</FormLabel>
<FormControl>
<Input placeholder={platformInfo?.clientRpcUrl || t('例如 https://rpc.example.com/')} {...field} />
<Input placeholder={platformInfo?.clientRpcUrl || t('settings.form.clientRpcPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -172,17 +172,17 @@ export function PlatformSettingsForm() {
}
onClick={() => setOpen(true)}
>
{t('保存更改')}
{t('settings.actions.saveChanges')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('确认保存')}</DialogTitle>
<DialogDescription>{t('确定保存修改?')}</DialogDescription>
<DialogTitle>{t('settings.dialog.confirmSaveTitle')}</DialogTitle>
<DialogDescription>{t('settings.dialog.confirmSaveDescription')}</DialogDescription>
</DialogHeader>
<DialogFooterUI>
<Button variant={'destructive'} onClick={() => setOpen(false)}>
{t('取消')}
{t('settings.dialog.cancel')}
</Button>
<Button
variant={'secondary'}
@@ -191,7 +191,7 @@ export function PlatformSettingsForm() {
setOpen(false)
}}
>
{t('确认')}
{t('settings.dialog.confirm')}
</Button>
</DialogFooterUI>
</DialogContent>

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