diff --git a/multiplayer-next.md b/multiplayer-next.md index 9f04ae18..25b4cd64 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -115,6 +115,8 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md). Valve-approved SDR POP/certificate/public-UDP overlays. - [ ] Add the local-safe Agones adapter and separate process-ready (listen then Ready) from assignment-ready (Allocated manifest verified and registered). + The Go supervisor now validates dynamic address/port data and gates Ready on + an explicit probe; Godot adapter and emulator integration remain. - [ ] **IN PROGRESS:** Allocate from Ready by region/build/protocol/transport; use separately verified ENet and SDR dynamic/passthrough port mappings. The Go allocator now owns assignment publication with idempotent replay/conflict handling; diff --git a/multiplayer-todo.md b/multiplayer-todo.md index 44eba23b..23ce4418 100644 --- a/multiplayer-todo.md +++ b/multiplayer-todo.md @@ -1209,9 +1209,9 @@ the local/CI/community transport, not a silent production fallback. | # | Task | Acceptance | |---|---|---| | 8.26 `[D:8.1,8.6,8.12]` | Portable Helm/Kustomize Fleets per build/EU/NA region; isolate provider edge/network/DNS/secret and SDR POP/cert/public-UDP overlays | Two provider fixtures render; labels select region/build/protocol/transport; each fixture documents Valve approval and externally reachable UDP mapping | -| 8.27 `[D:8.26]` | **IN PROGRESS.** Go supervisor package provides local-safe Agones REST discovery, dynamic `SDR_LISTEN_PORT`/`SDR_IP` injection, explicit process-ready probing and Ready transition; direct mode bypasses Agones | `server/supervisor/` covers allocated/direct startup and dynamic endpoint/Ready ordering; Godot Agones adapter, metadata watch, Health/annotation/Shutdown and emulator integration remain | +| 8.27 `[D:8.26]` | **IN PROGRESS.** Go supervisor package provides local-safe Agones REST discovery, validates assigned address/port data, injects dynamic `SDR_LISTEN_PORT`/`SDR_IP`, performs explicit process-ready probing and Ready transition; direct mode bypasses Agones | `server/supervisor/` covers allocated/direct startup, invalid endpoint rejection, dynamic endpoint/Ready ordering and authenticated drain; Godot Agones adapter, metadata watch, Health/annotation/Shutdown and emulator integration remain | | 8.28 `[D:8.6,8.27]` | **IN PROGRESS.** Supervisor separates explicit process-ready from Agones Ready and never scrapes stdout; allocated mode refuses to mark Ready without a configured readiness probe | `server/supervisor/` tests prove Ready follows the probe and direct mode remains functional; Godot readiness endpoint, detached-container and Health-reclaim integration remain | -| 8.29 `[D:8.26,8.27]` | **IN PROGRESS.** Supervisor discovers the Agones endpoint, propagates the actual dynamic `--port`, and exports `SDR_LISTEN_PORT`/`SDR_IP` only for Hosted-SDR while preserving an isolated ENet path | `server/supervisor/` tests cover dynamic port argument/env propagation and SDR-vs-ENet separation; real Agones dynamic/passthrough mapping, POP/cert/firewall/NAT and multi-match fixture remain | +| 8.29 `[D:8.26,8.27]` | **IN PROGRESS.** Supervisor discovers and validates the Agones endpoint, propagates the actual dynamic `--port`, and exports `SDR_LISTEN_PORT`/`SDR_IP` only for Hosted-SDR while preserving an isolated ENet path | `server/supervisor/` tests cover invalid address/port rejection, dynamic port argument/env propagation and SDR-vs-ENet separation; real Agones dynamic/passthrough mapping, POP/cert/firewall/NAT and multi-match fixture remain | | 8.30 `[D:8.18,8.26,8.28,8.29]` | **IN PROGRESS.** Pure Go allocator filters Ready servers by region/build/protocol/transport, atomically claims one with idempotent allocation replay, and now owns the assignment-publication boundary | `server/domain/allocator.go` covers deterministic compatible selection, exhaustion, conflicting/identical allocation replay, unknown allocations, and assignment replay/conflict; Agones `GameServerAllocation`, signed roster metadata, bounded cross-replica retry and live integration remain | | 8.31 `[D:8.9,8.30]` | **IN PROGRESS.** Pure Go assignment gate requires Allocated state, exact allocation ID/match/server/region/build/protocol/transport compatibility, non-empty hosted endpoint and verified manifest signature before exposure; allocator publication cannot expose Ready state | `server/domain/assignment.go` and `allocator.go` plus adversarial fixtures cover early-connect, tampered signature/manifest, wrong compatibility, empty endpoint, unknown allocation and post-publication mutation rejection; Agones metadata watch, hosted-address registration, production signer and client-ticket publication remain | | 8.32 `[D:8.2,8.26,8.30]` | FleetAutoscaler with >=2 Ready processes across >=2 on-demand nodes/failure domains per queue-enabled region; pre-pull current/rollback; scale **Allocated** count to zero, never the Ready floor | Warm allocation meets p95 5 s/p99 10 s; disabled regions alone scale fully to zero; one-node loss retains certified Ready/headroom | diff --git a/server/supervisor/supervisor.go b/server/supervisor/supervisor.go index f26fab88..d20bab22 100644 --- a/server/supervisor/supervisor.go +++ b/server/supervisor/supervisor.go @@ -145,11 +145,11 @@ func (s *Supervisor) assignedEndpoint(ctx context.Context) (int, string, error) if err := s.sdkGet(ctx, "/gameserver", &server); err != nil { return 0, "", err } - if len(server.Status.Ports) == 0 || server.Status.Address == "" { + if len(server.Status.Ports) == 0 || strings.TrimSpace(server.Status.Address) == "" || strings.ContainsAny(server.Status.Address, " \t\r\n") { return 0, "", fmt.Errorf("Agones returned no assigned endpoint") } for _, port := range server.Status.Ports { - if port.Port > 0 && (port.Name == "game" || len(server.Status.Ports) == 1) { + if port.Port > 0 && port.Port <= 65535 && (port.Name == "game" || len(server.Status.Ports) == 1) { return port.Port, server.Status.Address, nil } } diff --git a/server/supervisor/supervisor_test.go b/server/supervisor/supervisor_test.go index 1951dba7..fe81cf34 100644 --- a/server/supervisor/supervisor_test.go +++ b/server/supervisor/supervisor_test.go @@ -142,3 +142,28 @@ func TestDrainRequiresAndUsesAuthenticatedLocalEndpoint(t *testing.T) { t.Fatal("unauthenticated drain was allowed") } } + +func TestAssignedEndpointRejectsMalformedAddressAndPort(t *testing.T) { + for _, response := range []string{ + `{"status":{"address":"203.0.113.9","ports":[{"name":"game","port":65536}]}}`, + `{"status":{"address":" ","ports":[{"name":"game","port":31001}]}}`, + `{"status":{"address":"203.0.113.9 bad","ports":[{"name":"game","port":31001}]}}`, + } { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/gameserver" { + _, _ = w.Write([]byte(response)) + return + } + w.WriteHeader(http.StatusOK) + })) + s, err := New(Config{Command: []string{"/bin/sh", "-c", "exit 0"}, SDKBaseURL: server.URL, ReadyURL: server.URL + "/probe", ReadyTimeout: time.Second}) + if err != nil { + server.Close() + t.Fatal(err) + } + if err := s.Start(context.Background()); err == nil { + t.Errorf("malformed endpoint was accepted: %s", response) + } + server.Close() + } +}