bigywater.tech
Gin · 52

h2c、HTTP/2、unix socket、RunTLS

Gin 能不能跑 HTTP/2、明文 h2c、unix socket?这篇起了真实的服务进程,用 curl 与 handler 里看到的 Proto / ALPN / TLS 版本逐项验证,并发现了 HTTP/2 下 Hijack 会 panic、unix socket 上 ClientIP 的两个怪行为。

HTTP/2.0
RunTLS 下 curl 默认协商出的协议(ALPN=h2,TLS 1.3)
UseH2C
打开之后 curl --http2-prior-knowledge 成功;没打开时失败
""
unix socket 上没有转发头时 c.ClientIP() 的返回值

只是 http.Server

实测 · 本机执行输出
  gin.go 的 Handler(Gin v1.12.0):
     243 | func (engine *Engine) Handler() http.Handler {
     244 |     if !engine.UseH2C {
     245 |         return engine
     246 |     }
     247 | 
     248 |     h2s := &http2.Server{}
     249 |     return h2c.NewHandler(engine, h2s)
     250 | }
  gin.go 的 RunTLS(Gin v1.12.0):
     561 | func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
     562 |     debugPrint("Listening and serving HTTPS on %s\n", addr)
     563 |     defer func() { debugPrintError(err) }()
     564 | 
     565 |     if engine.isUnsafeTrustedProxies() {
     566 |         debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
     567 |             "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
     568 |     }
     569 | 
     570 |     server := &http.Server{ // #nosec G112
     571 |         Addr:    addr,
     572 |         Handler: engine.Handler(),
     573 |     }
     574 |     err = server.ListenAndServeTLS(certFile, keyFile)
     575 |     return
     576 | }

四个 Run* 方法都是新建一个只设了 Handler 的 http.Server。HTTPS 的 HTTP/2 是标准库 ListenAndServeTLS 通过 ALPN 自动启用的;明文 h2c 需要 engine.UseH2C = true,此时 Handler() 用 x/net 的 h2c.NewHandler 包一层,同一端口同时接受 HTTP/1.1 和 h2c。

h2c

实测 · 本机执行输出
  [plain]
    curl --http1.1:            Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53959"[1.1] 200
    curl --http2-prior-knowledge: [0] 000 curl: (55) Remote peer returned unexpected data while we expected SETTINGS frame.  Perhaps, peer does not support HTTP/2 properly.
    curl --http2(带 Upgrade: h2c):  Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53961"[1.1] 200
  [h2c]
    curl --http1.1:            Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53964"[1.1] 200
    curl --http2-prior-knowledge: Proto=HTTP/2.0 ProtoMajor=2 TLS=无 ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53965"[2] 200
    curl --http2(带 Upgrade: h2c):  Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53966"[2] 200

没开 UseH2C 时,以 prior knowledge 方式说 HTTP/2 的客户端直接失败(服务端回的是 HTTP/1.1 的数据)。开了之后 prior knowledge 成功,Request.Proto 是 HTTP/2.0。带 Upgrade: h2c 的请求,第一个请求仍按 HTTP/1.1 处理,之后连接才升级。

HTTPS 与 HTTP/2 的差别

实测 · 本机执行输出
    curl -k(默认):             Proto=HTTP/2.0 ProtoMajor=2 TLS=版本0x304 ALPN="h2" ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53969"[2] 200
    curl -k --http1.1:          Proto=HTTP/1.1 ProtoMajor=1 TLS=版本0x304 ALPN="http/1.1" ClientIP="127.0.0.1" RemoteAddr="127.0.0.1:53970"[1.1] 200
    curl -k --tlsv1.1 --tls-max 1.1: [0] 000 curl: (35) LibreSSL/3.3.6: error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version
    curl 不加 -k(证书不受信任): [0] 000 curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html  curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the web page mentioned above.
    HTTP/2 下的 Pusher:          Push 返回 feature not supported[2] 200
    HTTP/2 下的 Hijack:          Hijack panic: interface conversion: http.http2ResponseWriter is not http.Hijacker: missing method Hijack[2] 200
    HTTP/1.1 下的 Pusher:        Pusher() 为 nil[1.1] 200
    HTTP/1.1 下的 Hijack:        Hijack 成功(HTTP/1.1)[1.1] 200

TLS 默认最低 1.2(TLS 1.1 被拒),ALPN 协商出 h2 时 handler 看到 HTTP/2.0。注意几个 HTTP/2 下的不同:Pusher() 非 nil 但客户端关闭推送时 Push 返回 feature not supported;Hijack 不被支持,而 Gin 的 responseWriter.Hijack 直接做类型断言,所以在 HTTP/2 上调用会 panic(被 Recovery 接住也只是 500)。依赖 Hijack 的功能(WebSocket)要么用 HTTP/1.1,要么用 HTTP/2 的扩展 CONNECT。

证书缺失

实测 · 本机执行输出
    服务结束: open <tmp>/nope.pem: no such file or directory

RunTLS 找不到证书或密钥文件时立刻返回错误,不会退化成明文服务。它返回的 error 必须被检查——常见的写法 r.RunTLS(...) 不检查返回值时,程序会直接退出而没有任何提示。

unix socket

实测 · 本机执行输出
    socket 文件存在: True  权限: 0o755
    无头:                      Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="<nil>" RemoteAddr=""
    X-Forwarded-For: 1.2.3.4:   Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="1.2.3.4" RemoteAddr=""
    X-Forwarded-For: 9.9.9.9, 1.2.3.4: Proto=HTTP/1.1 ProtoMajor=1 TLS=无 ClientIP="1.2.3.4" RemoteAddr=""
    SIGTERM 之后 socket 文件仍存在: True (进程被信号杀死,defer os.Remove 没有机会执行)
    在残留的 socket 文件上重启: 服务结束: listen unix <tmp>/g.sock: bind: address already in use

RunUnix 对 unix socket 上的连接一律信任转发头,不管 SetTrustedProxies 怎么设(设计上假设 socket 后面是本机的反向代理)。注意没有转发头时 RemoteAddr 是空串,ClientIP() 会返回字面量 "<nil>"。socket 文件在正常返回时由 defer os.Remove 删除,进程被信号杀死时不会,所以重启前需要先清理残留文件,或者配合优雅关机。

分步演示

h2c、HTTP/2 与 unix socket
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端是编译好的真实二进制(自签名 ECDSA 证书在启动时生成,实验后删除),客户端用 curl 8.7.1(LibreSSL 3.3.6、nghttp2 1.69.0);每种情形各一次;handler 打印的 Proto、TLS 版本、ALPN 来自 http.Request
    • 没有验证:HTTP/2 的多路复用性能;Gin 的 HTTP/2 push 在真实浏览器里的行为(浏览器已移除);HTTP/3(QUIC);自定义 TLSConfig(如最低版本、CipherSuites,需要自己创建 http.Server);RunFd;unix socket 的权限修改(umask);h2c 的 Upgrade 路径在其它客户端里的行为
    京ICP备2026067502号-1京公网安备11010502063487号