diff --git a/_dev/t-update-inject.py b/_dev/t-update-inject.py index 8c2d623..bcf83e4 100644 --- a/_dev/t-update-inject.py +++ b/_dev/t-update-inject.py @@ -98,6 +98,14 @@ CASES = [ " import drouter_update as U", 'web.py 用 importlib'), + # backupd call() 去掉 sudo 前缀:drouter 身份(更新前备份)写不进 + # /opt/drouter/backups → [Errno 13] Permission denied(2026-10-10 真机修)。 + ('backupd call() 去掉 sudo(drouter 身份写不进备份目录)', + 'backend/drouter-backupd.py', + " ['sudo', '-n', '/usr/bin/python3', HELPER, action,", + " ['/usr/bin/python3', HELPER, action,", + 'backupd call() 用 sudo -n 提权'), + ('/api/update 路由被摘掉', 'backend/drouter-web.py', " if p.startswith('/api/update'):", diff --git a/_dev/t-update.py b/_dev/t-update.py index 34c1060..67b563f 100644 --- a/_dev/t-update.py +++ b/_dev/t-update.py @@ -792,6 +792,15 @@ ck('_do_backup 校验 BK_CREATED(不只看 returncode)', '(returncode==0 在「自动备份未开启」时也会出现,' '只看它会把「什么都没备份」当成「备份成功」)') ck('_do_backup 用 --run-now 调用', "'--run-now'" in uc) +# ⚠️ backupd 的 call() 必须用 sudo -n 提权(2026-10-10 真机修的 bug): +# 「更新前备份」由 drouter-web(drouter 用户)spawn backupd,这条链不带 sudo +# 时 helper 继承 drouter 身份,写 /opt/drouter/backups(root:root)直接 +# [Errno 13] Permission denied,更新被卡死在备份这步。sudoers 已放行 +# drouter 免密跑 helper 白名单,所以 root/drouter 两种身份都通。 +# 判据查**语义**(subprocess 命令数组里真有 sudo 前缀),不能只查「出现 sudo 这个词」。 +_bkd = read(P('backend', 'drouter-backupd.py')) +ck('backupd call() 用 sudo -n 提权(否则 drouter 身份写不进备份目录)', + re.search(r"\['sudo',\s*'-n',\s*'/usr/bin/python3',\s*HELPER", _bkd) is not None) # ④ P0-4:apply 必须有白名单且在写库前 ck('apply_module 有模块白名单', 'APPLY_MODULES' in w) diff --git a/backend/drouter-backupd.py b/backend/drouter-backupd.py index b0c00fb..3018cac 100644 --- a/backend/drouter-backupd.py +++ b/backend/drouter-backupd.py @@ -74,9 +74,21 @@ def emit(line): def call(action, payload=None, timeout=CALL_TIMEOUT): + """调用 helper 的备份能力。 + + ⚠️ 这里用 sudo -n 提权(2026-10-10 修): + helper 写备份包要进 /opt/drouter/backups(root:root drwxr-xr-x), + 而 backupd 有两种运行身份 —— + · 定时器(systemd timer)→ root,本来就能写; + · 「更新前自动备份」→ 由 drouter-web(drouter 用户)spawn, + 这条链不带 sudo 时 helper 也继承 drouter 身份,写 .part 直接 + [Errno 13] Permission denied,更新被卡在备份这步。 + sudoers 已放行 drouter ALL=(root) NOPASSWD: python3 .../drouter-helper.py, + 所以 root 跑 sudo -n 免密、drouter 跑也走白名单提权,两种身份都通。 + """ try: p = subprocess.run( - ['/usr/bin/python3', HELPER, action, + ['sudo', '-n', '/usr/bin/python3', HELPER, action, json.dumps(payload or {}, ensure_ascii=False)], capture_output=True, text=True, timeout=timeout, errors='replace') return json.loads(p.stdout or '{}')