Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8d2bed0
docs(gvisor-dns): run log with verified brief hash, env reality and g…
Sep 10, 2026
4549cc5
test(gvisor-dns): disposable-VM provisioning and gate 1 repro scripts
Sep 10, 2026
ab71684
test(gvisor-dns): gate 1 reproduced — runsc EAI_AGAIN vs runc OK, 127…
Sep 10, 2026
8b1c758
docs(gvisor-dns): fix design — sandbox resolv.conf, /32 resolver pinh…
Sep 10, 2026
66d0609
feat(sandbox): give contained jobs a reachable resolver under gVisor
Sep 10, 2026
7633be6
test(gvisor-dns): gate 2 — dns + verified TLS from the real shared jo…
Sep 10, 2026
9e60d2e
test(sandbox): pin the containment plane to the daemon runtime
Sep 10, 2026
4ee28b2
style(sandbox-probe): indent the resolv_conf field the bulk edit left…
Sep 10, 2026
d255c18
feat(doctor): block readiness on the job's own DNS/TLS route, not the…
Sep 10, 2026
11c2b65
test(gvisor-dns): gate 4 — real git delivery originating inside the s…
Sep 10, 2026
0f7ac70
test(gvisor-dns): gate 5 — the egress plan does not bind a gVisor job
Sep 10, 2026
840ea53
test(gvisor-dns): the shared job namespace is single-use for gVisor
Sep 10, 2026
30378d5
test(gvisor-dns): gate5c re-run soundly — DOCKER-USER cannot bind a b…
Sep 10, 2026
fc7e8a7
test(gvisor-dns): gate5e — INPUT binds host-directed traffic, DOCKER-…
Sep 10, 2026
6bc0117
feat(sandbox): host-side containment for the runtime the netns plan c…
Sep 10, 2026
3911afd
feat(sandbox): a network per job, and host-side rules owned by a Drop…
Sep 10, 2026
6994824
test(gvisor-dns): gate5f — the product's rendered host policy binds a…
Sep 10, 2026
bda6813
docs(sandbox): [sandbox] network names, it no longer shares
Sep 10, 2026
86ddf1b
test(gvisor-dns): rewrite gate 5 under the discipline its own failure…
Sep 10, 2026
e07889a
test(gvisor-dns): gate 5 PASSES — denial binds a runsc job, and three…
Sep 10, 2026
e7ccb60
test(gvisor-dns): a bounded gate runner, and proof it actually runs them
Sep 10, 2026
6d8793b
docs(gvisor-dns): gate 6 — reproducibility, and six things this branc…
Sep 10, 2026
2e72901
test(gvisor-dns): gate5g — reproduce the containment failure on origi…
Sep 10, 2026
03ce92b
test(gvisor-dns): gate5h — lifecycle, recycled address, recreation
Sep 10, 2026
11b8250
test(gvisor-dns): lock the fail-closed host-plan invariants
Sep 10, 2026
95ebb20
test(gvisor-dns): gate5i FAILS — partial host-rule install cannot be …
Sep 10, 2026
676052d
fix(gvisor-dns): tear down a partial host-rule install rule by rule
Sep 10, 2026
e6311a4
docs(gvisor-dns): gate5j — the IPv6 finding, measured
Sep 10, 2026
7dfcc03
test(gvisor-dns): gate5k v1 is CONFOUNDED — recorded, not rerun quietly
Sep 10, 2026
e7fac6c
test(gvisor-dns): gate5k v2 — a structural fail-closed result, and my…
Sep 10, 2026
da08194
docs(gvisor-dns): pin the baseline to a full immutable hash, and flag…
Sep 10, 2026
0560631
test(gvisor-dns): gate5k v3 PASSES — an unsupported daemon default fa…
Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions crates/maxplayer-core/examples/render_host_plan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Prints the **host-side** iptables plan [`HostPolicy`] would hand its applier, for gate evidence.
//!
//! Sibling of `render_net_plan`, and it exists for the same reason: a gate script must install the
//! rules the PRODUCT renders, never rules a script author transcribed. A transcription drifts the
//! moment the policy changes, and the gate then keeps passing against a firewall the product no
//! longer builds — the exact failure these gates exist to catch.
//!
//! It matters more here than for the namespace plan. The host plan lands in chains that are shared
//! with every other container on the daemon, so a hand-written approximation of it in a script is
//! not just drift, it is a rule keyed to the wrong source touching someone else's traffic.
//!
//! ```text
//! cargo run -p maxplayer-core --example render_host_plan -- 172.18.0.2
//! cargo run -p maxplayer-core --example render_host_plan -- 172.18.0.2 --teardown
//! ```
//!
//! Arguments: the job namespace's address, then optionally `--teardown` for the exact inverse plan.

