mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2026-09-21 08:11:06 +09:00
[refactor] adopt yamux-only tunnel transport [BREAK]
- replace gRPC/DTLS tunnel paths with TLS + yamux transport - remove legacy protocol, protobuf, DTLS, and proxy implementations - simplify server and client entrypoints around yamux - update configuration, metrics, API, and architecture documentation - add bidirectional yamux session tests - verify with go test ./...
This commit is contained in:
+60
-1505
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dalbodeule/hop-gate/internal/logging"
|
||||
"github.com/dalbodeule/hop-gate/internal/tunnel"
|
||||
)
|
||||
|
||||
type yamuxTunnelSession struct {
|
||||
session *tunnel.Session
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (t *yamuxTunnelSession) ForwardHTTP(ctx context.Context, logger logging.Logger, req *http.Request, serviceName string) (*tunnel.Response, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
meta := tunnel.StreamMeta{
|
||||
Kind: "http",
|
||||
Service: serviceName,
|
||||
Method: req.Method,
|
||||
Path: req.URL.RequestURI(),
|
||||
Host: req.Host,
|
||||
Headers: req.Header,
|
||||
}
|
||||
stream, err := t.session.Open(ctx, meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stream.Close()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = stream.SetDeadline(deadline)
|
||||
}
|
||||
|
||||
request := req.Clone(ctx)
|
||||
request.RequestURI = ""
|
||||
if err := request.Write(stream); err != nil {
|
||||
return nil, fmt.Errorf("write HTTP request to yamux stream: %w", err)
|
||||
}
|
||||
resp, err := http.ReadResponse(bufio.NewReader(stream), req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read HTTP response from yamux stream: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read HTTP response body from yamux stream: %w", err)
|
||||
}
|
||||
result := &tunnel.Response{
|
||||
RequestID: "yamux",
|
||||
Status: resp.StatusCode,
|
||||
Header: make(map[string][]string, len(resp.Header)),
|
||||
Body: body,
|
||||
}
|
||||
for key, values := range resp.Header {
|
||||
result.Header[key] = append([]string(nil), values...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func serveYamuxTunnel(ctx context.Context, address string, tlsConfig *tls.Config, logger logging.Logger, validator tunnel.DomainValidator) error {
|
||||
listener, err := tls.Listen("tcp", address, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen for yamux tunnel: %w", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
logger.Info("yamux tunnel listener started", logging.Fields{"addr": address})
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
logger.Error("yamux tunnel accept failed", logging.Fields{"error": err.Error()})
|
||||
continue
|
||||
}
|
||||
go handleYamuxTunnel(ctx, conn, logger, validator)
|
||||
}
|
||||
}
|
||||
|
||||
func handleYamuxTunnel(ctx context.Context, conn net.Conn, logger logging.Logger, validator tunnel.DomainValidator) {
|
||||
session, err := tunnel.NewServer(conn)
|
||||
if err != nil {
|
||||
logger.Error("create yamux server session failed", logging.Fields{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
control, err := session.Accept(ctx)
|
||||
if err != nil {
|
||||
logger.Error("accept yamux control stream failed", logging.Fields{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer control.Close()
|
||||
if control.Meta.Kind != "control" || strings.TrimSpace(control.Meta.Domain) == "" || strings.TrimSpace(control.Meta.Target) == "" {
|
||||
logger.Warn("invalid yamux control metadata", logging.Fields{"kind": control.Meta.Kind})
|
||||
return
|
||||
}
|
||||
apiKeys := control.Meta.Headers["X-HopGate-API-Key"]
|
||||
if len(apiKeys) == 0 || strings.TrimSpace(apiKeys[0]) == "" {
|
||||
logger.Warn("yamux control stream missing API key", logging.Fields{"domain": control.Meta.Domain})
|
||||
return
|
||||
}
|
||||
if validator != nil {
|
||||
if err := validator.ValidateDomainAPIKey(ctx, control.Meta.Domain, apiKeys[0]); err != nil {
|
||||
logger.Warn("yamux tunnel authentication failed", logging.Fields{"domain": control.Meta.Domain, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tunnelSession := &yamuxTunnelSession{session: session, logger: logger.With(logging.Fields{"domain": control.Meta.Domain})}
|
||||
domain := registerTunnelForDomain(control.Meta.Domain, tunnelSession, logger)
|
||||
defer unregisterTunnelForDomain(domain, tunnelSession, logger)
|
||||
logger.Info("yamux tunnel authenticated", logging.Fields{"domain": domain, "local_target": control.Meta.Target})
|
||||
|
||||
// The server opens HTTP streams. The client opens only the control stream,
|
||||
// so poll the session state rather than consuming the server's own streams.
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for !session.IsClosed() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user