mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
The cooling report secret: a driver harness, and two catalog tests
forgectrl's POST /cool/state now asks for the secret the supervisor hands the running controller at its spawn; both controllers' cooling clients send it. scripts/bench/cool_report_test.py is the driver's host harness for it (null-sink controller, a stand-in for forgectrl's listener on FORGECTRL_PORT): every report carries the secret and no other new header; none does when there is none; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; the homing runner the controller starts does not inherit the secret. It is in the bench registry and the bench README, and the driver's CI runs it. forgectrl.auth: the loopback case used to assert that any local peer is accepted. It now asserts the three answers: no secret 403, a made-up secret 403, and the running controller's own secret 200, read as only root on the machine can read it, out of the controller's environment, and never logged. The LAN cases carry the secret too and are still refused. cooling.report-channel is new, the drill the change exists for: M8 opens a run session, three forged idle reports from this host (no secret, a made-up one, a made-up one with a forged Host) are each refused, and over the next five seconds the engine stays in phase run and no commanded fan duty drops; M9 ends the session on the controller's own report. Proven. The harness passes on the host-built controller, with three negative controls that each fail as they should. The unit suite passes (421) with no undefined name. On the bench reference, forgectrl and both clients hot-deployed over image 20260920152153: forgectrl.auth PASS, cooling.report-channel PASS (phase run throughout, the exhaust at 65535 and the intake at 43278), and cooling.fans-quiet-after-motion and motion.job PASS on the same binaries. Acceptance. forgectrl.auth and cooling.report-channel are the gate for the report channel's secret; cloud.dark-print gates the cloud client's side.
This commit is contained in:
@@ -265,6 +265,14 @@ TOOLS = [
|
||||
"(a line queued right behind the port's included), the refusals, the single client, a CR LF "
|
||||
"sender, a soft reset, and the client as the dead-man. A CI harness (the grblHAL repo): needs "
|
||||
"the host-built null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
{"id": "cool-report-test", "title": "Cooling report secret harness", "script": "cool_report_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "The controller's cooling reports on the null-sink controller, read by a stand-in for "
|
||||
"forgectrl's listener: every report carries the secret the supervisor hands the controller at "
|
||||
"its spawn (GF_REPORT_SECRET), none does when there is none, a value that is not 32 hex digits "
|
||||
"(a CR LF with a header behind it included) never reaches the wire, and the homing runner the "
|
||||
"controller starts does not inherit it. A CI harness (the grblHAL repo): needs the host-built "
|
||||
"null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
{"id": "manual-home-test", "title": "Manual home and motor release harness", "script": "manual_home_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "The manual homing provider and the motor release on the null-sink controller: $H under manual "
|
||||
|
||||
@@ -1532,3 +1532,60 @@ def fail_tier_stop(ctx):
|
||||
"thresholds not restored: %s", {k: after.get(k) for k in CRASH_KEYS})
|
||||
ctx.log("PASS: the crash tier stopped the job, locked the latch, and the supervisor restarted "
|
||||
"the controller (pid %s -> %s)", pid0, m1 and m1.get("pid"))
|
||||
|
||||
|
||||
@test("cooling.report-channel", title="A forged cooling report inside a run session is refused and changes nothing",
|
||||
subsystem="cooling", kind="auto", mode="grbl", est_min=2,
|
||||
covers=_COOL_COVERS + [("forgectrl", "src/super.*"), ("forgectrl", "src/main.c"),
|
||||
("forgectrl", "src/auth.*")],
|
||||
requires=["forgectrl.auth"],
|
||||
description="The report channel (POST /cool/state) is what tells the engine a job is running: a "
|
||||
"forged idle report would stand the fans down under a cut. M8 opens a run session "
|
||||
"from the suite's Grbl client, and once the engine is in phase run with its run fan "
|
||||
"profile commanded, a process on this host that is not the controller reports "
|
||||
"mode=idle, armed=0: with no secret, with a made-up secret, and with a made-up "
|
||||
"secret and a forged Host header. Each is refused (403). Over the next five seconds "
|
||||
"the engine stays in phase run and no commanded fan duty drops. M9 "
|
||||
"ends the session, and the engine leaves phase run on the controller's own report.")
|
||||
def report_channel(ctx):
|
||||
fc = ctx.forgectrl
|
||||
ev = ctx.evidence
|
||||
forged = {"mode": "idle", "armed": "0"}
|
||||
|
||||
with ctx.grbl() as g:
|
||||
def in_run(c):
|
||||
return c.get("phase") == "run"
|
||||
ctx.check(_cool(fc).get("phase") != "run", "a run session is already open")
|
||||
g.command("M8")
|
||||
try:
|
||||
ok = ctx.wait_for(lambda: in_run(_cool(fc)), VERDICT_WAIT_S, poll=0.5)
|
||||
ctx.check(ok is not None, "M8 did not open a run session: %s", _cool(fc).get("phase"))
|
||||
ctx.sleep(2) # the run fan profile is commanded
|
||||
before = _cool(fc)
|
||||
duties0 = _duties()
|
||||
ev["before"] = {"phase": before.get("phase"), "duties": duties0}
|
||||
refused = {}
|
||||
for name, headers in (("no secret", {}),
|
||||
("a made-up secret", {"X-ForgeFIRM-Report": "0123456789abcdef" * 2}),
|
||||
("a made-up secret and a forged Host",
|
||||
{"X-ForgeFIRM-Report": "f" * 32, "Host": "localhost"})):
|
||||
st, body = fc.post("/cool/state", params=forged, headers=headers, auth=False)
|
||||
refused[name] = [st, str(body)[:100]]
|
||||
ctx.check(st == 403, "a forged report (%s) -> %s %s", name, st, str(body)[:100])
|
||||
ev["refused"] = refused
|
||||
held = []
|
||||
for _ in range(5):
|
||||
ctx.sleep(1)
|
||||
c = _cool(fc)
|
||||
held.append([c.get("phase"), _duties()])
|
||||
ev["after"] = held
|
||||
ctx.log("phase and duties over 5 s after three forged idle reports: %s", held)
|
||||
ctx.check(all(p == "run" for p, _d in held), "the engine left phase run: %s", [p for p, _d in held])
|
||||
ctx.check(all(d[k] >= duties0[k] for _p, d in held for k in duties0),
|
||||
"a commanded fan duty dropped under the forged reports: %s -> %s", duties0, held)
|
||||
finally:
|
||||
g.command("M9")
|
||||
_session_ended(ctx, fc, "report channel")
|
||||
ctx.check(_cool(fc).get("phase") != "run", "the session did not end on the controller's own report")
|
||||
ctx.log("PASS: three forged idle reports from this host were refused inside a run session, and the "
|
||||
"engine held its phase and its fan duties")
|
||||
|
||||
@@ -143,14 +143,39 @@ def auth(ctx):
|
||||
"GET /fuse-identity with the token but no button -> %s %r (expected the two-factor refusal)",
|
||||
st, body)
|
||||
|
||||
# the cooling report channel: the loopback peer is accepted. An idle
|
||||
# report is what the controller sends every period; the engine is idle
|
||||
# here, so it changes nothing. A dual-stack listener reports this peer
|
||||
# as ::ffff:127.0.0.1, which the check must recognize in full.
|
||||
st, body = fc.post("/cool/state", params={"mode": "idle", "armed": "0"})
|
||||
# the cooling report channel is the running controller's alone. A
|
||||
# loopback peer is not enough: anything on this host is one. The
|
||||
# supervisor hands the controller a secret at its spawn, and the route
|
||||
# asks for it. An idle report is what the controller sends every
|
||||
# period; the engine is idle here, so the accepted one changes nothing.
|
||||
# A dual-stack listener reports this peer as ::ffff:127.0.0.1, which
|
||||
# the loopback check must recognize in full.
|
||||
report = {"mode": "idle", "armed": "0"}
|
||||
st, body = fc.post("/cool/state", params=report)
|
||||
ev["cool_state_from_loopback_no_secret"] = st
|
||||
ctx.log("POST /cool/state from loopback, no secret -> %s %s", st, body if isinstance(body, str) else "")
|
||||
ctx.check(st == 403 and "controller's alone" in str(body),
|
||||
"/cool/state from loopback without the secret -> %s %r", st, body)
|
||||
st, body = fc.post("/cool/state", params=report, headers={"X-ForgeFIRM-Report": "0" * 32})
|
||||
ctx.check(st == 403, "/cool/state with a secret that is not the controller's -> %s %r", st, body)
|
||||
# The secret itself, as only root on this host can read it: out of
|
||||
# the running controller's environment. It is never logged.
|
||||
secret = None
|
||||
pid = (fc.get("/mode")[1] or {}).get("pid")
|
||||
if pid:
|
||||
try:
|
||||
with open("/proc/%d/environ" % int(pid), "rb") as f:
|
||||
for item in f.read().split(b"\0"):
|
||||
if item.startswith(b"GF_REPORT_SECRET="):
|
||||
secret = item.split(b"=", 1)[1].decode("ascii", "replace")
|
||||
except OSError:
|
||||
pass
|
||||
ev["controller_has_a_secret"] = bool(secret)
|
||||
ctx.check(secret and len(secret) == 32, "the running controller (pid %s) was handed no report secret", pid)
|
||||
st, body = fc.post("/cool/state", params=report, headers={"X-ForgeFIRM-Report": secret})
|
||||
ev["cool_state_from_loopback"] = st
|
||||
ctx.log("POST /cool/state from loopback -> %s %s", st, body if isinstance(body, dict) else "")
|
||||
ctx.check(st == 200, "/cool/state refused the loopback peer (%s %r): the controller's "
|
||||
ctx.log("POST /cool/state from loopback with the controller's secret -> %s", st)
|
||||
ctx.check(st == 200, "/cool/state refused the controller's own secret (%s %r): the controller's "
|
||||
"reports never reach the engine", st, body)
|
||||
|
||||
# ...and a non-loopback peer is refused, even with a token. Over HTTP
|
||||
@@ -162,8 +187,7 @@ def auth(ctx):
|
||||
ip = lan_ip()
|
||||
ev["lan_ip"] = ip
|
||||
ctx.check(ip, "cannot determine the board's LAN address")
|
||||
token = {"X-ForgeFIRM-Token": fc.token}
|
||||
report = {"mode": "idle", "armed": "0"}
|
||||
token = {"X-ForgeFIRM-Token": fc.token, "X-ForgeFIRM-Report": secret}
|
||||
st, body, hdrs = request("http://%s" % ip, "POST", "/cool/state", data=report, headers=token)
|
||||
loc = hdrs.get("location", "")
|
||||
ev["cool_state_from_lan_http"] = {"status": st, "location": loc}
|
||||
|
||||
Reference in New Issue
Block a user