Gin 能不能跑 HTTP/2、明文 h2c、unix socket?这篇起了真实的服务进程,用 curl 与 handler 里看到的 Proto / ALPN / TLS 版本逐项验证,并发现了 HTTP/2 下 Hijack 会 panic、unix socket 上 ClientIP 的两个怪行为。
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。
[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 处理,之后连接才升级。
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] 200TLS 默认最低 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(...) 不检查返回值时,程序会直接退出而没有任何提示。
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 useRunUnix 对 unix socket 上的连接一律信任转发头,不管 SetTrustedProxies 怎么设(设计上假设 socket 后面是本机的反向代理)。注意没有转发头时 RemoteAddr 是空串,ClientIP() 会返回字面量 "<nil>"。socket 文件在正常返回时由 defer os.Remove 删除,进程被信号杀死时不会,所以重启前需要先清理残留文件,或者配合优雅关机。