init commit

This commit is contained in:
2025-02-19 14:23:58 +08:00
commit 10a4f010ce
34 changed files with 1340 additions and 0 deletions

61
server/monitor/monitor.go Normal file
View File

@@ -0,0 +1,61 @@
package monitor
import (
"context"
"encoding/hex"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
"github.com/pkg/errors"
"log/slog"
)
func Start(ctx context.Context, errCh chan error) {
// 打开一个网络接口
device, err := pcap.OpenLive("WLAN", 1600, true, pcap.BlockForever)
if err != nil {
b, er := hex.DecodeString("\\xbb")
slog.Debug("b", b, er)
errCh <- errors.Wrap(err, "打开网络接口失败")
return
}
defer device.Close()
err = device.SetBPFFilter("tcp")
if err != nil {
errCh <- errors.Wrap(err, "设置 BPF 过滤器失败")
return
}
err = device.SetDirection(pcap.DirectionIn)
if err != nil {
errCh <- errors.Wrap(err, "设置捕获方向失败")
return
}
// 创建一个数据包源
source := gopacket.NewPacketSource(device, device.LinkType())
source.NoCopy = true
source.Lazy = true
for {
select {
case <-ctx.Done():
slog.Debug("monitor 被动结束")
errCh <- nil
return
case packet := <-source.Packets():
handle(packet)
}
}
}
func handle(packet gopacket.Packet) {
slog.Debug("Packet: ", packet)
slog.Debug("Layers: ", packet.Layers())
slog.Debug("Application: ", packet.ApplicationLayer())
slog.Debug("Transport: ", packet.TransportLayer())
slog.Debug("Network: ", packet.NetworkLayer())
slog.Debug("Link: ", packet.LinkLayer())
}

34
server/orm/orm.go Normal file
View File

@@ -0,0 +1,34 @@
package orm
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"os"
)
var DB *gorm.DB
func Init() {
Host := os.Getenv("DB_HOST")
Port := os.Getenv("DB_PORT")
Database := os.Getenv("DB_DATABASE")
Username := os.Getenv("DB_USERNAME")
Password := os.Getenv("DB_PASSWORD")
Timezone := os.Getenv("DB_TIMEZONE")
dsn := fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable TimeZone=%s",
Host, Port, Username, Password, Database, Timezone,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default,
})
if err != nil {
panic(err)
}
DB = db
}

71
server/service.go Normal file
View File

@@ -0,0 +1,71 @@
package server
import (
"context"
"github.com/joho/godotenv"
"github.com/lmittmann/tint"
"github.com/mattn/go-colorable"
"log/slog"
"os"
"proxy-server/server/orm"
"proxy-server/server/web"
)
func Start() {
defer func() {
err := recover()
if err != nil {
slog.Error("服务由于意外的 panic 导致退出", err)
}
}()
ctx := context.Background()
// 初始化环境变量
err := godotenv.Load()
if err != nil {
slog.Debug("没有本地环境变量文件")
}
// 配置日志
writer := colorable.NewColorable(os.Stdout)
logger := slog.New(tint.NewHandler(writer, &tint.Options{
Level: slog.LevelDebug,
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
err, ok := attr.Value.Any().(error)
if !ok {
return attr
}
return tint.Err(err)
},
}))
slog.SetDefault(logger)
// 初始化公共组件
orm.Init()
// 启动子服务
goCount := 1
errChan := make(chan error, goCount)
ctxC, cancel := context.WithCancel(ctx)
defer cancel()
go web.Start(ctxC, errChan)
//go monitor.Start(ctxC, errChan)
slog.Info("服务启动成功")
// 监听异常
well := true
for i := 0; i < goCount; i++ {
err := <-errChan
if err != nil {
slog.Error("服务异常退出", err)
if well { // 第一次出错时取消其他服务
well = false
cancel()
}
}
}
close(errChan)
slog.Info("服务已全部退出")
}

View File

