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)
|
af.ParseFrom(dstAddr)
|
||||||
resp.Features = append(resp.Features, af) // dst address
|
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 {
|
} else {
|
||||||
req.WriteTo(cc)
|
if _, err := req.WriteTo(cc); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
cc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
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++ {
|
for i := 0; i < retry; i++ {
|
||||||
c := d.pool.Get(network, tid)
|
c := d.pool.Get(network, tid)
|
||||||
if c == nil {
|
if c == nil {
|
||||||
|
err = nil // clear stale err so SD fallback is not masked
|
||||||
break
|
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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gost/core/handler"
|
|
||||||
"github.com/go-gost/core/ingress"
|
"github.com/go-gost/core/ingress"
|
||||||
"github.com/go-gost/core/listener"
|
|
||||||
"github.com/go-gost/core/logger"
|
"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/observer/stats"
|
||||||
"github.com/go-gost/core/recorder"
|
"github.com/go-gost/core/recorder"
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/relay"
|
"github.com/go-gost/relay"
|
||||||
dissector "github.com/go-gost/tls-dissector"
|
dissector "github.com/go-gost/tls-dissector"
|
||||||
admission "github.com/go-gost/x/admission/wrapper"
|
|
||||||
xctx "github.com/go-gost/x/ctx"
|
xctx "github.com/go-gost/x/ctx"
|
||||||
ictx "github.com/go-gost/x/internal/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"
|
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"
|
xstats "github.com/go-gost/x/observer/stats"
|
||||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||||
xrecorder "github.com/go-gost/x/recorder"
|
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 {
|
type entrypoint struct {
|
||||||
node string
|
node string
|
||||||
service 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)
|
return ep.handleConnect(ctx, conn, ro, log)
|
||||||
}
|
}
|
||||||
if v[0] == dissector.Handshake {
|
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)
|
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
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gost/core/auth"
|
"github.com/go-gost/core/auth"
|
||||||
"github.com/go-gost/core/handler"
|
"github.com/go-gost/core/handler"
|
||||||
"github.com/go-gost/core/ingress"
|
|
||||||
"github.com/go-gost/core/limiter"
|
"github.com/go-gost/core/limiter"
|
||||||
"github.com/go-gost/core/limiter/traffic"
|
"github.com/go-gost/core/limiter/traffic"
|
||||||
"github.com/go-gost/core/listener"
|
|
||||||
"github.com/go-gost/core/logger"
|
"github.com/go-gost/core/logger"
|
||||||
md "github.com/go-gost/core/metadata"
|
md "github.com/go-gost/core/metadata"
|
||||||
"github.com/go-gost/core/observer"
|
"github.com/go-gost/core/observer"
|
||||||
"github.com/go-gost/core/service"
|
"github.com/go-gost/core/service"
|
||||||
"github.com/go-gost/relay"
|
"github.com/go-gost/relay"
|
||||||
xctx "github.com/go-gost/x/ctx"
|
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"
|
stats_util "github.com/go-gost/x/internal/util/stats"
|
||||||
rate_limiter "github.com/go-gost/x/limiter/rate"
|
rate_limiter "github.com/go-gost/x/limiter/rate"
|
||||||
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
|
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
|
||||||
xrecorder "github.com/go-gost/x/recorder"
|
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
xservice "github.com/go-gost/x/service"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,106 +100,6 @@ func (h *tunnelHandler) Init(md md.Metadata) (err error) {
|
|||||||
return nil
|
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) {
|
func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@@ -255,7 +148,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
|||||||
|
|
||||||
if req.Version != relay.Version1 {
|
if req.Version != relay.Version1 {
|
||||||
resp.Status = relay.StatusBadRequest
|
resp.Status = relay.StatusBadRequest
|
||||||
resp.WriteTo(conn)
|
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||||
return ErrBadVersion
|
return ErrBadVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +184,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
|||||||
|
|
||||||
if tunnelID.IsZero() {
|
if tunnelID.IsZero() {
|
||||||
resp.Status = relay.StatusBadRequest
|
resp.Status = relay.StatusBadRequest
|
||||||
resp.WriteTo(conn)
|
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||||
return ErrTunnelID
|
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))
|
clientID, ok := h.options.Auther.Authenticate(ctx, user, pass, auth.WithService(h.options.Service))
|
||||||
if !ok {
|
if !ok {
|
||||||
resp.Status = relay.StatusUnauthorized
|
resp.Status = relay.StatusUnauthorized
|
||||||
resp.WriteTo(conn)
|
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
|
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)
|
return h.handleBind(ctx, conn, network, dstAddr, tunnelID, log)
|
||||||
default:
|
default:
|
||||||
resp.Status = relay.StatusBadRequest
|
resp.Status = relay.StatusBadRequest
|
||||||
resp.WriteTo(conn)
|
resp.WriteTo(conn) // write error ignored — conn is about to be closed
|
||||||
return ErrUnknownCmd
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/core/sd"
|
||||||
"github.com/go-gost/relay"
|
"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/go-gost/x/selector"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxWeight uint8 = 0xff
|
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 {
|
type Tunnel struct {
|
||||||
node string
|
node string
|
||||||
id relay.TunnelID
|
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