feat: make native TLS protocol boundary explicit
This commit is contained in:
@@ -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 生命周期指标。
|
||||
|
||||
@@ -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` 打结构化日志,不需要依赖具体网络库:
|
||||
|
||||
@@ -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 回收正常。
|
||||
|
||||
@@ -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)
|
||||
|
||||
| 项目 | 约定 |
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user