优化通道处理
This commit is contained in:
@@ -42,10 +42,10 @@ services:
|
|||||||
|
|
||||||
gost:
|
gost:
|
||||||
image: gogost/gost
|
image: gogost/gost
|
||||||
network_mode: host
|
command: >
|
||||||
command:
|
-api test:test@:9700
|
||||||
- -api
|
ports:
|
||||||
- :8900
|
- "9700:9700"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
2
pkg/env/env.go
vendored
2
pkg/env/env.go
vendored
@@ -45,7 +45,7 @@ var (
|
|||||||
BaiyinCloudUrl string
|
BaiyinCloudUrl string
|
||||||
BaiyinTokenUrl string
|
BaiyinTokenUrl string
|
||||||
|
|
||||||
GostApiPort = 8900
|
GostApiPort = 9700
|
||||||
GostApiPathPrefix = ""
|
GostApiPathPrefix = ""
|
||||||
|
|
||||||
IdenCallbackUrl string
|
IdenCallbackUrl string
|
||||||
|
|||||||
@@ -605,6 +605,7 @@ create table proxy (
|
|||||||
mac text not null,
|
mac text not null,
|
||||||
ip inet not null,
|
ip inet not null,
|
||||||
host text,
|
host text,
|
||||||
|
port int,
|
||||||
secret text,
|
secret text,
|
||||||
type int not null,
|
type int not null,
|
||||||
status int not null,
|
status int not null,
|
||||||
@@ -621,6 +622,7 @@ create index idx_proxy_created_at on proxy (created_at) where deleted_at is null
|
|||||||
comment on table proxy is '代理服务表';
|
comment on table proxy is '代理服务表';
|
||||||
comment on column proxy.id is '代理服务ID';
|
comment on column proxy.id is '代理服务ID';
|
||||||
comment on column proxy.version is '代理服务版本';
|
comment on column proxy.version is '代理服务版本';
|
||||||
|
comment on column proxy.port is '代理服务端口';
|
||||||
comment on column proxy.mac is '代理服务名称';
|
comment on column proxy.mac is '代理服务名称';
|
||||||
comment on column proxy.ip is '代理服务地址';
|
comment on column proxy.ip is '代理服务地址';
|
||||||
comment on column proxy.host is '代理服务域名';
|
comment on column proxy.host is '代理服务域名';
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Proxy struct {
|
|||||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||||
Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
|
Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
|
||||||
|
Port *int `json:"port,omitempty" gorm:"column:port"` // 代理服务端口
|
||||||
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
|
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
|
||||||
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
||||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func newEdge(db *gorm.DB, opts ...gen.DOOption) edge {
|
|||||||
_edge.Version = field.NewInt32(tableName, "version")
|
_edge.Version = field.NewInt32(tableName, "version")
|
||||||
_edge.Mac = field.NewString(tableName, "mac")
|
_edge.Mac = field.NewString(tableName, "mac")
|
||||||
_edge.IP = field.NewField(tableName, "ip")
|
_edge.IP = field.NewField(tableName, "ip")
|
||||||
|
_edge.Port = field.NewUint16(tableName, "port")
|
||||||
_edge.ISP = field.NewInt(tableName, "isp")
|
_edge.ISP = field.NewInt(tableName, "isp")
|
||||||
_edge.Prov = field.NewString(tableName, "prov")
|
_edge.Prov = field.NewString(tableName, "prov")
|
||||||
_edge.City = field.NewString(tableName, "city")
|
_edge.City = field.NewString(tableName, "city")
|
||||||
@@ -59,6 +60,7 @@ type edge struct {
|
|||||||
Version field.Int32
|
Version field.Int32
|
||||||
Mac field.String
|
Mac field.String
|
||||||
IP field.Field
|
IP field.Field
|
||||||
|
Port field.Uint16
|
||||||
ISP field.Int
|
ISP field.Int
|
||||||
Prov field.String
|
Prov field.String
|
||||||
City field.String
|
City field.String
|
||||||
@@ -89,6 +91,7 @@ func (e *edge) updateTableName(table string) *edge {
|
|||||||
e.Version = field.NewInt32(table, "version")
|
e.Version = field.NewInt32(table, "version")
|
||||||
e.Mac = field.NewString(table, "mac")
|
e.Mac = field.NewString(table, "mac")
|
||||||
e.IP = field.NewField(table, "ip")
|
e.IP = field.NewField(table, "ip")
|
||||||
|
e.Port = field.NewUint16(table, "port")
|
||||||
e.ISP = field.NewInt(table, "isp")
|
e.ISP = field.NewInt(table, "isp")
|
||||||
e.Prov = field.NewString(table, "prov")
|
e.Prov = field.NewString(table, "prov")
|
||||||
e.City = field.NewString(table, "city")
|
e.City = field.NewString(table, "city")
|
||||||
@@ -111,7 +114,7 @@ func (e *edge) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *edge) fillFieldMap() {
|
func (e *edge) fillFieldMap() {
|
||||||
e.fieldMap = make(map[string]field.Expr, 14)
|
e.fieldMap = make(map[string]field.Expr, 15)
|
||||||
e.fieldMap["id"] = e.ID
|
e.fieldMap["id"] = e.ID
|
||||||
e.fieldMap["created_at"] = e.CreatedAt
|
e.fieldMap["created_at"] = e.CreatedAt
|
||||||
e.fieldMap["updated_at"] = e.UpdatedAt
|
e.fieldMap["updated_at"] = e.UpdatedAt
|
||||||
@@ -120,6 +123,7 @@ func (e *edge) fillFieldMap() {
|
|||||||
e.fieldMap["version"] = e.Version
|
e.fieldMap["version"] = e.Version
|
||||||
e.fieldMap["mac"] = e.Mac
|
e.fieldMap["mac"] = e.Mac
|
||||||
e.fieldMap["ip"] = e.IP
|
e.fieldMap["ip"] = e.IP
|
||||||
|
e.fieldMap["port"] = e.Port
|
||||||
e.fieldMap["isp"] = e.ISP
|
e.fieldMap["isp"] = e.ISP
|
||||||
e.fieldMap["prov"] = e.Prov
|
e.fieldMap["prov"] = e.Prov
|
||||||
e.fieldMap["city"] = e.City
|
e.fieldMap["city"] = e.City
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
|||||||
_proxy.Mac = field.NewString(tableName, "mac")
|
_proxy.Mac = field.NewString(tableName, "mac")
|
||||||
_proxy.IP = field.NewField(tableName, "ip")
|
_proxy.IP = field.NewField(tableName, "ip")
|
||||||
_proxy.Host = field.NewString(tableName, "host")
|
_proxy.Host = field.NewString(tableName, "host")
|
||||||
|
_proxy.Port = field.NewInt(tableName, "port")
|
||||||
_proxy.Secret = field.NewString(tableName, "secret")
|
_proxy.Secret = field.NewString(tableName, "secret")
|
||||||
_proxy.Type = field.NewInt(tableName, "type")
|
_proxy.Type = field.NewInt(tableName, "type")
|
||||||
_proxy.Status = field.NewInt(tableName, "status")
|
_proxy.Status = field.NewInt(tableName, "status")
|
||||||
@@ -283,6 +284,7 @@ type proxy struct {
|
|||||||
Mac field.String
|
Mac field.String
|
||||||
IP field.Field
|
IP field.Field
|
||||||
Host field.String
|
Host field.String
|
||||||
|
Port field.Int
|
||||||
Secret field.String
|
Secret field.String
|
||||||
Type field.Int
|
Type field.Int
|
||||||
Status field.Int
|
Status field.Int
|
||||||
@@ -312,6 +314,7 @@ func (p *proxy) updateTableName(table string) *proxy {
|
|||||||
p.Mac = field.NewString(table, "mac")
|
p.Mac = field.NewString(table, "mac")
|
||||||
p.IP = field.NewField(table, "ip")
|
p.IP = field.NewField(table, "ip")
|
||||||
p.Host = field.NewString(table, "host")
|
p.Host = field.NewString(table, "host")
|
||||||
|
p.Port = field.NewInt(table, "port")
|
||||||
p.Secret = field.NewString(table, "secret")
|
p.Secret = field.NewString(table, "secret")
|
||||||
p.Type = field.NewInt(table, "type")
|
p.Type = field.NewInt(table, "type")
|
||||||
p.Status = field.NewInt(table, "status")
|
p.Status = field.NewInt(table, "status")
|
||||||
@@ -332,7 +335,7 @@ func (p *proxy) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxy) fillFieldMap() {
|
func (p *proxy) fillFieldMap() {
|
||||||
p.fieldMap = make(map[string]field.Expr, 13)
|
p.fieldMap = make(map[string]field.Expr, 14)
|
||||||
p.fieldMap["id"] = p.ID
|
p.fieldMap["id"] = p.ID
|
||||||
p.fieldMap["created_at"] = p.CreatedAt
|
p.fieldMap["created_at"] = p.CreatedAt
|
||||||
p.fieldMap["updated_at"] = p.UpdatedAt
|
p.fieldMap["updated_at"] = p.UpdatedAt
|
||||||
@@ -341,6 +344,7 @@ func (p *proxy) fillFieldMap() {
|
|||||||
p.fieldMap["mac"] = p.Mac
|
p.fieldMap["mac"] = p.Mac
|
||||||
p.fieldMap["ip"] = p.IP
|
p.fieldMap["ip"] = p.IP
|
||||||
p.fieldMap["host"] = p.Host
|
p.fieldMap["host"] = p.Host
|
||||||
|
p.fieldMap["port"] = p.Port
|
||||||
p.fieldMap["secret"] = p.Secret
|
p.fieldMap["secret"] = p.Secret
|
||||||
p.fieldMap["type"] = p.Type
|
p.fieldMap["type"] = p.Type
|
||||||
p.fieldMap["status"] = p.Status
|
p.fieldMap["status"] = p.Status
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"platform/pkg/env"
|
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
e "platform/web/events"
|
e "platform/web/events"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
|
"platform/web/globals/orm"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gen"
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,26 +29,278 @@ var Channel = &channelServer{
|
|||||||
provider: &channelGostProvider{},
|
provider: &channelGostProvider{},
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelServiceProvider interface {
|
type channelProvider interface {
|
||||||
CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
|
selectProxy(count int) (*m.Proxy, error)
|
||||||
RemoveChannels(batch string) error
|
prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error)
|
||||||
ClearExpiredChannels(proxyId int32) (int, error)
|
removeRemote(batchNo string, batch *usedChanBatch) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type channelServer struct {
|
type channelServer struct {
|
||||||
provider ChannelServiceProvider
|
provider channelProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
|
func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
|
||||||
return s.provider.CreateChannels(source, resourceNo, authWhitelist, authPassword, count, edgeFilter)
|
now := time.Now()
|
||||||
|
batchNo := ID.GenReadable("bat")
|
||||||
|
var channels []*m.Channel
|
||||||
|
if edgeFilter == nil {
|
||||||
|
edgeFilter = &EdgeFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var whitelistText *string
|
||||||
|
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
|
||||||
|
resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if authWhitelist {
|
||||||
|
joined := strings.Join(whitelists, ",")
|
||||||
|
whitelistText = &joined
|
||||||
|
}
|
||||||
|
|
||||||
|
expire := now.Add(resource.Live)
|
||||||
|
proxy, err := s.provider.selectProxy(count)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ports, err := selectPorts(proxy.ID, batchNo, count, expire)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createCtx := &channelCreateContext{
|
||||||
|
Now: now,
|
||||||
|
Source: source,
|
||||||
|
Resource: resource,
|
||||||
|
Proxy: proxy,
|
||||||
|
BatchNo: batchNo,
|
||||||
|
Ports: ports,
|
||||||
|
Expire: expire,
|
||||||
|
Count: count,
|
||||||
|
Filter: edgeFilter,
|
||||||
|
AuthWhitelist: authWhitelist,
|
||||||
|
AuthPassword: authPassword,
|
||||||
|
Whitelists: whitelists,
|
||||||
|
WhitelistText: whitelistText,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.provider.prepareCreate(createCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.applyRemote != nil {
|
||||||
|
if err := result.applyRemote(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := persistChannelCreate(createCtx, result.Channels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = result.Channels
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) RemoveChannels(batch string) error {
|
func (s *channelServer) RemoveChannels(batch string) error {
|
||||||
return s.provider.RemoveChannels(batch)
|
return g.Redsync.WithLock(lockChannelRemoveKey(batch), func() error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
usedBatch, err := findUsedChanBatch(batch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usedBatch == nil {
|
||||||
|
slog.Debug("通道为空,跳过清理", "batch", batch)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.provider.removeRemote(batch, usedBatch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := freeChans(usedBatch.ProxyID, batch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("清除通道配置", "proxy", usedBatch.ProxyID, "batch", batch, "duration", time.Since(start).String())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
|
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
|
||||||
return s.provider.ClearExpiredChannels(proxyId)
|
batchSet, err := findExpiredChannelBatches(proxyId, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("批量清理过期通道", "count", len(batchSet))
|
||||||
|
for batchNo := range batchSet {
|
||||||
|
if err := s.RemoveChannels(batchNo); err != nil {
|
||||||
|
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(batchSet), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelCreateContext struct {
|
||||||
|
Now time.Time
|
||||||
|
Source netip.Addr
|
||||||
|
Resource *ResourceView
|
||||||
|
Proxy *m.Proxy
|
||||||
|
BatchNo string
|
||||||
|
Ports []netip.AddrPort
|
||||||
|
Expire time.Time
|
||||||
|
Count int
|
||||||
|
Filter *EdgeFilter
|
||||||
|
AuthWhitelist bool
|
||||||
|
AuthPassword bool
|
||||||
|
Whitelists []string
|
||||||
|
WhitelistText *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelCreateResult struct {
|
||||||
|
Channels []*m.Channel
|
||||||
|
applyRemote func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
|
||||||
|
return &m.Channel{
|
||||||
|
UserID: ctx.Resource.User.ID,
|
||||||
|
ResourceID: ctx.Resource.ID,
|
||||||
|
BatchNo: ctx.BatchNo,
|
||||||
|
ProxyID: ctx.Proxy.ID,
|
||||||
|
Host: u.Else(ctx.Proxy.Host, ctx.Proxy.IP.String()),
|
||||||
|
Port: port,
|
||||||
|
FilterISP: ctx.Filter.Isp,
|
||||||
|
FilterProv: ctx.Filter.Prov,
|
||||||
|
FilterCity: ctx.Filter.City,
|
||||||
|
ExpiredAt: ctx.Expire,
|
||||||
|
Proxy: ctx.Proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyChannelAuth(ctx *channelCreateContext, channel *m.Channel) (username string, password string, ok bool) {
|
||||||
|
if ctx.AuthWhitelist {
|
||||||
|
channel.Whitelists = ctx.WhitelistText
|
||||||
|
}
|
||||||
|
if !ctx.AuthPassword {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password = genPassPair()
|
||||||
|
channel.Username = &username
|
||||||
|
channel.Password = &password
|
||||||
|
return username, password, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error {
|
||||||
|
return q.Q.Transaction(func(tx *q.Query) error {
|
||||||
|
var (
|
||||||
|
result gen.ResultInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch ctx.Resource.Type {
|
||||||
|
case m.ResourceTypeShort:
|
||||||
|
result, err = tx.ResourceShort.
|
||||||
|
Where(
|
||||||
|
tx.ResourceShort.ID.Eq(*ctx.Resource.ShortId),
|
||||||
|
tx.ResourceShort.Used.Eq(ctx.Resource.Used),
|
||||||
|
tx.ResourceShort.Daily.Eq(ctx.Resource.Daily),
|
||||||
|
).
|
||||||
|
UpdateSimple(
|
||||||
|
tx.ResourceShort.Used.Add(int32(ctx.Count)),
|
||||||
|
tx.ResourceShort.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
|
||||||
|
tx.ResourceShort.LastAt.Value(ctx.Now),
|
||||||
|
)
|
||||||
|
case m.ResourceTypeLong:
|
||||||
|
result, err = tx.ResourceLong.
|
||||||
|
Where(
|
||||||
|
tx.ResourceLong.ID.Eq(*ctx.Resource.LongId),
|
||||||
|
tx.ResourceLong.Used.Eq(ctx.Resource.Used),
|
||||||
|
tx.ResourceLong.Daily.Eq(ctx.Resource.Daily),
|
||||||
|
).
|
||||||
|
UpdateSimple(
|
||||||
|
tx.ResourceLong.Used.Add(int32(ctx.Count)),
|
||||||
|
tx.ResourceLong.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
|
||||||
|
tx.ResourceLong.LastAt.Value(ctx.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: ctx.Resource.User.ID,
|
||||||
|
ResourceID: ctx.Resource.ID,
|
||||||
|
BatchNo: ctx.BatchNo,
|
||||||
|
Count: int32(ctx.Count),
|
||||||
|
ISP: u.X(ctx.Filter.Isp.String()),
|
||||||
|
Prov: ctx.Filter.Prov,
|
||||||
|
City: ctx.Filter.City,
|
||||||
|
IP: orm.Inet{Addr: ctx.Source},
|
||||||
|
Time: ctx.Now,
|
||||||
|
}); err != nil {
|
||||||
|
return core.NewServErr("保存用户使用记录失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExpiredChannelBatches(proxyId int32, now time.Time) (map[string]struct{}, error) {
|
||||||
|
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("查询使用中通道失败", err)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return map[string]struct{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
batchList := make([]string, len(keys))
|
||||||
|
batchSet := make(map[string]struct{}, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
parsed, err := parseUsedChanKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
batchList[i] = parsed.BatchNo
|
||||||
|
batchSet[parsed.BatchNo] = 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 nil, core.NewServErr("查询过期通道失败", err)
|
||||||
|
}
|
||||||
|
for _, batch := range batchQueried {
|
||||||
|
delete(batchSet, batch.BatchNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return batchSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lockChannelCreateKey(resourceNo string) string {
|
func lockChannelCreateKey(resourceNo string) string {
|
||||||
@@ -87,36 +340,26 @@ func selectProxyByType(proxyType m.ProxyType, count int) (*m.Proxy, error) {
|
|||||||
return nil, core.NewBizErr("无可用代理")
|
return nil, core.NewBizErr("无可用代理")
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyIDs := make([]int32, 0, len(proxies))
|
var bestProxy *m.Proxy
|
||||||
proxyMap := make(map[int32]*m.Proxy, len(proxies))
|
|
||||||
for _, item := range proxies {
|
|
||||||
proxyIDs = append(proxyIDs, item.ID)
|
|
||||||
proxyMap[item.ID] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
maxID := int32(0)
|
|
||||||
maxCount := -1
|
maxCount := -1
|
||||||
for _, id := range proxyIDs {
|
for _, proxy := range proxies {
|
||||||
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(id)).Result()
|
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(proxy.ID)).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewServErr("查询可用通道数量失败", err)
|
return nil, core.NewServErr("查询可用通道数量失败", err)
|
||||||
}
|
}
|
||||||
if idCount > int64(maxCount) {
|
if idCount > int64(maxCount) {
|
||||||
maxCount = int(idCount)
|
maxCount = int(idCount)
|
||||||
maxID = id
|
bestProxy = proxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if maxCount < count {
|
if maxCount < count {
|
||||||
return nil, core.NewBizErr("无可用代理")
|
return nil, core.NewBizErr("无可用代理")
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxyMap[maxID], nil
|
return bestProxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) RefreshEdges() error {
|
func (s *channelServer) RefreshEdges() error {
|
||||||
if env.RunMode != env.RunModeProd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅白银网关支持边缘节点刷新,GOST 不参与此流程。
|
// 仅白银网关支持边缘节点刷新,GOST 不参与此流程。
|
||||||
proxies, err := q.Proxy.Where(
|
proxies, err := q.Proxy.Where(
|
||||||
@@ -355,6 +598,11 @@ type usedChanBatch struct {
|
|||||||
Chans []netip.AddrPort
|
Chans []netip.AddrPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type usedChanKey struct {
|
||||||
|
ProxyID int32
|
||||||
|
BatchNo string
|
||||||
|
}
|
||||||
|
|
||||||
func findUsedChanBatch(batch string) (*usedChanBatch, error) {
|
func findUsedChanBatch(batch string) (*usedChanBatch, error) {
|
||||||
keys, err := g.Redis.Keys(context.Background(), "channel:used:*:"+batch).Result()
|
keys, err := g.Redis.Keys(context.Background(), "channel:used:*:"+batch).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -393,7 +641,7 @@ func selectUsedChanBatchKey(batch string, keys []string) (string, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseUsedChanBatch(key string, chans []string) (*usedChanBatch, error) {
|
func parseUsedChanBatch(key string, chans []string) (*usedChanBatch, error) {
|
||||||
proxyID, err := parseUsedChansKey(key)
|
parsed, err := parseUsedChanKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -408,23 +656,26 @@ func parseUsedChanBatch(key string, chans []string) (*usedChanBatch, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &usedChanBatch{
|
return &usedChanBatch{
|
||||||
ProxyID: proxyID,
|
ProxyID: parsed.ProxyID,
|
||||||
Chans: addrs,
|
Chans: addrs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUsedChansKey(key string) (int32, error) {
|
func parseUsedChanKey(key string) (*usedChanKey, error) {
|
||||||
parts := strings.Split(key, ":")
|
parts := strings.Split(key, ":")
|
||||||
if len(parts) != 4 {
|
if len(parts) != 4 {
|
||||||
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
|
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyID, err := strconv.Atoi(parts[2])
|
proxyID, err := strconv.Atoi(parts[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), err)
|
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return int32(proxyID), nil
|
return &usedChanKey{
|
||||||
|
ProxyID: int32(proxyID),
|
||||||
|
BatchNo: parts[3],
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扩容通道
|
// 扩容通道
|
||||||
@@ -530,9 +781,7 @@ return 1
|
|||||||
// 错误信息
|
// 错误信息
|
||||||
var (
|
var (
|
||||||
ErrResourceNotExist = core.NewBizErr("套餐不存在")
|
ErrResourceNotExist = core.NewBizErr("套餐不存在")
|
||||||
ErrResourceInvalid = core.NewBizErr("套餐不可用")
|
|
||||||
ErrResourceExhausted = core.NewBizErr("套餐已用完")
|
ErrResourceExhausted = core.NewBizErr("套餐已用完")
|
||||||
ErrResourceExpired = core.NewBizErr("套餐已过期")
|
ErrResourceExpired = core.NewBizErr("套餐已过期")
|
||||||
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
|
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
|
||||||
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,225 +1,72 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
|
||||||
"platform/pkg/env"
|
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
"platform/web/globals/orm"
|
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelBaiyinProvider struct{}
|
type channelBaiyinProvider struct{}
|
||||||
|
|
||||||
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
|
func (s *channelBaiyinProvider) selectProxy(count int) (*m.Proxy, error) {
|
||||||
if filter == nil {
|
return selectProxyByType(m.ProxyTypeBaiYin, count)
|
||||||
return nil, core.NewBizErr("缺少节点过滤条件")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
|
||||||
batchNo := ID.GenReadable("bat")
|
gateway, err := proxyGateway(ctx.Proxy)
|
||||||
channels := make([]*m.Channel, count)
|
|
||||||
|
|
||||||
// 资源锁,防止并发扣减失败导致的端口悬空问题
|
|
||||||
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
|
|
||||||
// 检查并获取套餐与白名单
|
|
||||||
resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, core.NewServErr("创建代理网关失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := resource.User
|
channels := make([]*m.Channel, len(ctx.Ports))
|
||||||
expire := now.Add(resource.Live)
|
chanConfigs := make([]*g.PortConfigsReq, len(ctx.Ports))
|
||||||
|
for i, portRef := range ctx.Ports {
|
||||||
// 选择代理
|
channel := newBaseChannel(ctx, portRef.Port())
|
||||||
proxy, gateway, err := selectProxy(count)
|
cfg := &g.PortConfigsReq{
|
||||||
if err != nil {
|
Port: int(portRef.Port()),
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取用端口
|
|
||||||
chans, err := selectPorts(proxy.ID, batchNo, count, expire)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定节点端口
|
|
||||||
chanConfigs := make([]*g.PortConfigsReq, count)
|
|
||||||
edgeConfigs := make([]string, 0, count)
|
|
||||||
for i := range count {
|
|
||||||
ch := chans[i]
|
|
||||||
|
|
||||||
// 通道数据
|
|
||||||
channels[i] = &m.Channel{
|
|
||||||
UserID: user.ID,
|
|
||||||
ResourceID: resource.ID,
|
|
||||||
BatchNo: batchNo,
|
|
||||||
ProxyID: proxy.ID,
|
|
||||||
Host: u.Else(proxy.Host, proxy.IP.String()),
|
|
||||||
Port: ch.Port(),
|
|
||||||
FilterISP: filter.Isp,
|
|
||||||
FilterProv: filter.Prov,
|
|
||||||
FilterCity: filter.City,
|
|
||||||
ExpiredAt: expire,
|
|
||||||
Proxy: proxy,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通道配置数据
|
|
||||||
chanConfigs[i] = &g.PortConfigsReq{
|
|
||||||
Port: int(ch.Port()),
|
|
||||||
Status: true,
|
Status: true,
|
||||||
AutoEdgeConfig: &g.AutoEdgeConfig{
|
AutoEdgeConfig: &g.AutoEdgeConfig{
|
||||||
Province: u.Z(filter.Prov),
|
Province: u.Z(ctx.Filter.Prov),
|
||||||
City: u.Z(filter.City),
|
City: u.Z(ctx.Filter.City),
|
||||||
Isp: filter.Isp.String(),
|
Isp: ctx.Filter.Isp.String(),
|
||||||
Count: u.P(1),
|
Count: u.P(1),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 白名单模式
|
if ctx.AuthWhitelist {
|
||||||
if authWhitelist {
|
cfg.Whitelist = &ctx.Whitelists
|
||||||
channels[i].Whitelists = u.P(strings.Join(whitelists, ","))
|
}
|
||||||
chanConfigs[i].Whitelist = &whitelists
|
if username, password, ok := applyChannelAuth(ctx, channel); ok {
|
||||||
|
cfg.Userpass = u.P(username + ":" + password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 密码模式
|
channels[i] = channel
|
||||||
if authPassword {
|
chanConfigs[i] = cfg
|
||||||
username, password := genPassPair()
|
|
||||||
channels[i].Username = &username
|
|
||||||
channels[i].Password = &password
|
|
||||||
chanConfigs[i].Userpass = u.P(username + ":" + password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交配置
|
return &channelCreateResult{
|
||||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "total_count", len(chanConfigs), "remote_count", len(edgeConfigs))
|
Channels: channels,
|
||||||
if env.RunMode == env.RunModeProd {
|
applyRemote: func() error {
|
||||||
|
slog.Debug("提交代理端口配置", "proxy", ctx.Proxy.IP.String(), "total_count", len(chanConfigs))
|
||||||
// 从云端补足节点
|
if err := ensureEdges(ctx.Proxy, gateway, ctx.Filter, ctx.Count); err != nil {
|
||||||
err := ensureEdges(proxy, gateway, filter, count)
|
slog.Warn("ensureEdges 失败", "err", err)
|
||||||
if err != nil {
|
|
||||||
slog.Warn("ensureEdges 失败", "err", err) // 不阻止通道创建,继续走后续流程
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启用网关代理通道
|
|
||||||
if len(chanConfigs) > 0 {
|
if len(chanConfigs) > 0 {
|
||||||
if err := gateway.GatewayPortConfigs(chanConfigs); err != nil {
|
if err := gateway.GatewayPortConfigs(chanConfigs); err != nil {
|
||||||
slog.Warn("提交代理端口配置失败", "error", err.Error())
|
slog.Warn("提交代理端口配置失败", "error", err.Error())
|
||||||
return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
return core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", ctx.Proxy.IP.String()), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for _, item := range chanConfigs {
|
|
||||||
str, _ := json.Marshal(item)
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存数据
|
|
||||||
err = q.Q.Transaction(func(q *q.Query) error {
|
|
||||||
// 更新使用记录
|
|
||||||
var result gen.ResultInfo
|
|
||||||
var err error
|
|
||||||
switch resource.Type {
|
|
||||||
case m.ResourceTypeShort:
|
|
||||||
result, err = q.ResourceShort.
|
|
||||||
Where(
|
|
||||||
q.ResourceShort.ID.Eq(*resource.ShortId),
|
|
||||||
q.ResourceShort.Used.Eq(resource.Used),
|
|
||||||
q.ResourceShort.Daily.Eq(resource.Daily),
|
|
||||||
).
|
|
||||||
UpdateSimple(
|
|
||||||
q.ResourceShort.Used.Add(int32(count)),
|
|
||||||
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
|
|
||||||
q.ResourceShort.LastAt.Value(now),
|
|
||||||
)
|
|
||||||
|
|
||||||
case m.ResourceTypeLong:
|
|
||||||
result, err = q.ResourceLong.
|
|
||||||
Where(
|
|
||||||
q.ResourceLong.ID.Eq(*resource.LongId),
|
|
||||||
q.ResourceLong.Used.Eq(resource.Used),
|
|
||||||
q.ResourceLong.Daily.Eq(resource.Daily),
|
|
||||||
).
|
|
||||||
UpdateSimple(
|
|
||||||
q.ResourceLong.Used.Add(int32(count)),
|
|
||||||
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
|
|
||||||
q.ResourceLong.LastAt.Value(now),
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return core.NewBizErr("套餐类型不正确,无法更新")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("更新套餐使用记录失败", err)
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return core.NewBizErr("套餐状态已过期")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存通道
|
|
||||||
err = q.Channel.
|
|
||||||
Omit(field.AssociationFields).
|
|
||||||
Create(channels...)
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("保存通道失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存提取记录
|
|
||||||
err = q.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,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("保存用户使用记录失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
if err != nil {
|
}, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelBaiyinProvider) RemoveChannels(batchNo string) error {
|
func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
configs := make([]*g.PortConfigsReq, len(batch.Chans))
|
configs := make([]*g.PortConfigsReq, len(batch.Chans))
|
||||||
for i, ch := range batch.Chans {
|
for i, ch := range batch.Chans {
|
||||||
configs[i] = &g.PortConfigsReq{
|
configs[i] = &g.PortConfigsReq{
|
||||||
@@ -230,8 +77,6 @@ func (s *channelBaiyinProvider) RemoveChannels(batchNo string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交配置
|
|
||||||
if env.RunMode == env.RunModeProd {
|
|
||||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("获取代理数据失败", err)
|
return core.NewServErr("获取代理数据失败", err)
|
||||||
@@ -245,87 +90,7 @@ func (s *channelBaiyinProvider) RemoveChannels(batchNo string) error {
|
|||||||
if err = gateway.GatewayPortConfigs(configs); err != nil {
|
if err = gateway.GatewayPortConfigs(configs); err != nil {
|
||||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for _, item := range configs {
|
|
||||||
str, _ := json.Marshal(item)
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := freeChans(batch.ProxyID, batchNo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("清除代理端口配置", "proxy", batch.ProxyID, "batch", batchNo, "duration", time.Since(start).String())
|
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearExpiredChannels 清理指定代理的过期通道,并返回清理数量(现在理论上不会有需要手动批量清理的通道,未来可以废弃)
|
|
||||||
func (s *channelBaiyinProvider) 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("批量清理过期通道", "count", len(batchSet))
|
|
||||||
for batchNo, _ := range batchSet {
|
|
||||||
err := s.RemoveChannels(batchNo)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(batchSet), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectProxy(count int) (*m.Proxy, g.GatewayClient, error) {
|
|
||||||
proxy, err := selectProxyByType(m.ProxyTypeBaiYin, count)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway, err := proxyGateway(proxy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, core.NewServErr("创建代理网关失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy, gateway, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
|
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
|
||||||
|
|||||||
@@ -1,90 +1,42 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
"platform/web/globals/orm"
|
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelGostProvider struct{}
|
type channelGostProvider struct{}
|
||||||
|
|
||||||
func (s *channelGostProvider) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
|
func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
|
||||||
now := time.Now()
|
edges, err := s.selectEdge(ctx.Filter, ctx.Count)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user := resource.User
|
client, err := proxyGost(ctx.Proxy)
|
||||||
expire := now.Add(resource.Live)
|
|
||||||
|
|
||||||
proxy, err := s.selectProxy(count)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
chans, err := selectPorts(proxy.ID, batchNo, count, expire)
|
admissions := make([]*g.GostAdmissionConfig, 0, ctx.Count)
|
||||||
if err != nil {
|
authers := make([]*g.GostAutherConfig, 0, ctx.Count)
|
||||||
return err
|
services := make([]*g.GostServiceConfig, len(ctx.Ports))
|
||||||
}
|
channels := make([]*m.Channel, len(ctx.Ports))
|
||||||
|
|
||||||
edges, err := s.selectEdge(filter, count)
|
for i, portRef := range ctx.Ports {
|
||||||
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]
|
edge := edges[i]
|
||||||
port := ch.Port()
|
port := portRef.Port()
|
||||||
host := u.Else(proxy.Host, proxy.IP.String())
|
serviceName := gostServiceName(ctx.BatchNo, port)
|
||||||
|
channel := newBaseChannel(ctx, port)
|
||||||
serviceName := gostServiceName(batchNo, port)
|
channel.EdgeID = u.P(edge.ID)
|
||||||
channel := &m.Channel{
|
channel.EdgeRef = u.P(serviceName)
|
||||||
UserID: user.ID,
|
channel.IP = u.P(edge.IP)
|
||||||
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{
|
service := &g.GostServiceConfig{
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
@@ -98,36 +50,32 @@ func (s *channelGostProvider) CreateChannels(source netip.Addr, resourceNo strin
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if authWhitelist {
|
if ctx.AuthWhitelist {
|
||||||
channel.Whitelists = u.P(strings.Join(whitelists, ","))
|
service.Admission = gostAdmissionName(ctx.BatchNo, port)
|
||||||
service.Admission = gostAdmissionName(batchNo, port)
|
admissions = append(admissions, &g.GostAdmissionConfig{
|
||||||
admission := &g.GostAdmissionConfig{
|
|
||||||
Name: service.Admission,
|
Name: service.Admission,
|
||||||
Whitelist: true,
|
Whitelist: true,
|
||||||
Matchers: whitelists,
|
Matchers: ctx.Whitelists,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
admissions = append(admissions, admission)
|
if username, password, ok := applyChannelAuth(ctx, channel); ok {
|
||||||
}
|
service.Handler.Auther = gostAutherName(ctx.BatchNo, port)
|
||||||
|
authers = append(authers, &g.GostAutherConfig{
|
||||||
if authPassword {
|
|
||||||
username, password := genPassPair()
|
|
||||||
channel.Username = &username
|
|
||||||
channel.Password = &password
|
|
||||||
service.Handler.Auther = gostAutherName(batchNo, port)
|
|
||||||
auther := &g.GostAutherConfig{
|
|
||||||
Name: service.Handler.Auther,
|
Name: service.Handler.Auther,
|
||||||
Auths: []g.GostAuthConfig{{
|
Auths: []g.GostAuthConfig{{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
}},
|
}},
|
||||||
}
|
})
|
||||||
authers = append(authers, auther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
services[i] = service
|
services[i] = service
|
||||||
channels[i] = channel
|
channels[i] = channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &channelCreateResult{
|
||||||
|
Channels: channels,
|
||||||
|
applyRemote: func() error {
|
||||||
for _, admission := range admissions {
|
for _, admission := range admissions {
|
||||||
if err := client.CreateAdmission(admission); err != nil {
|
if err := client.CreateAdmission(admission); err != nil {
|
||||||
return core.NewServErr(fmt.Sprintf("创建 GOST admission 失败: %s", admission.Name), err)
|
return core.NewServErr(fmt.Sprintf("创建 GOST admission 失败: %s", admission.Name), err)
|
||||||
@@ -143,92 +91,12 @@ func (s *channelGostProvider) CreateChannels(source netip.Addr, resourceNo strin
|
|||||||
return core.NewServErr(fmt.Sprintf("创建 GOST service 失败: %s", service.Name), err)
|
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
|
return nil
|
||||||
})
|
},
|
||||||
if err != nil {
|
}, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelGostProvider) RemoveChannels(batchNo string) error {
|
func (s *channelGostProvider) removeRemote(batchNo string, batch *usedChanBatch) 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()
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("获取代理数据失败", err)
|
return core.NewServErr("获取代理数据失败", err)
|
||||||
@@ -256,66 +124,7 @@ func (s *channelGostProvider) RemoveChannels(batchNo string) error {
|
|||||||
return client.DeleteAdmission(admissionName)
|
return client.DeleteAdmission(admissionName)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if err := u.CombineErrors(deleteErrs); err != nil {
|
return u.CombineErrors(deleteErrs)
|
||||||
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) {
|
func (s *channelGostProvider) selectProxy(count int) (*m.Proxy, error) {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ type CreateProxy struct {
|
|||||||
Mac string `json:"mac" validate:"required"`
|
Mac string `json:"mac" validate:"required"`
|
||||||
IP string `json:"ip" validate:"required"`
|
IP string `json:"ip" validate:"required"`
|
||||||
Host *string `json:"host"`
|
Host *string `json:"host"`
|
||||||
|
Port *int `json:"port"`
|
||||||
Secret *string `json:"secret"`
|
Secret *string `json:"secret"`
|
||||||
Type *m.ProxyType `json:"type"`
|
Type *m.ProxyType `json:"type"`
|
||||||
Status *m.ProxyStatus `json:"status"`
|
Status *m.ProxyStatus `json:"status"`
|
||||||
@@ -80,6 +81,7 @@ func (s *proxyService) Create(create *CreateProxy) error {
|
|||||||
Mac: create.Mac,
|
Mac: create.Mac,
|
||||||
IP: orm.Inet{Addr: addr},
|
IP: orm.Inet{Addr: addr},
|
||||||
Host: create.Host,
|
Host: create.Host,
|
||||||
|
Port: create.Port,
|
||||||
Secret: create.Secret,
|
Secret: create.Secret,
|
||||||
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
|
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
|
||||||
Status: u.Else(create.Status, m.ProxyStatusOffline),
|
Status: u.Else(create.Status, m.ProxyStatusOffline),
|
||||||
@@ -99,6 +101,7 @@ type UpdateProxy struct {
|
|||||||
Mac *string `json:"mac"`
|
Mac *string `json:"mac"`
|
||||||
IP *string `json:"ip"`
|
IP *string `json:"ip"`
|
||||||
Host *string `json:"host"`
|
Host *string `json:"host"`
|
||||||
|
Port *int `json:"port"`
|
||||||
Secret *string `json:"secret"`
|
Secret *string `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +124,9 @@ func (s *proxyService) Update(update *UpdateProxy) error {
|
|||||||
if update.Host != nil {
|
if update.Host != nil {
|
||||||
simples = append(simples, q.Proxy.Host.Value(*update.Host))
|
simples = append(simples, q.Proxy.Host.Value(*update.Host))
|
||||||
}
|
}
|
||||||
|
if update.Port != nil {
|
||||||
|
simples = append(simples, q.Proxy.Port.Value(*update.Port))
|
||||||
|
}
|
||||||
if update.Secret != nil {
|
if update.Secret != nil {
|
||||||
hasSideEffect = true
|
hasSideEffect = true
|
||||||
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
|
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
|
||||||
|
|||||||
Reference in New Issue
Block a user