gobridge

0010 — MQTT bridge-to-bridge loop-prevention contract

Status: accepted Date: 2026-07-13 Deciders: GoBridge core Decision recorded and implemented: commit 4d8d76d (2026-07-13; the opt-in no_local/NoLocal subscribe path). The reserved x-bridge.forwarded-* headers this ADR references predate it: HeaderForwardedFrom/HeaderForwardedHop were introduced in commit cffe7fa (2026-03-27) and relocated into domain/messaging/headers.go by the bounded-context split c5bae72 (2026-05-06); they remain reserved-but-unenforced.

Context

A message bridge that both subscribes and publishes on the same broker can feed its own output back into its input. The sharpest case is a single session on a single broker whose subscription filter overlaps its publish topic: MQTT delivers every publish back to the same client that sent it, the bridge re-forwards it, and the loop amplifies without bound — a self-inflicted broker meltdown that no downstream dedup can absorb, because each turn of the loop mints a new message.

A second, looser case is a cross-bridge relay (bridge → broker → bridge → broker → …) where two or more bridges relay each other’s traffic in a cycle. Here the messages carry bridge-stamped provenance, so the loop is in principle observable, but the reserved-header trust model complicates it: every x-bridge.* key is stripped from untrusted transport input at ingress (ADR 0001, StripReservedHeaders), so a provenance marker only survives a hop if the bridge lifts it back in through a typed field the way ADR 0008 lifts the identity keys.

The bridge needs a stated position on both cases rather than leaving loop avoidance entirely to operator topology design.

Decision

Break the dangerous same-session loop at the transport with an opt-in MQTT 5 No-Local flag; treat the cross-bridge relay as an operator-topology and dedup-layer concern, with the reserved-header namespace reserved for a future hop-provenance contract.

Consequences

Rejected alternatives