Files
x/handler/tunnel/eptls.go
T
ginuerzh 31878a72ec docs(handler/tunnel): add architecture documentation and fix observeStats event loss
- Add package-level architecture doc in handler.go covering:
  - NAT traversal reverse proxy architecture
  - CmdBind/CmdConnect roles and data flow
  - Connector lifecycle and waitClose semantics
  - Entrypoint protocol dispatch (first-byte sniffing)
  - SD fallback behavior
- Add data-flow-oriented doc comments to all business files:
  - entrypoint.go: protocol dispatch, dial flow
  - connector.go: Connector/ConnectorPool semantics
  - dialer.go: two-phase dial strategy
  - tunnel.go: MaxWeight semantics, selection algorithm
  - bind.go: 6-step CmdBind flow
  - connect.go: CmdConnect flow with relay framing
  - ephttp.go, eptls.go, eprelay.go: per-protocol entrypoint flow
- Fix observeStats: after successful error retry, also flush new events
  instead of skipping the current tick (handler.go:261-271)
- Add test for observeStats retry-then-flush (handler_test.go)
2026-06-02 18:36:52 +08:00

94 lines
2.6 KiB
Go

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"
)
// handleTLS processes a TLS connection arriving at the entrypoint.
//
// Flow:
// 1. Parse ClientHello (tee-reads first bytes for recording).
// 2. Extract SNI hostname → ingress lookup → tunnelID.
// 3. ep.dial() → Dialer.Dial() → mux stream (or SD TCP connection).
// 4. Write buffered ClientHello bytes to the mux stream.
// 5. Parse ServerHello from the mux stream (for TLS recording).
// 6. Write ServerHello bytes back to the public connection.
// 7. Pipe(publicConn, muxStream) — bidirectional TLS passthrough.
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
}