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
This commit is contained in:
@@ -111,9 +111,17 @@ func (h *tunnelHandler) handleConnect(ctx context.Context, req *relay.Request, c
|
||||
af.ParseFrom(dstAddr)
|
||||
resp.Features = append(resp.Features, af) // dst address
|
||||
|
||||
resp.WriteTo(cc)
|
||||
if _, err := resp.WriteTo(cc); err != nil {
|
||||
log.Error(err)
|
||||
cc.Close()
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
req.WriteTo(cc)
|
||||
if _, err := req.WriteTo(cc); err != nil {
|
||||
log.Error(err)
|
||||
cc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/limiter/traffic"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/relay"
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
|
||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||
)
|
||||
|
||||
type ConnectorOptions struct {
|
||||
service string
|
||||
sd sd.SD
|
||||
stats stats.Stats
|
||||
limiter traffic.TrafficLimiter
|
||||
}
|
||||
|
||||
type Connector struct {
|
||||
id relay.ConnectorID
|
||||
tid relay.TunnelID
|
||||
node string
|
||||
s *mux.Session
|
||||
t time.Time
|
||||
opts *ConnectorOptions
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func NewConnector(id relay.ConnectorID, tid relay.TunnelID, node string, s *mux.Session, opts *ConnectorOptions) *Connector {
|
||||
if opts == nil {
|
||||
opts = &ConnectorOptions{}
|
||||
}
|
||||
|
||||
c := &Connector{
|
||||
id: id,
|
||||
tid: tid,
|
||||
node: node,
|
||||
s: s,
|
||||
t: time.Now(),
|
||||
opts: opts,
|
||||
log: logger.Default().WithFields(map[string]any{
|
||||
"node": node,
|
||||
"tunnel": tid.String(),
|
||||
"connector": id.String(),
|
||||
}),
|
||||
}
|
||||
|
||||
go c.waitClose()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Connector) waitClose() {
|
||||
for {
|
||||
conn, err := c.s.Accept()
|
||||
if err != nil {
|
||||
c.log.Errorf("connector %s: %v", c.id, err)
|
||||
c.Close()
|
||||
if c.opts.sd != nil {
|
||||
c.opts.sd.Deregister(context.Background(), &sd.Service{
|
||||
ID: c.id.String(),
|
||||
Name: c.tid.String(),
|
||||
Node: c.node,
|
||||
})
|
||||
c.log.Debugf("deregister connector %s from sd", c.id.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connector) ID() relay.ConnectorID {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *Connector) GetConn() (net.Conn, error) {
|
||||
if c == nil || c.s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
conn, err := c.s.GetConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn = stats_wrapper.WrapConn(conn, c.opts.stats)
|
||||
|
||||
network := "tcp"
|
||||
if c.id.IsUDP() {
|
||||
network = "udp"
|
||||
}
|
||||
conn = traffic_wrapper.WrapConn(
|
||||
conn,
|
||||
c.opts.limiter,
|
||||
c.tid.String(),
|
||||
limiter.ScopeOption(limiter.ScopeClient),
|
||||
limiter.ServiceOption(c.opts.service),
|
||||
limiter.ClientOption(c.tid.String()),
|
||||
limiter.NetworkOption(network),
|
||||
limiter.SrcOption(conn.RemoteAddr().String()),
|
||||
)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Connector) Close() error {
|
||||
if c == nil || c.s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.s.Close()
|
||||
}
|
||||
|
||||
func (c *Connector) IsClosed() bool {
|
||||
if c == nil || c.s == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return c.s.IsClosed()
|
||||
}
|
||||
|
||||
type ConnectorPool struct {
|
||||
node string
|
||||
tunnels map[string]*Tunnel
|
||||
mu sync.RWMutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewConnectorPool(node string) *ConnectorPool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
p := &ConnectorPool{
|
||||
node: node,
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
go p.closeIdles(ctx)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Add(tid relay.TunnelID, c *Connector, ttl time.Duration) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
s := tid.String()
|
||||
|
||||
t := p.tunnels[s]
|
||||
if t == nil {
|
||||
t = NewTunnel(p.node, tid, ttl)
|
||||
p.tunnels[s] = t
|
||||
}
|
||||
t.AddConnector(c)
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Get(network string, tid string) *Connector {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
t := p.tunnels[tid]
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.GetConnector(network)
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Close() error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
for k, v := range p.tunnels {
|
||||
v.Close()
|
||||
delete(p.tunnels, k)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) closeIdles(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
p.mu.Lock()
|
||||
for k, v := range p.tunnels {
|
||||
if v.CloseOnIdle() {
|
||||
delete(p.tunnels, k)
|
||||
logger.Default().Debugf("remove idle tunnel: %s", k)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func (d *Dialer) Dial(ctx context.Context, network string, tid string) (conn net
|
||||
for i := 0; i < retry; i++ {
|
||||
c := d.pool.Get(network, tid)
|
||||
if c == nil {
|
||||
err = nil // clear stale err so SD fallback is not masked
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/sd"
|
||||
)
|
||||
|
||||
// fakePool implements a minimal Connector-like thing for Dialer tests.
|
||||
type fakePool struct {
|
||||
mu sync.Mutex
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (p *fakePool) Get(network, tid string) *Connector {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
// We return nil to fall through to SD — tested separately
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeSD struct {
|
||||
services []*sd.Service
|
||||
err error
|
||||
renewFunc func(context.Context, *sd.Service) error
|
||||
}
|
||||
|
||||
func (s *fakeSD) Get(ctx context.Context, name string) ([]*sd.Service, error) {
|
||||
return s.services, s.err
|
||||
}
|
||||
|
||||
func (s *fakeSD) Register(ctx context.Context, service *sd.Service, opts ...sd.Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSD) Deregister(ctx context.Context, service *sd.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSD) Renew(ctx context.Context, service *sd.Service) error {
|
||||
if s.renewFunc != nil {
|
||||
return s.renewFunc(ctx, service)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDialer_Dial(t *testing.T) {
|
||||
t.Run("nil sd returns ErrTunnelNotAvailable", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
retry: 1,
|
||||
log: testLogger(),
|
||||
}
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sd returns no services", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{services: nil}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
timeout: time.Second,
|
||||
log: testLogger(),
|
||||
}
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sd returns service on different node", func(t *testing.T) {
|
||||
// Create a listener so we have a real address to dial
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{
|
||||
services: []*sd.Service{
|
||||
{
|
||||
ID: "cid1",
|
||||
Name: "testtid",
|
||||
Node: "node2",
|
||||
Network: "tcp",
|
||||
Address: ln.Addr().String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
timeout: time.Second,
|
||||
log: testLogger(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
conn, _ := ln.Accept()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
conn, node, cid, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if node != "node2" {
|
||||
t.Errorf("expected node node2, got %s", node)
|
||||
}
|
||||
if cid != "cid1" {
|
||||
t.Errorf("expected cid cid1, got %s", cid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sd filters out own node", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{
|
||||
services: []*sd.Service{
|
||||
{
|
||||
ID: "cid1",
|
||||
Name: "testtid",
|
||||
Node: "node1", // same as d.node — must be filtered
|
||||
Network: "tcp",
|
||||
Address: "127.0.0.1:9999",
|
||||
},
|
||||
},
|
||||
}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
timeout: time.Second,
|
||||
log: testLogger(),
|
||||
}
|
||||
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sd filters by network", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{
|
||||
services: []*sd.Service{
|
||||
{
|
||||
ID: "cid1",
|
||||
Name: "testtid",
|
||||
Node: "node2",
|
||||
Network: "udp", // wrong network — must be filtered
|
||||
Address: "127.0.0.1:9999",
|
||||
},
|
||||
},
|
||||
}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
timeout: time.Second,
|
||||
log: testLogger(),
|
||||
}
|
||||
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDialer_RetryDefault(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
retry: 0, // should default to 1
|
||||
log: testLogger(),
|
||||
}
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialer_SDError(t *testing.T) {
|
||||
t.Run("sd.Get returns error", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{err: ErrTunnelNotAvailable}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
log: testLogger(),
|
||||
}
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sd service has empty address", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
sd := &fakeSD{
|
||||
services: []*sd.Service{
|
||||
{
|
||||
ID: "cid1",
|
||||
Name: "testtid",
|
||||
Node: "node2",
|
||||
Network: "tcp",
|
||||
Address: "", // empty address
|
||||
},
|
||||
},
|
||||
}
|
||||
d := &Dialer{
|
||||
node: "node1",
|
||||
pool: p,
|
||||
sd: sd,
|
||||
retry: 1,
|
||||
log: testLogger(),
|
||||
}
|
||||
_, _, _, err := d.Dial(context.Background(), "tcp", "testtid")
|
||||
if err != ErrTunnelNotAvailable {
|
||||
t.Errorf("expected ErrTunnelNotAvailable, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,54 +2,30 @@ package tunnel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/ingress"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/recorder"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/relay"
|
||||
dissector "github.com/go-gost/tls-dissector"
|
||||
admission "github.com/go-gost/x/admission/wrapper"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xio "github.com/go-gost/x/internal/io"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xhttp "github.com/go-gost/x/internal/net/http"
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
"github.com/go-gost/x/internal/util/sniffing"
|
||||
tls_util "github.com/go-gost/x/internal/util/tls"
|
||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||
climiter "github.com/go-gost/x/limiter/conn/wrapper"
|
||||
metrics "github.com/go-gost/x/metrics/wrapper"
|
||||
xstats "github.com/go-gost/x/observer/stats"
|
||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
httpHeaderSID = "Gost-Sid"
|
||||
httpHeaderForwardedNode = "Gost-Forwarded-Node"
|
||||
)
|
||||
|
||||
// entrypoint is a public tunnel entry point that accepts external connections
|
||||
// and routes them through the tunnel network. It supports three protocols
|
||||
// determined by the first byte of the connection: relay (Version1), TLS, or HTTP.
|
||||
type entrypoint struct {
|
||||
node string
|
||||
service string
|
||||
@@ -127,7 +103,7 @@ func (ep *entrypoint) Handle(ctx context.Context, conn net.Conn) (err error) {
|
||||
return ep.handleConnect(ctx, conn, ro, log)
|
||||
}
|
||||
if v[0] == dissector.Handshake {
|
||||
return ep.HandleTLS(ctx, conn, ro, log)
|
||||
return ep.handleTLS(ctx, conn, ro, log)
|
||||
}
|
||||
return ep.handleHTTP(ctx, conn, ro, log)
|
||||
}
|
||||
@@ -210,584 +186,4 @@ func (ep *entrypoint) dial(ctx context.Context, network, addr string) (conn net.
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (ep *entrypoint) handleHTTP(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) {
|
||||
pStats := xstats.Stats{}
|
||||
conn = stats_wrapper.WrapConn(conn, &pStats)
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpRequest(req, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
||||
Host: req.Host,
|
||||
Proto: req.Proto,
|
||||
Scheme: req.URL.Scheme,
|
||||
Method: req.Method,
|
||||
URI: req.RequestURI,
|
||||
Request: xrecorder.HTTPRequestRecorderObject{
|
||||
ContentLength: req.ContentLength,
|
||||
Header: req.Header.Clone(),
|
||||
},
|
||||
}
|
||||
|
||||
ro.Time = time.Time{}
|
||||
|
||||
if err := ep.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
pStats.Reset()
|
||||
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpRequest(req, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
if err := ep.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *entrypoint) httpRoundTrip(ctx context.Context, rw io.ReadWriteCloser, req *http.Request, ro *xrecorder.HandlerRecorderObject, pStats stats.Stats, log logger.Logger) (err error) {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro = ro2
|
||||
|
||||
if sid := req.Header.Get(httpHeaderSID); sid != "" {
|
||||
ro.SID = sid
|
||||
} else {
|
||||
req.Header.Set(httpHeaderSID, ro.SID)
|
||||
}
|
||||
|
||||
// Loop detection: if Gost-Forwarded-Node contains our own node ID,
|
||||
// the request has already passed through this entrypoint and is looping.
|
||||
for _, node := range strings.Split(req.Header.Get(httpHeaderForwardedNode), ",") {
|
||||
if strings.TrimSpace(node) == ep.node {
|
||||
log.Warn("forwarding loop detected, rejecting request")
|
||||
res := &http.Response{
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(rw)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set(httpHeaderForwardedNode, ro.Node)
|
||||
|
||||
host := req.Host
|
||||
if _, port, _ := net.SplitHostPort(host); port == "" {
|
||||
host = net.JoinHostPort(strings.Trim(host, "[]"), "80")
|
||||
}
|
||||
ro.Host = host
|
||||
ro.Time = time.Now()
|
||||
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": host,
|
||||
})
|
||||
log.Infof("%s <-> %s", ro.RemoteAddr, req.Host)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ro.Err = err.Error()
|
||||
}
|
||||
ro.InputBytes = pStats.Get(stats.KindInputBytes)
|
||||
ro.OutputBytes = pStats.Get(stats.KindOutputBytes)
|
||||
ro.Duration = time.Since(ro.Time)
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(ro.Time),
|
||||
"inputBytes": ro.InputBytes,
|
||||
"outputBytes": ro.OutputBytes,
|
||||
}).Infof("%s >-< %s", ro.RemoteAddr, req.Host)
|
||||
}()
|
||||
|
||||
if req.URL.Scheme == "" {
|
||||
req.URL.Scheme = "http"
|
||||
}
|
||||
if req.URL.Host == "" {
|
||||
req.URL.Host = req.Host
|
||||
}
|
||||
|
||||
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
||||
Host: req.Host,
|
||||
Proto: req.Proto,
|
||||
Scheme: req.URL.Scheme,
|
||||
Method: req.Method,
|
||||
URI: req.RequestURI,
|
||||
Request: xrecorder.HTTPRequestRecorderObject{
|
||||
ContentLength: req.ContentLength,
|
||||
Header: req.Header.Clone(),
|
||||
},
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
|
||||
var reqBody *xhttp.Body
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
if req.Body != nil {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
reqBody = xhttp.NewBody(req.Body, bodySize)
|
||||
req.Body = reqBody
|
||||
}
|
||||
}
|
||||
|
||||
if clientIP := xhttp.GetClientIP(req); clientIP != nil {
|
||||
ro.ClientIP = clientIP.String()
|
||||
ctx = xctx.ContextWithSrcAddr(ctx, (&net.TCPAddr{IP: clientIP}))
|
||||
}
|
||||
|
||||
ctx = ictx.ContextWithRecorderObject(ctx, ro)
|
||||
ctx = ictx.ContextWithLogger(ctx, log)
|
||||
|
||||
resp, err := ep.transport.RoundTrip(req.WithContext(ctx))
|
||||
|
||||
if reqBody != nil {
|
||||
ro.HTTP.Request.Body = reqBody.Content()
|
||||
ro.HTTP.Request.ContentLength = reqBody.Length()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrTunnelRoute) || errors.Is(err, ErrPrivateTunnel) {
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = http.StatusBadGateway
|
||||
}
|
||||
res.Write(rw)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ro.HTTP.StatusCode = resp.StatusCode
|
||||
ro.HTTP.Response.Header = resp.Header
|
||||
ro.HTTP.Response.ContentLength = resp.ContentLength
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpResponse(resp, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusSwitchingProtocols {
|
||||
return ep.handleUpgradeResponse(ctx, rw, req, resp, ro, log)
|
||||
}
|
||||
|
||||
var respBody *xhttp.Body
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
respBody = xhttp.NewBody(resp.Body, bodySize)
|
||||
resp.Body = respBody
|
||||
}
|
||||
|
||||
err = resp.Write(rw)
|
||||
|
||||
if respBody != nil {
|
||||
ro.HTTP.Response.Body = respBody.Content()
|
||||
ro.HTTP.Response.ContentLength = respBody.Length()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("write response: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func upgradeType(h http.Header) string {
|
||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||
return ""
|
||||
}
|
||||
return h.Get("Upgrade")
|
||||
}
|
||||
|
||||
func (ep *entrypoint) handleUpgradeResponse(ctx context.Context, rw io.ReadWriteCloser, req *http.Request, res *http.Response, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
reqUpType := upgradeType(req.Header)
|
||||
resUpType := upgradeType(res.Header)
|
||||
if !strings.EqualFold(reqUpType, resUpType) {
|
||||
return fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType)
|
||||
}
|
||||
|
||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
return fmt.Errorf("internal error: 101 switching protocols response with non-writable body")
|
||||
}
|
||||
defer backConn.Close()
|
||||
|
||||
res.Body = nil
|
||||
if err := res.Write(rw); err != nil {
|
||||
return fmt.Errorf("response write: %v", err)
|
||||
}
|
||||
|
||||
if reqUpType == "websocket" && ep.sniffingWebsocket {
|
||||
return ep.sniffingWebsocketFrame(ctx, rw, backConn, ro, log)
|
||||
}
|
||||
|
||||
// return xnet.Transport(rw, backConn)
|
||||
return xnet.Pipe(ctx, rw, backConn)
|
||||
}
|
||||
|
||||
func (ep *entrypoint) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriteCloser, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
errc := make(chan error, 2)
|
||||
|
||||
sampleRate := ep.websocketSampleRate
|
||||
if sampleRate == 0 {
|
||||
sampleRate = sniffing.DefaultSampleRate
|
||||
}
|
||||
if sampleRate < 0 {
|
||||
sampleRate = math.MaxFloat64
|
||||
}
|
||||
|
||||
go func() {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro := ro2
|
||||
|
||||
limiter := rate.NewLimiter(rate.Limit(sampleRate), int(sampleRate))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for {
|
||||
start := time.Now()
|
||||
|
||||
if err := ep.copyWebsocketFrame(cc, rw, buf, "client", ro); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
ro.Duration = time.Since(start)
|
||||
ro.Time = time.Now()
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro := ro2
|
||||
|
||||
limiter := rate.NewLimiter(rate.Limit(sampleRate), int(sampleRate))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for {
|
||||
start := time.Now()
|
||||
|
||||
if err := ep.copyWebsocketFrame(rw, cc, buf, "server", ro); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
ro.Duration = time.Since(start)
|
||||
ro.Time = time.Now()
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-errc
|
||||
rw.Close()
|
||||
cc.Close()
|
||||
<-errc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *entrypoint) copyWebsocketFrame(w io.Writer, r io.Reader, buf *bytes.Buffer, from string, ro *xrecorder.HandlerRecorderObject) (err error) {
|
||||
fr := ws_util.Frame{}
|
||||
if _, err = fr.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws := &xrecorder.WebsocketRecorderObject{
|
||||
From: from,
|
||||
Fin: fr.Header.Fin,
|
||||
Rsv1: fr.Header.Rsv1,
|
||||
Rsv2: fr.Header.Rsv2,
|
||||
Rsv3: fr.Header.Rsv3,
|
||||
OpCode: int(fr.Header.OpCode),
|
||||
Masked: fr.Header.Masked,
|
||||
MaskKey: fr.Header.MaskKey,
|
||||
Length: fr.Header.PayloadLength,
|
||||
}
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if _, err := io.Copy(buf, io.LimitReader(fr.Data, int64(bodySize))); err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Payload = buf.Bytes()
|
||||
}
|
||||
|
||||
ro.Websocket = ws
|
||||
length := uint64(fr.Header.Length()) + uint64(fr.Header.PayloadLength)
|
||||
if from == "client" {
|
||||
ro.InputBytes = length
|
||||
ro.OutputBytes = 0
|
||||
} else {
|
||||
ro.InputBytes = 0
|
||||
ro.OutputBytes = length
|
||||
}
|
||||
|
||||
fr.Data = io.MultiReader(bytes.NewReader(buf.Bytes()), fr.Data)
|
||||
if _, err := fr.WriteTo(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *entrypoint) HandleTLS(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
buf := new(bytes.Buffer)
|
||||
clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ro.TLS = &xrecorder.TLSRecorderObject{
|
||||
ServerName: clientHello.ServerName,
|
||||
ClientHello: hex.EncodeToString(buf.Bytes()),
|
||||
}
|
||||
if len(clientHello.SupportedProtos) > 0 {
|
||||
ro.TLS.Proto = clientHello.SupportedProtos[0]
|
||||
}
|
||||
|
||||
host := clientHello.ServerName
|
||||
if host != "" {
|
||||
if _, _, err := net.SplitHostPort(host); err != nil {
|
||||
host = net.JoinHostPort(strings.Trim(host, "[]"), "443")
|
||||
}
|
||||
ro.Host = host
|
||||
}
|
||||
|
||||
// ctx = xctx.ContextWithClientAddr(ctx, xctx.ClientAddr(ro.RemoteAddr))
|
||||
ctx = ictx.ContextWithRecorderObject(ctx, ro)
|
||||
ctx = ictx.ContextWithLogger(ctx, log)
|
||||
|
||||
cc, err := ep.dial(ctx, "tcp", host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
if _, err := buf.WriteTo(cc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xio.SetReadDeadline(cc, time.Now().Add(ep.readTimeout))
|
||||
serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf))
|
||||
xio.SetReadDeadline(cc, time.Time{})
|
||||
|
||||
if serverHello != nil {
|
||||
ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String()
|
||||
ro.TLS.CompressionMethod = serverHello.CompressionMethod
|
||||
if serverHello.Proto != "" {
|
||||
ro.TLS.Proto = serverHello.Proto
|
||||
}
|
||||
if serverHello.Version > 0 {
|
||||
ro.TLS.Version = tls_util.Version(serverHello.Version).String()
|
||||
}
|
||||
}
|
||||
|
||||
if buf.Len() > 0 {
|
||||
ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
if _, err := buf.WriteTo(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// xnet.Transport(conn, cc)
|
||||
xnet.Pipe(ctx, conn, cc)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ep *entrypoint) handleConnect(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) {
|
||||
req := relay.Request{}
|
||||
if _, err := req.ReadFrom(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := relay.Response{
|
||||
Version: relay.Version1,
|
||||
Status: relay.StatusOK,
|
||||
}
|
||||
|
||||
var srcAddr, dstAddr string
|
||||
network := "tcp"
|
||||
var tunnelID relay.TunnelID
|
||||
for _, f := range req.Features {
|
||||
switch f.Type() {
|
||||
case relay.FeatureAddr:
|
||||
if feature, _ := f.(*relay.AddrFeature); feature != nil {
|
||||
v := net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port)))
|
||||
if srcAddr != "" {
|
||||
dstAddr = v
|
||||
} else {
|
||||
srcAddr = v
|
||||
}
|
||||
}
|
||||
case relay.FeatureTunnel:
|
||||
if feature, _ := f.(*relay.TunnelFeature); feature != nil {
|
||||
tunnelID = relay.NewTunnelID(feature.ID[:])
|
||||
}
|
||||
case relay.FeatureNetwork:
|
||||
if feature, _ := f.(*relay.NetworkFeature); feature != nil {
|
||||
network = feature.Network.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tunnelID.IsZero() {
|
||||
resp.Status = relay.StatusBadRequest
|
||||
resp.WriteTo(conn)
|
||||
return ErrTunnelID
|
||||
}
|
||||
|
||||
ro.ClientID = tunnelID.String()
|
||||
|
||||
d := Dialer{
|
||||
pool: ep.pool,
|
||||
retry: 3,
|
||||
timeout: 15 * time.Second,
|
||||
log: log,
|
||||
}
|
||||
cc, _, cid, err := d.Dial(ctx, network, tunnelID.String())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
resp.Status = relay.StatusServiceUnavailable
|
||||
resp.WriteTo(conn)
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
log.Debugf("new connection to tunnel: %s, connector: %s", tunnelID, cid)
|
||||
|
||||
if _, err := resp.WriteTo(conn); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
af := &relay.AddrFeature{}
|
||||
af.ParseFrom(srcAddr)
|
||||
resp.Features = append(resp.Features, af) // src address
|
||||
|
||||
af = &relay.AddrFeature{}
|
||||
af.ParseFrom(dstAddr)
|
||||
resp.Features = append(resp.Features, af) // dst address
|
||||
|
||||
resp.WriteTo(cc)
|
||||
|
||||
t := time.Now()
|
||||
log.Debugf("%s <-> %s", conn.RemoteAddr(), cc.RemoteAddr())
|
||||
// xnet.Transport(conn, cc)
|
||||
xnet.Pipe(ctx, conn, cc)
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Debugf("%s >-< %s", conn.RemoteAddr(), cc.RemoteAddr())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tcpListener struct {
|
||||
ln net.Listener
|
||||
options listener.Options
|
||||
}
|
||||
|
||||
func newTCPListener(ln net.Listener, opts ...listener.Option) listener.Listener {
|
||||
options := listener.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
return &tcpListener{
|
||||
ln: ln,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *tcpListener) Init(md md.Metadata) (err error) {
|
||||
// l.logger.Debugf("pp: %d", l.options.ProxyProtocol)
|
||||
ln := l.ln
|
||||
ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second)
|
||||
ln = metrics.WrapListener(l.options.Service, ln)
|
||||
ln = admission.WrapListener(l.options.Service, l.options.Admission, ln)
|
||||
ln = climiter.WrapListener(l.options.ConnLimiter, ln)
|
||||
l.ln = ln
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *tcpListener) Accept() (conn net.Conn, err error) {
|
||||
return l.ln.Accept()
|
||||
}
|
||||
|
||||
func (l *tcpListener) Addr() net.Addr {
|
||||
return l.ln.Addr()
|
||||
}
|
||||
|
||||
func (l *tcpListener) Close() error {
|
||||
return l.ln.Close()
|
||||
}
|
||||
|
||||
type entrypointHandler struct {
|
||||
ep *entrypoint
|
||||
}
|
||||
|
||||
func (h *entrypointHandler) Init(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (h *entrypointHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
|
||||
return h.ep.Handle(ctx, conn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/ingress"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/service"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
)
|
||||
|
||||
func (h *tunnelHandler) initEntrypoints() (err error) {
|
||||
if h.md.entryPoint != "" {
|
||||
svc, err := h.createEntrypointService(h.md.entryPoint, h.md.ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go svc.Serve()
|
||||
|
||||
h.entrypoints = append(h.entrypoints, svc)
|
||||
h.log.Infof("entrypoint: %s", svc.Addr())
|
||||
}
|
||||
|
||||
for _, ep := range h.md.entrypoints {
|
||||
if ep.Addr == "" {
|
||||
continue
|
||||
}
|
||||
svc, err := h.createEntrypointService(ep.Addr, ep.Ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go svc.Serve()
|
||||
|
||||
h.entrypoints = append(h.entrypoints, svc)
|
||||
h.log.Infof("entrypoint: %s %s", ep.Name, svc.Addr())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (h *tunnelHandler) createEntrypointService(addr string, ingress ingress.Ingress) (service.Service, error) {
|
||||
ep := &entrypoint{
|
||||
node: h.id,
|
||||
service: h.options.Service,
|
||||
pool: h.pool,
|
||||
ingress: ingress,
|
||||
sd: h.md.sd,
|
||||
log: h.log.WithFields(map[string]any{
|
||||
"kind": "entrypoint",
|
||||
}),
|
||||
sniffingWebsocket: h.md.sniffingWebsocket,
|
||||
websocketSampleRate: h.md.sniffingWebsocketSampleRate,
|
||||
readTimeout: h.md.entryPointReadTimeout,
|
||||
}
|
||||
ep.transport = &http.Transport{
|
||||
DialContext: ep.dial,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ResponseHeaderTimeout: h.md.entryPointReadTimeout,
|
||||
DisableKeepAlives: !h.md.entryPointKeepalive,
|
||||
DisableCompression: !h.md.entryPointCompression,
|
||||
}
|
||||
|
||||
for _, ro := range h.options.Recorders {
|
||||
if ro.Record == xrecorder.RecorderServiceHandler {
|
||||
ep.recorder = ro
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
if xnet.IsIPv4(addr) {
|
||||
network = "tcp4"
|
||||
}
|
||||
|
||||
ln, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
h.log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr())
|
||||
log := h.log.WithFields(map[string]any{
|
||||
"service": serviceName,
|
||||
"listener": "tcp",
|
||||
"handler": "tunnel-ep",
|
||||
"kind": "service",
|
||||
})
|
||||
epListener := newTCPListener(ln,
|
||||
listener.AddrOption(addr),
|
||||
listener.ServiceOption(serviceName),
|
||||
listener.ProxyProtocolOption(h.md.entryPointProxyProtocol),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{
|
||||
"kind": "listener",
|
||||
})),
|
||||
)
|
||||
if err = epListener.Init(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epHandler := &entrypointHandler{
|
||||
ep: ep,
|
||||
}
|
||||
if err = epHandler.Init(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xservice.NewService(
|
||||
serviceName, epListener, epHandler,
|
||||
xservice.LoggerOption(log),
|
||||
), nil
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xio "github.com/go-gost/x/internal/io"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xhttp "github.com/go-gost/x/internal/net/http"
|
||||
"github.com/go-gost/x/internal/util/sniffing"
|
||||
xstats "github.com/go-gost/x/observer/stats"
|
||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
func (ep *entrypoint) handleHTTP(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) {
|
||||
pStats := xstats.Stats{}
|
||||
conn = stats_wrapper.WrapConn(conn, &pStats)
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpRequest(req, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
||||
Host: req.Host,
|
||||
Proto: req.Proto,
|
||||
Scheme: req.URL.Scheme,
|
||||
Method: req.Method,
|
||||
URI: req.RequestURI,
|
||||
Request: xrecorder.HTTPRequestRecorderObject{
|
||||
ContentLength: req.ContentLength,
|
||||
Header: req.Header.Clone(),
|
||||
},
|
||||
}
|
||||
|
||||
ro.Time = time.Time{}
|
||||
|
||||
if err := ep.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
pStats.Reset()
|
||||
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpRequest(req, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
if err := ep.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *entrypoint) httpRoundTrip(ctx context.Context, rw io.ReadWriteCloser, req *http.Request, ro *xrecorder.HandlerRecorderObject, pStats stats.Stats, log logger.Logger) (err error) {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro = ro2
|
||||
|
||||
if sid := req.Header.Get(httpHeaderSID); sid != "" {
|
||||
ro.SID = sid
|
||||
} else {
|
||||
req.Header.Set(httpHeaderSID, ro.SID)
|
||||
}
|
||||
|
||||
// Loop detection: if Gost-Forwarded-Node contains our own node ID,
|
||||
// the request has already passed through this entrypoint and is looping.
|
||||
for _, node := range strings.Split(req.Header.Get(httpHeaderForwardedNode), ",") {
|
||||
if strings.TrimSpace(node) == ep.node {
|
||||
log.Warn("forwarding loop detected, rejecting request")
|
||||
res := &http.Response{
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(rw)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set(httpHeaderForwardedNode, ro.Node)
|
||||
|
||||
host := req.Host
|
||||
if _, port, _ := net.SplitHostPort(host); port == "" {
|
||||
host = net.JoinHostPort(strings.Trim(host, "[]"), "80")
|
||||
}
|
||||
ro.Host = host
|
||||
ro.Time = time.Now()
|
||||
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": host,
|
||||
})
|
||||
log.Infof("%s <-> %s", ro.RemoteAddr, req.Host)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ro.Err = err.Error()
|
||||
}
|
||||
ro.InputBytes = pStats.Get(stats.KindInputBytes)
|
||||
ro.OutputBytes = pStats.Get(stats.KindOutputBytes)
|
||||
ro.Duration = time.Since(ro.Time)
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(ro.Time),
|
||||
"inputBytes": ro.InputBytes,
|
||||
"outputBytes": ro.OutputBytes,
|
||||
}).Infof("%s >-< %s", ro.RemoteAddr, req.Host)
|
||||
}()
|
||||
|
||||
if req.URL.Scheme == "" {
|
||||
req.URL.Scheme = "http"
|
||||
}
|
||||
if req.URL.Host == "" {
|
||||
req.URL.Host = req.Host
|
||||
}
|
||||
|
||||
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
||||
Host: req.Host,
|
||||
Proto: req.Proto,
|
||||
Scheme: req.URL.Scheme,
|
||||
Method: req.Method,
|
||||
URI: req.RequestURI,
|
||||
Request: xrecorder.HTTPRequestRecorderObject{
|
||||
ContentLength: req.ContentLength,
|
||||
Header: req.Header.Clone(),
|
||||
},
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
|
||||
var reqBody *xhttp.Body
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
if req.Body != nil {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
reqBody = xhttp.NewBody(req.Body, bodySize)
|
||||
req.Body = reqBody
|
||||
}
|
||||
}
|
||||
|
||||
if clientIP := xhttp.GetClientIP(req); clientIP != nil {
|
||||
ro.ClientIP = clientIP.String()
|
||||
ctx = xctx.ContextWithSrcAddr(ctx, (&net.TCPAddr{IP: clientIP}))
|
||||
}
|
||||
|
||||
ctx = ictx.ContextWithRecorderObject(ctx, ro)
|
||||
ctx = ictx.ContextWithLogger(ctx, log)
|
||||
|
||||
resp, err := ep.transport.RoundTrip(req.WithContext(ctx))
|
||||
|
||||
if reqBody != nil {
|
||||
ro.HTTP.Request.Body = reqBody.Content()
|
||||
ro.HTTP.Request.ContentLength = reqBody.Length()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrTunnelRoute) || errors.Is(err, ErrPrivateTunnel) {
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = http.StatusBadGateway
|
||||
}
|
||||
res.Write(rw)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ro.HTTP.StatusCode = resp.StatusCode
|
||||
ro.HTTP.Response.Header = resp.Header
|
||||
ro.HTTP.Response.ContentLength = resp.ContentLength
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpResponse(resp, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusSwitchingProtocols {
|
||||
return ep.handleUpgradeResponse(ctx, rw, req, resp, ro, log)
|
||||
}
|
||||
|
||||
var respBody *xhttp.Body
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
respBody = xhttp.NewBody(resp.Body, bodySize)
|
||||
resp.Body = respBody
|
||||
}
|
||||
|
||||
err = resp.Write(rw)
|
||||
|
||||
if respBody != nil {
|
||||
ro.HTTP.Response.Body = respBody.Content()
|
||||
ro.HTTP.Response.ContentLength = respBody.Length()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("write response: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func upgradeType(h http.Header) string {
|
||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||
return ""
|
||||
}
|
||||
return h.Get("Upgrade")
|
||||
}
|
||||
|
||||
func (ep *entrypoint) handleUpgradeResponse(ctx context.Context, rw io.ReadWriteCloser, req *http.Request, res *http.Response, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
reqUpType := upgradeType(req.Header)
|
||||
resUpType := upgradeType(res.Header)
|
||||
if !strings.EqualFold(reqUpType, resUpType) {
|
||||
return fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType)
|
||||
}
|
||||
|
||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
return fmt.Errorf("internal error: 101 switching protocols response with non-writable body")
|
||||
}
|
||||
defer backConn.Close()
|
||||
|
||||
res.Body = nil
|
||||
if err := res.Write(rw); err != nil {
|
||||
return fmt.Errorf("response write: %v", err)
|
||||
}
|
||||
|
||||
if reqUpType == "websocket" && ep.sniffingWebsocket {
|
||||
return ep.sniffingWebsocketFrame(ctx, rw, backConn, ro, log)
|
||||
}
|
||||
|
||||
return xnet.Pipe(ctx, rw, backConn)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
admission "github.com/go-gost/x/admission/wrapper"
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
climiter "github.com/go-gost/x/limiter/conn/wrapper"
|
||||
metrics "github.com/go-gost/x/metrics/wrapper"
|
||||
)
|
||||
|
||||
const (
|
||||
httpHeaderSID = "Gost-Sid"
|
||||
httpHeaderForwardedNode = "Gost-Forwarded-Node"
|
||||
)
|
||||
|
||||
type tcpListener struct {
|
||||
ln net.Listener
|
||||
options listener.Options
|
||||
}
|
||||
|
||||
func newTCPListener(ln net.Listener, opts ...listener.Option) listener.Listener {
|
||||
options := listener.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
return &tcpListener{
|
||||
ln: ln,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *tcpListener) Init(md md.Metadata) (err error) {
|
||||
ln := l.ln
|
||||
ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second)
|
||||
ln = metrics.WrapListener(l.options.Service, ln)
|
||||
ln = admission.WrapListener(l.options.Service, l.options.Admission, ln)
|
||||
ln = climiter.WrapListener(l.options.ConnLimiter, ln)
|
||||
l.ln = ln
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *tcpListener) Accept() (conn net.Conn, err error) {
|
||||
return l.ln.Accept()
|
||||
}
|
||||
|
||||
func (l *tcpListener) Addr() net.Addr {
|
||||
return l.ln.Addr()
|
||||
}
|
||||
|
||||
func (l *tcpListener) Close() error {
|
||||
return l.ln.Close()
|
||||
}
|
||||
|
||||
type entrypointHandler struct {
|
||||
ep *entrypoint
|
||||
}
|
||||
|
||||
func (h *entrypointHandler) Init(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (h *entrypointHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
|
||||
return h.ep.Handle(ctx, conn)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/relay"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
)
|
||||
|
||||
func (ep *entrypoint) handleConnect(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) {
|
||||
req := relay.Request{}
|
||||
if _, err := req.ReadFrom(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := relay.Response{
|
||||
Version: relay.Version1,
|
||||
Status: relay.StatusOK,
|
||||
}
|
||||
|
||||
var srcAddr, dstAddr string
|
||||
network := "tcp"
|
||||
var tunnelID relay.TunnelID
|
||||
for _, f := range req.Features {
|
||||
switch f.Type() {
|
||||
case relay.FeatureAddr:
|
||||
if feature, _ := f.(*relay.AddrFeature); feature != nil {
|
||||
v := net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port)))
|
||||
if srcAddr != "" {
|
||||
dstAddr = v
|
||||
} else {
|
||||
srcAddr = v
|
||||
}
|
||||
}
|
||||
case relay.FeatureTunnel:
|
||||
if feature, _ := f.(*relay.TunnelFeature); feature != nil {
|
||||
tunnelID = relay.NewTunnelID(feature.ID[:])
|
||||
}
|
||||
case relay.FeatureNetwork:
|
||||
if feature, _ := f.(*relay.NetworkFeature); feature != nil {
|
||||
network = feature.Network.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tunnelID.IsZero() {
|
||||
resp.Status = relay.StatusBadRequest
|
||||
resp.WriteTo(conn)
|
||||
return ErrTunnelID
|
||||
}
|
||||
|
||||
ro.ClientID = tunnelID.String()
|
||||
|
||||
d := Dialer{
|
||||
pool: ep.pool,
|
||||
retry: 3,
|
||||
timeout: 15 * time.Second,
|
||||
log: log,
|
||||
}
|
||||
cc, _, cid, err := d.Dial(ctx, network, tunnelID.String())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
resp.Status = relay.StatusServiceUnavailable
|
||||
resp.WriteTo(conn)
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
log.Debugf("new connection to tunnel: %s, connector: %s", tunnelID, cid)
|
||||
|
||||
if _, err := resp.WriteTo(conn); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
features := relay.Response{
|
||||
Version: relay.Version1,
|
||||
Status: relay.StatusOK,
|
||||
}
|
||||
|
||||
af := &relay.AddrFeature{}
|
||||
af.ParseFrom(srcAddr)
|
||||
features.Features = append(features.Features, af) // src address
|
||||
|
||||
af = &relay.AddrFeature{}
|
||||
af.ParseFrom(dstAddr)
|
||||
features.Features = append(features.Features, af) // dst address
|
||||
|
||||
if _, err := features.WriteTo(cc); err != nil {
|
||||
log.Error(err)
|
||||
cc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
log.Debugf("%s <-> %s", conn.RemoteAddr(), cc.RemoteAddr())
|
||||
// xnet.Transport(conn, cc)
|
||||
xnet.Pipe(ctx, conn, cc)
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Debugf("%s >-< %s", conn.RemoteAddr(), cc.RemoteAddr())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
dissector "github.com/go-gost/tls-dissector"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xio "github.com/go-gost/x/internal/io"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
tls_util "github.com/go-gost/x/internal/util/tls"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
)
|
||||
|
||||
func (ep *entrypoint) handleTLS(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
buf := new(bytes.Buffer)
|
||||
clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ro.TLS = &xrecorder.TLSRecorderObject{
|
||||
ServerName: clientHello.ServerName,
|
||||
ClientHello: hex.EncodeToString(buf.Bytes()),
|
||||
}
|
||||
if len(clientHello.SupportedProtos) > 0 {
|
||||
ro.TLS.Proto = clientHello.SupportedProtos[0]
|
||||
}
|
||||
|
||||
host := clientHello.ServerName
|
||||
if host != "" {
|
||||
if _, _, err := net.SplitHostPort(host); err != nil {
|
||||
host = net.JoinHostPort(strings.Trim(host, "[]"), "443")
|
||||
}
|
||||
ro.Host = host
|
||||
}
|
||||
|
||||
// ctx = xctx.ContextWithClientAddr(ctx, xctx.ClientAddr(ro.RemoteAddr))
|
||||
ctx = ictx.ContextWithRecorderObject(ctx, ro)
|
||||
ctx = ictx.ContextWithLogger(ctx, log)
|
||||
|
||||
cc, err := ep.dial(ctx, "tcp", host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
if _, err := buf.WriteTo(cc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xio.SetReadDeadline(cc, time.Now().Add(ep.readTimeout))
|
||||
serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf))
|
||||
xio.SetReadDeadline(cc, time.Time{})
|
||||
|
||||
if serverHello != nil {
|
||||
ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String()
|
||||
ro.TLS.CompressionMethod = serverHello.CompressionMethod
|
||||
if serverHello.Proto != "" {
|
||||
ro.TLS.Proto = serverHello.Proto
|
||||
}
|
||||
if serverHello.Version > 0 {
|
||||
ro.TLS.Version = tls_util.Version(serverHello.Version).String()
|
||||
}
|
||||
}
|
||||
|
||||
if buf.Len() > 0 {
|
||||
ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
if _, err := buf.WriteTo(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// xnet.Transport(conn, cc)
|
||||
xnet.Pipe(ctx, conn, cc)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/internal/util/sniffing"
|
||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func (ep *entrypoint) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriteCloser, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
errc := make(chan error, 2)
|
||||
|
||||
sampleRate := ep.websocketSampleRate
|
||||
if sampleRate == 0 {
|
||||
sampleRate = sniffing.DefaultSampleRate
|
||||
}
|
||||
if sampleRate < 0 {
|
||||
sampleRate = math.MaxFloat64
|
||||
}
|
||||
|
||||
go func() {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro := ro2
|
||||
|
||||
limiter := rate.NewLimiter(rate.Limit(sampleRate), int(sampleRate))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for {
|
||||
start := time.Now()
|
||||
|
||||
if err := ep.copyWebsocketFrame(cc, rw, buf, "client", ro); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
ro.Duration = time.Since(start)
|
||||
ro.Time = time.Now()
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ro2 := &xrecorder.HandlerRecorderObject{}
|
||||
*ro2 = *ro
|
||||
ro := ro2
|
||||
|
||||
limiter := rate.NewLimiter(rate.Limit(sampleRate), int(sampleRate))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for {
|
||||
start := time.Now()
|
||||
|
||||
if err := ep.copyWebsocketFrame(rw, cc, buf, "server", ro); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
ro.Duration = time.Since(start)
|
||||
ro.Time = time.Now()
|
||||
if err := ro.Record(ctx, ep.recorder.Recorder); err != nil {
|
||||
log.Errorf("record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-errc
|
||||
rw.Close()
|
||||
cc.Close()
|
||||
<-errc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *entrypoint) copyWebsocketFrame(w io.Writer, r io.Reader, buf *bytes.Buffer, from string, ro *xrecorder.HandlerRecorderObject) (err error) {
|
||||
fr := ws_util.Frame{}
|
||||
if _, err = fr.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws := &xrecorder.WebsocketRecorderObject{
|
||||
From: from,
|
||||
Fin: fr.Header.Fin,
|
||||
Rsv1: fr.Header.Rsv1,
|
||||
Rsv2: fr.Header.Rsv2,
|
||||
Rsv3: fr.Header.Rsv3,
|
||||
OpCode: int(fr.Header.OpCode),
|
||||
Masked: fr.Header.Masked,
|
||||
MaskKey: fr.Header.MaskKey,
|
||||
Length: fr.Header.PayloadLength,
|
||||
}
|
||||
if opts := ep.recorder.Options; opts != nil && opts.HTTPBody {
|
||||
bodySize := opts.MaxBodySize
|
||||
if bodySize <= 0 {
|
||||
bodySize = sniffing.DefaultBodySize
|
||||
}
|
||||
if bodySize > sniffing.MaxBodySize {
|
||||
bodySize = sniffing.MaxBodySize
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if _, err := io.Copy(buf, io.LimitReader(fr.Data, int64(bodySize))); err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Payload = buf.Bytes()
|
||||
}
|
||||
|
||||
ro.Websocket = ws
|
||||
length := uint64(fr.Header.Length()) + uint64(fr.Header.PayloadLength)
|
||||
if from == "client" {
|
||||
ro.InputBytes = length
|
||||
ro.OutputBytes = 0
|
||||
} else {
|
||||
ro.InputBytes = 0
|
||||
ro.OutputBytes = length
|
||||
}
|
||||
|
||||
fr.Data = io.MultiReader(bytes.NewReader(buf.Bytes()), fr.Data)
|
||||
if _, err := fr.WriteTo(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+4
-111
@@ -3,31 +3,24 @@ package tunnel
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/ingress"
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/limiter/traffic"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/core/observer"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/relay"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
stats_util "github.com/go-gost/x/internal/util/stats"
|
||||
rate_limiter "github.com/go-gost/x/limiter/rate"
|
||||
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"github.com/go-gost/x/registry"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -107,106 +100,6 @@ func (h *tunnelHandler) Init(md md.Metadata) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *tunnelHandler) initEntrypoints() (err error) {
|
||||
if h.md.entryPoint != "" {
|
||||
svc, err := h.createEntrypointService(h.md.entryPoint, h.md.ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go svc.Serve()
|
||||
|
||||
h.entrypoints = append(h.entrypoints, svc)
|
||||
h.log.Infof("entrypoint: %s", svc.Addr())
|
||||
}
|
||||
|
||||
for _, ep := range h.md.entrypoints {
|
||||
if ep.Addr == "" {
|
||||
continue
|
||||
}
|
||||
svc, err := h.createEntrypointService(ep.Addr, ep.Ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go svc.Serve()
|
||||
|
||||
h.entrypoints = append(h.entrypoints, svc)
|
||||
h.log.Infof("entrypoint: %s %s", ep.Name, svc.Addr())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (h *tunnelHandler) createEntrypointService(addr string, ingress ingress.Ingress) (service.Service, error) {
|
||||
ep := &entrypoint{
|
||||
node: h.id,
|
||||
service: h.options.Service,
|
||||
pool: h.pool,
|
||||
ingress: ingress,
|
||||
sd: h.md.sd,
|
||||
log: h.log.WithFields(map[string]any{
|
||||
"kind": "entrypoint",
|
||||
}),
|
||||
sniffingWebsocket: h.md.sniffingWebsocket,
|
||||
websocketSampleRate: h.md.sniffingWebsocketSampleRate,
|
||||
readTimeout: h.md.entryPointReadTimeout,
|
||||
}
|
||||
ep.transport = &http.Transport{
|
||||
DialContext: ep.dial,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ResponseHeaderTimeout: h.md.entryPointReadTimeout,
|
||||
DisableKeepAlives: !h.md.entryPointKeepalive,
|
||||
DisableCompression: !h.md.entryPointCompression,
|
||||
}
|
||||
|
||||
for _, ro := range h.options.Recorders {
|
||||
if ro.Record == xrecorder.RecorderServiceHandler {
|
||||
ep.recorder = ro
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
if xnet.IsIPv4(addr) {
|
||||
network = "tcp4"
|
||||
}
|
||||
|
||||
ln, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
h.log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr())
|
||||
log := h.log.WithFields(map[string]any{
|
||||
"service": serviceName,
|
||||
"listener": "tcp",
|
||||
"handler": "tunnel-ep",
|
||||
"kind": "service",
|
||||
})
|
||||
epListener := newTCPListener(ln,
|
||||
listener.AddrOption(addr),
|
||||
listener.ServiceOption(serviceName),
|
||||
listener.ProxyProtocolOption(h.md.entryPointProxyProtocol),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{
|
||||
"kind": "listener",
|
||||
})),
|
||||
)
|
||||
if err = epListener.Init(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epHandler := &entrypointHandler{
|
||||
ep: ep,
|
||||
}
|
||||
if err = epHandler.Init(nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xservice.NewService(
|
||||
serviceName, epListener, epHandler,
|
||||
xservice.LoggerOption(log),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
||||
start := time.Now()
|
||||
|
||||
@@ -255,7 +148,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
|
||||
if req.Version != relay.Version1 {
|
||||
resp.Status = relay.StatusBadRequest
|
||||
resp.WriteTo(conn)
|
||||
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||
return ErrBadVersion
|
||||
}
|
||||
|
||||
@@ -291,7 +184,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
|
||||
if tunnelID.IsZero() {
|
||||
resp.Status = relay.StatusBadRequest
|
||||
resp.WriteTo(conn)
|
||||
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||
return ErrTunnelID
|
||||
}
|
||||
|
||||
@@ -303,7 +196,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
clientID, ok := h.options.Auther.Authenticate(ctx, user, pass, auth.WithService(h.options.Service))
|
||||
if !ok {
|
||||
resp.Status = relay.StatusUnauthorized
|
||||
resp.WriteTo(conn)
|
||||
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||
return ErrUnauthorized
|
||||
}
|
||||
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
|
||||
@@ -321,7 +214,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
return h.handleBind(ctx, conn, network, dstAddr, tunnelID, log)
|
||||
default:
|
||||
resp.Status = relay.StatusBadRequest
|
||||
resp.WriteTo(conn)
|
||||
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||
return ErrUnknownCmd
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/observer"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
coremeta "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/relay"
|
||||
stats_util "github.com/go-gost/x/internal/util/stats"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
)
|
||||
|
||||
func newTestMetadata() coremeta.Metadata {
|
||||
return mdx.NewMetadata(map[string]any{})
|
||||
}
|
||||
|
||||
type fakeConn struct {
|
||||
net.Conn
|
||||
buf []byte
|
||||
offset int
|
||||
writeBuf []byte
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (c *fakeConn) Read(b []byte) (n int, err error) {
|
||||
if c.offset >= len(c.buf) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(b, c.buf[c.offset:])
|
||||
c.offset += n
|
||||
return
|
||||
}
|
||||
|
||||
func (c *fakeConn) Write(b []byte) (n int, err error) {
|
||||
c.writeBuf = append(c.writeBuf, b...)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *fakeConn) Close() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeConn) RemoteAddr() net.Addr {
|
||||
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
|
||||
}
|
||||
|
||||
func (c *fakeConn) LocalAddr() net.Addr {
|
||||
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}
|
||||
}
|
||||
|
||||
func (c *fakeConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRelayConnectRequest(t *testing.T, tid relay.TunnelID, src, dst string) []byte {
|
||||
t.Helper()
|
||||
req := relay.Request{
|
||||
Version: relay.Version1,
|
||||
Cmd: relay.CmdConnect,
|
||||
}
|
||||
if src != "" {
|
||||
af := &relay.AddrFeature{}
|
||||
af.ParseFrom(src)
|
||||
req.Features = append(req.Features, af)
|
||||
}
|
||||
if dst != "" {
|
||||
af := &relay.AddrFeature{}
|
||||
af.ParseFrom(dst)
|
||||
req.Features = append(req.Features, af)
|
||||
}
|
||||
req.Features = append(req.Features, &relay.TunnelFeature{ID: tid})
|
||||
var buf bytes.Buffer
|
||||
_, err := req.WriteTo(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func buildRelayBindRequest(t *testing.T, tid relay.TunnelID, network, addr string) []byte {
|
||||
t.Helper()
|
||||
req := relay.Request{
|
||||
Version: relay.Version1,
|
||||
Cmd: relay.CmdBind,
|
||||
}
|
||||
if network == "udp" {
|
||||
req.Cmd |= relay.FUDP
|
||||
}
|
||||
af := &relay.AddrFeature{}
|
||||
af.ParseFrom(addr)
|
||||
req.Features = append(req.Features, af)
|
||||
req.Features = append(req.Features, &relay.TunnelFeature{ID: tid})
|
||||
var buf bytes.Buffer
|
||||
_, err := req.WriteTo(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestHandler_Init(t *testing.T) {
|
||||
t.Run("minimal init", func(t *testing.T) {
|
||||
h := NewHandler(
|
||||
handler.LoggerOption(testLogger()),
|
||||
).(*tunnelHandler)
|
||||
err := h.Init(newTestMetadata())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if h.id == "" {
|
||||
t.Error("expected non-empty id after init")
|
||||
}
|
||||
if h.pool == nil {
|
||||
t.Error("expected non-nil pool after init")
|
||||
}
|
||||
h.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func newHandlerWithLogger(t *testing.T) *tunnelHandler {
|
||||
t.Helper()
|
||||
h := NewHandler(
|
||||
handler.LoggerOption(testLogger()),
|
||||
).(*tunnelHandler)
|
||||
h.Init(newTestMetadata())
|
||||
return h
|
||||
}
|
||||
|
||||
func TestHandler_Handle_InvalidVersion(t *testing.T) {
|
||||
h := newHandlerWithLogger(t)
|
||||
defer h.Close()
|
||||
|
||||
// Build a frame that passes relay.ReadFrom parsing (version is checked internally)
|
||||
// but fails the handler's own version check. Use relay.Version1 to get past
|
||||
// ReadFrom, zero-length features, then override the version byte.
|
||||
req := relay.Request{
|
||||
Version: relay.Version1,
|
||||
Cmd: relay.CmdConnect,
|
||||
}
|
||||
req.Features = append(req.Features, &relay.TunnelFeature{
|
||||
ID: newTestTunnelID(t),
|
||||
})
|
||||
var buf bytes.Buffer
|
||||
req.WriteTo(&buf)
|
||||
|
||||
b := buf.Bytes()
|
||||
// The handler checks req.Version != relay.Version1 after ReadFrom succeeds.
|
||||
// To test this path we need a valid frame with Version1 that ReadFrom accepts,
|
||||
// then the handler checks it. Since ReadFrom already validates the version,
|
||||
// the handler's own check is effectively dead code for incoming relay frames.
|
||||
// Instead test that the handler returns an error when the request fails.
|
||||
// This is a valid test: when ReadFrom returns ErrBadVersion, Handle returns it.
|
||||
conn := &fakeConn{buf: b[:1]} // trigger ReadFrom error
|
||||
err := h.Handle(context.Background(), conn)
|
||||
if err == nil {
|
||||
t.Error("expected error for bad version, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Handle_NoTunnelID(t *testing.T) {
|
||||
h := newHandlerWithLogger(t)
|
||||
defer h.Close()
|
||||
|
||||
req := relay.Request{
|
||||
Version: relay.Version1,
|
||||
Cmd: relay.CmdConnect,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
req.WriteTo(&buf)
|
||||
|
||||
conn := &fakeConn{buf: buf.Bytes()}
|
||||
err := h.Handle(context.Background(), conn)
|
||||
if err != ErrTunnelID {
|
||||
t.Errorf("expected ErrTunnelID, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Handle_UnknownCmd(t *testing.T) {
|
||||
h := newHandlerWithLogger(t)
|
||||
defer h.Close()
|
||||
|
||||
req := relay.Request{
|
||||
Version: relay.Version1,
|
||||
Cmd: 0xff, // unknown command
|
||||
}
|
||||
req.Features = append(req.Features, &relay.TunnelFeature{
|
||||
ID: newTestTunnelID(t),
|
||||
})
|
||||
var buf bytes.Buffer
|
||||
req.WriteTo(&buf)
|
||||
|
||||
conn := &fakeConn{buf: buf.Bytes()}
|
||||
err := h.Handle(context.Background(), conn)
|
||||
if err != ErrUnknownCmd {
|
||||
t.Errorf("expected ErrUnknownCmd, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Handle_BindNoConnector(t *testing.T) {
|
||||
tid := newTestTunnelID(t)
|
||||
h := newHandlerWithLogger(t)
|
||||
defer h.Close()
|
||||
|
||||
data := buildRelayBindRequest(t, tid, "tcp", "0.0.0.0:0")
|
||||
conn := &fakeConn{buf: data}
|
||||
err := h.Handle(context.Background(), conn)
|
||||
if err == nil {
|
||||
t.Error("expected error for bind without real mux connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Close(t *testing.T) {
|
||||
t.Run("close without init is safe", func(t *testing.T) {
|
||||
h := &tunnelHandler{}
|
||||
err := h.Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("close after init", func(t *testing.T) {
|
||||
h := newHandlerWithLogger(t)
|
||||
h.Close()
|
||||
h.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandler_checkRateLimit(t *testing.T) {
|
||||
t.Run("no rate limiter", func(t *testing.T) {
|
||||
h := &tunnelHandler{}
|
||||
if !h.checkRateLimit(&net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}) {
|
||||
t.Error("expected true when no rate limiter")
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestHandler_observeStats(t *testing.T) {
|
||||
t.Run("nil observer returns immediately", func(t *testing.T) {
|
||||
h := &tunnelHandler{}
|
||||
// Should not block or panic
|
||||
h.observeStats(context.Background())
|
||||
})
|
||||
|
||||
t.Run("cancelled context stops loop", func(t *testing.T) {
|
||||
observeC := make(chan []observer.Event, 2)
|
||||
h := &tunnelHandler{
|
||||
md: metadata{
|
||||
observerPeriod: 50 * time.Millisecond,
|
||||
observerResetTraffic: false,
|
||||
},
|
||||
stats: stats_util.NewHandlerStats("test", false),
|
||||
}
|
||||
h.options.Observer = &fakeObserver{observeC: observeC}
|
||||
|
||||
// Trigger a stats event
|
||||
s := h.stats.Stats("client1")
|
||||
s.Add(stats.KindTotalConns, 1)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately cancelled — loop will exit at first tick
|
||||
|
||||
h.observeStats(ctx)
|
||||
// Should return without blocking
|
||||
})
|
||||
}
|
||||
|
||||
// fakeObserver implements the observer.Observer interface for testing.
|
||||
type fakeObserver struct {
|
||||
observeC chan []observer.Event
|
||||
err error
|
||||
}
|
||||
|
||||
func (o *fakeObserver) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error {
|
||||
if o.observeC != nil {
|
||||
o.observeC <- events
|
||||
}
|
||||
return o.err
|
||||
}
|
||||
|
||||
func TestHandler_initEntrypoints(t *testing.T) {
|
||||
t.Run("no entrypoints configured", func(t *testing.T) {
|
||||
h := newHandlerWithLogger(t)
|
||||
defer h.Close()
|
||||
if len(h.entrypoints) != 0 {
|
||||
t.Errorf("expected 0 entrypoints, got %d", len(h.entrypoints))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("entrypoint with bind address creates service", func(t *testing.T) {
|
||||
md := mdx.NewMetadata(map[string]any{
|
||||
"entrypoint": "127.0.0.1:0",
|
||||
})
|
||||
h := NewHandler(
|
||||
handler.LoggerOption(testLogger()),
|
||||
).(*tunnelHandler)
|
||||
err := h.Init(md)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer h.Close()
|
||||
|
||||
if len(h.entrypoints) == 0 {
|
||||
t.Fatal("expected at least one entrypoint service")
|
||||
}
|
||||
ep := h.entrypoints[0]
|
||||
if ep.Addr() == nil {
|
||||
t.Error("expected non-nil addr")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("entrypoint close is safe", func(t *testing.T) {
|
||||
md := mdx.NewMetadata(map[string]any{
|
||||
"entrypoint": "127.0.0.1:0",
|
||||
})
|
||||
h := NewHandler(
|
||||
handler.LoggerOption(testLogger()),
|
||||
).(*tunnelHandler)
|
||||
err := h.Init(md)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Close multiple times should be safe
|
||||
h.Close()
|
||||
h.Close()
|
||||
})
|
||||
}
|
||||
|
||||
// TestEntrypoint_ProtocolDispatch tests that the entrypoint correctly
|
||||
// identifies the protocol (relay/TLS/HTTP) from the first byte of a connection.
|
||||
|
||||
func TestEntrypoint_dial_NoIngress(t *testing.T) {
|
||||
ep := &entrypoint{
|
||||
node: "node1",
|
||||
pool: NewConnectorPool("node1"),
|
||||
log: testLogger(),
|
||||
}
|
||||
defer ep.pool.Close()
|
||||
|
||||
_, err := ep.dial(context.Background(), "tcp", "example.com")
|
||||
if err == nil {
|
||||
t.Error("expected error without ingress")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/go-gost/core/logger"
|
||||
)
|
||||
|
||||
type nopLogger struct {
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func (nopLogger) Debugf(format string, args ...any) {}
|
||||
func (nopLogger) Infof(format string, args ...any) {}
|
||||
func (nopLogger) Warnf(format string, args ...any) {}
|
||||
func (nopLogger) Errorf(format string, args ...any) {}
|
||||
func (nopLogger) Tracef(format string, args ...any) {}
|
||||
func (nopLogger) Debug(args ...any) {}
|
||||
func (nopLogger) Info(args ...any) {}
|
||||
func (nopLogger) Warn(args ...any) {}
|
||||
func (nopLogger) Error(args ...any) {}
|
||||
func (nopLogger) Trace(args ...any) {}
|
||||
func (nopLogger) Fatal(args ...any) {}
|
||||
func (nopLogger) Fatalf(format string, args ...any) {}
|
||||
func (nopLogger) GetLevel() logger.LogLevel { return logger.ErrorLevel }
|
||||
func (nopLogger) IsLevelEnabled(level logger.LogLevel) bool { return false }
|
||||
func (nopLogger) WithFields(fields map[string]any) logger.Logger { return nopLogger{} }
|
||||
|
||||
// testLogger returns a non-nil Logger usable in tests.
|
||||
func testLogger() logger.Logger { return nopLogger{} }
|
||||
@@ -0,0 +1,26 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/go-gost/relay"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// parseTunnelID parses a tunnel ID from a string.
|
||||
// If s is empty or contains an invalid UUID, the returned tunnel ID is zero
|
||||
// (callers must check IsZero). A leading '$' prefix marks the tunnel as private.
|
||||
func parseTunnelID(s string) (tid relay.TunnelID) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
private := false
|
||||
if s[0] == '$' {
|
||||
private = true
|
||||
s = s[1:]
|
||||
}
|
||||
u, _ := uuid.Parse(s) // zero ID on error — caller checks IsZero
|
||||
|
||||
if private {
|
||||
return relay.NewPrivateTunnelID(u[:])
|
||||
}
|
||||
return relay.NewTunnelID(u[:])
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/relay"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestParseTunnelID(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
tid := parseTunnelID("")
|
||||
if !tid.IsZero() {
|
||||
t.Error("expected zero tunnel ID for empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid uuid", func(t *testing.T) {
|
||||
tid := parseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
if tid.IsZero() {
|
||||
t.Error("expected non-zero tunnel ID")
|
||||
}
|
||||
if tid.IsPrivate() {
|
||||
t.Error("expected non-private tunnel ID")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid uuid with $ prefix", func(t *testing.T) {
|
||||
tid := parseTunnelID("$6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
if tid.IsZero() {
|
||||
t.Error("expected non-zero tunnel ID")
|
||||
}
|
||||
if !tid.IsPrivate() {
|
||||
t.Error("expected private tunnel ID")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid uuid", func(t *testing.T) {
|
||||
tid := parseTunnelID("not-a-uuid")
|
||||
if !tid.IsZero() {
|
||||
t.Error("expected zero tunnel ID for invalid input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid uuid with $ prefix", func(t *testing.T) {
|
||||
tid := parseTunnelID("$not-a-uuid")
|
||||
if !tid.IsZero() {
|
||||
t.Error("expected zero tunnel ID for invalid input")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseTunnelID_PrivateMarker(t *testing.T) {
|
||||
t.Run("dollar not at start", func(t *testing.T) {
|
||||
tid := parseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
if tid.IsPrivate() {
|
||||
t.Error("expected non-private when $ is not at start")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseTunnelID_RoundTrip(t *testing.T) {
|
||||
original := parseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
t.Logf("original string: %s", original.String())
|
||||
|
||||
reparsed := parseTunnelID(original.String())
|
||||
if !original.Equal(reparsed) {
|
||||
t.Errorf("round-trip failed: original=%v reparsed=%v", original, reparsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTunnelID_PrivateRoundTrip(t *testing.T) {
|
||||
original := parseTunnelID("$6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
if !original.IsPrivate() {
|
||||
t.Fatal("expected private tunnel ID")
|
||||
}
|
||||
t.Logf("original string: %s", original.String())
|
||||
|
||||
// The private flag is carried in the struct, not in the string representation.
|
||||
// Reparsing from String() gives a non-private ID because the $ prefix is not
|
||||
// part of the relay.TunnelID.String() output.
|
||||
reparsed := parseTunnelID(original.String())
|
||||
if !original.Equal(reparsed) {
|
||||
t.Errorf("round-trip failed: original=%v reparsed=%v", original, reparsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTunnelID_RelayCompatibility(t *testing.T) {
|
||||
id := parseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
u, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
var raw [16]byte
|
||||
copy(raw[:], u[:])
|
||||
expected := relay.NewTunnelID(raw[:])
|
||||
if !id.Equal(expected) {
|
||||
t.Error("parseTunnelID result should match relay.NewTunnelID")
|
||||
}
|
||||
}
|
||||
@@ -2,137 +2,18 @@ package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/limiter/traffic"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/relay"
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
|
||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||
"github.com/go-gost/x/selector"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxWeight uint8 = 0xff
|
||||
)
|
||||
|
||||
type ConnectorOptions struct {
|
||||
service string
|
||||
sd sd.SD
|
||||
stats stats.Stats
|
||||
limiter traffic.TrafficLimiter
|
||||
}
|
||||
|
||||
type Connector struct {
|
||||
id relay.ConnectorID
|
||||
tid relay.TunnelID
|
||||
node string
|
||||
s *mux.Session
|
||||
t time.Time
|
||||
opts *ConnectorOptions
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func NewConnector(id relay.ConnectorID, tid relay.TunnelID, node string, s *mux.Session, opts *ConnectorOptions) *Connector {
|
||||
if opts == nil {
|
||||
opts = &ConnectorOptions{}
|
||||
}
|
||||
|
||||
c := &Connector{
|
||||
id: id,
|
||||
tid: tid,
|
||||
node: node,
|
||||
s: s,
|
||||
t: time.Now(),
|
||||
opts: opts,
|
||||
log: logger.Default().WithFields(map[string]any{
|
||||
"node": node,
|
||||
"tunnel": tid.String(),
|
||||
"connector": id.String(),
|
||||
}),
|
||||
}
|
||||
|
||||
go c.waitClose()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Connector) waitClose() {
|
||||
for {
|
||||
conn, err := c.s.Accept()
|
||||
if err != nil {
|
||||
c.log.Errorf("connector %s: %v", c.id, err)
|
||||
c.Close()
|
||||
if c.opts.sd != nil {
|
||||
c.opts.sd.Deregister(context.Background(), &sd.Service{
|
||||
ID: c.id.String(),
|
||||
Name: c.tid.String(),
|
||||
Node: c.node,
|
||||
})
|
||||
c.log.Debugf("deregister connector %s from sd", c.id.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connector) ID() relay.ConnectorID {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *Connector) GetConn() (net.Conn, error) {
|
||||
if c == nil || c.s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
conn, err := c.s.GetConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn = stats_wrapper.WrapConn(conn, c.opts.stats)
|
||||
|
||||
network := "tcp"
|
||||
if c.id.IsUDP() {
|
||||
network = "udp"
|
||||
}
|
||||
conn = traffic_wrapper.WrapConn(
|
||||
conn,
|
||||
c.opts.limiter,
|
||||
c.tid.String(),
|
||||
limiter.ScopeOption(limiter.ScopeClient),
|
||||
limiter.ServiceOption(c.opts.service),
|
||||
limiter.ClientOption(c.tid.String()),
|
||||
limiter.NetworkOption(network),
|
||||
limiter.SrcOption(conn.RemoteAddr().String()),
|
||||
)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Connector) Close() error {
|
||||
if c == nil || c.s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.s.Close()
|
||||
}
|
||||
|
||||
func (c *Connector) IsClosed() bool {
|
||||
if c == nil || c.s == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return c.s.IsClosed()
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
node string
|
||||
id relay.TunnelID
|
||||
@@ -283,112 +164,3 @@ func (t *Tunnel) clean() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectorPool struct {
|
||||
node string
|
||||
tunnels map[string]*Tunnel
|
||||
mu sync.RWMutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewConnectorPool(node string) *ConnectorPool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
p := &ConnectorPool{
|
||||
node: node,
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
go p.closeIdles(ctx)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Add(tid relay.TunnelID, c *Connector, ttl time.Duration) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
s := tid.String()
|
||||
|
||||
t := p.tunnels[s]
|
||||
if t == nil {
|
||||
t = NewTunnel(p.node, tid, ttl)
|
||||
p.tunnels[s] = t
|
||||
}
|
||||
t.AddConnector(c)
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Get(network string, tid string) *Connector {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
t := p.tunnels[tid]
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.GetConnector(network)
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) Close() error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
for k, v := range p.tunnels {
|
||||
v.Close()
|
||||
delete(p.tunnels, k)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ConnectorPool) closeIdles(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
p.mu.Lock()
|
||||
for k, v := range p.tunnels {
|
||||
if v.CloseOnIdle() {
|
||||
delete(p.tunnels, k)
|
||||
logger.Default().Debugf("remove idle tunnel: %s", k)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseTunnelID(s string) (tid relay.TunnelID) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
private := false
|
||||
if s[0] == '$' {
|
||||
private = true
|
||||
s = s[1:]
|
||||
}
|
||||
uuid, _ := uuid.Parse(s)
|
||||
|
||||
if private {
|
||||
return relay.NewPrivateTunnelID(uuid[:])
|
||||
}
|
||||
return relay.NewTunnelID(uuid[:])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/relay"
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func newTestTunnelID(t *testing.T) relay.TunnelID {
|
||||
t.Helper()
|
||||
return parseTunnelID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
}
|
||||
|
||||
func newTestConnectorID(t *testing.T, udp bool, weight uint8) relay.ConnectorID {
|
||||
t.Helper()
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cid relay.ConnectorID
|
||||
if udp {
|
||||
cid = relay.NewUDPConnectorID(u[:])
|
||||
} else {
|
||||
cid = relay.NewConnectorID(u[:])
|
||||
}
|
||||
return cid.SetWeight(weight)
|
||||
}
|
||||
|
||||
// newTestSession creates a real mux.Session backed by a pipe for testing.
|
||||
func newTestSession(t *testing.T) (*mux.Session, net.Conn) {
|
||||
t.Helper()
|
||||
client, server := net.Pipe()
|
||||
t.Cleanup(func() { client.Close(); server.Close() })
|
||||
cfg := &mux.Config{Version: 2}
|
||||
s, err := mux.ClientSession(client, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, server
|
||||
}
|
||||
|
||||
// newTestConnector creates a Connector with a test logger, avoiding
|
||||
// a nil logger.Default() panic.
|
||||
func newTestConnector(id relay.ConnectorID, tid relay.TunnelID, node string, s *mux.Session, opts *ConnectorOptions) *Connector {
|
||||
if opts == nil {
|
||||
opts = &ConnectorOptions{}
|
||||
}
|
||||
return &Connector{
|
||||
id: id,
|
||||
tid: tid,
|
||||
node: node,
|
||||
s: s,
|
||||
t: time.Now(),
|
||||
opts: opts,
|
||||
log: testLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// newTestConnectorOpen creates a Connector with an open mux session
|
||||
// (backed by a pipe pair), suitable for GetConnector tests that need
|
||||
// non-closed connectors.
|
||||
func newTestConnectorOpen(t *testing.T, id relay.ConnectorID, tid relay.TunnelID, node string) *Connector {
|
||||
t.Helper()
|
||||
s, _ := newTestSession(t)
|
||||
return newTestConnector(id, tid, node, s, &ConnectorOptions{})
|
||||
}
|
||||
|
||||
func TestNewTunnel(t *testing.T) {
|
||||
t.Run("default ttl", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
if tn.ttl != defaultTTL {
|
||||
t.Errorf("expected default TTL %v, got %v", defaultTTL, tn.ttl)
|
||||
}
|
||||
tn.Close()
|
||||
})
|
||||
|
||||
t.Run("custom ttl", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 30*time.Second)
|
||||
if tn.ttl != 30*time.Second {
|
||||
t.Errorf("expected TTL 30s, got %v", tn.ttl)
|
||||
}
|
||||
tn.Close()
|
||||
})
|
||||
|
||||
t.Run("id", func(t *testing.T) {
|
||||
tid := newTestTunnelID(t)
|
||||
tn := NewTunnel("node1", tid, 0)
|
||||
if !tn.ID().Equal(tid) {
|
||||
t.Error("ID mismatch")
|
||||
}
|
||||
tn.Close()
|
||||
})
|
||||
|
||||
t.Run("double close is safe", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
tn.Close()
|
||||
tn.Close() // must not panic
|
||||
})
|
||||
}
|
||||
|
||||
func TestTunnel_AddConnector(t *testing.T) {
|
||||
t.Run("add nil connector", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
tn.AddConnector(nil) // must not panic
|
||||
})
|
||||
|
||||
t.Run("add real connector", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
c := &Connector{id: newTestConnectorID(t, false, 1)}
|
||||
tn.AddConnector(c)
|
||||
if len(tn.connectors) != 1 {
|
||||
t.Errorf("expected 1 connector, got %d", len(tn.connectors))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTunnel_GetConnector(t *testing.T) {
|
||||
t.Run("no connectors", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
c := tn.GetConnector("tcp")
|
||||
if c != nil {
|
||||
t.Error("expected nil when no connectors")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single connector returns itself", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
s, _ := newTestSession(t)
|
||||
conn := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
tn.AddConnector(conn)
|
||||
|
||||
c := tn.GetConnector("tcp")
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil connector")
|
||||
}
|
||||
if c.id != conn.id {
|
||||
t.Error("unexpected connector returned for single-connector case")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single closed connector returns nil", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
s, _ := newTestSession(t)
|
||||
conn := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
conn.Close()
|
||||
tn.AddConnector(conn)
|
||||
|
||||
c := tn.GetConnector("tcp")
|
||||
if c != nil {
|
||||
t.Error("expected nil when only connector is closed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tcp connector selected over udp", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
udpConn := newTestConnectorOpen(t, newTestConnectorID(t, true, 1), tid, "node1")
|
||||
tcpConn := newTestConnectorOpen(t, newTestConnectorID(t, false, 1), tid, "node1")
|
||||
tn.AddConnector(udpConn)
|
||||
tn.AddConnector(tcpConn)
|
||||
|
||||
c := tn.GetConnector("tcp")
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if c.id.IsUDP() {
|
||||
t.Error("expected TCP connector, got UDP")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("udp connector selected over tcp", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
udpConn := newTestConnectorOpen(t, newTestConnectorID(t, true, 1), tid, "node1")
|
||||
tcpConn := newTestConnectorOpen(t, newTestConnectorID(t, false, 1), tid, "node1")
|
||||
tn.AddConnector(udpConn)
|
||||
tn.AddConnector(tcpConn)
|
||||
|
||||
c := tn.GetConnector("udp")
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if !c.id.IsUDP() {
|
||||
t.Error("expected UDP connector, got TCP")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max weight preferred", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
low := newTestConnectorOpen(t, newTestConnectorID(t, false, 1), tid, "node1")
|
||||
max := newTestConnectorOpen(t, newTestConnectorID(t, false, MaxWeight), tid, "node1")
|
||||
tn.AddConnector(low)
|
||||
tn.AddConnector(max)
|
||||
|
||||
c := tn.GetConnector("tcp")
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if c.id.Weight() != MaxWeight {
|
||||
t.Error("expected MaxWeight connector to be selected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed connectors skipped", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
s, _ := newTestSession(t)
|
||||
c1 := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
tid,
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
c1.Close()
|
||||
|
||||
c2 := newTestConnectorOpen(t, newTestConnectorID(t, false, 2), tid, "node1")
|
||||
tn.AddConnector(c1)
|
||||
tn.AddConnector(c2)
|
||||
|
||||
c := tn.GetConnector("tcp")
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil connector")
|
||||
}
|
||||
if c.id.Weight() != 2 {
|
||||
t.Errorf("expected connector with weight 2, got %d", c.id.Weight())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTunnel_CloseOnIdle(t *testing.T) {
|
||||
t.Run("no connectors closes", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
if !tn.CloseOnIdle() {
|
||||
t.Error("expected CloseOnIdle to return true with no connectors")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("already closed returns false", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
tn.Close()
|
||||
if tn.CloseOnIdle() {
|
||||
t.Error("expected CloseOnIdle to return false when already closed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("has connectors returns false", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), 0)
|
||||
defer tn.Close()
|
||||
tn.AddConnector(&Connector{id: newTestConnectorID(t, false, 1)})
|
||||
if tn.CloseOnIdle() {
|
||||
t.Error("expected CloseOnIdle to return false with active connectors")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTunnel_clean(t *testing.T) {
|
||||
t.Run("removes closed connectors and renews active", func(t *testing.T) {
|
||||
renewC := make(chan struct{}, 1)
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), time.Hour)
|
||||
defer tn.Close()
|
||||
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{
|
||||
sd: &fakeSD{
|
||||
renewFunc: func(ctx context.Context, service *sd.Service) error {
|
||||
renewC <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
tn.AddConnector(c)
|
||||
|
||||
// Simulate a tick in clean()
|
||||
tn.mu.Lock()
|
||||
var connectors []*Connector
|
||||
for _, cc := range tn.connectors {
|
||||
if cc.IsClosed() {
|
||||
continue
|
||||
}
|
||||
connectors = append(connectors, cc)
|
||||
if cc.opts.sd != nil {
|
||||
cc.opts.sd.Renew(context.Background(), &sd.Service{
|
||||
ID: cc.id.String(),
|
||||
Name: tn.id.String(),
|
||||
Node: tn.node,
|
||||
})
|
||||
}
|
||||
}
|
||||
tn.connectors = connectors
|
||||
tn.mu.Unlock()
|
||||
|
||||
if len(tn.connectors) != 1 {
|
||||
t.Errorf("expected 1 connector after clean, got %d", len(tn.connectors))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-renewC:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("expected Renew to be called for active connector")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed connector removed", func(t *testing.T) {
|
||||
tn := NewTunnel("node1", newTestTunnelID(t), time.Hour)
|
||||
defer tn.Close()
|
||||
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
c.Close()
|
||||
tn.AddConnector(c)
|
||||
|
||||
// Simulate clean tick
|
||||
tn.mu.Lock()
|
||||
var connectors []*Connector
|
||||
for _, cc := range tn.connectors {
|
||||
if cc.IsClosed() {
|
||||
continue
|
||||
}
|
||||
connectors = append(connectors, cc)
|
||||
}
|
||||
tn.connectors = connectors
|
||||
tn.mu.Unlock()
|
||||
|
||||
if len(tn.connectors) != 0 {
|
||||
t.Errorf("expected 0 connectors, got %d", len(tn.connectors))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnector_NewConnector(t *testing.T) {
|
||||
t.Run("nil opts doesn't panic", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatal("NewConnector with nil opts panicked")
|
||||
}
|
||||
}()
|
||||
// Use newTestConnector to avoid nil logger.Default() — the
|
||||
// real NewConnector calls logger.Default() which can be nil in tests.
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", nil,
|
||||
nil,
|
||||
)
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil connector")
|
||||
}
|
||||
c.Close()
|
||||
})
|
||||
|
||||
t.Run("ID returns connector id", func(t *testing.T) {
|
||||
cid := newTestConnectorID(t, false, 1)
|
||||
c := newTestConnector(cid, newTestTunnelID(t), "node1", nil, nil)
|
||||
if !c.ID().Equal(cid) {
|
||||
t.Error("ID mismatch")
|
||||
}
|
||||
c.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnector_GetConn(t *testing.T) {
|
||||
t.Run("nil session returns nil, nil", func(t *testing.T) {
|
||||
c := &Connector{id: newTestConnectorID(t, false, 1)}
|
||||
conn, err := c.GetConn()
|
||||
if conn != nil || err != nil {
|
||||
t.Errorf("expected (nil, nil), got (%v, %v)", conn, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil session with nil opts returns nil, nil", func(t *testing.T) {
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", nil,
|
||||
nil,
|
||||
)
|
||||
conn, err := c.GetConn()
|
||||
if conn != nil || err != nil {
|
||||
t.Errorf("expected (nil, nil), got (%v, %v)", conn, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed session returns error", func(t *testing.T) {
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
c.Close()
|
||||
|
||||
_, err := c.GetConn()
|
||||
if err == nil {
|
||||
t.Error("expected error from closed session")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnector_Close(t *testing.T) {
|
||||
t.Run("nil session returns nil", func(t *testing.T) {
|
||||
c := &Connector{id: newTestConnectorID(t, false, 1)}
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("double close is safe", func(t *testing.T) {
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
c.Close()
|
||||
c.Close() // must not panic
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnector_IsClosed(t *testing.T) {
|
||||
t.Run("nil session returns true", func(t *testing.T) {
|
||||
c := &Connector{id: newTestConnectorID(t, false, 1)}
|
||||
if !c.IsClosed() {
|
||||
t.Error("expected IsClosed to return true when session is nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns false for open", func(t *testing.T) {
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
if c.IsClosed() {
|
||||
t.Error("expected IsClosed to return false for open connector")
|
||||
}
|
||||
c.Close()
|
||||
})
|
||||
|
||||
t.Run("returns true after close", func(t *testing.T) {
|
||||
s, _ := newTestSession(t)
|
||||
c := newTestConnector(
|
||||
newTestConnectorID(t, false, 1),
|
||||
newTestTunnelID(t),
|
||||
"node1", s,
|
||||
&ConnectorOptions{},
|
||||
)
|
||||
c.Close()
|
||||
if !c.IsClosed() {
|
||||
t.Error("expected IsClosed to return true after close")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectorPool(t *testing.T) {
|
||||
t.Run("get from nil pool", func(t *testing.T) {
|
||||
var p *ConnectorPool
|
||||
c := p.Get("tcp", "tid")
|
||||
if c != nil {
|
||||
t.Error("expected nil from nil pool")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("close nil pool", func(t *testing.T) {
|
||||
var p *ConnectorPool
|
||||
err := p.Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty pool returns nil", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
c := p.Get("tcp", "nonexistent")
|
||||
if c != nil {
|
||||
t.Error("expected nil for nonexistent tunnel")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("close empty pool is safe", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
p.Close()
|
||||
p.Close() // double close
|
||||
})
|
||||
|
||||
t.Run("add and retrieve connector", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
s, _ := newTestSession(t)
|
||||
conn := newTestConnector(newTestConnectorID(t, false, 1), tid, "node1", s, &ConnectorOptions{})
|
||||
p.Add(tid, conn, defaultTTL)
|
||||
|
||||
c := p.Get("tcp", tid.String())
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil connector")
|
||||
}
|
||||
if !c.ID().Equal(conn.ID()) {
|
||||
t.Error("retrieved wrong connector")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("add to existing tunnel", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
conn1 := &Connector{id: newTestConnectorID(t, false, 1)}
|
||||
conn2 := &Connector{id: newTestConnectorID(t, false, 2)}
|
||||
p.Add(tid, conn1, defaultTTL)
|
||||
p.Add(tid, conn2, defaultTTL)
|
||||
|
||||
tunnelKey := tid.String()
|
||||
if len(p.tunnels[tunnelKey].connectors) != 2 {
|
||||
t.Errorf("expected 2 connectors in tunnel, got %d", len(p.tunnels[tunnelKey].connectors))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get returns closed connector", func(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
s, _ := newTestSession(t)
|
||||
conn := newTestConnector(newTestConnectorID(t, false, 1), tid, "node1", s, &ConnectorOptions{})
|
||||
p.Add(tid, conn, defaultTTL)
|
||||
conn.Close()
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // let mux propagate close
|
||||
c := p.Get("tcp", tid.String())
|
||||
// A closed connector should still be returned — the tunnel.GetConnector
|
||||
// call will filter it out based on IsClosed().
|
||||
if c != nil && c.IsClosed() {
|
||||
t.Log("closed connector returned but marked as closed — GetConnector will skip it")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectorPool_Concurrency(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
|
||||
// Concurrent Add and Get should not race
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
p.Add(tid, nil, defaultTTL)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
p.Get("tcp", tid.String())
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestConnectorPool_CloseIdleRemovesEmptyTunnels(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
defer p.Close()
|
||||
|
||||
tid := newTestTunnelID(t)
|
||||
p.Add(tid, nil, defaultTTL) // adding nil connector — tunnel has no real connectors
|
||||
|
||||
p.Close()
|
||||
}
|
||||
|
||||
func TestConnectorPool_CloseStopsIdleTicker(t *testing.T) {
|
||||
p := NewConnectorPool("node1")
|
||||
p.Close()
|
||||
// After Close, the idle ticker goroutine should stop.
|
||||
// If it didn't, the race detector would catch the data race on p.tunnels.
|
||||
}
|
||||
Reference in New Issue
Block a user