Skip to content

HTTPUpgrade 传输

简介

HTTPUpgrade 是一种轻量级传输协议,模拟 WebSocket 握手(HTTP/1.1 Upgrade: websocket)但不使用 WebSocket 帧格式。升级握手完成后,连接变为原始 TCP 流。这使得它比完整的 WebSocket 传输更简单、更高效,同时仍能通过期望 HTTP Upgrade 流的中间设备。它支持 TLS、uTLS 指纹、PROXY 协议和早期数据。

协议注册

"httpupgrade" 名称注册(transport/internet/httpupgrade/httpupgrade.go:3):

go
const protocolName = "httpupgrade"
  • 拨号器httpupgrade/dialer.go:134-136
  • 监听器httpupgrade/hub.go:165-167
  • 配置httpupgrade/config.go:19-23

拨号流程

握手

dialhttpUpgradehttpupgrade/dialer.go:46-115)执行手动 HTTP Upgrade:

go
func dialhttpUpgrade(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
    // 1. 通过 internet.DialSystem 拨号原始 TCP
    pconn, _ := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)

    // 2. 可选地用 TLS 包装
    if tConfig != nil {
        // 应用带 uTLS 指纹的 TLS 或标准 Go TLS
        conn = tls.UClient(pconn, tlsConfig, fingerprint) // 或 tls.Client
    }

    // 3. 构建 HTTP 请求
    req := &http.Request{
        Method: http.MethodGet,
        URL:    &requestURL,
        Header: make(http.Header),
    }
    req.Header.Set("Connection", "Upgrade")
    req.Header.Set("Upgrade", "websocket")

    // 4. 将请求直接写入连接
    req.Write(conn)

    // 5. 用 ConnRF 包装以读取响应
    connRF := &ConnRF{Conn: conn, Req: req, First: true}
    return connRF, nil
}

与 WebSocket 传输不同,这里不使用 gorilla/websocket。HTTP 请求直接写入 TCP 流,响应也手动解析。

响应读取(ConnRF)

ConnRF 结构体(httpupgrade/dialer.go:19-44)拦截首次 Read 调用以解析 HTTP 响应:

go
type ConnRF struct {
    net.Conn
    Req   *http.Request
    First bool
}

func (c *ConnRF) Read(b []byte) (int, error) {
    if c.First {
        c.First = false
        reader := bufio.NewReaderSize(c.Conn, len(b))
        resp, err := http.ReadResponse(reader, c.Req)
        if resp.Status != "101 Switching Protocols" ||
            strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
            strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
            return 0, errors.New("unrecognized reply")
        }
        // 排空 bufreader 中缓冲的字节
        return reader.Read(b[:reader.Buffered()])
    }
    return c.Conn.Read(b)
}

关键设计:bufio.Reader 的大小限制为 len(b),确保 HTTP 响应头之后的任何数据(可能在同一 TCP 报文段中到达)都能被捕获并在首次读取中返回。

早期数据

Ed == 0(默认值)时,响应在拨号期间立即读取以确认升级成功(dialer.go:107-112)。当 Ed > 0 时,响应读取推迟到应用程序的首次 Read 调用,使拨号更快完成。

头部处理

通过 AddHeader 函数(dialer.go:120-122)添加自定义头部,该函数绕过 Go 的 MIME 头部规范化:

go
func AddHeader(header http.Header, key, value string) {
    header[key] = append(header[key], value)
}

这保留了头部名称的精确大小写(例如 "Web*S*ocket" 而非 "Websocket")。

监听流程

服务器架构

ListenHTTPUpgradehttpupgrade/hub.go:115-163)创建原始 TCP 监听器(而非 HTTP 服务器):

go
func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
    streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
    // 通过 internet.ListenSystem 创建 TCP/Unix 监听器
    // 可选地用 TLS 包装
    serverInstance := &server{
        config:         transportConfiguration,
        addConn:        addConn,
        innnerListener: listener,
    }
    go serverInstance.keepAccepting()
    return serverInstance, nil
}

与 WebSocket 传输(使用 http.Server)不同,HTTPUpgrade 接受原始连接并手动解析 HTTP 请求。

连接处理

server.Handlehttpupgrade/hub.go:34-42)和 server.upgradehub.go:45-103):

