From a343c7a6057a77e1ad2685cd11e89d500a1bef3f Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 21 Jun 2026 10:15:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Agent=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20volume=20=E6=98=A0=E5=B0=84=E6=97=B6=20PoW/WAF=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=85=8D=E7=BD=AE=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 2 +- docs/changelog/index.md | 2 + internal/apps/agent/nginx/manager.go | 62 +++++++++++++++++++++++ internal/apps/agent/nginx/manager_test.go | 25 +++++++++ internal/apps/agent/nginx/pow_assets.go | 7 ++- 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5b91cda8..1863f10b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -105,7 +105,7 @@ services: - ./data/agent/:/data environment: OPENFLARE_SERVER_URL: "http://host.docker.internal:3000" - OPENFLARE_AGENT_TOKEN: "ce5a664631a7c5c54d3406a376847535" + OPENFLARE_AGENT_TOKEN: "6ef051dba8b1b7c7e8a4b5dc1b138593" LOG_LEVEL: "debug" extra_hosts: - "host.docker.internal:host-gateway" diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 362d6013..53d3c49b 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -32,6 +32,8 @@ sidebar: false - 修复 WAF PoW 已启用但挑战页不弹出:`pow_enabled=true` 且 `pow_config` 为空时补齐默认配置写入 `waf_config.json`,OpenResty 按全局+已绑定规则组解析 PoW 注入,Lua 对空配置使用运行时默认值。 +- 修复 Agent 使用 volume 映射时 PoW/WAF 运行时配置无法加载:OpenResty worker(`nobody`)对 `0700` 父目录无法遍历导致 `waf_config.json` 虽为 `0644` 仍不可读;Apply 后强制修正 `data_dir` 至运行时目录链为 `0755`,PoW Lua 在不可读时输出 WARN。 + - 收敛子代理站点标识双轨逻辑:新增 `routeidentity` 统一包,`proxy_route`、`config_version`、`uptimekuma`、`flared` 与 OpenResty 渲染共用 `ResolveSiteName` / `DecodeDomains`;移除废弃 `RenderPoWConfig`;PoW Lua 与 WAF 一致仅依赖 `$openflare_waf_site`。 - 修复全球态势板在仅有 `geo_name`(如 mmdb 的 Germany)而无经纬度时误用美国 fallback 坐标的问题;按国家名/ISO 匹配地图质心。 diff --git a/internal/apps/agent/nginx/manager.go b/internal/apps/agent/nginx/manager.go index fe3e5fc5..559e2b3b 100644 --- a/internal/apps/agent/nginx/manager.go +++ b/internal/apps/agent/nginx/manager.go @@ -275,6 +275,68 @@ func (m *Manager) writeTargetFiles(mainConfig string, routeConfig string, suppor if err := os.WriteFile(m.RouteConfigPath, []byte(renderedRouteConfig), nginxConfigFilePerm); err != nil { return err } + return m.ensureOpenRestyWorkerReadAccess() +} + +// ensureOpenRestyWorkerReadAccess makes runtime config and Lua paths traversable by the +// unprivileged OpenResty worker user (typically nobody). Volume mounts may create parent +// directories as 0700 root-owned; MkdirAll does not fix existing modes. +func (m *Manager) ensureOpenRestyWorkerReadAccess() error { + targets := []string{ + m.RuntimeConfigDir, + m.LuaDir, + filepath.Dir(m.MainConfigPath), + filepath.Dir(m.RouteConfigPath), + } + if m.AccessLogPath != "" { + targets = append(targets, filepath.Dir(m.AccessLogPath)) + } + for _, target := range targets { + if err := ensureWorldTraversablePath(target); err != nil { + return err + } + } + if strings.TrimSpace(m.RuntimeConfigDir) == "" { + return nil + } + entries, err := os.ReadDir(m.RuntimeConfigDir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + path := filepath.Join(m.RuntimeConfigDir, entry.Name()) + if chmodErr := os.Chmod(path, nginxConfigFilePerm); chmodErr != nil && !os.IsNotExist(chmodErr) { + return chmodErr + } + } + return nil +} + +func ensureWorldTraversablePath(targetDir string) error { + const maxDepth = 12 + current := filepath.Clean(strings.TrimSpace(targetDir)) + if current == "" || current == "." { + return nil + } + for depth := 0; depth < maxDepth; depth++ { + if err := os.Chmod(current, nginxDirPerm); err != nil { + if os.IsNotExist(err) || os.IsPermission(err) { + break + } + return fmt.Errorf("chmod %s: %w", current, err) + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } return nil } diff --git a/internal/apps/agent/nginx/manager_test.go b/internal/apps/agent/nginx/manager_test.go index 8b95aa91..f349d0d9 100644 --- a/internal/apps/agent/nginx/manager_test.go +++ b/internal/apps/agent/nginx/manager_test.go @@ -966,3 +966,28 @@ func TestObservabilityListenAddress(t *testing.T) { t.Fatalf("unexpected path observability listen address: %s", got) } } + +func TestEnsureWorldTraversableChainFixesRestrictedParentDirs(t *testing.T) { + tempDir := t.TempDir() + dataDir := filepath.Join(tempDir, "data") + runtimeDir := filepath.Join(dataDir, "etc", "openflare") + if err := os.MkdirAll(runtimeDir, 0o700); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + configPath := filepath.Join(runtimeDir, "waf_config.json") + if err := os.WriteFile(configPath, []byte(`{}`), 0o644); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + + if err := ensureWorldTraversablePath(runtimeDir); err != nil { + t.Fatalf("ensureWorldTraversablePath failed: %v", err) + } + + info, err := os.Stat(filepath.Join(dataDir, "etc")) + if err != nil { + t.Fatalf("Stat failed: %v", err) + } + if info.Mode().Perm()&0o005 == 0 { + t.Fatalf("expected etc directory to be world-traversable, got %o", info.Mode().Perm()) + } +} diff --git a/internal/apps/agent/nginx/pow_assets.go b/internal/apps/agent/nginx/pow_assets.go index 8d0ba2a8..e2eb137c 100644 --- a/internal/apps/agent/nginx/pow_assets.go +++ b/internal/apps/agent/nginx/pow_assets.go @@ -103,9 +103,14 @@ local function load_pow_config() pow_config_dict:set("_domain_keys", table.concat(domain_keys, "\n"), 0) pow_config_dict:set("_config_hash", current_hash, 0) - return + return true end end + + if pow_config_dict:add("_pow_unreadable_config_logged", true, 60) then + ngx.log(ngx.WARN, "openflare pow config is not readable by worker; check directory permissions under ", "__OPENFLARE_RUNTIME_CONFIG_DIR__") + end + return false end load_pow_config()