Skip to content

Commit 9e2018e

Browse files
Drop decommissioned us-west region and verify node identity in health check
cdn-2.runonflux.io was shut down for non-payment and the hosting provider re-leased its IP, 107.175.82.227, to another tenant. Every US client subnet was still being handed that address: US-West 23.235.32.0/24 -> 107.175.82.227 US-East 4.14.0.0/24 -> 107.175.82.227 EU 159.195.85.0/24 -> 159.195.85.44 Asia 180.188.197.0/24 -> 180.188.197.165 The new occupant answers 443 with a Let's Encrypt certificate for an unrelated domain, so US visitors to images.runonflux.io and cdn.runonflux.io got a dropped connection, and flux_configd's fetch_params mirror failed on US nodes. Remove the region from both the production (cdn-geo) and staging (cdn-geodev) zone configs. The deeper problem is that this never failed over. ifportup(443, ...) only establishes that something accepts TCP on 443, and the new tenant runs nginx there, so PowerDNS went on marking the node healthy indefinitely. A server that had simply been powered off would have been removed automatically; one replaced by a stranger's was not. Switch to ifurlup() against a /health endpoint added to every CDN vhost in flux-cdn-deploy, matching a distinctive token in the body. The check now answers "is this still our node" rather than "is a socket open". Details worth noting: - ifurlup() takes a list of address SETS where ifportup() took a flat list. All candidates go in one set, so the selector still chooses among every healthy node. - The probe URL is cdn.runonflux.io, not the per-zone hostname. It has to be a vhost every candidate serves, and cdn-geodev.runonflux.io is not served by cdn-1 or cdn-3 - using it would mark every node down and take the zone out entirely. - timeout goes 2s -> 3s: the old check timed a TCP connect, this also completes a TLS handshake and an HTTP GET, and EU->Hong Kong is about three round trips. minimumFailures=3 still absorbs transient overruns. - backupSelector stays pickclosest, so if every node were to fail the check the zone degrades to nearest-node rather than returning nothing. Verified by rendering both environments and exercising the result under a real Lua interpreter with the PowerDNS globals stubbed: URL, set structure, per-function selectors, and that the shared option table is copied rather than mutated between calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dd02cc3 commit 9e2018e

2 files changed

Lines changed: 78 additions & 29 deletions

File tree

templates/geo_routing.lua.j2

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,54 @@ function getAllServerIPs()
3333
end
3434

3535

36+
-- Health check settings, shared by every routing function below.
37+
--
38+
-- ifurlup() rather than ifportup(): a port check only establishes that
39+
-- something is listening on 443. When cdn-2 was shut down for non-payment, its
40+
-- provider re-leased the IP to a tenant who also ran nginx there, so the port
41+
-- check passed and US traffic kept being routed to a stranger's server. Fetching
42+
-- a page we serve and matching a token in it asks the question that matters -
43+
-- is this still our node - instead of merely whether a socket is open.
44+
--
45+
-- Note the address argument differs between the two: ifportup() takes a flat
46+
-- list, ifurlup() takes a list of address SETS and uses the first set with an
47+
-- available member. All candidates go in one set here, so the selector chooses
48+
-- among every healthy node exactly as before.
49+
local HEALTH_URL = "{{ powerdns.geo_health_check.url }}"
50+
local HEALTH_OPTS = {
51+
stringmatch = "{{ powerdns.geo_health_check.stringmatch }}",
52+
-- Higher than the old port check: that timed a TCP connect, this must also
53+
-- complete a TLS handshake and an HTTP GET, and EU->Hong Kong runs about
54+
-- three round trips. minimumFailures below absorbs the occasional overrun.
55+
timeout = 3,
56+
minimumFailures = 3, -- 3 consecutive failures before marking as down
57+
interval = 15 -- Check every 15 seconds
58+
}
59+
60+
-- Build the option table for a given selector without mutating the shared one.
61+
local function healthOpts(selector, backupSelector)
62+
local opts = {}
63+
for k, v in pairs(HEALTH_OPTS) do
64+
opts[k] = v
65+
end
66+
opts.selector = selector
67+
if backupSelector then
68+
opts.backupSelector = backupSelector
69+
end
70+
return opts
71+
end
72+
73+
3674
-- Main geo-routing function called by PowerDNS
3775
function geoRoute()
3876
local all_ips = getAllServerIPs()
3977

40-
-- Health check with working parameters
41-
local available_ips = ifportup(443, all_ips, {
42-
timeout = 2, -- 2 seconds connection timeout
43-
minimumFailures = 3, -- 3 consecutive failures before marking as down
44-
interval = 15, -- Check every 15 seconds
45-
selector = 'pickclosest', -- Use geographic selection for healthy servers
46-
backupSelector = 'pickclosest' -- Use geographic selection even when all appear down
47-
})
78+
-- pickclosest as backupSelector too, so that if every node fails the check
79+
-- we still answer with the geographically nearest rather than at random.
80+
-- ifurlup falls back to backupSelector instead of returning nothing, so a
81+
-- broken health check degrades routing quality without taking the zone out.
82+
local available_ips = ifurlup(HEALTH_URL, {all_ips},
83+
healthOpts('pickclosest', 'pickclosest'))
4884