@@ -0,0 +1,166 @@
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"log/slog"
"proxy-server/pkg/resp"
"proxy-server/server/orm"
"proxy-server/server/web/app/models"
"strings"
"time"
)
// region frp 接口
type FrpData struct {
Reject bool
RejectReason string
Unchange bool
}
func ChanRequest(c *gin.Context) {
type Body struct {
Content struct {
ProxyName string `json:"proxy_name"`
ProxyType string `json:"proxy_type"`
RemoteAddr string `json:"remote_addr"`
User interface{}
}
}
op := c.Query("op")
if op != "NewUserConn" {
_ = c.Error(errors.New("不支持的操作"))
return
}
id := c.GetHeader("X-Frp-Reqid")
if id == "" {
_ = c.Error(errors.New("请求头中缺少 X-Frp-Reqid"))
return
}
var body Body
err := c.ShouldBindJSON(&body)
if err != nil {
_ = c.Error(errors.Wrap(err, "解析请求正文失败"))
return
}
content := body.Content
// 检查此 ip 是否有权限访问目标 node
clientIp := strings.Split(content.RemoteAddr, ":")[0]
targetNode := content.ProxyName
slog.Debug(id + " 客户端 " + clientIp + " 请求连接到 " + targetNode)
var channels []models.Channel
err = orm.DB.
Joins("INNER JOIN public.nodes n ON channels.node_id = n.id AND n.name = ?", targetNode).
Joins("INNER JOIN public.users u ON channels.user_id = u.id").
Joins("INNER JOIN public.user_ips ip ON u.id = ip.user_id AND ip.ip_address = ?", clientIp).
Find(&channels).Error
if err != nil {
_ = c.Error(errors.Wrap(err, "查询用户权限失败"))
return
}
// 返回响应
rsCount := len(channels)
if rsCount > 1 {
slog.Warn(clientIp + " + " + targetNode + "的组合有多个权限结果,这是不应当存在的")
}
if rsCount == 0 {
slog.Debug(id + " 没有权限")
reject(c)
return
}
channel := channels[0]
if channel.Expiration.Before(time.Now()) {
slog.Debug(id + " 权限已过期")
reject(c)
return
}
slog.Debug(id + " 通过验证")
confirm(c)
}
func ChanTest(c *gin.Context) {
var body map[string]interface{}
err := c.ShouldBindJSON(&body)
if err != nil {
slog.Error("解析请求正文失败", err)
}
for k, v := range body {
slog.Debug("map", "key: ", k, " value: ", v)
}
confirm(c)
}
func confirm(c *gin.Context) {
c.JSON(200, FrpData{
Reject: false,
Unchange: true,
})
}
func reject(c *gin.Context) {
c.JSON(401, FrpData{
Reject: true,
RejectReason: "客户端没有权限访问该节点",
})
}
// endregion
func ChanAuth(c *gin.Context) {
type Body struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Data struct {
Timeout uint64 `json:"timeout"`
}
var body Body
err := c.ShouldBindJSON(&body)
if err != nil {
_ = c.Error(err)
c.JSON(400, resp.Fail("请求参数错误"))
return
}
// 查找通道
var result *models.Channel
orm.DB.
Model(&models.Channel{}).
Where(&models.Channel{
Username: body.Username,
Password: body.Password,
}).
First(&result)
if result == nil {
_ = c.Error(errors.New("用户信息不存在"))
c.JSON(401, resp.Fail("账号密码错误"))
return
}
// 验证账号密码 todo 哈希密码验证
if result.Username != body.Username || result.Password != body.Password {
_ = c.Error(errors.New("账号密码错误"))
c.JSON(401, resp.Fail("账号密码错误"))
return
}
// 计算到期时间
timeout := result.Expiration.Sub(time.Now())
// todo 保存会话 对于大量短连接的情况,考虑如何保存连接会话信息
// 返回结果
c.JSON(200, resp.Done(Data{
Timeout: uint64(timeout.Seconds()),
}))
}

View File

@@ -0,0 +1,115 @@
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"gorm.io/gorm"
"os"
"proxy-server/server/orm"
"proxy-server/server/web/app/models"
)
type NodeRegisterReq struct {
Name string
Secret string
}
func NodeRegister(c *gin.Context) {
// 请求参数
var req NodeRegisterReq
err := c.ShouldBind(&req)
if err != nil {
_ = c.Error(errors.Wrap(err, "参数解析错误"))
return
}
// 验证 secret
secret := os.Getenv("SECRET")
if req.Secret != secret {
_ = c.Error(errors.New("拒绝连接"))
return
}
// 注册节点
// todo 查询运营商和地区
err = orm.DB.Transaction(func(tx *gorm.DB) error {
// 查询节点是否已存在
var count int64
err := orm.DB.Where(&models.Node{
Name: req.Name,
}).Count(&count).Error
if err != nil {
return err
}
// 不存在则注册
if count == 0 {
ipAddress := c.ClientIP()
node := models.Node{
Name: req.Name,
Provider: "",
Location: "",
IPAddress: ipAddress,
}
err = orm.DB.Create(&node).Error
if err != nil {
return err
}
}
return nil
})
if err != nil {
_ = c.Error(errors.Wrap(err, "注册节点失败"))
return
}
c.Status(200)
}
type NodeReportReq struct {
Name string
}
func NodeReport(c *gin.Context) {
// 请求参数
var req NodeReportReq
err := c.ShouldBind(&req)
if err != nil {
_ = c.Error(errors.Wrap(err, "参数解析错误"))
return
}
// 上报节点信息
err = orm.DB.Transaction(func(tx *gorm.DB) error {
// 查询节点
var node models.Node
err = orm.DB.Where(&models.Node{
Name: req.Name,
}).First(&node).Error
if err != nil {
return err
}
// 更新节点信息
ipAddress := c.ClientIP()
if ipAddress != node.IPAddress {
err = orm.DB.Model(&node).Update("ip_address", ipAddress).Error
if err != nil {
return err
}
}
return nil
})
if err != nil {
_ = c.Error(errors.Wrap(err, "上报节点信息失败"))
return
}
c.Status(200)
}

