part 4 缺陷 2:volatile 空根缺 /app 与 /etc 关键文件
RETEST3 缺陷 2:--volatile=yes 下单元根是基本空的 tmpfs, /app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 全无, 执行器单元路径一律 ENOENT、DNS/TLS 不可用。 - build_nspawn_argv:固定只读绑 --bind-ro=/app 与 --bind-ro=/etc/ssl/certs;新增 extra_ro_binds 参数 (单元专属 resolv.conf/hosts 由 run_cell 传进来)。 - prepare_cell_files():按客机有效 DNS 生成单元专属 resolv.conf/hosts(/run/chv/resolv.conf.<machine>、 hosts.<machine>,0644——单元 root 映射为客机 uid 1000, 0600 的 root 文件单元内读不到;DNS/hosts 不含秘密)。 客机 resolv.conf 可能是 systemd-resolved stub (127.0.0.53),单元内无 resolved,直接绑 stub 即 DNS 全灭,故过滤 stub 后备选 /run/systemd/resolve/resolv.conf, 两处都没有即大声失败。nsswitch.conf 故意不绑(客机版 可能含 resolve 模块;glibc 默认 files+dns 正是单元需要的)。 新测试:CellFilesTest、CellPart4ArgvTest(绑定两法)。 run_cell 接线在缺陷 3 的重写里做。
This commit is contained in:
1 parent
fc33eed262
commit
2be69af92c
2 files changed
+170
-2
No files matched your search
+92
-2
@@ -123,9 +123,13 @@ def _playwright_package_dir():
|
|||||||
def build_nspawn_argv(command, network="veth",
|
def build_nspawn_argv(command, network="veth",
|
||||||
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
||||||
broker_dir=BROKER_DIR,
|
broker_dir=BROKER_DIR,
|
||||||
playwright_package_dir=None):
|
playwright_package_dir=None,
|
||||||
|
extra_ro_binds=None):
|
||||||
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
||||||
(list,如 ["python3", "/app/host.py", ...])。"""
|
(list,如 ["python3", "/app/host.py", ...])。
|
||||||
|
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
|
||||||
|
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
|
||||||
|
直接列在下面)。"""
|
||||||
if not command:
|
if not command:
|
||||||
raise ValueError("chv-cell: 单元内命令不能为空")
|
raise ValueError("chv-cell: 单元内命令不能为空")
|
||||||
if network not in ("veth", "shared"):
|
if network not in ("veth", "shared"):
|
||||||
@@ -162,8 +166,25 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
"--bind=%s" % workspace,
|
"--bind=%s" % workspace,
|
||||||
"--bind=%s" % codex_home,
|
"--bind=%s" % codex_home,
|
||||||
"--bind=%s" % broker_dir,
|
"--bind=%s" % broker_dir,
|
||||||
|
# part 4(volatile 空根缺口):--volatile=yes 下单元根是
|
||||||
|
# 基本空的 tmpfs,执行器默认在单元内跑 python3 /app/host.py、
|
||||||
|
# codex 要做 DNS 与 TLS——/app、CA bundle、resolv.conf、
|
||||||
|
# hosts 都须显式绑进去,否则单元内一律 ENOENT/无 DNS。
|
||||||
|
# /app 与 /etc/ssl/certs 是镜像/系统固定路径,只读绑;
|
||||||
|
# resolv.conf/hosts 是 run_cell 按客机有效 DNS 生成的
|
||||||
|
# 单元专属文件,经 extra_ro_binds 传进来(见
|
||||||
|
# prepare_cell_files:客机 /etc/resolv.conf 可能是
|
||||||
|
# systemd-resolved stub(127.0.0.53),单元内无 resolved,
|
||||||
|
# 直接绑 stub 即 DNS 全灭,故须生成真实服务器列表)。
|
||||||
|
# 不绑 /etc/nsswitch.conf:客机版可能含 resolve 模块,
|
||||||
|
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
|
||||||
|
# nsswitch.conf 时默认 files+dns,正是单元需要的。
|
||||||
|
"--bind-ro=/app",
|
||||||
|
"--bind-ro=/etc/ssl/certs",
|
||||||
"--tmpfs=/tmp",
|
"--tmpfs=/tmp",
|
||||||
]
|
]
|
||||||
|
for src, dest in (extra_ro_binds or []):
|
||||||
|
argv.append("--bind-ro=%s:%s" % (src, dest))
|
||||||
if network == "veth":
|
if network == "veth":
|
||||||
argv.append("--network-veth")
|
argv.append("--network-veth")
|
||||||
argv += [
|
argv += [
|
||||||
@@ -317,6 +338,75 @@ def gate_wrapper_command(command, gate_path):
|
|||||||
gate_path] + list(command)
|
gate_path] + list(command)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_nameservers(path):
|
||||||
|
"""读一个 resolv.conf 风格文件,返回 nameserver 列表
|
||||||
|
(去 127.0.0.0/8 与 ::1——单元内无 resolved,stub 指过去
|
||||||
|
即 DNS 全灭)。文件不存在或读不出即 []。"""
|
||||||
|
servers = []
|
||||||
|
try:
|
||||||
|
with open(path) as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("nameserver"):
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
ip = parts[1]
|
||||||
|
if ip.startswith("127.") or ip == "::1":
|
||||||
|
continue
|
||||||
|
if ip not in servers:
|
||||||
|
servers.append(ip)
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return servers
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_nameservers(resolv_conf="/etc/resolv.conf",
|
||||||
|
resolved_conf="/run/systemd/resolve/resolv.conf"):
|
||||||
|
"""客机有效 DNS:先读 /etc/resolv.conf;只剩 stub(127.0.0.53)
|
||||||
|
或为空时读 resolved 的 resolv.conf;仍为空即大声失败。"""
|
||||||
|
servers = _parse_nameservers(resolv_conf)
|
||||||
|
if not servers:
|
||||||
|
servers = _parse_nameservers(resolved_conf)
|
||||||
|
if not servers:
|
||||||
|
raise RuntimeError(
|
||||||
|
"chv-cell: 客机无有效 DNS,配不出单元 resolv.conf")
|
||||||
|
return servers
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_cell_files(machine, run_dir=BROKER_DIR,
|
||||||
|
resolv_conf="/etc/resolv.conf",
|
||||||
|
resolved_conf="/run/systemd/resolve/resolv.conf",
|
||||||
|
hosts_file="/etc/hosts"):
|
||||||
|
"""准备单元专属文件(run_cell 在 Popen 之前调),返回 dict:
|
||||||
|
gate(起步门,run_cell 配网完成后才创建——注意 gate 文件本身
|
||||||
|
在这里不创建,只给路径)、resolv(单元专属 resolv.conf,已写
|
||||||
|
好)、hosts(单元专属 hosts,已写好)。文件都在 /run/chv 下
|
||||||
|
(bind 进单元),0644(单元内 root 在客机侧是 chv,0600 的
|
||||||
|
root 文件在单元内读不到;DNS/hosts 不含秘密,0644 安全)。
|
||||||
|
/app 与 /etc/ssl/certs 是固定只读绑定,不走这里。"""
|
||||||
|
gate = os.path.join(run_dir, ".cell-gate-%s" % machine)
|
||||||
|
resolv = os.path.join(run_dir, "resolv.conf.%s" % machine)
|
||||||
|
hosts = os.path.join(run_dir, "hosts.%s" % machine)
|
||||||
|
servers = _effective_nameservers(resolv_conf, resolved_conf)
|
||||||
|
with open(resolv, "w") as fh:
|
||||||
|
for server in servers:
|
||||||
|
fh.write("nameserver %s\n" % server)
|
||||||
|
os.chmod(resolv, 0o644)
|
||||||
|
try:
|
||||||
|
with open(hosts_file) as src:
|
||||||
|
content = src.read()
|
||||||
|
except OSError:
|
||||||
|
content = ""
|
||||||
|
if "localhost" not in content:
|
||||||
|
content = "127.0.0.1 localhost\n" + content
|
||||||
|
with open(hosts, "w") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
os.chmod(hosts, 0o644)
|
||||||
|
return {"gate": gate, "resolv": resolv, "hosts": hosts}
|
||||||
|
|
||||||
|
|
||||||
def _wait_for_iface(iface, timeout=10.0):
|
def _wait_for_iface(iface, timeout=10.0):
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
|
|||||||
@@ -812,5 +812,83 @@ class CellInitPidPollTest(unittest.TestCase):
|
|||||||
self.assertLess(_time.time() - start, 5.0)
|
self.assertLess(_time.time() - start, 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
class CellPart4ArgvTest(unittest.TestCase):
|
||||||
|
"""part 4:volatile 空根补 bind + 唯一身份进 argv。"""
|
||||||
|
|
||||||
|
def test_app_and_ca_bundle_bound_read_only(self):
|
||||||
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
|
self.assertIn("--bind-ro=/app", argv)
|
||||||
|
self.assertIn("--bind-ro=/etc/ssl/certs", argv)
|
||||||
|
# /app 是只读绑:单元内 python3 /app/host.py 只读,不写。
|
||||||
|
self.assertNotIn("--bind=/app", argv)
|
||||||
|
|
||||||
|
def test_extra_ro_binds(self):
|
||||||
|
argv = CELL.build_nspawn_argv(
|
||||||
|
["true"], extra_ro_binds=[
|
||||||
|
("/run/chv/resolv.conf.chv-cell-00", "/etc/resolv.conf"),
|
||||||
|
("/run/chv/hosts.chv-cell-00", "/etc/hosts")])
|
||||||
|
self.assertIn(
|
||||||
|
"--bind-ro=/run/chv/resolv.conf.chv-cell-00:/etc/resolv.conf",
|
||||||
|
argv)
|
||||||
|
self.assertIn(
|
||||||
|
"--bind-ro=/run/chv/hosts.chv-cell-00:/etc/hosts", argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CellFilesTest(unittest.TestCase):
|
||||||
|
"""part 4:单元专属 resolv.conf/hosts 生成。"""
|
||||||
|
|
||||||
|
def _write(self, path, content):
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_resolv_filters_stub_and_falls_back(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
stub = self._write(os.path.join(tmp, "resolv.conf"),
|
||||||
|
"nameserver 127.0.0.53\n"
|
||||||
|
"options edns0\n")
|
||||||
|
real = self._write(os.path.join(tmp, "resolved.conf"),
|
||||||
|
"nameserver 8.8.8.8\n"
|
||||||
|
"nameserver 1.1.1.1\n")
|
||||||
|
files = CELL.prepare_cell_files(
|
||||||
|
"chv-cell-00", run_dir=tmp, resolv_conf=stub,
|
||||||
|
resolved_conf=real,
|
||||||
|
hosts_file=os.path.join(tmp, "no-hosts"))
|
||||||
|
with open(files["resolv"]) as fh:
|
||||||
|
content = fh.read()
|
||||||
|
self.assertEqual(content,
|
||||||
|
"nameserver 8.8.8.8\nnameserver 1.1.1.1\n")
|
||||||
|
self.assertNotIn("127.0.0.53", content)
|
||||||
|
with open(files["hosts"]) as fh:
|
||||||
|
self.assertIn("127.0.0.1 localhost", fh.read())
|
||||||
|
self.assertEqual(oct(os.stat(files["resolv"]).st_mode
|
||||||
|
& 0o777), "0o644")
|
||||||
|
self.assertEqual(files["gate"],
|
||||||
|
os.path.join(tmp, ".cell-gate-chv-cell-00"))
|
||||||
|
# gate 文件本身不在这里创建(run_cell 配网后才开门)。
|
||||||
|
self.assertFalse(os.path.exists(files["gate"]))
|
||||||
|
|
||||||
|
def test_resolv_real_servers_used_directly(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
resolv = self._write(os.path.join(tmp, "resolv.conf"),
|
||||||
|
"nameserver 8.8.4.4\n")
|
||||||
|
files = CELL.prepare_cell_files(
|
||||||
|
"chv-cell-01", run_dir=tmp, resolv_conf=resolv,
|
||||||
|
resolved_conf=os.path.join(tmp, "missing"),
|
||||||
|
hosts_file=os.path.join(tmp, "missing-hosts"))
|
||||||
|
with open(files["resolv"]) as fh:
|
||||||
|
self.assertEqual(fh.read(), "nameserver 8.8.4.4\n")
|
||||||
|
|
||||||
|
def test_no_dns_anywhere_is_loud(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
CELL.prepare_cell_files(
|
||||||
|
"chv-cell-00", run_dir=tmp,
|
||||||
|
resolv_conf=os.path.join(tmp, "missing"),
|
||||||
|
resolved_conf=os.path.join(tmp, "missing2"),
|
||||||
|
hosts_file=os.path.join(tmp, "missing-hosts"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user