4985
return available_ips
5086
end
@@ -54,13 +90,8 @@ end
5490
function geoRouteWeighted()
5591
local all_ips = getAllServerIPs()
5692

57-
-- Health check with same parameters as above
58-
local available_ips = ifportup(443, all_ips, {
59-
timeout = 2,
60-
minimumFailures = 3,
61-
interval = 15,
62-
selector = 'all' -- Return ALL healthy servers, not just one
63-
})
93+
-- selector 'all' returns every healthy server, not just one
94+
local available_ips = ifurlup(HEALTH_URL, {all_ips}, healthOpts('all'))
6495

6596
-- Return weighted selection based on geographic distribution
6697
return pickwrandom(available_ips)
@@ -79,12 +110,7 @@ end
79110
-- Function to get server status (for monitoring/debugging)
80111
function getServerStatus()
81112
local all_ips = getAllServerIPs()
82-
local available_ips = ifportup(443, all_ips, {
83-
timeout = 2,
84-
minimumFailures = 3,
85-
interval = 15,
86-
selector = 'all'
87-
})
113+
local available_ips = ifurlup(HEALTH_URL, {all_ips}, healthOpts('all'))
88114

89115
local status_parts = {}
90116
local up_count = 0

vars.yaml

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ powerdns:
2121
jammy: "5.0.6-1pdns.ubuntu22.04"
2222
cert_server_ip: "10.100.0.172"
2323

24+
# Health check behind the geo-routing Lua (see templates/geo_routing.lua.j2).
25+
#
26+
# This replaced ifportup(443, ...), which only proved that something accepted
27+
# TCP on 443. When cdn-2 was shut down for non-payment its provider re-leased
28+
# the IP to another tenant who also ran nginx on 443, so the check kept
29+
# reporting the node healthy and US traffic was routed to a stranger's server
30+
# for as long as it took someone to notice. Matching a token we serve makes the
31+
# check answer "is this still our node", not merely "is a socket open".
32+
geo_health_check:
33+
# Probed against each candidate IP in turn, with this hostname as Host/SNI.
34+
# It must be a vhost that every candidate node serves. cdn.runonflux.io (the
35+
# cdn-aliases vhost) qualifies in both environments; the per-zone names do
36+
# not - cdn-geodev.runonflux.io is not served by cdn-1 or cdn-3, so using it
37+
# here would mark every node down and take the zone out entirely.
38+
url: "https://cdn.runonflux.io/health"
39+
# Token from cdn-locations.conf.j2 in flux-cdn-deploy. Deliberately
40+
# distinctive so a recycled IP cannot satisfy the check by accident.
41+
stringmatch: "flux-cdn-healthy"
42+
2443
staging:
2544
zone_configs:
2645
- domain: "cdn-geodev.runonflux.io"
@@ -29,15 +48,17 @@ powerdns:
2948
default_ttl: "300"
3049
geo_routing: true
3150
lua_routing: false
51+
# No us-west region: cdn-2.runonflux.io (107.175.82.227) was removed on
52+
# 2026-07-29 after the server was shut down for non-payment and the
53+
# provider re-leased its IP to another tenant. US clients now resolve
54+
# to whichever of the remaining nodes pickclosest favours. Restoring a
55+
# US region means adding it back here with a new address - do not
56+
# reuse the old one.
3257
geo_regions:
3358
- name: "eu-central"
3459
description: "Germany EU"
3560
server: "cdn-1.runonflux.io"
3661
ip: "159.195.85.44"
37-
- name: "us-west"
38-
description: "West Coast USA"
39-
server: "cdn-2.runonflux.io"
40-
ip: "107.175.82.227"
4162
- name: "as-east"
4263
description: "Hong Kong Asia"
4364
server: "cdn-3.runonflux.io"
@@ -69,15 +90,17 @@ powerdns:
6990
default_ttl: "300"
7091
geo_routing: true
7192
lua_routing: false
93+
# No us-west region: cdn-2.runonflux.io (107.175.82.227) was removed on
94+
# 2026-07-29 after the server was shut down for non-payment and the
95+
# provider re-leased its IP to another tenant. US clients now resolve
96+
# to whichever of the remaining nodes pickclosest favours. Restoring a
97+
# US region means adding it back here with a new address - do not
98+
# reuse the old one.
7299
geo_regions:
73100
- name: "eu-central"
74101
description: "Germany EU"
75102
server: "cdn-1.runonflux.io"
76103
ip: "159.195.85.44"
77-
- name: "us-west"
78-
description: "West Coast USA"
79-
server: "cdn-2.runonflux.io"
80-
ip: "107.175.82.227"
81104
- name: "as-east"
82105
description: "Hong Kong Asia"
83106
server: "cdn-3.runonflux.io"

0 commit comments

Comments
 (0)