phase: 32_gateway_resolved_target_liveness

Fixture and live stream.conf are byte-identical (modulo header comment). Let me test the parser directly:
This commit is contained in:
2026-10-26 14:03:42 -04:00
parent 25fd74b7c5
commit 51772ffef2
58 changed files with 5272 additions and 478 deletions
+741 -26
View File
@@ -18,13 +18,18 @@ outside the two configs is *reported*, because it no longer is —
"""
from __future__ import annotations
import ipaddress
import json
import socket as socket_module
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any
from dashboards.sources import gateway
#: This repo's recorded captures, for the pins that read a fixture directly.
FIXTURES = Path(__file__).resolve().parent / "fixtures"
# --------------------------------------------------------------------------- #
# synthetic builders: a section exactly as the agent serves it, minus the file
# --------------------------------------------------------------------------- #
@@ -111,10 +116,31 @@ def conn(local: tuple[str, int], remote: tuple[str, int], *,
"remote": {"addr": remote[0], "port": remote[1]}}
def resolver_section(entries: Sequence[dict[str, Any]] = (), *, available: bool | None = True,
via: str | None = "libc", config_directive: str | None = "10.1.0.1",
ts: float = 1_760_000_000.0,
errors: Sequence[str] = ()) -> dict[str, Any]:
"""Agent 1.3.0's fourth section, in the shape `host_agent.Collector.resolver` publishes."""
return {"available": available, "via": via, "config_directive": config_directive, "ts": ts,
"entries": list(entries), "errors": list(errors)}
def resolved(name: str, port: int | None, addresses: Sequence[str], *,
status: str = "ok", ts: float | None = 1_760_000_000.0) -> dict[str, Any]:
"""One resolver entry: what libc answered for `(name, port)`, and when."""
return {"name": name, "port": port, "status": status, "addresses": list(addresses), "ts": ts}
def snapshot(*, haproxy: dict[str, Any] | None = None, nginx: dict[str, Any] | None = None,
sockets: dict[str, Any] | None = None, errors: Sequence[str] = (),
sockets: dict[str, Any] | None = None, resolver: dict[str, Any] | None = None,
errors: Sequence[str] = (),
agent_version: str | None = "1.2.0") -> dict[str, Any]:
"""The document one agent `/snapshot` would answer, with only what a test names."""
"""The document one agent `/snapshot` would answer, with only what a test names.
No `resolver` key is the default, because that is the arm most of this file predates: a 1.2.0
agent, or a 1.3.0 one run with `--no-resolve`. A test that wants a name to be countable passes
`resolver=resolver_section([...])` and says so.
"""
doc: dict[str, Any] = {"ok": True, "schema": 1, "ts": 1_760_000_000.0, "host": "gateway",
"errors": list(errors)}
if agent_version is not None:
@@ -125,6 +151,8 @@ def snapshot(*, haproxy: dict[str, Any] | None = None, nginx: dict[str, Any] | N
doc["nginx"] = nginx
if sockets is not None:
doc["sockets"] = sockets
if resolver is not None:
doc["resolver"] = resolver
return doc
@@ -162,6 +190,52 @@ def out_metrics(payload: Mapping[str, Any]) -> list[str]:
return [row["metric"] for row in payload["connections"] if row["metric"] is not None]
def endpoint_keys(row: Mapping[str, Any]) -> set[tuple[str, int]]:
"""The normalised `(address, port)` keys one route reaches, derived **in the test**.
Read straight off `out.address` / `out.resolved` with `ipaddress`, deliberately not by calling
`gateway.endpoint_of`: a total a test compares against must not come from the function under
test. The normalisation is the same one the join makes, because `::ffff:10.1.2.64` and
`10.1.2.64` are one endpoint and a ledger that counted them apart would reconcile to nothing.
"""
out = row["out"]
port = out.get("port")
if not isinstance(port, int):
return set()
spellings = [out["host"]] if out.get("address") is True else list(out.get("resolved") or [])
keys: set[tuple[str, int]] = set()
for spelling in spellings:
try:
address = ipaddress.ip_address(str(spelling))
except ValueError:
continue
mapped = getattr(address, "ipv4_mapped", None)
keys.add((str(mapped if mapped is not None else address), port))
return keys
def endpoint_groups(payload: Mapping[str, Any]) -> dict[tuple[str, int], list[Mapping[str, Any]]]:
"""The route lines grouped by the **distinct endpoint** they reach, keyed by min(key).
Lines whose key sets are identical share a group, which is every case a capture can produce
(one address per name, or one name written by two vhosts). `gateway._endpoint_groups` merges
*overlapping* sets as well, for a multi-homed name sharing one address with a single-homed one;
no capture has that shape, and if one ever does this helper disagrees by keeping two groups —
which fails the totals test below loudly rather than quietly.
"""
groups: dict[tuple[str, int], list[Mapping[str, Any]]] = {}
for row in payload["connections"]:
keys = endpoint_keys(row)
if keys:
groups.setdefault(min(keys), []).append(row)
return groups
def distinct_endpoint_totals(payload: Mapping[str, Any]) -> dict[tuple[str, int], Any]:
"""One count per distinct endpoint — the figure a total may add, never one per line."""
return {key: rows[0]["connections"] for key, rows in endpoint_groups(payload).items()}
def walk_keys(node: Any) -> set[str]:
"""Every key name anywhere in a payload — the device the "no severity / no window" pins use."""
if isinstance(node, Mapping):
@@ -200,7 +274,7 @@ def test_the_captured_configs_yield_every_configured_pipe(gateway_snapshot: Any)
"five vhosts share 4443 and their domains belong to the one pipe for that port")
assert payload["nginx"]["map_targets"], "the stream map is the fan-out the page draws"
assert len(payload["nginx"]["map_targets"]) == 17
assert len(payload["nginx"]["map_targets"]) == 18
homepage = next(row for row in payload["nginx"]["map_targets"]
if row["key"] == "homepage.reeseapps.com")
assert homepage == {"key": "homepage.reeseapps.com",
@@ -229,7 +303,12 @@ def test_the_source_reads_the_keys_the_agent_actually_publishes(gateway_snapshot
assert {row["port"] for row in payload["nginx"]["servers"]} == agent_listen_ports
# and the three section names themselves, as the snapshot publishes them
assert {"haproxy", "nginx", "sockets"} <= set(snap)
assert host_agent_module.AGENT_VERSION == "1.2.0", "the sections these keys belong to"
# At least 1.2.0, the version that shipped these three keys. It is a floor and not an
# equality on purpose: phase 32 added a fourth section and bumped the constant, and the
# coupling this test pins is that the three the source joins still exist in what an agent
# publishes, whatever the number on it has since grown.
version = tuple(int(part) for part in host_agent_module.AGENT_VERSION.split("."))
assert version >= (1, 2, 0), "the sections these keys belong to"
def test_the_live_counts_reconcile_with_the_socket_table(gateway_snapshot: Any) -> None:
@@ -247,29 +326,60 @@ def test_the_live_counts_reconcile_with_the_socket_table(gateway_snapshot: Any)
What stays pinned is the direction of the error and the absence of a residual: the reported
figures are always **under** the kernel's number, and there is no field anywhere that quietly
absorbs the difference to make the two agree.
Run against the **`resolved`** capture, because that is what the deployed ingress serves since
phase 32 task 05: the widened match set is the one the arithmetic has to hold for. Three
figures, read three ways:
* `in_total` — the sum over the listeners (each socket at most once, inbound);
* `out_total` — the sum over **distinct endpoints**, derived here from `out.address` /
`out.resolved` rather than from `gateway.endpoint_of`;
* `lines_total` — the sum over the **lines**, which is deliberately larger wherever a line
repeats a number (`shared`), and is pinned to be exactly `out_total` plus the repetitions.
The `127.0.0.1:4443` loopback hop is the boundary case: it is booked **once inbound** (the rows
whose local port is 4443, on the `:4443` trunk) and **once outbound** (the rows whose remote is
4443, on the `:443 → 127.0.0.1:4443` pipe) and no single `/proc` row is in both.
"""
snap = gateway_snapshot("full")
snap = gateway_snapshot("resolved")
payload = gateway.parse(snap, "gateway")
established = len(snap["sockets"]["established"])
established = snap["sockets"]["established"]
configured = {row["port"] for row in payload["listeners"]}
assert configured == {22, 2000, 5201, 8080, 25565, 25566, 80, 443, 4443, 8443}
assert all(row["port"] in configured for row in payload["listeners"]), (
"an inbound count on a port no config names cannot exist in this payload")
in_total = sum(row["connections"] for row in payload["listeners"])
endpoints = {(row["out"]["host"], row["out"]["port"]): row["connections"]
for row in payload["connections"] if row["metric"] is not None}
out_total = sum(value or 0 for value in endpoints.values())
totals = distinct_endpoint_totals(payload)
out_total = sum(value or 0 for value in totals.values())
lines_total = sum(row["connections"] or 0 for row in payload["connections"])
repeated = sum((len(rows) - 1) * (rows[0]["connections"] or 0)
for rows in endpoint_groups(payload).values() if len(rows) > 1)
rows_in_table = len(established)
assert payload["sockets"] == {"listens": len(snap["sockets"]["listens"]),
"established": established}
assert in_total + out_total <= established
assert in_total + out_total < established, (
"established": rows_in_table}
assert in_total + out_total <= rows_in_table
assert in_total + out_total < rows_in_table, (
"this capture has traffic outside both config files; if it ever stops having any, this "
"test is proving nothing and should be re-written against a capture that does")
assert "unmatched" not in payload and "partial" not in payload
# the loopback hop is the case that used to double count: `127.0.0.1:4443` is both a listener's
# remote end and an outbound endpoint, and it may be booked on only one of them
assert sum(row["connections"] or 0 for row in payload["connections"]) == out_total
assert lines_total == out_total + repeated, (
"the only difference between the lines and the endpoints is a repeated count — no socket "
"is booked twice and no endpoint is booked once per name that reaches it")
# the loopback hop, both sides, from the raw table rather than from either counter
accepted = [row for row in established if (row["local"]["port"]) == 4443]
dialled = [row for row in established
if row["local"]["port"] != 4443 and (row["remote"]["addr"], row["remote"]["port"])
== ("127.0.0.1", 4443)]
assert accepted and dialled, "this capture is the loop the stream map's `default` produces"
assert listener_at(payload, 4443)["connections"] == len(accepted)
assert endpoint_count(payload, "127.0.0.1", 4443) == len(dialled)
assert not [row for row in established
if row["local"]["port"] == 4443
and (row["remote"]["addr"], row["remote"]["port"]) == ("127.0.0.1", 4443)], (
"a row that was both would be counted on two pipes by two rules, and the ≤ above would "
"still hold while every figure in the payload was wrong")
def test_a_socket_neither_config_describes_is_counted_nowhere(gateway_snapshot: Any) -> None:
@@ -293,7 +403,7 @@ def test_every_connection_the_captured_configs_declare_is_drawn(gateway_snapshot
"""The graph as a **set**, so a missing or invented route fails by name.
The join this task rewrote stamps counts onto these rows, so the rows themselves are pinned
here too: 6 haproxy `server` lines, 17 stream-`map` entries (the 16 `*.reeselink.com:443` fan-
here too: 6 haproxy `server` lines, 18 stream-`map` entries (the 17 `*.reeselink.com:443` fan-
out plus the literal `default` → `127.0.0.1:4443`), and 9 vhost `proxy_pass` targets across
`:4443` and `:8443`. `:80` is the refusal that lives on this fleet: the catch-all vhost declares
no `proxy_pass`, so it is a trunk with no connection and no invented line beside it.
@@ -317,6 +427,7 @@ def test_every_connection_the_captured_configs_declare_is_drawn(gateway_snapshot
(443, "reeseapps-gitea.reeselink.com", 443),
(443, "reeseapps-homepage.reeselink.com", 443),
(443, "reeseapps-immich.reeselink.com", 443),
(443, "reeseapps-whiteboard.reeselink.com", 443),
(443, "reeseapps-keycloak.reeselink.com", 443),
(443, "reeseapps-litellm.reeselink.com", 443),
(443, "reeseapps-ntfy.reeselink.com", 443),
@@ -362,9 +473,9 @@ def test_the_map_fanout_is_routes_and_never_a_variable_host(gateway_snapshot: An
"a proxy_pass $var carries no address, so it must not become a line of its own")
brain = next(row for row in map_routes
if row["out"]["host"] == "reeseapps-brain-of-reese.reeselink.com")
assert brain["out"]["address"] is False, "the config wrote a name, and this stack resolves none"
assert brain["connections"] is None, "so the line carries no count"
assert brain["metric"] is None, "and publishes no series"
assert brain["out"]["address"] is False, "the config wrote a name, and the config is verbatim"
assert brain["connections"] is None, "and on this arm (no resolver section) nothing placed it"
assert brain["metric"] is None, "so it publishes no series"
# --------------------------------------------------------------------------- #
@@ -476,10 +587,12 @@ def test_a_name_target_is_never_credited_for_a_socket_on_its_port() -> None:
"""The rule that replaced `by_port`, stated as a fact about the payload and not a chip.
`proxy_pass https://nextcloud.example.test:11000` and the socket to `10.1.0.254:11000` are the
same connection in the operator's head and two unrelated strings in this stack. Attributing one
to the other needed a resolver, and the port-only guess the old join made in its place was a
guess about a **port**, not about a host — which is why its count was the port's and its chip
said so. Now the line simply has no number, no gauge, and no apology to render.
same connection in the operator's head and two unrelated strings **in this stack**. The bridge is
the gateway's own resolution (phase 32, tested below), and the port-only guess the old join made
in its place was a guess about a **port**, not about a host — which is why its count was the
port's and its chip said so. With nothing to bridge them the line has no number, no gauge, and no
apology to render; with the agent's document it is counted through the address, and this test
stays true because the snapshot it builds has no resolver section.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["cloud.example.test"],
@@ -503,7 +616,7 @@ def test_a_name_target_is_never_credited_for_a_socket_on_its_port() -> None:
def test_a_port_shared_by_several_names_still_resolves_none_of_them() -> None:
"""The real `stream.conf` shape: sixteen names on 443, none of them counted.
"""The real `stream.conf` shape: sixteen names on 443, none of them counted **without DNS**.
The answer is the same `null` the old join gave, minus the machinery that produced it and the
`unmatched.outbound` total that used to double-count the same sockets.
@@ -557,6 +670,68 @@ def test_no_join_ever_asks_a_resolver() -> None:
assert calls == []
def test_the_recorded_resolver_document_reaches_every_name_written_route(
gateway_snapshot: Any) -> None:
"""What phase 32 buys, priced against the captures — and spent, since task 03.
31 of this ingress's 33 routes carried an em dash because this module joins sockets by
**address** and the two config files write those endpoints as **names**. The recorded
`gateway_resolver.json` is the thing that removed that reason, so this pins the two properties
the join needs and no more:
* every name-written route has an entry in the document, for that name **and that port**, with
at least one address — so no route on `/gateway` is dashed forever because the resolver had
nothing to say about its endpoint;
* the addresses are the same currency the socket table is written in: the two captures overlap
on live remote endpoints, which is what a join has to have to join anything.
and, in the last few lines, that spending it is a fact about the **document** rather than a
coincidence of the port table: the name-written routes are counted exactly where the entry puts
an address and nowhere else. This module still resolves nothing — the spy pin above stays exactly
as it was, and is the reason the fixture has to exist.
"""
snap = gateway_snapshot("full")
payload = gateway.parse(snap, "gateway")
document = json.loads((FIXTURES / "gateway_resolver.json").read_text())
answered = {(row["name"], row["port"]) for row in document["entries"] if row["addresses"]}
written = {(row["out"]["host"], row["out"]["port"]) for row in payload["connections"]
if not row["out"]["address"]}
assert written, "this capture writes its targets as names, which is the gap the fixture fills"
unreached = written - answered
assert not unreached, ("name-written routes the resolver document cannot reach: "
f"{sorted(unreached)} — re-run ./scripts/capture-gateway-config.sh")
resolved = {address for row in document["entries"] for address in row["addresses"]}
remote = {row["remote"]["addr"] for row in snap["sockets"]["established"]}
assert resolved & remote, (
"the resolver document and the socket table share no address, so a join built on it could "
"attribute nothing — the two captures are from different boxes or one scrub moved a number")
assert all(row["connections"] is None for row in payload["connections"]
if (row["out"]["host"], row["out"]["port"]) in written), (
"this arm is the snapshot with no `resolver` key at all, so a name-written route still "
"carries no count here — and adding one address to that section is what changes it")
spent = gateway.parse(gateway_snapshot("resolved"), "gateway")
rows = {(row["out"]["host"], row["out"]["port"]): row for row in spent["connections"]}
trunks = {row["port"] for row in spent["listeners"]}
for name, port in sorted(written):
row = rows[(name, port)]
entry = next(item for item in document["entries"]
if (item["name"], item["port"]) == (name, port))
assert row["out"]["resolved"] == entry["addresses"], f"{name}:{port} rewrites the capture"
assert row["connections"] is not None, f"{name}:{port} is placed and still carries no count"
# the exact figure, re-counted from the raw table by this test: the rows that reach this
# endpoint, minus the ones the inbound arm claimed first (a socket is counted at most once)
keys = {(address, port) for address in entry["addresses"]}
live = [item for item in snap["sockets"]["established"]
if item["local"]["port"] not in trunks
and (item["remote"]["addr"], item["remote"]["port"]) in keys]
assert row["connections"] == len(live), (
f"{name}:{port} resolves to {sorted(keys)} and the table holds {len(live)} such "
f"sockets, but the line reads {row['connections']}")
def test_two_services_on_one_host_are_two_pipes_not_one_sum() -> None:
"""The reason the outbound series name carries its port (`minecraft` 25565 and 25566)."""
snap = snapshot(
@@ -750,6 +925,546 @@ def test_an_endpoint_reached_by_one_line_is_not_shared() -> None:
assert row["connections"] == 1
# --------------------------------------------------------------------------- #
# phase 32: the resolver section — a name counted through the address the gateway dialled
#
# Everything above this line runs on a snapshot with **no** `resolver` key, which is agent 1.2.0 (or
# 1.3.0 with `--no-resolve`) and therefore phase 30 verbatim. Everything below it spends the
# document `scripts/capture-gateway-config.sh` recorded with `getent` on the router: the resolution
# haproxy and nginx perform **on the box that opens the connection**, which is the only vantage that
# can join `reeseapps-brain-of-reese.reeselink.com` to the `10.1.2.64` `/proc` reports. The
# dashboard asking the same name through its own `resolv.conf` would be a different vantage
# answering a different question — and `test_the_dashboard_still_never_resolves` is the spy that
# keeps it that way.
# --------------------------------------------------------------------------- #
def test_a_name_route_counts_through_the_address_the_gateway_published(
gateway_snapshot: Any) -> None:
"""The whole point of phase 32, measured against the captures.
`stream.conf` routes `brain.reeseapps.com` to `reeseapps-brain-of-reese.reeselink.com:443`,
`/proc` would report a socket to `10.1.2.64:443`, and until this section arrived nothing could
join the two. Now the line carries the config's text and the agent's answer side by side, and
the count comes through the answer.
"""
snap = gateway_snapshot("resolved")
payload = gateway.parse(snap, "gateway")
brain = next(row for row in payload["connections"]
if row["out"]["host"] == "reeseapps-brain-of-reese.reeselink.com")
assert brain["in"] == {"port": 443}
assert brain["out"]["selectors"] == ["brain.reeseapps.com"], "the config's word, untouched"
assert brain["out"]["address"] is False, "the config wrote a name, and still says so"
assert brain["out"]["resolved"] == ["10.1.2.64"], "the agent's answer, verbatim"
assert brain["out"]["via"] == "libc", "the vantage is the agent's own string, copied"
assert brain["out"]["resolved_ts"] == 1_792_886_400.0, "that entry's own timestamp"
assert brain["metric"] == "gateway.out_conns@reeseapps-brain-of-reese.reeselink.com:443", (
"the gauge keeps the config's spelling, so history continues under one name")
assert brain["connections"] == 0, "resolved and idle is `0`, never the dash"
ntfy = next(row for row in payload["connections"]
if row["out"]["host"] == "reeseapps-ntfy.reeselink.com")
live = [row for row in snap["sockets"]["established"]
if (row["remote"]["addr"], row["remote"]["port"]) == ("10.1.1.176", 443)]
assert live, "the recorded table has live ntfy sockets for the join to find"
assert ntfy["connections"] == len(live), "every socket to the address the name became"
assert ntfy["shared"] is False, "one route reaches that endpoint, so nothing to repeat"
def test_an_endpoint_the_config_wrote_as_an_address_claims_no_lookup(
gateway_snapshot: Any) -> None:
"""`resolved` answers *what did this name become*, and an address was never a name.
The agent publishes a `literal` entry for a target that is already an address, and the resolver
block lists it verbatim. Stamping that echo onto the route as `resolved` beside `via: "libc"`
would report a `getaddrinfo` this gateway never ran, so the three provenance fields stay `null`
on an `address: true` row while the count and the gauge keep working exactly as phase 30 left
them. Two facts stay two sentences: `address` is what the config wrote, `resolved` is what the
gateway looked up.
"""
payload = gateway.parse(gateway_snapshot("resolved"), "gateway")
written = [row for row in payload["connections"] if row["out"]["address"] is True]
assert written, "this capture writes two targets as addresses, which is phase 30's joinable pair"
for row in written:
assert row["out"]["host"] in {entry["name"]
for entry in payload["resolver"]["entries"]}, (
"the agent did publish this literal, and the block still lists it")
assert row["out"]["resolved"] is None and row["out"]["via"] is None \
and row["out"]["resolved_ts"] is None, (
f"{row['id']}: an address row must not borrow a lookup claim")
assert row["metric"] == f"gateway.out_conns@{row['out']['host']}:{row['out']['port']}"
assert row["connections"] is not None, "phase 30's address join is untouched by this"
def test_a_socket_lands_on_the_name_route_that_resolved_to_its_address() -> None:
"""The synthetic version, so the claim does not depend on what a capture happened to contain."""
snap = snapshot(
nginx=nginx_section([nginx_server([443], [], [proxy_pass(None, variable="targetBackend")])],
maps=[mapping_block("$targetBackend",
[("brain.example.test",
("brain-backend.example.test", 443))])]),
sockets=sockets_section(
listens=[listen(443)],
established=[conn(("::", 51000), ("10.1.2.64", 443), family="v6"),
conn(("10.1.1.27", 443), ("203.0.113.7", 900))]),
resolver=resolver_section([resolved("brain-backend.example.test", 443, ["10.1.2.64"])]))
payload = gateway.parse(snap, "gateway")
row = routes_to(payload, "brain-backend.example.test")[0]
assert row["out"]["address"] is False and row["out"]["resolved"] == ["10.1.2.64"]
assert row["connections"] == 1
assert row["metric"] == "gateway.out_conns@brain-backend.example.test:443"
assert listener_at(payload, 443)["connections"] == 1, "the inbound half is untouched"
gauge = [g for g in gateway.samples(payload, 5.0)
if g.metric == "gateway.out_conns@brain-backend.example.test:443"]
assert [(g.metric, g.value) for g in gauge] == [
("gateway.out_conns@brain-backend.example.test:443", 1.0)]
def test_the_match_is_an_address_not_a_string() -> None:
"""One address, two spellings, in either direction: the join still lands.
`/proc`'s `inet_ntop` writes an IPv4 peer seen through the IPv6 table as `::ffff:10.1.2.64`
while a resolver answers `10.1.2.64`, and an IPv6 in one table can carry leading zeros and
capitals the other does not. `ipaddress` canonicalises both, so the **match key** is one address
while the **payload** keeps publishing whatever the config and the agent wrote — a page that
printed the normalised form would be quoting a string nobody configured.
A join that compared strings instead would report a resolved, dialled route as idle forever:
the exact failure this phase exists to remove, arriving back through a spelling nobody thought
about. The two directions are asserted separately because each half is a different table.
"""
def build(written: str, socketed: str) -> dict[str, Any]:
snap = snapshot(
haproxy=haproxy_section([frontend("peer", [8001], default="peer-be")],
[backend("peer-be", [server("p", "peer.example.test", 443)])]),
sockets=sockets_section(
established=[conn(("::", 40000), (socketed, 443), family="v6")]),
resolver=resolver_section([resolved("peer.example.test", 443, [written])]))
return gateway.parse(snap, "gateway")
for written, socketed in (("10.1.2.64", "::ffff:10.1.2.64"),
("::ffff:10.1.2.64", "10.1.2.64"),
("2001:db8::1", "2001:0DB8::0001"),
("2001:0DB8::0001", "2001:db8::1")):
payload = build(written, socketed)
row = routes_to(payload, "peer.example.test")[0]
assert row["connections"] == 1, f"{written} vs {socketed} is one address and did not match"
assert row["out"]["resolved"] == [written], "the payload keeps the agent's own spelling"
def test_a_zero_padded_ipv4_is_no_address_to_anyone_and_matches_nothing() -> None:
"""The one spelling this module refuses to fold, and why refusing is the safe half.
`ipaddress` rejects `010.1.2.64` because `inet_aton` reads a leading `0` as **octal**: the
decimal and octal readings are two different addresses, and picking one here would book a
socket on a route by coin flip. So a padded target is a **name** to this module — and the
resolver section, which is libc's own answer, is what turns it into an address the kernel agrees
with. Nothing is guessed in between.
"""
snap = snapshot(
haproxy=haproxy_section([frontend("be", [8080], default="be")],
[backend("be", [server("odd", "010.1.2.64", 443)])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.1.2.64", 443)),
conn(("10.1.1.27", 40002), ("8.1.2.64", 443))]))
row = routes_to(gateway.parse(snap, "gateway"), "010.1.2.64")[0]
assert row["out"]["address"] is False, "not an address here, so it is a name"
assert row["connections"] is None and row["metric"] is None, (
"and with no resolver entry it is counted nowhere — neither reading is chosen for it")
# …and the fix is the resolver's, not this module's: libc's own answer places the same route,
# the padded spelling travels in the payload untouched, and neither socket is read as an octet.
placed = snapshot(
haproxy=haproxy_section([frontend("be", [8080], default="be")],
[backend("be", [server("odd", "010.1.2.64", 443)])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.1.2.64", 443)),
conn(("10.1.1.27", 40002), ("8.1.2.64", 443))]),
resolver=resolver_section([resolved("010.1.2.64", 443, ["10.1.2.64"])]))
fixed = routes_to(gateway.parse(placed, "gateway"), "010.1.2.64")[0]
assert fixed["out"]["resolved"] == ["10.1.2.64"]
assert fixed["connections"] == 1, (
"the one socket to the address libc returned, not the octal-reading 8.1.2.64 and not both")
def test_two_names_one_endpoint_is_one_count_shared() -> None:
"""Two **names** the gateway placed on one address are one endpoint and one number.
Phase 30 knew this shape only for one address written twice; resolving makes it ordinary, and
the rule did not change: the endpoint's whole count is repeated on both lines with
`shared: true`, never halved and never attributed to one of them.
Two readers then differ, and both are right. **Every total** books the endpoint once
(`distinct_endpoint_counts`, which is what `/api/summary`'s `live_out` reads), while the gauge
is per line-family — one series per name the config wrote, each carrying that endpoint's number,
because a route's own history is asked for by the name on the route. Two names therefore store
two series of one figure; lines that share a **name** (`:4443` and `:8443` reaching one target)
still store exactly one.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["a.example.test"],
[proxy_pass(("one.example.test", 8080))]),
nginx_server([8443], ["b.example.test"],
[proxy_pass(("two.example.test", 8080))])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 8080)),
conn(("10.1.1.27", 40002), ("10.5.5.5", 8080))]),
resolver=resolver_section([resolved("one.example.test", 8080, ["10.5.5.5"]),
resolved("two.example.test", 8080, ["10.5.5.5"])]))
payload = gateway.parse(snap, "gateway")
rows = [row for row in payload["connections"] if row["in"]["port"] in (4443, 8443)]
assert [row["connections"] for row in rows] == [2, 2], "the endpoint's total, twice"
assert [row["shared"] for row in rows] == [True, True]
assert gateway.distinct_endpoint_counts(payload["connections"]) == [2], (
"and one figure for anything that adds the outbound side up")
assert out_metrics(payload) == ["gateway.out_conns@one.example.test:8080",
"gateway.out_conns@two.example.test:8080"]
def test_two_spellings_of_one_multi_homed_name_are_one_endpoint() -> None:
"""A name with three addresses is **one** route, not three, and its sockets are one count.
Every key the resolver published points at the same counter row, so a client round-robin across
`10.5.5.5`/`10.5.5.6` reads as three connections on one pipe. The case union-find exists for is
two names whose address sets *overlap* — a multi-homed host and a single-homed alias for one of
its addresses — which would otherwise count the same endpoint twice in one total.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["a.example.test"],
[proxy_pass(("multi.example.test", 443))]),
nginx_server([8443], ["b.example.test"],
[proxy_pass(("alias.example.test", 443))])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 443)),
conn(("10.1.1.27", 40002), ("10.5.5.6", 443))]),
resolver=resolver_section([resolved("multi.example.test", 443,
["10.5.5.5", "10.5.5.6"]),
resolved("alias.example.test", 443, ["10.5.5.6"])]))
payload = gateway.parse(snap, "gateway")
rows = [row for row in payload["connections"] if row["in"]["port"] in (4443, 8443)]
assert [row["connections"] for row in rows] == [2, 2], (
"the shared endpoint's two sockets, on both lines — not one socket each")
assert [row["shared"] for row in rows] == [True, True]
assert gateway.distinct_endpoint_counts(payload["connections"]) == [2], (
"overlapping address sets are one endpoint, so a total books them once")
def test_a_name_that_bridges_two_others_merges_all_three_lines_into_one_endpoint() -> None:
"""A→{x}, B→{x,y}, C→{y}: three routes, one endpoint, one figure — transitivity included.
The bridging name is what makes this a *merge* rather than three pairwise comparisons: the first
and third share no address, and yet the same kernel endpoint sits behind both, so counting them
separately would book two endpoints where `/proc` has one. **The config order is load-bearing**
beyond the transitivity: the bridge arrives second and its lowest address (`10.5.5.5`) is lower
than the root the first route already built, so that whole group is re-parented, and the third
route's lookup then has to walk through two links to find it — the path the union-find compresses
so a fan-out of this size stays cheap on the next poll.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["a.example.test"],
[proxy_pass(("multi.example.test", 443))]),
nginx_server([8443], ["b.example.test"],
[proxy_pass(("bridge.example.test", 443))]),
nginx_server([8444], ["c.example.test"],
[proxy_pass(("alias.example.test", 443))])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.5.5.6", 443)),
conn(("10.1.1.27", 40002), ("10.5.5.7", 443))]),
resolver=resolver_section([resolved("multi.example.test", 443,
["10.5.5.6", "10.5.5.7"]),
resolved("bridge.example.test", 443,
["10.5.5.5", "10.5.5.6"]),
resolved("alias.example.test", 443, ["10.5.5.7"])]))
payload = gateway.parse(snap, "gateway")
rows = [row for row in payload["connections"] if row["in"]["port"] in (4443, 8443, 8444)]
assert len(rows) == 3
assert [row["connections"] for row in rows] == [2, 2, 2], (
"one endpoint's two sockets, repeated on all three lines")
assert [row["shared"] for row in rows] == [True, True, True]
assert gateway.distinct_endpoint_counts(payload["connections"]) == [2], (
"and one figure for any total, however many names bridged their way to it")
def test_a_resolver_entry_that_publishes_nothing_address_like_places_nothing() -> None:
"""A malformed answer is a name with no address, not an address that matches by luck.
The agent publishes strings it got from `getaddrinfo`, and this module has no reason to believe
them: an entry whose `addresses` are not addresses yields no endpoint key, so the route keeps its
dash and its missing gauge exactly as an `nxdomain` does. The alternative — comparing the junk
text against socket addresses — is how a typo in one file could credit a socket to a route.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["odd.example.test"],
[proxy_pass(("odd.example.test", 443))])]),
sockets=sockets_section(established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 443))]),
resolver=resolver_section([
resolved("odd.example.test", 443, ["localhost", "10.5.5.5.6", ""])]))
row = routes_to(gateway.parse(snap, "gateway"), "odd.example.test")[0]
assert row["out"]["resolved"] == ["localhost", "10.5.5.5.6"], (
"the agent's bytes verbatim, minus the empty string — the payload is not where this is fixed")
assert row["connections"] is None and row["metric"] is None, (
"and not one of them is an address to match a socket with")
def test_a_line_with_no_endpoint_contributes_nothing_to_a_total() -> None:
"""`distinct_endpoint_counts` skips the unplaced line instead of offering it as a zero.
The caller turns "no figures at all" into `null` (`api._live_total`), which is the arm an
ingress of nothing-but-unresolved-names lands in. A `null` inside the list instead would make
every total on the page `null` because one route has a name nobody could place — and rule 2's
complaint about that shape is precisely that it invents a fault in the healthy routes beside it.
"""
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["up.example.test"],
[proxy_pass(("up.example.test", 443))]),
nginx_server([8443], ["gone.example.test"],
[proxy_pass(("gone.example.test", 443))])]),
sockets=sockets_section(established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 443))]),
resolver=resolver_section([resolved("up.example.test", 443, ["10.5.5.5"]),
resolved("gone.example.test", 443, [], status="nxdomain")]))
payload = gateway.parse(snap, "gateway")
placed, unplaced = (routes_to(payload, "up.example.test")[0],
routes_to(payload, "gone.example.test")[0])
assert (placed["connections"], unplaced["connections"]) == (1, None)
assert gateway.distinct_endpoint_counts(payload["connections"]) == [1], (
"the placed endpoint alone: no second entry, no None, no zero")
assert gateway.distinct_endpoint_counts([unplaced]) == [], (
"and a payload of nothing-but-dashes is no figures at all, which the API renders as null")
def test_an_unresolved_name_publishes_no_metric_and_no_sample() -> None:
"""`nxdomain`, an error, and no entry at all are one answer here: drawn, never counted.
Three arms in one snapshot — a name the resolver placed, a name it looked up and got nothing
for, and a name it never mentioned — so the difference between "no record" and "nobody asked"
stays visible in the resolver block while the **route rows** behave identically: `connections`
`null`, `metric` `null`, no sample, and never a `0` that reads as an idle route.
"""
snap = snapshot(
nginx=nginx_section([
nginx_server([4443], ["up.example.test"], [proxy_pass(("up.example.test", 443))]),
nginx_server([8443], ["gone.example.test"], [proxy_pass(("gone.example.test", 443))]),
nginx_server([9443], ["silent.example.test"],
[proxy_pass(("silent.example.test", 443))])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 443)),
conn(("10.1.1.27", 40002), ("10.5.5.9", 443))]),
resolver=resolver_section([
resolved("up.example.test", 443, ["10.5.5.5"]),
resolved("gone.example.test", 443, [], status="nxdomain")]))
payload = gateway.parse(snap, "gateway")
assert routes_to(payload, "up.example.test")[0]["connections"] == 1
for name in ("gone.example.test", "silent.example.test"):
row = routes_to(payload, name)[0]
assert row["out"]["resolved"] is None, "never `[]`: unknown is one test, not a length"
assert row["out"]["via"] is None and row["out"]["resolved_ts"] is None
assert row["connections"] is None, "no count, and no 0 standing in for one"
assert row["metric"] is None, "no series for a number that cannot exist"
stored = [g.metric for g in gateway.samples(payload, 5.0)
if g.metric.startswith("gateway.out_conns@")]
assert stored == ["gateway.out_conns@up.example.test:443"]
assert sum(row["connections"] or 0 for row in payload["connections"]) == 1, (
"the socket to the unresolved target is credited to nothing at all")
def test_a_resolver_section_that_did_not_answer_places_nothing(gateway_snapshot: Any) -> None:
"""`available: false` and `available: null` are the agent's answers, and neither joins.
`false` is "there was nothing to resolve" (no proxy config was read) and `null` is "a pass that
could not run"; both arrive with no usable entries, and the payload's `resolver.available` says
which one it was rather than leaving a page to infer it from an empty list.
"""
for available in (False, None):
snap = snapshot(
nginx=nginx_section([nginx_server([4443], ["a.example.test"],
[proxy_pass(("x.example.test", 443))])]),
sockets=sockets_section(
established=[conn(("10.1.1.27", 40000), ("10.5.5.5", 443))]),
resolver=resolver_section([resolved("x.example.test", 443, ["10.5.5.5"])],
available=available, via=None))
payload = gateway.parse(snap, "gateway")
assert payload["resolver"]["available"] is available
assert payload["resolver"]["entries"] == [], "nothing was indexed from a dead pass"
assert routes_to(payload, "x.example.test")[0]["connections"] is None, (
"an entry beside `available: null` is a stale answer, not a joinable address")
def test_the_resolver_block_names_the_entries_the_index_was_built_from(
gateway_snapshot: Any) -> None:
"""The payload carries the document it was drawn from, so provenance is one read.
`entries` is every entry the section published — the index filters **within** it, so a page can
still say *this name has no record* — and each is projected to the five keys the agent owns.
`via` is the section's string. The `stub` arm keeps the same key set with nulls, which is what
the parse/stub parity pin in `tests/test_api.py` compares.
"""
payload = gateway.parse(gateway_snapshot("resolved"), "gateway")
block = payload["resolver"]
assert set(block) == {"available", "via", "entries"}
assert block["available"] is True and block["via"] == "libc"
document = json.loads((FIXTURES / "gateway_resolver.json").read_text())
assert [row["name"] for row in block["entries"]] == [row["name"] for row in document["entries"]]
assert [row["port"] for row in block["entries"]] == [row["port"] for row in document["entries"]]
assert all(set(row) == {"name", "port", "status", "addresses", "ts"}
for row in block["entries"]), "no agent field reaches a page unreviewed"
assert {row["status"] for row in block["entries"]} == {"ok", "literal"}, (
"this capture resolves every name it was asked about, plus the two `127.0.0.1` targets it "
"recognised as addresses without asking at all")
empty = gateway.stub("gateway")
assert empty["resolver"] == {"available": None, "via": None, "entries": []}, (
"a host that has not answered has not said resolution is unavailable")
def test_the_dashboard_still_never_resolves(gateway_snapshot: Any) -> None:
"""The spy pin, restated for phase 32 rather than relaxed by it.
`parse()` may **read** a resolver document and must not produce one, so the spy now runs over
three paths: a synthetic snapshot whose every target is a name, the same with a resolver section
full of answers, and the captured `resolved` ingress — then `samples()` over the result, since
a gauge name is built from a host string and is the other place a resolver could creep in.
Why the split is the design and not a limitation: the agent resolves with `getaddrinfo` on the
host that **opens** these connections (haproxy's `init-addr last,libc,none`), so its answer is
the one that decides what `/proc` then says. The dashboard asking the same name through its own
`resolv.conf` would answer a different question — a different server, a different view, and on
this fleet a name that resolves back through this same router — and would then attribute a
socket to a route on the strength of a lookup the router never made.
"""
calls: list[tuple[Any, ...]] = []
def spy(*args: Any, **kwargs: Any) -> Any: # pragma: no cover - must never run
calls.append((args, kwargs))
raise AssertionError("the gateway join must never resolve a name")
named = snapshot(
haproxy=haproxy_section([frontend("mc", [25565])],
[backend("mc-be", [server("mc1", "minecraft.example.test",
25565)])]),
nginx=nginx_section([nginx_server([443], ["a.example.test"],
[proxy_pass(("app.example.test", 8080))])]),
sockets=sockets_section(listens=[listen(25565), listen(443)],
established=[conn(("10.1.1.27", 443), ("203.0.113.5", 900)),
conn(("10.1.1.27", 1), ("10.2.2.2", 8080))]))
answered = snapshot(
haproxy=named["haproxy"], nginx=named["nginx"], sockets=named["sockets"],
resolver=resolver_section([resolved("minecraft.example.test", 25565, ["10.9.9.9"]),
resolved("app.example.test", 8080, ["10.2.2.2"])]))
original = socket_module.getaddrinfo
socket_module.getaddrinfo = spy
try:
for document in (named, answered, gateway_snapshot("resolved")):
payload = gateway.parse(document, "gateway")
gateway.samples(payload, 5.0)
gateway.warnings(gateway._section(document, "haproxy"),
gateway._section(document, "nginx"))
finally:
socket_module.getaddrinfo = original
assert calls == []
def test_a_snapshot_without_a_resolver_section_is_phase_30_verbatim(
gateway_snapshot: Any) -> None:
"""A 1.2.0 agent, or a 1.3.0 one run with `--no-resolve`, is **not** a changed page.
The topology, the sections, the warnings and the two loopback counts are byte-for-byte what
phase 30 produced; what differs is three `null`s on every `out` and an empty `resolver` block,
plus the routes phase 32 makes countable. `supported` stays `true` — a missing resolver is a
missing measurement, not an unsupported agent, and a fleet running two versions (which this one
did until task 05) must not read half-red.
"""
without = gateway.parse(gateway_snapshot("full"), "gateway")
with_resolver = gateway.parse(gateway_snapshot("resolved"), "gateway")
assert without["supported"] is True and with_resolver["supported"] is True
assert without["resolver"] == {"available": None, "via": None, "entries": []}
assert all(row["out"]["resolved"] is None and row["out"]["via"] is None
and row["out"]["resolved_ts"] is None for row in without["connections"])
def shape(payload: Mapping[str, Any]) -> Any:
"""Everything except the counts, which is the one thing the section may move."""
return {
"routes": [row["id"] for row in payload["connections"]],
"out": [(row["out"]["host"], row["out"]["port"], row["out"]["name"],
row["out"]["kind"], tuple(row["out"]["selectors"]), row["out"]["address"])
for row in payload["connections"]],
"trunks": [(row["port"], row["source"], tuple(row["domains"]))
for row in payload["listeners"]],
"sections": payload["sections"],
"why": payload["why"],
"warnings": payload["warnings"],
"sockets": payload["sockets"],
}
assert shape(without) == shape(with_resolver), (
"the resolver section counts through an address; it moves no route, trunk or warning")
counted = {row["id"] for row in without["connections"] if row["connections"] is not None}
assert counted == {"nginx:443->127.0.0.1:4443", "nginx:8443->127.0.0.1:8081"}, (
"phase 30 counted exactly the two endpoints the config wrote as addresses")
def test_resolution_raises_the_counted_route_count_from_two_to_every_name(
gateway_snapshot: Any) -> None:
"""The change, as one printed number rather than as an implied one.
Phase 30 could count the two routes whose target the configs wrote as an address. The recorded
resolver document places the other thirty-one, so against the captures every route on the
ingress now carries a figure — which is what the operator's "the only animated line is `:443 ->
127.0.0.1:4443`" was really asking for.
"""
before = gateway.parse(gateway_snapshot("full"), "gateway")
after = gateway.parse(gateway_snapshot("resolved"), "gateway")
counted_before = [row for row in before["connections"] if row["connections"] is not None]
counted_after = [row for row in after["connections"] if row["connections"] is not None]
written_as_name = [row for row in after["connections"] if row["out"]["address"] is not True]
unplaced = sorted(row["out"]["host"] for row in written_as_name
if row["connections"] is None)
report = (f"/gateway counted routes: {len(counted_before)} "
f"(phase 30, addresses only) → {len(counted_after)} of {len(after['connections'])}; "
f"the resolver placed {len(written_as_name)} name-written routes")
print(report)
assert len(counted_before) == 2, "phase 30's figure, so the delta is measurable"
assert not unplaced, f"{report} — unplaced: {unplaced}"
assert len(counted_after) == len(after["connections"]), (
f"{report} — a route the document places must carry a count")
def test_two_ports_on_one_host_are_two_gauges_even_when_both_names_resolve() -> None:
"""`minecraft:25565` and `minecraft:25566` stay two series — the port earns its place in the name.
This is why phase 29 put the port in `gateway.out_conns@<host>:<port>` in the first place, and
resolution does not remove the reason: this fleet's two haproxy Minecraft backends name the *same*
domain on two ports, libc answers one address for both, and a gauge keyed on the name alone would
have the second poll overwrite the first in silence. Two endpoints, two counters, two series — and
the fact that they share an address is invisible in the names and visible in the traffic.
"""
snap = snapshot(
haproxy=haproxy_section(
[frontend("mc", [25565], default="mc-a"), frontend("mc2", [25566], default="mc-b")],
[backend("mc-a", [server("mc1", "minecraft.example.test", 25565)]),
backend("mc-b", [server("mc2", "minecraft.example.test", 25566)])]),
sockets=sockets_section(established=[
conn(("10.1.1.27", 40000), ("10.9.9.9", 25565)),
conn(("10.1.1.27", 40002), ("10.9.9.9", 25565)),
conn(("10.1.1.27", 40004), ("10.9.9.9", 25566))]),
resolver=resolver_section([resolved("minecraft.example.test", 25565, ["10.9.9.9"]),
resolved("minecraft.example.test", 25566, ["10.9.9.9"])]))
payload = gateway.parse(snap, "gateway")
by_port = {row["in"]["port"]: row for row in payload["connections"]}
assert by_port[25565]["metric"] == "gateway.out_conns@minecraft.example.test:25565"
assert by_port[25566]["metric"] == "gateway.out_conns@minecraft.example.test:25566"
assert [by_port[25565]["connections"], by_port[25566]["connections"]] == [2, 1], (
"two endpoints, two counts — one address does not merge two ports")
assert by_port[25565]["shared"] is False and by_port[25566]["shared"] is False, (
"sharing an *address* is not sharing an *endpoint*, which is what `shared` means")
stored = gateway.samples(payload)
assert [(s.metric, s.value) for s in stored if "out_conns" in s.metric] == [
("gateway.out_conns@minecraft.example.test:25565", 2),
("gateway.out_conns@minecraft.example.test:25566", 1)], (
"neither gauge overwrites the other on the way to the store")
assert gateway.distinct_endpoint_counts(payload["connections"]) == [2, 1], (
"and the total books both, because the kernel has both")
# --------------------------------------------------------------------------- #
# the map: which maps route, and which are just header words
# --------------------------------------------------------------------------- #
@@ -1336,9 +2051,9 @@ def test_the_captured_configs_produce_no_warnings(gateway_snapshot: Any) -> None
assert used == {"targetBackend"} and used <= defined, (
"the one variable a `proxy_pass` passes IS defined by a map")
referenced = [block for block in nginx["maps"] if block["variable"].lstrip("$") in used]
assert len(referenced) == 1 and len(referenced[0]["entries"]) == 17
assert len(referenced) == 1 and len(referenced[0]["entries"]) == 18
assert all(entry["target"] for entry in referenced[0]["entries"]), (
"all 17 stream routes name an address, `default` included")
"all 18 stream routes name an address, `default` included")
unreferenced = [block for block in nginx["maps"] if block not in referenced]
assert any(entry["target"] is None for block in unreferenced for entry in block["entries"]), (
"the capture must still hold null-target entries on the UNREFERENCED map, or this pin "