use maxplayer_core::sandbox_net::HostPolicy;
use maxplayer_core::sandbox_netns::{host_install_stdin, host_teardown_stdin};

fn main() {
let mut args = std::env::args().skip(1);
let Some(job_addr) = args.next() else {
eprintln!("usage: render_host_plan <job_addr> [--teardown]");
std::process::exit(2);
};
let teardown = match args.next().as_deref() {
None => false,
Some("--teardown") => true,
Some(other) => {
eprintln!("unknown argument {other:?} — expected --teardown or nothing");
std::process::exit(2);
}
};

let policy = HostPolicy { job_addr };
let (plan, count) =
if teardown { host_teardown_stdin(&policy) } else { host_install_stdin(&policy) };
eprintln!("# {count} rules ({})", if teardown { "teardown" } else { "install" });
print!("{plan}");
}
33 changes: 33 additions & 0 deletions crates/maxplayer-core/examples/render_net_plan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Prints the iptables plan [`NetPolicy`] would hand its sidecar, for gate evidence.
//!
//! This exists so the gVisor gate scripts install the rules the PRODUCT renders rather than rules a
//! script author transcribed by hand. A transcription drifts the moment the policy changes and the
//! gate keeps passing against a firewall the product no longer builds — which is the exact failure
//! the gates are supposed to catch.
//!
//! ```text
//! cargo run -p maxplayer-core --example render_net_plan -- 172.18.0.1 1.1.1.1
//! ```
//!
//! Arguments: the namespace gateway, then every resolver the job is allowed to reach on port 53.

use maxplayer_core::sandbox_net::{NetPolicy, PortRange};
use maxplayer_core::sandbox_netns::plan_stdin;

fn main() {
let mut args = std::env::args().skip(1);
let Some(gateway) = args.next() else {
eprintln!("usage: render_net_plan <gateway> [resolver ...]");
std::process::exit(2);
};
let dns_resolvers: Vec<String> = args.collect();
let policy = NetPolicy {
gateway,
proxy_ports: Some(PortRange::new(49200, 49299).expect("a valid fixed range")),
log_connections: true,
dns_resolvers,
};
let (plan, count) = plan_stdin(&policy);
eprintln!("# {count} rules");
print!("{plan}");
}
45 changes: 34 additions & 11 deletions crates/maxplayer-core/src/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,20 @@ pub struct SandboxConfig {
/// not name, or to carry a gateway base-URL. Unused under `launcher` mode.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forward_env: Vec<String>,
/// `docker` mode: the DNS resolver ADDRESSES a contained job's `/etc/resolv.conf` names.
/// Omitted ⇒ the host's own upstream resolvers are discovered and used; a host that names none
/// refuses to run jobs rather than picking a public resolver nobody chose.
///
/// This exists because docker's embedded resolver at `127.0.0.11` is unreachable from a gVisor
/// sandbox — measured, with a runc control that succeeds on the identical image and network —
/// and because `docker run --dns` does not change what the daemon writes into a container on a
/// user-defined network. Addresses only, never hostnames: resolving the resolver is the problem
/// being fixed. A loopback address is refused for the same reason `127.0.0.11` fails.
///
/// Each address named here is opened by the job's egress policy on port 53 and nothing else,
/// as a single host (`/32`, or `/128` for v6) — never a subnet.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dns_servers: Vec<String>,
/// `docker` mode: the container runtime to run the job under (`docker run --runtime <name>`).
/// Omitted ⇒ the daemon's default runtime (`runc`). The v1 sandbox posture sets this to `runsc`
/// on Linux, where the default container shares the host kernel and gVisor is the primary
Expand All @@ -329,24 +343,33 @@ pub struct SandboxConfig {
/// Unused under `launcher` mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime: Option<String>,
/// `docker` mode: the dedicated docker network a job's container joins (`docker run --network`).
/// Omitted ⇒ the daemon default (the shared `bridge` network).
/// `docker` mode: the **name prefix** for the docker network each job gets to itself.
/// Omitted ⇒ no containment, and the daemon default bridge.
///
/// **Setting this is what turns #797 egress containment on for this seat.** A job launched under
/// it runs in a network namespace whose rules were installed before the job process existed, and
/// a job whose containment cannot be established FAILS rather than running exposed. There is no
/// second step: no root command, and nothing to reinstall after a reboot.
///
/// Two reasons a *named* network rather than the default bridge:
/// **It names, it does not share.** This value is not one bridge every job sits on: each job gets
/// `<this>-job-<job_id>`, created before its holder and removed after it. The name still does the
/// job it always did — telling this seat's networks from a co-tenant daemon's — and no longer
/// puts two jobs on one wire. That changed because of a measurement: with jobs sharing a bridge,
/// a gVisor job REACHED a live listener inside another job's namespace and no host rule stopped
/// it, two containers on one bridge being switched rather than routed, and switched frames
/// entering no iptables chain at all on a host without `br_netfilter`. A network per job leaves
/// the job no on-link peer but its own gateway, which is also what makes every other destination
/// routed, and therefore visible to the host-side policy that binds a gVisor job at all.
///
/// A *named* network rather than the default bridge, for a reason that predates all of that:
/// **the seller's own services are not on it.** The rules deny by destination, and the seat's LAN
/// and host addresses fall inside those denies, so a job must not share the bridge every other
/// container on the box uses.
///
/// * **The seller's own services are not on it.** The rules deny by destination, and the seat's
/// LAN and host addresses fall inside those denies. A dedicated network keeps a job's traffic
/// off the bridge every other container on the box shares.
/// * **DNS keeps working.** On a user-defined network a container resolves through docker's
/// embedded resolver at `127.0.0.11` inside its own netns, so no packet crosses to a host or LAN
/// resolver. On the shared default bridge docker copies the host's `resolv.conf` instead, and if
/// that names a LAN or host resolver then denying the LAN also denies DNS — which presents as
/// "the internet is broken" rather than as a firewall rule.
/// DNS does not come from the network. A contained job is handed a generated read-only
/// `/etc/resolv.conf` naming real upstream resolvers, because docker's embedded resolver at
/// `127.0.0.11` is a daemon-side socket reached by NAT inside the netns and a gVisor sandbox
/// terminates loopback in its own netstack, where it never answers. See [`crate::sandbox_dns`].
///
/// See [`crate::sandbox_net`] for what the rules are and [`crate::sandbox_netns`] for how they are
/// put in force.
Expand Down
4 changes: 4 additions & 0 deletions crates/maxplayer-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub mod runtime_guard;
/// `wallet`-only, so a default-features test run cannot execute a line of them — the policy is the
/// part that decides what a stranger's job can reach, and it is compiled and tested on every build
/// rather than only on the money-path one.
/// The resolver a contained job can reach, and the `resolv.conf` that names it. Ungated for the
/// same reason as `sandbox_net`: under gVisor a job that cannot resolve cannot deliver, so the
/// decision about where its lookups go is policy, and it is compiled and tested on every build.
pub mod sandbox_dns;
pub mod sandbox_net;
/// Putting `sandbox_net`'s policy in force: the holder container that owns the job's network
/// namespace, and the sidecar that installs the rules into it before the job exists. Unconditional
Expand Down
Loading
Loading