| 1 | package edge |
| 2 | |
| 3 | // The steering probe: the small 200 that decides whether DNS steering's |
| 4 | // rung-2 withdrawal is real or decorative (spec: |
| 5 | // docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md, Part 4). |
| 6 | // |
| 7 | // The converger (internal/steering/route53.go) binds every latency record it |
| 8 | // writes to a Route53 health check that probes the edge's EIP over HTTPS |
| 9 | // with SNI edge.<apps-domain> and GET /. That check is the ONLY thing that |
| 10 | // withdraws a dead edge's answers, so it has to pass on a live box — and on |
| 11 | // an unmodified edge it could not, twice over: |
| 12 | // |
| 13 | // - TLS: edge.<apps-domain> is one label under the apps domain, and every |
| 14 | // wildcard this edge holds is a per-ACCOUNT parent one label DEEPER |
| 15 | // (*.<acct>.<apps-domain>). No cert covered the name, so the probe died |
| 16 | // at the handshake and every record looked dead. The fix is in |
| 17 | // wildcard.go: with steering armed, the apps domain itself joins the |
| 18 | // wildcard parent set, so *.<apps-domain> covers exactly this name, |
| 19 | // issued over DNS-01. The narrow alternative — a per-host exception for |
| 20 | // the single name — was rejected on three counts (the full argument is |
| 21 | // at the arming site in wildcard.go): it would have to breach the |
| 22 | // exact-registry Allowed rule (PR #180) in TWO places, issuerHostPolicy |
| 23 | // AND the mirror's m.allowed bound on the live challenge read-through; |
| 24 | // with the name resolving to every armed edge, the CA reaches an |
| 25 | // arbitrary one, so issuance would hang on the cross-box challenge |
| 26 | // mirror; and every attempt is paid for in failed health checks and in |
| 27 | // the shared ACME account's failed-validation budget. NOT because an |
| 28 | // order could not complete — autocert orders under context.Background() |
| 29 | // with a 5-minute budget (autocert.go:274), so a checker that hangs up |
| 30 | // on the handshake does not cancel the order it triggered. |
| 31 | // |
| 32 | // - HTTP: the hostname is not a registry route, so it fell through to |
| 33 | // unknownHost — a 404, or on a -fallback edge (every flagship box) a |
| 34 | // 302, which Route53 counts as HEALTHY. A false-healthy check is worse |
| 35 | // than no check: it never withdraws anything, and nothing in the journal |
| 36 | // says so. Hence the answer below, placed AHEAD of every other serving |
| 37 | // path. (Since #174 the name, one label under the apps domain, would get |
| 38 | // the pending page's 503 instead, which Route53 scores unhealthy on |
| 39 | // every edge: wrong the other way. The probe still answers first.) |
| 40 | // |
| 41 | // Inert unless the deployment steers: steeringProbeHostFromEnv returns "" |
| 42 | // unless CARLOS_STEERING_ZONE_ID is set AND usable — the same read, with the |
| 43 | // same validation, that the converger performs — so on every box today the |
| 44 | // probe is one comparison against an empty string and nothing else changes. |
| 45 | // PLATFORM_APPS_DOMAIN alone is NOT the gate: both production deployments |
| 46 | // set it, and gating there would answer for a hostname no zone steers and |
| 47 | // order a wildcard nobody asked for. |
| 48 | |
| 49 | import ( |
| 50 | "io" |
| 51 | "log" |
| 52 | "net/http" |
| 53 | "os" |
| 54 | "strings" |
| 55 | |
| 56 | "github.com/carlosframework/platform/internal/steering" |
| 57 | ) |
| 58 | |
| 59 | // steeringProbeBody is what the health check reads. Deliberately tiny and |
| 60 | // deliberately not a status page: it says the edge answered, nothing about |
| 61 | // what it serves or who it serves it for. |
| 62 | const steeringProbeBody = "carlos steering probe ok\n" |
| 63 | |
| 64 | // steeringProbeHostFromEnv derives this deployment's steering hostname, or "" |
| 65 | // when the deployment does not steer. Read once, at Edge construction (the |
| 66 | // gzipEnabled precedent), because a hostname that changes under a running |
| 67 | // edge is a redeploy, not a reload. |
| 68 | func steeringProbeHostFromEnv() string { |
| 69 | if steering.SteeringZoneID() == "" { |
| 70 | return "" |
| 71 | } |
| 72 | domain := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(os.Getenv("PLATFORM_APPS_DOMAIN")), "."))) |
| 73 | if domain == "" { |
| 74 | // Armed but with nothing to build the name from. The converger says |
| 75 | // the same thing on its own pass (desiredState mints no steering |
| 76 | // hostname without an apps domain); saying it here too is what tells |
| 77 | // an operator reading the EDGE's journal why no probe is answered. |
| 78 | log.Printf("edge: steering zone is set but PLATFORM_APPS_DOMAIN is not — no steering hostname to answer; health checks on this box will fail until it is set") |
| 79 | return "" |
| 80 | } |
| 81 | host := "edge." + domain |
| 82 | log.Printf("edge: steering probe armed for %s (GET / answers 200; every other path on that host routes normally)", host) |
| 83 | return host |
| 84 | } |
| 85 | |
| 86 | // steeringProbe answers the deployment's Route53 health check, reporting |
| 87 | // whether it did. |
| 88 | // |
| 89 | // Narrow on purpose, in all three axes: |
| 90 | // |
| 91 | // - the HOST must be the steering hostname exactly (case-insensitively, |
| 92 | // trailing dot tolerated — DNS names are case-insensitive and a client |
| 93 | // may send either form); |
| 94 | // - the PATH must be exactly "/" — the health check's own path |
| 95 | // (healthCheckPath, internal/steering/route53.go). Anything else on that |
| 96 | // host carries on to the ordinary routing path, so this is a reserved |
| 97 | // ANSWER, not a reserved hostname; |
| 98 | // - the METHOD must be GET or HEAD. Nothing here mutates, so anything else |
| 99 | // falls through rather than being handled. |
| 100 | // |
| 101 | // Callers place it before every other branch — including the fallback |
| 102 | // redirect, which would otherwise 302 the check into false health. |
| 103 | func (e *Edge) steeringProbe(w http.ResponseWriter, r *http.Request, host string) bool { |
| 104 | if e.steeringHost == "" { |
| 105 | return false |
| 106 | } |
| 107 | if !strings.EqualFold(strings.TrimSuffix(host, "."), e.steeringHost) { |
| 108 | return false |
| 109 | } |
| 110 | if r.URL.Path != "/" || (r.Method != http.MethodGet && r.Method != http.MethodHead) { |
| 111 | return false |
| 112 | } |
| 113 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
| 114 | // A health check must never be answered from anyone's cache: the whole |
| 115 | // point of the probe is that it reflects THIS box, right now. |
| 116 | w.Header().Set("Cache-Control", "no-store") |
| 117 | w.WriteHeader(http.StatusOK) |
| 118 | if r.Method == http.MethodGet { |
| 119 | _, _ = io.WriteString(w, steeringProbeBody) |
| 120 | } |
| 121 | return true |
| 122 | } |
| 123 | |