From a3c83eceb12bc66263f23bd6f91941a149149e4f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 28 Oct 2026 05:33:24 +0000 Subject: [PATCH 1/2] Pin public addresses of allowed names on microsandbox restore --- contracts/agents-api/environments.md | 7 +-- contracts/agents-api/index.md | 2 +- contracts/agents-api/zh/environments.md | 9 ++- contracts/agents-api/zh/index.md | 4 +- docs/sandbox-provider.md | 2 +- docs/zh/sandbox-provider.md | 4 +- .../tools/microsandbox-provider/README.md | 2 +- .../tools/microsandbox-provider/backend.go | 9 +-- .../tools/microsandbox-provider/bootstrap.go | 6 +- .../tools/microsandbox-provider/network.go | 60 +++++++++++++++---- .../microsandbox-provider/network_test.go | 43 ++++++++++++- .../tools/microsandbox-provider/snapshot.go | 2 +- 12 files changed, 111 insertions(+), 39 deletions(-) diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index f7edd247c..548f7f2d2 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -306,14 +306,13 @@ Core may start a hosted Environment from the result of an earlier preparation in A hosted Session's effective `network` narrows the deployment's [network cap](./sandbox-deployment.md#network), and the policy holds for the sandbox's whole life, including package installation and setup commands: - `enabled`: the sandbox reaches what the cap allows. -- `disabled`: the sandbox reaches only Core and the Session's model provider. The model provider stays reachable on its `base_url` port at public addresses; a private model provider host is reachable only when the cap lists its exact name in the extra domains. Package installation and setup commands that need the network fail as ordinary setup errors. +- `disabled`: the sandbox reaches only Core, the Session's model provider and the host's DNS server on port 53. The model provider stays reachable on its `base_url` port at public addresses; a private model provider host is reachable only when the cap lists its exact name in the extra domains. Package installation and setup commands that need the network fail as ordinary setup errors. - `restricted`: the sandbox reaches the listed domains, Core and the model provider (on the same terms as `disabled`). A domain is allowed on public addresses when the cap allows the public internet, and on its resolved addresses when it is one of the cap's extra domains. Any other domain is rejected at Session creation with `sandbox_network_outside_cap`, whose `param` is that `allowed_domains` entry. If the cap shrinks later, the sandbox attempt fails with the same error. Limits ([adapter rules](../../docs/sandbox-provider.md#network-egress)): -- Under `disabled`, the host's DNS server stays reachable on port 53. -- A private model provider host that the cap lists is reachable on all its ports. -- Reserved ranges such as 198.18.0.0/15 and the documentation ranges are never reachable, not even through a listed name or the model provider. +- A model provider host that the cap lists is reachable like a listed domain, not only on its port. +- On microsandbox, once a `disabled` or `restricted` sandbox has resumed, a domain allowed on public addresses, the model provider included, is reachable over HTTPS only at the addresses its name had at resume; other sites on those addresses are reachable too. - On E2B, a restricted domain is reachable only on ports 80 and 443, and a model provider on another port is reachable on every port of its public addresses. `disabled` and `restricted` return `sandbox_network_unsupported` when the deployment's sandbox Provider cannot restrict egress. A `self_hosted` Environment accepts only `enabled`, because Core cannot enforce a policy on a machine it does not run. diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index a77a417f7..a1e9105ad 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -160,7 +160,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Hosted workspace admission is limited by the [tenant capacity policy](../../docs/configuration.md#tenant-capacity) and the [input reservation deadline](./environments.md#reservations). - MiniMax Code does not declare `retained_native_history`, so it cannot run on microsandbox, which keeps every Environment on workspace storage; Core rejects the Session ([Harness capabilities](./harness-capabilities.md)). -- The model provider's port stays reachable at public addresses from the sandbox because the Harness runs there, and a private model provider host only when the network cap lists its name; MCP connections from the Environment follow the deployment's [network cap](./sandbox-deployment.md#network). E2B sandboxes cannot reach private networks, and `169.254.169.254` answers inside an E2B sandbox under every cap. +- The model provider's port stays reachable at public addresses from the sandbox because the Harness runs there, and a private model provider host only when the network cap lists its name; under `disabled` the host's DNS server stays reachable on port 53; MCP connections from the Environment follow the deployment's [network cap](./sandbox-deployment.md#network). E2B sandboxes cannot reach private networks, and `169.254.169.254` answers inside an E2B sandbox under every cap. - `disabled` and `restricted` networking run on hosted Environments whose sandbox Provider can restrict egress, within the deployment's network cap; `self_hosted` Environments accept only `enabled` ([network policy](./environments.md#restricted-network)). The official API accepts an `allowed_domains` entry with a leading dot, such as `.example.com`, with unspecified meaning; Core rejects it. - `packages.system` other than null or an empty list is rejected; system packages must be preinstalled. - `Environment.files` lists the files recorded at Session creation; files created later through the Environment files API do not appear. diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index 4ae1bf06f..b6bdfc6aa 100644 --- a/contracts/agents-api/zh/environments.md +++ b/contracts/agents-api/zh/environments.md @@ -1,7 +1,7 @@ --- title: "Environment 与模板" source: contracts/agents-api/environments.md -source_hash: cfbfc9f651036fb7e4e528717631e1f1baa30c19115bf8f5c8cacc3ce26a4417 +source_hash: 108a9ece9f826d222eecec0de0843b4e5aeeff09dd98eacc864e88d62af46439 --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -308,14 +308,13 @@ Core 可以用同一 Project 中较早一次准备的结果启动托管 Environm 托管 Session 的有效 `network` 在部署的[网络上限](sandbox-deployment.md#network)内收窄,并且该策略在沙箱的整个生命周期内生效,包括包安装和设置命令: - `enabled`:沙箱可访问上限允许的范围。 -- `disabled`:沙箱只能访问 Core 和 Session 的模型提供商。模型提供商仅在公开地址上的 `base_url` 端口可达;私有的模型提供商主机只有在上限的额外域名列出其精确名称时才可达。需要网络的包安装和设置命令按普通设置错误失败。 +- `disabled`:沙箱只能访问 Core、Session 的模型提供商和主机 53 端口上的 DNS 服务器。模型提供商仅在公开地址上的 `base_url` 端口可达;私有的模型提供商主机只有在上限的额外域名列出其精确名称时才可达。需要网络的包安装和设置命令按普通设置错误失败。 - `restricted`:沙箱可访问所列域名、Core 和模型提供商(条件同 `disabled`)。上限允许公网时,域名在公开地址上被允许;域名属于上限的额外域名时,在其解析出的地址上被允许。其他域名在创建 Session 时被拒绝,错误码为 `sandbox_network_outside_cap`,`param` 指向该 `allowed_domains` 条目。上限之后缩小时,沙箱尝试以同一错误失败。 限制([适配器规则](../../../docs/zh/sandbox-provider.md#network-egress)): -- `disabled` 下,主机的 DNS 服务器仍可通过 53 端口访问。 -- 上限列出的私有模型提供商主机,所有端口均可达。 -- 198.18.0.0/15、文档保留地址段等保留范围始终不可达,经所列域名或模型提供商也不行。 +- 上限列出的模型提供商主机与所列域名一样可达,不限于其端口。 +- 在 microsandbox 上,`disabled` 或 `restricted` 沙箱恢复后,在公网地址上放行的域名(包括模型提供商)只能经其名称在恢复时的地址通过 HTTPS 访问;同一地址上的其他站点也可访问。 - 在 E2B 上,受限域名只能通过 80 和 443 端口访问;模型提供商使用其他端口时,其公网地址的所有端口均可达。 部署的沙箱 Provider 无法限制出站时,`disabled` 和 `restricted` 返回 `sandbox_network_unsupported`。`self_hosted` Environment 只接受 `enabled`,因为 Core 无法在不由它运行的机器上强制策略。 diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index b9443c477..16d3715d9 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: cf0ef0f7d2bf076ed3b402b0c8c98f55d53c584c14b2f45714c17cb34cd14877 +source_hash: 0cf1499e96991dcf395556149664fee7659d17db35c8cee0853e399d6c57d807 --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -163,7 +163,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - Hosted 工作区准入受[租户容量策略](../../../docs/zh/configuration.md#tenant-capacity)及既有[输入预留截止时间](environments.md#reservations)限制。 - MiniMax Code 未声明 `retained_native_history`,因此无法在 microsandbox 上运行(其每个 Environment 都使用工作区存储);Core 会拒绝该 Session([Harness 能力](./harness-capabilities.md))。 -- 模型提供商在公开地址上的端口从沙箱内始终可达,因为 Harness 运行在那里,私有的模型提供商主机只有在网络上限列出其名称时才可达;Environment 中的 MCP 连接遵循部署的[网络上限](sandbox-deployment.md#network)。E2B 沙箱无法访问私有网络,且在任何上限下 `169.254.169.254` 都会在 E2B 沙箱内应答。 +- 模型提供商在公开地址上的端口从沙箱内始终可达,因为 Harness 运行在那里,私有的模型提供商主机只有在网络上限列出其名称时才可达;`disabled` 下主机 53 端口上的 DNS 服务器仍可达;Environment 中的 MCP 连接遵循部署的[网络上限](sandbox-deployment.md#network)。E2B 沙箱无法访问私有网络,且在任何上限下 `169.254.169.254` 都会在 E2B 沙箱内应答。 - `disabled` 和 `restricted` 网络在沙箱 Provider 能限制出站的托管 Environment 上运行,并受部署网络上限约束;`self_hosted` Environment 只接受 `enabled`。官方 API 接受带前导点的 `allowed_domains` 条目(如 `.example.com`),含义未规定,Core 拒绝它([restricted network policy](environments.md#restricted-network))。 - 除 null 或空列表以外的 `packages.system` 都会被拒绝;系统软件包必须预先安装。 - `Environment.files` 列出 Session 创建时记录的文件;之后通过 Environment files API 创建的文件不会出现。 diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 863d36893..d00c8aca2 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -204,7 +204,7 @@ Core sends the same `Egress` in `Bootstrap` and in every `ResumeRequest`, derive The Provider enforces exactly this on top of the always-allowed connection to Core, and blocks loopback, link-local, cloud-metadata and every private address that `Egress` does not name, except that E2B cannot block the metadata address 169.254.169.254. It resolves names with its host's DNS when it creates and when it resumes, and cannot follow later changes of an answer. A name that does not resolve gets no rule. MCP traffic has no entry: it follows `Public`, `PrivateDomains` and `Domains`. -microsandbox applies the rules with first-match order, default deny, and host DNS reachable; it allows the resolved private addresses without a port so that DNS rebind protection passes them. E2B declares `Restrict` only: `Public` adds a deny list for loopback, private and link-local ranges, and not `Public` denies all IPv4 and IPv6 traffic except the Core host, `Endpoints` and `Domains`. E2B matches a host name only on ports 80 and 443, so the adapter also allows the global IPv4 addresses of the Core host and of each Endpoint on any other port. An IP literal is allowed only as a global address range of one address, and a private literal gets no rule. +microsandbox applies the rules with first-match order, default deny, and host DNS reachable; it allows the resolved private addresses without a port so that DNS rebind protection passes them. Its restore cannot turn strict hostname enforcement off, under which a domain allow passes no HTTPS, so a restored sandbox that is not `Public` also allows the public addresses each name of `Domains`, `PrivateDomains` and `Endpoints` resolves to at restore, on the same ports. E2B declares `Restrict` only: `Public` adds a deny list for loopback, private and link-local ranges, and not `Public` denies all IPv4 and IPv6 traffic except the Core host, `Endpoints` and `Domains`. E2B matches a host name only on ports 80 and 443, so the adapter also allows the global IPv4 addresses of the Core host and of each Endpoint on any other port. An IP literal is allowed only as a global address range of one address, and a private literal gets no rule. ## Checkpoint lifecycle diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 055aec30b..c3340a55c 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "沙箱提供商协议" source: docs/sandbox-provider.md -source_hash: 12713a187e000d21ad8a466d986d71297183347a772848104ac0d365b328fbbc +source_hash: 624d3a362c2696bc95096284e4ffbe399e97b54979b87062d7de6187a045ead3 --- 沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。microsandbox 和 E2B 实现了该协议。 @@ -206,7 +206,7 @@ Core 在 `Bootstrap` 和每个 `ResumeRequest` 中发送同一个 `Egress`,它 Provider 在始终允许到 Core 的连接之上严格执行这些内容,并阻止回环、链路本地、云元数据以及 `Egress` 未列出的所有私有地址,但 E2B 无法阻止元数据地址 169.254.169.254。它在创建和恢复时用主机 DNS 解析名称,无法跟随之后应答的变化。无法解析的名称没有规则。MCP 流量没有条目:它遵循 `Public`、`PrivateDomains` 和 `Domains`。 -microsandbox 按首条匹配的顺序、默认拒绝并保持主机 DNS 可达来应用规则;它不带端口地允许解析出的私有地址,使 DNS rebind 保护放行它们。E2B 只声明 `Restrict`:`Public` 时加入回环、私有和链路本地网段的拒绝列表,非 `Public` 时拒绝除 Core 主机、`Endpoints` 和 `Domains` 之外的全部 IPv4 和 IPv6 流量。E2B 只在 80 和 443 端口匹配主机名,因此 adapter 还会为 Core 主机和每个 Endpoint 在其他端口上允许其全球 IPv4 地址。IP 字面量只作为单地址的全球地址范围被允许,私有字面量没有规则。 +microsandbox 按首条匹配的顺序、默认拒绝并保持主机 DNS 可达来应用规则;它不带端口地允许解析出的私有地址,使 DNS rebind 保护放行它们。它的恢复无法关闭严格主机名校验,而该校验下域名放行规则不放行任何 HTTPS,因此非 `Public` 的沙箱恢复后,还会在相同端口上允许 `Domains`、`PrivateDomains` 和 `Endpoints` 中每个名称在恢复时解析出的公网地址。E2B 只声明 `Restrict`:`Public` 时加入回环、私有和链路本地网段的拒绝列表,非 `Public` 时拒绝除 Core 主机、`Endpoints` 和 `Domains` 之外的全部 IPv4 和 IPv6 流量。E2B 只在 80 和 443 端口匹配主机名,因此 adapter 还会为 Core 主机和每个 Endpoint 在其他端口上允许其全球 IPv4 地址。IP 字面量只作为单地址的全球地址范围被允许,私有字面量没有规则。 ## 检查点生命周期 {#checkpoint-lifecycle} diff --git a/services/core/tools/microsandbox-provider/README.md b/services/core/tools/microsandbox-provider/README.md index 20004d89c..7fc82862c 100644 --- a/services/core/tools/microsandbox-provider/README.md +++ b/services/core/tools/microsandbox-provider/README.md @@ -20,7 +20,7 @@ The checkpoint root is a separate adapter-owned directory, mode 0700, with a pri ## Create and bootstrap -Create names the VM from a hash of the installation and allocation reference plus the compute generation; a name never serves another incarnation. It creates the VM with the saved CPUs and memory as both initial and maximum, a managed root disk of `root_disk_mib`, the explicitly resolved workspace directory mounted at `/environment`, user 1000:1000, working directory `/` and the node's network policy ([`bootstrap.go`](bootstrap.go)). The resource checks run before bootstrap. Creation disables the SDK's strict hostname policy enforcement, so domain allows match by the guest's DNS answer and the TLS server name without TLS interception; restore accepts only the policy rules and cannot set it. +Create names the VM from a hash of the installation and allocation reference plus the compute generation; a name never serves another incarnation. It creates the VM with the saved CPUs and memory as both initial and maximum, a managed root disk of `root_disk_mib`, the explicitly resolved workspace directory mounted at `/environment`, user 1000:1000, working directory `/` and the node's network policy ([`bootstrap.go`](bootstrap.go)). The resource checks run before bootstrap. Creation disables the SDK's strict hostname policy enforcement, so domain allows match by the guest's DNS answer and the TLS server name without TLS interception. Restore accepts only the policy and cannot disable it, so a restored sandbox that is not public also allows the public addresses each allowed name resolves to at restore, ahead of its domain allow ([`network.go`](network.go)). Workspace, staging and outputs share the `/environment` filesystem, which keeps the Runtime's cross-device and link checks intact; the layered root filesystem can report different device IDs for a directory and its upper-layer files, so it holds no workspace data. The filesystem provider owns the directory; sandbox removal never deletes it. The binding and resource rules belong to [Workspace filesystem protocol](../../../../docs/workspace-provider.md#attachment). The [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) owns the Runtime directory layout. diff --git a/services/core/tools/microsandbox-provider/backend.go b/services/core/tools/microsandbox-provider/backend.go index a13dcfb92..5d34afe42 100644 --- a/services/core/tools/microsandbox-provider/backend.go +++ b/services/core/tools/microsandbox-provider/backend.go @@ -114,10 +114,11 @@ func (b backend) kill(ctx context.Context, c wire.Compute) error { return e } -// network builds the allocation's network policy from its Egress with the -// node host's DNS. Names that do not resolve are reported to the node's log. -func (b backend) network(ctx context.Context, egress sandbox.Egress) (*sdk.NetworkConfig, error) { - return egressNetwork(ctx, b.q.Config.CoreURL, egress, hostResolver, os.Stderr) +// network builds the allocation's network policy for create or restore from +// its Egress with the node host's DNS. Names that do not resolve are reported +// to the node's log. +func (b backend) network(ctx context.Context, egress sandbox.Egress, restore bool) (*sdk.NetworkConfig, error) { + return egressNetwork(ctx, b.q.Config.CoreURL, egress, restore, hostResolver, os.Stderr) } func (b backend) resumeCompute(ctx context.Context, c wire.Compute) (wire.State, error) { diff --git a/services/core/tools/microsandbox-provider/bootstrap.go b/services/core/tools/microsandbox-provider/bootstrap.go index a5f9e27aa..ed4b28700 100644 --- a/services/core/tools/microsandbox-provider/bootstrap.go +++ b/services/core/tools/microsandbox-provider/bootstrap.go @@ -52,14 +52,10 @@ func (b backend) create(ctx context.Context) (wire.Response, error) { for key, value := range workspaceLabels(b.q.Workspace) { labels[key] = value } - network, e := b.network(ctx, bootstrap.Egress) + network, e := b.network(ctx, bootstrap.Egress, false) if e != nil { return wire.Response{}, e } - // Strict hostname enforcement accepts a domain allow over TLS only with TLS - // interception, so the domain allows of a not-public egress would never pass - // HTTPS. Restore cannot set it (the SDK takes only the policy there). - network.DisableStrict = true live, e := sdk.CreateSandbox(ctx, c.Name, // Every compatible node holds the pinned image; nothing is ever pulled. sdk.WithImage(b.q.Config.Image), sdk.WithPullPolicy(sdk.PullPolicyNever), sdk.WithMemory(b.q.Config.MemoryMiB), sdk.WithCPUs(b.q.Config.CPUs), diff --git a/services/core/tools/microsandbox-provider/network.go b/services/core/tools/microsandbox-provider/network.go index 5c8c05e63..bcf68d023 100644 --- a/services/core/tools/microsandbox-provider/network.go +++ b/services/core/tools/microsandbox-provider/network.go @@ -31,7 +31,8 @@ func isPrivate(a netip.Addr) bool { return a.IsPrivate() || carrierGradeNAT.Cont const resolveTimeout = 5 * time.Second // egressNetwork builds one allocation's network policy; Create and Resume call -// it with the same Egress. Rules are first match with default deny: +// it with the same Egress, and restore tells which. Rules are first match with +// default deny: // 1. host DNS // 2. deny loopback, link-local and metadata // 3. each address of the Core host, on its port @@ -43,13 +44,21 @@ const resolveTimeout = 5 * time.Second // 6. unless Public, a tcp allow on its port for each Endpoint (domain, or a // public IP literal), and a port-less domain allow for each PrivateDomains // and Domains name; only public answers reach a domain allow, since rule 5 -// and rebind protection already refuse private ones +// and rebind protection already refuse private ones; on restore each +// domain allow is preceded by the same allow for the public addresses the +// name resolves to now // 7. public addresses, when Public // -// Core and PrivateDomains names are resolved together under one deadline. A -// PrivateDomains name that does not resolve gets no address rule and is +// Strict hostname enforcement accepts a domain allow over TLS only with TLS +// interception, so create turns it off. Restore cannot (the SDK takes only the +// policy there) and the domain allows of rule 6 no longer pass HTTPS, hence the +// address allows, which miss later DNS changes. They go once restore can turn +// strict enforcement off. +// +// Core, PrivateDomains and, on restore, the rule 6 names are resolved together +// under one deadline. A name that does not resolve gets no address rule and is // reported to report. -func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, resolve resolver, report io.Writer) (*sdk.NetworkConfig, error) { +func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, restore bool, resolve resolver, report io.Writer) (*sdk.NetworkConfig, error) { core, err := url.Parse(coreURL) if err != nil || core.Hostname() == "" { return nil, sandbox.ErrInvalid @@ -64,6 +73,13 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r names := slices.Concat(egress.PrivateDomains, egress.Domains) slices.Sort(names) names = slices.Compact(names) + pin := restore && !egress.Public + lookups := slices.Clone(egress.PrivateDomains) + if pin { + lookups = slices.Concat(names, endpointDomains(egress.Endpoints)) + } + slices.Sort(lookups) + lookups = slices.Compact(lookups) ctx, cancel := context.WithTimeout(ctx, resolveTimeout) defer cancel() @@ -71,9 +87,9 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r var coreAddresses []netip.Addr var coreErr error wg.Go(func() { coreAddresses, coreErr = addresses(ctx, core.Hostname(), resolve) }) - found := make([][]netip.Addr, len(egress.PrivateDomains)) - failed := make([]error, len(egress.PrivateDomains)) - for i, name := range egress.PrivateDomains { + found := make([][]netip.Addr, len(lookups)) + failed := make([]error, len(lookups)) + for i, name := range lookups { wg.Go(func() { found[i], failed[i] = addresses(ctx, name, resolve) }) } wg.Wait() @@ -81,21 +97,26 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r return nil, coreErr } var private []string - for i, name := range egress.PrivateDomains { + public := map[string][]string{} + for i, name := range lookups { if failed[i] != nil { fmt.Fprintf(report, "unresolved %s\n", name) continue } for _, a := range found[i] { if isPrivate(a) { - private = append(private, a.String()) + if slices.Contains(egress.PrivateDomains, name) { + private = append(private, a.String()) + } + } else if pin && a.IsGlobalUnicast() { + public[name] = append(public[name], a.String()) } } } slices.Sort(private) private = slices.Compact(private) - n := &sdk.NetworkConfig{DefaultEgress: sdk.PolicyActionDeny, DefaultIngress: sdk.PolicyActionDeny} + n := &sdk.NetworkConfig{DefaultEgress: sdk.PolicyActionDeny, DefaultIngress: sdk.PolicyActionDeny, DisableStrict: !restore} allow := func(destination, protocol, port string) { n.Rules = append(n.Rules, sdk.PolicyRule{Action: sdk.PolicyActionAllow, Direction: sdk.PolicyDirectionEgress, Destination: destination, Protocol: sdk.PolicyProtocol(protocol), Port: port}) } @@ -124,12 +145,18 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r allow(a.String(), "tcp", strconv.Itoa(p)) } } else if sandbox.ValidDomain(host) { + for _, a := range public[host] { + allow(a, "tcp", strconv.Itoa(p)) + } allow(host, "tcp", strconv.Itoa(p)) } else { fmt.Fprintf(report, "unsupported %s\n", endpoint) } } for _, name := range names { + for _, a := range public[name] { + allow(a, "", "") + } allow(name, "", "") } } @@ -139,6 +166,17 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r return n, nil } +// endpointDomains returns the host names of the Endpoints that name one. +func endpointDomains(endpoints []string) []string { + var hosts []string + for _, endpoint := range endpoints { + if host, _, err := sandbox.SplitEndpoint(endpoint); err == nil && sandbox.ValidDomain(host) { + hosts = append(hosts, host) + } + } + return hosts +} + // addresses returns the sorted, unmapped addresses of a host name or literal. func addresses(ctx context.Context, host string, resolve resolver) ([]netip.Addr, error) { if a, err := netip.ParseAddr(host); err == nil { diff --git a/services/core/tools/microsandbox-provider/network_test.go b/services/core/tools/microsandbox-provider/network_test.go index cb7f4dcbf..e73e6f834 100644 --- a/services/core/tools/microsandbox-provider/network_test.go +++ b/services/core/tools/microsandbox-provider/network_test.go @@ -27,13 +27,17 @@ func fixedDNS(table map[string][]string) resolver { } func rules(t *testing.T, egress sandbox.Egress, dns map[string][]string) (string, string) { + return policy(t, egress, false, dns) +} + +func policy(t *testing.T, egress sandbox.Egress, restore bool, dns map[string][]string) (string, string) { t.Helper() var report bytes.Buffer - n, err := egressNetwork(context.Background(), "https://core.example.com:24443", egress, fixedDNS(dns), &report) + n, err := egressNetwork(context.Background(), "https://core.example.com:24443", egress, restore, fixedDNS(dns), &report) if err != nil { t.Fatal(err) } - if n.DefaultEgress != "deny" || n.DefaultIngress != "deny" || n.DisableStrict || n.DNS != nil { + if n.DefaultEgress != "deny" || n.DefaultIngress != "deny" || n.DisableStrict == restore || n.DNS != nil { t.Fatalf("defaults or DNS options changed: %+v", n) } var lines []string @@ -85,3 +89,38 @@ allow pypi.org ` t.Fatalf("report %q", report) } } + +// Restore keeps strict hostname enforcement, so each domain allow is preceded +// by the public addresses its name resolves to; private answers of a Domains +// name or an Endpoint stay blocked. +func TestRestorePinsPublicAddresses(t *testing.T) { + egress := sandbox.Egress{PrivateDomains: []string{"gitlab.example.com"}, Domains: []string{"pypi.org", "gone.example.com"}, + Endpoints: []string{"models.example.com:8443"}} + dns := map[string][]string{"core.example.com": {"172.29.144.1"}, "gitlab.example.com": {"10.74.2.81", "8.8.8.8"}, + "pypi.org": {"151.101.0.223", "10.1.1.1"}, "models.example.com": {"10.9.9.9", "2606:4700::1"}} + got, report := policy(t, egress, true, dns) + want := `allow host udp 53 +allow host tcp 53 +deny loopback +deny link-local +deny metadata +allow 172.29.144.1 tcp 24443 +allow 10.74.2.81 +deny private +allow 2606:4700::1 tcp 8443 +allow models.example.com tcp 8443 +allow 8.8.8.8 +allow gitlab.example.com +allow gone.example.com +allow 151.101.0.223 +allow pypi.org ` + if got != want { + t.Fatalf("rules:\n%s", got) + } + if report != "unresolved gone.example.com\n" { + t.Fatalf("report %q", report) + } + if got, _ := policy(t, sandbox.Egress{Public: true, Domains: []string{"pypi.org"}}, true, dns); strings.Contains(got, "151.101.0.223") { + t.Fatalf("public egress pinned addresses:\n%s", got) + } +} diff --git a/services/core/tools/microsandbox-provider/snapshot.go b/services/core/tools/microsandbox-provider/snapshot.go index 889a502c1..4ea6205b4 100644 --- a/services/core/tools/microsandbox-provider/snapshot.go +++ b/services/core/tools/microsandbox-provider/snapshot.go @@ -341,7 +341,7 @@ func (b backend) resume(ctx context.Context, q wire.ResumeRequest) (wire.State, if q.ObserveOnly || q.Target.ID != "" { return wire.State{}, wire.ErrUnconfirmed } - network, e := b.network(ctx, q.Egress) + network, e := b.network(ctx, q.Egress, true) if e != nil { return wire.State{}, b.closeUndispatchedRestore(q, e) } From 75598d60ae0187c2148d1c423e5e5ba0cf57aa6c Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 28 Oct 2026 05:57:23 +0000 Subject: [PATCH 2/2] Order restore pins first and state the limit for every not-public sandbox --- contracts/agents-api/environments.md | 2 +- contracts/agents-api/index.md | 2 +- contracts/agents-api/sandbox-deployment.md | 2 +- contracts/agents-api/zh/environments.md | 4 ++-- contracts/agents-api/zh/index.md | 4 ++-- contracts/agents-api/zh/sandbox-deployment.md | 4 ++-- docs/sandbox-provider.md | 2 +- docs/zh/sandbox-provider.md | 4 ++-- .../core/tools/microsandbox-provider/network.go | 14 ++++++++++---- .../tools/microsandbox-provider/network_test.go | 12 ++++++------ 10 files changed, 28 insertions(+), 22 deletions(-) diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index 548f7f2d2..f24774dbb 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -312,7 +312,7 @@ A hosted Session's effective `network` narrows the deployment's [network cap](./ Limits ([adapter rules](../../docs/sandbox-provider.md#network-egress)): - A model provider host that the cap lists is reachable like a listed domain, not only on its port. -- On microsandbox, once a `disabled` or `restricted` sandbox has resumed, a domain allowed on public addresses, the model provider included, is reachable over HTTPS only at the addresses its name had at resume; other sites on those addresses are reachable too. +- On microsandbox, once a sandbox without public internet (`disabled`, `restricted`, or `enabled` under a cap without it) has resumed, a domain allowed on public addresses, the model provider included, is reachable only at the addresses its name had at resume, except over plain HTTP; other sites on those addresses are reachable too. - On E2B, a restricted domain is reachable only on ports 80 and 443, and a model provider on another port is reachable on every port of its public addresses. `disabled` and `restricted` return `sandbox_network_unsupported` when the deployment's sandbox Provider cannot restrict egress. A `self_hosted` Environment accepts only `enabled`, because Core cannot enforce a policy on a machine it does not run. diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index a1e9105ad..65ff8e77f 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -160,7 +160,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Hosted workspace admission is limited by the [tenant capacity policy](../../docs/configuration.md#tenant-capacity) and the [input reservation deadline](./environments.md#reservations). - MiniMax Code does not declare `retained_native_history`, so it cannot run on microsandbox, which keeps every Environment on workspace storage; Core rejects the Session ([Harness capabilities](./harness-capabilities.md)). -- The model provider's port stays reachable at public addresses from the sandbox because the Harness runs there, and a private model provider host only when the network cap lists its name; under `disabled` the host's DNS server stays reachable on port 53; MCP connections from the Environment follow the deployment's [network cap](./sandbox-deployment.md#network). E2B sandboxes cannot reach private networks, and `169.254.169.254` answers inside an E2B sandbox under every cap. +- The model provider's port stays reachable at public addresses from the sandbox because the Harness runs there, and a private model provider host only when the network cap lists its name; under `disabled` the host's DNS server stays reachable on port 53; MCP connections from the Environment follow the deployment's [network cap](./sandbox-deployment.md#network). After a microsandbox sandbox without public internet resumes, other sites on the addresses of its allowed domains are reachable too ([limits](./environments.md#restricted-network)). E2B sandboxes cannot reach private networks, and `169.254.169.254` answers inside an E2B sandbox under every cap. - `disabled` and `restricted` networking run on hosted Environments whose sandbox Provider can restrict egress, within the deployment's network cap; `self_hosted` Environments accept only `enabled` ([network policy](./environments.md#restricted-network)). The official API accepts an `allowed_domains` entry with a leading dot, such as `.example.com`, with unspecified meaning; Core rejects it. - `packages.system` other than null or an empty list is rejected; system packages must be preinstalled. - `Environment.files` lists the files recorded at Session creation; files created later through the Environment files API do not appear. diff --git a/contracts/agents-api/sandbox-deployment.md b/contracts/agents-api/sandbox-deployment.md index 129c4c439..03576eb99 100644 --- a/contracts/agents-api/sandbox-deployment.md +++ b/contracts/agents-api/sandbox-deployment.md @@ -67,7 +67,7 @@ Select the filesystem configuration before the microsandbox deployment. Selectin | Field | Accepted value | | --- | --- | -| `public` | Boolean. `true` allows public addresses; `false` allows only Core, the model provider and `domains` | +| `public` | Boolean. `true` allows public addresses; `false` allows only Core, the model provider and `domains` ([microsandbox limit after a resume](./environments.md#restricted-network)) | | `domains` | At most 100 exact host names of at most 253 characters, in lowercase letters, digits, hyphens and dots, whose last label starts with a letter. Wildcards, IP literals, ports and URLs are rejected. Core stores them lowercase, deduplicated and sorted. Every address a name resolves to is allowed, private addresses included | The default is `{"public": true, "domains": []}`. Under every cap, loopback, link-local and cloud-metadata addresses and all other private addresses stay blocked, except that E2B cannot block the metadata address 169.254.169.254 and its sandboxes cannot reach private networks at all. Addresses are resolved when a sandbox starts and when it resumes, so a name whose answer changes in between is not followed. A sandbox keeps the cap of the generation that created it. diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index b6bdfc6aa..37a657ad0 100644 --- a/contracts/agents-api/zh/environments.md +++ b/contracts/agents-api/zh/environments.md @@ -1,7 +1,7 @@ --- title: "Environment 与模板" source: contracts/agents-api/environments.md -source_hash: 108a9ece9f826d222eecec0de0843b4e5aeeff09dd98eacc864e88d62af46439 +source_hash: 4175f4a847bf337d1b5c233a56e054218588408429107afa59fbadfc46bfab1e --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -314,7 +314,7 @@ Core 可以用同一 Project 中较早一次准备的结果启动托管 Environm 限制([适配器规则](../../../docs/zh/sandbox-provider.md#network-egress)): - 上限列出的模型提供商主机与所列域名一样可达,不限于其端口。 -- 在 microsandbox 上,`disabled` 或 `restricted` 沙箱恢复后,在公网地址上放行的域名(包括模型提供商)只能经其名称在恢复时的地址通过 HTTPS 访问;同一地址上的其他站点也可访问。 +- 在 microsandbox 上,不能访问公网的沙箱(`disabled`、`restricted`,或上限不允许公网时的 `enabled`)恢复后,在公网地址上放行的域名(包括模型提供商)除明文 HTTP 外只能经其名称在恢复时的地址访问;同一地址上的其他站点也可访问。 - 在 E2B 上,受限域名只能通过 80 和 443 端口访问;模型提供商使用其他端口时,其公网地址的所有端口均可达。 部署的沙箱 Provider 无法限制出站时,`disabled` 和 `restricted` 返回 `sandbox_network_unsupported`。`self_hosted` Environment 只接受 `enabled`,因为 Core 无法在不由它运行的机器上强制策略。 diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 16d3715d9..8c165ba68 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: 0cf1499e96991dcf395556149664fee7659d17db35c8cee0853e399d6c57d807 +source_hash: e40f743525e7a4a41df4dfd1dbcacfc187bf5bb169652613d48031fe3fa850ef --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -163,7 +163,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - Hosted 工作区准入受[租户容量策略](../../../docs/zh/configuration.md#tenant-capacity)及既有[输入预留截止时间](environments.md#reservations)限制。 - MiniMax Code 未声明 `retained_native_history`,因此无法在 microsandbox 上运行(其每个 Environment 都使用工作区存储);Core 会拒绝该 Session([Harness 能力](./harness-capabilities.md))。 -- 模型提供商在公开地址上的端口从沙箱内始终可达,因为 Harness 运行在那里,私有的模型提供商主机只有在网络上限列出其名称时才可达;`disabled` 下主机 53 端口上的 DNS 服务器仍可达;Environment 中的 MCP 连接遵循部署的[网络上限](sandbox-deployment.md#network)。E2B 沙箱无法访问私有网络,且在任何上限下 `169.254.169.254` 都会在 E2B 沙箱内应答。 +- 模型提供商在公开地址上的端口从沙箱内始终可达,因为 Harness 运行在那里,私有的模型提供商主机只有在网络上限列出其名称时才可达;`disabled` 下主机 53 端口上的 DNS 服务器仍可达;Environment 中的 MCP 连接遵循部署的[网络上限](sandbox-deployment.md#network)。不能访问公网的 microsandbox 沙箱恢复后,与其放行域名同一地址上的其他站点也可访问([限制](environments.md#restricted-network))。E2B 沙箱无法访问私有网络,且在任何上限下 `169.254.169.254` 都会在 E2B 沙箱内应答。 - `disabled` 和 `restricted` 网络在沙箱 Provider 能限制出站的托管 Environment 上运行,并受部署网络上限约束;`self_hosted` Environment 只接受 `enabled`。官方 API 接受带前导点的 `allowed_domains` 条目(如 `.example.com`),含义未规定,Core 拒绝它([restricted network policy](environments.md#restricted-network))。 - 除 null 或空列表以外的 `packages.system` 都会被拒绝;系统软件包必须预先安装。 - `Environment.files` 列出 Session 创建时记录的文件;之后通过 Environment files API 创建的文件不会出现。 diff --git a/contracts/agents-api/zh/sandbox-deployment.md b/contracts/agents-api/zh/sandbox-deployment.md index e8cfdad2d..492702dfe 100644 --- a/contracts/agents-api/zh/sandbox-deployment.md +++ b/contracts/agents-api/zh/sandbox-deployment.md @@ -1,7 +1,7 @@ --- title: "沙箱部署" source: contracts/agents-api/sandbox-deployment.md -source_hash: 5afb4db75820cfbadd390409074c94730a559ba86594f8bc40de4796cd174b94 +source_hash: 0d245fcadbcc3b56f82a596c2bb1fd294e3e278b394f92aebe08e3d43efc45b9 --- 沙箱部署为 Core 管理的 `openai_hosted` 执行选择沙箱提供商和每个沙箱的资源。PostgreSQL 为每个安装维护一个当前有效选择;Web 和 Core API 写入同一配置。节点文件保存其已安装副本和特定于主机的路径,且不能覆盖其资源。Runtime 不属于该选择:每个节点运行其所属安装验证过的 Runtime。该选择独立于 Harness。部署可以保持未配置状态,没有节点;此时它拒绝托管准入。 @@ -70,7 +70,7 @@ microsandbox 会配置 CPU、内存和托管根磁盘,并在挂载于 `/enviro | 字段 | 可接受的值 | | --- | --- | -| `public` | 布尔值。`true` 允许公开地址;`false` 只允许 Core、模型提供商和 `domains` | +| `public` | 布尔值。`true` 允许公开地址;`false` 只允许 Core、模型提供商和 `domains`([microsandbox 恢复后的限制](environments.md#restricted-network)) | | `domains` | 最多 100 个精确主机名,每个不超过 253 个字符,由小写字母、数字、连字符和点组成,且最后一个标签以字母开头。通配符、IP 字面量、端口和 URL 会被拒绝。Core 以小写、去重并排序的形式存储。名称解析出的每个地址都被允许,包括私有地址 | 默认值为 `{"public": true, "domains": []}`。无论哪种上限,回环、链路本地、云元数据地址以及其他所有私有地址始终被阻止,但 E2B 无法阻止元数据地址 169.254.169.254,且 E2B 沙箱根本无法访问私有网络。地址在沙箱启动和恢复时解析,因此两次解析之间应答变化的名称不会被跟随。沙箱保留创建它的代次的上限。 diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index d00c8aca2..3264e51d1 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -204,7 +204,7 @@ Core sends the same `Egress` in `Bootstrap` and in every `ResumeRequest`, derive The Provider enforces exactly this on top of the always-allowed connection to Core, and blocks loopback, link-local, cloud-metadata and every private address that `Egress` does not name, except that E2B cannot block the metadata address 169.254.169.254. It resolves names with its host's DNS when it creates and when it resumes, and cannot follow later changes of an answer. A name that does not resolve gets no rule. MCP traffic has no entry: it follows `Public`, `PrivateDomains` and `Domains`. -microsandbox applies the rules with first-match order, default deny, and host DNS reachable; it allows the resolved private addresses without a port so that DNS rebind protection passes them. Its restore cannot turn strict hostname enforcement off, under which a domain allow passes no HTTPS, so a restored sandbox that is not `Public` also allows the public addresses each name of `Domains`, `PrivateDomains` and `Endpoints` resolves to at restore, on the same ports. E2B declares `Restrict` only: `Public` adds a deny list for loopback, private and link-local ranges, and not `Public` denies all IPv4 and IPv6 traffic except the Core host, `Endpoints` and `Domains`. E2B matches a host name only on ports 80 and 443, so the adapter also allows the global IPv4 addresses of the Core host and of each Endpoint on any other port. An IP literal is allowed only as a global address range of one address, and a private literal gets no rule. +microsandbox applies the rules with first-match order, default deny, and host DNS reachable; it allows the resolved private addresses without a port so that DNS rebind protection passes them. Its restore cannot turn strict hostname enforcement off, under which a domain allow passes only plaintext protocols where the client speaks first, such as HTTP, so a restored sandbox that is not `Public` also allows the public addresses each name of `Domains`, `PrivateDomains` and `Endpoints` resolves to at restore, on the same ports. E2B declares `Restrict` only: `Public` adds a deny list for loopback, private and link-local ranges, and not `Public` denies all IPv4 and IPv6 traffic except the Core host, `Endpoints` and `Domains`. E2B matches a host name only on ports 80 and 443, so the adapter also allows the global IPv4 addresses of the Core host and of each Endpoint on any other port. An IP literal is allowed only as a global address range of one address, and a private literal gets no rule. ## Checkpoint lifecycle diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index c3340a55c..583887f14 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "沙箱提供商协议" source: docs/sandbox-provider.md -source_hash: 624d3a362c2696bc95096284e4ffbe399e97b54979b87062d7de6187a045ead3 +source_hash: f6f2e826a7857ca1c7bc5a6c58dcacfb044aa1627abf1b42f4b5ac4d2cd06d62 --- 沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。microsandbox 和 E2B 实现了该协议。 @@ -206,7 +206,7 @@ Core 在 `Bootstrap` 和每个 `ResumeRequest` 中发送同一个 `Egress`,它 Provider 在始终允许到 Core 的连接之上严格执行这些内容,并阻止回环、链路本地、云元数据以及 `Egress` 未列出的所有私有地址,但 E2B 无法阻止元数据地址 169.254.169.254。它在创建和恢复时用主机 DNS 解析名称,无法跟随之后应答的变化。无法解析的名称没有规则。MCP 流量没有条目:它遵循 `Public`、`PrivateDomains` 和 `Domains`。 -microsandbox 按首条匹配的顺序、默认拒绝并保持主机 DNS 可达来应用规则;它不带端口地允许解析出的私有地址,使 DNS rebind 保护放行它们。它的恢复无法关闭严格主机名校验,而该校验下域名放行规则不放行任何 HTTPS,因此非 `Public` 的沙箱恢复后,还会在相同端口上允许 `Domains`、`PrivateDomains` 和 `Endpoints` 中每个名称在恢复时解析出的公网地址。E2B 只声明 `Restrict`:`Public` 时加入回环、私有和链路本地网段的拒绝列表,非 `Public` 时拒绝除 Core 主机、`Endpoints` 和 `Domains` 之外的全部 IPv4 和 IPv6 流量。E2B 只在 80 和 443 端口匹配主机名,因此 adapter 还会为 Core 主机和每个 Endpoint 在其他端口上允许其全球 IPv4 地址。IP 字面量只作为单地址的全球地址范围被允许,私有字面量没有规则。 +microsandbox 按首条匹配的顺序、默认拒绝并保持主机 DNS 可达来应用规则;它不带端口地允许解析出的私有地址,使 DNS rebind 保护放行它们。它的恢复无法关闭严格主机名校验,而该校验下域名放行规则只放行由客户端先发数据的明文协议(如 HTTP),因此非 `Public` 的沙箱恢复后,还会在相同端口上允许 `Domains`、`PrivateDomains` 和 `Endpoints` 中每个名称在恢复时解析出的公网地址。E2B 只声明 `Restrict`:`Public` 时加入回环、私有和链路本地网段的拒绝列表,非 `Public` 时拒绝除 Core 主机、`Endpoints` 和 `Domains` 之外的全部 IPv4 和 IPv6 流量。E2B 只在 80 和 443 端口匹配主机名,因此 adapter 还会为 Core 主机和每个 Endpoint 在其他端口上允许其全球 IPv4 地址。IP 字面量只作为单地址的全球地址范围被允许,私有字面量没有规则。 ## 检查点生命周期 {#checkpoint-lifecycle} diff --git a/services/core/tools/microsandbox-provider/network.go b/services/core/tools/microsandbox-provider/network.go index bcf68d023..6d957d89b 100644 --- a/services/core/tools/microsandbox-provider/network.go +++ b/services/core/tools/microsandbox-provider/network.go @@ -44,9 +44,9 @@ const resolveTimeout = 5 * time.Second // 6. unless Public, a tcp allow on its port for each Endpoint (domain, or a // public IP literal), and a port-less domain allow for each PrivateDomains // and Domains name; only public answers reach a domain allow, since rule 5 -// and rebind protection already refuse private ones; on restore each -// domain allow is preceded by the same allow for the public addresses the -// name resolves to now +// and rebind protection already refuse private ones; on restore the same +// allows for the public addresses these names resolve to now come first, +// since a domain allow that matches an address first refuses it // 7. public addresses, when Public // // Strict hostname enforcement accepts a domain allow over TLS only with TLS @@ -136,6 +136,7 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r } deny("private") if !egress.Public { + var domains [][2]string for _, endpoint := range egress.Endpoints { host, p, err := sandbox.SplitEndpoint(endpoint) if err != nil { @@ -148,7 +149,7 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r for _, a := range public[host] { allow(a, "tcp", strconv.Itoa(p)) } - allow(host, "tcp", strconv.Itoa(p)) + domains = append(domains, [2]string{host, strconv.Itoa(p)}) } else { fmt.Fprintf(report, "unsupported %s\n", endpoint) } @@ -157,6 +158,11 @@ func egressNetwork(ctx context.Context, coreURL string, egress sandbox.Egress, r for _, a := range public[name] { allow(a, "", "") } + } + for _, d := range domains { + allow(d[0], "tcp", d[1]) + } + for _, name := range names { allow(name, "", "") } } diff --git a/services/core/tools/microsandbox-provider/network_test.go b/services/core/tools/microsandbox-provider/network_test.go index e73e6f834..d5d2696b5 100644 --- a/services/core/tools/microsandbox-provider/network_test.go +++ b/services/core/tools/microsandbox-provider/network_test.go @@ -77,8 +77,8 @@ deny metadata allow 172.29.144.1 tcp 24443 allow 10.74.2.81 deny private -allow models.example.com tcp 8443 allow 203.0.113.9 tcp 8000 +allow models.example.com tcp 8443 allow gitlab.example.com allow gone.example.com allow pypi.org ` @@ -90,9 +90,9 @@ allow pypi.org ` } } -// Restore keeps strict hostname enforcement, so each domain allow is preceded -// by the public addresses its name resolves to; private answers of a Domains -// name or an Endpoint stay blocked. +// Restore keeps strict hostname enforcement, so the public addresses the names +// resolve to are allowed ahead of every domain allow; private answers of a +// Domains name or an Endpoint stay blocked. func TestRestorePinsPublicAddresses(t *testing.T) { egress := sandbox.Egress{PrivateDomains: []string{"gitlab.example.com"}, Domains: []string{"pypi.org", "gone.example.com"}, Endpoints: []string{"models.example.com:8443"}} @@ -108,11 +108,11 @@ allow 172.29.144.1 tcp 24443 allow 10.74.2.81 deny private allow 2606:4700::1 tcp 8443 -allow models.example.com tcp 8443 allow 8.8.8.8 +allow 151.101.0.223 +allow models.example.com tcp 8443 allow gitlab.example.com allow gone.example.com -allow 151.101.0.223 allow pypi.org ` if got != want { t.Fatalf("rules:\n%s", got)