Added per-network LAN broadcast relay

LAN game discovery previously dropped IPv4 broadcasts at TUN ingress. Carry
limited and subnet-directed UDP broadcasts to authenticated, opted-in members
of the source network, including destinations reached through mesh relays.
Preserve the original IP/UDP bytes and deliver received broadcasts only to the
local TUN; never reflood them or expose another pair's plaintext at transit.

Build immutable recipient snapshots on address and participation changes. The
origin sends one ordinary end-to-end encrypted copy per recipient; the existing
fast, bounded-hop transport router remains unchanged. Validate UDP framing,
source ownership and destination admission without game-specific port rules.
Keep network domains isolated and refuse implicit gateways to physical LANs.
The separate broadcast policy/domain layer is the extension point for future
authorized subnet exports; physical capture, bridging and LAN deduplication
are deliberately not implemented yet.

Persist default-on participation independently for each local network. Add
join --no-broadcast/--broadcast and network broadcast <id> [on|off], including
live updates and authenticated announcements. Joining without a flag preserves
the saved choice. Opt-out stops local origination and delivery, while opaque
unicast transit for other members keeps working.

Migrate SQLite schema 3 to 4 without replacing identities or signed state.
Use control ALPN 3 and local IPC protocol 14 for the new announcement/request
shapes; update peers and restart running agents together. The data ALPN 4
envelope remains unchanged. No release version bump, tag or push is included.

Document agent-owned commits in AGENTS.md: short English subjects, explanatory
bodies, scoped staging, honest validation, and repository-local fallback author
AB <ab@hexor.cy> only when an effective name/email is missing. Release actions
remain the user's responsibility.

