From 4c4f7f9ced8bce39a0a9241aeaca0adcd8b3dd12 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Mar 2026 11:16:56 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E6=B7=BB=E5=8A=A0OpenRe?= =?UTF-8?q?sty=E8=A7=A3=E6=9E=90=E5=99=A8=E6=8C=87=E4=BB=A4=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=85=8D=E7=BD=AE=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=92=8C=E8=BF=90=E8=A1=8C=E6=97=B6=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_agent/cmd/agent/main.go | 1 + openflare_agent/internal/nginx/manager.go | 87 ++++++++++++++++++- .../internal/nginx/manager_test.go | 30 ++++++- openflare_server/common/constants.go | 2 +- openflare_server/service/config_version.go | 38 +++++++- openflare_server/service/https_phase1_test.go | 12 +++ 6 files changed, 165 insertions(+), 5 deletions(-) diff --git a/openflare_agent/cmd/agent/main.go b/openflare_agent/cmd/agent/main.go index 3ef961ff..7df23c0f 100644 --- a/openflare_agent/cmd/agent/main.go +++ b/openflare_agent/cmd/agent/main.go @@ -73,6 +73,7 @@ func main() { NginxLuaDir: cfg.OpenrestyLuaDir, OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort), OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, + OpenrestyResolverDirective: nginx.ResolverDirective(cfg.OpenrestyPath), Executor: nginx.NewExecutor(nginx.ExecutorOptions{ NginxPath: cfg.OpenrestyPath, DockerBinary: cfg.DockerBinary, diff --git a/openflare_agent/internal/nginx/manager.go b/openflare_agent/internal/nginx/manager.go index c57991fd..b5d1d01a 100644 --- a/openflare_agent/internal/nginx/manager.go +++ b/openflare_agent/internal/nginx/manager.go @@ -24,6 +24,7 @@ const AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__" const LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__" const ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__" const ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" +const ResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__" const DockerMainConfigPath = "/usr/local/openresty/nginx/conf/nginx.conf" const DockerRouteConfigPath = "/etc/nginx/conf.d/openflare_routes.conf" const DockerAccessLogPath = "/etc/nginx/conf.d/openflare_access.log" @@ -171,7 +172,7 @@ func (e *DockerExecutor) CheckHealth(ctx context.Context) error { return fmt.Errorf("docker inspect openresty failed: %w: %s", err, string(output)) } if strings.TrimSpace(string(output)) != "true" { - return errors.New("docker openresty container is not running") + return e.containerNotRunningError(ctx) } return nil } @@ -235,6 +236,46 @@ func (e *DockerExecutor) validateMountSources() error { return nil } +func (e *DockerExecutor) containerNotRunningError(ctx context.Context) error { + inspectSummary := "" + inspectOutput, inspectErr := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "status={{.State.Status}} exit_code={{.State.ExitCode}} error={{printf \"%q\" .State.Error}} oom_killed={{.State.OOMKilled}} finished_at={{.State.FinishedAt}}", e.ContainerName) + if inspectErr == nil { + inspectSummary = strings.TrimSpace(string(inspectOutput)) + } + + logTail := "" + logOutput, logErr := e.Runner.Run(ctx, e.DockerBinary, "logs", "--tail", "50", e.ContainerName) + if logErr == nil { + logTail = strings.TrimSpace(string(logOutput)) + } + + message := "docker openresty container is not running" + if inspectSummary != "" { + message += ": " + inspectSummary + } + if logTail != "" { + message += "; recent logs: " + compactDiagnosticText(logTail) + } + return errors.New(message) +} + +func compactDiagnosticText(text string) string { + trimmed := strings.TrimSpace(text) + if trimmed == "" { + return "" + } + lines := strings.Split(trimmed, "\n") + if len(lines) > 8 { + lines = lines[len(lines)-8:] + } + joined := strings.Join(lines, " | ") + joined = strings.Join(strings.Fields(joined), " ") + if len(joined) > 800 { + return joined[len(joined)-800:] + } + return joined +} + func ensureRegularFile(path string, label string) error { cleanPath := strings.TrimSpace(path) if cleanPath == "" { @@ -281,6 +322,7 @@ type Manager struct { NginxLuaDir string OpenrestyObservabilityListen string OpenrestyObservabilityPort int + OpenrestyResolverDirective string Executor Executor } @@ -406,6 +448,9 @@ func (m *Manager) CurrentChecksum() (string, error) { if m.OpenrestyObservabilityPort > 0 { normalizedMain = strings.ReplaceAll(normalizedMain, fmt.Sprintf("%d", m.OpenrestyObservabilityPort), ObservabilityPortPlaceholder) } + if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" { + normalizedMain = strings.ReplaceAll(normalizedMain, resolverDirective, ResolverDirectivePlaceholder) + } normalizedRoute := string(data) if m.NginxCertDir != "" { normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, CertDirPlaceholder) @@ -807,6 +852,9 @@ func (m *Manager) renderMainConfig(content string) string { if m.OpenrestyObservabilityPort > 0 { rendered = strings.ReplaceAll(rendered, ObservabilityPortPlaceholder, fmt.Sprintf("%d", m.OpenrestyObservabilityPort)) } + if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" { + rendered = strings.ReplaceAll(rendered, ResolverDirectivePlaceholder, resolverDirective) + } return rendered } @@ -820,6 +868,43 @@ func ObservabilityListenAddress(openrestyPath string, port int) string { return fmt.Sprintf("%d", port) } +func ResolverDirective(openrestyPath string) string { + resolvers := resolverAddresses(openrestyPath) + if len(resolvers) == 0 { + return "" + } + return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " ")) +} + +func resolverAddresses(openrestyPath string) []string { + if strings.TrimSpace(openrestyPath) == "" { + return []string{"127.0.0.11"} + } + data, err := os.ReadFile("/etc/resolv.conf") + if err != nil { + return nil + } + lines := strings.Split(string(data), "\n") + resolvers := make([]string, 0, 2) + seen := make(map[string]struct{}) + for _, line := range lines { + fields := strings.Fields(strings.TrimSpace(line)) + if len(fields) < 2 || fields[0] != "nameserver" { + continue + } + addr := strings.TrimSpace(fields[1]) + if addr == "" { + continue + } + if _, ok := seen[addr]; ok { + continue + } + seen[addr] = struct{}{} + resolvers = append(resolvers, addr) + } + return resolvers +} + func (m *Manager) routeConfigIncludePath() string { if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" { return strings.TrimSpace(m.RuntimeRouteConfigPath) diff --git a/openflare_agent/internal/nginx/manager_test.go b/openflare_agent/internal/nginx/manager_test.go index a6d1da3c..8cfe135d 100644 --- a/openflare_agent/internal/nginx/manager_test.go +++ b/openflare_agent/internal/nginx/manager_test.go @@ -113,6 +113,15 @@ func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) { func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) { runner := &fakeRunner{ runFn: func(name string, args ...string) ([]byte, error) { + if len(args) >= 4 && args[0] == "inspect" && args[2] == "{{.State.Running}}" { + return []byte("false"), nil + } + if len(args) >= 4 && args[0] == "inspect" { + return []byte("status=exited exit_code=1 error=\"\" oom_killed=false finished_at=2026-03-18T10:08:30Z"), nil + } + if len(args) >= 1 && args[0] == "logs" { + return []byte("nginx: [emerg] host not found in upstream \"c1\" in /etc/nginx/conf.d/openflare_routes.conf:30"), nil + } return []byte("false"), nil }, } @@ -130,6 +139,14 @@ func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) { } if err := executor.CheckHealth(context.Background()); err == nil { t.Fatal("expected CheckHealth to fail when container is not running") + } else { + text := err.Error() + if !strings.Contains(text, "exit_code=1") { + t.Fatalf("expected exit code in health error, got %v", err) + } + if !strings.Contains(text, "host not found in upstream") { + t.Fatalf("expected recent docker logs in health error, got %v", err) + } } } @@ -610,10 +627,11 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { LuaDir: filepath.Join(tempDir, "lua"), NginxLuaDir: "/etc/nginx/openflare-lua", OpenrestyObservabilityListen: "18081", + OpenrestyResolverDirective: " resolver 127.0.0.11 valid=30s ipv6=off;\n resolver_timeout 5s;\n", Executor: &fakeExecutor{}, } - err := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\nserver { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;", []protocol.SupportFile{ + err := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\n__OPENFLARE_RESOLVER_DIRECTIVE__server { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;", []protocol.SupportFile{ {Path: "1.crt", Content: "cert-data"}, {Path: "1.key", Content: "key-data"}, }) @@ -635,6 +653,9 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if !strings.Contains(string(mainData), "listen 18081;") { t.Fatalf("expected observability listen placeholder replacement in main config, got %s", string(mainData)) } + if !strings.Contains(string(mainData), "resolver 127.0.0.11 valid=30s ipv6=off;") { + t.Fatalf("expected resolver directive placeholder replacement in main config, got %s", string(mainData)) + } certData, err := os.ReadFile(filepath.Join(manager.CertDir, "1.crt")) if err != nil { t.Fatalf("failed to read cert file: %v", err) @@ -651,6 +672,13 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { } } +func TestResolverDirectiveForDockerMode(t *testing.T) { + got := ResolverDirective("") + if !strings.Contains(got, "resolver 127.0.0.11") { + t.Fatalf("expected docker resolver directive, got %q", got) + } +} + func TestCertFileMode(t *testing.T) { testCases := []struct { path string diff --git a/openflare_server/common/constants.go b/openflare_server/common/constants.go index 81a71ab0..e6ab7782 100644 --- a/openflare_server/common/constants.go +++ b/openflare_server/common/constants.go @@ -125,7 +125,7 @@ http { gzip {{OpenRestyGzip}}; gzip_min_length {{OpenRestyGzipMinLength}}; gzip_comp_level {{OpenRestyGzipCompLevel}}; -{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}}; +{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}}; } ` diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index e917a992..50823fcc 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -124,6 +124,7 @@ const ( nginxLuaDirPlaceholder = "__OPENFLARE_LUA_DIR__" nginxObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__" nginxObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" + nginxResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__" ) var requiredMainConfigTemplatePlaceholders = []string{ @@ -151,6 +152,7 @@ var requiredMainConfigTemplatePlaceholders = []string{ "{{OpenRestyGzip}}", "{{OpenRestyGzipMinLength}}", "{{OpenRestyGzipCompLevel}}", + "{{OpenRestyResolverDirective}}", "{{OpenRestyCacheBlock}}", "{{OpenRestyRouteConfigInclude}}", } @@ -671,6 +673,7 @@ func renderMainConfigTemplate(templateText string, cfg openRestyConfigSnapshot) "{{OpenRestyGzip}}", onOff(cfg.GzipEnabled), "{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength), "{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel), + "{{OpenRestyResolverDirective}}", nginxResolverDirectivePlaceholder, "{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg), "{{OpenRestyRouteConfigInclude}}", nginxRouteConfigPlaceholder, ) @@ -750,7 +753,7 @@ func nextVersionNumber(now time.Time) (string, error) { } func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput) string { - return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), originURL) + return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL)) } func renderHTTPRedirectServer(domain string) string { @@ -760,7 +763,7 @@ func renderHTTPRedirectServer(domain string) string { func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) 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 server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), originURL) + return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL)) } func renderExactHostGuard(domain string) string { @@ -795,6 +798,33 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [ return builder.String() } +func renderProxyPassBlock(originURL string) string { + parsed, err := url.Parse(originURL) + if err != nil || parsed.Host == "" || parsed.Scheme == "" { + 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() +} + func resolveUpstreamServerName(originURL string, originHost string) string { parsed, err := url.Parse(originURL) if err != nil || !strings.EqualFold(parsed.Scheme, "https") { @@ -811,6 +841,10 @@ func resolveUpstreamServerName(originURL string, originHost string) string { } func quoteNginxHeaderValue(value string) string { + return quoteNginxStringLiteral(value) +} + +func quoteNginxStringLiteral(value string) string { escaped := strings.ReplaceAll(value, `\`, `\\`) escaped = strings.ReplaceAll(escaped, `"`, `\"`) return fmt.Sprintf(`"%s"`, escaped) diff --git a/openflare_server/service/https_phase1_test.go b/openflare_server/service/https_phase1_test.go index 6e4f9528..c945df17 100644 --- a/openflare_server/service/https_phase1_test.go +++ b/openflare_server/service/https_phase1_test.go @@ -63,6 +63,9 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) { if !strings.Contains(result.Version.MainConfig, "listen __OPENFLARE_OBSERVABILITY_LISTEN__;") { t.Fatal("expected main config to include managed openresty observability listen placeholder") } + if !strings.Contains(result.Version.MainConfig, "__OPENFLARE_RESOLVER_DIRECTIVE__") { + t.Fatal("expected main config to include managed resolver directive placeholder") + } if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") { t.Fatal("expected main config to avoid hard-coded allow rules on observability server") } @@ -138,6 +141,12 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $http_connection;") { t.Fatal("expected rendered config to forward websocket connection header") } + if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) { + t.Fatal("expected rendered config to defer upstream resolution via variable proxy_pass") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") { + t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable") + } } func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) { @@ -166,6 +175,9 @@ 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, `set $openflare_upstream "https://git.arctel.net";`) { + t.Fatal("expected rendered config to avoid resolving https upstream during config load") + } if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) { t.Fatal("expected snapshot to include origin_host override") }