diff --git a/lib/bolt/config/src/ns.rs b/lib/bolt/config/src/ns.rs index f3d1afc9c0..8c060377f1 100644 --- a/lib/bolt/config/src/ns.rs +++ b/lib/bolt/config/src/ns.rs @@ -554,8 +554,6 @@ pub struct Rivet { pub cdn: Cdn, #[serde(default)] pub billing: Option, - #[serde(default)] - pub matchmaker: Option, } #[derive(Serialize, Deserialize, Clone, Debug, Default)] @@ -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 { diff --git a/lib/bolt/core/src/context/project.rs b/lib/bolt/core/src/context/project.rs index 8b7aef8fe8..16ca3417b8 100644 --- a/lib/bolt/core/src/context/project.rs +++ b/lib/bolt/core/src/context/project.rs @@ -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!( diff --git a/lib/bolt/core/src/context/service.rs b/lib/bolt/core/src/context/service.rs index 8432b4aa66..7648f94416 100644 --- a/lib/bolt/core/src/context/service.rs +++ b/lib/bolt/core/src/context/service.rs @@ -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)); diff --git a/lib/job-runner/src/main.rs b/lib/job-runner/src/main.rs index f74c88cacf..4667f5ebd6 100644 --- a/lib/job-runner/src/main.rs +++ b/lib/job-runner/src/main.rs @@ -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)) @@ -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::(&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 {}", @@ -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), ) { @@ -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), ) { @@ -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; } } @@ -238,7 +267,7 @@ fn ship_logs( /// Returns true if receiver is disconnected fn send_message( msg_tx: &mpsc::SyncSender, - throttle_error: &mut throttle::Throttle, + throttle_error: Option<&mut throttle::Throttle>, stream_type: log_shipper::StreamType, message: String, ) -> bool { @@ -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"); } } diff --git a/proto/backend/matchmaker.proto b/proto/backend/matchmaker.proto index 721aae00a2..c70a11f2aa 100644 --- a/proto/backend/matchmaker.proto +++ b/proto/backend/matchmaker.proto @@ -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; diff --git a/svc/Cargo.lock b/svc/Cargo.lock index 43e5faac44..ce118adc6a 100644 --- a/svc/Cargo.lock +++ b/svc/Cargo.lock @@ -2915,6 +2915,7 @@ dependencies = [ "faker-team", "game-create", "game-get", + "mm-config-game-upsert", "prost 0.10.4", "rivet-operation", ] @@ -3731,6 +3732,7 @@ dependencies = [ "faker-game", "game-version-get", "game-version-list", + "mm-config-game-get", "module-version-get", "prost 0.10.4", "region-get", @@ -4909,6 +4911,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mm-config-game-get" +version = "0.0.1" +dependencies = [ + "chirp-client", + "chirp-worker", + "rivet-operation", + "sqlx", +] + +[[package]] +name = "mm-config-game-upsert" +version = "0.0.1" +dependencies = [ + "chirp-client", + "chirp-worker", + "rivet-operation", + "sqlx", +] + [[package]] name = "mm-config-lobby-group-get" version = "0.0.1" @@ -5297,6 +5319,7 @@ dependencies = [ "job-run-get", "lazy_static", "maplit", + "mm-config-game-get", "mm-config-lobby-group-resolve-version", "mm-config-namespace-config-set", "mm-config-namespace-get", diff --git a/svc/Cargo.toml b/svc/Cargo.toml index 727ebc0b86..9555aae974 100644 --- a/svc/Cargo.toml +++ b/svc/Cargo.toml @@ -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", diff --git a/svc/pkg/faker/ops/game/Cargo.toml b/svc/pkg/faker/ops/game/Cargo.toml index e8bfb81a24..ca663682cd 100644 --- a/svc/pkg/faker/ops/game/Cargo.toml +++ b/svc/pkg/faker/ops/game/Cargo.toml @@ -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" } diff --git a/svc/pkg/faker/ops/game/src/lib.rs b/svc/pkg/faker/ops/game/src/lib.rs index c258b27650..6508a1dd91 100644 --- a/svc/pkg/faker/ops/game/src/lib.rs +++ b/svc/pkg/faker/ops/game/src/lib.rs @@ -1,4 +1,4 @@ -use proto::backend::pkg::*; +use proto::backend::{self, pkg::*}; use rivet_operation::prelude::*; #[operation(name = "faker-game")] @@ -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::::new(); let mut version_ids = Vec::::new(); if !ctx.skip_namespaces_and_versions { diff --git a/svc/pkg/game/ops/version-validate/Cargo.toml b/svc/pkg/game/ops/version-validate/Cargo.toml index 80c8c99758..89928503cc 100644 --- a/svc/pkg/game/ops/version-validate/Cargo.toml +++ b/svc/pkg/game/ops/version-validate/Cargo.toml @@ -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" } diff --git a/svc/pkg/game/ops/version-validate/src/lib.rs b/svc/pkg/game/ops/version-validate/src/lib.rs index 4a6c565c8d..49aab015dd 100644 --- a/svc/pkg/game/ops/version-validate/src/lib.rs +++ b/svc/pkg/game/ops/version-validate/src/lib.rs @@ -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 { @@ -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::::new(); let mut unique_ports = HashSet::<(u32, i32)>::new(); let mut ranges = Vec::::new(); @@ -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![ diff --git a/svc/pkg/mm-config/db/mm-config/migrations/20240228172257_toggle_root_host.down.sql b/svc/pkg/mm-config/db/mm-config/migrations/20240228172257_toggle_root_host.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/svc/pkg/mm-config/db/mm-config/migrations/20240228172257_toggle_root_host.up.sql b/svc/pkg/mm-config/db/mm-config/migrations/20240228172257_toggle_root_host.up.sql new file mode 100644 index 0000000000..e06f4c462f --- /dev/null +++ b/svc/pkg/mm-config/db/mm-config/migrations/20240228172257_toggle_root_host.up.sql @@ -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 +); + diff --git a/svc/pkg/mm-config/ops/game-get/Cargo.toml b/svc/pkg/mm-config/ops/game-get/Cargo.toml new file mode 100644 index 0000000000..bd10722106 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-get/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mm-config-game-get" +version = "0.0.1" +edition = "2021" +authors = ["Rivet Gaming, LLC "] +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" } diff --git a/svc/pkg/mm-config/ops/game-get/Service.toml b/svc/pkg/mm-config/ops/game-get/Service.toml new file mode 100644 index 0000000000..0b4e5eaf9b --- /dev/null +++ b/svc/pkg/mm-config/ops/game-get/Service.toml @@ -0,0 +1,7 @@ +[service] +name = "mm-config-game-get" + +[runtime] +kind = "rust" + +[operation] diff --git a/svc/pkg/mm-config/ops/game-get/src/lib.rs b/svc/pkg/mm-config/ops/game-get/src/lib.rs new file mode 100644 index 0000000000..9d03a8c490 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-get/src/lib.rs @@ -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, +) -> GlobalResult { + let game_ids = ctx + .game_ids + .iter() + .map(common::Uuid::as_uuid) + .collect::>(); + + 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 }) +} diff --git a/svc/pkg/mm-config/ops/game-get/tests/integration.rs b/svc/pkg/mm-config/ops/game-get/tests/integration.rs new file mode 100644 index 0000000000..7327144128 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-get/tests/integration.rs @@ -0,0 +1,11 @@ +use chirp_worker::prelude::*; + +#[worker_test] +async fn basic(ctx: TestCtx) { + // TODO: Test this better + op!([ctx] mm_config_game_get { + game_ids: vec![] + }) + .await + .unwrap(); +} diff --git a/svc/pkg/mm-config/ops/game-upsert/Cargo.toml b/svc/pkg/mm-config/ops/game-upsert/Cargo.toml new file mode 100644 index 0000000000..4ce41724f8 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-upsert/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mm-config-game-upsert" +version = "0.0.1" +edition = "2021" +authors = ["Rivet Gaming, LLC "] +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" } diff --git a/svc/pkg/mm-config/ops/game-upsert/Service.toml b/svc/pkg/mm-config/ops/game-upsert/Service.toml new file mode 100644 index 0000000000..d8ae9cb915 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-upsert/Service.toml @@ -0,0 +1,7 @@ +[service] +name = "mm-config-game-upsert" + +[runtime] +kind = "rust" + +[operation] diff --git a/svc/pkg/mm-config/ops/game-upsert/src/lib.rs b/svc/pkg/mm-config/ops/game-upsert/src/lib.rs new file mode 100644 index 0000000000..bd2c830c94 --- /dev/null +++ b/svc/pkg/mm-config/ops/game-upsert/src/lib.rs @@ -0,0 +1,24 @@ +use proto::backend::{self, pkg::*}; +use rivet_operation::prelude::*; + +#[operation(name = "mm-config-game-upsert")] +pub async fn handle( + ctx: OperationContext, +) -> GlobalResult { + let game_id = unwrap!(ctx.game_id).as_uuid(); + let config = unwrap_ref!(ctx.config); + + sql_execute!( + [ctx] + " + UPSERT INTO db_mm_config.games (game_id, host_networking_enabled, root_user_enabled) + VALUES ($1, $2, $3) + ", + game_id, + config.host_networking_enabled, + config.root_user_enabled, + ) + .await?; + + Ok(mm_config::game_upsert::Response {}) +} diff --git a/svc/pkg/mm-config/ops/game-upsert/tests/integration.rs b/svc/pkg/mm-config/ops/game-upsert/tests/integration.rs new file mode 100644 index 0000000000..d7d641e21e --- /dev/null +++ b/svc/pkg/mm-config/ops/game-upsert/tests/integration.rs @@ -0,0 +1,6 @@ +use chirp_worker::prelude::*; + +#[worker_test] +async fn basic(ctx: TestCtx) { + // TODO: +} diff --git a/svc/pkg/mm-config/types/game-get.proto b/svc/pkg/mm-config/types/game-get.proto new file mode 100644 index 0000000000..6145b125d0 --- /dev/null +++ b/svc/pkg/mm-config/types/game-get.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package rivet.backend.pkg.mm_config.game_get; + +import "proto/common.proto"; +import "proto/backend/matchmaker.proto"; + +message Request { + repeated rivet.common.Uuid game_ids = 1; +} + +message Response { + message Game { + rivet.common.Uuid game_id = 1; + rivet.backend.matchmaker.GameConfig config = 2; + } + + repeated Game games = 1; +} diff --git a/svc/pkg/mm-config/types/game-upsert.proto b/svc/pkg/mm-config/types/game-upsert.proto new file mode 100644 index 0000000000..af6706d219 --- /dev/null +++ b/svc/pkg/mm-config/types/game-upsert.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package rivet.backend.pkg.mm_config.game_upsert; + +import "proto/common.proto"; +import "proto/backend/matchmaker.proto"; + +message Request { + rivet.common.Uuid game_id = 1; + rivet.backend.matchmaker.GameConfig config = 2; +} + +message Response { + +} diff --git a/svc/pkg/mm/worker/Cargo.toml b/svc/pkg/mm/worker/Cargo.toml index c8d7e499fb..b3a9b4d705 100644 --- a/svc/pkg/mm/worker/Cargo.toml +++ b/svc/pkg/mm/worker/Cargo.toml @@ -33,6 +33,7 @@ game-get = { path = "../../game/ops/get" } game-namespace-get = { path = "../../game/ops/namespace-get" } game-version-get = { path = "../../game/ops/version-get" } job-run-get = { path = "../../job-run/ops/get" } +mm-config-game-get = { path = "../../mm-config/ops/game-get" } mm-config-lobby-group-resolve-version = { path = "../../mm-config/ops/lobby-group-resolve-version" } mm-config-namespace-get = { path = "../../mm-config/ops/namespace-get" } mm-config-version-get = { path = "../../mm-config/ops/version-get" } diff --git a/svc/pkg/mm/worker/src/workers/lobby_create/mod.rs b/svc/pkg/mm/worker/src/workers/lobby_create/mod.rs index 250f9a7894..e3b63b6892 100644 --- a/svc/pkg/mm/worker/src/workers/lobby_create/mod.rs +++ b/svc/pkg/mm/worker/src/workers/lobby_create/mod.rs @@ -67,7 +67,13 @@ async fn worker(ctx: &OperationContext) -> Globa .await; } - let (namespace, mm_ns_config, (lobby_group, lobby_group_meta, version_id), region, tiers) = tokio::try_join!( + let ( + (mm_game_config, namespace), + mm_ns_config, + (lobby_group, lobby_group_meta, version_id), + region, + tiers, + ) = tokio::try_join!( fetch_namespace(ctx, namespace_id), fetch_mm_namespace_config(ctx, namespace_id), fetch_lobby_group_config(ctx, lobby_group_id), @@ -235,6 +241,7 @@ async fn worker(ctx: &OperationContext) -> Globa runtime_meta, &namespace, &version, + &mm_game_config, &lobby_group, &lobby_group_meta, ®ion, @@ -317,15 +324,22 @@ async fn fetch_tiers( async fn fetch_namespace( ctx: &OperationContext, namespace_id: Uuid, -) -> GlobalResult { +) -> GlobalResult<(backend::matchmaker::GameConfig, backend::game::Namespace)> { let get_res = op!([ctx] game_namespace_get { namespace_ids: vec![namespace_id.into()], }) .await?; let namespace = unwrap!(get_res.namespaces.first(), "namespace not found").clone(); + let game_id = unwrap!(namespace.game_id); - Ok(namespace) + let get_res = op!([ctx] mm_config_game_get { + game_ids: vec![game_id], + }) + .await?; + let game_config = unwrap_ref!(unwrap!(get_res.games.first()).config).clone(); + + Ok((game_config, namespace)) } #[tracing::instrument] @@ -569,6 +583,7 @@ async fn create_docker_job( runtime_meta: &backend::matchmaker::lobby_runtime_meta::Docker, namespace: &backend::game::Namespace, version: &backend::game::Version, + mm_game_config: &backend::matchmaker::GameConfig, lobby_group: &backend::matchmaker::LobbyGroup, lobby_group_meta: &backend::matchmaker::LobbyGroupMeta, region: &backend::region::Region, @@ -738,6 +753,10 @@ async fn create_docker_job( key: "max_players_party".into(), value: lobby_group.max_players_party.to_string(), }, + job_run::msg::create::Parameter { + key: "root_user_enabled".into(), + value: if mm_game_config.root_user_enabled { "1" } else { "0" }.into() + }, ], job_spec_json: job_spec_json, proxied_ports: proxied_ports, diff --git a/svc/pkg/mm/worker/src/workers/lobby_create/nomad_job.rs b/svc/pkg/mm/worker/src/workers/lobby_create/nomad_job.rs index 89bcc34ad3..ba7462afb9 100644 --- a/svc/pkg/mm/worker/src/workers/lobby_create/nomad_job.rs +++ b/svc/pkg/mm/worker/src/workers/lobby_create/nomad_job.rs @@ -404,6 +404,7 @@ pub fn gen_lobby_docker_job( "max_players_normal".into(), "max_players_direct".into(), "max_players_party".into(), + "root_user_enabled".into(), ]), meta_optional: None, })),