feat: make native TLS protocol boundary explicit

This commit is contained in:
2026-09-22 18:39:57 +08:00
parent 4a8feaca36
commit a523e6ba3f
6 changed files with 93 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
## Unreleased
- native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。
- native `Server.Stats()` 新增连接和请求过载拒绝计数;新增可信代理配置,访问日志只在 socket 对端属于配置网段时解析 `X-Forwarded-For`。
- 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。
- native 新增 `Server.Stats()`,提供 accepted connections、active connections 和 active requests 生命周期指标。

View File

@@ -124,6 +124,8 @@ if err := app.RunWith(ctx, native.New(nil), ":8080"); err != nil {
`transport/native` 当前支持 HTTP/1.1、Keep-Alive、`Content-Length` 和 chunked 请求体、`http.Flusher` 流式响应、可选 TLS、读写超时和优雅关闭;不支持的传输编码会被拒绝。它会复用连接状态和响应对象,并可通过 `MaxConnections`、`MaxConcurrentRequests` 限制内存峰值。它仍通过标准 HTTP 请求/响应接口连接业务层,适合在现有 Handler / Service 上做网络层验证。
native TLS 当前只支持并宣告 ALPN `http/1.1`;配置 `h2` 会在启动时快速失败。需要 HTTP/2 时,可由 Nginx/Cloudflare 终止后回源 HTTP/1.1,或使用默认的 `transport/nethttp` 适配器。
自研网络层还提供 `server.Stats()`,可读取 accepted、rejected、active connections 和 active requests,与上面的业务请求 metrics 组合后能区分连接堆积、过载拒绝和 Handler 慢请求。`rejectedConnections` 表示连接闸门拒绝数,`rejectedRequests` 表示并发请求闸门返回 503 的次数。
业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库:

View File

@@ -49,6 +49,7 @@
- 已验证 `MaxConnections` 满载时快速拒绝新连接、请求完成后释放容量;后续仍需在目标部署环境观察文件描述符、内核 socket 缓冲和反向代理联动。
- 已验证 `MaxConcurrentRequests` 满载返回 503,活动请求结束后槽位释放,后续请求可以恢复处理。
- native `Stats()` 已记录连接和请求过载拒绝数;访问日志已支持可信代理网段配置,默认拒绝伪造的 `X-Forwarded-For`。
- native TLS 已明确只宣告 HTTP/1.1 ALPN;HTTP/2 暂由边缘代理终止或使用 `transport/nethttp`,待独立协议设计、流控和压测完成后再接入 native。
- 已用 16 个并发 worker 完成 3,200 次建连/断连 race 验收;MaxConnections 满载拒绝、槽位释放和 TLS 连接上限均已覆盖。
- 新增 `scripts/verifyLinux.py`,固定在 Go 1.25 Linux 容器内运行 race、vet、build 和可选 TLS/非 TLS perfcheck;perfcheck 记录堆、goroutine、连接和 Linux 文件描述符。
- Linux/arm64 已完成 30 秒、128 并发、512 空闲连接的 TLS 与非 TLS 持续回归,报告见 `performance/linuxSoak.json` 和 `performance/linuxSoakTLS.json`,均为 0 错误且连接、goroutine、FD 回收正常。

View File

@@ -54,6 +54,8 @@ if err := application.RunWith(ctx, server, ":8443"); err != nil {
需要自己持有 listener 或配置证书文件时,分别使用 `ServeTLS` 或 `RunTLS`;TLS 握手仍受 native 的读超时、连接上限和优雅关闭控制,Handler 可通过 `Request.TLS` 读取标准 `tls.ConnectionState`。
native 当前只宣告 TLS ALPN `http/1.1`。未配置 `NextProtos` 时会自动补上;如果配置了 `h2` 等未实现协议,服务会在启动时直接返回错误,避免 TLS 协商成功后再被 HTTP/1.1 解析器拒绝。需要 HTTP/2 时,先让 Nginx/Cloudflare 在边缘终止 HTTP/2,再以 HTTP/1.1 回源;或继续使用默认的 `transport/nethttp` 适配器。native 的 HTTP/2 会在独立协议设计和压测完成后再进入。
## 可移植路由规则(v0.3.0)
| 项目 | 约定 |

View File

@@ -12,6 +12,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
@@ -33,6 +34,7 @@ const (
defaultMaxBodyBytes = 8 << 20
defaultReadBufferSize = 4 << 10
maxPooledResponseBytes = 64 << 10
nativeALPNProtocol = "http/1.1"
)
// Server is a small HTTP/1.1 server with project-owned connection lifecycle.
@@ -194,9 +196,27 @@ func (s *Server) ServeTLS(ctx context.Context, listener net.Listener, config *tl
if config == nil {
return errors.New("go-omni/native: nil TLS config")
}
config, err := prepareTLSConfig(config)
if err != nil {
_ = listener.Close()
return err
}
return s.Serve(ctx, tls.NewListener(listener, config))
}
func prepareTLSConfig(config *tls.Config) (*tls.Config, error) {
clone := config.Clone()
if len(clone.NextProtos) == 0 {
clone.NextProtos = []string{nativeALPNProtocol}
}
for _, protocol := range clone.NextProtos {
if protocol != nativeALPNProtocol {
return nil, fmt.Errorf("go-omni/native: unsupported TLS ALPN protocol %q; only %q is supported", protocol, nativeALPNProtocol)
}
}
return clone, nil
}
// Serve accepts connections from listener until the context is canceled or an
// unrecoverable accept error occurs. The listener is owned by the Server.
func (s *Server) Serve(ctx context.Context, listener net.Listener) error {

View File

@@ -10,6 +10,7 @@ import (
"io"
"net"
"net/http"
"strings"
"sync/atomic"
"testing"
"time"
@@ -186,3 +187,69 @@ func TestNativeTLSRequestMetadata(t *testing.T) {
t.Fatalf("TLS metadata unavailable to handler: status=%d", response.StatusCode)
}
}
func TestNativeTLSDefaultsToHTTP11ALPN(t *testing.T) {
server := native.New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || r.TLS.NegotiatedProtocol != "http/1.1" {
http.Error(w, "missing HTTP/1.1 ALPN", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}))
certificate := testCertificate(t)
leaf, err := x509.ParseCertificate(certificate.Certificate[0])
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AddCert(leaf)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() {
done <- server.ServeTLS(context.Background(), listener, &tls.Config{
Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12,
})
}()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
t.Errorf("TLS shutdown: %v", err)
}
if err := <-done; err != nil {
t.Errorf("TLS serve: %v", err)
}
}()
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{
RootCAs: roots, ServerName: "localhost", MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1"},
}}, Timeout: 2 * time.Second}
defer client.CloseIdleConnections()
response, err := client.Get("https://" + listener.Addr().String() + "/")
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusNoContent {
t.Fatalf("unexpected status: %d", response.StatusCode)
}
}
func TestNativeTLSRejectsUnsupportedALPN(t *testing.T) {
server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
certificate := testCertificate(t)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
err = server.ServeTLS(context.Background(), listener, &tls.Config{
Certificates: []tls.Certificate{certificate}, NextProtos: []string{"h2"},
})
if err == nil || !strings.Contains(err.Error(), "unsupported TLS ALPN protocol") {
t.Fatalf("expected unsupported ALPN error, got %v", err)
}
}