go
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
    connReader := bufio.NewReader(conn)
    req, _ := http.ReadRequest(connReader)

    // 验证 host 和路径
    if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) {
        return nil, errors.New("bad host")
    }
    if req.URL.Path != path {
        return nil, errors.New("bad path")
    }

    // 验证升级头部
    if connection != "upgrade" || upgrade != "websocket" {
        return nil, errors.New("unrecognized request")
    }

    // 发送 101 响应
    resp := &http.Response{
        Status:     "101 Switching Protocols",
        StatusCode: 101,
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     http.Header{},
    }
    resp.Header.Set("Connection", "Upgrade")
    resp.Header.Set("Upgrade", "websocket")
    resp.Write(conn)

    return stat.Connection(newConnection(conn, remoteAddr)), nil
}

X-Forwarded-For

从转发头部提取真实客户端 IP(hub.go:83-100),遵循 TrustedXForwardedFor 套接字设置。

线格式

客户端 -> 服务端:
GET /path HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
User-Agent: Mozilla/5.0 ...
[自定义头部]

服务端 -> 客户端:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket

[原始双向 TCP 流]

握手完成后,数据以原始字节流传输 —— 无 WebSocket 帧格式、无长度前缀、无掩码。这是与完整 WebSocket 传输的关键区别。

mermaid
sequenceDiagram
    participant Client as 客户端
    participant Server as 服务端

    Client->>Server: GET /path HTTP/1.1\r\nUpgrade: websocket\r\n...
    Server->>Client: HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n
    Note over Client,Server: 原始 TCP 流开始
    Client->>Server: [代理数据字节]
    Server->>Client: [代理数据字节]

连接包装器

connection 结构体(httpupgrade/connection.go:5-19)非常简洁:

go
type connection struct {
    net.Conn
    remoteAddr net.Addr
}

func (c *connection) RemoteAddr() net.Addr {
    return c.remoteAddr
}

它只覆盖 RemoteAddr() 以支持 X-Forwarded-For。所有其他方法委托给底层 net.Conn

PROXY 协议支持

客户端和服务端均支持 PROXY 协议:

  • 服务端:继承自套接字设置。当 AcceptProxyProtocol 为 true 时,底层 internet.ListenSystem 包装监听器(hub.go:117-122hub.go:145-147)。
  • 配置合并:传输配置或套接字设置中的 AcceptProxyProtocol 均会触发 PROXY 协议(hub.go:121)。

配置选项

来自 httpupgrade/config.go

  • Path:URL 路径,以 / 开头规范化(config.go:8-17
  • Host:服务端验证用的预期 Host 头部
  • Header:自定义 HTTP 头部(映射)
  • AcceptProxyProtocol:在监听器上启用 PROXY 协议
  • Ed:早期数据大小。非零时,响应解析推迟。

实现说明

  • 无 WebSocket 帧101 Switching Protocols 响应之后,数据以原始字节发送。这消除了 WebSocket 开销(每帧 2-14 字节)和掩码要求。
  • 无 gorilla 依赖:与 WebSocket 传输不同,HTTPUpgrade 不使用 gorilla/websocket,直接构建和解析 HTTP 消息。
  • 无状态服务器:每个连接独立处理,服务器不维护会话状态。
  • 头部大小写保留AddHeader 函数绕过 Go 的 textproto.CanonicalMIMEHeaderKey,允许精确大小写的头部名称。这有助于匹配特定 CDN 或中间设备的期望。
  • bufio 大小技巧ConnRF.Read 创建的 bufio.ReaderSize(conn, len(b)) 限制为调用者缓冲区大小。这确保缓冲读取器不会读取超过可返回的数据量,防止双缓冲导致的数据丢失。
  • 监听器 TLS:与 gRPC(使用 gRPC 的凭证系统)不同,HTTPUpgrade 直接使用 tls.NewListener 包装 net.Listener(hub.go:149-153)。
  • 与 WebSocket 的比较:HTTPUpgrade 在代理用途上严格更简单、更高效。只有在需要 WebSocket 特定功能(心跳 ping、逐消息压缩、浏览器拨号器)时才应优先选择 WebSocket 传输。

用于重新实现目的的技术分析。