mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2026-09-21 08:11:06 +09:00
- 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 ./...
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|