Peers can now end their relay session with a post-handshake Disconnect
control message, but the relay previously only returned from the peer loop.
Explicitly close that peer connection before leaving the room so graceful
disconnects are finite from both sides.
The relay still runs the normal leave path after the close. That keeps room
cleanup, session removal, and PeerLeft notification centralized, while the
PeerLeft reason now comes from the peer's Disconnect message.
The integration test covers two connected clients. One sends a TimedOut
Disconnect, the relay closes that client connection, and the remaining peer
receives PeerLeft with the TimedOut reason.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay \
forwards_graceful_disconnect_reason_to_remaining_peers \
-- --nocapture
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
Peers can now send post-handshake Stats messages over unidirectional control
streams. The relay listens for those streams alongside Ethernet datagrams,
stores the latest snapshot on the peer session, and logs the counters with
room, peer, and role context.
Unexpected post-handshake control messages now close the peer as a protocol
error. Peer-requested Disconnect messages leave the room through the normal
cleanup path, which keeps lifecycle notifications centralized.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay \
forwards_ethernet_datagrams_between_joined_peers \
-- --nocapture
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay now sends PeerJoined catch-up events to a newly accepted peer for
peers that were already present in the room. This makes lifecycle delivery
symmetric enough for clients and gateways to learn the current room membership
after welcome, not only future joins.
The catch-up list is built from a cloned room snapshot before opening control
event streams, so room state is not locked across QUIC I/O. Delivery remains
best-effort and uses the same one-frame unidirectional control stream path as
live PeerJoined and PeerLeft notifications.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay \
forwards_ethernet_datagrams_between_joined_peers -- --nocapture
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay now sends a reliable PeerLeft control event to remaining room peers
after removing the departed peer from session and room state. Normal connection
closure is reported as Normal, while malformed-datagram disconnects are reported
as ProtocolError.
This completes the first relay-side lifecycle event pair. Delivery remains
best-effort and client-side event consumption is intentionally left for a
separate slice.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay forwards_ethernet_datagrams_between_joined_peers \
-- --nocapture
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay now sends a reliable PeerJoined control event to peers that were
already present in the room after a new peer completes the hello/welcome
handshake. Events are sent on one-frame unidirectional QUIC streams, reusing the
existing control codec without keeping the room/session lock across I/O.
Delivery is best-effort for this first lifecycle slice: a notification failure
is logged, but the newly accepted peer remains joined. PeerLeft delivery and
client-side event consumption remain separate follow-up work.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay forwards_ethernet_datagrams_between_joined_peers \
-- --nocapture
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
ServerWelcome now carries an initial gateway_connected flag with serde defaulting
for older welcome payloads. The relay sets it from room admission state so a
gateway sees itself as connected, clients joining behind an existing gateway see
yes, and clients that arrive first see no.
The Windows client prints that handshake fact at startup. This does not replace
the later peer-event stream; it gives phase-1 diagnostics an immediate answer
for whether the relay already has a LAN gateway in the room.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-ctrl -p lanparty-relay -p lanparty-client-core \
-p lanparty-client-win
- cargo clippy -p lanparty-ctrl -p lanparty-relay -p lanparty-client-core \
-p lanparty-client-win --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay models the physical LAN as the gateway port, not as another remote
client. Client-originated unknown unicast now forwards only to the gateway, and
gateway-originated unknown unicast is dropped unless it resolves to a registered
remote client. Broadcast and multicast fanout is unchanged.
This prevents promiscuous gateway capture of unrelated LAN unicast from being
flooded to every remote client. It also keeps client-to-LAN traffic from
needlessly leaking to other clients in the room.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
Client-originated Ethernet frames now pass through a per-peer byte token bucket
after source and safety validation and before destination-specific forwarding.
The fixed MVP default allows a 4 MiB burst with a 2 MiB/s refill, giving normal
LAN-game traffic headroom while preventing one client from filling the relay
path indefinitely.
The existing frame burst buckets now share the same `TokenBucket` type with a
unit cost of one frame. Keeping this state in `Room` preserves forwarding-policy
ownership and lets tests drive explicit instants.
This completes the relay-side abuse limit list from PLAN.md. Configurable rate
limits remain future work.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
Client-originated unknown unicast now has its own relay-side token bucket.
When a client sends to an unlearned unicast MAC, the relay consumes from that
bucket before flooding the frame to the room.
Known unicast to a registered client bypasses this limit, and broadcast or
multicast traffic continues to use its separate bucket. Keeping the buckets in
room state matches the existing forwarding-policy ownership and lets unit tests
drive explicit instants.
This is the second rate-limit slice from PLAN.md. A total bandwidth cap remains
future work.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay now applies a small token bucket to broadcast and multicast frames
originating from remote clients. Those frames are necessary for ARP, DHCP, and
LAN discovery, but they are also the easiest way for one remote peer to flood
every participant and the LAN gateway. When a client exceeds the burst budget,
the relay returns a rate-limited forwarding decision instead of forwarding the
frame.
This is intentionally only the first rate-limit slice from PLAN.md. Unknown
unicast limits and total bandwidth limits remain separate follow-up work. The
limiter lives in room state because forwarding policy already knows the ingress
role, destination MAC, and room membership, and tests can drive it with explicit
Instants without involving QUIC timing.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
The relay now tracks malformed datagrams per accepted peer and closes the QUIC
connection after a small threshold. Malformed overlay bytes, datagrams with the
wrong room/peer/type header, and malformed Ethernet payloads all count toward
that threshold.
This implements the malformed-packet disconnect part of PLAN.md without mixing
in broader bandwidth or broadcast rate limiting. Ordinary safety-filter drops
still remain non-fatal; this only targets peers that repeatedly send packets the
relay cannot treat as valid tunnel Ethernet traffic.
The threshold state lives in the relay server loop, while the forwarding helper
returns a small outcome enum so malformed classification stays testable without
running a full QUIC server. The room registry remains responsible for Ethernet
policy decisions such as unauthorized source MACs, jumbo frames, and control
plane filters.
Test Plan:
- cargo fmt --check
- cargo test -p lanparty-relay
- cargo clippy -p lanparty-relay --all-targets -- -D warnings
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md
Phase 1 needs noisy frame diagnostics while the tunnel is being proven on real
LANs. The relay already had forwarding/drop decisions, but the runtime did not
emit the MAC-level fields needed to understand what happened to each frame.
Print one relay ingress log line for every accepted Ethernet datagram after the
room registry decides whether to forward or drop it. The line includes room,
peer id, source/destination MACs, ethertype or length, frame length, action,
drop reason, and target count using the shared diagnostics vocabulary.
This keeps logging simple stdout text for now. A later product slice can route
the same `lanparty-obs` fields through tracing or JSON logs without changing the
forwarding rules.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md logging diagnostics
The gateway and Windows client now pin a relay certificate, but local relay
runs generated an ephemeral self-signed certificate only in memory. That made
the development trust flow awkward because there was no stable DER artifact to
feed into the new CLIs.
Add `--dev-cert-der-out` to write the generated development certificate before
the relay binds its endpoint. The file is DER-encoded and parent directories
are created when needed. This keeps the production certificate/key path explicit
future work while making the current pinned-trust flow usable.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
Refs: PLAN.md relay/client trust bootstrap
The gateway binary now has a real relay-facing configuration and QUIC control
handshake. It accepts a relay socket address, expected TLS server name, pinned
DER relay certificate, room code, LAN interface name, and advertised datagram
budget, then connects as role = gateway and waits for a welcome response.
The ALPN token moved into lanparty-ctrl so relay and gateway share the same
protocol identifier instead of carrying duplicate private constants. The gateway
still stops after the control-plane connection; AF_PACKET capture and injection
remain a later slice.
The connector test spins up a local Quinn server with a self-signed certificate,
trusts that certificate explicitly, verifies the outgoing gateway hello, and
checks the received welcome metadata.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md Linux gateway outbound relay connection
Relay forwarding now applies the MVP L2 safety policy before choosing output
peers. It drops jumbo frames, link-local switch-control destinations, EAPOL,
LLDP, and slow-protocol frames in both directions, and it blocks remote clients
from sending DHCP server replies or IPv6 router advertisements toward the LAN.
The filters live in the room forwarding path so the pure admission/forwarding
tests and live QUIC datagram path share the same policy. Gateway-origin DHCP
server replies remain allowed, which preserves the plan's goal that remote TAP
clients can receive LAN DHCP through the tunnel.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md L2 control-plane safety filters
The relay now keeps active peer sessions alongside room admission state. After
a successful hello/welcome handshake, the connection enters a datagram loop
and stays registered until the QUIC connection closes.
Incoming datagrams are only considered for forwarding when their overlay room
id, peer id, and Ethernet frame type match the peer assigned by the relay.
The relay then reuses the existing room forwarding decision logic, clones the
matching live target sessions, and sends a relay-stamped Ethernet datagram to
each connected target that can carry the frame.
This keeps spoofable wire metadata out of the trust boundary: clients can put
whatever they want in an overlay header, but the relay forwards using the
room and peer identity established during the control handshake.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md QUIC DATAGRAM Ethernet forwarding path
The relay now keeps a shared room registry behind the QUIC endpoint and
runs an accept loop instead of only binding the socket. Each accepted
connection must open its first bidirectional control stream with a hello
frame; the relay joins the room registry and replies with welcome or reject.
Admission clamps the hello datagram budget to Quinn's negotiated peer
datagram size before choosing the effective room MTU, so room state is based
on what the connection can actually carry. Accepted peers remain present
until the QUIC connection closes, then the relay removes them through the
existing leave cleanup path.
The development self-signed certificate helper now exposes the certificate
to tests so a loopback Quinn client can trust the relay and exercise the real
stream codec path.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md relay QUIC control-stream startup flow
Add explicit room leave semantics for future relay connection tasks. Disconnect
handling will need to remove peers from room membership without reaching into the
room internals or leaving stale MAC indexes behind.
Leaving a client now removes both its peer entry and MAC mapping so the same MAC
can rejoin later. Leaving a gateway clears gateway occupancy while preserving any
remaining clients. If the last peer leaves, the room is removed from the
registry. The result reports which peer left and whether the room was removed so
the networking layer can emit lifecycle events cleanly.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md relay disconnect reason and reconnect handling groundwork
Make the relay binary bind a real Quinn endpoint instead of only printing its
configuration. This is the next runtime step toward the public relay while still
keeping connection handling out of this commit.
The relay now builds a self-signed development TLS configuration, advertises the
lanparty ALPN, enables QUIC datagram buffers, binds the configured UDP address,
prints the actual local address, and waits for Ctrl-C before closing the
endpoint. The generated certificate is explicitly a development placeholder;
production certificate and client trust handling remain future work.
The rustls dependency is pinned to the ring provider to match Quinn's selected
crypto backend and avoid process-level provider ambiguity at runtime.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- timeout 2s cargo run -p lanparty-relay -- --listen 127.0.0.1:0 || test $? -eq 124
Refs: PLAN.md public relay QUIC data path
Refs: https://docs.rs/quinn/0.11.9
Replace the placeholder relay binary with a typed command-line configuration
entry point. This gives the future QUIC server loop the listen endpoint and room
limit configuration it needs without mixing command parsing into networking or
room-state code.
The relay now accepts --listen as either a socket address or an explicit UDP
shorthand such as 443/udp, defaults to 0.0.0.0:443/udp, and validates that the
per-room client limit is positive. The binary currently reports the parsed
configuration and clearly states that the QUIC server loop is not wired yet, so
this commit does not pretend to provide a working relay.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- cargo run -p lanparty-relay -- --listen 443/udp
Refs: PLAN.md public relay --listen requirement
Add socket-free relay forwarding logic for Ethernet datagrams. The future QUIC
relay loop can now ask the room registry which peer IDs should receive a frame
instead of embedding switching policy in network IO code.
Forwarding validates that the ingress peer belongs to the room, drops malformed
Ethernet frames, rejects client frames whose source MAC does not match the MAC
announced during admission, never reflects frames back to ingress, routes known
client unicasts directly, and floods broadcast/multicast or unknown unicast
frames to the other room peers. The decision reports shared observability action
and drop-reason values so the networking layer can log consistently.
This still does not send bytes over QUIC; it only defines the room-local switch
decision that the datagram loop will use.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md Switching model
Add a tested relay room layer before introducing QUIC socket handling. The relay
now has a focused place to enforce room membership rules instead of mixing those
rules into the future networking loop.
RoomRegistry accepts validated endpoint hellos, assigns room and peer IDs,
returns server welcome data, limits clients per room, permits only one gateway,
rejects duplicate client MACs, and keeps the room TAP MTU stable once the first
peer joins. A later peer must support the existing room MTU rather than silently
shrinking it after an earlier client may already have configured its TAP adapter.
The networking pieces still need to call this layer from the reliable control
stream and use the resulting peer metadata for datagram forwarding.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
Refs: PLAN.md relay responsibilities and MAC identity