2 Commits

Author SHA1 Message Date
25cacf0bca 完善节点筛选机制 2026-06-12 16:51:08 +08:00
513fe78815 实现手动 proxy 同步接口 2026-06-11 17:52:21 +08:00
13 changed files with 402 additions and 104 deletions

View File

@@ -1,6 +1,6 @@
## TODO ## TODO
- 日志记录 - edge.area_id 可为空,代表节点无固定地区
- 后台展示 mac, ip:port实际地区 - 后台展示 mac, ip:port实际地区
上传文件平铺到 uploads不分子文件夹 上传文件平铺到 uploads不分子文件夹

View File

@@ -5010,6 +5010,42 @@ paths:
default: default:
$ref: "#/components/responses/PlainTextError" $ref: "#/components/responses/PlainTextError"
/api/admin/proxy/sync/ports:
post:
tags: [admin/proxy]
summary: 重建端口池
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/IdRequest"
responses:
"200":
description: 成功,无响应体
default:
$ref: "#/components/responses/PlainTextError"
/api/admin/proxy/sync/chains:
post:
tags: [admin/proxy]
summary: 重建代理链
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/IdRequest"
responses:
"200":
description: 成功,无响应体
default:
$ref: "#/components/responses/PlainTextError"
/api/admin/proxy/remove: /api/admin/proxy/remove:
post: post:
tags: [admin/proxy] tags: [admin/proxy]

View File

