Skip to content

Send Origin messages to one registered listener, signed, instead of broadcasting - #377

Draft
mullinmax wants to merge 2 commits into
mainfrom
claude/vector-network-discovery-windows-2xeoja
Draft

Send Origin messages to one registered listener, signed, instead of broadcasting#377
mullinmax wants to merge 2 commits into
mainfrom
claude/vector-network-discovery-windows-2xeoja

Conversation

@mullinmax

Copy link
Copy Markdown
Contributor

Description

Game events (game state, end of game, reset) no longer go to the broadcast address. A listener registers itself over a new authenticated route and the board unicasts to that one address, signing every datagram.

New route — POST /api/origin/target (authenticated), mirroring the shape of /api/memory/toggle-broadcast:

{"enable": true, "secret": "", "ip": "192.168.1.5"}

ip is optional; when omitted the board uses request.client_ip. That is the case that matters — a listener behind NAT cannot name its own translated address, but the board sees it. {"enable": false} stops the stream.

src/common/origin.py keeps the target as 4 raw IP bytes plus the secret, and prefixes each datagram with 16 hex characters of HMAC-SHA256(secret, body):

+--------------------------+------------------+
| 16 ASCII hex chars (MAC) | UTF-8 JSON body  |
+--------------------------+------------------+

The body gains "n", a counter that increments per send, so a listener can drop replays. Registering again rotates the secret and resets the counter.

Motivation and Context

Two problems with broadcasting:

  1. It jams the WiFi chip. Every board broadcasts to every listener on the LAN, several datagrams a second each. A room of machines is a lot of broadcast traffic for a Pico W radio.
  2. Anything on the LAN could forge a score. The datagrams were unauthenticated plain JSON.

HMAC on every datagram costs well under 0.5 ms on the RP2350 (hashlib.sha256 is native C) — roughly 0.2% duty cycle at 4 Hz, less than the ujson.dumps already in the send path. Truncating the tag to 64 bits keeps the packet small; that is far past what a LAN attacker brute-forces within the life of a session secret.

Target state is RAM-only by design: a reboot clears it and the listener re-registers, which is the same path that recovers a moved or restarted listener. No FRAM layout change, no stale target surviving a move.

Related Issues

Part of a three-repo change:

Testing

  • black, isort, flake8 clean (the two F824 warnings in backend.py are pre-existing).
  • docs/routes.md regenerated via tools/gen_api_docs.py.
  • Round-trip framing is verified against the library's implementation in Add Origin message framing, formats wrappers, and origin target registration python-library#19 (tests/test_origin.py) — both sides compute the tag over the same bytes.
  • Not yet flashed to hardware. Worth confirming on a board before merge: register from a listener, watch datagrams arrive at the registered IP only, and confirm nothing is sent before registration.

Types of Changes

  • New feature (non-breaking change to add functionality)
  • Breaking change (alters existing functionality)
  • Documentation update required

On the breaking half: a board running this firmware sends nothing until a listener registers, so an older Origin that only listens for broadcasts will see no game events from it. The matching Origin change registers on startup and periodically thereafter.

Checklist

  • My code follows the project's style guidelines.
  • I have updated documentation as needed.
  • I have added or updated tests. (dev/tests covers build tooling only; there is no harness for src/common — framing is covered on the library side.)
  • All new and existing tests pass.

Generated by Claude Code

claude and others added 2 commits August 25, 2026 02:13
…roadcasting

Game events went out as plain JSON to 255.255.255.255:6809, so every board on
the network shouted at every listener. That is enough broadcast traffic to jam
the WiFi chip, and anything on the LAN could forge a score packet.

A listener now registers over the authenticated /api/origin/target route,
handing over a shared secret and, optionally, the address to send to -- when
it is omitted the board uses the address the request arrived from, which is
what a listener behind NAT needs. The board keeps the target as 4 raw IP bytes
plus the secret in RAM, unicasts every game event there, and prefixes each
datagram with 16 hex characters of HMAC-SHA256(secret, body). Bodies carry a
counter that increments per send so a listener can drop replays; registering
again rotates the secret and resets it.

With nobody registered the board sends nothing at all -- there is no broadcast
fallback. State is deliberately RAM-only: a reboot clears it and the listener
re-registers, which is also how a moved or restarted listener recovers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmtVSp58rDkDBRPzL9hJkt
@github-actions

Copy link
Copy Markdown
Contributor

Developer build links:
Sys11

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/sys11-update.json

Sys11 (Tiny)

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/sys11-tiny-update.json

WPC

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/wpc-update.json

EM

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/em-update.json

WhiteStar

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/whitestar-update.json

DataEast

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/data-east-update.json

Classic

https://raw.githubusercontent.com/warped-pinball/vector/pr-update-artifacts/pr-artifacts/pr-377/classic-update.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants