Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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}");
}
26 changes: 15 additions & 11 deletions crates/maxplayer-core/src/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,24 +607,28 @@ 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.
///
/// * **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.
/// 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.
///
/// See [`crate::sandbox_net`] for what the rules are and [`crate::sandbox_netns`] for how they are
/// put in force.
Expand Down
Loading
Loading