Validation on Windows: cargo fmt --all -- --check; cargo check --locked
--workspace --all-targets; cargo clippy --locked --workspace --all-targets --
-D warnings; release workspace/all-target tests: 313 passed. The two existing
SQLite wipe failures (a_wipe_removes_everything_and_the_next_start_is_a_stranger
and wiping_twice_is_as_ordinary_as_wiping_once) were explicitly skipped; the
public-DHT smoke test and forwarding benchmark remain ignored by default.
New coverage exercises real iroh/WireGuard multihop fanout, single delivery,
runtime opt-out, unicast replies, domain isolation, malformed input and schema
migration. TUNs are in-memory; actual games and OS adapter selection were not
tested.
This commit is contained in:
ab
2026-09-22 18:33:52 +03:00
parent b4f3e57c8d
commit c724981bfd
23 changed files with 1110 additions and 21 deletions
+1
View File
@@ -60,6 +60,7 @@ not tunnel control sessions. See [routing.md](routing.md).
| `dataplane::routing` | transport-independent graph, shortest paths and opaque flow identifiers |
| `dataplane::relay` | immutable forwarding snapshots and transport-to-transport transit |
| `dataplane` | the contract an IP protocol implements |
| `overlay::broadcast` | domain-scoped IPv4 UDP discovery fanout and receive admission |
| `overlay` | the one interface an agent owns: provisioning, the TUN, whose packet is whose |
| `dns` | the DNS view of a network, and telling the system resolver about it |
+91
View File
@@ -0,0 +1,91 @@
# LAN discovery broadcasts
Broadcast participation is a **local setting of each network**, enabled by
default. Different networks on the same agent can have different settings;
each member advertises whether it accepts broadcasts over its authenticated
control session. It is not a network-wide vote or signed shared configuration.
```sh
tsunagi join -n games --no-broadcast
tsunagi join -n games --broadcast
tsunagi network broadcast <network-id-or-prefix> off
tsunagi network broadcast <network-id-or-prefix> on
tsunagi network broadcast <network-id-or-prefix>
```
Joining without either flag preserves the saved choice. The `network broadcast`
command can change a running network immediately or configure it while the
agent is stopped. `status` reports the local setting. There is no global `up`
override: starting the agent preserves every network's independent policy.
## Delivery
At local TUN ingress, the IP router recognizes IPv4 UDP addressed to the limited
broadcast `255.255.255.255` or the configured overlay subnet's directed broadcast
(for example `10.13.37.255` for `/24`). These address forms follow
[RFC 1122 section 3.3.6](https://www.rfc-editor.org/rfc/rfc1122#section-3.3.6).
There is no port allowlist: discovery requests and server announcements on
game-specific UDP ports use the same mechanism. IPv4/UDP lengths are checked
before fanout, including handling IP fragments. IPv6 multicast, mDNS, DHCP
bridging, Ethernet/IPX and physical LAN capture are outside this feature.
The local source address selects exactly one broadcast domain. A limited
broadcast is never copied into all networks merely because they share a TUN.
A directed broadcast must belong to that source's network. Unspecified sources
and physical LAN sources have no authorized domain yet. Recipients are live,
authenticated, opted-in members with signed overlay address ownership. The
immutable recipient list is rebuilt with address/policy changes, not per packet.
The origin creates **one encrypted unicast copy per eligible peer** and hands it
to the existing shortest-path router. Each can travel through several transport
links. Transit handles opaque ciphertext with the existing hop limit and does
not create another fanout. The original IP source, destination, TTL, UDP ports,
checksum and payload remain unchanged. Reply packets use ordinary unicast.
There is no broadcast retransmission timer or application-payload deduplication:
games may legitimately repeat the same discovery query. WireGuard replay checks
reject duplicated encrypted packets delivered by the transport.
At the destination, the usual authenticated-source ownership check still runs.
Broadcast admission also checks the receiving network's setting, the sender's
announced participation, the destination domain and UDP framing. Disabling it
takes effect locally even while a remote sender still has an older announcement.
Received broadcasts terminate in the local TUN and never enter the fanout path.
An ordinary decrypted packet must target this host's address in that network;
the TUN is not a transit gateway to another overlay or physical subnet.
Opt-out disables originating and accepting broadcasts on that host/network.
It does not disable opaque unicast transit for other members: intermediate
routers cannot inspect another pair's encrypted IP payload.
## Host behavior and verification
The game must send discovery through the Tsunagi interface. Traffic bound to a
physical adapter never reaches this TUN, and this feature does not capture it.
The suite tests real agents, authenticated iroh links and WireGuard encryption
with memory TUNs: limited/directed UDP discovery, multihop fanout, single-copy
delivery, opt-out, unicast replies, malformed input, source validation and
network isolation. It does not launch CS 1.6 or Warcraft III or establish that
every game/host chooses the virtual adapter automatically.
## Future exported LANs
`overlay::broadcast` owns domain selection and recipient policy separately from
IP parsing, encryption and encrypted transit. A future LAN adapter should feed
an explicitly authorized ingress domain into this layer, and export routes
through the corresponding source/destination admission policy. It must not
relax address checks globally or reinterpret all `.255` addresses as broadcasts.
Multiple gateways to the same LAN will need origin identifiers and bounded
duplicate suppression at LAN ingress/egress before physical rebroadcast is
enabled. No subnet export or host forwarding configuration is added here.
## Compatibility
Control ALPN is `tsunagi/ctrl/3` because `Announce` now carries participation.
The encrypted transit envelope and data ALPN `tsunagi/data/4` stay unchanged.
Local control protocol 14 rejects commands from mismatched running binaries
with a restart message instead of decoding a different request shape.
Upgrade all members and restart the agent before using the new CLI.
SQLite schema 4 adds `networks.broadcast` with default `1`. Existing network
names, secrets, identity keys and signed records are retained. The old binary
does not understand schema 4; the migration is not a downgrade mechanism.
+8 -3
View File
@@ -9,7 +9,7 @@ Two versions exist and are independent:
- **Identity scheme**, `tsunagi-network-id-v1`. Frozen. Changing it creates a
different network space for the same name and secret.
- **Control protocol**, ALPN `tsunagi/ctrl/2`, `PROTOCOL_VERSION = 2`.
- **Control protocol**, ALPN `tsunagi/ctrl/3`, `PROTOCOL_VERSION = 3`.
Upgrading the crate or bumping the control protocol must never change an
existing `NetworkId`.
@@ -191,7 +191,7 @@ transport connection, not supplied by the frame. Intermediate nodes cannot
decrypt or authenticate the inner WireGuard payload; the destination does that.
Flow ids are routing hints, not authorization proofs.
Control ALPN 2 carries `Reachable { links: [{ peer, protocol }] }`. Each row
Control ALPN 3 carries `Reachable { links: [{ peer, protocol }] }`. Each row
belongs to the authenticated sender and is replaced atomically, expires after
90 seconds, and is withdrawn on session closure. Only compatible authenticated
members enter a protocol's graph; local edges always come from actual links.
@@ -215,7 +215,7 @@ not affect other networks.
| message | meaning |
|---|---|
| `Announce { hostname, capabilities }` | this agent's hostname and IP-plugin capabilities |
| `Announce { hostname, capabilities, broadcast }` | this agent's hostname, IP-plugin capabilities and local broadcast participation |
| `Ping { seq, payload }` | small request used to verify the exchange |
| `Pong { seq, payload }` | the echoed reply |
| `State { records }` | a snapshot of signed records, merged into what the receiver holds |
@@ -223,6 +223,11 @@ not affect other networks.
| `Reachable { links }` | sender's current direct data links, scoped by protocol |
| `Bye { reason }` | graceful goodbye; not a revocation of anything |
`broadcast` is a per-network local opt-in, enabled by default and treated as
false until an authenticated announcement arrives. It governs IP broadcast
fanout and local admission; encrypted transit still uses the existing envelope.
See [broadcast.md](broadcast.md) for scope and persistence.
A `State` snapshot is merged, never substituted: an author missing from it is
left untouched. Each record carries its own signature, so a peer forwarding
somebody else's record cannot alter it, and a record that fails verification
+1 -1
View File
@@ -86,5 +86,5 @@ counts sends without storing frames. Results are CPU forwarding cost, **not**
end-to-end network latency or VPN throughput; encryption, fragmentation,
sockets, congestion and scheduling contribute separately.
Wire compatibility: control ALPN `tsunagi/ctrl/2`, data ALPN `tsunagi/data/4`.
Wire compatibility: control ALPN `tsunagi/ctrl/3`, data ALPN `tsunagi/data/4`.
Upgrade every participant together; saved identities and network state persist.
+9
View File
@@ -76,6 +76,15 @@ links. Real encrypted 1280-byte TCP packets travel in both directions while the
middle TUNs remain empty. A direct A—D link is enabled, then removed; the route
switches back to the chain without replacing end-to-end tunnels.
Broadcast tests use limited and directed UDP game discovery packets over real
WireGuard tunnels, including a missing direct link. Each willing peer gets one
copy; disabling reception/origination works at runtime and unicast still works.
IP-router tests cover source/destination domain checks, malformed UDP and the
absence of reflection into outgoing fanout. SQLite migration tests preserve v3
identity/settings and check opt-out after reopen/rejoin. CLI tests cover default
on, per-network opt-out, runtime updates and conflicting flags. Actual games and
host adapter selection are not simulated by these tests.
The ignored `forwarding_benchmark` measures the synchronous transit routine in
release mode, excluding crypto and socket I/O. Run it explicitly as described
in [routing.md](routing.md); it has no timing threshold in the default suite.