diff --git a/docs/app-config.md b/docs/app-config.md index db2ebcdc..9807be01 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -76,15 +76,14 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前 * `OpenRestyProxyReadTimeout` * `OpenRestyProxyBufferingEnabled` * `OpenRestyGzipEnabled` -* `OpenRestyResolvers` * `OpenRestyCacheEnabled` * `OpenRestyCachePath` * `OpenRestyCacheMaxSize` 这类参数必须以结构化方式校验、保存并参与版本渲染。 -* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。 -* 规则内如使用多主机名上游做负载均衡,或希望主机名上游渲染为带 keepalive 的 named `upstream`,必须先配置 `OpenRestyResolvers`;否则会回退为 direct `proxy_pass`,或在不安全的多上游场景直接阻止发布。 +* 管理端不再暴露 `resolver` 配置;规则上游统一渲染为 named `upstream` 并启用 keepalive,单上游如带 base path 或 query,会在 `proxy_pass` 中补回原始 URI。 +* 多上游仍要求每个上游都为纯 `scheme://host[:port]`,且同一规则内协议一致,避免在负载均衡模式下引入不可预测的 URI 差异。 * `OpenRestyCacheEnabled` 用于启用缓存基础设施与全局默认参数;实际是否缓存、按 URL / 后缀 / 路径等命中策略由各条 `proxy_routes` 单独决定,不再默认对所有规则开启缓存。 * 默认缓存 Key 为 `$scheme$host$request_uri`,更贴近代理域名维度;如需按其他维度命中,可在性能页显式覆盖。 * 默认 `keepalive_timeout` 为 `20` 秒,默认 `proxy_connect_timeout` 为 `3` 秒,优先兼顾资源占用与回源失败切换速度。 diff --git a/docs/design.md b/docs/design.md index 70cfd06a..d19f2d22 100644 --- a/docs/design.md +++ b/docs/design.md @@ -121,7 +121,7 @@ Origin * 一个域名只对应一条 `proxy_routes` 规则 * `proxy_routes` 至少包含一个上游地址;为兼容历史数据保留 `origin_url` 主上游字段,也允许在同一规则内补充多个上游做负载均衡 -* 单上游或多上游只有在目标可静态解析,或已配置 `OpenRestyResolvers` 时,才会渲染为带 keepalive 的 named `upstream` +* `proxy_routes` 上游统一渲染为带 keepalive 的 named `upstream`;单上游可附带 base path 或 query 并在 `proxy_pass` 中追加,多上游仍限定为纯 `scheme://host[:port]` * `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名 * `proxy_routes.domain` 必须唯一 * 所有上游地址都必须为合法 `http://` 或 `https://` diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md index b7dd1234..96dac923 100644 --- a/docs/development-guidelines.md +++ b/docs/development-guidelines.md @@ -116,7 +116,7 @@ * 不新增平台化对象,除非设计文档明确要求 * `proxy_routes` 维持一条域名对应一条规则;规则内允许保存一个或多个上游地址用于负载均衡,但不引入独立 `origin_pool` -* 生成 named `upstream` 时必须区分可静态解析与依赖运行时 resolver 的主机名;禁止在无 `OpenRestyResolvers` 的情况下为多主机名上游生成会导致 OpenResty 启动失败的配置 +* `proxy_routes` 的上游统一使用 named `upstream` + keepalive;单上游如带 base path 或 query,应在 `proxy_pass` 上补回 URI,多上游仅允许纯 `scheme://host[:port]` * `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象 * `config_versions` 必须保存完整快照与渲染结果 * 全局同时只能有一个激活版本 diff --git a/openflare_server/common/constants.go b/openflare_server/common/constants.go index 1a9fe6f5..2b693c8d 100644 --- a/openflare_server/common/constants.go +++ b/openflare_server/common/constants.go @@ -81,7 +81,6 @@ var OpenRestyProxyBusyBuffersSize = "64k" var OpenRestyGzipEnabled = true var OpenRestyGzipMinLength = 1024 var OpenRestyGzipCompLevel = 5 -var OpenRestyResolvers = "" var OpenRestyCacheEnabled = false var OpenRestyCachePath = "" var OpenRestyCacheLevels = "1:2" @@ -126,7 +125,7 @@ http { gzip {{OpenRestyGzip}}; gzip_min_length {{OpenRestyGzipMinLength}}; gzip_comp_level {{OpenRestyGzipCompLevel}}; -{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}}; +{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}}; } ` diff --git a/openflare_server/controller/option.go b/openflare_server/controller/option.go index 9f1afaca..7ff7d2b0 100644 --- a/openflare_server/controller/option.go +++ b/openflare_server/controller/option.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" - "net" "net/http" "openflare/common" "openflare/model" @@ -115,15 +114,7 @@ func validateOpenRestyOption(key string, value string) error { return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key) } case "OpenRestyResolvers": - if trimmed == "" { - return nil - } - for _, token := range splitOpenRestyResolvers(trimmed) { - if net.ParseIP(token) == nil { - return fmt.Errorf("%s only supports IP resolver entries, invalid value %q", key, token) - } - } - return nil + return fmt.Errorf("%s 已废弃,不再支持配置 resolver", key) case "OpenRestyEventsMultiAcceptEnabled", "OpenRestyWebsocketEnabled", "OpenRestyProxyRequestBufferingEnabled", @@ -187,12 +178,6 @@ func validateOpenRestyOption(key string, value string) error { } } -func splitOpenRestyResolvers(value string) []string { - return strings.FieldsFunc(value, func(r rune) bool { - return r == ',' || r == '\n' || r == '\r' || r == '\t' || r == ' ' - }) -} - // GetOptions godoc // @Summary List editable options // @Tags Options diff --git a/openflare_server/controller/option_test.go b/openflare_server/controller/option_test.go index d69cfdd4..7c3925ff 100644 --- a/openflare_server/controller/option_test.go +++ b/openflare_server/controller/option_test.go @@ -14,6 +14,7 @@ func TestValidateOpenRestyOption(t *testing.T) { {name: "worker processes invalid", key: "OpenRestyWorkerProcesses", value: "0", wantErr: true}, {name: "events use empty", key: "OpenRestyEventsUse", value: ""}, {name: "events use invalid", key: "OpenRestyEventsUse", value: "io_uring", wantErr: true}, + {name: "resolvers deprecated", key: "OpenRestyResolvers", value: "1.1.1.1", wantErr: true}, {name: "proxy buffers valid", key: "OpenRestyProxyBuffers", value: "16 16k"}, {name: "proxy buffers invalid", key: "OpenRestyProxyBuffers", value: "16x16k", wantErr: true}, {name: "cache max size valid", key: "OpenRestyCacheMaxSize", value: "2g"}, @@ -32,9 +33,6 @@ func TestValidateOpenRestyOption(t *testing.T) { {name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true}, {name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"}, {name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true}, - {name: "resolvers empty", key: "OpenRestyResolvers", value: ""}, - {name: "resolvers valid", key: "OpenRestyResolvers", value: "1.1.1.1, 8.8.8.8"}, - {name: "resolvers invalid", key: "OpenRestyResolvers", value: "dns.internal", wantErr: true}, } for _, testCase := range testCases { diff --git a/openflare_server/model/option.go b/openflare_server/model/option.go index 3aa11ae7..2945fcc5 100644 --- a/openflare_server/model/option.go +++ b/openflare_server/model/option.go @@ -80,7 +80,6 @@ func InitOptionMap() { common.OptionMap["OpenRestyGzipEnabled"] = strconv.FormatBool(common.OpenRestyGzipEnabled) common.OptionMap["OpenRestyGzipMinLength"] = strconv.Itoa(common.OpenRestyGzipMinLength) common.OptionMap["OpenRestyGzipCompLevel"] = strconv.Itoa(common.OpenRestyGzipCompLevel) - common.OptionMap["OpenRestyResolvers"] = common.OpenRestyResolvers common.OptionMap["OpenRestyCacheEnabled"] = strconv.FormatBool(common.OpenRestyCacheEnabled) common.OptionMap["OpenRestyCachePath"] = common.OpenRestyCachePath common.OptionMap["OpenRestyCacheLevels"] = common.OpenRestyCacheLevels @@ -132,6 +131,11 @@ func updateOptionMap(key string, value string) { common.OptionMap = make(map[string]string) } common.OptionMap[key] = value + if key == "OpenRestyResolvers" { + delete(common.OptionMap, key) + common.OptionMapRWMutex.Unlock() + return + } if strings.HasSuffix(key, "Permission") { intValue, _ := strconv.Atoi(value) switch key { @@ -299,8 +303,6 @@ func updateOptionMap(key string, value string) { if v, err := strconv.Atoi(value); err == nil && v > 0 { common.OpenRestyGzipCompLevel = v } - case "OpenRestyResolvers": - common.OpenRestyResolvers = strings.TrimSpace(value) case "OpenRestyCacheEnabled": common.OpenRestyCacheEnabled = value == "true" case "OpenRestyCachePath": diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index ba246f39..3480f8f9 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "net" "net/url" "openflare/common" "openflare/model" @@ -83,15 +82,11 @@ type routeCacheConfig struct { type routeUpstreamConfig struct { Name string Scheme string - Servers []routeUpstreamServer + ProxyPassURI string + Servers []string UsesNamedUpstream bool } -type routeUpstreamServer struct { - Address string - Resolve bool -} - type openRestyConfigSnapshot struct { WorkerProcesses string `json:"worker_processes"` WorkerConnections int `json:"worker_connections"` @@ -117,7 +112,6 @@ type openRestyConfigSnapshot struct { GzipEnabled bool `json:"gzip_enabled"` GzipMinLength int `json:"gzip_min_length"` GzipCompLevel int `json:"gzip_comp_level"` - Resolvers string `json:"resolvers,omitempty"` CacheEnabled bool `json:"cache_enabled"` CachePath string `json:"cache_path,omitempty"` CacheLevels string `json:"cache_levels"` @@ -182,7 +176,6 @@ var requiredMainConfigTemplatePlaceholders = []string{ "{{OpenRestyGzip}}", "{{OpenRestyGzipMinLength}}", "{{OpenRestyGzipCompLevel}}", - "{{OpenRestyResolverDirective}}", "{{OpenRestyCacheBlock}}", "{{OpenRestyRouteConfigInclude}}", } @@ -541,7 +534,6 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { GzipEnabled: common.OpenRestyGzipEnabled, GzipMinLength: common.OpenRestyGzipMinLength, GzipCompLevel: common.OpenRestyGzipCompLevel, - Resolvers: common.OpenRestyResolvers, CacheEnabled: common.OpenRestyCacheEnabled, CachePath: common.OpenRestyCachePath, CacheLevels: common.OpenRestyCacheLevels, @@ -603,7 +595,6 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon appendIfChanged("OpenRestyGzipEnabled", fmt.Sprintf("%t", left.GzipEnabled), fmt.Sprintf("%t", right.GzipEnabled)) appendIfChanged("OpenRestyGzipMinLength", fmt.Sprintf("%d", left.GzipMinLength), fmt.Sprintf("%d", right.GzipMinLength)) appendIfChanged("OpenRestyGzipCompLevel", fmt.Sprintf("%d", left.GzipCompLevel), fmt.Sprintf("%d", right.GzipCompLevel)) - appendIfChanged("OpenRestyResolvers", left.Resolvers, right.Resolvers) appendIfChanged("OpenRestyCacheEnabled", fmt.Sprintf("%t", left.CacheEnabled), fmt.Sprintf("%t", right.CacheEnabled)) appendIfChanged("OpenRestyCachePath", left.CachePath, right.CachePath) appendIfChanged("OpenRestyCacheLevels", left.CacheLevels, right.CacheLevels) @@ -650,7 +641,6 @@ func openRestyOptionKeys() []string { "OpenRestyGzipEnabled", "OpenRestyGzipMinLength", "OpenRestyGzipCompLevel", - "OpenRestyResolvers", "OpenRestyCacheEnabled", "OpenRestyCachePath", "OpenRestyCacheLevels", @@ -676,9 +666,6 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) if err != nil { return "", nil, fmt.Errorf("路由 %s 上游配置无效", route.Domain) } - if err := validateRenderableUpstreams(route.Domain, upstreams, cfg); err != nil { - return "", nil, err - } cacheRules, err := decodeStoredCacheRules(route.CacheRules) if err != nil { return "", nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain) @@ -688,7 +675,7 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) Policy: route.CachePolicy, Rules: cacheRules, } - upstreamConfig := buildRouteUpstreamConfig(route, upstreams, cfg) + upstreamConfig := buildRouteUpstreamConfig(route, upstreams) if upstreamConfig.UsesNamedUpstream { builder.WriteString(renderNamedUpstreamBlock(upstreamConfig)) } @@ -770,7 +757,7 @@ func renderMainConfigTemplate(templateText string, cfg openRestyConfigSnapshot) "{{OpenRestyGzip}}", onOff(cfg.GzipEnabled), "{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength), "{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel), - "{{OpenRestyResolverDirective}}", renderResolverDirective(cfg.Resolvers), + "{{OpenRestyResolverDirective}}", "", "{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg), "{{OpenRestyRouteConfigInclude}}", nginxRouteConfigPlaceholder, ) @@ -850,7 +837,7 @@ func nextVersionNumber(now time.Time) (string, error) { } func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { - return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg)) + return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig)) } func renderHTTPRedirectServer(domain string) string { @@ -860,7 +847,7 @@ func renderHTTPRedirectServer(domain string) string { func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(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\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg)) + 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\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig)) } func renderConnectionUpgradeMap() string { @@ -885,12 +872,13 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [ builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n") builder.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") builder.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n") - if common.OpenRestyWebsocketEnabled || upstreamConfig.UsesNamedUpstream { + if common.OpenRestyWebsocketEnabled { builder.WriteString(" proxy_http_version 1.1;\n") builder.WriteString(" proxy_set_header Connection $connection_upgrade;\n") - } - if common.OpenRestyWebsocketEnabled { builder.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") + } else if upstreamConfig.UsesNamedUpstream { + builder.WriteString(" proxy_http_version 1.1;\n") + builder.WriteString(" proxy_set_header Connection \"\";\n") } for _, header := range customHeaders { builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxHeaderValue(header.Value))) @@ -963,40 +951,18 @@ func buildPathExactMatchPattern(rules []string) string { return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|")) } -func renderProxyPassBlock(originURL string, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { +func renderProxyPassBlock(originURL string, upstreamConfig routeUpstreamConfig) string { parsed, err := url.Parse(originURL) if err != nil || parsed.Host == "" || parsed.Scheme == "" { return fmt.Sprintf(" proxy_pass %s;\n", originURL) } if upstreamConfig.UsesNamedUpstream { - return fmt.Sprintf(" proxy_pass %s://%s;\n", upstreamConfig.Scheme, upstreamConfig.Name) + return fmt.Sprintf(" proxy_pass %s://%s%s;\n", upstreamConfig.Scheme, upstreamConfig.Name, upstreamConfig.ProxyPassURI) } - if !shouldUseRuntimeResolver(originURL, cfg.Resolvers) { - return fmt.Sprintf(" proxy_pass %s;\n", originURL) - } - upstreamURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) - basePath := strings.TrimRight(parsed.EscapedPath(), "/") - if basePath == "" || basePath == "." { - basePath = "" - } - if parsed.RawQuery != "" { - if basePath == "" { - basePath = "/" - } - basePath += "?" + parsed.RawQuery - } - var builder strings.Builder - builder.WriteString(fmt.Sprintf(" set $openflare_upstream %s;\n", quoteNginxStringLiteral(upstreamURL))) - if basePath != "" { - builder.WriteString(fmt.Sprintf(" set $openflare_upstream_base_path %s;\n", quoteNginxStringLiteral(basePath))) - builder.WriteString(" proxy_pass $openflare_upstream$openflare_upstream_base_path$request_uri;\n") - return builder.String() - } - builder.WriteString(" proxy_pass $openflare_upstream$request_uri;\n") - return builder.String() + return fmt.Sprintf(" proxy_pass %s;\n", originURL) } -func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg openRestyConfigSnapshot) routeUpstreamConfig { +func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string) routeUpstreamConfig { if len(upstreams) == 0 { return routeUpstreamConfig{} } @@ -1005,27 +971,15 @@ func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg o if err != nil || parsed.Host == "" || parsed.Scheme == "" { return routeUpstreamConfig{} } - if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" { - return routeUpstreamConfig{} - } - if parsed.RawQuery != "" { - return routeUpstreamConfig{} - } - server := routeUpstreamServer{Address: parsed.Host} - if !isIPAddressHostname(parsed.Hostname()) { - if strings.TrimSpace(cfg.Resolvers) == "" { - return routeUpstreamConfig{} - } - server.Resolve = true - } return routeUpstreamConfig{ Name: buildRouteUpstreamName(route), Scheme: parsed.Scheme, - Servers: []routeUpstreamServer{server}, + ProxyPassURI: buildUpstreamProxyPassURI(parsed), + Servers: []string{parsed.Host}, UsesNamedUpstream: true, } } - servers := make([]routeUpstreamServer, 0, len(upstreams)) + servers := make([]string, 0, len(upstreams)) var scheme string for _, upstream := range upstreams { parsed, err := url.Parse(strings.TrimSpace(upstream)) @@ -1043,11 +997,7 @@ func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg o } else if scheme != parsed.Scheme { return routeUpstreamConfig{} } - server := routeUpstreamServer{Address: parsed.Host} - if !isIPAddressHostname(parsed.Hostname()) { - server.Resolve = true - } - servers = append(servers, server) + servers = append(servers, parsed.Host) } return routeUpstreamConfig{ Name: buildRouteUpstreamName(route), @@ -1057,6 +1007,20 @@ func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg o } } +func buildUpstreamProxyPassURI(parsed *url.URL) string { + if parsed == nil { + return "" + } + path := parsed.EscapedPath() + if path == "/" { + path = "" + } + if parsed.RawQuery == "" { + return path + } + return fmt.Sprintf("%s?%s", path, parsed.RawQuery) +} + func buildRouteUpstreamName(route *model.ProxyRoute) string { sanitized := strings.Map(func(r rune) rune { switch { @@ -1081,68 +1045,12 @@ func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string { var builder strings.Builder builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name)) for _, server := range upstreamConfig.Servers { - if server.Resolve { - builder.WriteString(fmt.Sprintf(" server %s resolve max_fails=3 fail_timeout=10s;\n", server.Address)) - continue - } - builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", server.Address)) + builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", server)) } builder.WriteString(" keepalive 128;\n}\n\n") return builder.String() } -func validateRenderableUpstreams(domain string, upstreams []string, cfg openRestyConfigSnapshot) error { - if len(upstreams) <= 1 { - return nil - } - if strings.TrimSpace(cfg.Resolvers) != "" { - return nil - } - for _, upstream := range upstreams { - parsed, err := url.Parse(strings.TrimSpace(upstream)) - if err != nil || parsed.Hostname() == "" { - return fmt.Errorf("路由 %s 上游配置无效", domain) - } - if !isIPAddressHostname(parsed.Hostname()) { - return fmt.Errorf("路由 %s 的多上游主机名需要先配置 OpenRestyResolvers", domain) - } - } - return nil -} - -func isIPAddressHostname(host string) bool { - return net.ParseIP(strings.TrimSpace(host)) != nil -} - -func shouldUseRuntimeResolver(originURL string, resolvers string) bool { - if strings.TrimSpace(resolvers) == "" { - return false - } - return requiresRuntimeResolver(originURL) -} - -func requiresRuntimeResolver(originURL string) bool { - parsed, err := url.Parse(strings.TrimSpace(originURL)) - if err != nil || parsed.Hostname() == "" { - return false - } - return net.ParseIP(parsed.Hostname()) == nil -} - -func renderResolverDirective(value string) string { - resolvers := splitResolverList(value) - if len(resolvers) == 0 { - return "" - } - return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " ")) -} - -func splitResolverList(value string) []string { - return strings.FieldsFunc(strings.TrimSpace(value), func(r rune) bool { - return r == ',' || r == '\n' || r == '\r' || r == '\t' || r == ' ' - }) -} - func resolveUpstreamServerName(originURL string, originHost string) string { parsed, err := url.Parse(originURL) if err != nil || !strings.EqualFold(parsed.Scheme, "https") { diff --git a/openflare_server/service/https_phase1_test.go b/openflare_server/service/https_phase1_test.go index b2184405..1518aafc 100644 --- a/openflare_server/service/https_phase1_test.go +++ b/openflare_server/service/https_phase1_test.go @@ -156,11 +156,17 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $connection_upgrade;") { t.Fatal("expected rendered config to use normalized websocket connection header") } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") { - t.Fatal("expected hostname origin without resolver to keep direct proxy_pass") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_custom_example_com_1 {") { + t.Fatal("expected hostname origin to render named upstream") } - if strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") { - t.Fatal("expected rendered config to avoid runtime-resolved proxy_pass when no resolvers are configured") + if !strings.Contains(result.Version.RenderedConfig, "server origin.internal max_fails=3 fail_timeout=10s;") { + t.Fatal("expected hostname origin to render upstream server entry") + } + if !strings.Contains(result.Version.RenderedConfig, "keepalive 128;") { + t.Fatal("expected named upstream to enable keepalive") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_custom_example_com_1;") { + t.Fatal("expected hostname origin to proxy through named upstream") } } @@ -239,8 +245,8 @@ func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) { if strings.Count(result.Version.RenderedConfig, "proxy_cache openflare_cache;") != 1 { t.Fatal("expected only cache-enabled route to include proxy_cache directive") } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") { - t.Fatal("expected cache-enabled hostname route without resolver to keep direct proxy_pass") + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_static_example_com_1;") { + t.Fatal("expected cache-enabled hostname route to proxy through named upstream") } if !strings.Contains(result.Version.SnapshotJSON, `"cache_enabled":true`) { t.Fatal("expected snapshot to include route cache toggle") @@ -294,7 +300,7 @@ func TestPublishConfigVersionRendersMultipleUpstreams(t *testing.T) { } } -func TestPublishConfigVersionRejectsHostnameLoadBalancingWithoutResolvers(t *testing.T) { +func TestPublishConfigVersionRendersHostnameLoadBalancingUpstream(t *testing.T) { setupServiceTestDB(t) _, err := CreateProxyRoute(ProxyRouteInput{ @@ -307,9 +313,24 @@ func TestPublishConfigVersionRejectsHostnameLoadBalancingWithoutResolvers(t *tes t.Fatalf("CreateProxyRoute failed: %v", err) } - _, err = PublishConfigVersion("root") - if err == nil || !strings.Contains(err.Error(), "多上游主机名需要先配置 OpenRestyResolvers") { - t.Fatalf("expected hostname load balancing publish validation error, got %v", err) + result, err := PublishConfigVersion("root") + if err != nil { + t.Fatalf("PublishConfigVersion failed: %v", err) + } + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_hostname_lb_example_com_1 {") { + t.Fatal("expected hostname load balancing route to define named upstream") + } + if !strings.Contains(result.Version.RenderedConfig, "server c1:39010 max_fails=3 fail_timeout=10s;") { + t.Fatal("expected rendered config to include primary hostname upstream") + } + if !strings.Contains(result.Version.RenderedConfig, "server c2:39010 max_fails=3 fail_timeout=10s;") { + t.Fatal("expected rendered config to include secondary hostname upstream") + } + if strings.Contains(result.Version.RenderedConfig, " resolve ") { + t.Fatal("expected hostname upstreams to avoid resolver-based server parameters") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://backend_hostname_lb_example_com_1;") { + t.Fatal("expected hostname load balancing route to proxy through named upstream") } } @@ -339,23 +360,23 @@ func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, `proxy_ssl_name "git.arctel.net";`) { t.Fatal("expected rendered config to set proxy ssl name from origin host override") } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://git.arctel.net;") { - t.Fatal("expected rendered config to keep direct proxy_pass for hostname origin when resolvers are blank") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_git_arctel_de_1 {") { + t.Fatal("expected hostname origin to render named upstream") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_git_arctel_de_1;") { + t.Fatal("expected rendered config to proxy through named upstream for hostname origin") } if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) { t.Fatal("expected snapshot to include origin_host override") } } -func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) { +func TestPublishConfigVersionUsesNamedUpstreamForOriginBasePath(t *testing.T) { setupServiceTestDB(t) - if err := model.UpdateOption("OpenRestyResolvers", "1.1.1.1, 8.8.8.8"); err != nil { - t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err) - } _, err := CreateProxyRoute(ProxyRouteInput{ Domain: "resolver.example.com", - OriginURL: "https://origin.internal/api", + OriginURL: "https://origin.internal/api/", Enabled: true, }) if err != nil { @@ -366,28 +387,16 @@ func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) { if err != nil { t.Fatalf("PublishConfigVersion failed: %v", err) } - if !strings.Contains(result.Version.MainConfig, "resolver 1.1.1.1 8.8.8.8 valid=30s ipv6=off;") { - t.Fatal("expected main config to render configured resolver directive") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_example_com_1 {") { + t.Fatal("expected hostname origin with base path to still render named upstream") } - if strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_example_com_1 {") { - t.Fatal("expected hostname origin with base path to avoid named upstream block") - } - if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) { - t.Fatal("expected rendered config to use runtime upstream variable when resolvers are configured") - } - if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream_base_path "/api";`) { - t.Fatal("expected rendered config to preserve origin base path for runtime-resolved origin") - } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$openflare_upstream_base_path$request_uri;") { - t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable and base path when resolvers are configured") + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_resolver_example_com_1/api/;") { + t.Fatal("expected rendered config to preserve base path while proxying through named upstream") } } -func TestPublishConfigVersionUsesNamedUpstreamForHostnameOriginsWhenResolversConfigured(t *testing.T) { +func TestPublishConfigVersionUsesNamedUpstreamForHostnameOrigins(t *testing.T) { setupServiceTestDB(t) - if err := model.UpdateOption("OpenRestyResolvers", "1.1.1.1"); err != nil { - t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err) - } _, err := CreateProxyRoute(ProxyRouteInput{ Domain: "resolver-upstream.example.com", @@ -403,17 +412,17 @@ func TestPublishConfigVersionUsesNamedUpstreamForHostnameOriginsWhenResolversCon t.Fatalf("PublishConfigVersion failed: %v", err) } if !strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_upstream_example_com_1 {") { - t.Fatal("expected rendered config to define named upstream for hostname origin when resolvers are configured") + t.Fatal("expected rendered config to define named upstream for hostname origin") } - if !strings.Contains(result.Version.RenderedConfig, "server origin.internal resolve max_fails=3 fail_timeout=10s;") { - t.Fatal("expected rendered config to mark hostname upstream server as resolve") + if !strings.Contains(result.Version.RenderedConfig, "server origin.internal max_fails=3 fail_timeout=10s;") { + t.Fatal("expected rendered config to include hostname upstream server entry") } if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_resolver_upstream_example_com_1;") { - t.Fatal("expected rendered config to proxy through named upstream when resolver-backed hostname upstream is safe") + t.Fatal("expected rendered config to proxy through named upstream for hostname origin") } } -func TestPublishConfigVersionKeepsDirectProxyPassForIPOrigins(t *testing.T) { +func TestPublishConfigVersionUsesNamedUpstreamForIPOrigins(t *testing.T) { setupServiceTestDB(t) _, err := CreateProxyRoute(ProxyRouteInput{ @@ -442,10 +451,6 @@ func TestPublishConfigVersionKeepsDirectProxyPassForIPOrigins(t *testing.T) { func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) { setupServiceTestDB(t) - if err := model.UpdateOption("OpenRestyResolvers", ""); err != nil { - t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err) - } - _, err := CreateProxyRoute(ProxyRouteInput{ Domain: "ws-off.example.com", OriginURL: "https://origin.internal", @@ -462,8 +467,11 @@ func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) { if err != nil { t.Fatalf("PreviewConfigVersion failed: %v", err) } - if strings.Contains(preview.RenderedConfig, "proxy_http_version 1.1;") { - t.Fatal("expected preview config to omit websocket proxy_http_version when disabled") + if !strings.Contains(preview.RenderedConfig, "proxy_http_version 1.1;") { + t.Fatal("expected preview config to keep HTTP/1.1 proxying for named upstream keepalive") + } + if !strings.Contains(preview.RenderedConfig, `proxy_set_header Connection "";`) { + t.Fatal("expected preview config to clear connection header when websocket upgrades are disabled") } if strings.Contains(preview.RenderedConfig, "proxy_set_header Upgrade $http_upgrade;") { t.Fatal("expected preview config to omit websocket upgrade header when disabled") diff --git a/openflare_server/web/features/performance/components/performance-page.tsx b/openflare_server/web/features/performance/components/performance-page.tsx index fbdb6f7f..51626bbc 100644 --- a/openflare_server/web/features/performance/components/performance-page.tsx +++ b/openflare_server/web/features/performance/components/performance-page.tsx @@ -55,7 +55,6 @@ const defaultPerformanceFields = { OpenRestyGzipEnabled: true, OpenRestyGzipMinLength: '1024', OpenRestyGzipCompLevel: '5', - OpenRestyResolvers: '', OpenRestyCacheEnabled: false, OpenRestyCachePath: '', OpenRestyCacheLevels: '1:2', @@ -104,8 +103,6 @@ const performanceFieldTooltips: Record = { gzip_min_length: '只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。', gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。', - resolvers: - '可选填写运行时 DNS 解析器 IP,支持逗号、空格或换行分隔;留空时不额外生成 resolver 指令。', proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。', levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。', inactive: '缓存对象在未命中访问时的失活时间,例如 30m。', @@ -244,7 +241,6 @@ export function PerformancePage() { OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true), OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024', OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5', - OpenRestyResolvers: optionMap.OpenRestyResolvers ?? '', OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false), OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '', OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2', @@ -424,7 +420,6 @@ export function PerformancePage() { 'OpenRestyProxyReadTimeout', performanceFields.OpenRestyProxyReadTimeout.trim(), ], - ['OpenRestyResolvers', performanceFields.OpenRestyResolvers.trim()], [ 'OpenRestyWebsocketEnabled', String(performanceFields.OpenRestyWebsocketEnabled), @@ -943,23 +938,6 @@ export function PerformancePage() { } /> - - - setPerformanceFields((previous) => ({ - ...previous, - OpenRestyResolvers: event.target.value, - })) - } - placeholder="例如:10.0.0.2, 1.1.1.1" - rows={3} - /> -