实现 gost 网关
This commit is contained in:
390
web/services/channel_gost.go
Normal file
390
web/services/channel_gost.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
"platform/web/globals/orm"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
)
|
||||
|
||||
type channelGostProvider struct{}
|
||||
|
||||
func (s *channelGostProvider) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
|
||||
now := time.Now()
|
||||
batchNo := ID.GenReadable("bat")
|
||||
channels := make([]*m.Channel, count)
|
||||
if filter == nil {
|
||||
filter = &EdgeFilter{}
|
||||
}
|
||||
|
||||
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
|
||||
resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := resource.User
|
||||
expire := now.Add(resource.Live)
|
||||
|
||||
proxy, err := s.selectProxy(count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chans, err := selectPorts(proxy.ID, batchNo, count, expire)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edges, err := s.selectEdge(filter, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := proxyGost(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admissions := make([]*g.GostAdmissionConfig, 0, count)
|
||||
authers := make([]*g.GostAutherConfig, 0, count)
|
||||
services := make([]*g.GostServiceConfig, count)
|
||||
|
||||
for i := range count {
|
||||
ch := chans[i]
|
||||
edge := edges[i]
|
||||
port := ch.Port()
|
||||
host := u.Else(proxy.Host, proxy.IP.String())
|
||||
|
||||
serviceName := gostServiceName(batchNo, port)
|
||||
channel := &m.Channel{
|
||||
UserID: user.ID,
|
||||
ResourceID: resource.ID,
|
||||
BatchNo: batchNo,
|
||||
ProxyID: proxy.ID,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EdgeID: u.P(edge.ID),
|
||||
EdgeRef: u.P(serviceName),
|
||||
FilterISP: filter.Isp,
|
||||
FilterProv: filter.Prov,
|
||||
FilterCity: filter.City,
|
||||
IP: u.P(edge.IP),
|
||||
ExpiredAt: expire,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
service := &g.GostServiceConfig{
|
||||
Name: serviceName,
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: g.GostHandlerConfig{
|
||||
Type: "auto",
|
||||
Chain: edge.Mac,
|
||||
},
|
||||
Listener: g.GostListenerConfig{
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
|
||||
if authWhitelist {
|
||||
channel.Whitelists = u.P(strings.Join(whitelists, ","))
|
||||
service.Admission = gostAdmissionName(batchNo, port)
|
||||
admission := &g.GostAdmissionConfig{
|
||||
Name: service.Admission,
|
||||
Whitelist: true,
|
||||
Matchers: whitelists,
|
||||
}
|
||||
admissions = append(admissions, admission)
|
||||
}
|
||||
|
||||
if authPassword {
|
||||
username, password := genPassPair()
|
||||
channel.Username = &username
|
||||
channel.Password = &password
|
||||
service.Handler.Auther = gostAutherName(batchNo, port)
|
||||
auther := &g.GostAutherConfig{
|
||||
Name: service.Handler.Auther,
|
||||
Auths: []g.GostAuthConfig{{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}},
|
||||
}
|
||||
authers = append(authers, auther)
|
||||
}
|
||||
|
||||
services[i] = service
|
||||
channels[i] = channel
|
||||
}
|
||||
|
||||
for _, admission := range admissions {
|
||||
if err := client.CreateAdmission(admission); err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("创建 GOST admission 失败: %s", admission.Name), err)
|
||||
}
|
||||
}
|
||||
for _, auther := range authers {
|
||||
if err := client.CreateAuther(auther); err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("创建 GOST auther 失败: %s", auther.Name), err)
|
||||
}
|
||||
}
|
||||
for _, service := range services {
|
||||
if err := client.CreateService(service); err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("创建 GOST service 失败: %s", service.Name), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
var result gen.ResultInfo
|
||||
var err error
|
||||
switch resource.Type {
|
||||
case m.ResourceTypeShort:
|
||||
result, err = tx.ResourceShort.
|
||||
Where(
|
||||
tx.ResourceShort.ID.Eq(*resource.ShortId),
|
||||
tx.ResourceShort.Used.Eq(resource.Used),
|
||||
tx.ResourceShort.Daily.Eq(resource.Daily),
|
||||
).
|
||||
UpdateSimple(
|
||||
tx.ResourceShort.Used.Add(int32(count)),
|
||||
tx.ResourceShort.Daily.Value(int32(resource.Today+count)),
|
||||
tx.ResourceShort.LastAt.Value(now),
|
||||
)
|
||||
case m.ResourceTypeLong:
|
||||
result, err = tx.ResourceLong.
|
||||
Where(
|
||||
tx.ResourceLong.ID.Eq(*resource.LongId),
|
||||
tx.ResourceLong.Used.Eq(resource.Used),
|
||||
tx.ResourceLong.Daily.Eq(resource.Daily),
|
||||
).
|
||||
UpdateSimple(
|
||||
tx.ResourceLong.Used.Add(int32(count)),
|
||||
tx.ResourceLong.Daily.Value(int32(resource.Today+count)),
|
||||
tx.ResourceLong.LastAt.Value(now),
|
||||
)
|
||||
default:
|
||||
return core.NewBizErr("套餐类型不正确,无法更新")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewServErr("更新套餐使用记录失败", err)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return core.NewBizErr("套餐状态已过期")
|
||||
}
|
||||
|
||||
if err := tx.Channel.Omit(field.AssociationFields).Create(channels...); err != nil {
|
||||
return core.NewServErr("保存通道失败", err)
|
||||
}
|
||||
|
||||
if err := tx.LogsUserUsage.Create(&m.LogsUserUsage{
|
||||
UserID: user.ID,
|
||||
ResourceID: resource.ID,
|
||||
BatchNo: batchNo,
|
||||
Count: int32(count),
|
||||
ISP: u.X(filter.Isp.String()),
|
||||
Prov: filter.Prov,
|
||||
City: filter.City,
|
||||
IP: orm.Inet{Addr: source},
|
||||
Time: now,
|
||||
}); err != nil {
|
||||
return core.NewServErr("保存用户使用记录失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (s *channelGostProvider) RemoveChannels(batchNo string) error {
|
||||
return g.Redsync.WithLock(lockChannelRemoveKey(batchNo), func() error {
|
||||
start := time.Now()
|
||||
|
||||
batch, err := findUsedChanBatch(batchNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if batch == nil {
|
||||
slog.Debug("通道为空,跳过清理", "batch", batchNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
if env.RunMode == env.RunModeProd {
|
||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
||||
if err != nil {
|
||||
return core.NewServErr("获取代理数据失败", err)
|
||||
}
|
||||
|
||||
client, err := proxyGost(proxy)
|
||||
if err != nil {
|
||||
return core.NewServErr("创建 GOST 客户端失败", err)
|
||||
}
|
||||
var deleteErrs []error
|
||||
for _, ch := range batch.Chans {
|
||||
port := ch.Port()
|
||||
serviceName := gostServiceName(batchNo, port)
|
||||
deleteErrs = append(deleteErrs, deleteGostResource("service", serviceName, func() error {
|
||||
return client.DeleteService(serviceName)
|
||||
}))
|
||||
|
||||
autherName := gostAutherName(batchNo, port)
|
||||
deleteErrs = append(deleteErrs, deleteGostResource("auther", autherName, func() error {
|
||||
return client.DeleteAuther(autherName)
|
||||
}))
|
||||
|
||||
admissionName := gostAdmissionName(batchNo, port)
|
||||
deleteErrs = append(deleteErrs, deleteGostResource("admission", admissionName, func() error {
|
||||
return client.DeleteAdmission(admissionName)
|
||||
}))
|
||||
}
|
||||
if err := u.CombineErrors(deleteErrs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := freeChans(batch.ProxyID, batchNo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Debug("清除 GOST 端口配置", "proxy", batch.ProxyID, "batch", batchNo, "duration", time.Since(start).String())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *channelGostProvider) ClearExpiredChannels(proxyId int32) (int, error) {
|
||||
now := time.Now()
|
||||
|
||||
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
|
||||
if err != nil {
|
||||
return 0, core.NewServErr("查询使用中通道失败", err)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
batchList := make([]string, len(keys))
|
||||
batchSet := make(map[string]struct{}, len(keys))
|
||||
for i, key := range keys {
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) != 4 {
|
||||
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
|
||||
}
|
||||
batchList[i] = parts[3]
|
||||
batchSet[parts[3]] = struct{}{}
|
||||
}
|
||||
|
||||
var batchQueried []struct{ BatchNo string }
|
||||
err = q.Channel.
|
||||
Select(q.Channel.BatchNo).
|
||||
Where(
|
||||
q.Channel.BatchNo.In(batchList...),
|
||||
q.Channel.ExpiredAt.Gte(now.UTC()),
|
||||
).
|
||||
Group(q.Channel.BatchNo).
|
||||
Scan(&batchQueried)
|
||||
if err != nil {
|
||||
return 0, core.NewServErr("查询过期通道失败", err)
|
||||
}
|
||||
for _, batch := range batchQueried {
|
||||
delete(batchSet, batch.BatchNo)
|
||||
}
|
||||
|
||||
slog.Info("批量清理过期 GOST 通道", "count", len(batchSet))
|
||||
for batchNo := range batchSet {
|
||||
if err := s.RemoveChannels(batchNo); err != nil {
|
||||
slog.Error("清理过期 GOST 通道失败", "batch", batchNo, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return len(batchSet), nil
|
||||
}
|
||||
|
||||
func (s *channelGostProvider) selectProxy(count int) (*m.Proxy, error) {
|
||||
return selectProxyByType(m.ProxyTypeGost, count)
|
||||
}
|
||||
|
||||
func (s *channelGostProvider) selectEdge(filter *EdgeFilter, count int) ([]*m.Edge, error) {
|
||||
if filter == nil {
|
||||
filter = &EdgeFilter{}
|
||||
}
|
||||
|
||||
do := q.Edge.Where(
|
||||
q.Edge.Type.Eq(int(m.EdgeTypeGostChain)),
|
||||
q.Edge.Status.Eq(int(m.EdgeStatusNormal)),
|
||||
)
|
||||
if prov := u.N(filter.Prov); prov != nil {
|
||||
do = do.Where(q.Edge.Prov.Eq(*prov))
|
||||
}
|
||||
if city := u.N(filter.City); city != nil {
|
||||
do = do.Where(q.Edge.City.Eq(*city))
|
||||
}
|
||||
if isp := u.X(filter.Isp.String()); isp != nil {
|
||||
do = do.Where(q.Edge.ISP.Eq(int(*filter.Isp)))
|
||||
}
|
||||
|
||||
edges, err := q.Edge.Where(do).Order(q.Edge.ID).Limit(count).Find()
|
||||
if err != nil {
|
||||
return nil, core.NewBizErr("查询可用节点失败", err)
|
||||
}
|
||||
return expandGostEdges(edges, count)
|
||||
}
|
||||
|
||||
func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) {
|
||||
if len(edges) == 0 {
|
||||
return nil, core.NewBizErr("地区可用节点数量不足")
|
||||
}
|
||||
|
||||
result := make([]*m.Edge, count)
|
||||
for i := range count {
|
||||
result[i] = edges[i%len(edges)]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func proxyGost(proxy *m.Proxy) (g.GostClient, error) {
|
||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||
if len(secret) != 2 {
|
||||
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
|
||||
}
|
||||
|
||||
host := u.Else(proxy.Host, proxy.IP.String())
|
||||
return g.NewGost(host, env.GostApiPort, env.GostApiPathPrefix, secret[0], secret[1]), nil
|
||||
}
|
||||
|
||||
func deleteGostResource(kind string, name string, deleteFn func() error) error {
|
||||
if err := deleteFn(); err != nil && !g.IsGostNotFound(err) {
|
||||
return core.NewServErr(fmt.Sprintf("删除 GOST %s 配置失败: %s", kind, name), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func gostServiceName(batchNo string, port uint16) string {
|
||||
return fmt.Sprintf("gost-svc-%s-%d", batchNo, port)
|
||||
}
|
||||
|
||||
func gostAutherName(batchNo string, port uint16) string {
|
||||
return fmt.Sprintf("gost-auther-%s-%d", batchNo, port)
|
||||
}
|
||||
|
||||
func gostAdmissionName(batchNo string, port uint16) string {
|
||||
return fmt.Sprintf("gost-adm-%s-%d", batchNo, port)
|
||||
}
|
||||
Reference in New Issue
Block a user