31878a72ec
- Add package-level architecture doc in handler.go covering: - NAT traversal reverse proxy architecture - CmdBind/CmdConnect roles and data flow - Connector lifecycle and waitClose semantics - Entrypoint protocol dispatch (first-byte sniffing) - SD fallback behavior - Add data-flow-oriented doc comments to all business files: - entrypoint.go: protocol dispatch, dial flow - connector.go: Connector/ConnectorPool semantics - dialer.go: two-phase dial strategy - tunnel.go: MaxWeight semantics, selection algorithm - bind.go: 6-step CmdBind flow - connect.go: CmdConnect flow with relay framing - ephttp.go, eptls.go, eprelay.go: per-protocol entrypoint flow - Fix observeStats: after successful error retry, also flush new events instead of skipping the current tick (handler.go:261-271) - Add test for observeStats retry-then-flush (handler_test.go)
187 lines
3.7 KiB
Go
187 lines
3.7 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"
|
|
)
|
|
|
|
// MaxWeight is the exclusive takeover weight (255). A connector at MaxWeight
|
|
// triggers rw.Reset() in GetConnector, clearing all previously added connectors
|
|
// so that only this one is selected. If that MaxWeight connector later dies
|
|
// (IsClosed), GetConnector returns nil until Tunnel.clean() removes it.
|
|
const (
|
|
MaxWeight uint8 = 0xff
|
|
)
|
|
|
|
// Tunnel groups Connectors that share the same tunnel ID.
|
|
//
|
|
// Connector selection (GetConnector):
|
|
// - Single connector: fast path, no weighted random.
|
|
// - Multiple connectors: weighted random selection by network type (tcp/udp).
|
|
// - MaxWeight (255): exclusive — triggers Reset() and only the MaxWeight
|
|
// connector is added. If the MaxWeight connector closes, selection returns
|
|
// nil until clean() removes it.
|
|
//
|
|
// Tunnel lifecycle:
|
|
// - NewTunnel starts a clean() goroutine that runs every TTL (default 15s).
|
|
// Each tick removes closed connectors and renews SD registrations.
|
|
// A tunnel with 0 connectors gets cleaned up.
|
|
// - CloseOnIdle closes the tunnel if it has 0 connectors (called by
|
|
// ConnectorPool.closeIdles on a 15-minute ticker).
|
|
// - Close() closes all connectors and signals shutdown.
|
|
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
|
|
}
|
|
}
|
|
}
|