Skip to content

Commit

Permalink
feat(mm): add config to opt-in individual games for host networking &…
Browse files Browse the repository at this point in the history
… root containers (#549)

fix(mm): attempting to run a root container will log to stderr

Fixes RVT-3523
Fixes MIS-144
  • Loading branch information
NathanFlurry committed Feb 28, 2024
1 parent 748aaa8 commit be9ddd6
Show file tree
Hide file tree
Showing 26 changed files with 296 additions and 48 deletions.
9 changes: 0 additions & 9 deletions lib/bolt/config/src/ns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,6 @@ pub struct Rivet {
pub cdn: Cdn,
#[serde(default)]
pub billing: Option<RivetBilling>,
#[serde(default)]
pub matchmaker: Option<RivetMatchmaker>,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
Expand All @@ -566,13 +564,6 @@ pub struct Telemetry {
pub disable: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct RivetMatchmaker {
#[serde(default)]
pub host_networking: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub enum RivetAccess {
Expand Down
12 changes: 0 additions & 12 deletions lib/bolt/core/src/context/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,6 @@ impl ProjectContextData {
}
}

if self.ns().rivet.test.is_some()
&& !self.ns().pools.is_empty()
&& !self
.ns()
.rivet
.matchmaker
.as_ref()
.map_or(false, |mm| mm.host_networking)
{
panic!("must have host networking enabled if tests + pools are enabled (rivet.matchmaker.host_networking = true)");
}

// MARK: Billing emails
if self.ns().rivet.billing.is_some() {
assert!(
Expand Down
6 changes: 0 additions & 6 deletions lib/bolt/core/src/context/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,12 +783,6 @@ impl ServiceContextData {
env.push(("RIVET_ACCESS_TOKEN_LOGIN".into(), "1".into()));
}

if let Some(mm) = &project_ctx.ns().rivet.matchmaker {
if mm.host_networking {
env.push(("RIVET_HOST_NETWORKING".into(), "1".into()));
}
}

// Domains
if let Some(x) = project_ctx.domain_main() {
env.push(("RIVET_DOMAIN_MAIN".into(), x));
Expand Down
39 changes: 34 additions & 5 deletions lib/job-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ fn main() -> anyhow::Result<()> {
let nomad_alloc_dir = std::env::var("NOMAD_ALLOC_DIR").context("NOMAD_ALLOC_DIR")?;
let job_run_id = std::env::var("NOMAD_META_job_run_id").context("NOMAD_META_job_run_id")?;
let nomad_task_name = std::env::var("NOMAD_TASK_NAME").context("NOMAD_TASK_NAME")?;
let root_user_enabled = std::env::var("NOMAD_META_root_user_enabled")
.context("NOMAD_META_root_user_enabled")?
== "1";

let oci_bundle_path = format!("{}/oci-bundle", nomad_alloc_dir);
let container_id = fs::read_to_string(format!("{}/container-id", nomad_alloc_dir))
Expand All @@ -42,6 +45,32 @@ fn main() -> anyhow::Result<()> {
};
let log_shipper_thread = log_shipper.spawn();

// Validate OCI bundle
let oci_bundle_str =
fs::read_to_string(&oci_bundle_path).context("failed to read OCI bundle")?;
let oci_bundle = serde_json::from_str::<serde_json::Value>(&oci_bundle_str)
.context("failed to parse OCI bundle")?;
let (Some(uid), Some(gid)) = (
oci_bundle["process"]["user"]["uid"].as_i64(),
oci_bundle["process"]["user"]["gid"].as_i64(),
) else {
bail!("missing uid or gid in OCI bundle")
};
if !root_user_enabled && (uid == 0 || gid == 0) {
send_message(
&msg_tx,
None,
log_shipper::StreamType::StdErr,
format!("Server is attempting to run as root user or group (uid: {uid}, gid: {gid})"),
);
send_message(
&msg_tx,
None,
log_shipper::StreamType::StdErr,
format!("See https://rivet.gg/docs/dynamic-servers/concepts/docker-root-user"),
);
}

// Spawn runc container
println!(
"Starting container {} with OCI bundle {}",
Expand Down Expand Up @@ -172,7 +201,7 @@ fn ship_logs(
if err.first_throttle_in_window {
if send_message(
&msg_tx,
&mut throttle_error,
Some(&mut throttle_error),
stream_type,
format_rate_limit(err.time_remaining),
) {
Expand All @@ -184,7 +213,7 @@ fn ship_logs(
if err.first_throttle_in_window {
if send_message(
&msg_tx,
&mut throttle_error,
Some(&mut throttle_error),
stream_type,
format_rate_limit(err.time_remaining),
) {
Expand Down Expand Up @@ -224,7 +253,7 @@ fn ship_logs(
}
}

if send_message(&msg_tx, &mut throttle_error, stream_type, message) {
if send_message(&msg_tx, Some(&mut throttle_error), stream_type, message) {
break;
}
}
Expand All @@ -238,7 +267,7 @@ fn ship_logs(
/// Returns true if receiver is disconnected
fn send_message(
msg_tx: &mpsc::SyncSender<log_shipper::ReceivedMessage>,
throttle_error: &mut throttle::Throttle,
throttle_error: Option<&mut throttle::Throttle>,
stream_type: log_shipper::StreamType,
message: String,
) -> bool {
Expand All @@ -258,7 +287,7 @@ fn send_message(
}) {
Result::Ok(_) => {}
Err(mpsc::TrySendError::Full(_)) => {
if throttle_error.tick().is_ok() {
if throttle_error.map_or(true, |x| x.tick().is_ok()) {
eprintln!("log shipper buffer full, logs are being dropped");
}
}
Expand Down
6 changes: 6 additions & 0 deletions proto/backend/matchmaker.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import "proto/common.proto";
import "proto/backend/captcha.proto";
import "proto/backend/region.proto";

// MARK: Game Config
message GameConfig {
bool host_networking_enabled = 1;
bool root_user_enabled = 2;
}

// MARK: Game Namespace Config
message NamespaceConfig {
uint32 lobby_count_max = 1;
Expand Down
23 changes: 23 additions & 0 deletions svc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions svc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ members = [
"pkg/load-test/standalone/mm-sustain",
"pkg/load-test/standalone/sqlx",
"pkg/load-test/standalone/watch-requests",
"pkg/mm-config/ops/game-get",
"pkg/mm-config/ops/game-upsert",
"pkg/mm-config/ops/lobby-group-get",
"pkg/mm-config/ops/lobby-group-resolve-name-id",
"pkg/mm-config/ops/lobby-group-resolve-version",
Expand Down
1 change: 1 addition & 0 deletions svc/pkg/faker/ops/game/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ faker-game-namespace = { path = "../game-namespace" }
faker-game-version = { path = "../game-version" }
faker-team = { path = "../team" }
game-create = { path = "../../../game/ops/create" }
mm-config-game-upsert = { path = "../../../mm-config/ops/game-upsert" }

[dev-dependencies]
chirp-worker = { path = "../../../../../lib/chirp/worker" }
Expand Down
13 changes: 12 additions & 1 deletion svc/pkg/faker/ops/game/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use proto::backend::pkg::*;
use proto::backend::{self, pkg::*};
use rivet_operation::prelude::*;

#[operation(name = "faker-game")]
Expand Down Expand Up @@ -28,6 +28,17 @@ async fn handle(
})
.await?;

op!([ctx] mm_config_game_upsert {
game_id: game_create_res.game_id,
config: Some(backend::matchmaker::GameConfig {
// Required for testing
host_networking_enabled: true,
// Will never be tested
root_user_enabled: false,
})
})
.await?;

let mut namespace_ids = Vec::<common::Uuid>::new();
let mut version_ids = Vec::<common::Uuid>::new();
if !ctx.skip_namespaces_and_versions {
Expand Down
2 changes: 1 addition & 1 deletion svc/pkg/game/ops/version-validate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ util-mm = { package = "rivet-util-mm", path = "../../../mm/util" }
external-request-validate = { path = "../../../external/ops/request-validate" }
game-version-get = { path = "../version-get" }
game-version-list = { path = "../version-list" }
mm-config-game-get = { path = "../../../mm-config/ops/game-get" }
module-version-get = { path = "../../../module/ops/version-get" }
region-get = { path = "../../../region/ops/get" }
tier-list = { path = "../../../tier/ops/list" }

[dev-dependencies]
chirp-worker = { path = "../../../../../lib/chirp/worker" }

faker-game = { path = "../../../faker/ops/game" }
28 changes: 17 additions & 11 deletions svc/pkg/game/ops/version-validate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ async fn handle(
errors.push(util::err_path!["display-name", "invalid"]);
}

// Get game config
let game_res = op!([ctx] mm_config_game_get {
game_ids: vec![*game_id],
})
.await?;
let mm_game_config = unwrap_ref!(unwrap!(game_res.games.first()).config);

// Validate display name uniqueness
{
let version_list_res = op!([ctx] game_version_list {
Expand Down Expand Up @@ -789,11 +796,18 @@ async fn handle(
let network_mode = unwrap!(LobbyRuntimeNetworkMode::from_i32(
docker_config.network_mode
));
let host_networking_enabled =
std::env::var("RIVET_HOST_NETWORKING").map_or(false, |v| v == "1");
// Validate ports
if host_networking_enabled || !matches!(network_mode, LobbyRuntimeNetworkMode::Host)
if !mm_game_config.host_networking_enabled
&& matches!(network_mode, LobbyRuntimeNetworkMode::Host)
{
errors.push(util::err_path![
"config",
"matchmaker",
"game-modes",
lobby_group_label,
"host-networking-disabled",
]);
} else {
let mut unique_port_labels = HashSet::<String>::new();
let mut unique_ports = HashSet::<(u32, i32)>::new();
let mut ranges = Vec::<PortRange>::new();
Expand Down Expand Up @@ -1026,14 +1040,6 @@ async fn handle(
}
}
}
} else {
errors.push(util::err_path![
"config",
"matchmaker",
"game-modes",
lobby_group_label,
"host-networking-disabled",
]);
}
} else {
errors.push(util::err_path![
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE games (
game_id UUID PRIMARY KEY,
host_networking_enabled BOOLEAN NOT NULL DEFAULT FALSE,
root_user_enabled BOOLEAN NOT NULL DEFAULT FALSE
);

17 changes: 17 additions & 0 deletions svc/pkg/mm-config/ops/game-get/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "mm-config-game-get"
version = "0.0.1"
edition = "2021"
authors = ["Rivet Gaming, LLC <developer@rivet.gg>"]
license = "Apache-2.0"

[dependencies]
chirp-client = { path = "../../../../../lib/chirp/client" }
rivet-operation = { path = "../../../../../lib/operation/core" }

[dependencies.sqlx]
version = "0.7"
default-features = false

[dev-dependencies]
chirp-worker = { path = "../../../../../lib/chirp/worker" }
7 changes: 7 additions & 0 deletions svc/pkg/mm-config/ops/game-get/Service.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[service]
name = "mm-config-game-get"

[runtime]
kind = "rust"

[operation]
47 changes: 47 additions & 0 deletions svc/pkg/mm-config/ops/game-get/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use proto::backend::{self, pkg::*};
use rivet_operation::prelude::*;

#[derive(sqlx::FromRow)]
struct GameRow {
game_id: Uuid,
host_networking_enabled: bool,
root_user_enabled: bool,
}

#[operation(name = "mm-config-game-get")]
pub async fn handle(
ctx: OperationContext<mm_config::game_get::Request>,
) -> GlobalResult<mm_config::game_get::Response> {
let game_ids = ctx
.game_ids
.iter()
.map(common::Uuid::as_uuid)
.collect::<Vec<_>>();

let rows = sql_fetch_all!(
[ctx, GameRow]
"
SELECT game_id, host_networking_enabled, root_user_enabled
FROM db_mm_config.games
WHERE game_id = ANY($1)
",
&game_ids,
)
.await?;

let games = game_ids
.iter()
.map(|game_id| {
let row = rows.iter().find(|row| row.game_id == *game_id);
mm_config::game_get::response::Game {
game_id: Some((*game_id).into()),
config: Some(backend::matchmaker::GameConfig {
host_networking_enabled: row.map_or(false, |row| row.host_networking_enabled),
root_user_enabled: row.map_or(false, |row| row.root_user_enabled),
}),
}
})
.collect();

Ok(mm_config::game_get::Response { games })
}
Loading

0 comments on commit be9ddd6

Please sign in to comment.