Files
mihomo/transport/sudoku/httpmask_tunnel.go
2026-01-14 21:25:05 +08:00

158 lines
4.4 KiB
Go

package sudoku
import (
"context"
"fmt"
"net"
"strings"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
)
type HTTPMaskTunnelServer struct {
cfg *ProtocolConfig
ts *httpmask.TunnelServer
}
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if cfg == nil {
return &HTTPMaskTunnelServer{}
}
var ts *httpmask.TunnelServer
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
Mode: cfg.HTTPMaskMode,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
}
// WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed.
//
// Returns:
// - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return
// - done=false: handshakeConn+cfg are ready for ServerHandshake
func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) {
if rawConn == nil {
return nil, nil, true, fmt.Errorf("nil conn")
}
if s == nil {
return rawConn, nil, false, nil
}
if s.ts == nil {
return rawConn, s.cfg, false, nil
}
res, c, err := s.ts.HandleConn(rawConn)
if err != nil {
return nil, nil, true, err
}
switch res {
case httpmask.HandleDone:
return nil, nil, true, nil
case httpmask.HandlePassThrough:
return c, s.cfg, false, nil
case httpmask.HandleStartTunnel:
inner := *s.cfg
inner.DisableHTTPMask = true
return c, &inner, false, nil
default:
return nil, nil, true, nil
}
}
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if cfg.DisableHTTPMask {
return nil, fmt.Errorf("http mask is disabled")
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
default:
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
}
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: dial,
})
}
type HTTPMaskTunnelClient struct {
mode string
pathRoot string
authKey string
client *httpmask.TunnelClient
}
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if cfg.DisableHTTPMask {
return nil, fmt.Errorf("http mask is disabled")
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
default:
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMultiplex)) {
case "auto", "on":
default:
return nil, fmt.Errorf("http-mask-multiplex=%q does not enable reuse", cfg.HTTPMaskMultiplex)
}
c, err := httpmask.NewTunnelClient(serverAddress, httpmask.TunnelClientOptions{
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
DialContext: dial,
})
if err != nil {
return nil, err
}
return &HTTPMaskTunnelClient{
mode: cfg.HTTPMaskMode,
pathRoot: cfg.HTTPMaskPathRoot,
authKey: ClientAEADSeed(cfg.Key),
client: c,
}, nil
}
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if c == nil || c.client == nil {
return nil, fmt.Errorf("nil httpmask tunnel client")
}
return c.client.DialTunnel(ctx, httpmask.TunnelDialOptions{
Mode: c.mode,
PathRoot: c.pathRoot,
AuthKey: c.authKey,
Upgrade: upgrade,
})
}
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
if c == nil || c.client == nil {
return
}
c.client.CloseIdleConnections()
}