@@ -669,7 +669,7 @@ create table edge (
ip inet not null, ip inet not null,
port int, port int,
isp int not null, isp int not null,
area_id int not null, area_id int,
status int not null default 0, status int not null default 0,
rtt int default 0, rtt int default 0,
loss int default 0, loss int default 0,

View File

@@ -52,7 +52,8 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
case errors.As(err, &servErr): case errors.As(err, &servErr):
code = fiber.StatusInternalServerError code = fiber.StatusInternalServerError
message = err.Error() slog.Warn("服务端错误", slog.String("error", servErr.Error()))
message = "服务端错误"
case errors.As(err, &timeErr): case errors.As(err, &timeErr):
code = fiber.StatusBadRequest code = fiber.StatusBadRequest

View File

@@ -19,7 +19,11 @@ func IsGostNotFound(err error) bool {
} }
type GostClient interface { type GostClient interface {
ListChains() ([]*GostChainConfig, error)
GetChain(name string) (*GostChainConfig, error) GetChain(name string) (*GostChainConfig, error)
CreateChain(chain *GostChainConfig) error
DeleteChain(name string) error
SaveConfig() error
CreateService(service *GostServiceConfig) error CreateService(service *GostServiceConfig) error
DeleteService(name string) error DeleteService(name string) error
CreateAuther(auther *GostAutherConfig) error CreateAuther(auther *GostAutherConfig) error
@@ -54,15 +58,37 @@ func NewGost(host string, port int, pathPrefix, username, password string) GostC
} }
type GostChainConfig struct { type GostChainConfig struct {
Name string `json:"name"` Name string `json:"name"`
Hops []GostHopConfig `json:"hops,omitempty"`
}
type GostHopConfig struct {
Name string `json:"name,omitempty"`
Nodes []GostNodeConfig `json:"nodes,omitempty"`
}
type GostNodeConfig struct {
Name string `json:"name,omitempty"`
Addr string `json:"addr"`
Connector GostConnectorConfig `json:"connector"`
Dialer GostDialerConfig `json:"dialer"`
}
type GostConnectorConfig struct {
Type string `json:"type"`
}
type GostDialerConfig struct {
Type string `json:"type"`
} }
type GostServiceConfig struct { type GostServiceConfig struct {
Name string `json:"name"` Name string `json:"name"`
Addr string `json:"addr"` Addr string `json:"addr"`
Admission string `json:"admission,omitempty"` Admission string `json:"admission,omitempty"`
Handler GostHandlerConfig `json:"handler"` Handler GostHandlerConfig `json:"handler"`
Listener GostListenerConfig `json:"listener"` Listener GostListenerConfig `json:"listener"`
Recorders []GostRecorderConfig `json:"recorders,omitempty"`
} }
type GostHandlerConfig struct { type GostHandlerConfig struct {
@@ -75,6 +101,11 @@ type GostListenerConfig struct {
Type string `json:"type"` Type string `json:"type"`
} }
type GostRecorderConfig struct {
Name string `json:"name"`
Record string `json:"record"`
}
type GostAutherConfig struct { type GostAutherConfig struct {
Name string `json:"name"` Name string `json:"name"`
Auths []GostAuthConfig `json:"auths"` Auths []GostAuthConfig `json:"auths"`
@@ -115,6 +146,40 @@ func (c *gostClient) GetChain(name string) (*GostChainConfig, error) {
return &GostChainConfig{Name: name}, nil return &GostChainConfig{Name: name}, nil
} }
func (c *gostClient) ListChains() ([]*GostChainConfig, error) {
body, err := c.get("/config/chains")
if err != nil {
return nil, err
}
if len(body) == 0 {
return nil, nil
}
var resp struct {
Data struct {
Count int `json:"count"`
List []*GostChainConfig `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parse gost chain list failed %s: %w", string(body), err)
}
return resp.Data.List, nil
}
func (c *gostClient) CreateChain(chain *GostChainConfig) error {
return c.create("/config/chains", chain)
}
func (c *gostClient) DeleteChain(name string) error {
return c.delete("/config/chains/" + url.PathEscape(name))
}
func (c *gostClient) SaveConfig() error {
return c.create("/config", nil)
}
func (c *gostClient) CreateService(service *GostServiceConfig) error { func (c *gostClient) CreateService(service *GostServiceConfig) error {
return c.create("/config/services", service) return c.create("/config/services", service)
} }

107
web/globals/gost_test.go Normal file
View File

@@ -0,0 +1,107 @@
package globals
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestGostClientChainOperations(t *testing.T) {
var (
created *GostChainConfig
deleted []string
saved bool
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "user" || password != "pass" {
t.Errorf("unexpected auth: ok=%v username=%q password=%q", ok, username, password)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/config/chains":
_ = json.NewEncoder(w).Encode(map[string]any{
"count": 2,
"list": []map[string]any{
{"name": "old-a"},
{"name": "old-b"},
},
})
case r.Method == http.MethodPost && r.URL.Path == "/api/config/chains":
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
t.Errorf("Decode chain failed: %v", err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-a":
deleted = append(deleted, "old-a")
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-b":
deleted = append(deleted, "old-b")
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodPost && r.URL.Path == "/api/config":
saved = true
_, _ = w.Write([]byte(`{}`))
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
defer server.Close()
client := NewGost(server.URL, 9700, "/api", "user", "pass")
chains, err := client.ListChains()
if err != nil {
t.Fatalf("ListChains returned error: %v", err)
}
if len(chains) != 2 || chains[0].Name != "old-a" || chains[1].Name != "old-b" {
t.Fatalf("unexpected chains: %#v", chains)
}
if err := client.DeleteChain(chains[0].Name); err != nil {
t.Fatalf("DeleteChain old-a returned error: %v", err)
}
if err := client.DeleteChain(chains[1].Name); err != nil {
t.Fatalf("DeleteChain old-b returned error: %v", err)
}
if len(deleted) != 2 {
t.Fatalf("unexpected deleted chains: %#v", deleted)
}
err = client.CreateChain(&GostChainConfig{
Name: "edge-a",
Hops: []GostHopConfig{{
Nodes: []GostNodeConfig{{
Addr: "192.0.2.1:1080",
Connector: GostConnectorConfig{Type: "socks5"},
Dialer: GostDialerConfig{Type: "tcp"},
}},
}},
})
if err != nil {
t.Fatalf("CreateChain returned error: %v", err)
}
if created == nil || created.Name != "edge-a" {
t.Fatalf("unexpected created chain: %#v", created)
}
if len(created.Hops) != 1 || len(created.Hops[0].Nodes) != 1 {
t.Fatalf("unexpected created chain hops: %#v", created.Hops)
}
node := created.Hops[0].Nodes[0]
if node.Addr != "192.0.2.1:1080" || node.Connector.Type != "socks5" || node.Dialer.Type != "tcp" {
t.Fatalf("unexpected created node: %#v", node)
}
if err := client.SaveConfig(); err != nil {
t.Fatalf("SaveConfig returned error: %v", err)
}
if !saved {
t.Fatal("expected SaveConfig request")
}
}

View File

@@ -230,23 +230,6 @@ type CreateChannelReqV3 struct {
Isp *int `json:"isp"` Isp *int `json:"isp"`
} }
func buildCreateChannelResp(result []*m.Channel, protocol int, authType s.ChannelAuthType) []*CreateChannelRespItem {
resp := make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if authType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return resp
}
type CreateChannelRespItem struct { type CreateChannelRespItem struct {
Proto int `json:"-"` Proto int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
@@ -474,3 +457,20 @@ type SyncChannelClearExpiredByAdminReq struct {
type SyncChannelClearExpiredByAdminResp struct { type SyncChannelClearExpiredByAdminResp struct {
Count int `json:"count"` Count int `json:"count"`
} }
func buildCreateChannelResp(result []*m.Channel, protocol int, authType s.ChannelAuthType) []*CreateChannelRespItem {
resp := make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if authType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return resp
}

View File

@@ -105,7 +105,7 @@ func UpdateProxyStatus(c *fiber.Ctx) error {
return c.JSON(nil) return c.JSON(nil)
} }
func SyncProxyPool(c *fiber.Ctx) error { func SyncProxyPorts(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil { if err != nil {
return err return err
@@ -116,7 +116,25 @@ func SyncProxyPool(c *fiber.Ctx) error {
return err return err
} }
if err := s.Proxy.SyncPool(req.Id); err != nil { if err := s.Proxy.SyncPorts(req.Id); err != nil {
return err
}
return c.JSON(nil)
}
func SyncProxyChains(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.SyncChains(req.Id); err != nil {
return err return err
} }

View File

@@ -14,7 +14,7 @@ type Edge struct {
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP
Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口 Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动 ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动
AreaID int32 `json:"area_id" gorm:"column:area_id"` // 城市地区ID AreaID *int32 `json:"area_id,omitempty" gorm:"column:area_id"` // 城市地区ID
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常 Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟 RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率 Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
@@ -41,6 +41,7 @@ const (
type EdgeISP int type EdgeISP int
const ( const (
EdgeISPUnknown EdgeISP = 0 // 未知/任意
EdgeISPTelecom EdgeISP = 1 // 电信 EdgeISPTelecom EdgeISP = 1 // 电信
EdgeISPUnicom EdgeISP = 2 // 联通 EdgeISPUnicom EdgeISP = 2 // 联通
EdgeISPMobile EdgeISP = 3 // 移动 EdgeISPMobile EdgeISP = 3 // 移动

View File

@@ -1,13 +1,11 @@
package web package web
import ( import (
"fmt"
"platform/pkg/env" "platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/core" "platform/web/core"
"platform/web/globals" "platform/web/globals"
"platform/web/handlers" "platform/web/handlers"
"strings"
"time" "time"
q "platform/web/queries" q "platform/web/queries"
@@ -51,46 +49,6 @@ func ApplyRouters(app *fiber.App) {
} }
return ctx.JSON(resp) return ctx.JSON(resp)
}) })
debug.Get("/gen-edge", func(ctx *fiber.Ctx) error {
areas, err := q.Area.Where(q.Area.Level.Eq(2)).Find()
if err != nil {
return err
}
sb := strings.Builder{}
sb.WriteString("INSERT INTO edge (type, version, mac, ip, port, isp, area_id, status) VALUES\n")
for i, area := range areas {
// jh edges
for j := range 20 {
fmt.Fprintf(&sb, "(2, 1, 'jh-%d-%d-%d', '192.168.50.%d', %d, 0, %d, 1)", area.ID, j+1, i+44001, j+2, i+44001, area.ID)
sb.WriteString(",\n")
}
// jg edges
for j := range 10 {
var ip string
var n int
if i < 100 {
ip = "192.168.0.232"
n = 1
} else if i < 200 {
ip = "192.168.59.236"
n = 2
} else {
ip = "192.168.59.237"
n = 3
}
fmt.Fprintf(&sb, "(2, 1, 'jg-%d-%d-%d', '%s', %d, 0, %d, 1)", area.ID, n, i*10+j+20001, ip, i*10+j+20001, area.ID)
if i < len(areas)-1 || j < 9 {
sb.WriteString(",\n")
}
}
}
sb.WriteString(";\n")
return ctx.SendString(sb.String())
})
} }
} }
@@ -129,9 +87,6 @@ func clientRouter(api fiber.Router) {
client.Post("/verify/sms", handlers.SendSmsCode) client.Post("/verify/sms", handlers.SendSmsCode)
// 网关 // 网关
proxy := client.Group("/proxy")
proxy.Post("/sync-pool", handlers.SyncProxyPool)
// 通道管理 // 通道管理
channel := client.Group("/channel") channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels) channel.Post("/remove", handlers.RemoveChannels)
@@ -277,6 +232,8 @@ func adminRouter(api fiber.Router) {
proxy.Post("/create", handlers.CreateProxy) proxy.Post("/create", handlers.CreateProxy)
proxy.Post("/update", handlers.UpdateProxy) proxy.Post("/update", handlers.UpdateProxy)
proxy.Post("/update/status", handlers.UpdateProxyStatus) proxy.Post("/update/status", handlers.UpdateProxyStatus)
proxy.Post("/sync/ports", handlers.SyncProxyPorts)
proxy.Post("/sync/chains", handlers.SyncProxyChains)
proxy.Post("/remove", handlers.RemoveProxy) proxy.Post("/remove", handlers.RemoveProxy)
// trade 交易 // trade 交易

View File

@@ -40,6 +40,10 @@ type channelServer struct {
} }
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) {
if edgeFilter == nil {
edgeFilter = &EdgeFilter{}
}
var area *m.Area var area *m.Area
if edgeFilter.AreaID != nil { if edgeFilter.AreaID != nil {
var err error var err error
@@ -265,8 +269,8 @@ func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) erro
BatchNo: ctx.BatchNo, BatchNo: ctx.BatchNo,
Count: int32(ctx.Count), Count: int32(ctx.Count),
ISP: u.X(ctx.Filter.Isp.String()), ISP: u.X(ctx.Filter.Isp.String()),
Prov: prov, Prov: u.Ternary(ctx.Filter.AreaID != nil, prov, nil),
City: city, City: u.Ternary(ctx.Filter.AreaID != nil, city, nil),
IP: orm.Inet{Addr: ctx.Source}, IP: orm.Inet{Addr: ctx.Source},
Time: ctx.Now, Time: ctx.Now,
}); err != nil { }); err != nil {
@@ -397,7 +401,7 @@ func selectProxyByType(proxyType m.ProxyType, count int) (*m.Proxy, error) {
} }
} }
if maxCount < count { if maxCount < count {
return nil, core.NewBizErr("无可用代理") return nil, core.NewBizErr("无空闲代理")
} }
return bestProxy, nil return bestProxy, nil

View File

@@ -10,7 +10,7 @@ import (
q "platform/web/queries" q "platform/web/queries"
"strings" "strings"
"gorm.io/gen" "gorm.io/gen/field"
) )
type channelGostProvider struct{} type channelGostProvider struct{}
@@ -37,7 +37,7 @@ func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channel
serviceName := gostServiceName(ctx.BatchNo, port) serviceName := gostServiceName(ctx.BatchNo, port)
channel := newBaseChannel(ctx, port) channel := newBaseChannel(ctx, port)
channel.EdgeID = u.P(edge.ID) channel.EdgeID = u.P(edge.ID)
channel.EdgeRef = u.P(serviceName) channel.EdgeRef = u.P(edge.Mac)
channel.IP = u.P(edge.IP) channel.IP = u.P(edge.IP)
service := &g.GostServiceConfig{ service := &g.GostServiceConfig{
@@ -50,6 +50,9 @@ func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channel
Listener: g.GostListenerConfig{ Listener: g.GostListenerConfig{
Type: "tcp", Type: "tcp",
}, },
Recorders: []g.GostRecorderConfig{
{Name: "record-file", Record: "recorder.service.handler"},
},
} }
if ctx.AuthWhitelist { if ctx.AuthWhitelist {
@@ -138,40 +141,34 @@ func (s *channelGostProvider) selectEdge(filter *EdgeFilter, area *m.Area, count
filter = &EdgeFilter{} filter = &EdgeFilter{}
} }
conds := []gen.Condition{ do := q.Edge.Where(
q.Edge.Type.Eq(int(m.EdgeTypeGostChain)), q.Edge.Type.Eq(int(m.EdgeTypeGostChain)),
q.Edge.Status.Eq(int(m.EdgeStatusNormal)), q.Edge.Status.Eq(int(m.EdgeStatusNormal)),
)
if filter.Isp != nil {
do = do.Where(q.Edge.ISP.In(int(m.EdgeISPUnknown), int(*filter.Isp)))
} }
if isp := u.X(filter.Isp.String()); isp != nil {
conds = append(conds, q.Edge.ISP.Eq(int(*filter.Isp)))
}
query := q.Edge.Where(conds...)
if area != nil { if area != nil {
switch area.Level { switch area.Level {
case m.AreaLevelProvince: case m.AreaLevelProvince:
edgeArea := q.Area.As("EdgeArea") edgeArea := q.Area.As("EdgeArea")
query = query. do = do.
Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID)). Where(edgeArea.ParentID.Eq(area.ID)).
Where(edgeArea.ParentID.Eq(area.ID)) Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID))
case m.AreaLevelCity: case m.AreaLevelCity:
query = query.Where(q.Edge.AreaID.Eq(area.ID)) do = do.Where(q.Edge.AreaID.Eq(area.ID))
default: default:
return nil, core.NewBizErr("地区层级不支持") return nil, core.NewBizErr("地区层级不支持")
} }
} }
edges, err := query. edges, err := do.
Order(q.Edge.ID). Order(field.NewUnsafeFieldRaw("random()")).
Limit(count). Limit(count).
Find() Find()
if err != nil { if err != nil {
return nil, core.NewBizErr("查询可用节点失败", err) return nil, core.NewBizErr("查询可用节点失败", err)
} }
return expandGostEdges(edges, count)
}
func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) {
if len(edges) == 0 { if len(edges) == 0 {
return nil, core.NewBizErr("地区可用节点数量不足") return nil, core.NewBizErr("地区可用节点数量不足")
} }
@@ -180,6 +177,7 @@ func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) {
for i := range count { for i := range count {
result[i] = edges[i%len(edges)] result[i] = edges[i%len(edges)]
} }
return result, nil return result, nil
} }

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"platform/pkg/u" "platform/pkg/u"
@@ -14,6 +15,7 @@ import (
"time" "time"
"gorm.io/gen/field" "gorm.io/gen/field"
"gorm.io/gorm"
) )
var Proxy = &proxyService{} var Proxy = &proxyService{}
@@ -23,11 +25,20 @@ type proxyService struct{}
func hasUsedChans(proxyID int32) (bool, error) { func hasUsedChans(proxyID int32) (bool, error) {
ctx := context.Background() ctx := context.Background()
pattern := usedChansKey(proxyID, "*") pattern := usedChansKey(proxyID, "*")
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result() var cursor uint64
if err != nil { for {
return false, err 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
} }
return len(keys) > 0, nil
} }
func rebuildFreeChans(proxyID int32, addr netip.Addr) error { func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
@@ -161,17 +172,117 @@ func (s *proxyService) Update(update *UpdateProxy) error {
return nil return nil
} }
func (s *proxyService) SyncPool(id int32) error { func (s *proxyService) SyncPorts(id int32) error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Select(q.Proxy.ID, q.Proxy.IP).First() proxy, err := findOfflineProxy(id)
if err != nil { if err != nil {
return core.NewServErr("获取代理数据失败", err) return err
} }
if proxy == nil {
return core.NewBizErr("代理不存在") used, err := hasUsedChans(id)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
} }
if used {
return core.NewBizErr("代理存在未关闭通道,禁止重建端口池")
}
return rebuildFreeChans(id, proxy.IP.Addr) return rebuildFreeChans(id, proxy.IP.Addr)
} }
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
}
func (s *proxyService) Remove(id int32) error { func (s *proxyService) Remove(id int32) error {
used, err := hasUsedChans(id) used, err := hasUsedChans(id)
if err != nil { if err != nil {