mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2026-04-22 15:07:30 +08:00
feat: support wireguard
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -2,7 +2,7 @@
|
||||
__debug*
|
||||
.env
|
||||
tmp
|
||||
*.db
|
||||
*.db*
|
||||
frp-panel
|
||||
out
|
||||
cmd/out
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
fx.Provide(
|
||||
NewWorkerExecManager,
|
||||
NewWorkersManager,
|
||||
NewWireGuardManager,
|
||||
fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
|
||||
))
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+38
-16
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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')}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' : ''}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user