feat: validate allocated assignment manifest

This commit is contained in:
Josh Creek
2026-08-31 20:38:56 +01:00
parent ecc78b7a2a
commit 698413cd91
4 changed files with 36 additions and 20 deletions
+6 -2
View File
@@ -70,6 +70,8 @@ static func specs() -> Array[Spec]:
out.append(Spec.new("match-id", Kind.STRING, "", "allocation", "Opaque allocated match identifier")) out.append(Spec.new("match-id", Kind.STRING, "", "allocation", "Opaque allocated match identifier"))
out.append(Spec.new("server-id", Kind.STRING, "", "allocation", "Opaque allocated server identifier")) out.append(Spec.new("server-id", Kind.STRING, "", "allocation", "Opaque allocated server identifier"))
out.append(Spec.new("playlist-version", Kind.STRING, "", "allocation", "Matchmaking playlist contract version")) out.append(Spec.new("playlist-version", Kind.STRING, "", "allocation", "Matchmaking playlist contract version"))
out.append(Spec.new("client-build", Kind.STRING, "", "allocation", "Expected immutable client build identifier"))
out.append(Spec.new("assignment-expiry-unix", Kind.INT, 0, "allocation", "Unix expiry for the allocated assignment; must be in the future"))
out.append(Spec.new("server-image-digest", Kind.STRING, "", "allocation", "Expected immutable server image digest (sha256:...)")) out.append(Spec.new("server-image-digest", Kind.STRING, "", "allocation", "Expected immutable server image digest (sha256:...)"))
out.append(Spec.new("transport", Kind.STRING, "", "allocation", "Assigned transport: steam_sdr or enet")) out.append(Spec.new("transport", Kind.STRING, "", "allocation", "Assigned transport: steam_sdr or enet"))
out.append(Spec.new("region", Kind.STRING, "", "allocation", "Assigned region: EU or NA")) out.append(Spec.new("region", Kind.STRING, "", "allocation", "Assigned region: EU or NA"))
@@ -259,9 +261,11 @@ func _validate() -> void:
if not rotation in ["sequential", "random"]: if not rotation in ["sequential", "random"]:
errors.append("--arena-rotation must be sequential or random, got '%s'" % rotation) errors.append("--arena-rotation must be sequential or random, got '%s'" % rotation)
if bool(values["allocated-mode"]): if bool(values["allocated-mode"]):
for key in ["match-id", "server-id", "playlist-version", "server-image-digest", "transport", "region"]: for key in ["match-id", "server-id", "playlist-version", "client-build", "assignment-expiry-unix", "server-image-digest", "transport", "region"]:
if String(values[key]).is_empty(): if String(values[key]).is_empty():
errors.append("--allocated-mode requires --%s" % key) errors.append("--allocated-mode requires --%s" % key)
if int(values["assignment-expiry-unix"]) <= int(Time.get_unix_time_from_system()):
errors.append("--assignment-expiry-unix must be in the future")
var digest := String(values["server-image-digest"]) var digest := String(values["server-image-digest"])
if not _is_sha256_digest(digest): if not _is_sha256_digest(digest):
errors.append("--server-image-digest must be sha256:<64 hex characters>") errors.append("--server-image-digest must be sha256:<64 hex characters>")
@@ -307,7 +311,7 @@ static func help_text() -> String:
lines.append("") lines.append("")
lines.append("The command line overrides the config file, which overrides the defaults") lines.append("The command line overrides the config file, which overrides the defaults")
lines.append("shown below. An unknown flag is an error, not a warning.") lines.append("shown below. An unknown flag is an error, not a warning.")
var sections := ["general", "network", "match", "logging"] var sections := ["general", "network", "match", "logging", "allocation"]
var all := specs() var all := specs()
for section in sections: for section in sections:
lines.append("") lines.append("")
+9 -1
View File
@@ -136,7 +136,7 @@ func test_allocated_mode_is_opt_in_and_requires_compatibility_manifest() -> void
assert_true(not incomplete.is_valid(), "allocated mode cannot start without its manifest") assert_true(not incomplete.is_valid(), "allocated mode cannot start without its manifest")
var valid = _parse([ var valid = _parse([
"--allocated-mode", "--match-id=match_1234567890123456", "--server-id=server_1234567890123456", "--allocated-mode", "--match-id=match_1234567890123456", "--server-id=server_1234567890123456",
"--playlist-version=2026-08-31", "--server-image-digest=sha256:" + "a".repeat(64), "--playlist-version=2026-08-31", "--client-build=client-2026-08-31", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600), "--server-image-digest=sha256:" + "a".repeat(64),
"--transport=enet", "--region=EU" "--transport=enet", "--region=EU"
]) ])
assert_true(valid.is_valid(), "a complete allocated compatibility manifest is accepted: %s" % str(valid.errors)) assert_true(valid.is_valid(), "a complete allocated compatibility manifest is accepted: %s" % str(valid.errors))
@@ -145,7 +145,15 @@ func test_allocated_mode_is_opt_in_and_requires_compatibility_manifest() -> void
func test_allocated_mode_rejects_invalid_transport_region_or_digest() -> void: func test_allocated_mode_rejects_invalid_transport_region_or_digest() -> void:
var args := [ var args := [
"--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v",
"--client-build=client", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600),
"--server-image-digest=sha256:" + "g".repeat(64), "--transport=udp", "--region=AP" "--server-image-digest=sha256:" + "g".repeat(64), "--transport=udp", "--region=AP"
] ]
var config = _parse(args) var config = _parse(args)
assert_true(not config.is_valid(), "invalid compatibility values are rejected") assert_true(not config.is_valid(), "invalid compatibility values are rejected")
func test_allocated_mode_rejects_missing_or_expired_assignment_manifest_fields() -> void:
var missing = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"])
assert_true(not missing.is_valid(), "client build and expiry are required")
var expired = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--client-build=client", "--assignment-expiry-unix=1", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"])
assert_true(not expired.is_valid(), "expired assignment is rejected")
+20 -16
View File
@@ -35,19 +35,23 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
[MATCHMAKING-SLOs.md](docs/MATCHMAKING-SLOs.md). [MATCHMAKING-SLOs.md](docs/MATCHMAKING-SLOs.md).
- [x] Publish versioned OpenAPI/WebSocket contracts, stable IDs, legal state - [x] Publish versioned OpenAPI/WebSocket contracts, stable IDs, legal state
transitions, revisions and idempotency semantics ([v1 contracts](server/contracts/v1/)). transitions, revisions and idempotency semantics ([v1 contracts](server/contracts/v1/)).
- [ ] Add PostgreSQL queue ownership/active-participation fences, durable - [ ] **IN PROGRESS:** Add PostgreSQL queue ownership/active-participation
domain migrations/outbox and Redis indexes/TTLs; lost Redis writes must not fences, durable domain migrations/outbox and Redis indexes/TTLs; lost Redis
split a proposal or corrupt durable state. writes must not split a proposal or corrupt durable state. Initial migration
- [ ] Define assignment compatibility and opt-in `ServerConfig` flags whose and serializable store boundaries are implemented; live DB/cache repair gates remain.
defaults reproduce the community-server path. - [ ] **IN PROGRESS:** Define assignment compatibility and opt-in `ServerConfig`
flags whose defaults reproduce the community-server path. Allocation manifest
validation now covers client build and future expiry; signed admission remains.
## Phase 8 — identity and security ## Phase 8 — identity and security
- [ ] Validate Steam Web API tickets only in the secure backend; issue - [ ] Validate Steam Web API tickets only in the secure backend; issue
revocable sessions and reconnect-safe match/identity/slot authorisations revocable sessions and reconnect-safe match/identity/slot authorisations
with server-owned connection-generation fencing. with server-owned connection-generation fencing.
- [ ] Authenticate results with pod/GameServer-bound workload identity; make - [ ] **IN PROGRESS:** Authenticate results with pod/GameServer-bound workload
identical duplicates idempotent and conflicting results inert/alerting. identity; make identical duplicates idempotent and conflicting results
inert/alerting. Pure Go binding, hashing, reconciliation, and SQL boundaries exist;
production credential validation remains.
- [ ] Complete the threat model for forgery, replay, queue/flood/bot abuse, - [ ] Complete the threat model for forgery, replay, queue/flood/bot abuse,
workload/insider compromise, DDoS, supply chain and denial-of-wallet. workload/insider compromise, DDoS, supply chain and denial-of-wallet.
- [ ] Enforce restricted workloads/RBAC/networks/private stores/backups/secrets; - [ ] Enforce restricted workloads/RBAC/networks/private stores/backups/secrets;
@@ -58,23 +62,23 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
## Phase 8 — queues, playlists and rating ## Phase 8 — queues, playlists and rating
- [ ] Add one PostgreSQL-owned queue ticket/player with 10 s heartbeat, 30 s - [ ] **IN PROGRESS:** Add one PostgreSQL-owned queue ticket/player with 10 s
expiry, Redis candidate cache and restart/failover repair. heartbeat, 30 s expiry, Redis candidate cache and restart/failover repair.
- [ ] Validate opaque Steam ping locations and nonce-bound probes server-side; - [ ] **IN PROGRESS:** Validate opaque Steam ping locations and nonce-bound probes server-side;
require <=100 ms, enforce discrepancy quarantine and the locked widening/ require <=100 ms, enforce discrepancy quarantine and the locked widening/
region/team tie-break rules. region/team tie-break rules.
- [ ] Send 10 s proposals to every selected human: ranked six, relaxed casual - [ ] **IN PROGRESS:** Send 10 s proposals to every selected human: ranked six, relaxed casual
two to six with disclosed bots; enforce exact cooldown and queue-precedence two to six with disclosed bots; enforce exact cooldown and queue-precedence
behavior. behavior.
- [ ] Fence proposals/participants in a PostgreSQL serializable transaction; - [ ] **IN PROGRESS:** Fence proposals/participants in a PostgreSQL serializable transaction;
prove loss of an acknowledged Redis write cannot split players. prove loss of an acknowledged Redis write cannot split players.
- [ ] Casual: target 3v3 humans, after 60 s allow >=2 humans (one/team) plus - [ ] Casual: target 3v3 humans, after 60 s allow >=2 humans (one/team) plus
bots, kickoff-only human backfill and no backfill loss/decline penalty. bots, kickoff-only human backfill and no backfill loss/decline penalty.
- [ ] Ranked: exactly six humans, solo-only, no bots/backfill, random-enabled - [ ] **IN PROGRESS:** Ranked: exactly six humans, solo-only, no bots/backfill,
non-elevated arenas only, 60 s reconnect grace and escalating abandons. random-enabled non-elevated arenas only, 60 s reconnect grace and escalating abandons.
- [ ] Implement the documented exact Glicko-2 equations, fractional 3v3 - [ ] **IN PROGRESS:** Implement the documented exact Glicko-2 equations, fractional 3v3
weights, inactivity/update locking/golden vectors and ten provisional games. weights, inactivity/update locking/golden vectors and ten provisional games.
- [ ] Add ranked-only exactly-once 12-week soft seasons; distinguish retryable - [ ] **IN PROGRESS:** Add ranked-only exactly-once 12-week soft seasons; distinguish retryable
result-delivery outages from match-integrity failures and rating exemptions. result-delivery outages from match-integrity failures and rating exemptions.
## Phase 8 — Agones and regional server capacity ## Phase 8 — Agones and regional server capacity
+1 -1
View File
@@ -1173,7 +1173,7 @@ the local/CI/community transport, not a silent production fallback.
| 8.3 `[D:8.1]` | **DONE.** Publish versioned OpenAPI + WebSocket contracts for Steam login/session, profile/rating, queue create/heartbeat/cancel/resume, proposal accept/decline, assignment/status, server registration/roster/result/shutdown | [`server/contracts/v1/`](server/contracts/v1/) contains machine-readable REST/events contracts and dependency-free structural tests; REST resync is specified by the contract | | 8.3 `[D:8.1]` | **DONE.** Publish versioned OpenAPI + WebSocket contracts for Steam login/session, profile/rating, queue create/heartbeat/cancel/resume, proposal accept/decline, assignment/status, server registration/roster/result/shutdown | [`server/contracts/v1/`](server/contracts/v1/) contains machine-readable REST/events contracts and dependency-free structural tests; REST resync is specified by the contract |
| 8.4 `[D:8.3]` | **DONE.** Define opaque IDs, legal queue/match state transitions, revisions and idempotency keys | [`server/contracts/v1/state-transitions.json`](server/contracts/v1/state-transitions.json) locks terminal states, legal edges, stale-revision handling and same-key replay/conflict behavior; contract tests cover the invariants | | 8.4 `[D:8.3]` | **DONE.** Define opaque IDs, legal queue/match state transitions, revisions and idempotency keys | [`server/contracts/v1/state-transitions.json`](server/contracts/v1/state-transitions.json) locks terminal states, legal edges, stale-revision handling and same-key replay/conflict behavior; contract tests cover the invariants |
| 8.5 `[D:8.4]` | **IN PROGRESS.** Initial PostgreSQL migration now defines durable idempotency keys, queue ownership/active-participation fencing, identities, sessions/revocations, ranked seasons, ratings/events, matches/participants, penalties, results, audits and outbox | `server/migrations/0001_initial.sql` and static checks cover the durable tables, uniqueness/check constraints and Redis-as-cache boundary; live PostgreSQL up/rollback/forward migration, serializable adapters and cache-loss repair remain | | 8.5 `[D:8.4]` | **IN PROGRESS.** Initial PostgreSQL migration now defines durable idempotency keys, queue ownership/active-participation fencing, identities, sessions/revocations, ranked seasons, ratings/events, matches/participants, penalties, results, audits and outbox | `server/migrations/0001_initial.sql` and static checks cover the durable tables, uniqueness/check constraints and Redis-as-cache boundary; live PostgreSQL up/rollback/forward migration, serializable adapters and cache-loss repair remain |
| 8.6 `[D:8.3,8.4]` | **IN PROGRESS.** Add allocated-mode `ServerConfig` compatibility fields as opt-in defaults | `ServerConfig` now validates allocation mode, match/server IDs, playlist version, image digest, transport and EU/NA region; client-build/expiry/signed-authorisation admission and full manifest tests remain | | 8.6 `[D:8.3,8.4]` | **IN PROGRESS.** Add allocated-mode `ServerConfig` compatibility fields as opt-in defaults | `ServerConfig` now validates allocation mode, match/server IDs, playlist version, client build, future assignment expiry, image digest, transport and EU/NA region; signed-authorisation admission and full manifest/runtime tests remain |
#### 8B — Authentication and secure control plane #### 8B — Authentication and secure control plane