2025-11-24 18:44:06 +08:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-18 11:15:29 +08:00
|
|
|
"context"
|
2026-06-11 15:07:46 +08:00
|
|
|
"errors"
|
2026-05-18 13:54:01 +08:00
|
|
|
"fmt"
|
2025-12-01 12:42:51 +08:00
|
|
|
"net/netip"
|
|
|
|
|
"platform/pkg/u"
|
|
|
|
|
"platform/web/core"
|
2026-04-18 11:15:29 +08:00
|
|
|
g "platform/web/globals"
|
2025-12-01 12:42:51 +08:00
|
|
|
"platform/web/globals/orm"
|
2025-11-24 18:44:06 +08:00
|
|
|
m "platform/web/models"
|
|
|
|
|
q "platform/web/queries"
|
2026-05-18 13:54:01 +08:00
|
|
|
"strings"
|
2025-11-24 18:44:06 +08:00
|
|
|
"time"
|
2026-04-18 11:15:29 +08:00
|
|
|
|
|
|
|
|
"gorm.io/gen/field"
|
2026-06-11 15:07:46 +08:00
|
|
|
"gorm.io/gorm"
|
2025-11-24 18:44:06 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var Proxy = &proxyService{}
|
|
|
|
|
|
|
|
|
|
type proxyService struct{}
|
|
|
|
|
|
2026-04-18 11:15:29 +08:00
|
|
|
func hasUsedChans(proxyID int32) (bool, error) {
|
|
|
|
|
ctx := context.Background()
|
2026-05-08 17:30:51 +08:00
|
|
|
pattern := usedChansKey(proxyID, "*")
|
2026-06-11 15:07:46 +08:00
|
|
|
var cursor uint64
|
|
|
|
|
for {
|
|
|
|
|
keys, next, err := g.Redis.Scan(ctx, cursor, pattern, 100).Result()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
if len(keys) > 0 {
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
if next == 0 {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
cursor = next
|
2025-11-24 18:44:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 12:42:51 +08:00
|
|
|
|
2026-04-18 11:15:29 +08:00
|
|
|
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
|
|
|
|
|
if err := remChans(proxyID); err != nil {
|
|
|
|
|
return err
|
2025-12-01 12:42:51 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
chans := make([]netip.AddrPort, 10000)
|
|
|
|
|
for i := range 10000 {
|
2026-04-18 11:15:29 +08:00
|
|
|
chans[i] = netip.AddrPortFrom(addr, uint16(i+10000))
|
2025-12-08 14:22:30 +08:00
|
|
|
}
|
2026-04-18 11:15:29 +08:00
|
|
|
|
|
|
|
|
if err := regChans(proxyID, chans); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *proxyService) Page(req core.PageReq) (result []*m.Proxy, count int64, err error) {
|
|
|
|
|
return q.Proxy.
|
|
|
|
|
Omit(q.Proxy.Version, q.Proxy.Meta).
|
|
|
|
|
Order(q.Proxy.CreatedAt.Desc()).
|
|
|
|
|
FindByPage(req.GetOffset(), req.GetLimit())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *proxyService) All() (result []*m.Proxy, err error) {
|
|
|
|
|
return q.Proxy.
|
|
|
|
|
Omit(q.Proxy.Version, q.Proxy.Meta).
|
|
|
|
|
Order(q.Proxy.CreatedAt.Desc()).
|
|
|
|
|
Find()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CreateProxy struct {
|
|
|
|
|
Mac string `json:"mac" validate:"required"`
|
|
|
|
|
IP string `json:"ip" validate:"required"`
|
|
|
|
|
Host *string `json:"host"`
|
2026-06-09 16:30:19 +08:00
|
|
|
Port *int `json:"port"`
|
2026-04-18 11:15:29 +08:00
|
|
|
Secret *string `json:"secret"`
|
|
|
|
|
Type *m.ProxyType `json:"type"`
|
|
|
|
|
Status *m.ProxyStatus `json:"status"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *proxyService) Create(create *CreateProxy) error {
|
|
|
|
|
addr, err := netip.ParseAddr(create.IP)
|
2025-12-08 14:22:30 +08:00
|
|
|
if err != nil {
|
2026-04-18 11:15:29 +08:00
|
|
|
return core.NewServErr("IP地址格式错误", err)
|
2025-12-08 14:22:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-18 11:15:29 +08:00
|
|
|
return q.Q.Transaction(func(tx *q.Query) error {
|
|
|
|
|
proxy := &m.Proxy{
|
|
|
|
|
Mac: create.Mac,
|
|
|
|
|
IP: orm.Inet{Addr: addr},
|
|
|
|
|
Host: create.Host,
|
2026-06-09 16:30:19 +08:00
|
|
|
Port: create.Port,
|
2026-04-18 11:15:29 +08:00
|
|
|
Secret: create.Secret,
|
|
|
|
|
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
|
|
|
|
|
Status: u.Else(create.Status, m.ProxyStatusOffline),
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Proxy.Create(proxy); err != nil {
|
|
|
|
|
return core.NewServErr("保存代理数据失败", err)
|
|
|
|
|
}
|
|
|
|
|
if err := rebuildFreeChans(proxy.ID, addr); err != nil {
|
|
|
|
|
return core.NewServErr("初始化代理通道失败", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UpdateProxy struct {
|
|
|
|
|
ID int32 `json:"id" validate:"required"`
|
|
|
|
|
Mac *string `json:"mac"`
|
|
|
|
|
IP *string `json:"ip"`
|
|
|
|
|
Host *string `json:"host"`
|
2026-06-09 16:30:19 +08:00
|
|
|
Port *int `json:"port"`
|
2026-04-18 11:15:29 +08:00
|
|
|
Secret *string `json:"secret"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *proxyService) Update(update *UpdateProxy) error {
|
|
|
|
|
simples := make([]field.AssignExpr, 0)
|
|
|
|
|
hasSideEffect := false
|
|
|
|
|
|
|
|
|
|
if update.Mac != nil {
|
|
|
|
|
hasSideEffect = true
|
|
|
|
|
simples = append(simples, q.Proxy.Mac.Value(*update.Mac))
|
|
|
|
|
}
|
|
|
|
|
if update.IP != nil {
|
|
|
|
|
addr, err := netip.ParseAddr(*update.IP)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return core.NewServErr("IP地址格式错误", err)
|
|
|
|
|
}
|
|
|
|
|
hasSideEffect = true
|
|
|
|
|
simples = append(simples, q.Proxy.IP.Value(orm.Inet{Addr: addr}))
|
|
|
|
|
}
|
|
|
|
|
if update.Host != nil {
|
|
|
|
|
simples = append(simples, q.Proxy.Host.Value(*update.Host))
|
|
|
|
|
}
|
2026-06-09 16:30:19 +08:00
|
|
|
if update.Port != nil {
|
|
|
|
|
simples = append(simples, q.Proxy.Port.Value(*update.Port))
|
|
|
|
|
}
|
2026-04-18 11:15:29 +08:00
|
|
|
if update.Secret != nil {
|
|
|
|
|
hasSideEffect = true
|
|
|
|
|
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(simples) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if hasSideEffect {
|
|
|
|
|
used, err := hasUsedChans(update.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return core.NewServErr("检查代理通道状态失败", err)
|
|
|
|
|
}
|
|
|
|
|
if used {
|
|
|
|
|
return core.NewBizErr("代理存在未关闭通道,禁止修改")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rs, err := q.Proxy.
|
|
|
|
|
Where(
|
|
|
|
|
q.Proxy.ID.Eq(update.ID),
|
|
|
|
|
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
|
|
|
|
|
).
|
|
|
|
|
UpdateSimple(simples...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if rs.RowsAffected == 0 {
|
|
|
|
|
return core.NewBizErr("代理未下线,禁止修改")
|
|
|
|
|
}
|
2025-12-01 12:42:51 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:07:46 +08:00
|
|
|
func (s *proxyService) SyncPorts(id int32) error {
|
|
|
|
|
proxy, err := findOfflineProxy(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
used, err := hasUsedChans(id)
|
2026-06-10 14:32:45 +08:00
|
|
|
if err != nil {
|
2026-06-11 15:07:46 +08:00
|
|
|
return core.NewServErr("检查代理通道状态失败", err)
|
2026-06-10 14:32:45 +08:00
|
|
|
}
|
2026-06-11 15:07:46 +08:00
|
|
|
if used {
|
|
|
|
|
return core.NewBizErr("代理存在未关闭通道,禁止重建端口池")
|
2026-06-10 14:32:45 +08:00
|
|
|
}
|
2026-06-11 15:07:46 +08:00
|
|
|
|
2026-06-10 14:32:45 +08:00
|
|
|
return rebuildFreeChans(id, proxy.IP.Addr)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:07:46 +08:00
|
|
|
func (s *proxyService) SyncChains(id int32) error {
|
|
|
|
|
proxy, err := findOfflineProxy(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if proxy.Type != m.ProxyTypeGost {
|
|
|
|
|
return core.NewBizErr("仅 GOST 代理支持重建代理链")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chains, err := buildGostChainsFromEdges()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := proxyGost(proxy)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return core.NewServErr("创建 GOST 客户端失败", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldChains, err := client.ListChains()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return core.NewServErr("查询 GOST chains 失败", err)
|
|
|
|
|
}
|
|
|
|
|
for _, chain := range oldChains {
|
|
|
|
|
if err := client.DeleteChain(chain.Name); err != nil {
|
|
|
|
|
return core.NewServErr(fmt.Sprintf("删除 GOST chain 失败: %s", chain.Name), err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, chain := range chains {
|
|
|
|
|
if err := client.CreateChain(chain); err != nil {
|
|
|
|
|
return core.NewServErr(fmt.Sprintf("创建 GOST chain 失败: %s", chain.Name), err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := client.SaveConfig(); err != nil {
|
|
|
|
|
return core.NewServErr("保存 GOST 配置失败", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findOfflineProxy(id int32) (*m.Proxy, error) {
|
|
|
|
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Take()
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, core.NewBizErr("代理不存在")
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, core.NewServErr("获取代理数据失败", err)
|
|
|
|
|
}
|
|
|
|
|
if proxy.Status != m.ProxyStatusOffline {
|
|
|
|
|
return nil, core.NewBizErr("代理未下线,禁止同步")
|
|
|
|
|
}
|
|
|
|
|
return proxy, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildGostChainsFromEdges() ([]*g.GostChainConfig, error) {
|
|
|
|
|
edges, err := q.Edge.
|
|
|
|
|
Where(q.Edge.Type.Eq(int(m.EdgeTypeGostChain))).
|
|
|
|
|
Order(q.Edge.ID).
|
|
|
|
|
Find()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, core.NewServErr("查询 GOST edge 数据失败", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chains := make([]*g.GostChainConfig, len(edges))
|
|
|
|
|
for i, edge := range edges {
|
|
|
|
|
if strings.TrimSpace(edge.Mac) == "" {
|
|
|
|
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %d chain 名称为空", edge.ID))
|
|
|
|
|
}
|
|
|
|
|
if !edge.IP.Addr.IsValid() {
|
|
|
|
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s IP 无效", edge.Mac))
|
|
|
|
|
}
|
|
|
|
|
if edge.Port == nil || *edge.Port == 0 {
|
|
|
|
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s 端口为空", edge.Mac))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chains[i] = &g.GostChainConfig{
|
|
|
|
|
Name: edge.Mac,
|
|
|
|
|
Hops: []g.GostHopConfig{{
|
|
|
|
|
Nodes: []g.GostNodeConfig{{
|
|
|
|
|
Addr: netip.AddrPortFrom(edge.IP.Addr, *edge.Port).String(),
|
|
|
|
|
Connector: g.GostConnectorConfig{
|
|
|
|
|
Type: "socks5",
|
|
|
|
|
},
|
|
|
|
|
Dialer: g.GostDialerConfig{
|
|
|
|
|
Type: "tcp",
|
|
|
|
|
},
|
|
|
|
|
}},
|
|
|
|
|
}},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return chains, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 11:15:29 +08:00
|
|
|
func (s *proxyService) Remove(id int32) error {
|
|
|
|
|
used, err := hasUsedChans(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return core.NewServErr("检查代理通道状态失败", err)
|
|
|
|
|
}
|
|
|
|
|
if used {
|
|
|
|
|
return core.NewBizErr("代理存在未关闭通道,禁止删除")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rs, err := q.Proxy.
|
|
|
|
|
Where(
|
|
|
|
|
q.Proxy.ID.Eq(id),
|
|
|
|
|
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
|
|
|
|
|
).
|
|
|
|
|
UpdateColumn(q.Proxy.DeletedAt, time.Now())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if rs.RowsAffected == 0 {
|
|
|
|
|
return core.NewBizErr("代理未下线,禁止删除")
|
|
|
|
|
}
|
|
|
|
|
if err := remChans(id); err != nil {
|
|
|
|
|
return core.NewServErr("注销代理通道失败", err)
|
|
|
|
|
}
|
2025-12-01 12:42:51 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-18 11:15:29 +08:00
|
|
|
|
|
|
|
|
type UpdateProxyStatus struct {
|
|
|
|
|
ID int32 `json:"id" validate:"required"`
|
2026-04-21 16:32:07 +08:00
|
|
|
Status m.ProxyStatus `json:"status"`
|
2026-04-18 11:15:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
|
2026-05-13 16:17:57 +08:00
|
|
|
return q.Q.Transaction(func(tx *q.Query) error {
|
2026-04-18 11:15:29 +08:00
|
|
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(update.ID)).Take()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if proxy.Status == update.Status {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if update.Status == m.ProxyStatusOnline {
|
|
|
|
|
if err := rebuildFreeChans(proxy.ID, proxy.IP.Addr); err != nil {
|
|
|
|
|
return core.NewServErr("初始化代理通道失败", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = q.Proxy.
|
|
|
|
|
Where(q.Proxy.ID.Eq(update.ID)).
|
|
|
|
|
UpdateSimple(q.Proxy.Status.Value(int(update.Status)))
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-05-18 13:54:01 +08:00
|
|
|
|
|
|
|
|
func proxyGateway(proxy *m.Proxy) (g.GatewayClient, error) {
|
|
|
|
|
|
|
|
|
|
secret := strings.Split(u.Z(proxy.Secret), ":")
|
|
|
|
|
if len(secret) != 2 {
|
|
|
|
|
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
|
|
|
|
|
}
|
|
|
|
|
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
|
|
|
|
|
|
|
|
|
return gateway, nil
|
|
|
|
|
}
|