diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ed3f5..b284290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 生命周期指标。 diff --git a/README.md b/README.md index 17b746c..6ee8406 100644 --- a/README.md +++ b/README.md @@ -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` 打结构化日志,不需要依赖具体网络库: diff --git a/ROADMAP.md b/ROADMAP.md index eaf8d93..a0a9df4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 回收正常。 diff --git a/transport/README.md b/transport/README.md index ac88ce0..5886123 100644 --- a/transport/README.md +++ b/transport/README.md @@ -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) | 项目 | 约定 | diff --git a/transport/native/server.go b/transport/native/server.go index 08a5d9f..f0d0949 100644 --- a/transport/native/server.go +++ b/transport/native/server.go @@ -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 { diff --git a/transport/native/tls_test.go b/transport/native/tls_test.go index 4f4815c..847a673 100644 --- a/transport/native/tls_test.go +++ b/transport/native/tls_test.go @@ -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) + } +}