View File

@@ -0,0 +1 @@
package handlers

View File

@@ -0,0 +1,19 @@
package models
import (
"gorm.io/gorm"
"time"
)
// Channel 连接认证模型
type Channel struct {
gorm.Model
UserId uint
NodeId uint
Protocol string
Username string
Password string
AuthIp bool
AuthPass bool
Expiration time.Time
}

View File

@@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
// Node 客户端模型
type Node struct {
gorm.Model
Name string
Provider string
Location string
IPAddress string
Channels []Channel `gorm:"foreignKey:NodeId"`
}

View File

@@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Password string
Username string
Email string
Phone string
Name string
Channels []Channel `gorm:"foreignKey:UserId"`
}

10
server/web/auth/auth.go Normal file
View File

@@ -0,0 +1,10 @@
package auth
import "github.com/gin-gonic/gin"
type Config struct {
}
func Apply(r *gin.Engine, config *Config) {
r.Use(middleware)
}

View File

@@ -0,0 +1,41 @@
package auth
type Context interface {
Permissions() map[string]struct{}
PermitAll(permissions ...string) bool
PermitAny(permissions ...string) bool
}
// region DeviceContext
type DeviceContext struct {
ID uint
IpAddress string
Permissions map[string]struct{}
}
func (c DeviceContext) PermitAny(permissions ...string) bool {
if _, exist := c.Permissions["*"]; exist {
return true
}
for _, permission := range permissions {
if _, ok := c.Permissions[permission]; ok {
return true
}
}
return false
}
func (c DeviceContext) PermitAll(permissions ...string) bool {
if _, exist := c.Permissions["*"]; exist {
return true
}
for _, permission := range permissions {
if _, ok := c.Permissions[permission]; !ok {
return false
}
}
return true
}
// endregion

View File

@@ -0,0 +1,97 @@
package auth
import (
"encoding/base64"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"log/slog"
"net/http"
"os"
"proxy-server/pkg/resp"
"slices"
"strings"
)
func middleware(c *gin.Context) {
auth := check(c)
if auth {
secret, err := getSecret(c)
if err != nil {
slog.Error("认证失败", err)
fail400(c, err)
return
}
err = authenticate(c, secret)
if err != nil {
slog.Error("认证失败", err)
fail401(c, err)
return
}
}
c.Next()
}
var (
securedPaths = []string{
"/connect",
}
)
func check(c *gin.Context) bool {
path := c.Request.URL.Path
if slices.Contains(securedPaths, path) {
return true
}
return false
}
func getSecret(c *gin.Context) (string, error) {
// 获取认证信息
header := strings.Split(c.GetHeader("Authorization"), " ")
if len(header) != 2 {
return "", errors.New("无认证信息")
}
// 检查认证类型
schema := header[0]
if schema != "Secret" {
return "", errors.New("不支持的认证类型 " + schema)
}
// 解码密钥
parameters := header[1]
result, err := base64.URLEncoding.DecodeString(parameters)
if err != nil {
return "", errors.Wrap(err, "密钥解析失败")
}
return string(result), nil
}
func authenticate(_ *gin.Context, secret string) error {
if secret != os.Getenv("SECRET") {
return errors.New("认证失败")
}
return nil
}
func fail400(c *gin.Context, err error) {
_ = c.Error(err)
c.Abort()
c.JSON(
http.StatusBadRequest,
resp.Fail(err.Error()),
)
}
func fail401(c *gin.Context, err error) {
_ = c.Error(err)
c.Abort()
c.JSON(
http.StatusUnauthorized,
resp.Fail(err.Error()),
)
}

View File

@@ -0,0 +1,16 @@
package router
import (
"github.com/gin-gonic/gin"
"proxy-server/server/web/app/handlers"
)
func Apply(r *gin.Engine) {
r.POST("/node/register", handlers.NodeRegister)
r.POST("/node/report", handlers.NodeReport)
r.POST("/chan/request", handlers.ChanRequest)
r.POST("/chan/auth", handlers.ChanAuth)
r.POST("/chan/test", handlers.ChanTest)
}

44
server/web/web.go Normal file
View File

@@ -0,0 +1,44 @@
package web
import (
"context"
"github.com/gin-gonic/gin"
"log/slog"
"net/http"
"os"
"proxy-server/server/web/auth"
"proxy-server/server/web/router"
)
var server *http.Server
func Start(ctx context.Context, errCh chan error) {
address := ":" + os.Getenv("PORT")
engine := gin.Default()
server = &http.Server{Addr: address, Handler: engine}
// 监听关闭信号
go func() {
<-ctx.Done()
slog.Info("web 服务被动关闭")
err := server.Shutdown(ctx)
if err != nil {
slog.Error("web 服务关闭失败", err)
return
}
}()
// 配置中间件和路由
auth.Apply(engine, nil)
router.Apply(engine)
// 启动服务
err := server.ListenAndServe()
if err != nil {
errCh <- err
return
}
slog.Debug("web 服务主动结束")
errCh <- nil
}