fix: validate Agones assigned endpoints

This commit is contained in:
Josh Creek
2026-08-31 21:48:00 +01:00
parent 307828ff7f
commit faede927fc
4 changed files with 31 additions and 4 deletions
+2
View File
@@ -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;
+2 -2
View File
@@ -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 |
+2 -2
View File
@@ -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
}
}
+25
View File
@@ -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()
}
}