-
-
Notifications
You must be signed in to change notification settings - Fork 3
Configuration
Nimbus.Proxy reads nimbus.proxy.toml from the same directory as the executable. If the file is
missing it is written with defaults on startup, and the proxy carries on and starts: the file a
first run writes passes the validation that reads it back, so nothing has to be edited before the
proxy will listen.
Two defaults in that file are chosen for the machine rather than shipped as literals. The registry
binds on loopback, and registry.embedded_shared_secret is generated on the spot, with a comment
above it saying where the copies go. Both are covered in the registry section below.
Most sections reload at runtime without restarting:
nimctl reload| Reloadable at runtime | Requires restart |
|---|---|
[servers], try, proxy_protocol_servers, [transfers], [whitelist], [logging], [status], [plugins], [advanced]
|
bind, [admin], [registry], [metrics], [persistence]
|
/nimbus reload on a backend is a different command with a similar name: it reloads that
backend's nimbus-server.json, not the proxy's config. See Server Mod.
bind = "0.0.0.0:42420"
[servers]
hub = "127.0.0.1:42421"
factions = "127.0.0.1:42422"
survival = "127.0.0.1:42423"
try = ["hub", "survival"]
proxy_protocol_servers = []| Key | Default | Description |
|---|---|---|
bind |
0.0.0.0:42420 |
TCP and UDP address players connect to. |
[servers] |
- | Named backend map. Key = server ID used everywhere. Value = "host:port". |
try |
[] |
Ordered list of server IDs to attempt on initial connect. First healthy one wins. Empty means all servers in definition order. |
proxy_protocol_servers |
[] |
Server IDs that receive a PROXY Protocol v2 header before the first upstream byte. The backend must support PROXY v2 to use this. See Forwarding. |
Leave the seamless switches alone, only redirect is currently supported. redirect_address is
the one key here worth setting.
[transfers]
default_mode = "redirect"
allow_seamless = false
require_seamless_capability = true
fallback_to_redirect_when_seamless_unavailable = true
redirect_address = ""
enable_unsafe_seamless_splice = false| Key | Default | Description |
|---|---|---|
default_mode |
redirect |
Default transfer mode when not specified. redirect or seamless. |
allow_seamless |
false |
Master switch for seamless transfers. Must be true to use the visual seamless path. |
require_seamless_capability |
true |
When true, seamless is only used for sessions where the player has Nimbus.Client installed. |
fallback_to_redirect_when_seamless_unavailable |
true |
When seamless is requested but the client lacks the mod, fall back to redirect instead of failing. |
redirect_address |
"" |
"host" or "host:port" stamped into forged redirect packets. Set it to the proxy's own player-facing address. Empty keeps the old behaviour of stamping the target backend's PublicHost. See Transfers. |
enable_unsafe_seamless_splice |
false |
Enables raw TCP upstream splice. Experimental - has caused atlas and tessellation crashes in VS. Do not enable on production servers. |
redirect_address exists because of a client that does not exist yet. Today's RedirectFix clients
ignore the stamped host and reconnect to the proxy address they already had, so either value
works. A vanilla client with the redirect crash fixed will dial the stamped host literally, and if
that host is the backend, the player leaves the proxy behind and lands on the backend directly,
past reservation gating and everything else the proxy does. Setting this to the proxy keeps such a
client on the proxy, where the staged sticky route sends it to the right backend. The value is
validated at startup and must be host or host:port.
[admin]
bind = "127.0.0.1:42499"
secret = ""
enabled = true
granted_permissions = ["*"]| Key | Default | Description |
|---|---|---|
bind |
127.0.0.1:42499 |
Address the admin socket listens on. Keep on localhost unless you have an authenticated reverse proxy in front. |
secret |
"" |
Optional shared secret. If non-empty, the admin client must provide it. Empty means no auth (rely on the localhost binding). |
enabled |
true |
Set false to disable the admin socket entirely. |
granted_permissions |
["*"] |
Permission nodes granted after auth. "*" grants all commands. |
[registry]
mode = "embedded"
proxy_id = "nimbus-proxy"
# embedded mode
embedded_bind = "http://127.0.0.1:8765"
embedded_shared_secret = "<generated on first run>"
embedded_state_dir = "."
# remote mode
url = ""
shared_secret = ""
# tuning (applies to both modes)
backend_stale_seconds = 20
backend_drop_seconds = 120
max_reservation_ttl_seconds = 300
fail_on_error = true
transfer_intent_poll_ms = 1000
# scoped API tokens, embedded mode only
api_tokens_enabled = false
api_tokens_rate_limit_per_minute = 60
api_tokens_trust_forwarded_proto = false| Key | Default | Description |
|---|---|---|
mode |
embedded |
embedded - registry runs in-process. remote - connect to a standalone Nimbus.Registry. disabled - no registry; single-backend setups only. |
proxy_id |
nimbus-proxy |
Identifier for this proxy instance, used in transfer intent routing. |
embedded_bind |
http://127.0.0.1:8765 |
Kestrel bind URL for the embedded registry HTTP API. Loopback by default, which nothing off the box can reach; widen it to http://0.0.0.0:8765 for backends on other hosts. Set to "" to disable the HTTP listener entirely - the proxy keeps its in-process registry path either way, and external backends have nowhere to heartbeat. |
embedded_shared_secret |
generated on first run | HMAC secret. Must match SharedSecret in every backend's nimbus-server.json. A first run mints one for this install rather than shipping a literal, so there is nothing to change here unless you are rotating it. |
embedded_state_dir |
. |
Where the embedded registry keeps nimbus.bans.json, nimbus.whitelist.json and nimbus.tokens.json, so bans, whitelist entries and issued tokens survive a restart. Relative paths resolve next to the executable. |
url |
- | Remote registry URL. Only used when mode = "remote". |
shared_secret |
- | HMAC secret for remote registry. |
fail_on_error |
true |
When true, a reservation mint failure aborts the transfer rather than proceeding without one. |
transfer_intent_poll_ms |
1000 |
How often the proxy polls the registry for pending transfer intents. Below 250 is clamped to 250. |
api_tokens_enabled |
false |
Master switch for scoped bearer tokens. false refuses bearer authentication outright, whatever tokens exist. Minting tokens with nimctl token create works either way; only authenticating with one needs this. |
api_tokens_rate_limit_per_minute |
60 |
Per-token budget, on top of any per-IP limiting in front. Must be greater than zero. |
api_tokens_trust_forwarded_proto |
false |
Makes the registry believe an X-Forwarded-Proto header on a plain-HTTP bind. Only set it when a TLS-terminating reverse proxy is the sole route to embedded_bind, because anything that can reach the bind directly can write that header itself. |
The three api_tokens_* keys apply to the embedded registry only. In remote mode the standalone
registry's own [api_tokens] section decides, and the proxy warns that the switch you flipped
changed nothing.
Those two are one decision, not two. The proxy refuses to start on an embedded_bind other
hosts can reach while embedded_shared_secret is still one of the documented placeholders, since
anyone able to reach the registry could otherwise mint themselves a reservation onto any backend.
Every literal this project has ever printed counts as a placeholder, change-me-and-keep-secret
and the wiki's older REPLACE_ME_WITH_A_LONG_RANDOM_STRING included, and so does an empty string.
The list is shared, so the standalone registry and the backend mod refuse exactly the same values:
a literal the proxy rejects and a backend accepts would be a network held together by a secret one
of its three components considers unset.
api_tokens_enabled = true on a non-loopback plain-HTTP bind is a warning rather than a refusal.
Nothing breaks, which is why it is worth a line: bearer auth simply refuses every request, because
a token is only as safe as the transport under it.
embedded (default) - the proxy hosts the registry in-process. No extra process to run. External backends heartbeat over HTTP to embedded_bind. The proxy itself talks to the registry directly (no HTTP round-trip, no HMAC overhead).
remote - the proxy connects to a standalone Nimbus.Registry process. Use this when running multiple proxy instances that need to share state, or when you want the control plane on a separate host. That process reads its own nimbus.registry.toml, where the same settings are spelled bind_url, shared_secret, state_dir and an [api_tokens] section with enabled, rate_limit_per_minute and trust_forwarded_proto. It generates its own shared_secret on a first run in the same way the proxy does, and warns rather than refuses when one is left as a placeholder: a refusal there takes the whole network down instead of keeping it from opening.
disabled - no registry. Transfers, reservations, and forwarding are unavailable. Only makes sense for single-backend setups.
[whitelist]
network = false
servers = []
fail_open_until_first_sync = false| Key | Default | Description |
|---|---|---|
network |
false |
true closes the whole network: only players with a whitelist entry get past the proxy door. |
servers |
[] |
Server IDs closed to unlisted players even when the network as a whole is open. The staff or build server sitting inside a public network. A backend configured as host:port with no ID can only be gated by network, since there is no ID for this list to name it by. |
fail_open_until_first_sync |
false |
Cold start only. true lets players in unchecked until the proxy has read the list from the registry once. The default refuses gated joins until then. |
These switches decide where a whitelist entry is required. The list itself lives in the registry
and is managed with nimctl whitelist add / remove / list; see Admin Commands.
Nothing is inferred from the list. An empty whitelist with network = true means nobody gets in,
which is the only reading that does not turn "the last entry was just removed" into "the door is
now open to everyone". It also has to work this way because the registry has no idea which
backends a given proxy is gating.
Enforcement needs a registry to read the list from. The proxy refuses to start with network = true or a non-empty servers while registry.mode = "disabled", and warns when servers names
a backend that is not in [servers].
fail_open_until_first_sync exists because a freshly started proxy that has never reached the
registry cannot tell an empty whitelist from an unread one. Left at the default it keeps the door
shut and logs a warning; set to true it warns at startup instead and lets everyone in until the
registry answers once. This only ever applies before the first successful fetch. After that, an
outage leaves the last known list in force.
[logging]
verbose = false
sniff_frames = false
log_traffic_bytes = false| Key | Default | Description |
|---|---|---|
verbose |
false |
Enables debug output (pump exits, frame sniffer detail, internal state transitions). Very noisy - for troubleshooting only. |
sniff_frames |
false |
Logs every VS frame header passing through the sniffer. Only has output when verbose = true. |
log_traffic_bytes |
false |
Logs per-session byte counts when a session ends. Useful for spotting a backend that is streaming far more than its neighbours. |
[metrics]
enabled = true
bind = "http://127.0.0.1:42500"
path = "/metrics"
status_api = true
status_api_token = ""| Key | Default | Description |
|---|---|---|
enabled |
true |
Serves the metrics host at bind. |
bind |
http://127.0.0.1:42500 |
Host for both /metrics and /status. |
path |
/metrics |
Where the Prometheus text endpoint lives. |
status_api |
true |
Read-only JSON network status at /status on the same host, for game panels and dashboards: configured pool, live registry snapshot, drained backends. |
status_api_token |
"" |
Optional bearer token for /status. Empty leaves it open, which is fine on the loopback default; set it before exposing the bind beyond localhost. Send it as Authorization: Bearer <token>. A ?token= query fallback exists for panels that cannot set headers, but query strings end up in access logs. |
Prometheus counters include: sessions accepted/closed, backend connect attempts/successes/failures, redirect and seamless counts, bytes proxied, transfer intents dispatched, drained backends, and joins rejected by the ban list and the whitelist.
[status]
enabled = true
name = "Nimbus"
motd = "Vintage Story proxy"
game_mode = "survival"
password = false
server_version = ""
max_players = 100
query_timeout_ms = 1500Controls what players see when their VS client sends a server-list ping to the proxy address. When the registry is running, live player counts come from the backend snapshot. server_version left empty means Nimbus forwards the version reported by the first backend to respond.
[plugins]
enabled = true
directory = "plugins"
disabled = []| Key | Default | Description |
|---|---|---|
enabled |
true |
Set false to skip all plugin loading. |
directory |
plugins |
Path to the plugins folder. Relative paths resolve from the executable directory. |
disabled |
[] |
Plugin IDs to skip on load. Useful for temporarily disabling one plugin without removing it. |
[persistence]
persist_drain_flags = true
drain_flags_file = "nimbus.drain-state.json"Drain flags (which backends are out of rotation) survive a proxy restart when persist_drain_flags = true. The state file is written next to the executable by default.
[advanced]
connect_timeout_ms = 5000
buffer_size = 16384| Key | Default | Description |
|---|---|---|
connect_timeout_ms |
5000 |
Milliseconds before an upstream TCP connect attempt is abandoned and the next failover candidate is tried. |
buffer_size |
16384 |
Per-pump byte buffer size. Changing this rarely helps. |