From af635e8e986417f84d43a9d39c1ff4b3f180684f Mon Sep 17 00:00:00 2001 From: NingWei Date: Mon, 22 Jun 2026 13:53:14 +0800 Subject: [PATCH] =?UTF-8?q?Agent=E6=94=AF=E6=8C=81IP=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E8=BF=99=E6=A0=B7=E6=89=8D=E8=83=BD=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=B1=80=E5=9F=9F=E7=BD=91=E4=B8=AD=E7=9A=84=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=9C=8D=E5=8A=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- docker-images/all-in-one/README.md | 12 +- .../all-in-one/assets/scripts/startup.sh | 32 +++ .../all-in-one/docs/startup-steps.md | 4 +- docker-images/all-in-one/sample/env.json | 3 +- docker-images/kernal/README.md | 6 +- .../kernal/assets/scripts/startup.sh | 209 +++++++++++++++++- docker-images/kernal/docs/startup-steps.md | 4 +- docker-images/kernal/sample/env.json | 3 +- .../kernal-tests/test/startup-config.test.ts | 19 ++ 10 files changed, 287 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ffd1021..a941592 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ sudo sh ./get-docker.sh docker run -d --restart unless-stopped --name aios-all-in-one --cap-add NET_ADMIN -p 3030:3030 -p 1883:1883 -p 9000:9000 -v /var/aios_root/:/var/aios-all-in-one crpi-4auaoyyj6r36p6lb.cn-hangzhou.personal.cr.aliyuncs.com/huozige_lab/aios-all-x64:[版本号] ``` -其中的 `[版本号]` 请参考本项目的Release,如 `20260618.04`。默认的启动参数可适配大多数场景,如需定制,请 [点击查看详细说明](/docker-images/all-in-one/README.md)。 +其中的 `[版本号]` 请参考本项目的Release,如 `20260622.01`。默认的启动参数可适配大多数场景,如需定制,请 [点击查看详细说明](/docker-images/all-in-one/README.md)。如需将数据文件存储到其他路径,请在执行前将 `/var/aios_root/` 替换为该路径。 ### 3、配置 AIOS @@ -44,7 +44,7 @@ AIOS 支持为不同的数字员工配置不同的大语言模型。您可以在 创建服务商时需要提供的信息: -- 服务商ID:服务商的标识,您可以自行拟定,推荐采用简短的英文单词,如 `qwen` +- 服务商ID:服务商的标识,您可以自行拟定,推荐采用简短的英文单词,如 `glm` - 基础地址:服务商文档中提供的 `OpenAI兼容 Chat Completion API` 地址,通常以 `v1` 结尾,如 `https://dashscope.aliyuncs.com/compatible-mode/v1` 或 `https://api.siliconflow.cn/v1` - 访问密钥:服务商提供的访问密钥,通常需要在服务商的 `API密钥` 或 `API Key` 等页面中创建 @@ -56,6 +56,12 @@ AIOS 支持为不同的数字员工配置不同的大语言模型。您可以在 - 上下文长度:从服务商提供的文档中获取,最新旗舰模型通常为1M,即 `1000000` - 输入上限:从服务商提供的文档中获取,最新旗舰模型通常为384K,即 `384000`,部分模型(如 `qwen-plus`)为32K,即 `32000` +AIOS 的安全设置较为严格,默认不允许 AI 访问局域网的资源。所以,如果大模型服务位于局域网内,需要手动修改第2步中指定的数据文件路径下的 `env.json` 文件中 `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` 的值,将其加入白名单。如大语言服务的地址为 `api.gcscn.gccode.cn` ,该服务器位于内网,且IP为 `172.16.11.12` ,此处需要修改为: + +```json + "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "172.16.11.12" +``` + 模型创建完成后,推荐点击 `测试连接`,确保大模型配置和参数正确。 > 提示:大模型配置需要 AIOS 重新加载内核才能生效,您可以在 AIOS 管理控制台中点击【系统设置】菜单,找到 `重启服务` 按钮,手动触发重新加载,或等待创建数字员工时,一并重启。 diff --git a/docker-images/all-in-one/README.md b/docker-images/all-in-one/README.md index 428e823..8969396 100644 --- a/docker-images/all-in-one/README.md +++ b/docker-images/all-in-one/README.md @@ -108,7 +108,9 @@ docker run -d --restart unless-stopped --name aios-all-in-one --cap-add NET_ADMI ## Agent 网络边界 -kernel 启动脚本会按 `aios-agent` 的 UID 安装 netfilter 规则,默认阻断它访问私有网段、链路本地地址和 loopback。启动时只从 `AIOS_MQTT_CHANNEL_BROKER` 和 `AIOS_S3_HOST` 解析内网 IP 白名单:如果解析结果是内网地址且不是 `localhost`/`127.0.0.1`,只放行该 IP 的 MQTT/S3 TCP 端口;公网地址不进白名单,也不需要白名单。all-in-one 内置 MQTT/MinIO 使用 `127.0.0.1` 时,只按本机服务端口单独放行,不作为局域网 IP 白名单。规则只匹配 `aios-agent`,不影响 `aios-svc`、内置 MQTT、MinIO、management web 或 invoke proxy。 +kernel 启动脚本会按 `aios-agent` 的 UID 安装 netfilter 规则,默认阻断它访问私有网段、链路本地地址和 loopback。启动时会从 `AIOS_MQTT_CHANNEL_BROKER` 和 `AIOS_S3_HOST` 解析内网 IP 白名单:如果解析结果是内网地址且不是 `localhost`/`127.0.0.1`,只放行该 IP 的 MQTT/S3 TCP 端口;也会读取 `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` 中显式配置的 IP 或 CIDR,并对 `aios-agent` 放行这些目标的所有端口。公网地址不进默认白名单,也不需要白名单。all-in-one 内置 MQTT/MinIO 使用 `127.0.0.1` 时,只按本机服务端口单独放行,不作为局域网 IP 白名单。规则只匹配 `aios-agent`,不影响 `aios-svc`、内置 MQTT、MinIO、management web 或 invoke proxy。 + +`AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` 可写在 `/var/aios-all-in-one/env.json` 中;如果缺少这个节点,启动脚本会自动补成空字符串。all-in-one 会透传到 kernel 运行配置。支持逗号、空格、换行或 JSON 数组分隔的 IPv4/IPv6 地址和 CIDR,例如 `"192.168.1.10,10.20.0.0/16,[fd00::10]"`。兼容别名 `AIOS_AGENT_INTERNAL_NETWORK_WHITELIST`。 这个边界要求使用 Docker 默认 bridge 或自定义 bridge 网络,并配置 `--cap-add NET_ADMIN`。不建议使用 `--network host`;host 网络会扩大网络命名空间边界,不适合作为 agent 隔离部署方式。 @@ -181,6 +183,14 @@ all-in-one 启动脚本会长期守护内置服务。`mqtt`、`minio`、kernel s } ``` +如果需要让 agent 访问指定内网地址,可以追加: + +```json +{ + "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "192.168.1.10,10.20.0.0/16" +} +``` + 大语言模型由管理控制台维护。启动后登录控制台,在“大语言模型”菜单中先创建 Provider(`baseUrl + SK`),再创建归属该 Provider 的 Model;模板可以设置默认 Provider/Model,创建数字员工时会自动带出,也可以在创建或编辑数字员工时手动选择 Provider 和 Model。启动脚本不会再通过 `AIOS_MODEL_*` 注入 OpenClaw 模型配置。 内置组件约定: diff --git a/docker-images/all-in-one/assets/scripts/startup.sh b/docker-images/all-in-one/assets/scripts/startup.sh index c3b7585..ca56a35 100644 --- a/docker-images/all-in-one/assets/scripts/startup.sh +++ b/docker-images/all-in-one/assets/scripts/startup.sh @@ -215,10 +215,24 @@ EOF startup_step_error "invalid runtime config JSON: ${USER_CONFIG_JSON} must be a JSON object" return 1 fi + ensure_user_env_defaults ensure_service_file "${USER_ENV_JSON}" 0640 ensure_service_file "${USER_CONFIG_JSON}" 0644 } +ensure_user_env_defaults() { + local temp_env + + if jq -e 'has("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST")' "${USER_ENV_JSON}" >/dev/null; then + return + fi + + temp_env="${USER_ENV_JSON}.tmp.$$" + jq '. + { "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "" }' "${USER_ENV_JSON}" > "${temp_env}" + mv "${temp_env}" "${USER_ENV_JSON}" + ensure_service_file "${USER_ENV_JSON}" 0640 +} + ensure_credentials() { node - "${CREDENTIALS_JSON}" <<'NODE' const crypto = require("node:crypto"); @@ -512,6 +526,22 @@ function value(name, fallback = "") { return current; } +function valueWithDefault(name, fallback = "") { + const current = userEnv[name]; + if (current === undefined || current === null) { + return fallback; + } + return current; +} + +function copyOptionalUserEnv(name) { + const current = userEnv[name]; + if (current === undefined || current === null || current === "") { + return {}; + } + return { [name]: current }; +} + const prefix = "aios"; const mqtt = { AIOS_MQTT_CHANNEL_BROKER: "mqtt://127.0.0.1:1883", @@ -541,6 +571,8 @@ const s3 = { const shared = { ...mqtt, ...s3, + AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST: valueWithDefault("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST", ""), + ...copyOptionalUserEnv("AIOS_AGENT_INTERNAL_NETWORK_WHITELIST"), AIOS_MANAGEMENT_WEBSITE_TIMEOUT: String(value("AIOS_MANAGEMENT_WEBSITE_TIMEOUT", "180")) }; diff --git a/docker-images/all-in-one/docs/startup-steps.md b/docker-images/all-in-one/docs/startup-steps.md index ca85251..0206eb7 100644 --- a/docker-images/all-in-one/docs/startup-steps.md +++ b/docker-images/all-in-one/docs/startup-steps.md @@ -34,10 +34,10 @@ | 序号 | stepName | 具体工作内容 | | --- | --- | --- | -| 1/12 | `require_input_files` | 检查 `/var/aios-all-in-one/env.json` 和 `config.json`;缺失时播种 sample 文件并退出,存在时校验为 JSON object 并规范权限。 | +| 1/12 | `require_input_files` | 检查 `/var/aios-all-in-one/env.json` 和 `config.json`;缺失时播种 sample 文件并退出,存在时校验为 JSON object、补齐默认 env 节点并规范权限。 | | 2/12 | `ensure_directories` | 创建 all-in-one、kernel、apps、generated、logs、run 和 MinIO 数据目录,并规范服务用户权限。 | | 3/12 | `ensure_credentials` | 生成或复用 MQTT/MinIO 随机密码,写入 `credentials.json`,并以北京时间写入 `generatedAt`。 | -| 4/12 | `generate_runtime_files` | 合并用户环境、凭据和配置,生成 kernel/apps 运行所需的 `env.json` 和 `config.json`。 | +| 4/12 | `generate_runtime_files` | 合并用户环境、凭据和配置,生成 kernel/apps 运行所需的 `env.json` 和 `config.json`,并把 agent 内网白名单透传给 kernel。 | | 5/12 | `sync_optional_user_files` | 同步可选的 agent workspace 模板和自定义启动脚本到 kernel 运行目录。 | | 6/12 | `start_mqtt` | 启动内置 MQTT broker,写入 `mqtt-{yyyyMMdd}.log`,并等待 `1883` 端口可用。 | | 7/12 | `start_minio` | 启动内置 MinIO,写入 `minio-{yyyyMMdd}.log`,并等待健康检查通过。 | diff --git a/docker-images/all-in-one/sample/env.json b/docker-images/all-in-one/sample/env.json index 224e818..17097fd 100644 --- a/docker-images/all-in-one/sample/env.json +++ b/docker-images/all-in-one/sample/env.json @@ -1,3 +1,4 @@ { - "AIOS_MANAGEMENT_CONSOLE_PORT": 3030 + "AIOS_MANAGEMENT_CONSOLE_PORT": 3030, + "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "" } diff --git a/docker-images/kernal/README.md b/docker-images/kernal/README.md index c3c3b42..775a2b5 100644 --- a/docker-images/kernal/README.md +++ b/docker-images/kernal/README.md @@ -148,7 +148,9 @@ aios-data/ `/opt/aios-seed` 是镜像内置 seed 目录,最终权限主要为 `aios-svc:aios`:目录 `0750`,可执行文件 `0750`,普通文件 `0640`。因此 `aios-agent` 只能通过共享组只读访问 seed 内容,`aios-svc` 可以读写。OpenClaw 2026.6.5 会拒绝加载非当前用户或 root 拥有的非 bundled 插件,所以 seed 中的 `aios-mqtt-channel` npm project 额外固定为 `root:aios` 只读。镜像默认直接使用其中的 seed npm 插件和内置 uv 工具,不会把这些大目录复制到 `/var/aios`。 -启动脚本还会按 `aios-agent` 的 UID 安装 netfilter 规则,默认阻断它访问私有网段和链路本地地址。启动时只从 `AIOS_MQTT_CHANNEL_BROKER` 和 `AIOS_S3_HOST` 解析内网 IP 白名单:如果解析结果是内网地址,只放行该 IP 的 MQTT/S3 TCP 端口;公网地址不进白名单,也不需要白名单。本机 `localhost`/`127.0.0.1`、Docker 标记为本机的地址,以及 `/etc/resolv.conf` 中 nameserver 的 TCP/UDP 53 会放行,保证本机访问和 DNS 解析可用。规则只匹配 `aios-agent`,不影响 `aios-svc`。 +启动脚本还会按 `aios-agent` 的 UID 安装 netfilter 规则,默认阻断它访问私有网段和链路本地地址。启动时会从 `AIOS_MQTT_CHANNEL_BROKER` 和 `AIOS_S3_HOST` 解析内网 IP 白名单:如果解析结果是内网地址,只放行该 IP 的 MQTT/S3 TCP 端口;也会读取 `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` 中显式配置的 IP 或 CIDR,并对 `aios-agent` 放行这些目标的所有端口。公网地址不进默认白名单,也不需要白名单。本机 `localhost`/`127.0.0.1`、Docker 标记为本机的地址,以及 `/etc/resolv.conf` 中 nameserver 的 TCP/UDP 53 会放行,保证本机访问和 DNS 解析可用。规则只匹配 `aios-agent`,不影响 `aios-svc`。 + +`AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` 可写在 `/var/aios/env.json` 中;如果缺少这个节点,启动脚本会自动补成空字符串。它支持逗号、空格、换行或 JSON 数组分隔的 IPv4/IPv6 地址和 CIDR,例如 `"192.168.1.10,10.20.0.0/16,[fd00::10]"`。兼容别名 `AIOS_AGENT_INTERNAL_NETWORK_WHITELIST`。 这个网络边界要求使用 Docker 默认 bridge 或自定义 bridge 网络,并在 `docker run` 中添加 `--cap-add NET_ADMIN`。如果没有这个能力,容器会拒绝启动,避免 agent 在未受控状态下运行。不建议使用 `--network host`。 @@ -266,6 +268,8 @@ aios-data/ - `AIOS_S3_FORCE_PATH_STYLE` - `AIOS_S3_AGENT_INBOX_BUCKET` - `AIOS_S3_AGENT_OUTBOX_BUCKET` +- `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` +- `AIOS_AGENT_INTERNAL_NETWORK_WHITELIST` 注意: diff --git a/docker-images/kernal/assets/scripts/startup.sh b/docker-images/kernal/assets/scripts/startup.sh index d457762..f39ba5f 100644 --- a/docker-images/kernal/assets/scripts/startup.sh +++ b/docker-images/kernal/assets/scripts/startup.sh @@ -571,6 +571,173 @@ async function resolveEndpoint(endpoint) { NODE } +collect_agent_internal_network_allowlist_cidrs() { + node <<'NODE' +const net = require("node:net"); + +function env(name) { + return (process.env[name] || "").trim(); +} + +function splitRaw(value) { + const raw = String(value || "").trim(); + if (!raw) return []; + + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return parsed.flatMap((item) => splitRaw(item)); + } + } catch { + // Continue with delimited string parsing. + } + + return raw.split(/[\s,;]+/).filter(Boolean); +} + +function stripOptionalQuotes(value) { + return String(value || "").trim().replace(/^['"]|['"]$/g, ""); +} + +function stripIpv6Brackets(value) { + const trimmed = stripOptionalQuotes(value); + if (trimmed.startsWith("[") && trimmed.endsWith("]") && net.isIP(trimmed.slice(1, -1))) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +function parsePrefix(rawPrefix, maxPrefix, source) { + const value = String(rawPrefix || "").trim(); + if (!/^\d+$/.test(value)) { + throw new Error(`invalid CIDR prefix in ${source}`); + } + const prefix = Number(value); + if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) { + throw new Error(`CIDR prefix out of range in ${source}`); + } + return prefix; +} + +function ipv4ToBigInt(address) { + const parts = address.split(".").map((part) => Number(part)); + return parts.reduce((value, part) => (value << 8n) + BigInt(part), 0n); +} + +function bigIntToIpv4(value) { + const parts = []; + for (let shift = 24; shift >= 0; shift -= 8) { + parts.push(Number((value >> BigInt(shift)) & 255n)); + } + return parts.join("."); +} + +function normalizeIpv4Cidr(address, prefix) { + const value = ipv4ToBigInt(address); + const mask = prefix === 0 ? 0n : ((1n << BigInt(prefix)) - 1n) << BigInt(32 - prefix); + return `${bigIntToIpv4(value & mask)}/${prefix}`; +} + +function expandIpv4Embedded(address) { + if (!address.includes(".")) return address; + const lastColon = address.lastIndexOf(":"); + if (lastColon < 0) return address; + const ipv4 = address.slice(lastColon + 1); + if (net.isIP(ipv4) !== 4) return address; + const parts = ipv4.split(".").map((part) => Number(part)); + const high = ((parts[0] << 8) | parts[1]).toString(16); + const low = ((parts[2] << 8) | parts[3]).toString(16); + return `${address.slice(0, lastColon)}:${high}:${low}`; +} + +function ipv6ToBigInt(address) { + const zoneIndex = address.indexOf("%"); + const withoutZone = zoneIndex >= 0 ? address.slice(0, zoneIndex) : address; + const normalized = expandIpv4Embedded(withoutZone.toLowerCase()); + const doubleColonParts = normalized.split("::"); + if (doubleColonParts.length > 2) { + throw new Error(`invalid IPv6 address: ${address}`); + } + + const left = doubleColonParts[0] ? doubleColonParts[0].split(":") : []; + const right = doubleColonParts.length === 2 && doubleColonParts[1] ? doubleColonParts[1].split(":") : []; + const missing = doubleColonParts.length === 2 ? 8 - left.length - right.length : 0; + if (missing < 0 || (doubleColonParts.length === 1 && left.length !== 8)) { + throw new Error(`invalid IPv6 address: ${address}`); + } + + const hextets = [...left, ...Array(missing).fill("0"), ...right]; + if (hextets.length !== 8) { + throw new Error(`invalid IPv6 address: ${address}`); + } + + return hextets.reduce((value, hextet) => { + if (!/^[0-9a-f]{1,4}$/i.test(hextet)) { + throw new Error(`invalid IPv6 address: ${address}`); + } + return (value << 16n) + BigInt(parseInt(hextet, 16)); + }, 0n); +} + +function bigIntToIpv6(value) { + const hextets = []; + for (let shift = 112; shift >= 0; shift -= 16) { + hextets.push(Number((value >> BigInt(shift)) & 0xffffn).toString(16)); + } + return hextets.join(":"); +} + +function normalizeIpv6Cidr(address, prefix) { + const value = ipv6ToBigInt(address); + const mask = prefix === 0 ? 0n : ((1n << BigInt(prefix)) - 1n) << BigInt(128 - prefix); + return `${bigIntToIpv6(value & mask)}/${prefix}`; +} + +function normalizeAllowlistEntry(rawEntry) { + const source = stripOptionalQuotes(rawEntry); + if (!source) return undefined; + + const slashIndex = source.lastIndexOf("/"); + const rawAddress = slashIndex >= 0 ? source.slice(0, slashIndex) : source; + const rawPrefix = slashIndex >= 0 ? source.slice(slashIndex + 1) : ""; + const address = stripIpv6Brackets(rawAddress); + const family = net.isIP(address); + if (!family) { + throw new Error(`invalid IP address in allowlist: ${source}`); + } + + const maxPrefix = family === 4 ? 32 : 128; + const prefix = rawPrefix ? parsePrefix(rawPrefix, maxPrefix, source) : maxPrefix; + const cidr = family === 4 ? normalizeIpv4Cidr(address, prefix) : normalizeIpv6Cidr(address, prefix); + return { family: family === 6 ? "ipv6" : "ipv4", cidr }; +} + +function readAllowlistEntries() { + const rawValues = [ + env("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST"), + env("AIOS_AGENT_INTERNAL_NETWORK_WHITELIST"), + ]; + return rawValues.flatMap((value) => splitRaw(value)); +} + +try { + const seen = new Set(); + for (const entry of readAllowlistEntries()) { + const normalized = normalizeAllowlistEntry(entry); + if (!normalized) continue; + const key = `${normalized.family}\t${normalized.cidr}`; + if (!seen.has(key)) { + seen.add(key); + console.log(`${key}\tagent-allowlist`); + } + } +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} +NODE +} + add_agent_dns_resolver_rules() { local command_name="$1" local chain_name="$2" @@ -612,8 +779,10 @@ add_agent_netfilter_rules() { local chain_name="$2" local cidr_array_name="$3" local allowed_endpoints="$4" - local endpoint_family="$5" + local allowed_cidrs="$5" + local endpoint_family="$6" local cidr endpoint_address endpoint_port endpoint_scope endpoint_name endpoint_cidr + local allow_family allow_cidr allow_name local -n cidrs="$cidr_array_name" "$command_name" -w -A "$chain_name" \ @@ -634,6 +803,15 @@ add_agent_netfilter_rules() { add_agent_dns_resolver_rules "$command_name" "$chain_name" "$endpoint_family" + while IFS=$'\t' read -r allow_family allow_cidr allow_name; do + if [[ -z "${allow_family:-}" || "$allow_family" != "$endpoint_family" || -z "${allow_cidr:-}" ]]; then + continue + fi + + "$command_name" -w -A "$chain_name" -d "$allow_cidr" -j RETURN \ + || fail_agent_internal_network_blocker "${command_name} cannot add ${allow_name:-agent-allowlist} allow rule for ${allow_cidr}" + done <<< "$allowed_cidrs" + while IFS=$'\t' read -r endpoint_family_line endpoint_address endpoint_port endpoint_scope endpoint_name; do if [[ -z "${endpoint_family_line:-}" || "$endpoint_family_line" != "$endpoint_family" ]]; then continue @@ -673,30 +851,37 @@ configure_agent_netfilter_family() { local agent_uid="$3" local cidr_array_name="$4" local allowed_endpoints="$5" - local endpoint_family="$6" + local allowed_cidrs="$6" + local endpoint_family="$7" require_netfilter_command "$command_name" reset_netfilter_chain "$command_name" "$chain_name" - add_agent_netfilter_rules "$command_name" "$chain_name" "$cidr_array_name" "$allowed_endpoints" "$endpoint_family" + add_agent_netfilter_rules "$command_name" "$chain_name" "$cidr_array_name" "$allowed_endpoints" "$allowed_cidrs" "$endpoint_family" replace_agent_netfilter_jump "$command_name" "$agent_uid" "$chain_name" } enforce_agent_internal_network_block() { local agent_uid local allowed_endpoints + local allowed_cidrs agent_uid="$(id -u "$AIOS_OPENCLAW_USER" 2>/dev/null)" \ || fail_agent_internal_network_blocker "user ${AIOS_OPENCLAW_USER} does not exist" allowed_endpoints="$(collect_agent_allowed_internal_tcp_endpoints)" + allowed_cidrs="$(collect_agent_internal_network_allowlist_cidrs)" \ + || fail_agent_internal_network_blocker "invalid AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST" - configure_agent_netfilter_family iptables AIOS_AGENT_INTERNAL_BLOCK "$agent_uid" AGENT_INTERNAL_IPV4_CIDRS "$allowed_endpoints" ipv4 - configure_agent_netfilter_family ip6tables AIOS_AGENT_INTERNAL_BLOCK6 "$agent_uid" AGENT_INTERNAL_IPV6_CIDRS "$allowed_endpoints" ipv6 + configure_agent_netfilter_family iptables AIOS_AGENT_INTERNAL_BLOCK "$agent_uid" AGENT_INTERNAL_IPV4_CIDRS "$allowed_endpoints" "$allowed_cidrs" ipv4 + configure_agent_netfilter_family ip6tables AIOS_AGENT_INTERNAL_BLOCK6 "$agent_uid" AGENT_INTERNAL_IPV6_CIDRS "$allowed_endpoints" "$allowed_cidrs" ipv6 if [[ -n "$allowed_endpoints" ]]; then log "agent internal network block installed for ${AIOS_OPENCLAW_USER} uid ${agent_uid}; allowed endpoints: $(printf '%s' "$allowed_endpoints" | tr '\n' ';')" else log "agent internal network block installed for ${AIOS_OPENCLAW_USER} uid ${agent_uid}; no internal MQTT/S3 endpoints detected" fi + if [[ -n "$allowed_cidrs" ]]; then + log "allowed networks: $(printf '%s' "$allowed_cidrs" | tr '\n' ';')" + fi } sync_missing_tree() { @@ -1621,10 +1806,24 @@ require_runtime_input_files() { startup_step_error "invalid runtime env JSON: ${RUNTIME_ENV_JSON} must be a JSON object" return 1 fi + ensure_runtime_env_defaults ensure_service_file_mode "$RUNTIME_CONFIG_JSON" 0644 ensure_service_file_mode "$RUNTIME_ENV_JSON" 0640 } +ensure_runtime_env_defaults() { + local temp_env + + if jq -e 'has("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST")' "$RUNTIME_ENV_JSON" >/dev/null; then + return + fi + + temp_env="${RUNTIME_ENV_JSON}.tmp.$$" + jq '. + { "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "" }' "$RUNTIME_ENV_JSON" > "$temp_env" + mv "$temp_env" "$RUNTIME_ENV_JSON" + ensure_service_file_mode "$RUNTIME_ENV_JSON" 0640 +} + load_runtime_env_file() { local entry key value diff --git a/docker-images/kernal/docs/startup-steps.md b/docker-images/kernal/docs/startup-steps.md index ca944a4..04e5a14 100644 --- a/docker-images/kernal/docs/startup-steps.md +++ b/docker-images/kernal/docs/startup-steps.md @@ -31,7 +31,7 @@ | 序号 | stepName | 具体工作内容 | | --- | --- | --- | -| 1/30 | `require_runtime_input_files` | 校验 `/var/aios/config.json` 和 `/var/aios/env.json` 存在且都是 JSON object,并规范文件权限。 | +| 1/30 | `require_runtime_input_files` | 校验 `/var/aios/config.json` 和 `/var/aios/env.json` 存在且都是 JSON object,补齐默认 env 节点,并规范文件权限。 | | 2/30 | `load_runtime_env_file` | 从 `env.json` 读取运行时环境变量,校验变量名,拒绝覆盖保留变量,并导出给后续步骤和子进程。 | | 3/30 | `export_runtime_environment` | 导出 AIOS、OpenClaw、QMD、npm、uv、pip、XDG、浏览器路径、`AIOS_LOG_DIR` 和 `PATH` 等运行环境。 | | 4/30 | `ensure_directories` | 创建并规范服务目录、OpenClaw 运行目录、workspace、ontology、tmp、统一 logs 目录、包缓存和 XDG 目录的权限。 | @@ -57,7 +57,7 @@ | 24/30 | `patch_openclaw_config` | 基于运行时配置修补 `/var/aios/.openclaw/openclaw.json`,设置默认 workspace、gateway 鉴权、QMD memory 和 MQTT 插件。 | | 25/30 | `refresh_openclaw_plugin_registry` | 根据 OpenClaw 版本、插件配置和内置内容指纹判断是否刷新插件 registry,并写入启动指纹。 | | 26/30 | `ensure_python_venv` | 创建或修复持久化 Python shim、pip shim、site-packages 和 pip cache。 | -| 27/30 | `enforce_agent_internal_network_block` | 为 `aios-agent` 安装 netfilter 规则,阻断未白名单的内网地址和端口,并放行必要的 MQTT/S3/DNS 端点。 | +| 27/30 | `enforce_agent_internal_network_block` | 为 `aios-agent` 安装 netfilter 规则,阻断未白名单的内网地址和端口,并放行显式配置的内网 IP/CIDR 以及必要的 MQTT/S3/DNS 端点。 | | 28/30 | `run_runtime_startup_hook` | 如果 `/var/aios/run_after_startup.sh` 存在,规范换行并以服务用户身份执行。 | | 29/30 | `start_management_cli` | 启动 `aios-management-serivce` supervisor,写入统一日志目录下的 pid 文件,并将输出镜像到 Docker stdout。 | | 30/30 | `start_openclaw` | 停止遗留 gateway,首次启动 `openclaw gateway run --force`,写入统一日志目录下的 gateway pid 文件,并将输出写入 `openclaw-{yyyyMMdd}.log` 且镜像到 Docker stdout。 | diff --git a/docker-images/kernal/sample/env.json b/docker-images/kernal/sample/env.json index 84c8fcb..416065f 100644 --- a/docker-images/kernal/sample/env.json +++ b/docker-images/kernal/sample/env.json @@ -17,5 +17,6 @@ "AIOS_S3_ADMIN_INBOX_BUCKET": "admin-in", "AIOS_S3_ADMIN_OUTBOX_BUCKET": "admin-out", "AIOS_S3_AGENT_INBOX_BUCKET": "agents-in", - "AIOS_S3_AGENT_OUTBOX_BUCKET": "agents-out" + "AIOS_S3_AGENT_OUTBOX_BUCKET": "agents-out", + "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "" } diff --git a/docker-images/test/kernal-tests/test/startup-config.test.ts b/docker-images/test/kernal-tests/test/startup-config.test.ts index 1b758b0..7058da3 100644 --- a/docker-images/test/kernal-tests/test/startup-config.test.ts +++ b/docker-images/test/kernal-tests/test/startup-config.test.ts @@ -236,6 +236,25 @@ describe("docker image startup configuration", () => { expect(allInOneStartupScript).toContain('profile_step "wait_for_apps" wait_for_apps'); }); + it("supports explicit agent internal network allowlist entries", () => { + const { allInOneStartupScript, kernalStartupScript } = readStartupScripts(); + + expect(kernalStartupScript).toContain("ensure_runtime_env_defaults()"); + expect(kernalStartupScript).toContain('"AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": ""'); + expect(kernalStartupScript).toContain("collect_agent_internal_network_allowlist_cidrs()"); + expect(kernalStartupScript).toContain('env("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST")'); + expect(kernalStartupScript).toContain('env("AIOS_AGENT_INTERNAL_NETWORK_WHITELIST")'); + expect(kernalStartupScript).toContain('normalizeAllowlistEntry(entry)'); + expect(kernalStartupScript).toContain('-d "$allow_cidr" -j RETURN'); + expect(kernalStartupScript).toContain('allowed_cidrs="$(collect_agent_internal_network_allowlist_cidrs)"'); + expect(kernalStartupScript).toContain('"invalid AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST"'); + expect(kernalStartupScript).toContain('"allowed networks: $(printf \'%s\' "$allowed_cidrs" | tr \'\\n\' \';\')"'); + + expect(allInOneStartupScript).toContain("ensure_user_env_defaults()"); + expect(allInOneStartupScript).toContain('AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST: valueWithDefault("AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST", "")'); + expect(allInOneStartupScript).toContain('copyOptionalUserEnv("AIOS_AGENT_INTERNAL_NETWORK_WHITELIST")'); + }); + it("passes MQTT channel chat log directory into kernal runtime", () => { const { allInOneStartupScript, kernalStartupScript } = readStartupScripts(); const { allInOneDockerfile, kernalDockerfile } = readDockerfiles(); -- Gitee