Files
x/handler/tunnel/tunnel.go
T
ginuerzh e791ba47e2 refactor(handler/tunnel): split 794-line entrypoint.go into 6 files, fix 3 bugs, add 48 tests
Breaking Change: moves Connector, ConnectorPool, parseTunnelID from
tunnel.go into dedicated connector.go and id.go.

Bug Fixes:
- dialer.go: clear stale retry err so SD fallback is not masked (nil err
  from pool.Get() left previous iteration's error live)
- connect.go: check WriteTo errors in both mux and non-mux paths
- eprelay.go: use fresh relay.Response for features sent over mux
  (previously reused same resp struct after resp.WriteTo(muxConn))

Refactor:
- entrypoint.go: split ~790 lines into 6 files (ephttp, eptls, eprelay,
  epwebsocket, eplistener, entrypointsvc), moved to dedicated package
- handler.go: moved initEntrypoints/createEntrypointService to
  entrypointsvc.go
- tunnel.go: moved Connector/ConnectorPool/parseTunnelID to
  connector.go and id.go
- connect.go: moved entrypoint-originated handleConnect to eprelay.go
  (handler-side handleConnect kept in connect.go)

Tests (48 new, 88 total):
- id_test.go: parseTunnelID variants (empty, UUID, private '$', invalid)
- tunnel_test.go: Tunnel lifecycle (AddConnector, GetConnector
  weighted/closed filtering, CloseOnIdle, clean),
  Connector (NewConnector nil-opts, GetConn nil/closed session, Close,
  IsClosed), ConnectorPool (Add/Get/Close, idle cleanup, concurrency)
- dialer_test.go: SD error paths (sd.Get error, empty address)
- handler_test.go: observeStats (nil observer, cancelled context loop),
  initEntrypoints (no entrypoints configured)
- helpers_test.go: testLogger, fakeConn for handler tests
- eprelay_test.go: handleConnect ingress/dial/SD round-trip
2026-06-01 22:20:43 +08:00

167 lines
2.5 KiB
Go

package tunnel
import (
"context"
"sync"
"time"
"github.com/go-gost/core/sd"
"github.com/go-gost/relay"
"github.com/go-gost/x/selector"
)
const (
MaxWeight uint8 = 0xff
)
type Tunnel struct {
node string
id relay.TunnelID
connectors []*Connector
t time.Time
close chan struct{}
mu sync.RWMutex
ttl time.Duration
}
func NewTunnel(node string, tid relay.TunnelID, ttl time.Duration) *Tunnel {
t := &Tunnel{
node: node,
id: tid,
t: time.Now(),
close: make(chan struct{}),
ttl: ttl,
}
if t.ttl <= 0 {
t.ttl = defaultTTL
}
go t.clean()
return t
}
func (t *Tunnel) ID() relay.TunnelID {
return t.id
}
func (t *Tunnel) AddConnector(c *Connector) {
if c == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.connectors = append(t.connectors, c)
}
func (t *Tunnel) GetConnector(network string) *Connector {
t.mu.RLock()
defer t.mu.RUnlock()
if len(t.connectors) == 1 {
if t.connectors[0].IsClosed() {
return nil
}
return t.connectors[0]
}
rw := selector.NewRandomWeighted[*Connector]()
found := false
for _, c := range t.connectors {
if c.IsClosed() {
continue
}
weight := c.ID().Weight()
if weight == 0 {
weight = 1
}
if network == "udp" && c.id.IsUDP() ||
network != "udp" && !c.id.IsUDP() {
if weight == MaxWeight && !found {
rw.Reset()
found = true
}
if weight == MaxWeight || !found {
rw.Add(c, int(weight))
}
}
}
return rw.Next()
}
func (t *Tunnel) Close() error {
t.mu.Lock()
defer t.mu.Unlock()
select {
case <-t.close:
default:
for _, c := range t.connectors {
c.Close()
}
close(t.close)
}
return nil
}
func (t *Tunnel) CloseOnIdle() bool {
t.mu.Lock()
defer t.mu.Unlock()
select {
case <-t.close:
default:
if len(t.connectors) == 0 {
close(t.close)
return true
}
}
return false
}
func (t *Tunnel) clean() {
ticker := time.NewTicker(t.ttl)
defer ticker.Stop()
for {
select {
case <-ticker.C:
t.mu.Lock()
if len(t.connectors) == 0 {
t.mu.Unlock()
break
}
var connectors []*Connector
for _, c := range t.connectors {
if c.IsClosed() {
c.log.Debugf("remove connector: %s %s", t.id, c.id)
continue
}
connectors = append(connectors, c)
if c.opts.sd != nil {
c.opts.sd.Renew(context.Background(), &sd.Service{
ID: c.id.String(),
Name: t.id.String(),
Node: t.node,
})
}
}
if len(connectors) != len(t.connectors) {
t.connectors = connectors
}
t.mu.Unlock()
case <-t.close:
return
}
}
}