From 9f9c609809aeb9044ee9f9777a05847ba1d6296c Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 2 Jun 2026 16:07:44 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0=20HTT?= =?UTF-8?q?P/3=20=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_server/common/constants.go | 1 + openflare_server/controller/option.go | 1 + openflare_server/model/option.go | 3 + openflare_server/service/config_version.go | 4 + openflare_server/service/https_phase1_test.go | 77 +++++++++++++++++++ .../utils/render/openresty/render.go | 18 ++++- .../utils/render/openresty/types.go | 1 + .../components/performance-page.tsx | 21 +++++ 8 files changed, 122 insertions(+), 4 deletions(-) diff --git a/openflare_server/common/constants.go b/openflare_server/common/constants.go index f563f0da..51b55660 100644 --- a/openflare_server/common/constants.go +++ b/openflare_server/common/constants.go @@ -77,6 +77,7 @@ var OpenRestyProxyConnectTimeout = 3 var OpenRestyProxySendTimeout = 60 var OpenRestyProxyReadTimeout = 60 var OpenRestyWebsocketEnabled = true +var OpenRestyHTTP3Enabled = true var OpenRestyProxyRequestBufferingEnabled = false var OpenRestyProxyBufferingEnabled = true var OpenRestyProxyBuffers = "16 16k" diff --git a/openflare_server/controller/option.go b/openflare_server/controller/option.go index 80f68a3b..b178a0d4 100644 --- a/openflare_server/controller/option.go +++ b/openflare_server/controller/option.go @@ -159,6 +159,7 @@ func validateOpenRestyOption(key string, value string) error { return nil case "OpenRestyEventsMultiAcceptEnabled", "OpenRestyWebsocketEnabled", + "OpenRestyHTTP3Enabled", "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyGzipEnabled", diff --git a/openflare_server/model/option.go b/openflare_server/model/option.go index 776b33c1..3b96eca1 100644 --- a/openflare_server/model/option.go +++ b/openflare_server/model/option.go @@ -70,6 +70,7 @@ func InitOptionMap() { common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout) common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout) common.OptionMap["OpenRestyWebsocketEnabled"] = strconv.FormatBool(common.OpenRestyWebsocketEnabled) + common.OptionMap["OpenRestyHTTP3Enabled"] = strconv.FormatBool(common.OpenRestyHTTP3Enabled) common.OptionMap["OpenRestyProxyRequestBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyRequestBufferingEnabled) common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled) common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers @@ -278,6 +279,8 @@ func updateOptionMap(key string, value string) { } case "OpenRestyWebsocketEnabled": common.OpenRestyWebsocketEnabled = value == "true" + case "OpenRestyHTTP3Enabled": + common.OpenRestyHTTP3Enabled = value == "true" case "OpenRestyProxyRequestBufferingEnabled": common.OpenRestyProxyRequestBufferingEnabled = value == "true" case "OpenRestyProxyBufferingEnabled": diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index 231885a5..8c15ba7b 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -152,6 +152,7 @@ type openRestyConfigSnapshot struct { ProxySendTimeout int `json:"proxy_send_timeout"` ProxyReadTimeout int `json:"proxy_read_timeout"` WebsocketEnabled bool `json:"websocket_enabled"` + HTTP3Enabled bool `json:"http3_enabled"` ProxyRequestBuffering bool `json:"proxy_request_buffering"` ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"` ProxyBuffers string `json:"proxy_buffers"` @@ -952,6 +953,7 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { ProxySendTimeout: common.OpenRestyProxySendTimeout, ProxyReadTimeout: common.OpenRestyProxyReadTimeout, WebsocketEnabled: common.OpenRestyWebsocketEnabled, + HTTP3Enabled: common.OpenRestyHTTP3Enabled, ProxyRequestBuffering: common.OpenRestyProxyRequestBufferingEnabled, ProxyBufferingEnabled: common.OpenRestyProxyBufferingEnabled, ProxyBuffers: common.OpenRestyProxyBuffers, @@ -1071,6 +1073,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon appendIfChanged("OpenRestyProxySendTimeout", fmt.Sprintf("%d", left.ProxySendTimeout), fmt.Sprintf("%d", right.ProxySendTimeout)) appendIfChanged("OpenRestyProxyReadTimeout", fmt.Sprintf("%d", left.ProxyReadTimeout), fmt.Sprintf("%d", right.ProxyReadTimeout)) appendIfChanged("OpenRestyWebsocketEnabled", fmt.Sprintf("%t", left.WebsocketEnabled), fmt.Sprintf("%t", right.WebsocketEnabled)) + appendIfChanged("OpenRestyHTTP3Enabled", fmt.Sprintf("%t", left.HTTP3Enabled), fmt.Sprintf("%t", right.HTTP3Enabled)) appendIfChanged("OpenRestyProxyRequestBufferingEnabled", fmt.Sprintf("%t", left.ProxyRequestBuffering), fmt.Sprintf("%t", right.ProxyRequestBuffering)) appendIfChanged("OpenRestyProxyBufferingEnabled", fmt.Sprintf("%t", left.ProxyBufferingEnabled), fmt.Sprintf("%t", right.ProxyBufferingEnabled)) appendIfChanged("OpenRestyProxyBuffers", left.ProxyBuffers, right.ProxyBuffers) @@ -1119,6 +1122,7 @@ func openRestyOptionKeys() []string { "OpenRestyProxySendTimeout", "OpenRestyProxyReadTimeout", "OpenRestyWebsocketEnabled", + "OpenRestyHTTP3Enabled", "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyProxyBuffers", diff --git a/openflare_server/service/https_phase1_test.go b/openflare_server/service/https_phase1_test.go index 2c6d232b..3903bb26 100644 --- a/openflare_server/service/https_phase1_test.go +++ b/openflare_server/service/https_phase1_test.go @@ -489,6 +489,9 @@ func TestPublishConfigVersionRendersMultipleCertificatesForMultiDomainWebsite(t func TestPublishConfigVersionSkipsHTTPSForDomainsWithoutCertificate(t *testing.T) { setupServiceTestDB(t) + if err := model.UpdateOption("OpenRestyHTTP3Enabled", "false"); err != nil { + t.Fatalf("UpdateOption failed: %v", err) + } appCertPEM, appKeyPEM := generateCertificatePair(t, []string{"app.example.com"}) appCertificate, err := CreateTLSCertificate(TLSCertificateInput{ @@ -1338,6 +1341,80 @@ func TestOpenRestyProxyRequestBufferingDefaultsToOff(t *testing.T) { } } +func TestPreviewConfigVersionSupportsHTTP3(t *testing.T) { + setupServiceTestDB(t) + + appCertPEM, appKeyPEM := generateCertificatePair(t, []string{"h3.example.com"}) + appCertificate, err := CreateTLSCertificate(TLSCertificateInput{ + Name: "h3-cert", + CertPEM: appCertPEM, + KeyPEM: appKeyPEM, + }) + if err != nil { + t.Fatalf("CreateTLSCertificate failed: %v", err) + } + + _, err = CreateProxyRoute(ProxyRouteInput{ + SiteName: "h3-site", + Domains: []string{"h3.example.com"}, + OriginURL: "https://origin.internal", + Enabled: true, + EnableHTTPS: true, + CertID: &appCertificate.ID, + DomainCertIDs: []uint{appCertificate.ID}, + }) + if err != nil { + t.Fatalf("CreateProxyRoute failed: %v", err) + } + + if err := model.UpdateOption("OpenRestyHTTP3Enabled", "true"); err != nil { + t.Fatalf("UpdateOption OpenRestyHTTP3Enabled failed: %v", err) + } + + preview, err := PreviewConfigVersion() + if err != nil { + t.Fatalf("PreviewConfigVersion failed: %v", err) + } + + if !strings.Contains(preview.MainConfig, "listen 443 ssl default_server;\n listen 443 quic reuseport default_server;") { + t.Fatalf("expected default server block to contain quic reuseport listener, main config: %s", preview.MainConfig) + } + + if !strings.Contains(preview.RenderedConfig, "listen 443 quic;") { + t.Fatalf("expected routing server block to contain listen 443 quic, rendered config: %s", preview.RenderedConfig) + } + if !strings.Contains(preview.RenderedConfig, "add_header Alt-Svc 'h3=\":443\"; ma=86400';") { + t.Fatalf("expected routing server block to contain Alt-Svc header, rendered config: %s", preview.RenderedConfig) + } + + if !strings.Contains(preview.RenderedConfig, "listen 443 ssl;") { + t.Fatal("expected standard listen 443 ssl to be preserved") + } + if !strings.Contains(preview.RenderedConfig, "http2 on;") { + t.Fatal("expected standard http2 on to be preserved") + } + + if err := model.UpdateOption("OpenRestyHTTP3Enabled", "false"); err != nil { + t.Fatalf("UpdateOption OpenRestyHTTP3Enabled failed: %v", err) + } + + previewOff, err := PreviewConfigVersion() + if err != nil { + t.Fatalf("PreviewConfigVersion failed: %v", err) + } + + if strings.Contains(previewOff.MainConfig, "listen 443 quic reuseport default_server;") { + t.Fatal("expected default server block to omit quic listener when disabled") + } + + if strings.Contains(previewOff.RenderedConfig, "listen 443 quic;") { + t.Fatal("expected routing server block to omit quic listener when disabled") + } + if strings.Contains(previewOff.RenderedConfig, "add_header Alt-Svc") { + t.Fatal("expected routing server block to omit Alt-Svc header when disabled") + } +} + func setupServiceTestDB(t *testing.T) { t.Helper() nodeAccessTokenCache.reset() diff --git a/openflare_server/utils/render/openresty/render.go b/openflare_server/utils/render/openresty/render.go index fd94f465..6cf85fb3 100644 --- a/openflare_server/utils/render/openresty/render.go +++ b/openflare_server/utils/render/openresty/render.go @@ -305,7 +305,7 @@ func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot) string { "{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections), "{{OpenRestyWorkerRlimitNofile}}", fmt.Sprintf("%d", cfg.WorkerRlimitNofile), "{{OpenRestyConnectionUpgradeMap}}", renderConnectionUpgradeMap(), - "{{OpenRestyDefaultServerBlock}}", renderDefaultServerBlock(cfg.DefaultServerReturnStatus), + "{{OpenRestyDefaultServerBlock}}", renderDefaultServerBlock(cfg.DefaultServerReturnStatus, cfg.HTTP3Enabled), "{{OpenRestyAccessLogPath}}", AccessLogPlaceholder, "{{OpenRestyErrorLogPath}}", ErrorLogPlaceholder, "{{OpenRestyEventsUseDirective}}", renderTemplateDirective(cfg.EventsUse != "", fmt.Sprintf("use %s;", cfg.EventsUse)), @@ -379,7 +379,13 @@ func renderHTTPRedirectServer(serverNames string) string { func renderHTTPSServer(serverNames string, siteName string, originURL string, originHost string, certificateID uint, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg ConfigSnapshot) string { certPath := fmt.Sprintf("%s/%d.crt", CertDirPlaceholder, certificateID) keyPath := fmt.Sprintf("%s/%d.key", CertDirPlaceholder, certificateID) - return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) + var h3Listen string + var h3Header string + if cfg.HTTP3Enabled { + h3Listen = " listen 443 quic;\n" + h3Header = " add_header Alt-Svc 'h3=\":443\"; ma=86400';\n" + } + return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } func renderProxyHeaderBlock(originURL string, originHost string, customHeaders []CustomHeader, upstreamConfig routeUpstreamConfig, cfg ConfigSnapshot) string { @@ -586,10 +592,14 @@ func renderConnectionUpgradeMap() string { return " map $http_upgrade $connection_upgrade {\n default upgrade;\n '' \"\";\n }\n\n" } -func renderDefaultServerBlock(statusCode int) string { +func renderDefaultServerBlock(statusCode int, http3Enabled bool) string { if statusCode <= 0 { statusCode = 421 } + var h3Default string + if http3Enabled { + h3Default = "\n listen 443 quic reuseport default_server;" + } return strings.Join([]string{ " server {", " listen 80 default_server;", @@ -599,7 +609,7 @@ func renderDefaultServerBlock(statusCode int) string { " }", "", " server {", - " listen 443 ssl default_server;", + fmt.Sprintf(" listen 443 ssl default_server;%s", h3Default), " server_name _;", "", " ssl_reject_handshake on;", diff --git a/openflare_server/utils/render/openresty/types.go b/openflare_server/utils/render/openresty/types.go index 42be01a9..7436f851 100644 --- a/openflare_server/utils/render/openresty/types.go +++ b/openflare_server/utils/render/openresty/types.go @@ -175,6 +175,7 @@ type ConfigSnapshot struct { ProxySendTimeout int `json:"proxy_send_timeout"` ProxyReadTimeout int `json:"proxy_read_timeout"` WebsocketEnabled bool `json:"websocket_enabled"` + HTTP3Enabled bool `json:"http3_enabled"` ProxyRequestBuffering bool `json:"proxy_request_buffering"` ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"` ProxyBuffers string `json:"proxy_buffers"` diff --git a/openflare_server/web/features/performance/components/performance-page.tsx b/openflare_server/web/features/performance/components/performance-page.tsx index 85b026a2..4601e0d9 100644 --- a/openflare_server/web/features/performance/components/performance-page.tsx +++ b/openflare_server/web/features/performance/components/performance-page.tsx @@ -48,6 +48,7 @@ const defaultPerformanceFields = { OpenRestyProxySendTimeout: '60', OpenRestyProxyReadTimeout: '60', OpenRestyWebsocketEnabled: true, + OpenRestyHTTP3Enabled: true, OpenRestyProxyRequestBufferingEnabled: false, OpenRestyProxyBufferingEnabled: true, OpenRestyProxyBuffers: '16 16k', @@ -96,6 +97,7 @@ const performanceFieldTooltips: Record = { proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。', websocket: '控制是否为反向代理规则自动注入 WebSocket 升级所需的 HTTP/1.1、Upgrade 和 Connection 头。', + http3: '启用后,OpenResty 将在 443 端口启用 HTTP/3 (QUIC) 协议支持。', proxy_request_buffering: '控制请求体是否先在 Nginx 侧缓冲后再转发给上游,上传和流式场景经常会用到。', proxy_buffering: @@ -233,6 +235,10 @@ export function PerformancePage() { optionMap.OpenRestyWebsocketEnabled, true, ), + OpenRestyHTTP3Enabled: toBoolean( + optionMap.OpenRestyHTTP3Enabled, + false, + ), OpenRestyProxyRequestBufferingEnabled: toBoolean( optionMap.OpenRestyProxyRequestBufferingEnabled, false, @@ -450,6 +456,10 @@ export function PerformancePage() { 'OpenRestyWebsocketEnabled', String(performanceFields.OpenRestyWebsocketEnabled), ], + [ + 'OpenRestyHTTP3Enabled', + String(performanceFields.OpenRestyHTTP3Enabled), + ], [ 'OpenRestyProxyRequestBufferingEnabled', String(performanceFields.OpenRestyProxyRequestBufferingEnabled), @@ -1008,6 +1018,17 @@ export function PerformancePage() { })) } /> + + setPerformanceFields((previous) => ({ + ...previous, + OpenRestyHTTP3Enabled: checked, + })) + } + />