mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-14 16:54:24 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02ff215f99 | |||
| 2a4e7777ab | |||
| eac94a5719 | |||
| 96fcd0fc57 | |||
| 06869aedfd | |||
| 6a201131a3 | |||
| 1130a55ef5 | |||
| 265cd0a50e | |||
| e7ffa77b15 | |||
| 51cbd4b9de | |||
| e122e7460d | |||
| 7c898154b3 | |||
| 1d19d68019 | |||
| 09c58e2298 | |||
| 583905b7ed | |||
| 6e3f045b9b | |||
| ec41202b3c | |||
| 1ee7dea8b4 | |||
| 2d69350bab | |||
| c984e5b62a | |||
| 5b79b11101 | |||
| 2c22e600f7 | |||
| aef284c474 | |||
| 0443cd9ceb | |||
| 3337422775 | |||
| 3e046fc80e | |||
| 0273bc6921 | |||
| be095057bd | |||
| a98057d06a | |||
| 7d47903541 | |||
| a7aabdd1dd |
@@ -20,11 +20,9 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: vite-frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
@@ -100,11 +100,11 @@ jobs:
|
||||
|
||||
- name: Build GOST binary (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost-amd64
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ needs.check-version.outputs.version }}" -o gost-amd64
|
||||
|
||||
- name: Build GOST binary (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ needs.check-version.outputs.version }}" -o gost-arm64
|
||||
|
||||
- name: Compress with UPX
|
||||
working-directory: ./go-gost
|
||||
|
||||
@@ -16,24 +16,13 @@
|
||||
---
|
||||
### Docker Compose部署
|
||||
#### 快速部署
|
||||
面板端(稳定版):
|
||||
面板端:
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(稳定版):
|
||||
节点端:
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
面板端(开发版):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/beta/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(开发版):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/beta/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
#### 默认管理员账号
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ func main() {
|
||||
log := xlogger.NewLogger()
|
||||
logger.SetDefault(log)
|
||||
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.2")
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, version)
|
||||
defer wsReporter.Stop()
|
||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
package main
|
||||
|
||||
var (
|
||||
version = "3.1.0"
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
@@ -109,3 +109,23 @@ func LoggerFromContext(ctx context.Context) logger.Logger {
|
||||
v, _ := ctx.Value(keyLogger).(logger.Logger)
|
||||
return v
|
||||
}
|
||||
|
||||
// excludeNodesKey saves the list of node addresses to exclude during selection.
|
||||
// This is used for failover retry logic - when a node fails, it gets added to
|
||||
// the exclude list so the next Select() call will skip it.
|
||||
type excludeNodesKey struct{}
|
||||
|
||||
var (
|
||||
keyExcludeNodes = &excludeNodesKey{}
|
||||
)
|
||||
|
||||
// ContextWithExcludeNodes returns a context with the list of node addresses to exclude.
|
||||
func ContextWithExcludeNodes(ctx context.Context, nodes []string) context.Context {
|
||||
return context.WithValue(ctx, keyExcludeNodes, nodes)
|
||||
}
|
||||
|
||||
// ExcludeNodesFromContext returns the list of node addresses to exclude from selection.
|
||||
func ExcludeNodesFromContext(ctx context.Context) []string {
|
||||
v, _ := ctx.Value(keyExcludeNodes).([]string)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -176,51 +176,98 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand
|
||||
}
|
||||
}
|
||||
|
||||
target := &chain.Node{}
|
||||
if h.hop != nil {
|
||||
target = h.hop.Select(ctx,
|
||||
hop.ProtocolSelectOption(proto),
|
||||
)
|
||||
}
|
||||
if target == nil {
|
||||
err := errors.New("node not available")
|
||||
return err
|
||||
// Determine max retry attempts
|
||||
maxRetries := h.md.maxRetries
|
||||
if maxRetries <= 0 {
|
||||
// Default: try all available nodes
|
||||
if nl, ok := h.hop.(hop.NodeList); ok {
|
||||
maxRetries = len(nl.Nodes())
|
||||
}
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 1
|
||||
}
|
||||
}
|
||||
|
||||
addr := target.Addr
|
||||
if opts := target.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
network = opts.Network
|
||||
default:
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
addr += ":0"
|
||||
var triedNodes []string
|
||||
var lastErr error
|
||||
var cc net.Conn
|
||||
|
||||
h.options.Logger.Debugf("[handler.retry] starting retry loop: maxRetries=%d", maxRetries)
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Select a target node, excluding previously tried nodes
|
||||
selectCtx := ctxvalue.ContextWithExcludeNodes(ctx, triedNodes)
|
||||
var target *chain.Node
|
||||
if h.hop != nil {
|
||||
target = h.hop.Select(selectCtx,
|
||||
hop.ProtocolSelectOption(proto),
|
||||
)
|
||||
}
|
||||
if target == nil {
|
||||
h.options.Logger.Debugf("[handler.retry] attempt=%d target=nil, triedNodes=%v", attempt, triedNodes)
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return errors.New("node not available")
|
||||
}
|
||||
|
||||
h.options.Logger.Debugf("[handler.retry] attempt=%d selected node=%s addr=%s", attempt, target.Name, target.Addr)
|
||||
|
||||
// Track this node as tried
|
||||
triedNodes = append(triedNodes, target.Addr)
|
||||
|
||||
addr := target.Addr
|
||||
if opts := target.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
network = opts.Network
|
||||
default:
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
addr += ":0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ro.Network = network
|
||||
ro.Host = addr
|
||||
ro.Network = network
|
||||
ro.Host = addr
|
||||
|
||||
var buf bytes.Buffer
|
||||
cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), network, addr)
|
||||
ro.Route = buf.String()
|
||||
if err != nil {
|
||||
// TODO: the router itself may be failed due to the failed node in the router,
|
||||
// the dead marker may be a wrong operation.
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
var buf bytes.Buffer
|
||||
cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), network, addr)
|
||||
ro.Route = buf.String()
|
||||
if err != nil {
|
||||
// Mark node as failed for future selections
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
h.options.Logger.Debugf("[handler.retry] attempt=%d dial failed, marked node=%s count=%d err=%v",
|
||||
attempt, target.Addr, marker.Count(), err)
|
||||
}
|
||||
lastErr = err
|
||||
// Try next node
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
xnet.Transport(conn, cc)
|
||||
// Success - reset marker and proceed
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
return nil
|
||||
if err := xnet.Transport(conn, cc); err != nil {
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
h.options.Logger.Debugf("[handler.transport] transport failed, marked node=%s count=%d err=%v",
|
||||
target.Addr, marker.Count(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return errors.New("all nodes failed")
|
||||
}
|
||||
|
||||
func (h *forwardHandler) checkRateLimit(addr net.Addr) bool {
|
||||
|
||||
@@ -25,6 +25,12 @@ type metadata struct {
|
||||
privateKey crypto.PrivateKey
|
||||
alpn string
|
||||
mitmBypass bypass.Bypass
|
||||
|
||||
// maxRetries specifies the maximum number of failover retry attempts.
|
||||
// When a target node fails, the handler will try the next available node.
|
||||
// 0 means use the total number of available nodes (try all nodes once).
|
||||
// Default: 0 (try all available nodes)
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
@@ -56,5 +62,8 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
h.md.alpn = mdutil.GetString(md, "mitm.alpn")
|
||||
h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass"))
|
||||
|
||||
// maxRetries: 0 means try all available nodes (default behavior)
|
||||
h.md.maxRetries = mdutil.GetInt(md, "maxRetries", "retry.max")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -204,68 +204,101 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand
|
||||
}
|
||||
}
|
||||
|
||||
var target *chain.Node
|
||||
if host != "" {
|
||||
target = &chain.Node{
|
||||
Addr: host,
|
||||
// Determine max retry attempts
|
||||
maxRetries := h.md.maxRetries
|
||||
if maxRetries <= 0 {
|
||||
// Default: try all available nodes
|
||||
if nl, ok := h.hop.(hop.NodeList); ok {
|
||||
maxRetries = len(nl.Nodes())
|
||||
}
|
||||
}
|
||||
if h.hop != nil {
|
||||
target = h.hop.Select(ctx,
|
||||
hop.ProtocolSelectOption(proto),
|
||||
)
|
||||
}
|
||||
if target == nil {
|
||||
err := errors.New("node not available")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if opts := target.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
network = opts.Network
|
||||
default:
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 1
|
||||
}
|
||||
}
|
||||
|
||||
ro.Network = network
|
||||
ro.Host = target.Addr
|
||||
var triedNodes []string
|
||||
var lastErr error
|
||||
var cc net.Conn
|
||||
|
||||
log = log.WithFields(map[string]any{
|
||||
"node": target.Name,
|
||||
"dst": fmt.Sprintf("%s/%s", target.Addr, network),
|
||||
})
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Select a target node, excluding previously tried nodes
|
||||
selectCtx := ctxvalue.ContextWithExcludeNodes(ctx, triedNodes)
|
||||
var target *chain.Node
|
||||
if host != "" {
|
||||
target = chain.NewNode("", host)
|
||||
}
|
||||
if h.hop != nil {
|
||||
target = h.hop.Select(selectCtx,
|
||||
hop.ProtocolSelectOption(proto),
|
||||
)
|
||||
}
|
||||
if target == nil {
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
err := errors.New("node not available")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr)
|
||||
// Track this node as tried
|
||||
triedNodes = append(triedNodes, target.Addr)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), network, target.Addr)
|
||||
ro.Route = buf.String()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
// TODO: the router itself may be failed due to the failed node in the router,
|
||||
// the dead marker may be a wrong operation.
|
||||
if opts := target.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
network = opts.Network
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
ro.Network = network
|
||||
ro.Host = target.Addr
|
||||
|
||||
targetLog := log.WithFields(map[string]any{
|
||||
"node": target.Name,
|
||||
"dst": fmt.Sprintf("%s/%s", target.Addr, network),
|
||||
})
|
||||
|
||||
targetLog.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), network, target.Addr)
|
||||
ro.Route = buf.String()
|
||||
if err != nil {
|
||||
targetLog.Error(err)
|
||||
// Mark node as failed for future selections
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
}
|
||||
lastErr = err
|
||||
// Try next node
|
||||
continue
|
||||
}
|
||||
|
||||
// Success - reset marker and proceed
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
marker.Reset()
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
if marker := target.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
defer cc.Close()
|
||||
|
||||
cc = proxyproto.WrapClientConn(h.md.proxyProtocol, conn.RemoteAddr(), convertAddr(conn.LocalAddr()), cc)
|
||||
|
||||
t := time.Now()
|
||||
targetLog.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
||||
xnet.Transport(conn, cc)
|
||||
targetLog.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Infof("%s >-< %s", conn.RemoteAddr(), target.Addr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
cc = proxyproto.WrapClientConn(h.md.proxyProtocol, conn.RemoteAddr(), convertAddr(conn.LocalAddr()), cc)
|
||||
|
||||
t := time.Now()
|
||||
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
||||
xnet.Transport(conn, cc)
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Infof("%s >-< %s", conn.RemoteAddr(), target.Addr)
|
||||
|
||||
return nil
|
||||
// All retries exhausted
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return errors.New("all nodes failed")
|
||||
}
|
||||
|
||||
func (h *forwardHandler) checkRateLimit(addr net.Addr) bool {
|
||||
|
||||
@@ -26,6 +26,12 @@ type metadata struct {
|
||||
privateKey crypto.PrivateKey
|
||||
alpn string
|
||||
mitmBypass bypass.Bypass
|
||||
|
||||
// maxRetries specifies the maximum number of failover retry attempts.
|
||||
// When a target node fails, the handler will try the next available node.
|
||||
// 0 means use the total number of available nodes (try all nodes once).
|
||||
// Default: 0 (try all available nodes)
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
@@ -57,5 +63,9 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
}
|
||||
h.md.alpn = mdutil.GetString(md, "mitm.alpn")
|
||||
h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass"))
|
||||
|
||||
// maxRetries: 0 means try all available nodes (default behavior)
|
||||
h.md.maxRetries = mdutil.GetInt(md, "maxRetries", "retry.max")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+35
-4
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/go-gost/core/selector"
|
||||
"github.com/go-gost/x/config"
|
||||
node_parser "github.com/go-gost/x/config/parsing/node"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
)
|
||||
|
||||
@@ -141,11 +142,28 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get list of nodes to exclude (for failover retry)
|
||||
excludeNodes := ctxvalue.ExcludeNodesFromContext(ctx)
|
||||
excludeSet := make(map[string]bool)
|
||||
for _, addr := range excludeNodes {
|
||||
excludeSet[addr] = true
|
||||
}
|
||||
|
||||
// Debug logging for failover analysis
|
||||
log.Debugf("[hop.Select] excludeNodes=%v, totalNodes=%d", excludeNodes, len(p.Nodes()))
|
||||
|
||||
var nodes []*chain.Node
|
||||
for _, node := range p.Nodes() {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip nodes in the exclude list (failover retry)
|
||||
if excludeSet[node.Addr] || excludeSet[node.Name] {
|
||||
log.Debugf("node %s(%s) excluded for failover retry", node.Name, node.Addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// node level bypass
|
||||
if node.Options().Bypass != nil &&
|
||||
node.Options().Bypass.Contains(ctx, options.Network, options.Addr, bypass.WithHostOpton(options.Host)) {
|
||||
@@ -177,9 +195,6 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
||||
if len(nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(nodes) == 1 {
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Options().Priority > nodes[j].Options().Priority
|
||||
@@ -189,9 +204,25 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
// Use selector with FailFilter for proper failover.
|
||||
// FailFilter will exclude recently-failed nodes, allowing traffic to
|
||||
// be routed to healthy alternatives.
|
||||
// Note: FailFilter has a safety guard (len <= 1 returns as-is) to ensure
|
||||
// the last remaining node is never permanently blocked.
|
||||
if s := p.options.selector; s != nil {
|
||||
return s.Select(ctx, nodes...)
|
||||
log.Debugf("[hop.Select] calling selector.Select with %d nodes", len(nodes))
|
||||
if node := s.Select(ctx, nodes...); node != nil {
|
||||
log.Debugf("[hop.Select] selected node=%s addr=%s", node.Name, node.Addr)
|
||||
return node
|
||||
}
|
||||
// All nodes filtered out by FailFilter - all are marked as failed.
|
||||
// Return nil to signal "no healthy nodes available" to the caller.
|
||||
// The handler's retry loop will handle this appropriately.
|
||||
log.Debugf("all %d nodes filtered out by FailFilter, no healthy nodes available", len(nodes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: return first node if no selector configured
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
|
||||
@@ -247,64 +247,98 @@ func (h *Sniffer) dial(ctx context.Context, conn net.Conn, req *http.Request, ho
|
||||
}
|
||||
}
|
||||
|
||||
node = &chain.Node{
|
||||
Addr: host,
|
||||
// Determine max retry attempts
|
||||
maxRetries := 1
|
||||
if nl, ok := ho.Hop.(hop.NodeList); ok {
|
||||
maxRetries = len(nl.Nodes())
|
||||
}
|
||||
if ho.Hop != nil {
|
||||
node = ho.Hop.Select(ctx,
|
||||
hop.ClientIPSelectOption(net.ParseIP(ro.ClientIP)),
|
||||
hop.ProtocolSelectOption(sniffing.ProtoHTTP),
|
||||
hop.HostSelectOption(host),
|
||||
hop.MethodSelectOption(req.Method),
|
||||
hop.PathSelectOption(req.URL.Path),
|
||||
hop.QuerySelectOption(req.URL.Query()),
|
||||
hop.HeaderSelectOption(req.Header),
|
||||
)
|
||||
}
|
||||
if node == nil {
|
||||
ho.Log.Warnf("node for %s not found", host)
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
return nil, nil, errors.New("node not available")
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 1
|
||||
}
|
||||
|
||||
ro.Host = node.Addr
|
||||
ho.Log = ho.Log.WithFields(map[string]any{
|
||||
"node": node.Name,
|
||||
"dst": node.Addr,
|
||||
})
|
||||
ho.Log.Debugf("find node for host %s -> %s(%s)", host, node.Name, node.Addr)
|
||||
var triedNodes []string
|
||||
var lastErr error
|
||||
|
||||
cc, err = dial(ctx, "tcp", node.Addr)
|
||||
if err != nil {
|
||||
// TODO: the router itself may be failed due to the failed node in the router,
|
||||
// the dead marker may be a wrong operation.
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Select a node, excluding previously tried nodes
|
||||
selectCtx := ctxvalue.ContextWithExcludeNodes(ctx, triedNodes)
|
||||
|
||||
node = chain.NewNode("", host)
|
||||
if ho.Hop != nil {
|
||||
node = ho.Hop.Select(selectCtx,
|
||||
hop.ClientIPSelectOption(net.ParseIP(ro.ClientIP)),
|
||||
hop.ProtocolSelectOption(sniffing.ProtoHTTP),
|
||||
hop.HostSelectOption(host),
|
||||
hop.MethodSelectOption(req.Method),
|
||||
hop.PathSelectOption(req.URL.Path),
|
||||
hop.QuerySelectOption(req.URL.Query()),
|
||||
hop.HeaderSelectOption(req.Header),
|
||||
)
|
||||
}
|
||||
ho.Log.Warnf("connect to node %s(%s) failed: %v", node.Name, node.Addr, err)
|
||||
res.Write(conn)
|
||||
return
|
||||
}
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
|
||||
if tlsSettings := node.Options().TLS; tlsSettings != nil {
|
||||
cfg := &tls.Config{
|
||||
ServerName: tlsSettings.ServerName,
|
||||
InsecureSkipVerify: !tlsSettings.Secure,
|
||||
if node == nil {
|
||||
if lastErr != nil {
|
||||
ho.Log.Warnf("node for %s not found after retries", host)
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
return nil, nil, lastErr
|
||||
}
|
||||
ho.Log.Warnf("node for %s not found", host)
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
return nil, nil, errors.New("node not available")
|
||||
}
|
||||
tls_util.SetTLSOptions(cfg, &config.TLSOptions{
|
||||
MinVersion: tlsSettings.Options.MinVersion,
|
||||
MaxVersion: tlsSettings.Options.MaxVersion,
|
||||
CipherSuites: tlsSettings.Options.CipherSuites,
|
||||
ALPN: tlsSettings.Options.ALPN,
|
||||
|
||||
// Track this node as tried
|
||||
triedNodes = append(triedNodes, node.Addr)
|
||||
|
||||
ro.Host = node.Addr
|
||||
ho.Log = ho.Log.WithFields(map[string]any{
|
||||
"node": node.Name,
|
||||
"dst": node.Addr,
|
||||
})
|
||||
cc = tls.Client(cc, cfg)
|
||||
ho.Log.Debugf("find node for host %s -> %s(%s)", host, node.Name, node.Addr)
|
||||
|
||||
cc, err = dial(ctx, "tcp", node.Addr)
|
||||
if err != nil {
|
||||
// Mark node as failed for future selections
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
}
|
||||
ho.Log.Warnf("connect to node %s(%s) failed: %v, trying next node", node.Name, node.Addr, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Success - reset marker
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
|
||||
if tlsSettings := node.Options().TLS; tlsSettings != nil {
|
||||
cfg := &tls.Config{
|
||||
ServerName: tlsSettings.ServerName,
|
||||
InsecureSkipVerify: !tlsSettings.Secure,
|
||||
}
|
||||
tls_util.SetTLSOptions(cfg, &config.TLSOptions{
|
||||
MinVersion: tlsSettings.Options.MinVersion,
|
||||
MaxVersion: tlsSettings.Options.MaxVersion,
|
||||
CipherSuites: tlsSettings.Options.CipherSuites,
|
||||
ALPN: tlsSettings.Options.ALPN,
|
||||
})
|
||||
cc = tls.Client(cc, cfg)
|
||||
}
|
||||
return node, cc, nil
|
||||
}
|
||||
return
|
||||
|
||||
// All retries exhausted
|
||||
ho.Log.Warnf("all nodes failed for host %s", host)
|
||||
res.Write(conn)
|
||||
if lastErr != nil {
|
||||
return nil, nil, lastErr
|
||||
}
|
||||
return nil, nil, errors.New("all nodes failed")
|
||||
}
|
||||
|
||||
func (h *Sniffer) serveH2(ctx context.Context, conn net.Conn, ho *HandleOptions) error {
|
||||
@@ -847,74 +881,105 @@ func (h *Sniffer) dialTLS(ctx context.Context, host string, ho *HandleOptions) (
|
||||
return
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
node = &chain.Node{
|
||||
Addr: host,
|
||||
}
|
||||
}
|
||||
|
||||
ro := ho.RecorderObject
|
||||
if ho.Hop != nil {
|
||||
node = ho.Hop.Select(ctx,
|
||||
hop.ClientIPSelectOption(net.ParseIP(ro.ClientIP)),
|
||||
hop.HostSelectOption(host),
|
||||
hop.ProtocolSelectOption(sniffing.ProtoTLS),
|
||||
)
|
||||
|
||||
// Determine max retry attempts
|
||||
maxRetries := 1
|
||||
if nl, ok := ho.Hop.(hop.NodeList); ok {
|
||||
maxRetries = len(nl.Nodes())
|
||||
}
|
||||
if node == nil {
|
||||
err = errors.New("node not available")
|
||||
return
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 1
|
||||
}
|
||||
|
||||
addr := node.Addr
|
||||
if opts := node.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
ro.Network = opts.Network
|
||||
default:
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
addr += ":443"
|
||||
var triedNodes []string
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Select a node, excluding previously tried nodes
|
||||
selectCtx := ctxvalue.ContextWithExcludeNodes(ctx, triedNodes)
|
||||
|
||||
node = nil
|
||||
if host != "" {
|
||||
node = chain.NewNode("", host)
|
||||
}
|
||||
if ho.Hop != nil {
|
||||
node = ho.Hop.Select(selectCtx,
|
||||
hop.ClientIPSelectOption(net.ParseIP(ro.ClientIP)),
|
||||
hop.HostSelectOption(host),
|
||||
hop.ProtocolSelectOption(sniffing.ProtoTLS),
|
||||
)
|
||||
}
|
||||
if node == nil {
|
||||
if lastErr != nil {
|
||||
ho.Log.Warnf("node for %s not found after retries", host)
|
||||
return nil, nil, lastErr
|
||||
}
|
||||
ho.Log.Warnf("node for %s not found", host)
|
||||
return nil, nil, errors.New("node not available")
|
||||
}
|
||||
|
||||
// Track this node as tried
|
||||
triedNodes = append(triedNodes, node.Addr)
|
||||
|
||||
addr := node.Addr
|
||||
if opts := node.Options(); opts != nil {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
ro.Network = opts.Network
|
||||
default:
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
addr += ":443"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ro.Host = addr
|
||||
ro.Host = addr
|
||||
|
||||
ho.Log = ho.Log.WithFields(map[string]any{
|
||||
"host": host,
|
||||
"node": node.Name,
|
||||
"dst": fmt.Sprintf("%s/%s", addr, ro.Network),
|
||||
})
|
||||
ho.Log.Debugf("find node for host %s -> %s(%s)", host, node.Name, addr)
|
||||
|
||||
cc, err = dial(ctx, ro.Network, addr)
|
||||
if err != nil {
|
||||
// TODO: the router itself may be failed due to the failed node in the router,
|
||||
// the dead marker may be a wrong operation.
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
}
|
||||
ho.Log.Warnf("connect to node %s(%s) failed: %v", node.Name, node.Addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
|
||||
if tlsSettings := node.Options().TLS; tlsSettings != nil {
|
||||
cfg := &tls.Config{
|
||||
ServerName: tlsSettings.ServerName,
|
||||
InsecureSkipVerify: !tlsSettings.Secure,
|
||||
}
|
||||
tls_util.SetTLSOptions(cfg, &config.TLSOptions{
|
||||
MinVersion: tlsSettings.Options.MinVersion,
|
||||
MaxVersion: tlsSettings.Options.MaxVersion,
|
||||
CipherSuites: tlsSettings.Options.CipherSuites,
|
||||
ALPN: tlsSettings.Options.ALPN,
|
||||
ho.Log = ho.Log.WithFields(map[string]any{
|
||||
"host": host,
|
||||
"node": node.Name,
|
||||
"dst": fmt.Sprintf("%s/%s", addr, ro.Network),
|
||||
})
|
||||
cc = tls.Client(cc, cfg)
|
||||
ho.Log.Debugf("find node for host %s -> %s(%s)", host, node.Name, addr)
|
||||
|
||||
cc, err = dial(ctx, ro.Network, addr)
|
||||
if err != nil {
|
||||
// Mark node as failed for future selections
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Mark()
|
||||
}
|
||||
ho.Log.Warnf("connect to node %s(%s) failed: %v, trying next node", node.Name, node.Addr, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Success - reset marker
|
||||
if marker := node.Marker(); marker != nil {
|
||||
marker.Reset()
|
||||
}
|
||||
|
||||
if tlsSettings := node.Options().TLS; tlsSettings != nil {
|
||||
cfg := &tls.Config{
|
||||
ServerName: tlsSettings.ServerName,
|
||||
InsecureSkipVerify: !tlsSettings.Secure,
|
||||
}
|
||||
tls_util.SetTLSOptions(cfg, &config.TLSOptions{
|
||||
MinVersion: tlsSettings.Options.MinVersion,
|
||||
MaxVersion: tlsSettings.Options.MaxVersion,
|
||||
CipherSuites: tlsSettings.Options.CipherSuites,
|
||||
ALPN: tlsSettings.Options.ALPN,
|
||||
})
|
||||
cc = tls.Client(cc, cfg)
|
||||
}
|
||||
return node, cc, nil
|
||||
}
|
||||
return
|
||||
|
||||
// All retries exhausted
|
||||
ho.Log.Warnf("all nodes failed for host %s", host)
|
||||
if lastErr != nil {
|
||||
return nil, nil, lastErr
|
||||
}
|
||||
return nil, nil, errors.New("all nodes failed")
|
||||
}
|
||||
|
||||
func (h *Sniffer) terminateTLS(ctx context.Context, conn, cc net.Conn, clientHello *dissector.ClientHelloInfo, ho *HandleOptions) error {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# GO-GOST REGISTRY KNOWLEDGE BASE
|
||||
|
||||
**Generated:** Wed Feb 04 2026
|
||||
|
||||
## OVERVIEW
|
||||
Central registration point for all pluggable GOST components (handlers, listeners, dialers, etc.).
|
||||
Allows the configuration system to resolve string types (e.g., "socks5") to actual Go implementations.
|
||||
|
||||
## STRUCTURE
|
||||
One file per component type, exporting a standard Registry interface.
|
||||
```
|
||||
go-gost/x/registry/
|
||||
├── handler.go # RegisterHandler(name, newFunc)
|
||||
├── listener.go # RegisterListener(name, newFunc)
|
||||
├── dialer.go # RegisterDialer(name, newFunc)
|
||||
└── ... # Same pattern for auth, bypass, admission
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Register a new component | `go-gost/x/registry/{type}.go` | Use `Register{Type}(name, creator)` |
|
||||
| Component lookup | `go-gost/x/registry/{type}.go` | `Get{Type}(name)` returns the creator function |
|
||||
| Default registrations | `go-gost/x/` (init functions) | Most components register themselves in their package `init()` |
|
||||
|
||||
## CONVENTIONS
|
||||
- Thread-safe maps used for storage.
|
||||
- Names are case-sensitive (usually lowercase).
|
||||
- Components must be registered *before* the configuration parser runs (usually done via `import _ "..."` in `main.go`).
|
||||
@@ -2,11 +2,13 @@ package selector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
"github.com/go-gost/core/selector"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
)
|
||||
|
||||
type failFilter[T any] struct {
|
||||
@@ -24,6 +26,8 @@ func FailFilter[T any](maxFails int, timeout time.Duration) selector.Filter[T] {
|
||||
}
|
||||
|
||||
// Filter filters dead objects.
|
||||
// For single-node case, skip filtering to ensure availability (matches upstream).
|
||||
// For multi-node case, filter out failed nodes to enable failover.
|
||||
func (f *failFilter[T]) Filter(ctx context.Context, vs ...T) []T {
|
||||
if len(vs) <= 1 {
|
||||
return vs
|
||||
@@ -51,8 +55,21 @@ func (f *failFilter[T]) Filter(ctx context.Context, vs ...T) []T {
|
||||
|
||||
if mi, _ := any(v).(selector.Markable); mi != nil {
|
||||
if marker := mi.Marker(); marker != nil {
|
||||
if marker.Count() < int64(maxFails) ||
|
||||
time.Since(marker.Time()) >= failTimeout {
|
||||
count := marker.Count()
|
||||
timeSince := time.Since(marker.Time())
|
||||
passed := count < int64(maxFails) || timeSince >= failTimeout
|
||||
|
||||
// Debug logging for failover analysis
|
||||
nodeName := "unknown"
|
||||
nodeAddr := "unknown"
|
||||
if node, ok := any(v).(*chain.Node); ok {
|
||||
nodeName = node.Name
|
||||
nodeAddr = node.Addr
|
||||
}
|
||||
fmt.Printf("[FailFilter] node=%s addr=%s count=%d maxFails=%d timeSince=%v failTimeout=%v passed=%v\n",
|
||||
nodeName, nodeAddr, count, maxFails, timeSince, failTimeout, passed)
|
||||
|
||||
if passed {
|
||||
l = append(l, v)
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -2,11 +2,17 @@ package socket
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/go-gost/x/config"
|
||||
)
|
||||
|
||||
// configMutex 保护配置文件的并发写入
|
||||
var configMutex sync.Mutex
|
||||
|
||||
func saveConfig() {
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
|
||||
file := "gost.json"
|
||||
|
||||
|
||||
@@ -466,7 +466,13 @@ func (w *WebSocketReporter) handleReceivedMessage(messageType int, message []byt
|
||||
}
|
||||
|
||||
if cmdMsg.Type != "call" {
|
||||
w.routeCommand(cmdMsg)
|
||||
// TcpPing 诊断命令异步执行,避免阻塞其他命令
|
||||
// 其他状态变更命令保持同步,确保顺序执行
|
||||
if cmdMsg.Type == "TcpPing" {
|
||||
go w.routeCommand(cmdMsg)
|
||||
} else {
|
||||
w.routeCommand(cmdMsg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理普通消息
|
||||
@@ -477,7 +483,13 @@ func (w *WebSocketReporter) handleReceivedMessage(messageType int, message []byt
|
||||
return
|
||||
}
|
||||
if cmdMsg.Type != "call" {
|
||||
w.routeCommand(cmdMsg)
|
||||
// TcpPing 诊断命令异步执行,避免阻塞其他命令
|
||||
// 其他状态变更命令保持同步,确保顺序执行
|
||||
if cmdMsg.Type == "TcpPing" {
|
||||
go w.routeCommand(cmdMsg)
|
||||
} else {
|
||||
w.routeCommand(cmdMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +509,7 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
|
||||
fmt.Println("🔔 收到命令: ", string(jsonBytes))
|
||||
var err error
|
||||
var response CommandResponse
|
||||
var needSaveConfig bool // 标记是否需要保存配置(只有状态变更命令才需要)
|
||||
|
||||
// 传递 requestId
|
||||
response.RequestId = cmd.RequestId
|
||||
@@ -506,65 +519,81 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
|
||||
case "AddService":
|
||||
err = w.handleAddService(cmd.Data)
|
||||
response.Type = "AddServiceResponse"
|
||||
needSaveConfig = true
|
||||
case "UpdateService":
|
||||
err = w.handleUpdateService(cmd.Data)
|
||||
response.Type = "UpdateServiceResponse"
|
||||
needSaveConfig = true
|
||||
case "DeleteService":
|
||||
err = w.handleDeleteService(cmd.Data)
|
||||
response.Type = "DeleteServiceResponse"
|
||||
needSaveConfig = true
|
||||
case "PauseService":
|
||||
err = w.handlePauseService(cmd.Data)
|
||||
response.Type = "PauseServiceResponse"
|
||||
needSaveConfig = true
|
||||
case "ResumeService":
|
||||
err = w.handleResumeService(cmd.Data)
|
||||
response.Type = "ResumeServiceResponse"
|
||||
needSaveConfig = true
|
||||
|
||||
// Chain 相关命令
|
||||
case "AddChains":
|
||||
err = w.handleAddChain(cmd.Data)
|
||||
response.Type = "AddChainsResponse"
|
||||
needSaveConfig = true
|
||||
case "UpdateChains":
|
||||
err = w.handleUpdateChain(cmd.Data)
|
||||
response.Type = "UpdateChainsResponse"
|
||||
needSaveConfig = true
|
||||
case "DeleteChains":
|
||||
err = w.handleDeleteChain(cmd.Data)
|
||||
response.Type = "DeleteChainsResponse"
|
||||
needSaveConfig = true
|
||||
|
||||
// Limiter 相关命令
|
||||
case "AddLimiters":
|
||||
err = w.handleAddLimiter(cmd.Data)
|
||||
response.Type = "AddLimitersResponse"
|
||||
needSaveConfig = true
|
||||
case "UpdateLimiters":
|
||||
err = w.handleUpdateLimiter(cmd.Data)
|
||||
response.Type = "UpdateLimitersResponse"
|
||||
needSaveConfig = true
|
||||
case "DeleteLimiters":
|
||||
err = w.handleDeleteLimiter(cmd.Data)
|
||||
response.Type = "DeleteLimitersResponse"
|
||||
needSaveConfig = true
|
||||
|
||||
// TCP Ping 诊断命令
|
||||
// TCP Ping 诊断命令(只读,不需要保存配置)
|
||||
case "TcpPing":
|
||||
var tcpPingResult TcpPingResponse
|
||||
tcpPingResult, err = w.handleTcpPing(cmd.Data)
|
||||
response.Type = "TcpPingResponse"
|
||||
response.Data = tcpPingResult
|
||||
// needSaveConfig = false (默认值)
|
||||
|
||||
// Protocol blocking switches
|
||||
case "SetProtocol":
|
||||
err = w.handleSetProtocol(cmd.Data)
|
||||
response.Type = "SetProtocolResponse"
|
||||
needSaveConfig = true
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("未知命令类型: %s", cmd.Type)
|
||||
response.Type = "UnknownCommandResponse"
|
||||
}
|
||||
|
||||
// 只有状态变更命令才保存配置
|
||||
if needSaveConfig {
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
if err != nil {
|
||||
saveConfig()
|
||||
response.Success = false
|
||||
response.Message = err.Error()
|
||||
} else {
|
||||
saveConfig()
|
||||
response.Success = true
|
||||
response.Message = "OK"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -513,10 +514,10 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
.filter(ct -> ct.getChainType() == 3)
|
||||
.toList();
|
||||
|
||||
List<DiagnosisResult> results = new ArrayList<>();
|
||||
List<CompletableFuture<DiagnosisResult>> futures = new ArrayList<>();
|
||||
String[] remoteAddresses = forward.getRemoteAddr().split(",");
|
||||
|
||||
// 根据隧道类型执行不同的诊断策略
|
||||
// 根据隧道类型执行不同的诊断策略(并行执行所有诊断任务)
|
||||
if (tunnel.getType() == 1) {
|
||||
// 端口转发:入口节点直接TCP ping目标地址
|
||||
for (ChainTunnel inNode : inNodes) {
|
||||
@@ -526,12 +527,18 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
String targetIp = extractIpFromAddress(remoteAddress);
|
||||
int targetPort = extractPortFromAddress(remoteAddress);
|
||||
if (targetIp != null && targetPort != -1) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
node, targetIp, targetPort,
|
||||
"入口(" + node.getName() + ")->目标(" + remoteAddress + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
results.add(result);
|
||||
final Node finalNode = node;
|
||||
final String finalTargetIp = targetIp;
|
||||
final int finalTargetPort = targetPort;
|
||||
final String finalRemoteAddress = remoteAddress;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalNode, finalTargetIp, finalTargetPort,
|
||||
"入口(" + finalNode.getName() + ")->目标(" + finalRemoteAddress + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,27 +554,37 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
for (ChainTunnel firstChainNode : chainNodesList.getFirst()) {
|
||||
Node toNode = nodeService.getById(firstChainNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), firstChainNode.getPort(),
|
||||
"入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(2);
|
||||
result.setToInx(firstChainNode.getInx());
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalFirstChainNode = firstChainNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalFirstChainNode.getPort(),
|
||||
"入口(" + finalFromNode.getName() + ")->第1跳(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(2);
|
||||
result.setToInx(finalFirstChainNode.getInx());
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (!outNodes.isEmpty()) {
|
||||
for (ChainTunnel outNode : outNodes) {
|
||||
Node toNode = nodeService.getById(outNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(),
|
||||
"入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(3);
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalOutNode = outNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalOutNode.getPort(),
|
||||
"入口(" + finalFromNode.getName() + ")->出口(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,6 +594,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
// 2. 链路测试
|
||||
for (int i = 0; i < chainNodesList.size(); i++) {
|
||||
List<ChainTunnel> currentHop = chainNodesList.get(i);
|
||||
final int hopIndex = i;
|
||||
|
||||
for (ChainTunnel currentNode : currentHop) {
|
||||
Node fromNode = nodeService.getById(currentNode.getNodeId());
|
||||
@@ -586,29 +604,41 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
for (ChainTunnel nextNode : chainNodesList.get(i + 1)) {
|
||||
Node toNode = nodeService.getById(nextNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), nextNode.getPort(),
|
||||
"第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(currentNode.getInx());
|
||||
result.setToChainType(2);
|
||||
result.setToInx(nextNode.getInx());
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalCurrentNode = currentNode;
|
||||
final ChainTunnel finalNextNode = nextNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalNextNode.getPort(),
|
||||
"第" + (hopIndex + 1) + "跳(" + finalFromNode.getName() + ")->第" + (hopIndex + 2) + "跳(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(finalCurrentNode.getInx());
|
||||
result.setToChainType(2);
|
||||
result.setToInx(finalNextNode.getInx());
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (!outNodes.isEmpty()) {
|
||||
for (ChainTunnel outNode : outNodes) {
|
||||
Node toNode = nodeService.getById(outNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(),
|
||||
"第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(currentNode.getInx());
|
||||
result.setToChainType(3);
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalCurrentNode = currentNode;
|
||||
final ChainTunnel finalOutNode = outNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalOutNode.getPort(),
|
||||
"第" + (hopIndex + 1) + "跳(" + finalFromNode.getName() + ")->出口(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(finalCurrentNode.getInx());
|
||||
result.setToChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -624,18 +654,29 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
String targetIp = extractIpFromAddress(remoteAddress);
|
||||
int targetPort = extractPortFromAddress(remoteAddress);
|
||||
if (targetIp != null && targetPort != -1) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
node, targetIp, targetPort,
|
||||
"出口(" + node.getName() + ")->目标(" + remoteAddress + ")"
|
||||
);
|
||||
result.setFromChainType(3);
|
||||
results.add(result);
|
||||
final Node finalNode = node;
|
||||
final String finalTargetIp = targetIp;
|
||||
final int finalTargetPort = targetPort;
|
||||
final String finalRemoteAddress = remoteAddress;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalNode, finalTargetIp, finalTargetPort,
|
||||
"出口(" + finalNode.getName() + ")->目标(" + finalRemoteAddress + ")"
|
||||
);
|
||||
result.setFromChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有诊断任务完成并收集结果
|
||||
List<DiagnosisResult> results = futures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建诊断报告
|
||||
Map<String, Object> diagnosisReport = new HashMap<>();
|
||||
diagnosisReport.put("forwardId", id);
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -669,17 +670,20 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
.filter(ct -> ct.getChainType() == 3)
|
||||
.toList();
|
||||
|
||||
List<DiagnosisResult> results = new ArrayList<>();
|
||||
List<CompletableFuture<DiagnosisResult>> futures = new ArrayList<>();
|
||||
|
||||
if (tunnel.getType() == 1) {
|
||||
for (ChainTunnel inNode : inNodes) {
|
||||
Node node = nodeService.getById(inNode.getNodeId());
|
||||
if (node != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
node, "www.google.com", 443, "入口(" + node.getName() + ")->外网"
|
||||
);
|
||||
result.setFromChainType(1); // 入口
|
||||
results.add(result);
|
||||
final Node finalNode = node;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalNode, "www.google.com", 443, "入口(" + finalNode.getName() + ")->外网"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (tunnel.getType() == 2) {
|
||||
@@ -691,27 +695,37 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
for (ChainTunnel firstChainNode : chainNodesList.getFirst()) {
|
||||
Node toNode = nodeService.getById(firstChainNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), firstChainNode.getPort(),
|
||||
"入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1); // 入口
|
||||
result.setToChainType(2); // 链
|
||||
result.setToInx(firstChainNode.getInx());
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalFirstChainNode = firstChainNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalFirstChainNode.getPort(),
|
||||
"入口(" + finalFromNode.getName() + ")->第1跳(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(2);
|
||||
result.setToInx(finalFirstChainNode.getInx());
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (!outNodes.isEmpty()) {
|
||||
for (ChainTunnel outNode : outNodes) {
|
||||
Node toNode = nodeService.getById(outNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(),
|
||||
"入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(3);
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalOutNode = outNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalOutNode.getPort(),
|
||||
"入口(" + finalFromNode.getName() + ")->出口(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(1);
|
||||
result.setToChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,6 +734,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
|
||||
for (int i = 0; i < chainNodesList.size(); i++) {
|
||||
List<ChainTunnel> currentHop = chainNodesList.get(i);
|
||||
final int hopIndex = i;
|
||||
|
||||
for (ChainTunnel currentNode : currentHop) {
|
||||
Node fromNode = nodeService.getById(currentNode.getNodeId());
|
||||
@@ -729,29 +744,41 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
for (ChainTunnel nextNode : chainNodesList.get(i + 1)) {
|
||||
Node toNode = nodeService.getById(nextNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), nextNode.getPort(),
|
||||
"第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(currentNode.getInx());
|
||||
result.setToChainType(2);
|
||||
result.setToInx(nextNode.getInx());
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalCurrentNode = currentNode;
|
||||
final ChainTunnel finalNextNode = nextNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalNextNode.getPort(),
|
||||
"第" + (hopIndex + 1) + "跳(" + finalFromNode.getName() + ")->第" + (hopIndex + 2) + "跳(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(finalCurrentNode.getInx());
|
||||
result.setToChainType(2);
|
||||
result.setToInx(finalNextNode.getInx());
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (!outNodes.isEmpty()) {
|
||||
for (ChainTunnel outNode : outNodes) {
|
||||
Node toNode = nodeService.getById(outNode.getNodeId());
|
||||
if (toNode != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(),
|
||||
"第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(currentNode.getInx());
|
||||
result.setToChainType(3);
|
||||
results.add(result);
|
||||
final Node finalFromNode = fromNode;
|
||||
final Node finalToNode = toNode;
|
||||
final ChainTunnel finalCurrentNode = currentNode;
|
||||
final ChainTunnel finalOutNode = outNode;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalFromNode, GostUtil.selectDialHost(finalFromNode, finalToNode), finalOutNode.getPort(),
|
||||
"第" + (hopIndex + 1) + "跳(" + finalFromNode.getName() + ")->出口(" + finalToNode.getName() + ")"
|
||||
);
|
||||
result.setFromChainType(2);
|
||||
result.setFromInx(finalCurrentNode.getInx());
|
||||
result.setToChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,15 +788,22 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
for (ChainTunnel outNode : outNodes) {
|
||||
Node node = nodeService.getById(outNode.getNodeId());
|
||||
if (node != null) {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
node, "www.google.com", 443, "出口(" + node.getName() + ")->外网"
|
||||
);
|
||||
result.setFromChainType(3);
|
||||
results.add(result);
|
||||
final Node finalNode = node;
|
||||
futures.add(CompletableFuture.supplyAsync(() -> {
|
||||
DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck(
|
||||
finalNode, "www.google.com", 443, "出口(" + finalNode.getName() + ")->外网"
|
||||
);
|
||||
result.setFromChainType(3);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<DiagnosisResult> results = futures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> diagnosisReport = new HashMap<>();
|
||||
diagnosisReport.put("tunnelId", tunnelId);
|
||||
diagnosisReport.put("tunnelName", tunnel.getName());
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-is": "^19.2.4",
|
||||
"react-router-dom": "6.23.0",
|
||||
"recharts": "^3.1.1",
|
||||
"sonner": "^2.0.6",
|
||||
|
||||
Reference in New Issue
Block a user