Compare commits
5
Commits
b527019b76
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e62f584377
|
||
|
|
2bed62e9ec | ||
|
|
9175eedce9 | ||
|
|
16d0886f36
|
||
|
|
a66ea31ab3
|
@@ -0,0 +1,19 @@
|
||||
# Agent Instructions
|
||||
|
||||
## Commit Policy
|
||||
|
||||
Automatically commit changes once a full feature, bugfix, refactor, or other
|
||||
coherent unit of work is finished. Do not wait for the user to ask for a commit.
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
Only update the version, when the user explicitly asks for it.
|
||||
|
||||
### Guidelines for how to update the version
|
||||
|
||||
1. Use `cargo set-version` to bump the version. Decide, based on the actual
|
||||
changes, based on semver semantics, if major, minor or patch needs to be
|
||||
bumped.
|
||||
2. Update the CHANGELOG.md file accordingly.
|
||||
3. Create a release commit.
|
||||
4. Tag the release commit in the style of previous versioning tags.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-08-16
|
||||
|
||||
### Added
|
||||
|
||||
- **QUIC-based Layer 2 VPN Relay (`lanparty-relay`)**:
|
||||
- Room-isolated virtual Layer 2 Ethernet switching over QUIC datagrams
|
||||
(`quinn` / `rustls`).
|
||||
- Learning switch (CAM table) for unicast frame forwarding and
|
||||
broadcast/multicast flooding.
|
||||
- L2 safety filters: client source MAC anti-spoofing, rogue DHCPv4/DHCPv6
|
||||
server reply filtering, IPv6 Router Advertisement suppression (including
|
||||
behind extension headers), IPv6 fragment filtering, remote VLAN tag
|
||||
filtering, and malformed frame rejection.
|
||||
- Token-bucket rate limiting for broadcast, unknown unicast, and total client
|
||||
bandwidth.
|
||||
- Development TLS certificate generation helper.
|
||||
- **Linux Physical Gateway Bridge (`lanparty-gateway`)**:
|
||||
- Promiscuous `AF_PACKET` raw socket bridge connecting local physical LAN
|
||||
networks to virtual relay rooms.
|
||||
- Ingress/egress datagram budget enforcement, carrier validation, and
|
||||
self-injection filtering.
|
||||
- **Windows Client (`lanparty-client-win`, `lanparty-client-core`,
|
||||
`lanparty-client-tap`, `lanparty-client-route`)**:
|
||||
- TAP-Windows6 adapter management and Ethernet frame I/O.
|
||||
- Scoped Windows IP route management: relay host route pinning, TAP default
|
||||
route suppression, and interface metric configuration.
|
||||
- Virtual MAC persistence and reconnect handling.
|
||||
- Real-time diagnostic reporting for RTT, LAN DHCP lease state, gateway
|
||||
presence, and frame flow counters.
|
||||
- **Shared Infrastructure**:
|
||||
- Protocol primitives (`lanparty-proto`) for datagram framing, MTU
|
||||
negotiation, and safety classifications.
|
||||
- Control plane (`lanparty-ctrl`) for room admission, peer lifecycle events,
|
||||
and stats snapshots.
|
||||
- Observability suite (`lanparty-obs`) for structured frame logging and
|
||||
diagnostic formatting.
|
||||
- Network address resolution utilities (`lanparty-net`).
|
||||
Generated
+50
-50
@@ -138,9 +138,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.3.0"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -171,9 +171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.4"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -181,9 +181,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.2"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -252,9 +252,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
@@ -278,13 +278,13 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -311,9 +311,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
@@ -323,21 +323,21 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -441,9 +441,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -655,9 +655,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
@@ -716,9 +716,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
@@ -836,9 +836,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.14.8"
|
||||
version = "0.14.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
|
||||
checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
@@ -888,9 +888,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.42"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -914,9 +914,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
@@ -951,9 +951,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1157,18 +1157,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1177,9 +1177,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.54"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
@@ -1237,13 +1237,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1302,9 +1302,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1315,9 +1315,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1325,9 +1325,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1338,9 +1338,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
+24
-5
@@ -20,15 +20,34 @@ edition = "2024"
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
bytes = "1"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
getrandom = "0.4.2"
|
||||
clap = { version = "4.6.6", features = ["derive"] }
|
||||
getrandom = "0.4.3"
|
||||
libc = "0.2"
|
||||
quinn = "0.11.9"
|
||||
rcgen = "0.14.8"
|
||||
quinn = "0.11.11"
|
||||
rcgen = "0.14.9"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
tracing = "0.1"
|
||||
windows-sys = "0.61.2"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
strip = false
|
||||
debug-assertions = true
|
||||
overflow-checks = true
|
||||
lto = false
|
||||
panic = "unwind"
|
||||
incremental = true
|
||||
|
||||
[profile.production]
|
||||
inherits = "release"
|
||||
debug = false
|
||||
strip = true
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
lto = true
|
||||
incremental = false
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
# PLAN
|
||||
|
||||
What I want to do:
|
||||
|
||||
A simple one-click Layer 2 tunnel software (Windows 11 client) to bridge people who cannot participate in person at a LAN party to the LAN party. And a simple server endpoint (Linux) software that runs physically at the LAN party and bridges the tunneled traffic and the real LAN network.
|
||||
A simple one-click Layer 2 tunnel software (Windows 11 client) to bridge people
|
||||
who cannot participate in person at a LAN party to the LAN party. And a simple
|
||||
server endpoint (Linux) software that runs physically at the LAN party and
|
||||
bridges the tunneled traffic and the real LAN network.
|
||||
|
||||
I already talked a bit with different AIs about how to do this, here's the current plan:
|
||||
I already talked a bit with different AIs about how to do this, here's the
|
||||
current plan:
|
||||
|
||||
# LAN Party Tunnel Plan
|
||||
## LAN Party Tunnel Plan
|
||||
|
||||
Build a **TAP-based L2-over-QUIC tunnel**.
|
||||
|
||||
The remote Windows client gets a real virtual Ethernet adapter. Ethernet frames from that adapter are sent over QUIC to a public relay. The relay forwards them to a Linux gateway at the LAN party. The Linux gateway injects those frames onto the physical LAN and captures replies.
|
||||
The remote Windows client gets a real virtual Ethernet adapter. Ethernet frames
|
||||
from that adapter are sent over QUIC to a public relay. The relay forwards them
|
||||
to a Linux gateway at the LAN party. The Linux gateway injects those frames onto
|
||||
the physical LAN and captures replies.
|
||||
|
||||
```text
|
||||
Windows game
|
||||
@@ -21,13 +30,10 @@ Windows game
|
||||
⇄ physical Ethernet LAN
|
||||
```
|
||||
|
||||
No WireGuard.
|
||||
No Npcap.
|
||||
No Windows bridge.
|
||||
No packet rewriting from the user’s real NIC.
|
||||
No tunnel fragmentation for MVP.
|
||||
No WireGuard. No Npcap. No Windows bridge. No packet rewriting from the user’s
|
||||
real NIC. No tunnel fragmentation for MVP.
|
||||
|
||||
## Goal
|
||||
### Goal
|
||||
|
||||
The remote player should do this:
|
||||
|
||||
@@ -53,11 +59,12 @@ The public server does this:
|
||||
lanparty-relay --listen 443/udp
|
||||
```
|
||||
|
||||
UDP/443 is a good default, but the port must be configurable because some networks block QUIC/UDP.
|
||||
UDP/443 is a good default, but the port must be configurable because some
|
||||
networks block QUIC/UDP.
|
||||
|
||||
## Components
|
||||
### Components
|
||||
|
||||
### 1. Windows client: `lanparty-client.exe`
|
||||
#### 1. Windows client: `lanparty-client.exe`
|
||||
|
||||
Written in Rust.
|
||||
|
||||
@@ -75,9 +82,13 @@ Responsibilities:
|
||||
- keep the relay connection routed through the real internet NIC
|
||||
```
|
||||
|
||||
Use a real TAP/Ethernet adapter. `tap-windows6` is an NDIS TAP-Windows driver used by OpenVPN and other apps, which is the right class of device here because we need Ethernet frames, not just IP packets. ([GitHub][1])
|
||||
Use a real TAP/Ethernet adapter. `tap-windows6` is an NDIS TAP-Windows driver
|
||||
used by OpenVPN and other apps, which is the right class of device here because
|
||||
we need Ethernet frames, not just IP packets. ([GitHub][1])
|
||||
|
||||
Do **not** use Wintun for this design. Wintun is L3/TUN-style and does not give you the Ethernet/L2 behavior needed for ARP, DHCP, broadcast discovery, and old LAN games.
|
||||
Do **not** use Wintun for this design. Wintun is L3/TUN-style and does not give
|
||||
you the Ethernet/L2 behavior needed for ARP, DHCP, broadcast discovery, and old
|
||||
LAN games.
|
||||
|
||||
The TAP adapter is the remote player’s LAN-party identity.
|
||||
|
||||
@@ -88,7 +99,7 @@ Game sends ARP/broadcast/multicast through TAP
|
||||
Client tunnels the Ethernet frames
|
||||
```
|
||||
|
||||
### 2. Linux gateway: `lanparty-gateway`
|
||||
#### 2. Linux gateway: `lanparty-gateway`
|
||||
|
||||
Runs on the physical LAN party machine.
|
||||
|
||||
@@ -104,15 +115,23 @@ Responsibilities:
|
||||
- periodically refresh switch CAM table entries
|
||||
```
|
||||
|
||||
Use Linux `AF_PACKET` / `SOCK_RAW` on the real wired NIC. Packet sockets operate at device-driver / OSI Layer 2 level, and `SOCK_RAW` includes the link-layer header, which is exactly what we need for Ethernet frames. ([man7.org][2])
|
||||
Use Linux `AF_PACKET` / `SOCK_RAW` on the real wired NIC. Packet sockets operate
|
||||
at device-driver / OSI Layer 2 level, and `SOCK_RAW` includes the link-layer
|
||||
header, which is exactly what we need for Ethernet frames. ([man7.org][2])
|
||||
|
||||
For MVP, run as root. Later, reduce privileges. Opening raw sockets and changing/promiscuous network behavior needs elevated networking privileges; `CAP_NET_ADMIN` covers things like setting promiscuous mode, and `CAP_NET_RAW` covers raw packet access. ([man7.org][3])
|
||||
For MVP, run as root. Later, reduce privileges. Opening raw sockets and
|
||||
changing/promiscuous network behavior needs elevated networking privileges;
|
||||
`CAP_NET_ADMIN` covers things like setting promiscuous mode, and `CAP_NET_RAW`
|
||||
covers raw packet access. ([man7.org][3])
|
||||
|
||||
No Linux bridge is needed for MVP. No `br0`. No moving the host’s IP from `eth0` to a bridge. The gateway daemon directly captures and injects frames on the physical NIC.
|
||||
No Linux bridge is needed for MVP. No `br0`. No moving the host’s IP from `eth0`
|
||||
to a bridge. The gateway daemon directly captures and injects frames on the
|
||||
physical NIC.
|
||||
|
||||
Wired Ethernet only. No Wi-Fi gateway mode for MVP. Managed Wi-Fi NICs are not reliable for arbitrary source-MAC injection.
|
||||
Wired Ethernet only. No Wi-Fi gateway mode for MVP. Managed Wi-Fi NICs are not
|
||||
reliable for arbitrary source-MAC injection.
|
||||
|
||||
### 3. Public relay: `lanparty-relay`
|
||||
#### 3. Public relay: `lanparty-relay`
|
||||
|
||||
Runs on VPS/public server.
|
||||
|
||||
@@ -139,7 +158,7 @@ gateway → outbound QUIC → relay
|
||||
|
||||
No port forwarding. No NAT traversal pain. Direct P2P can come later.
|
||||
|
||||
## Transport
|
||||
### Transport
|
||||
|
||||
Use QUIC.
|
||||
|
||||
@@ -158,11 +177,16 @@ disconnect reason
|
||||
future auth
|
||||
```
|
||||
|
||||
Use QUIC DATAGRAM for Ethernet frames. QUIC DATAGRAM is specifically the unreliable datagram extension for QUIC, which fits Ethernet/game traffic better than reliable streams because old frames should not block newer frames. ([IETF Datatracker][4])
|
||||
Use QUIC DATAGRAM for Ethernet frames. QUIC DATAGRAM is specifically the
|
||||
unreliable datagram extension for QUIC, which fits Ethernet/game traffic better
|
||||
than reliable streams because old frames should not block newer frames. ([IETF
|
||||
Datatracker][4])
|
||||
|
||||
Rust QUIC implementation: start with `quinn`. It exposes `Connection::max_datagram_size()`, which returns the maximum datagram payload size or `None` if datagrams are unsupported/disabled. ([Docs.rs][5])
|
||||
Rust QUIC implementation: start with `quinn`. It exposes
|
||||
`Connection::max_datagram_size()`, which returns the maximum datagram payload
|
||||
size or `None` if datagrams are unsupported/disabled. ([Docs.rs][5])
|
||||
|
||||
## No fragmentation for MVP
|
||||
### No fragmentation for MVP
|
||||
|
||||
Do **not** fragment Ethernet frames inside the overlay.
|
||||
|
||||
@@ -200,9 +224,10 @@ tap_mtu <= quic_max_datagram_size
|
||||
- safety_margin
|
||||
```
|
||||
|
||||
No fragment table. No reassembly timeout. No “one lost fragment kills the whole Ethernet frame.” Add fragmentation later only if testing proves it is necessary.
|
||||
No fragment table. No reassembly timeout. No “one lost fragment kills the whole
|
||||
Ethernet frame.” Add fragmentation later only if testing proves it is necessary.
|
||||
|
||||
## Overlay frame format
|
||||
### Overlay frame format
|
||||
|
||||
Keep the outer routing header small and stable.
|
||||
|
||||
@@ -226,13 +251,16 @@ clear routing header
|
||||
encrypted Ethernet payload
|
||||
```
|
||||
|
||||
MVP can skip payload encryption beyond QUIC, but the wire format should not make later E2E encryption painful.
|
||||
MVP can skip payload encryption beyond QUIC, but the wire format should not make
|
||||
later E2E encryption painful.
|
||||
|
||||
## Trust model
|
||||
### Trust model
|
||||
|
||||
MVP relay sees plaintext Ethernet frames.
|
||||
|
||||
QUIC encrypts traffic on the wire, but because the relay terminates QUIC connections, it decrypts frames from clients and re-encrypts them to the gateway.
|
||||
QUIC encrypts traffic on the wire, but because the relay terminates QUIC
|
||||
connections, it decrypts frames from clients and re-encrypts them to the
|
||||
gateway.
|
||||
|
||||
That is acceptable for a LAN-party MVP, but it should be explicitly documented.
|
||||
|
||||
@@ -246,7 +274,7 @@ relay only sees room id, peer id, size, timing
|
||||
|
||||
Do not retrofit this into a bad packet format later. Reserve the shape now.
|
||||
|
||||
## Switching model
|
||||
### Switching model
|
||||
|
||||
Treat the whole thing as a tiny user-space Ethernet switch.
|
||||
|
||||
@@ -285,7 +313,7 @@ LAN frames go to matching remote client or all clients if broadcast/multicast
|
||||
|
||||
But MAC learning belongs in the real design.
|
||||
|
||||
## MAC identity
|
||||
### MAC identity
|
||||
|
||||
Each Windows client needs a unique locally administered unicast MAC.
|
||||
|
||||
@@ -295,7 +323,8 @@ Example range:
|
||||
02:xx:xx:xx:xx:xx
|
||||
```
|
||||
|
||||
Generate once per install or per profile. Store it. Configure TAP with it. Announce it during join.
|
||||
Generate once per install or per profile. Store it. Configure TAP with it.
|
||||
Announce it during join.
|
||||
|
||||
Relay must reject:
|
||||
|
||||
@@ -315,11 +344,13 @@ maybe 2 later for weird cases
|
||||
|
||||
This is your responsibility, not the user’s.
|
||||
|
||||
## Linux gateway CAM-table refresh
|
||||
### Linux gateway CAM-table refresh
|
||||
|
||||
The physical LAN switch must learn that remote clients’ MACs live behind the gateway port.
|
||||
The physical LAN switch must learn that remote clients’ MACs live behind the
|
||||
gateway port.
|
||||
|
||||
That happens when the gateway injects frames onto the LAN using the remote client’s source MAC.
|
||||
That happens when the gateway injects frames onto the LAN using the remote
|
||||
client’s source MAC.
|
||||
|
||||
But switch CAM entries age out. So the gateway should periodically refresh them.
|
||||
|
||||
@@ -330,7 +361,8 @@ for each connected remote MAC:
|
||||
inject a tiny harmless Ethernet frame with that MAC as source
|
||||
```
|
||||
|
||||
The exact frame can be decided during implementation, but the goal is simple: keep the LAN switch mapping the remote MAC to the gateway’s physical port.
|
||||
The exact frame can be decided during implementation, but the goal is simple:
|
||||
keep the LAN switch mapping the remote MAC to the gateway’s physical port.
|
||||
|
||||
Phase 1 success criterion:
|
||||
|
||||
@@ -340,9 +372,10 @@ remote client MAC appears in the LAN switch MAC table on the gateway port
|
||||
|
||||
If that is false, the L2 illusion is broken.
|
||||
|
||||
## Safety filters
|
||||
### Safety filters
|
||||
|
||||
Remote clients must not be allowed to spray arbitrary L2 control-plane junk onto the real LAN.
|
||||
Remote clients must not be allowed to spray arbitrary L2 control-plane junk onto
|
||||
the real LAN.
|
||||
|
||||
Drop remote → LAN unconditionally:
|
||||
|
||||
@@ -368,7 +401,8 @@ Also drop LAN → remote:
|
||||
|
||||
No remote Windows client needs to see switch/control-plane traffic.
|
||||
|
||||
EAPOL is especially important: remote clients should never be able to interfere with 802.1X or port authentication behavior on the physical switch.
|
||||
EAPOL is especially important: remote clients should never be able to interfere
|
||||
with 802.1X or port authentication behavior on the physical switch.
|
||||
|
||||
Add rate limits:
|
||||
|
||||
@@ -379,11 +413,12 @@ Add rate limits:
|
||||
- malformed packet disconnect threshold
|
||||
```
|
||||
|
||||
## Windows routing / metric handling
|
||||
### Windows routing / metric handling
|
||||
|
||||
The TAP adapter may receive DHCP from the party LAN. That is good.
|
||||
|
||||
But if DHCP gives it a default gateway, Windows might try to route the relay connection through the tunnel itself. That would break the tunnel.
|
||||
But if DHCP gives it a default gateway, Windows might try to route the relay
|
||||
connection through the tunnel itself. That would break the tunnel.
|
||||
|
||||
Client startup should:
|
||||
|
||||
@@ -396,7 +431,8 @@ Client startup should:
|
||||
6. detect and neutralize TAP default-route takeover
|
||||
```
|
||||
|
||||
The TAP should be preferred for the party LAN subnet, but it must not steal general internet traffic.
|
||||
The TAP should be preferred for the party LAN subnet, but it must not steal
|
||||
general internet traffic.
|
||||
|
||||
Also strongly recommend uncommon LAN party subnets:
|
||||
|
||||
@@ -409,9 +445,10 @@ bad: 192.168.178.0/24
|
||||
|
||||
Duplicate subnet with a remote user’s home LAN will be painful.
|
||||
|
||||
## Relay placement / latency
|
||||
### Relay placement / latency
|
||||
|
||||
Relay-as-data-path is the right MVP. It makes the product work through NAT immediately.
|
||||
Relay-as-data-path is the right MVP. It makes the product work through NAT
|
||||
immediately.
|
||||
|
||||
But latency becomes:
|
||||
|
||||
@@ -421,7 +458,10 @@ client → relay → gateway
|
||||
|
||||
So relay location matters.
|
||||
|
||||
For Europe/Germany-focused usage, put the relay near the expected players and LAN site, e.g. Frankfurt/Nuremberg/Amsterdam depending on hosting. Later, add direct QUIC path attempts with relay fallback, but do not block MVP on NAT traversal.
|
||||
For Europe/Germany-focused usage, put the relay near the expected players and
|
||||
LAN site, e.g. Frankfurt/Nuremberg/Amsterdam depending on hosting. Later, add
|
||||
direct QUIC path attempts with relay fallback, but do not block MVP on NAT
|
||||
traversal.
|
||||
|
||||
Design the room protocol so future modes are possible:
|
||||
|
||||
@@ -431,7 +471,7 @@ mode = direct-p2p
|
||||
mode = direct-failed-relay-fallback
|
||||
```
|
||||
|
||||
## Logging / diagnostics
|
||||
### Logging / diagnostics
|
||||
|
||||
Phase 1 should log heavily.
|
||||
|
||||
@@ -473,9 +513,9 @@ Broadcast traffic flowing
|
||||
Warning: TAP received default route, adjusted metric
|
||||
```
|
||||
|
||||
## Phase plan
|
||||
### Phase plan
|
||||
|
||||
### Phase 1: prove the illusion
|
||||
#### Phase 1: prove the illusion
|
||||
|
||||
Manual, ugly, real.
|
||||
|
||||
@@ -500,7 +540,7 @@ Success criteria:
|
||||
- one real LAN game discovers or joins a LAN server
|
||||
```
|
||||
|
||||
### Phase 2: multi-client
|
||||
#### Phase 2: multi-client
|
||||
|
||||
```text
|
||||
- multiple Windows clients
|
||||
@@ -512,7 +552,7 @@ Success criteria:
|
||||
- reconnect handling
|
||||
```
|
||||
|
||||
### Phase 3: safety and correctness
|
||||
#### Phase 3: safety and correctness
|
||||
|
||||
```text
|
||||
- L2 control-plane filters
|
||||
@@ -524,7 +564,7 @@ Success criteria:
|
||||
- better malformed-frame handling
|
||||
```
|
||||
|
||||
### Phase 4: product UX
|
||||
#### Phase 4: product UX
|
||||
|
||||
```text
|
||||
- Windows installer
|
||||
@@ -536,9 +576,11 @@ Success criteria:
|
||||
- logs export button
|
||||
```
|
||||
|
||||
Driver signing and TAP bundling must be validated early. `tap-windows6` is the right kind of driver, but Windows driver installation/signing is a product risk, not something to handwave. ([GitHub][1])
|
||||
Driver signing and TAP bundling must be validated early. `tap-windows6` is the
|
||||
right kind of driver, but Windows driver installation/signing is a product risk,
|
||||
not something to handwave. ([GitHub][1])
|
||||
|
||||
### Phase 5: better security and latency
|
||||
#### Phase 5: better security and latency
|
||||
|
||||
```text
|
||||
- invite tokens / auth
|
||||
@@ -549,7 +591,7 @@ Driver signing and TAP bundling must be validated early. `tap-windows6` is the r
|
||||
- regional relay selection
|
||||
```
|
||||
|
||||
## Explicit non-goals
|
||||
### Explicit non-goals
|
||||
|
||||
For MVP, do not build:
|
||||
|
||||
@@ -565,14 +607,19 @@ For MVP, do not build:
|
||||
- full internet VPN mode
|
||||
```
|
||||
|
||||
## One-sentence version
|
||||
### One-sentence version
|
||||
|
||||
Build a **Rust Windows TAP client + public QUIC relay + Linux AF_PACKET gateway** that carries one small-MTU Ethernet frame per QUIC datagram, gives each remote player a unique virtual MAC on the real LAN, filters dangerous L2 control traffic, and keeps the physical LAN gateway as the only machine touching the real LAN.
|
||||
Build a **Rust Windows TAP client + public QUIC relay + Linux AF_PACKET
|
||||
gateway** that carries one small-MTU Ethernet frame per QUIC datagram, gives
|
||||
each remote player a unique virtual MAC on the real LAN, filters dangerous L2
|
||||
control traffic, and keeps the physical LAN gateway as the only machine touching
|
||||
the real LAN.
|
||||
|
||||
[1]: https://github.com/OpenVPN/tap-windows6?utm_source=chatgpt.com "OpenVPN/tap-windows6: Windows TAP driver (NDIS 6)"
|
||||
[2]: https://man7.org/linux/man-pages/man7/packet.7.html?utm_source=chatgpt.com "packet(7) - Linux manual page"
|
||||
[3]: https://man7.org/linux/man-pages/man7/capabilities.7.html?utm_source=chatgpt.com "capabilities(7) - Linux manual page"
|
||||
[4]: https://datatracker.ietf.org/doc/html/rfc9221?utm_source=chatgpt.com "RFC 9221 - An Unreliable Datagram Extension to QUIC"
|
||||
[5]: https://docs.rs/quinn/latest/quinn/struct.Connection.html?utm_source=chatgpt.com "Connection in quinn - Rust"
|
||||
[1]: https://github.com/OpenVPN/tap-windows6
|
||||
[2]: https://man7.org/linux/man-pages/man7/packet.7.html
|
||||
[3]: https://man7.org/linux/man-pages/man7/capabilities.7.html
|
||||
[4]: https://datatracker.ietf.org/doc/html/rfc9221
|
||||
[5]: https://docs.rs/quinn/latest/quinn/struct.Connection.html
|
||||
|
||||
I want a mono-repo, Rust code, crates into a "crates" folder, one cargo workspace.
|
||||
I want a mono-repo, Rust code, crates into a "crates" folder, one cargo
|
||||
workspace.
|
||||
|
||||
@@ -82,7 +82,8 @@ Windows route-table boundary:
|
||||
- unicast IP address snapshots for TAP diagnostics
|
||||
- scoped host-route pinning for the relay IP on the pre-TAP interface
|
||||
- host-route pin matching for relay-route verification after TAP activation
|
||||
- reuse of an already-existing matching relay host route without deleting it on exit
|
||||
- reuse of an already-existing matching relay host route without deleting it on
|
||||
exit
|
||||
- non-Windows builds return a clear unsupported-platform error
|
||||
|
||||
### `lanparty-client-tap`
|
||||
@@ -109,9 +110,9 @@ Public relay binary and relay-owned room state:
|
||||
- per-peer egress budget checks against the negotiated datagram size
|
||||
- reliable `PeerJoined`/`PeerLeft` notifications plus gateway identity in
|
||||
welcome messages
|
||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN
|
||||
tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames,
|
||||
including frames behind ordinary IPv6 extension headers
|
||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN tags,
|
||||
remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames, including
|
||||
frames behind ordinary IPv6 extension headers
|
||||
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
||||
- malformed peer datagram disconnect threshold
|
||||
- peer stats control events retained for relay diagnostics
|
||||
@@ -122,16 +123,35 @@ Public relay binary and relay-owned room state:
|
||||
## Build And Local Checks
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo build --release -p lanparty-relay -p lanparty-gateway
|
||||
just fmt
|
||||
just test
|
||||
just clippy
|
||||
just build-release
|
||||
git diff --check
|
||||
```
|
||||
|
||||
These checks cover the local Rust code and the real client/relay/gateway
|
||||
session paths that can run without Windows TAP or LAN hardware. For the Windows
|
||||
client build and the manual MVP end-to-end proof, see [TESTING.md](TESTING.md).
|
||||
`just clippy` must stay completely clean: every crate turns on
|
||||
`clippy::pedantic`, `clippy::todo`, and `clippy::unwrap_used`, and the recipe
|
||||
runs with `-D warnings` over all targets and features. Crates that need OS FFI
|
||||
(`lanparty-gateway`, `lanparty-client-tap`, `lanparty-client-route`) set
|
||||
`unsafe_code = "deny"` instead of `"forbid"`, and the single module holding
|
||||
their FFI opts back in with `#![allow(unsafe_code)]`; every other crate forbids
|
||||
`unsafe` outright.
|
||||
|
||||
These checks cover the local Rust code and the real client/relay/gateway session
|
||||
paths that can run without Windows TAP or LAN hardware. For the Windows client
|
||||
build and the manual MVP end-to-end proof, see [TESTING.md](TESTING.md).
|
||||
|
||||
### Build Profiles
|
||||
|
||||
| Profile | Command | Purpose |
|
||||
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `dev` | `just build` | Everyday development. |
|
||||
| `release` | `just build-release` | Optimized but debuggable: debug info, debug assertions and overflow checks on, no LTO. |
|
||||
| `production` | `just build-production` | Shipping builds: no debug info, stripped, no debug assertions, fat LTO, one codegen unit. |
|
||||
|
||||
Use `release` when reproducing a bug at close-to-real speed, and `production`
|
||||
for anything handed to someone else.
|
||||
|
||||
## Relay
|
||||
|
||||
@@ -147,24 +167,24 @@ self-signed development certificate; `--dev-cert-der-out` writes that
|
||||
certificate so the gateway and client can pin it in development. Production
|
||||
certificate handling remains future work. Ethernet forwarding decisions are
|
||||
logged with room, peer, MAC, ethertype, action, drop reason, and target count.
|
||||
Safety-policy rejects use the `filtered` action so they are distinguishable
|
||||
from malformed/unknown-destination drops and rate limits.
|
||||
Malformed peer datagrams log their per-peer count before the relay disconnects
|
||||
peers that cross the malformed-datagram threshold.
|
||||
Relay egress skips caused by a target peer's smaller datagram budget are logged
|
||||
with the ingress peer, target peer, encoded length, and target budget.
|
||||
Ingress datagrams larger than the sending peer's negotiated datagram budget are
|
||||
dropped before decode/forwarding and logged with `reason=datagram_budget`.
|
||||
Unknown unicast from a client is forwarded only to the gateway port; unknown
|
||||
unicast from the gateway is dropped instead of flooded to every remote client.
|
||||
When a peer joins or leaves, the relay sends a reliable lifecycle control event
|
||||
to peers that are still present in the room. Newly joined peers also receive
|
||||
`PeerJoined` events for peers that were already present, and catch-up delivery
|
||||
is part of the accepted handshake rather than a best-effort follow-up. When a
|
||||
client joins, the relay notifies existing peers before the client receives its
|
||||
welcome, so gateways can seed client MAC state before that client starts
|
||||
sending frames. When a gateway joins, the relay gives the gateway the current
|
||||
client list before notifying clients that the gateway is available.
|
||||
Safety-policy rejects use the `filtered` action so they are distinguishable from
|
||||
malformed/unknown-destination drops and rate limits. Malformed peer datagrams
|
||||
log their per-peer count before the relay disconnects peers that cross the
|
||||
malformed-datagram threshold. Relay egress skips caused by a target peer's
|
||||
smaller datagram budget are logged with the ingress peer, target peer, encoded
|
||||
length, and target budget. Ingress datagrams larger than the sending peer's
|
||||
negotiated datagram budget are dropped before decode/forwarding and logged with
|
||||
`reason=datagram_budget`. Unknown unicast from a client is forwarded only to the
|
||||
gateway port; unknown unicast from the gateway is dropped instead of flooded to
|
||||
every remote client. When a peer joins or leaves, the relay sends a reliable
|
||||
lifecycle control event to peers that are still present in the room. Newly
|
||||
joined peers also receive `PeerJoined` events for peers that were already
|
||||
present, and catch-up delivery is part of the accepted handshake rather than a
|
||||
best-effort follow-up. When a client joins, the relay notifies existing peers
|
||||
before the client receives its welcome, so gateways can seed client MAC state
|
||||
before that client starts sending frames. When a gateway joins, the relay gives
|
||||
the gateway the current client list before notifying clients that the gateway is
|
||||
available.
|
||||
|
||||
### MVP Trust Model
|
||||
|
||||
@@ -195,41 +215,38 @@ and completes the control-stream hello/welcome handshake. That startup order
|
||||
keeps an invalid, wireless, or unplugged interface from briefly advertising a
|
||||
gateway that cannot bridge. Once both sides are ready, it bridges Ethernet
|
||||
frames between the relay and wired LAN until shutdown. It captures whole LAN
|
||||
frames up to the
|
||||
overlay payload-length ceiling before deciding whether they fit the tunnel. It
|
||||
never fragments Ethernet frames; LAN frames with invalid source MACs, L2
|
||||
control-plane traffic, jumbo frames, frames above the negotiated TAP MTU, or
|
||||
encoded datagrams exceeding the negotiated QUIC budget are counted, dropped,
|
||||
and logged locally instead of stopping the bridge or consuming relay bandwidth.
|
||||
Remote frames received from
|
||||
the relay are safety-checked again before LAN injection and must use the
|
||||
announced virtual MAC for their source peer, so invalid-source, forged-source,
|
||||
L2 control-plane, remote VLAN, DHCP-server, IPv6 Router Advertisement, IPv6
|
||||
fragment, jumbo, and over-TAP-MTU frames cannot cross the gateway's final
|
||||
physical-LAN boundary even if they reached the gateway over QUIC.
|
||||
`--relay` accepts a DNS name or socket address; bare hosts default to UDP/443.
|
||||
The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi, and rejects
|
||||
wired interfaces whose sysfs carrier state reports no link; managed wireless
|
||||
NICs are not supported for the physical LAN bridge.
|
||||
It tracks remote-client MACs from relay lifecycle events and periodically emits
|
||||
small CAM refresh frames, logged with `reason=periodic`, so the physical
|
||||
switch keeps those MACs associated with the gateway port. A newly observed
|
||||
client also triggers an immediate CAM refresh frame logged with
|
||||
`reason=peer_joined` instead of waiting for the first periodic refresh tick.
|
||||
When control events and frame work are both ready, the bridge handles the
|
||||
lifecycle event first so first packets after a client joins use the freshest
|
||||
remote-MAC state available locally. Gateway
|
||||
frame logs include direction, peer id when present, MACs, ethertype/length,
|
||||
frame length, action, and drop reason. The gateway also tracks frame/datagram
|
||||
counters and periodically sends stats snapshots to the relay. Malformed or runt
|
||||
LAN frames are counted and logged as dropped instead of disappearing before
|
||||
accounting. It drops unrelated LAN unicast locally once the destination is known
|
||||
not to be a connected remote client, so busy LAN traffic is not sent to the
|
||||
public relay just to be discarded there. Relay lifecycle events seed and retire
|
||||
remote-client MACs for CAM refresh and LAN-destination filtering even before
|
||||
that client sends traffic. On shutdown, the gateway sends a best-effort
|
||||
disconnect control message before closing QUIC so the relay can report the
|
||||
intended reason.
|
||||
frames up to the overlay payload-length ceiling before deciding whether they fit
|
||||
the tunnel. It never fragments Ethernet frames; LAN frames with invalid source
|
||||
MACs, L2 control-plane traffic, jumbo frames, frames above the negotiated TAP
|
||||
MTU, or encoded datagrams exceeding the negotiated QUIC budget are counted,
|
||||
dropped, and logged locally instead of stopping the bridge or consuming relay
|
||||
bandwidth. Remote frames received from the relay are safety-checked again before
|
||||
LAN injection and must use the announced virtual MAC for their source peer, so
|
||||
invalid-source, forged-source, L2 control-plane, remote VLAN, DHCP-server, IPv6
|
||||
Router Advertisement, IPv6 fragment, jumbo, and over-TAP-MTU frames cannot cross
|
||||
the gateway's final physical-LAN boundary even if they reached the gateway over
|
||||
QUIC. `--relay` accepts a DNS name or socket address; bare hosts default to
|
||||
UDP/443. The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi,
|
||||
and rejects wired interfaces whose sysfs carrier state reports no link; managed
|
||||
wireless NICs are not supported for the physical LAN bridge. It tracks
|
||||
remote-client MACs from relay lifecycle events and periodically emits small CAM
|
||||
refresh frames, logged with `reason=periodic`, so the physical switch keeps
|
||||
those MACs associated with the gateway port. A newly observed client also
|
||||
triggers an immediate CAM refresh frame logged with `reason=peer_joined` instead
|
||||
of waiting for the first periodic refresh tick. When control events and frame
|
||||
work are both ready, the bridge handles the lifecycle event first so first
|
||||
packets after a client joins use the freshest remote-MAC state available
|
||||
locally. Gateway frame logs include direction, peer id when present, MACs,
|
||||
ethertype/length, frame length, action, and drop reason. The gateway also tracks
|
||||
frame/datagram counters and periodically sends stats snapshots to the relay.
|
||||
Malformed or runt LAN frames are counted and logged as dropped instead of
|
||||
disappearing before accounting. It drops unrelated LAN unicast locally once the
|
||||
destination is known not to be a connected remote client, so busy LAN traffic is
|
||||
not sent to the public relay just to be discarded there. Relay lifecycle events
|
||||
seed and retire remote-client MACs for CAM refresh and LAN-destination filtering
|
||||
even before that client sends traffic. On shutdown, the gateway sends a
|
||||
best-effort disconnect control message before closing QUIC so the relay can
|
||||
report the intended reason.
|
||||
|
||||
## Windows Client
|
||||
|
||||
@@ -246,24 +263,21 @@ path depends on TAP-Windows6 and Windows route protection. Non-Windows builds
|
||||
are useful for type checking, but they fail before tunnel setup instead of
|
||||
joining a room without a TAP adapter. On Windows, the binary connects to the
|
||||
relay as `role = client` with a generated locally administered virtual MAC
|
||||
persisted in
|
||||
`lanparty-client-identity.json`. Before resolving or connecting to the relay,
|
||||
it writes the generated tunnel MAC to the selected TAP driver's
|
||||
persisted in `lanparty-client-identity.json`. Before resolving or connecting to
|
||||
the relay, it writes the generated tunnel MAC to the selected TAP driver's
|
||||
`NetworkAddress` registry setting and marks TAP media disconnected. That clears
|
||||
stale connected state from a previous crashed run without letting the TAP
|
||||
adapter influence relay DNS or route selection. The client then resolves the
|
||||
relay endpoint, pins a host route for the resolved relay IP on the current
|
||||
pre-TAP interface, verifies that Windows is using that host route, completes
|
||||
the control-stream hello/welcome handshake, verifies the host route again after
|
||||
TAP activation, and bridges Ethernet frames between the relay and the
|
||||
TAP-Windows6 adapter until shutdown. `--relay` accepts a DNS name or socket
|
||||
address; bare hosts default to UDP/443.
|
||||
TAP frames whose source MAC does not match that generated tunnel MAC are
|
||||
dropped locally before they can consume relay bandwidth; the relay still
|
||||
enforces the same source-MAC rule.
|
||||
If the exact relay host route already exists, the client uses it and leaves it
|
||||
alone on exit. The startup status reports whether the relay already has a LAN
|
||||
gateway for the room.
|
||||
pre-TAP interface, verifies that Windows is using that host route, completes the
|
||||
control-stream hello/welcome handshake, verifies the host route again after TAP
|
||||
activation, and bridges Ethernet frames between the relay and the TAP-Windows6
|
||||
adapter until shutdown. `--relay` accepts a DNS name or socket address; bare
|
||||
hosts default to UDP/443. TAP frames whose source MAC does not match that
|
||||
generated tunnel MAC are dropped locally before they can consume relay
|
||||
bandwidth; the relay still enforces the same source-MAC rule. If the exact relay
|
||||
host route already exists, the client uses it and leaves it alone on exit. The
|
||||
startup status reports whether the relay already has a LAN gateway for the room.
|
||||
`--virtual-mac` can still override the stored identity for manual testing. On
|
||||
Windows it sets the TAP IP interface MTU to the relay-selected MTU, marks the
|
||||
TAP media connected for the scoped client run, and reports the driver MAC/MTU
|
||||
@@ -271,40 +285,37 @@ before forwarding frames, along with the TAP interface index/LUID. The client
|
||||
applies a scoped TAP interface metric and disables TAP default routes while it
|
||||
runs, periodically rechecks that the relay route remains pinned, then restores
|
||||
the previous route policy and TAP media status on exit. Startup prints a warning
|
||||
when TAP default routes were enabled
|
||||
before the scoped protection was applied. Startup still fails before bridging
|
||||
if the driver-reported MAC does not match the tunnel identity, because an
|
||||
already-initialized Windows TAP adapter may need to be disabled/enabled or
|
||||
reinstalled before it reloads the configured `NetworkAddress`.
|
||||
If exactly one TAP-Windows6 adapter is installed, the client opens it
|
||||
automatically. If multiple TAP-Windows6 adapters are installed, startup fails
|
||||
until `--tap-instance-id` selects the intended adapter by NetCfgInstanceId /
|
||||
InterfaceGuid. `--list-tap-adapters` prints the TAP adapter ids and exits
|
||||
without connecting.
|
||||
It prints and reports client diagnostics snapshots with relay reachability,
|
||||
LAN-gateway presence, route-pinning, QUIC datagram budget, relay RTT, TAP
|
||||
status/IP, broadcast frame flow, frame/datagram counters, and drops. The
|
||||
periodic diagnostics refresh the TAP unicast IP so DHCP results that arrive
|
||||
after bridging starts become visible in later status lines, preferring a
|
||||
when TAP default routes were enabled before the scoped protection was applied.
|
||||
Startup still fails before bridging if the driver-reported MAC does not match
|
||||
the tunnel identity, because an already-initialized Windows TAP adapter may need
|
||||
to be disabled/enabled or reinstalled before it reloads the configured
|
||||
`NetworkAddress`. If exactly one TAP-Windows6 adapter is installed, the client
|
||||
opens it automatically. If multiple TAP-Windows6 adapters are installed, startup
|
||||
fails until `--tap-instance-id` selects the intended adapter by NetCfgInstanceId
|
||||
/ InterfaceGuid. `--list-tap-adapters` prints the TAP adapter ids and exits
|
||||
without connecting. It prints and reports client diagnostics snapshots with
|
||||
relay reachability, LAN-gateway presence, route-pinning, QUIC datagram budget,
|
||||
relay RTT, TAP status/IP, broadcast frame flow, frame/datagram counters, and
|
||||
drops. The periodic diagnostics refresh the TAP unicast IP so DHCP results that
|
||||
arrive after bridging starts become visible in later status lines, preferring a
|
||||
non-link-local IPv4 address when Windows reports several TAP addresses. Each
|
||||
snapshot also emits short user-facing lines such as relay/gateway connection status,
|
||||
relay-route and TAP readiness warnings, DHCP address presence, relay RTT, and
|
||||
broadcast-flow confirmation. One-way broadcast diagnostics distinguish frames
|
||||
sent toward the LAN from broadcast frames received back from the LAN. Malformed frames
|
||||
read from TAP, invalid or unauthorized source-MAC frames, L2 control-plane
|
||||
traffic, remote VLAN tags, DHCP server replies, IPv6 Router Advertisements, IPv6
|
||||
fragments, jumbo frames, frames above the negotiated TAP MTU, and TAP frames
|
||||
whose encoded datagrams exceed the negotiated QUIC budget are counted and
|
||||
dropped before relay send without stopping the bridge. Relayed LAN frames are
|
||||
also safety-checked before TAP writes, so switch-control traffic,
|
||||
snapshot also emits short user-facing lines such as relay/gateway connection
|
||||
status, relay-route and TAP readiness warnings, DHCP address presence, relay
|
||||
RTT, and broadcast-flow confirmation. One-way broadcast diagnostics distinguish
|
||||
frames sent toward the LAN from broadcast frames received back from the LAN.
|
||||
Malformed frames read from TAP, invalid or unauthorized source-MAC frames, L2
|
||||
control-plane traffic, remote VLAN tags, DHCP server replies, IPv6 Router
|
||||
Advertisements, IPv6 fragments, jumbo frames, frames above the negotiated TAP
|
||||
MTU, and TAP frames whose encoded datagrams exceed the negotiated QUIC budget
|
||||
are counted and dropped before relay send without stopping the bridge. Relayed
|
||||
LAN frames are also safety-checked before TAP writes, so switch-control traffic,
|
||||
invalid-source frames, jumbo frames, and over-TAP-MTU frames stay out of the
|
||||
Windows adapter even if they reached the client over QUIC.
|
||||
Misdirected unicast frames not addressed to the client's virtual MAC are also
|
||||
counted, skipped, and logged with the drop reason; accepted TAP-to-relay and
|
||||
relay-to-TAP frames are logged with direction, peer id, MACs, ethertype/length,
|
||||
frame length, action, and drop reason. TAP device read/write errors still stop
|
||||
the bridge.
|
||||
Relay lifecycle events are logged as they arrive, including gateway joins and
|
||||
peer leaves. The client remembers peer identities from join and catch-up events
|
||||
and from the initial welcome, so later leave logs can identify a disconnected
|
||||
LAN gateway or client MAC when that peer was known.
|
||||
Windows adapter even if they reached the client over QUIC. Misdirected unicast
|
||||
frames not addressed to the client's virtual MAC are also counted, skipped, and
|
||||
logged with the drop reason; accepted TAP-to-relay and relay-to-TAP frames are
|
||||
logged with direction, peer id, MACs, ethertype/length, frame length, action,
|
||||
and drop reason. TAP device read/write errors still stop the bridge. Relay
|
||||
lifecycle events are logged as they arrive, including gateway joins and peer
|
||||
leaves. The client remembers peer identities from join and catch-up events and
|
||||
from the initial welcome, so later leave logs can identify a disconnected LAN
|
||||
gateway or client MAC when that peer was known.
|
||||
|
||||
+24
-25
@@ -58,8 +58,8 @@ Windows TAP IPv4:
|
||||
- Gateway: Linux machine plugged into the LAN party switch with wired Ethernet.
|
||||
- Client: Windows 11 machine with TAP-Windows6 installed.
|
||||
|
||||
Use the same room code everywhere, for example `ROOM1`.
|
||||
Start order is relay first, gateway second, Windows client last.
|
||||
Use the same room code everywhere, for example `ROOM1`. Start order is relay
|
||||
first, gateway second, Windows client last.
|
||||
|
||||
## Log Capture
|
||||
|
||||
@@ -153,8 +153,8 @@ Linux: ./target/release/lanparty-gateway
|
||||
Windows: .\target\release\lanparty-client-win.exe
|
||||
```
|
||||
|
||||
The Windows client must run elevated because it opens TAP and edits routes.
|
||||
The gateway usually needs root because it opens an AF_PACKET raw socket.
|
||||
The Windows client must run elevated because it opens TAP and edits routes. The
|
||||
gateway usually needs root because it opens an AF_PACKET raw socket.
|
||||
|
||||
## Start The Relay
|
||||
|
||||
@@ -196,8 +196,8 @@ sudo ./target/release/lanparty-gateway \
|
||||
```
|
||||
|
||||
Use the real wired LAN interface name for `--interface`. `--iface` is accepted
|
||||
as a shorter alias. Do not use Wi-Fi. The gateway fails before joining the
|
||||
relay if sysfs reports no Ethernet carrier.
|
||||
as a shorter alias. Do not use Wi-Fi. The gateway fails before joining the relay
|
||||
if sysfs reports no Ethernet carrier.
|
||||
|
||||
Expected gateway output:
|
||||
|
||||
@@ -245,7 +245,8 @@ one explicitly:
|
||||
Expected client output:
|
||||
|
||||
```text
|
||||
prepared TAP adapter ... MAC ... configured and media disconnected before relay connect
|
||||
prepared TAP adapter ... MAC ... configured and media disconnected before relay
|
||||
connect
|
||||
relay route pinned before TAP ...
|
||||
relay route verified before TAP activation ...
|
||||
lanparty-client-win connecting virtual MAC ... to relay ... room ROOM1
|
||||
@@ -261,11 +262,10 @@ relay event: LAN gateway connected as peer ...
|
||||
|
||||
The route pin line ends with `(created)` or `(already existed)`. Either is OK.
|
||||
`already existed` usually means a matching relay host route was already present,
|
||||
for example after a previous crashed test run.
|
||||
You may also see TAP IPv4/IPv6 MTU, metric, and default-route protection lines
|
||||
between the connect and TAP-open lines. Those are expected.
|
||||
The lifecycle event may appear after the bridge starts because event logging
|
||||
begins once TAP and route protection are ready.
|
||||
for example after a previous crashed test run. You may also see TAP IPv4/IPv6
|
||||
MTU, metric, and default-route protection lines between the connect and TAP-open
|
||||
lines. Those are expected. The lifecycle event may appear after the bridge
|
||||
starts because event logging begins once TAP and route protection are ready.
|
||||
|
||||
The first diagnostics line may show `IP unknown`. After DHCP succeeds, a later
|
||||
line should show:
|
||||
@@ -274,8 +274,8 @@ line should show:
|
||||
DHCP received: 10.x.x.x
|
||||
```
|
||||
|
||||
If Windows reports both a `169.254.x.x` TAP address and a real LAN IPv4
|
||||
address, the client diagnostics should prefer the real LAN address.
|
||||
If Windows reports both a `169.254.x.x` TAP address and a real LAN IPv4 address,
|
||||
the client diagnostics should prefer the real LAN address.
|
||||
|
||||
## Verify The Tunnel
|
||||
|
||||
@@ -395,10 +395,9 @@ drop_reason=RateLimit
|
||||
|
||||
On gateway `LanToRemote` logs, `UnknownDestination` usually means the gateway
|
||||
captured unrelated LAN unicast and dropped it locally instead of sending it to
|
||||
the relay.
|
||||
`TapMtuExceeded` means a host emitted an Ethernet frame larger than the
|
||||
negotiated tunnel MTU; occasional drops can happen while testing software that
|
||||
does not honor the smaller adapter MTU yet.
|
||||
the relay. `TapMtuExceeded` means a host emitted an Ethernet frame larger than
|
||||
the negotiated tunnel MTU; occasional drops can happen while testing software
|
||||
that does not honor the smaller adapter MTU yet.
|
||||
|
||||
Drops that should be investigated if they dominate:
|
||||
|
||||
@@ -414,8 +413,8 @@ drop_reason=Ipv6Fragment
|
||||
```
|
||||
|
||||
On gateway `RemoteToLan` logs, `UnauthorizedSourceMac` means the relayed peer id
|
||||
did not match the client MAC announced by lifecycle events. If it repeats,
|
||||
check relay lifecycle logs and duplicate-MAC rejection first.
|
||||
did not match the client MAC announced by lifecycle events. If it repeats, check
|
||||
relay lifecycle logs and duplicate-MAC rejection first.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -454,11 +453,11 @@ If ping fails but DHCP worked, check Windows firewall, the target LAN host
|
||||
firewall, and whether the LAN subnet conflicts with the client's home LAN.
|
||||
Uncommon LAN subnets such as `10.73.42.0/24` are safer than `192.168.0.0/24`.
|
||||
|
||||
If switch MAC learning does not show the Windows client MAC on the gateway
|
||||
port, look for `gateway CAM refresh ... reason=peer_joined` immediately after
|
||||
join and `gateway CAM refresh ... reason=periodic` about once per minute after
|
||||
that. If those lines are present but the switch still does not learn it, check
|
||||
the selected gateway interface and switch port first.
|
||||
If switch MAC learning does not show the Windows client MAC on the gateway port,
|
||||
look for `gateway CAM refresh ... reason=peer_joined` immediately after join and
|
||||
`gateway CAM refresh ... reason=periodic` about once per minute after that. If
|
||||
those lines are present but the switch still does not learn it, check the
|
||||
selected gateway interface and switch port first.
|
||||
|
||||
## Cleanup
|
||||
|
||||
|
||||
@@ -18,3 +18,11 @@ serde_json.workspace = true
|
||||
[dev-dependencies]
|
||||
rcgen.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! crate owns the shared relay-facing state machine: connect to the relay,
|
||||
//! announce the client's virtual MAC, and exchange Ethernet frames as QUIC
|
||||
//! datagrams after the control-plane welcome.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
@@ -21,15 +22,29 @@ use std::{
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bytes::Bytes;
|
||||
use lanparty_ctrl::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlMessage, DisconnectReason, EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN, RELAY_ALPN, RoomCode, ServerWelcome, decode_control_frame,
|
||||
CONTROL_LENGTH_PREFIX_LEN,
|
||||
ControlMessage,
|
||||
DisconnectReason,
|
||||
EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN,
|
||||
RELAY_ALPN,
|
||||
RoomCode,
|
||||
ServerWelcome,
|
||||
decode_control_frame,
|
||||
encode_control_message,
|
||||
};
|
||||
use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, FrameType, MacAddr, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram,
|
||||
ethernet_frame_exceeds_tap_mtu, gateway_lan_safety_drop_reason,
|
||||
remote_client_safety_drop_reason, validate_datagram_budget,
|
||||
EthernetFrame,
|
||||
FrameType,
|
||||
MacAddr,
|
||||
OVERLAY_FLAGS_NONE,
|
||||
decode_datagram,
|
||||
encode_datagram,
|
||||
ethernet_frame_exceeds_tap_mtu,
|
||||
gateway_lan_safety_drop_reason,
|
||||
remote_client_safety_drop_reason,
|
||||
validate_datagram_budget,
|
||||
};
|
||||
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||
use rustls::pki_types::CertificateDer;
|
||||
@@ -43,6 +58,12 @@ pub struct ClientIdentity {
|
||||
}
|
||||
|
||||
impl ClientIdentity {
|
||||
/// Adopts `virtual_mac` as this client's identity on the virtual LAN.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error unless `virtual_mac` is a locally administered unicast
|
||||
/// address, which is what the relay requires of a client.
|
||||
pub fn new(virtual_mac: MacAddr) -> Result<Self> {
|
||||
if !virtual_mac.is_valid_client_identity() {
|
||||
bail!("client virtual MAC must be locally administered unicast");
|
||||
@@ -51,6 +72,11 @@ impl ClientIdentity {
|
||||
Ok(Self { virtual_mac })
|
||||
}
|
||||
|
||||
/// Generates a fresh random locally administered unicast identity.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the OS random source is unavailable.
|
||||
pub fn generate() -> Result<Self> {
|
||||
let mut octets = [0_u8; 6];
|
||||
getrandom::fill(&mut octets).context("failed to generate client virtual MAC")?;
|
||||
@@ -71,6 +97,11 @@ pub struct ClientIdentityStore {
|
||||
}
|
||||
|
||||
impl ClientIdentityStore {
|
||||
/// Points the store at the file that persists the client identity.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `path` is empty.
|
||||
pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
|
||||
let path = path.into();
|
||||
if path.as_os_str().is_empty() {
|
||||
@@ -85,6 +116,12 @@ impl ClientIdentityStore {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Loads the stored identity, generating and persisting one on first run.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the file exists but cannot be read or parsed, or if
|
||||
/// a newly generated identity cannot be written.
|
||||
pub fn load_or_create(&self) -> Result<ClientIdentity> {
|
||||
match fs::read(&self.path) {
|
||||
Ok(bytes) => read_identity(&bytes)
|
||||
@@ -155,6 +192,12 @@ pub struct ClientSessionConfig {
|
||||
}
|
||||
|
||||
impl ClientSessionConfig {
|
||||
/// Assembles everything the client needs to open a relay session.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the server name or CA certificate is empty, or if
|
||||
/// `virtual_mac` and `max_datagram_size` would not produce a valid hello.
|
||||
pub fn new(
|
||||
relay_addr: SocketAddr,
|
||||
server_name: impl Into<String>,
|
||||
@@ -313,26 +356,61 @@ impl ClientSession {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends one Ethernet frame from the TAP device to the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the frame is rejected or the datagram cannot be
|
||||
/// sent; see [`ClientRelayIo::send_ethernet`].
|
||||
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
||||
self.relay_io().send_ethernet(frame)
|
||||
}
|
||||
|
||||
/// Sends one Ethernet frame, reporting whether it was dropped instead of
|
||||
/// failing on frames the client itself filters.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error only if the QUIC datagram send fails.
|
||||
pub fn send_ethernet_with_outcome(&self, frame: &[u8]) -> Result<ClientSendOutcome> {
|
||||
self.relay_io().send_ethernet_with_outcome(frame)
|
||||
}
|
||||
|
||||
/// Waits for the next Ethernet frame the client accepts, skipping filtered
|
||||
/// ones.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC connection fails while reading.
|
||||
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||
self.relay_io().recv_ethernet().await
|
||||
}
|
||||
|
||||
/// Waits for the next Ethernet frame, also reporting filtered ones so
|
||||
/// callers can account for them.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC connection fails while reading.
|
||||
pub async fn recv_ethernet_outcome(&self) -> Result<ClientReceiveOutcome> {
|
||||
self.relay_io().recv_ethernet_outcome().await
|
||||
}
|
||||
|
||||
/// Waits for the next control-plane message from the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the control stream fails or carries a frame that
|
||||
/// cannot be decoded.
|
||||
pub async fn recv_control_event(&self) -> Result<ControlMessage> {
|
||||
recv_control_event(&self.connection).await
|
||||
}
|
||||
|
||||
/// Reports the current tunnel counters to the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the control stream write fails.
|
||||
pub async fn send_stats_snapshot(&self) -> Result<()> {
|
||||
self.relay_io().send_stats_snapshot().await
|
||||
}
|
||||
@@ -394,6 +472,13 @@ impl ClientRelayIo {
|
||||
self.virtual_mac
|
||||
}
|
||||
|
||||
/// Sends one Ethernet frame, turning a drop into an error.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the frame is malformed, has an unusable or foreign
|
||||
/// source MAC, exceeds the negotiated datagram budget, is filtered by the
|
||||
/// LAN safety rules, or the datagram send fails.
|
||||
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
||||
match self.send_ethernet_with_outcome(frame)? {
|
||||
ClientSendOutcome::Sent => Ok(()),
|
||||
@@ -409,13 +494,16 @@ impl ClientRelayIo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends one Ethernet frame, returning the drop reason instead of an error
|
||||
/// for frames the client filters itself.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error only if the QUIC datagram send fails.
|
||||
pub fn send_ethernet_with_outcome(&self, frame: &[u8]) -> Result<ClientSendOutcome> {
|
||||
let ethernet_frame = match EthernetFrame::parse(frame) {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => {
|
||||
self.stats.record_malformed_frame();
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::Malformed));
|
||||
}
|
||||
let Ok(ethernet_frame) = EthernetFrame::parse(frame) else {
|
||||
self.stats.record_malformed_frame();
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::Malformed));
|
||||
};
|
||||
if !ethernet_frame.source().is_valid_unicast() {
|
||||
self.stats.record_dropped_frame();
|
||||
@@ -462,6 +550,11 @@ impl ClientRelayIo {
|
||||
Ok(ClientSendOutcome::Sent)
|
||||
}
|
||||
|
||||
/// Waits for the next accepted Ethernet frame, discarding filtered ones.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC connection fails while reading.
|
||||
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||
loop {
|
||||
match self.recv_ethernet_outcome().await? {
|
||||
@@ -471,6 +564,12 @@ impl ClientRelayIo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the next Ethernet frame and reports whether it was accepted or
|
||||
/// filtered.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC connection fails while reading.
|
||||
pub async fn recv_ethernet_outcome(&self) -> Result<ClientReceiveOutcome> {
|
||||
loop {
|
||||
let datagram = self.connection.read_datagram().await?;
|
||||
@@ -487,12 +586,9 @@ impl ClientRelayIo {
|
||||
self.stats.record_dropped_frame();
|
||||
continue;
|
||||
}
|
||||
let ethernet_frame = match EthernetFrame::parse(packet.payload()) {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => {
|
||||
self.stats.record_malformed_frame();
|
||||
continue;
|
||||
}
|
||||
let Ok(ethernet_frame) = EthernetFrame::parse(packet.payload()) else {
|
||||
self.stats.record_malformed_frame();
|
||||
continue;
|
||||
};
|
||||
|
||||
self.stats.record_ethernet_rx(ethernet_frame);
|
||||
@@ -536,6 +632,11 @@ impl ClientRelayIo {
|
||||
self.stats.snapshot()
|
||||
}
|
||||
|
||||
/// Reports the current tunnel counters to the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the control stream write fails.
|
||||
pub async fn send_stats_snapshot(&self) -> Result<()> {
|
||||
let stats = self.stats.snapshot();
|
||||
send_control_event(&self.connection, ControlMessage::Stats(stats)).await
|
||||
@@ -604,6 +705,14 @@ impl ClientTunnelStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the relay, announces the client hello, and returns the session
|
||||
/// once the relay answers with a welcome.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC endpoint cannot be created or connected, the
|
||||
/// relay does not negotiate DATAGRAM support, the control handshake fails, or
|
||||
/// the relay rejects the hello.
|
||||
pub async fn connect_client(config: ClientSessionConfig) -> Result<ClientSession> {
|
||||
let client_config = relay_client_config(config.relay_ca_cert_der())?;
|
||||
let mut endpoint = Endpoint::client(client_bind_addr(config.relay_addr()))
|
||||
@@ -647,7 +756,7 @@ pub async fn connect_client(config: ClientSessionConfig) -> Result<ClientSession
|
||||
|
||||
#[must_use]
|
||||
fn negotiated_quic_datagram_size(configured: u16, peer: usize) -> u16 {
|
||||
usize::from(configured).min(peer).min(usize::from(u16::MAX)) as u16
|
||||
u16::try_from(peer).unwrap_or(u16::MAX).min(configured)
|
||||
}
|
||||
|
||||
fn relay_client_config(relay_ca_cert_der: &[u8]) -> Result<ClientConfig> {
|
||||
@@ -853,6 +962,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// One end-to-end handshake: a scripted relay plus the client side of the
|
||||
// exchange only make sense read together.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn connects_to_relay_control_stream_as_client() {
|
||||
let (server_config, certificate) = test_server_config();
|
||||
let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
|
||||
@@ -1165,10 +1277,10 @@ mod tests {
|
||||
#[test]
|
||||
fn snapshots_client_tunnel_stats() {
|
||||
let stats = ClientTunnelStats::default();
|
||||
let broadcast_tx_bytes = broadcast_ethernet_frame(b"broadcast tx");
|
||||
let broadcast_rx_bytes = broadcast_ethernet_frame(b"broadcast rx");
|
||||
let broadcast_tx = EthernetFrame::parse(&broadcast_tx_bytes).unwrap();
|
||||
let broadcast_rx = EthernetFrame::parse(&broadcast_rx_bytes).unwrap();
|
||||
let sent_frame_bytes = broadcast_ethernet_frame(b"broadcast tx");
|
||||
let received_frame_bytes = broadcast_ethernet_frame(b"broadcast rx");
|
||||
let broadcast_tx = EthernetFrame::parse(&sent_frame_bytes).unwrap();
|
||||
let broadcast_rx = EthernetFrame::parse(&received_frame_bytes).unwrap();
|
||||
|
||||
stats.record_ethernet_tx(broadcast_tx);
|
||||
stats.record_datagram_rx();
|
||||
|
||||
@@ -6,10 +6,23 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
] }
|
||||
[target."cfg(windows)".dependencies]
|
||||
windows-sys = {
|
||||
workspace = true,
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
]
|
||||
}
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
# `deny` rather than `forbid`: this crate needs OS FFI, so single
|
||||
# modules opt back in with `#![allow(unsafe_code)]`.
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! The client binary uses this crate to keep Win32 route/metric calls out of
|
||||
//! the relay session code. The crate can snapshot the current relay route and
|
||||
//! install scoped route/interface overrides that are restored when dropped.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
@@ -286,11 +287,24 @@ mod windows;
|
||||
pub use windows::{PinnedRelayRoute, best_route_to, interface_identity_from_guid, pin_relay_route};
|
||||
#[cfg(windows)]
|
||||
pub use windows::{
|
||||
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu, interface_metric,
|
||||
interface_mtu, interface_unicast_addresses, set_scoped_default_routes_disabled,
|
||||
set_scoped_interface_metric, set_scoped_interface_mtu,
|
||||
ScopedDefaultRoutes,
|
||||
ScopedInterfaceMetric,
|
||||
ScopedInterfaceMtu,
|
||||
interface_metric,
|
||||
interface_mtu,
|
||||
interface_unicast_addresses,
|
||||
set_scoped_default_routes_disabled,
|
||||
set_scoped_interface_metric,
|
||||
set_scoped_interface_mtu,
|
||||
};
|
||||
|
||||
/// Returns the route Windows would currently use to reach
|
||||
/// `destination`, so the relay path can be pinned to it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
|
||||
bail!("Windows route inspection is only available on Windows");
|
||||
@@ -302,11 +316,26 @@ pub struct PinnedRelayRoute {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
/// Installs a host route that keeps relay traffic on `route`'s
|
||||
/// interface, restoring the previous state when the returned guard is
|
||||
/// dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn pin_relay_route(_route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
||||
bail!("Windows route pinning is only available on Windows");
|
||||
}
|
||||
|
||||
/// Resolves a Windows interface GUID string to the LUID and index the
|
||||
/// IP Helper API expects.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn interface_identity_from_guid(_interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
|
||||
bail!("Windows interface identity lookup is only available on Windows");
|
||||
@@ -318,6 +347,12 @@ pub struct ScopedInterfaceMetric {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
/// Reads the current routing metric of an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn interface_metric(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
@@ -326,6 +361,13 @@ pub fn interface_metric(
|
||||
bail!("Windows interface metric lookup is only available on Windows");
|
||||
}
|
||||
|
||||
/// Overrides an interface's routing metric until the returned guard is
|
||||
/// dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn set_scoped_interface_metric(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
@@ -341,6 +383,13 @@ pub struct ScopedDefaultRoutes {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
/// Enables or disables an interface's default routes until the returned
|
||||
/// guard is dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn set_scoped_default_routes_disabled(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
@@ -350,6 +399,12 @@ pub fn set_scoped_default_routes_disabled(
|
||||
bail!("Windows interface default-route updates are only available on Windows");
|
||||
}
|
||||
|
||||
/// Reads the current MTU of an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn interface_mtu(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
@@ -358,6 +413,12 @@ pub fn interface_mtu(
|
||||
bail!("Windows interface MTU lookup is only available on Windows");
|
||||
}
|
||||
|
||||
/// Lists the unicast IP addresses currently assigned to an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn interface_unicast_addresses(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
@@ -371,6 +432,12 @@ pub struct ScopedInterfaceMtu {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
/// Overrides an interface's MTU until the returned guard is dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; this crate only implements the
|
||||
/// Windows IP Helper backend.
|
||||
#[cfg(not(windows))]
|
||||
pub fn set_scoped_interface_mtu(
|
||||
_identity: NetworkInterfaceIdentity,
|
||||
|
||||
@@ -1,35 +1,76 @@
|
||||
//! Windows IP Helper (`iphlpapi`) route and interface manipulation.
|
||||
//!
|
||||
//! All Win32 FFI is confined to this module, which is why it opts back into
|
||||
//! `unsafe`.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
fmt, io,
|
||||
fmt,
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
ptr::{null, null_mut},
|
||||
slice,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{ERROR_OBJECT_ALREADY_EXISTS, ERROR_SUCCESS},
|
||||
NetworkManagement::{
|
||||
IpHelper::{
|
||||
ConvertInterfaceGuidToLuid, ConvertInterfaceLuidToIndex, CreateIpForwardEntry2,
|
||||
DeleteIpForwardEntry2, FreeMibTable, GetBestRoute2, GetIpInterfaceEntry,
|
||||
GetUnicastIpAddressTable, IP_ADDRESS_PREFIX, InitializeIpForwardEntry,
|
||||
InitializeIpInterfaceEntry, MIB_IPFORWARD_ROW2, MIB_IPINTERFACE_ROW,
|
||||
MIB_UNICASTIPADDRESS_ROW, MIB_UNICASTIPADDRESS_TABLE, SetIpInterfaceEntry,
|
||||
use windows_sys::{
|
||||
Win32::{
|
||||
Foundation::{ERROR_OBJECT_ALREADY_EXISTS, ERROR_SUCCESS},
|
||||
NetworkManagement::{
|
||||
IpHelper::{
|
||||
ConvertInterfaceGuidToLuid,
|
||||
ConvertInterfaceLuidToIndex,
|
||||
CreateIpForwardEntry2,
|
||||
DeleteIpForwardEntry2,
|
||||
FreeMibTable,
|
||||
GetBestRoute2,
|
||||
GetIpInterfaceEntry,
|
||||
GetUnicastIpAddressTable,
|
||||
IP_ADDRESS_PREFIX,
|
||||
InitializeIpForwardEntry,
|
||||
InitializeIpInterfaceEntry,
|
||||
MIB_IPFORWARD_ROW2,
|
||||
MIB_IPINTERFACE_ROW,
|
||||
MIB_UNICASTIPADDRESS_ROW,
|
||||
MIB_UNICASTIPADDRESS_TABLE,
|
||||
SetIpInterfaceEntry,
|
||||
},
|
||||
Ndis::NET_LUID_LH,
|
||||
},
|
||||
Networking::WinSock::{
|
||||
AF_INET,
|
||||
AF_INET6,
|
||||
AF_UNSPEC,
|
||||
IN_ADDR,
|
||||
IN_ADDR_0,
|
||||
IN6_ADDR,
|
||||
IN6_ADDR_0,
|
||||
RouteProtocolNetMgmt,
|
||||
SOCKADDR_IN,
|
||||
SOCKADDR_IN6,
|
||||
SOCKADDR_IN6_0,
|
||||
SOCKADDR_INET,
|
||||
},
|
||||
Ndis::NET_LUID_LH,
|
||||
},
|
||||
Networking::WinSock::{
|
||||
AF_INET, AF_INET6, AF_UNSPEC, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0,
|
||||
RouteProtocolNetMgmt, SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_INET,
|
||||
},
|
||||
core::GUID,
|
||||
};
|
||||
use windows_sys::core::GUID;
|
||||
|
||||
use crate::{
|
||||
InterfaceMetricSnapshot, InterfaceMtuSnapshot, InterfaceUnicastAddress, IpInterfaceFamily,
|
||||
InterfaceMetricSnapshot,
|
||||
InterfaceMtuSnapshot,
|
||||
InterfaceUnicastAddress,
|
||||
IpInterfaceFamily,
|
||||
NetworkInterfaceIdentity,
|
||||
RouteSnapshot,
|
||||
};
|
||||
use crate::{NetworkInterfaceIdentity, RouteSnapshot};
|
||||
|
||||
/// Resolves a Windows interface GUID string to the LUID and index the
|
||||
/// IP Helper API expects.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `interface_guid` is not a well-formed GUID or
|
||||
/// does not name an existing interface.
|
||||
pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
|
||||
let guid = parse_interface_guid(interface_guid)?;
|
||||
let mut luid = NET_LUID_LH::default();
|
||||
@@ -55,6 +96,11 @@ pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInter
|
||||
Ok(NetworkInterfaceIdentity::new(index, luid_value(luid)))
|
||||
}
|
||||
|
||||
/// Reads the current routing metric of an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the interface row cannot be read.
|
||||
pub fn interface_metric(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
family: IpInterfaceFamily,
|
||||
@@ -64,6 +110,11 @@ pub fn interface_metric(
|
||||
Ok(metric_snapshot(identity, family, row))
|
||||
}
|
||||
|
||||
/// Reads the current MTU of an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the interface row cannot be read.
|
||||
pub fn interface_mtu(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
family: IpInterfaceFamily,
|
||||
@@ -73,6 +124,11 @@ pub fn interface_mtu(
|
||||
Ok(mtu_snapshot(identity, family, row))
|
||||
}
|
||||
|
||||
/// Lists the unicast IP addresses currently assigned to an interface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the unicast address table cannot be read.
|
||||
pub fn interface_unicast_addresses(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
) -> Result<Vec<InterfaceUnicastAddress>> {
|
||||
@@ -100,6 +156,13 @@ pub fn interface_unicast_addresses(
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Overrides an interface's routing metric until the returned guard is
|
||||
/// dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the previous metric cannot be read or the new
|
||||
/// one cannot be applied.
|
||||
pub fn set_scoped_interface_metric(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
family: IpInterfaceFamily,
|
||||
@@ -118,6 +181,12 @@ pub fn set_scoped_interface_metric(
|
||||
})
|
||||
}
|
||||
|
||||
/// Enables or disables an interface's default routes until the returned
|
||||
/// guard is dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the interface row cannot be read or updated.
|
||||
pub fn set_scoped_default_routes_disabled(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
family: IpInterfaceFamily,
|
||||
@@ -136,6 +205,12 @@ pub fn set_scoped_default_routes_disabled(
|
||||
})
|
||||
}
|
||||
|
||||
/// Overrides an interface's MTU until the returned guard is dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `mtu` is zero, or if the interface row cannot be
|
||||
/// read or updated.
|
||||
pub fn set_scoped_interface_mtu(
|
||||
identity: NetworkInterfaceIdentity,
|
||||
family: IpInterfaceFamily,
|
||||
@@ -271,6 +346,12 @@ impl Drop for ScopedInterfaceMtu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the route Windows would currently use to reach
|
||||
/// `destination`, so the relay path can be pinned to it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the Windows best-route lookup fails.
|
||||
pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
|
||||
let destination_sockaddr = sockaddr_from_ip(destination);
|
||||
let mut route = MIB_IPFORWARD_ROW2::default();
|
||||
@@ -453,6 +534,13 @@ const fn address_family(family: IpInterfaceFamily) -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs a host route that keeps relay traffic on `route`'s
|
||||
/// interface, restoring the previous state when the returned guard is
|
||||
/// dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the route cannot be installed.
|
||||
pub fn pin_relay_route(route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
|
||||
let mut pinned = pinned_route_row(route);
|
||||
let status = unsafe {
|
||||
|
||||
@@ -7,11 +7,24 @@ edition.workspace = true
|
||||
anyhow.workspace = true
|
||||
lanparty-proto = { path = "../lanparty-proto" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Registry",
|
||||
] }
|
||||
[target."cfg(windows)".dependencies]
|
||||
windows-sys = {
|
||||
workspace = true,
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Registry",
|
||||
]
|
||||
}
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
# `deny` rather than `forbid`: this crate needs OS FFI, so single
|
||||
# modules opt back in with `#![allow(unsafe_code)]`.
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! This crate deliberately stays below the relay session layer. It only knows
|
||||
//! how to find and open an installed TAP-Windows6 Ethernet adapter; the Windows
|
||||
//! client binary owns when to connect it to QUIC and how to protect routes.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use lanparty_proto::{EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN, MacAddr};
|
||||
@@ -31,6 +32,12 @@ pub struct TapAdapterInfo {
|
||||
}
|
||||
|
||||
impl TapAdapterInfo {
|
||||
/// Describes an adapter that was identified outside the registry scan.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `instance_id` is blank or `component_id` is not a
|
||||
/// supported TAP-Windows6 component id.
|
||||
pub fn new(instance_id: impl Into<String>, component_id: impl Into<String>) -> Result<Self> {
|
||||
Self::from_parts(instance_id, component_id, None)
|
||||
}
|
||||
@@ -104,6 +111,13 @@ pub fn tap_device_path(instance_id: &str) -> String {
|
||||
format!("{TAP_DEVICE_PREFIX}{instance_id}{TAP_DEVICE_SUFFIX}")
|
||||
}
|
||||
|
||||
/// Checks that a buffer read from the TAP device is a well-formed,
|
||||
/// non-oversized Ethernet frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is too short to be an Ethernet frame or is
|
||||
/// longer than [`MAX_STANDARD_ETHERNET_FRAME_LEN`].
|
||||
pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> {
|
||||
let frame = EthernetFrame::parse(frame).context("TAP Ethernet frame is malformed")?;
|
||||
if frame.is_jumbo() {
|
||||
@@ -117,13 +131,20 @@ pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders a MAC address the way the TAP-Windows6 driver's `NetworkAddress`
|
||||
/// registry value expects it: twelve upper-case hex digits, no separators.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `mac` is not a locally administered unicast address, as
|
||||
/// the driver rejects anything else.
|
||||
pub fn tap_network_address_value(mac: MacAddr) -> Result<String> {
|
||||
if !mac.is_valid_client_identity() {
|
||||
bail!("TAP MAC {mac} is not a locally administered unicast address");
|
||||
}
|
||||
|
||||
let [a, b, c, d, e, f] = mac.octets();
|
||||
Ok(format!("{a:02X}{b:02X}{c:02X}{d:02X}{e:02X}{f:02X}"))
|
||||
let [o0, o1, o2, o3, o4, o5] = mac.octets();
|
||||
Ok(format!("{o0:02X}{o1:02X}{o2:02X}{o3:02X}{o4:02X}{o5:02X}"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -152,6 +173,11 @@ mod windows;
|
||||
#[cfg(windows)]
|
||||
pub use windows::{TapAdapter, available_adapters, configure_adapter_mac, open_first_adapter};
|
||||
|
||||
/// Enumerates the installed TAP-Windows6 adapters.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Always fails on non-Windows targets; TAP-Windows6 is a Windows driver.
|
||||
#[cfg(not(windows))]
|
||||
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||
bail!("TAP-Windows6 adapter discovery is only available on Windows");
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
//! TAP-Windows6 device and registry access.
|
||||
//!
|
||||
//! All Win32 FFI is confined to this module, which is why it opts back into
|
||||
//! `unsafe`.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
io::{self, ErrorKind},
|
||||
@@ -8,25 +14,51 @@ use anyhow::{Context, Result, bail};
|
||||
use lanparty_proto::MacAddr;
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{
|
||||
CloseHandle, ERROR_FILE_NOT_FOUND, ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS,
|
||||
GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
|
||||
CloseHandle,
|
||||
ERROR_FILE_NOT_FOUND,
|
||||
ERROR_MORE_DATA,
|
||||
ERROR_NO_MORE_ITEMS,
|
||||
ERROR_SUCCESS,
|
||||
GENERIC_READ,
|
||||
GENERIC_WRITE,
|
||||
HANDLE,
|
||||
INVALID_HANDLE_VALUE,
|
||||
},
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_ATTRIBUTE_SYSTEM, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||
ReadFile, WriteFile,
|
||||
CreateFileW,
|
||||
FILE_ATTRIBUTE_SYSTEM,
|
||||
FILE_SHARE_READ,
|
||||
FILE_SHARE_WRITE,
|
||||
OPEN_EXISTING,
|
||||
ReadFile,
|
||||
WriteFile,
|
||||
},
|
||||
System::{
|
||||
IO::DeviceIoControl,
|
||||
Registry::{
|
||||
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_SET_VALUE, REG_SZ, RegCloseKey, RegEnumKeyExW,
|
||||
RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
|
||||
HKEY,
|
||||
HKEY_LOCAL_MACHINE,
|
||||
KEY_READ,
|
||||
KEY_SET_VALUE,
|
||||
REG_SZ,
|
||||
RegCloseKey,
|
||||
RegEnumKeyExW,
|
||||
RegOpenKeyExW,
|
||||
RegQueryValueExW,
|
||||
RegSetValueExW,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu,
|
||||
tap_ioctl_set_media_status, tap_network_address_value, validate_tap_ethernet_frame,
|
||||
TAP_ADAPTER_KEY,
|
||||
TapAdapterInfo,
|
||||
is_tap_component_id,
|
||||
tap_ioctl_get_mac,
|
||||
tap_ioctl_get_mtu,
|
||||
tap_ioctl_set_media_status,
|
||||
tap_network_address_value,
|
||||
validate_tap_ethernet_frame,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -36,6 +68,12 @@ pub struct TapAdapter {
|
||||
}
|
||||
|
||||
impl TapAdapter {
|
||||
/// Opens the adapter's device file for synchronous frame I/O.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the device cannot be opened, e.g. because the
|
||||
/// adapter is gone or the process lacks the required privileges.
|
||||
pub fn open(info: TapAdapterInfo) -> Result<Self> {
|
||||
let path = info.device_path();
|
||||
let wide_path = wide_null(&path);
|
||||
@@ -63,6 +101,12 @@ impl TapAdapter {
|
||||
&self.info
|
||||
}
|
||||
|
||||
/// Reports the adapter's media state to Windows, which decides whether the
|
||||
/// interface counts as connected.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the driver rejects the IOCTL.
|
||||
pub fn set_media_connected(&self, connected: bool) -> Result<()> {
|
||||
let mut status = u32::from(connected);
|
||||
self.device_io_control(
|
||||
@@ -77,6 +121,11 @@ impl TapAdapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the MAC address the driver currently presents.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the driver rejects the IOCTL.
|
||||
pub fn driver_mac(&self) -> Result<MacAddr> {
|
||||
let mut bytes = [0_u8; 6];
|
||||
self.device_io_control(
|
||||
@@ -91,6 +140,11 @@ impl TapAdapter {
|
||||
Ok(MacAddr::new(bytes))
|
||||
}
|
||||
|
||||
/// Reads the MTU the driver currently presents.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the driver rejects the IOCTL.
|
||||
pub fn driver_mtu(&self) -> Result<u32> {
|
||||
let mut mtu = 0_u32;
|
||||
self.device_io_control(
|
||||
@@ -105,6 +159,12 @@ impl TapAdapter {
|
||||
Ok(mtu)
|
||||
}
|
||||
|
||||
/// Reads one raw frame into `buffer`, returning its length.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `buffer` is larger than a Win32 read can express or
|
||||
/// the device read fails.
|
||||
pub fn read_frame(&self, buffer: &mut [u8]) -> Result<usize> {
|
||||
let mut bytes_read = 0_u32;
|
||||
let ok = unsafe {
|
||||
@@ -128,6 +188,12 @@ impl TapAdapter {
|
||||
Ok(bytes_read as usize)
|
||||
}
|
||||
|
||||
/// Reads one frame and validates it as a standard-sized Ethernet frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the read fails or the frame fails
|
||||
/// [`validate_tap_ethernet_frame`].
|
||||
pub fn read_ethernet_frame(&self, buffer: &mut [u8]) -> Result<usize> {
|
||||
let len = self.read_frame(buffer)?;
|
||||
validate_tap_ethernet_frame(&buffer[..len])?;
|
||||
@@ -135,6 +201,12 @@ impl TapAdapter {
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Writes one raw frame, returning how many bytes the driver accepted.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `frame` is larger than a Win32 write can express or
|
||||
/// the device write fails.
|
||||
pub fn write_frame(&self, frame: &[u8]) -> Result<usize> {
|
||||
let mut bytes_written = 0_u32;
|
||||
let ok = unsafe {
|
||||
@@ -155,6 +227,12 @@ impl TapAdapter {
|
||||
Ok(bytes_written as usize)
|
||||
}
|
||||
|
||||
/// Validates `frame` and writes it in full.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the frame fails [`validate_tap_ethernet_frame`], the
|
||||
/// write fails, or the driver accepts only part of the frame.
|
||||
pub fn write_ethernet_frame(&self, frame: &[u8]) -> Result<()> {
|
||||
validate_tap_ethernet_frame(frame)?;
|
||||
let written = self.write_frame(frame)?;
|
||||
@@ -196,6 +274,12 @@ impl TapAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the first installed TAP-Windows6 adapter.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if adapter enumeration fails, no adapter is installed, or
|
||||
/// the adapter cannot be opened.
|
||||
pub fn open_first_adapter() -> Result<TapAdapter> {
|
||||
let mut adapters = available_adapters()?;
|
||||
let info = adapters
|
||||
@@ -206,6 +290,13 @@ pub fn open_first_adapter() -> Result<TapAdapter> {
|
||||
TapAdapter::open(info)
|
||||
}
|
||||
|
||||
/// Persists `mac` as the adapter's `NetworkAddress`, which the driver picks up
|
||||
/// on its next restart.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the adapter was not discovered from the registry, `mac`
|
||||
/// is not a valid client identity, or the registry write fails.
|
||||
pub fn configure_adapter_mac(info: &TapAdapterInfo, mac: MacAddr) -> Result<()> {
|
||||
let driver_key_name = info
|
||||
.driver_key_name()
|
||||
@@ -219,6 +310,12 @@ pub fn configure_adapter_mac(info: &TapAdapterInfo, mac: MacAddr) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enumerates the installed TAP-Windows6 adapters by scanning the network class
|
||||
/// registry key.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the registry cannot be read.
|
||||
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
||||
.context("failed to open TAP adapter registry key")?;
|
||||
|
||||
@@ -14,5 +14,13 @@ lanparty-obs = { path = "../lanparty-obs" }
|
||||
lanparty-proto = { path = "../lanparty-proto" }
|
||||
tokio.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
[target."cfg(windows)".dependencies]
|
||||
lanparty-client-route = { path = "../lanparty-client-route" }
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
#[cfg(any(windows, test))]
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(any(windows, test))]
|
||||
@@ -12,14 +13,23 @@ use std::{sync::mpsc, thread, time::Duration};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Parser;
|
||||
use lanparty_client_core::{
|
||||
ClientIdentity, ClientIdentityStore, ClientSession, ClientSessionConfig, connect_client,
|
||||
ClientIdentity,
|
||||
ClientIdentityStore,
|
||||
ClientSession,
|
||||
ClientSessionConfig,
|
||||
connect_client,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use lanparty_client_core::{ClientReceiveOutcome, ClientRelayIo};
|
||||
#[cfg(windows)]
|
||||
use lanparty_client_route::{
|
||||
IpInterfaceFamily, NetworkInterfaceIdentity, PinnedRelayRoute, RouteSnapshot,
|
||||
ScopedDefaultRoutes, ScopedInterfaceMetric, ScopedInterfaceMtu,
|
||||
IpInterfaceFamily,
|
||||
NetworkInterfaceIdentity,
|
||||
PinnedRelayRoute,
|
||||
RouteSnapshot,
|
||||
ScopedDefaultRoutes,
|
||||
ScopedInterfaceMetric,
|
||||
ScopedInterfaceMtu,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use lanparty_client_tap::TapAdapter;
|
||||
@@ -375,6 +385,8 @@ async fn run_client(
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
// Mirrors the Windows signature so the call site needs no `cfg`.
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn run_client(_session: &ClientSession) -> Result<()> {
|
||||
unreachable!("ensure_supported_platform rejects non-Windows before tunnel setup")
|
||||
}
|
||||
@@ -747,24 +759,19 @@ fn client_frame_log_line(
|
||||
};
|
||||
let source_mac = log
|
||||
.source_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let destination_mac = log
|
||||
.destination_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let ethertype_or_len = log
|
||||
.ethertype_or_len()
|
||||
.map(|value| format!("0x{value:04x}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}"));
|
||||
let peer_id = log
|
||||
.peer_id()
|
||||
.map(|peer_id| peer_id.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string());
|
||||
let drop_reason = log
|
||||
.drop_reason()
|
||||
.map(|reason| format!("{reason:?}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}"));
|
||||
|
||||
format!(
|
||||
"client frame direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}",
|
||||
@@ -925,8 +932,7 @@ impl ControlEventFormatter {
|
||||
Role::Client => {
|
||||
let mac = peer
|
||||
.mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
|
||||
format!(
|
||||
"relay event: client peer {} with MAC {} left ({reason:?})",
|
||||
peer.peer_id(),
|
||||
@@ -952,8 +958,7 @@ fn format_peer_joined(peer: &PeerInfo) -> String {
|
||||
Role::Client => {
|
||||
let mac = peer
|
||||
.mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
|
||||
format!(
|
||||
"relay event: client peer {} joined with MAC {}",
|
||||
peer.peer_id(),
|
||||
@@ -1263,11 +1268,12 @@ mod tests {
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use lanparty_ctrl::{DisconnectReason, PeerInfo};
|
||||
use lanparty_net::DEFAULT_RELAY_PORT;
|
||||
use lanparty_obs::{QuicDiagnostics, TunnelStats};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn rejects_runtime_on_non_windows() {
|
||||
|
||||
@@ -9,3 +9,11 @@ lanparty-proto = { path = "../lanparty-proto" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -23,24 +23,49 @@ pub enum ControlCodecError {
|
||||
InvalidMessage(#[from] ControlError),
|
||||
}
|
||||
|
||||
/// Encodes a control message as a length-prefixed JSON frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlCodecError::InvalidMessage`] if `message` does not pass its
|
||||
/// own validation, [`ControlCodecError::Json`] if serialization fails, or
|
||||
/// [`ControlCodecError::MessageTooLarge`] if the payload exceeds
|
||||
/// [`MAX_CONTROL_MESSAGE_LEN`].
|
||||
pub fn encode_control_message(message: &ControlMessage) -> Result<Vec<u8>, ControlCodecError> {
|
||||
message.validate()?;
|
||||
|
||||
let payload = serde_json::to_vec(message)?;
|
||||
let payload_len = payload.len();
|
||||
if payload_len > MAX_CONTROL_MESSAGE_LEN {
|
||||
// The wire prefix is a u32, and MAX_CONTROL_MESSAGE_LEN keeps it in range,
|
||||
// so both conditions collapse into the same "too large" error.
|
||||
let Some(prefix) = u32::try_from(payload_len)
|
||||
.ok()
|
||||
.filter(|_| payload_len <= MAX_CONTROL_MESSAGE_LEN)
|
||||
else {
|
||||
return Err(ControlCodecError::MessageTooLarge {
|
||||
len: payload_len,
|
||||
max: MAX_CONTROL_MESSAGE_LEN,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut frame = Vec::with_capacity(CONTROL_LENGTH_PREFIX_LEN + payload_len);
|
||||
frame.extend_from_slice(&(payload_len as u32).to_be_bytes());
|
||||
frame.extend_from_slice(&prefix.to_be_bytes());
|
||||
frame.extend_from_slice(&payload);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Decodes exactly one control frame, which must contain one whole message and
|
||||
/// nothing else.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlCodecError::FrameTooShort`] or
|
||||
/// [`ControlCodecError::IncompletePayload`] if `frame` does not hold a complete
|
||||
/// message, [`ControlCodecError::TrailingBytes`] if it holds more than one,
|
||||
/// [`ControlCodecError::MessageTooLarge`] if the declared length is over the
|
||||
/// limit, [`ControlCodecError::Json`] if the payload is not valid JSON, or
|
||||
/// [`ControlCodecError::InvalidMessage`] if the decoded message fails
|
||||
/// validation.
|
||||
pub fn decode_control_frame(frame: &[u8]) -> Result<ControlMessage, ControlCodecError> {
|
||||
let Some(total_len) = complete_control_frame_len(frame)? else {
|
||||
return Err(incomplete_frame_error(frame));
|
||||
@@ -59,6 +84,14 @@ pub fn decode_control_frame(frame: &[u8]) -> Result<ControlMessage, ControlCodec
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Returns the total length of the first complete frame in `buffer`, or `None`
|
||||
/// if more bytes are still needed. Stream readers use this to decide when a
|
||||
/// frame can be handed to [`decode_control_frame`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlCodecError::MessageTooLarge`] if the declared payload
|
||||
/// length exceeds [`MAX_CONTROL_MESSAGE_LEN`].
|
||||
pub fn complete_control_frame_len(buffer: &[u8]) -> Result<Option<usize>, ControlCodecError> {
|
||||
if buffer.len() < CONTROL_LENGTH_PREFIX_LEN {
|
||||
return Ok(None);
|
||||
@@ -81,18 +114,14 @@ pub fn complete_control_frame_len(buffer: &[u8]) -> Result<Option<usize>, Contro
|
||||
}
|
||||
|
||||
fn declared_payload_len(buffer: &[u8]) -> Result<usize, ControlCodecError> {
|
||||
if buffer.len() < CONTROL_LENGTH_PREFIX_LEN {
|
||||
let Some(prefix) = buffer.first_chunk::<CONTROL_LENGTH_PREFIX_LEN>() else {
|
||||
return Err(ControlCodecError::FrameTooShort {
|
||||
actual: buffer.len(),
|
||||
minimum: CONTROL_LENGTH_PREFIX_LEN,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(u32::from_be_bytes(
|
||||
buffer[0..CONTROL_LENGTH_PREFIX_LEN]
|
||||
.try_into()
|
||||
.expect("length prefix slice has exact size"),
|
||||
) as usize)
|
||||
Ok(u32::from_be_bytes(*prefix) as usize)
|
||||
}
|
||||
|
||||
fn incomplete_frame_error(frame: &[u8]) -> ControlCodecError {
|
||||
@@ -174,7 +203,8 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_oversized_declared_length() {
|
||||
let mut frame = [0; CONTROL_LENGTH_PREFIX_LEN];
|
||||
frame.copy_from_slice(&((MAX_CONTROL_MESSAGE_LEN as u32) + 1).to_be_bytes());
|
||||
let oversized = u32::try_from(MAX_CONTROL_MESSAGE_LEN).unwrap() + 1;
|
||||
frame.copy_from_slice(&oversized.to_be_bytes());
|
||||
|
||||
assert!(matches!(
|
||||
complete_control_frame_len(&frame).unwrap_err(),
|
||||
@@ -185,11 +215,10 @@ mod tests {
|
||||
#[test]
|
||||
fn validates_decoded_messages() {
|
||||
let json = format!(
|
||||
r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{}}}}}"#,
|
||||
MIN_USEFUL_TAP_MTU
|
||||
r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}}}"#
|
||||
);
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&(json.len() as u32).to_be_bytes());
|
||||
frame.extend_from_slice(&u32::try_from(json.len()).unwrap().to_be_bytes());
|
||||
frame.extend_from_slice(json.as_bytes());
|
||||
|
||||
assert!(matches!(
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
//! QUIC streams carry these messages as length-prefixed JSON frames. The crate
|
||||
//! defines the typed handshake/status model and the small framing layer needed
|
||||
//! by client, relay, and gateway stream handlers.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
mod codec;
|
||||
|
||||
pub use codec::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlCodecError, MAX_CONTROL_MESSAGE_LEN,
|
||||
complete_control_frame_len, decode_control_frame, encode_control_message,
|
||||
CONTROL_LENGTH_PREFIX_LEN,
|
||||
ControlCodecError,
|
||||
MAX_CONTROL_MESSAGE_LEN,
|
||||
complete_control_frame_len,
|
||||
decode_control_frame,
|
||||
encode_control_message,
|
||||
};
|
||||
pub use lanparty_obs::TunnelStats;
|
||||
use lanparty_proto::{MIN_USEFUL_TAP_MTU, MacAddr, MtuError, recommended_tap_mtu};
|
||||
@@ -50,6 +55,12 @@ pub enum ControlError {
|
||||
pub struct RoomCode(String);
|
||||
|
||||
impl RoomCode {
|
||||
/// Validates and wraps a room code.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`ControlError`] variant describing which room-code rule the
|
||||
/// value breaks (length or allowed characters).
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ControlError> {
|
||||
let value = value.into();
|
||||
validate_room_code(&value)?;
|
||||
@@ -131,6 +142,13 @@ pub struct EndpointHello {
|
||||
}
|
||||
|
||||
impl EndpointHello {
|
||||
/// Builds the hello a tunnel client sends.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `announced_mac` is not a locally administered
|
||||
/// unicast address or `max_datagram_size` is too small to carry a useful
|
||||
/// TAP MTU.
|
||||
pub fn client(
|
||||
room: RoomCode,
|
||||
announced_mac: MacAddr,
|
||||
@@ -148,6 +166,13 @@ impl EndpointHello {
|
||||
Ok(hello)
|
||||
}
|
||||
|
||||
/// Builds the hello the LAN gateway sends. A gateway announces no MAC
|
||||
/// because it bridges the whole LAN rather than one identity.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `max_datagram_size` is too small to carry a useful
|
||||
/// TAP MTU.
|
||||
pub fn gateway(room: RoomCode, max_datagram_size: u16) -> Result<Self, ControlError> {
|
||||
let hello = Self {
|
||||
protocol_version: CONTROL_PROTOCOL_VERSION,
|
||||
@@ -161,6 +186,14 @@ impl EndpointHello {
|
||||
Ok(hello)
|
||||
}
|
||||
|
||||
/// Re-checks a decoded [`EndpointHello`], which serde may have built
|
||||
/// without going through the constructors.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the protocol version is not supported, the datagram
|
||||
/// size cannot carry a useful TAP MTU, or the role and announced MAC do not
|
||||
/// match.
|
||||
pub fn validate(&self) -> Result<(), ControlError> {
|
||||
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
|
||||
return Err(ControlError::UnsupportedVersion {
|
||||
@@ -221,6 +254,13 @@ pub struct ServerWelcome {
|
||||
}
|
||||
|
||||
impl ServerWelcome {
|
||||
/// Builds the welcome the relay sends once a peer is admitted to a room.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlError::InvalidPeerId`] if `peer_id` is 0, or
|
||||
/// [`ControlError::EffectiveMtuTooSmall`] if `effective_tap_mtu` is below
|
||||
/// [`MIN_USEFUL_TAP_MTU`].
|
||||
pub fn new(room_id: u64, peer_id: u32, effective_tap_mtu: u16) -> Result<Self, ControlError> {
|
||||
if peer_id == 0 {
|
||||
return Err(ControlError::InvalidPeerId);
|
||||
@@ -268,6 +308,13 @@ impl ServerWelcome {
|
||||
self
|
||||
}
|
||||
|
||||
/// Re-checks a decoded [`ServerWelcome`], which serde may have built
|
||||
/// without going through [`ServerWelcome::new`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the protocol version is not supported, a peer id is
|
||||
/// 0, or the effective TAP MTU is below [`MIN_USEFUL_TAP_MTU`].
|
||||
pub fn validate(&self) -> Result<(), ControlError> {
|
||||
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
|
||||
return Err(ControlError::UnsupportedVersion {
|
||||
@@ -338,6 +385,12 @@ pub struct PeerInfo {
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
/// Describes one peer of a room.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `peer_id` is 0, a client has no or an unusable MAC,
|
||||
/// or a gateway announces one.
|
||||
pub fn new(peer_id: u32, role: Role, mac: Option<MacAddr>) -> Result<Self, ControlError> {
|
||||
if peer_id == 0 {
|
||||
return Err(ControlError::InvalidPeerId);
|
||||
@@ -360,6 +413,12 @@ impl PeerInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-checks a decoded [`PeerInfo`], which serde may have built without
|
||||
/// going through [`PeerInfo::new`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the peer id is 0 or the role and MAC do not match.
|
||||
pub fn validate(&self) -> Result<(), ControlError> {
|
||||
if self.peer_id == 0 {
|
||||
return Err(ControlError::InvalidPeerId);
|
||||
@@ -459,14 +518,21 @@ pub enum ControlMessage {
|
||||
}
|
||||
|
||||
impl ControlMessage {
|
||||
/// Validates a decoded control message by delegating to the payload type.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the [`ControlError`] reported by the contained payload, or
|
||||
/// [`ControlError::InvalidPeerId`] for a peer-left notice about peer 0.
|
||||
pub fn validate(&self) -> Result<(), ControlError> {
|
||||
match self {
|
||||
Self::Hello(hello) => hello.validate(),
|
||||
Self::Welcome(welcome) => welcome.validate(),
|
||||
Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } => Ok(()),
|
||||
Self::PeerJoined(peer) => peer.validate(),
|
||||
Self::PeerLeft { peer_id, .. } if *peer_id == 0 => Err(ControlError::InvalidPeerId),
|
||||
Self::PeerLeft { .. } => Ok(()),
|
||||
Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } | Self::PeerLeft { .. } => {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,15 +621,15 @@ mod tests {
|
||||
#[test]
|
||||
fn server_welcome_rejects_reserved_peer_id_and_tiny_mtu() {
|
||||
assert_eq!(
|
||||
ServerWelcome::new(1, 0, MIN_USEFUL_TAP_MTU as u16).unwrap_err(),
|
||||
ServerWelcome::new(1, 0, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap_err(),
|
||||
ControlError::InvalidPeerId
|
||||
);
|
||||
assert!(matches!(
|
||||
ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(),
|
||||
ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU - 1).unwrap()).unwrap_err(),
|
||||
ControlError::EffectiveMtuTooSmall { .. }
|
||||
));
|
||||
assert_eq!(
|
||||
ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16)
|
||||
ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap())
|
||||
.unwrap()
|
||||
.with_gateway_peer_id(Some(0))
|
||||
.validate()
|
||||
@@ -574,7 +640,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn server_welcome_reports_gateway_presence() {
|
||||
let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16).unwrap();
|
||||
let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(welcome.mode(), ConnectionMode::Relay);
|
||||
assert!(!welcome.gateway_connected());
|
||||
@@ -591,7 +657,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn server_welcome_reports_connection_mode() {
|
||||
let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16)
|
||||
let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap())
|
||||
.unwrap()
|
||||
.with_mode(ConnectionMode::DirectFailedRelayFallback);
|
||||
|
||||
@@ -615,8 +681,7 @@ mod tests {
|
||||
#[test]
|
||||
fn server_welcome_defaults_missing_mode_to_relay() {
|
||||
let json = format!(
|
||||
r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{}}}"#,
|
||||
MIN_USEFUL_TAP_MTU
|
||||
r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}"#
|
||||
);
|
||||
let welcome: ServerWelcome = serde_json::from_str(&json).unwrap();
|
||||
|
||||
|
||||
@@ -18,3 +18,13 @@ tokio.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
# `deny` rather than `forbid`: this crate needs OS FFI, so single
|
||||
# modules opt back in with `#![allow(unsafe_code)]`.
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Linux LAN gateway control-plane connection.
|
||||
//!
|
||||
//! This crate owns the gateway binary's relay connection and Linux AF_PACKET
|
||||
//! This crate owns the gateway binary's relay connection and Linux `AF_PACKET`
|
||||
//! bridge loop that moves Ethernet frames between the relay and wired LAN.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod packet;
|
||||
@@ -23,31 +24,46 @@ use anyhow::{Context, Result, bail};
|
||||
use bytes::Bytes;
|
||||
use clap::Parser;
|
||||
use lanparty_ctrl::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlMessage, DisconnectReason, EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Role, RoomCode, ServerWelcome,
|
||||
decode_control_frame, encode_control_message,
|
||||
CONTROL_LENGTH_PREFIX_LEN,
|
||||
ControlMessage,
|
||||
DisconnectReason,
|
||||
EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN,
|
||||
PeerInfo,
|
||||
RELAY_ALPN,
|
||||
Role,
|
||||
RoomCode,
|
||||
ServerWelcome,
|
||||
decode_control_frame,
|
||||
encode_control_message,
|
||||
};
|
||||
use lanparty_net::RelayEndpoint;
|
||||
use lanparty_obs::{DropReason, TunnelStats};
|
||||
#[cfg(target_os = "linux")]
|
||||
use lanparty_obs::{FrameAction, FrameDirection, FrameLog};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, FrameType, MacAddr, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram,
|
||||
ethernet_frame_exceeds_tap_mtu, gateway_lan_safety_drop_reason,
|
||||
remote_client_safety_drop_reason, validate_datagram_budget,
|
||||
EthernetFrame,
|
||||
FrameType,
|
||||
MacAddr,
|
||||
OVERLAY_FLAGS_NONE,
|
||||
decode_datagram,
|
||||
encode_datagram,
|
||||
ethernet_frame_exceeds_tap_mtu,
|
||||
gateway_lan_safety_drop_reason,
|
||||
remote_client_safety_drop_reason,
|
||||
validate_datagram_budget,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use packet::PacketSocket;
|
||||
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||
use rustls::pki_types::CertificateDer;
|
||||
#[cfg(target_os = "linux")]
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use packet::PacketSocket;
|
||||
|
||||
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN;
|
||||
const DISCONNECT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
#[cfg(target_os = "linux")]
|
||||
const CAM_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const CAM_REFRESH_INTERVAL: Duration = Duration::from_mins(1);
|
||||
#[cfg(target_os = "linux")]
|
||||
const GATEWAY_STATS_INTERVAL: Duration = Duration::from_secs(10);
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -87,7 +103,7 @@ pub struct GatewayArgs {
|
||||
#[arg(long)]
|
||||
room: RoomCode,
|
||||
|
||||
/// Wired LAN interface that will later be opened with AF_PACKET.
|
||||
/// Wired LAN interface that will later be opened with `AF_PACKET`.
|
||||
#[arg(long, visible_alias = "iface")]
|
||||
interface: String,
|
||||
|
||||
@@ -97,6 +113,13 @@ pub struct GatewayArgs {
|
||||
}
|
||||
|
||||
impl GatewayArgs {
|
||||
/// Resolves the parsed arguments into a validated [`GatewayConfig`],
|
||||
/// reading the relay CA certificate from disk.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the CA certificate cannot be read, the relay
|
||||
/// endpoint cannot be resolved, or the resulting configuration is invalid.
|
||||
pub fn into_config(self) -> Result<GatewayConfig> {
|
||||
let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| {
|
||||
format!(
|
||||
@@ -132,6 +155,12 @@ pub struct GatewayConfig {
|
||||
}
|
||||
|
||||
impl GatewayConfig {
|
||||
/// Assembles everything the gateway needs to bridge one LAN into one room.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the server name, CA certificate, or interface name is
|
||||
/// empty, or if `max_datagram_size` cannot carry a useful TAP MTU.
|
||||
pub fn new(
|
||||
relay_addr: SocketAddr,
|
||||
server_name: impl Into<String>,
|
||||
@@ -255,6 +284,13 @@ impl GatewayConnection {
|
||||
self.quic_max_datagram_size
|
||||
}
|
||||
|
||||
/// Forwards one LAN Ethernet frame to the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the frame is malformed, filtered by the LAN safety
|
||||
/// rules, exceeds the negotiated datagram budget, or the datagram send
|
||||
/// fails.
|
||||
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
|
||||
match send_gateway_ethernet(
|
||||
&self.connection,
|
||||
@@ -273,10 +309,21 @@ impl GatewayConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the next Ethernet frame a remote client sent into the room.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC connection fails while reading.
|
||||
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
|
||||
recv_gateway_ethernet(&self.connection, &self.welcome, &self.stats).await
|
||||
}
|
||||
|
||||
/// Waits for the next control-plane message from the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the control stream fails or carries a frame that
|
||||
/// cannot be decoded.
|
||||
pub async fn recv_control_event(&self) -> Result<ControlMessage> {
|
||||
recv_gateway_control_event(&self.connection).await
|
||||
}
|
||||
@@ -286,11 +333,26 @@ impl GatewayConnection {
|
||||
self.stats.snapshot()
|
||||
}
|
||||
|
||||
/// Reports the current tunnel counters to the relay.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the control stream write fails.
|
||||
pub async fn send_stats_snapshot(&self) -> Result<()> {
|
||||
send_gateway_stats(&self.connection, self.stats.snapshot()).await
|
||||
}
|
||||
|
||||
/// Runs the LAN-to-relay bridge until the connection closes or the process
|
||||
/// is asked to shut down.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the LAN socket or the relay connection fails in a
|
||||
/// way the bridge cannot recover from.
|
||||
#[cfg(target_os = "linux")]
|
||||
// The bridge is one `select!` loop over every event source; splitting arms
|
||||
// into helpers would hide the shared state they all mutate.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn bridge_until_shutdown(self, packet_socket: PacketSocket) -> Result<()> {
|
||||
let mut remote_clients = RemoteClientTable::new(packet_socket.interface_mac());
|
||||
let mut cam_refresh_tick = tokio::time::interval_at(
|
||||
@@ -581,12 +643,9 @@ async fn recv_gateway_ethernet_outcome(
|
||||
stats.record_dropped_frame();
|
||||
continue;
|
||||
}
|
||||
let ethernet_frame = match EthernetFrame::parse(packet.payload()) {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => {
|
||||
stats.record_malformed_frame();
|
||||
continue;
|
||||
}
|
||||
let Ok(ethernet_frame) = EthernetFrame::parse(packet.payload()) else {
|
||||
stats.record_malformed_frame();
|
||||
continue;
|
||||
};
|
||||
|
||||
stats.record_ethernet_rx(ethernet_frame);
|
||||
@@ -742,8 +801,7 @@ fn format_gateway_control_event(event: &ControlMessage) -> String {
|
||||
ControlMessage::PeerJoined(peer) if peer.role() == Role::Client => {
|
||||
let mac = peer
|
||||
.mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
|
||||
format!(
|
||||
"gateway control event: client peer {} joined with MAC {}",
|
||||
peer.peer_id(),
|
||||
@@ -781,24 +839,19 @@ fn gateway_frame_log_line(
|
||||
};
|
||||
let source_mac = log
|
||||
.source_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let destination_mac = log
|
||||
.destination_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let ethertype_or_len = log
|
||||
.ethertype_or_len()
|
||||
.map(|value| format!("0x{value:04x}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}"));
|
||||
let peer_id = log
|
||||
.peer_id()
|
||||
.map(|peer_id| peer_id.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string());
|
||||
let drop_reason = log
|
||||
.drop_reason()
|
||||
.map(|reason| format!("{reason:?}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}"));
|
||||
|
||||
format!(
|
||||
"gateway frame interface={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}",
|
||||
@@ -838,7 +891,7 @@ async fn read_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>) -> Result<Byte
|
||||
return Ok(Bytes::from(buffer));
|
||||
}
|
||||
Ok(Err(error)) => return Err(error).context("failed to read LAN Ethernet frame"),
|
||||
Err(_would_block) => continue,
|
||||
Err(_would_block) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -855,7 +908,7 @@ async fn write_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>, frame: &[u8])
|
||||
Ok(Ok(sent)) if sent == frame.len() => return Ok(()),
|
||||
Ok(Ok(sent)) => bail!("partial LAN Ethernet frame write: {sent}/{}", frame.len()),
|
||||
Ok(Err(error)) => return Err(error).context("failed to write LAN Ethernet frame"),
|
||||
Err(_would_block) => continue,
|
||||
Err(_would_block) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,6 +1034,14 @@ fn cam_refresh_frame(source: MacAddr, destination: MacAddr) -> Vec<u8> {
|
||||
frame
|
||||
}
|
||||
|
||||
/// Connects to the relay, announces the gateway hello, and returns the
|
||||
/// connection once the relay answers with a welcome.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the QUIC endpoint cannot be created or connected, the
|
||||
/// relay does not negotiate DATAGRAM support, the control handshake fails, or
|
||||
/// the relay rejects the hello.
|
||||
pub async fn connect_gateway(config: GatewayConfig) -> Result<GatewayConnection> {
|
||||
let client_config = relay_client_config(config.relay_ca_cert_der())?;
|
||||
let mut endpoint = Endpoint::client(client_bind_addr(config.relay_addr()))
|
||||
@@ -994,9 +1055,9 @@ pub async fn connect_gateway(config: GatewayConfig) -> Result<GatewayConnection>
|
||||
let peer_datagram_size = connection
|
||||
.max_datagram_size()
|
||||
.context("relay did not negotiate QUIC DATAGRAM support")?;
|
||||
let hello_datagram_size = usize::from(config.max_datagram_size())
|
||||
.min(peer_datagram_size)
|
||||
.min(usize::from(u16::MAX)) as u16;
|
||||
let hello_datagram_size = u16::try_from(peer_datagram_size)
|
||||
.unwrap_or(u16::MAX)
|
||||
.min(config.max_datagram_size());
|
||||
let hello = EndpointHello::gateway(config.room().clone(), hello_datagram_size)
|
||||
.context("failed to build gateway hello")?;
|
||||
let response = request_control_message(&connection, ControlMessage::Hello(hello)).await?;
|
||||
@@ -1146,12 +1207,15 @@ mod tests {
|
||||
let help = String::from_utf8(help).unwrap();
|
||||
|
||||
assert!(
|
||||
help.contains("[aliases: --iface]"),
|
||||
help.contains("[alias: --iface]") || help.contains("[aliases: --iface]"),
|
||||
"gateway help should advertise --iface alias:\n{help}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// One end-to-end handshake: a scripted relay plus the gateway side of the
|
||||
// exchange only make sense read together.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn connects_to_relay_control_stream_as_gateway() {
|
||||
let (server_config, certificate) = test_server_config();
|
||||
let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
|
||||
@@ -1324,10 +1388,10 @@ mod tests {
|
||||
#[test]
|
||||
fn snapshots_gateway_broadcast_stats() {
|
||||
let stats = GatewayTunnelStats::default();
|
||||
let broadcast_tx_bytes = broadcast_ethernet_frame(b"broadcast tx");
|
||||
let broadcast_rx_bytes = broadcast_ethernet_frame(b"broadcast rx");
|
||||
let broadcast_tx = EthernetFrame::parse(&broadcast_tx_bytes).unwrap();
|
||||
let broadcast_rx = EthernetFrame::parse(&broadcast_rx_bytes).unwrap();
|
||||
let sent_frame_bytes = broadcast_ethernet_frame(b"broadcast tx");
|
||||
let received_frame_bytes = broadcast_ethernet_frame(b"broadcast rx");
|
||||
let broadcast_tx = EthernetFrame::parse(&sent_frame_bytes).unwrap();
|
||||
let broadcast_rx = EthernetFrame::parse(&received_frame_bytes).unwrap();
|
||||
|
||||
stats.record_ethernet_tx(broadcast_tx);
|
||||
stats.record_datagram_rx();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
use clap::Parser;
|
||||
use lanparty_gateway::GatewayArgs;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
//! Raw `AF_PACKET` socket plumbing for the Linux gateway.
|
||||
//!
|
||||
//! Everything that talks to the kernel through libc lives here, which is why
|
||||
//! this is the one gateway module that opts back into `unsafe`.
|
||||
#![allow(unsafe_code)]
|
||||
// The libc bindings type kernel ABI fields as whatever C type the header uses
|
||||
// (`c_char`, `c_int`, `c_ushort`, ...). Moving values between those and the Rust
|
||||
// types used here is reinterpretation of a value that is already known to fit -
|
||||
// interface indices, `ETH_P_ALL`, MAC octets, and the byte counts returned by
|
||||
// send/recv after their error check - so a fallible conversion would only add
|
||||
// unreachable branches.
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
|
||||
use std::{
|
||||
ffi::CString,
|
||||
fs, io,
|
||||
fs,
|
||||
io,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||
path::Path,
|
||||
};
|
||||
@@ -21,6 +39,14 @@ pub struct PacketSocket {
|
||||
}
|
||||
|
||||
impl PacketSocket {
|
||||
/// Opens a promiscuous `AF_PACKET` socket bound to `interface`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the interface name is unusable or unknown, the
|
||||
/// interface is wireless or has no carrier, the interface is not Ethernet,
|
||||
/// or any of the socket, bind, promiscuous-membership, or MAC-lookup calls
|
||||
/// fails - typically for lack of `CAP_NET_RAW`.
|
||||
pub fn open(interface: &str) -> io::Result<Self> {
|
||||
let interface_index = interface_index(interface)?;
|
||||
reject_wireless_interface(interface)?;
|
||||
@@ -57,7 +83,7 @@ impl PacketSocket {
|
||||
// matches that struct. fd remains owned by this function across the call.
|
||||
libc::bind(
|
||||
fd.as_raw_fd(),
|
||||
(&address as *const libc::sockaddr_ll).cast::<libc::sockaddr>(),
|
||||
(&raw const address).cast::<libc::sockaddr>(),
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
@@ -90,6 +116,13 @@ impl PacketSocket {
|
||||
self.interface_mac
|
||||
}
|
||||
|
||||
/// Sends one Ethernet frame on the LAN, returning the number of bytes the
|
||||
/// kernel accepted.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the OS error if the send fails, including `WouldBlock` on this
|
||||
/// non-blocking socket.
|
||||
pub fn send_frame(&self, frame: &[u8]) -> io::Result<usize> {
|
||||
let sent = unsafe {
|
||||
// SAFETY: frame.as_ptr() is valid for frame.len() bytes for the duration of send,
|
||||
@@ -108,6 +141,13 @@ impl PacketSocket {
|
||||
Ok(sent as usize)
|
||||
}
|
||||
|
||||
/// Receives one inbound Ethernet frame into `buffer`, skipping the copies
|
||||
/// of our own outgoing frames that `AF_PACKET` also delivers.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the OS error if the receive fails, including `WouldBlock` on this
|
||||
/// non-blocking socket.
|
||||
pub fn recv_frame(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
let mut address = unsafe {
|
||||
@@ -125,8 +165,8 @@ impl PacketSocket {
|
||||
buffer.as_mut_ptr().cast::<libc::c_void>(),
|
||||
buffer.len(),
|
||||
0,
|
||||
(&mut address as *mut libc::sockaddr_ll).cast::<libc::sockaddr>(),
|
||||
&mut address_len,
|
||||
(&raw mut address).cast::<libc::sockaddr>(),
|
||||
&raw mut address_len,
|
||||
)
|
||||
};
|
||||
if received < 0 {
|
||||
@@ -145,6 +185,12 @@ impl AsRawFd for PacketSocket {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a network interface name to its kernel index.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the name is empty, contains a path separator or NUL, is
|
||||
/// longer than `IFNAMSIZ`, or does not name an existing interface.
|
||||
pub fn interface_index(interface: &str) -> io::Result<u32> {
|
||||
let name = interface_name(interface)?;
|
||||
|
||||
@@ -169,7 +215,7 @@ fn enable_promiscuous_membership(fd: RawFd, interface_index: u32) -> io::Result<
|
||||
fd,
|
||||
libc::SOL_PACKET,
|
||||
libc::PACKET_ADD_MEMBERSHIP,
|
||||
(&membership as *const libc::packet_mreq).cast::<libc::c_void>(),
|
||||
(&raw const membership).cast::<libc::c_void>(),
|
||||
std::mem::size_of::<libc::packet_mreq>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -5,3 +5,11 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Shared network endpoint parsing for LAN party tunnel binaries.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -17,6 +18,12 @@ pub struct RelayEndpoint {
|
||||
}
|
||||
|
||||
impl RelayEndpoint {
|
||||
/// Builds an endpoint from an already split host and port.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`RelayEndpointError::EmptyHost`] if `host` is empty or only
|
||||
/// whitespace, or [`RelayEndpointError::InvalidPort`] if `port` is zero.
|
||||
pub fn new(host: impl Into<String>, port: u16) -> Result<Self, RelayEndpointError> {
|
||||
let host = host.into();
|
||||
let host = host.trim();
|
||||
@@ -45,6 +52,14 @@ impl RelayEndpoint {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// Resolves the endpoint to its first socket address, performing a DNS
|
||||
/// lookup when the host is not an IP literal.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`RelayEndpointError::ResolveFailed`] if resolution itself
|
||||
/// fails, or [`RelayEndpointError::NoResolvedAddress`] if it succeeds but
|
||||
/// yields no address.
|
||||
pub fn resolve(&self) -> Result<SocketAddr, RelayEndpointError> {
|
||||
let mut addrs = (self.host.as_str(), self.port)
|
||||
.to_socket_addrs()
|
||||
|
||||
@@ -9,3 +9,11 @@ serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! Runtime crates can convert these values into `tracing` fields, JSON logs, or
|
||||
//! user-facing status lines without each component inventing its own vocabulary.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
|
||||
@@ -6,3 +6,11 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -12,6 +12,13 @@ pub struct EthernetFrame<'a> {
|
||||
}
|
||||
|
||||
impl<'a> EthernetFrame<'a> {
|
||||
/// Borrows `bytes` as an Ethernet frame after checking that it is long
|
||||
/// enough to contain a complete Ethernet header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::EthernetFrameTooShort`] if `bytes` is shorter than
|
||||
/// [`MIN_ETHERNET_FRAME_LEN`].
|
||||
pub fn parse(bytes: &'a [u8]) -> Result<Self, ProtoError> {
|
||||
if bytes.len() < MIN_ETHERNET_FRAME_LEN {
|
||||
return Err(ProtoError::EthernetFrameTooShort {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! This crate intentionally contains no socket, TAP, QUIC, or OS-specific
|
||||
//! behavior. It is the small contract that the Windows client, Linux gateway,
|
||||
//! and relay must all agree on.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
mod ethernet;
|
||||
mod mac;
|
||||
@@ -11,22 +12,46 @@ mod overlay;
|
||||
mod safety;
|
||||
|
||||
pub use ethernet::{
|
||||
ETHERNET_HEADER_LEN, EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN,
|
||||
MAX_STANDARD_ETHERNET_PAYLOAD_LEN, MIN_ETHERNET_FRAME_LEN,
|
||||
ETHERNET_HEADER_LEN,
|
||||
EthernetFrame,
|
||||
MAX_STANDARD_ETHERNET_FRAME_LEN,
|
||||
MAX_STANDARD_ETHERNET_PAYLOAD_LEN,
|
||||
MIN_ETHERNET_FRAME_LEN,
|
||||
};
|
||||
pub use mac::{MacAddr, MacParseError};
|
||||
pub use mtu::{
|
||||
DEFAULT_DATAGRAM_SAFETY_MARGIN, DEFAULT_TAP_MTU, MIN_USEFUL_TAP_MTU, MtuError,
|
||||
ethernet_frame_exceeds_tap_mtu, max_ethernet_frame_len_for_tap_mtu, max_tap_mtu_for_datagram,
|
||||
DEFAULT_DATAGRAM_SAFETY_MARGIN,
|
||||
DEFAULT_TAP_MTU,
|
||||
MIN_USEFUL_TAP_MTU,
|
||||
MtuError,
|
||||
ethernet_frame_exceeds_tap_mtu,
|
||||
max_ethernet_frame_len_for_tap_mtu,
|
||||
max_tap_mtu_for_datagram,
|
||||
recommended_tap_mtu,
|
||||
};
|
||||
pub use overlay::{
|
||||
FrameType, OVERLAY_FLAGS_NONE, OVERLAY_HEADER_LEN, OVERLAY_MAGIC, OVERLAY_VERSION,
|
||||
OverlayHeader, OverlayPacket, ProtoError, decode_datagram, encode_datagram,
|
||||
FrameType,
|
||||
OVERLAY_FLAGS_NONE,
|
||||
OVERLAY_HEADER_LEN,
|
||||
OVERLAY_MAGIC,
|
||||
OVERLAY_VERSION,
|
||||
OverlayHeader,
|
||||
OverlayPacket,
|
||||
ProtoError,
|
||||
decode_datagram,
|
||||
encode_datagram,
|
||||
validate_datagram_budget,
|
||||
};
|
||||
pub use safety::{
|
||||
ETHERTYPE_8021AD, ETHERTYPE_8021Q, ETHERTYPE_EAPOL, ETHERTYPE_IPV4, ETHERTYPE_IPV6,
|
||||
ETHERTYPE_LLDP, ETHERTYPE_QINQ, ETHERTYPE_SLOW_PROTOCOLS, EthernetSafetyDrop,
|
||||
gateway_lan_safety_drop_reason, remote_client_safety_drop_reason,
|
||||
ETHERTYPE_8021AD,
|
||||
ETHERTYPE_8021Q,
|
||||
ETHERTYPE_EAPOL,
|
||||
ETHERTYPE_IPV4,
|
||||
ETHERTYPE_IPV6,
|
||||
ETHERTYPE_LLDP,
|
||||
ETHERTYPE_QINQ,
|
||||
ETHERTYPE_SLOW_PROTOCOLS,
|
||||
EthernetSafetyDrop,
|
||||
gateway_lan_safety_drop_reason,
|
||||
remote_client_safety_drop_reason,
|
||||
};
|
||||
|
||||
@@ -91,8 +91,8 @@ impl From<MacAddr> for [u8; 6] {
|
||||
|
||||
impl fmt::Display for MacAddr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let [a, b, c, d, e, g] = self.0;
|
||||
write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}")
|
||||
let [o0, o1, o2, o3, o4, o5] = self.0;
|
||||
write!(f, "{o0:02x}:{o1:02x}:{o2:02x}:{o3:02x}:{o4:02x}:{o5:02x}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@ pub enum MtuError {
|
||||
},
|
||||
}
|
||||
|
||||
/// Returns the largest TAP MTU that still fits into the negotiated QUIC
|
||||
/// datagram budget, after subtracting overlay and Ethernet framing overhead
|
||||
/// plus `safety_margin`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`MtuError::DatagramTooSmall`] if the budget cannot even carry
|
||||
/// [`MIN_USEFUL_TAP_MTU`] bytes of payload on top of that overhead.
|
||||
pub fn max_tap_mtu_for_datagram(
|
||||
quic_max_datagram_size: usize,
|
||||
safety_margin: usize,
|
||||
@@ -34,6 +42,13 @@ pub fn max_tap_mtu_for_datagram(
|
||||
Ok(quic_max_datagram_size - overhead)
|
||||
}
|
||||
|
||||
/// Returns the TAP MTU to configure: [`DEFAULT_TAP_MTU`], clamped down when the
|
||||
/// negotiated QUIC datagram budget cannot carry it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`MtuError::DatagramTooSmall`] if the budget is too small to be
|
||||
/// useful at all; see [`max_tap_mtu_for_datagram`].
|
||||
pub fn recommended_tap_mtu(quic_max_datagram_size: usize) -> Result<usize, MtuError> {
|
||||
let max = max_tap_mtu_for_datagram(quic_max_datagram_size, DEFAULT_DATAGRAM_SAFETY_MARGIN)?;
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ impl FrameType {
|
||||
self as u8
|
||||
}
|
||||
|
||||
/// Decodes the on-wire frame type discriminant.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::UnknownFrameType`] for a discriminant this
|
||||
/// protocol version does not define.
|
||||
pub const fn from_u8(value: u8) -> Result<Self, ProtoError> {
|
||||
match value {
|
||||
1 => Ok(Self::Ethernet),
|
||||
@@ -39,6 +45,13 @@ pub struct OverlayHeader {
|
||||
}
|
||||
|
||||
impl OverlayHeader {
|
||||
/// Builds a header for a payload of `payload_len` bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::UnsupportedFlags`] if `flags` sets any bit that is
|
||||
/// still reserved, or [`ProtoError::PayloadTooLarge`] if `payload_len` does
|
||||
/// not fit into the 16-bit wire field.
|
||||
pub fn new(
|
||||
frame_type: FrameType,
|
||||
room_id: u64,
|
||||
@@ -99,35 +112,49 @@ impl OverlayHeader {
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Decodes the fixed-size header prefix of a received datagram.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::DatagramTooShort`] if `bytes` is shorter than
|
||||
/// [`OVERLAY_HEADER_LEN`], [`ProtoError::BadMagic`] or
|
||||
/// [`ProtoError::UnsupportedVersion`] if the datagram is not a datagram of
|
||||
/// this protocol version, [`ProtoError::UnsupportedFlags`] if a reserved
|
||||
/// flag bit is set, or [`ProtoError::UnknownFrameType`] for an unknown
|
||||
/// frame type.
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, ProtoError> {
|
||||
if bytes.len() < OVERLAY_HEADER_LEN {
|
||||
// Taking the header as a fixed-size array up front makes every field
|
||||
// read below a statically bounds-checked array index, so this function
|
||||
// has no panicking path.
|
||||
let Some(header) = bytes.first_chunk::<OVERLAY_HEADER_LEN>() else {
|
||||
return Err(ProtoError::DatagramTooShort {
|
||||
actual: bytes.len(),
|
||||
minimum: OVERLAY_HEADER_LEN,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let magic = u32::from_be_bytes(bytes[0..4].try_into().expect("header magic slice length"));
|
||||
let magic = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
|
||||
if magic != OVERLAY_MAGIC {
|
||||
return Err(ProtoError::BadMagic { actual: magic });
|
||||
}
|
||||
|
||||
let version = bytes[4];
|
||||
let version = header[4];
|
||||
if version != OVERLAY_VERSION {
|
||||
return Err(ProtoError::UnsupportedVersion { actual: version });
|
||||
}
|
||||
|
||||
let flags = u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length"));
|
||||
let flags = u16::from_be_bytes([header[18], header[19]]);
|
||||
validate_overlay_flags(flags)?;
|
||||
|
||||
Ok(Self {
|
||||
frame_type: FrameType::from_u8(bytes[5])?,
|
||||
room_id: u64::from_be_bytes(bytes[6..14].try_into().expect("room id slice length")),
|
||||
peer_id: u32::from_be_bytes(bytes[14..18].try_into().expect("peer id slice length")),
|
||||
frame_type: FrameType::from_u8(header[5])?,
|
||||
room_id: u64::from_be_bytes([
|
||||
header[6], header[7], header[8], header[9], header[10], header[11], header[12],
|
||||
header[13],
|
||||
]),
|
||||
peer_id: u32::from_be_bytes([header[14], header[15], header[16], header[17]]),
|
||||
flags,
|
||||
payload_len: u16::from_be_bytes(
|
||||
bytes[20..22].try_into().expect("payload len slice length"),
|
||||
),
|
||||
payload_len: u16::from_be_bytes([header[20], header[21]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -139,6 +166,12 @@ pub struct OverlayPacket<'a> {
|
||||
}
|
||||
|
||||
impl<'a> OverlayPacket<'a> {
|
||||
/// Pairs a decoded header with its payload.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::PayloadLengthMismatch`] if `payload` is not
|
||||
/// exactly as long as the header declares.
|
||||
pub fn new(header: OverlayHeader, payload: &'a [u8]) -> Result<Self, ProtoError> {
|
||||
let declared = usize::from(header.payload_len);
|
||||
|
||||
@@ -193,6 +226,11 @@ fn validate_overlay_flags(flags: u16) -> Result<(), ProtoError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes a complete overlay datagram: header followed by `payload`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Propagates every error of [`OverlayHeader::new`].
|
||||
pub fn encode_datagram(
|
||||
frame_type: FrameType,
|
||||
room_id: u64,
|
||||
@@ -207,6 +245,13 @@ pub fn encode_datagram(
|
||||
Ok(datagram)
|
||||
}
|
||||
|
||||
/// Checks an encoded datagram against the QUIC datagram size the peers
|
||||
/// negotiated.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::DatagramExceedsBudget`] if `datagram_len` exceeds
|
||||
/// `max_datagram_size`.
|
||||
pub fn validate_datagram_budget(
|
||||
datagram_len: usize,
|
||||
max_datagram_size: usize,
|
||||
@@ -221,6 +266,12 @@ pub fn validate_datagram_budget(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decodes a received overlay datagram, borrowing its payload from `bytes`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Propagates every error of [`OverlayHeader::decode`] and
|
||||
/// [`OverlayPacket::new`].
|
||||
pub fn decode_datagram(bytes: &[u8]) -> Result<OverlayPacket<'_>, ProtoError> {
|
||||
let header = OverlayHeader::decode(bytes)?;
|
||||
let payload = &bytes[OVERLAY_HEADER_LEN..];
|
||||
|
||||
@@ -14,7 +14,6 @@ const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
|
||||
const IPV6_NEXT_HEADER_ROUTING: u8 = 43;
|
||||
const IPV6_NEXT_HEADER_FRAGMENT: u8 = 44;
|
||||
const IPV6_NEXT_HEADER_AH: u8 = 51;
|
||||
const IPV6_NEXT_HEADER_NO_NEXT: u8 = 59;
|
||||
const IPV6_NEXT_HEADER_DESTINATION_OPTIONS: u8 = 60;
|
||||
const IPV6_NEXT_HEADER_ICMPV6: u8 = 58;
|
||||
const DHCPV4_SERVER_PORT: u16 = 67;
|
||||
@@ -34,6 +33,9 @@ pub enum EthernetSafetyDrop {
|
||||
Ipv6Fragment,
|
||||
}
|
||||
|
||||
/// Returns why a frame coming from the physical LAN must not be forwarded into
|
||||
/// the tunnel, or `None` if it may be forwarded.
|
||||
#[must_use]
|
||||
pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
|
||||
if !frame.source().is_valid_unicast() {
|
||||
return Some(EthernetSafetyDrop::InvalidSourceMac);
|
||||
@@ -42,6 +44,12 @@ pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<Ethern
|
||||
common_safety_drop_reason(frame)
|
||||
}
|
||||
|
||||
/// Returns why a frame received from a remote tunnel client must not be
|
||||
/// injected into the physical LAN, or `None` if it may be injected.
|
||||
///
|
||||
/// This is stricter than [`gateway_lan_safety_drop_reason`]: remote peers must
|
||||
/// not be able to act as a VLAN trunk, DHCP server, or IPv6 router on the LAN.
|
||||
#[must_use]
|
||||
pub fn remote_client_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
|
||||
if let Some(drop_reason) = common_safety_drop_reason(frame) {
|
||||
return Some(drop_reason);
|
||||
@@ -93,9 +101,9 @@ fn is_vlan_tagged_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
}
|
||||
|
||||
fn is_link_local_control_destination(mac: MacAddr) -> bool {
|
||||
let [a, b, c, d, e, f] = mac.octets();
|
||||
let [prefix @ .., last] = mac.octets();
|
||||
|
||||
[a, b, c, d, e] == [0x01, 0x80, 0xc2, 0x00, 0x00] && f <= 0x0f
|
||||
prefix == [0x01, 0x80, 0xc2, 0x00, 0x00] && last <= 0x0f
|
||||
}
|
||||
|
||||
fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool {
|
||||
@@ -190,7 +198,6 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt
|
||||
loop {
|
||||
match next_header {
|
||||
next_header if next_header == expected_next_header => return Some(offset),
|
||||
IPV6_NEXT_HEADER_NO_NEXT => return None,
|
||||
IPV6_NEXT_HEADER_HOP_BY_HOP
|
||||
| IPV6_NEXT_HEADER_ROUTING
|
||||
| IPV6_NEXT_HEADER_DESTINATION_OPTIONS => {
|
||||
@@ -216,6 +223,9 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Any other next-header value is either an upper-layer protocol we
|
||||
// were not asked about or the "no next header" terminator (59), so
|
||||
// the header we are looking for is not in this packet.
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,3 +20,11 @@ tokio.workspace = true
|
||||
[dev-dependencies]
|
||||
lanparty-client-core = { path = "../lanparty-client-core" }
|
||||
lanparty-gateway = { path = "../lanparty-gateway" }
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -29,6 +29,12 @@ pub struct RelayArgs {
|
||||
}
|
||||
|
||||
impl RelayArgs {
|
||||
/// Turns the parsed arguments into a validated [`RelayConfig`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if the per-room client
|
||||
/// limit is zero.
|
||||
pub fn into_config(self) -> Result<RelayConfig, ConfigError> {
|
||||
RelayConfig::with_dev_cert_der_out(
|
||||
self.listen,
|
||||
@@ -46,10 +52,24 @@ pub struct RelayConfig {
|
||||
}
|
||||
|
||||
impl RelayConfig {
|
||||
/// Builds a relay configuration that does not export its development
|
||||
/// certificate.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if
|
||||
/// `max_clients_per_room` is zero.
|
||||
pub fn new(listen: ListenEndpoint, max_clients_per_room: usize) -> Result<Self, ConfigError> {
|
||||
Self::with_dev_cert_der_out(listen, max_clients_per_room, None)
|
||||
}
|
||||
|
||||
/// Builds a relay configuration that also writes its generated development
|
||||
/// certificate to `dev_cert_der_out`, so clients can trust it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if
|
||||
/// `max_clients_per_room` is zero.
|
||||
pub fn with_dev_cert_der_out(
|
||||
listen: ListenEndpoint,
|
||||
max_clients_per_room: usize,
|
||||
|
||||
@@ -3,24 +3,35 @@
|
||||
//! The QUIC server loop admits peers through this room registry, while the
|
||||
//! registry itself stays socket-free so the relay invariants remain directly
|
||||
//! testable.
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
mod config;
|
||||
mod server;
|
||||
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
pub use config::{ConfigError, DEFAULT_RELAY_PORT, ListenEndpoint, RelayArgs, RelayConfig};
|
||||
use lanparty_ctrl::{
|
||||
ControlError, EndpointHello, PeerInfo, Reject, RejectReason, Role, RoomCode, ServerWelcome,
|
||||
ControlError,
|
||||
EndpointHello,
|
||||
PeerInfo,
|
||||
Reject,
|
||||
RejectReason,
|
||||
Role,
|
||||
RoomCode,
|
||||
ServerWelcome,
|
||||
};
|
||||
use lanparty_obs::{DropReason, FrameAction};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, MacAddr, ethernet_frame_exceeds_tap_mtu, gateway_lan_safety_drop_reason,
|
||||
recommended_tap_mtu, remote_client_safety_drop_reason,
|
||||
EthernetFrame,
|
||||
MacAddr,
|
||||
ethernet_frame_exceeds_tap_mtu,
|
||||
gateway_lan_safety_drop_reason,
|
||||
recommended_tap_mtu,
|
||||
remote_client_safety_drop_reason,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use config::{ConfigError, DEFAULT_RELAY_PORT, ListenEndpoint, RelayArgs, RelayConfig};
|
||||
pub use server::RelayServer;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const DEFAULT_MAX_CLIENTS_PER_ROOM: usize = 16;
|
||||
const MEBIBYTE: u64 = 1024 * 1024;
|
||||
@@ -223,6 +234,13 @@ impl Default for RoomRegistry {
|
||||
}
|
||||
|
||||
impl RoomRegistry {
|
||||
/// Creates an empty registry that admits at most `max_clients_per_room`
|
||||
/// clients per room.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `max_clients_per_room` is zero, which would make every join
|
||||
/// impossible.
|
||||
#[must_use]
|
||||
pub fn new(max_clients_per_room: usize) -> Self {
|
||||
assert!(
|
||||
@@ -237,12 +255,24 @@ impl RoomRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&mut self, hello: EndpointHello) -> Result<JoinAccepted, Reject> {
|
||||
hello.validate().map_err(reject_control_error)?;
|
||||
/// Admits an endpoint to the room named in its hello, creating the room if
|
||||
/// this is its first member.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`Reject`] if the hello is invalid, its datagram budget cannot
|
||||
/// carry a useful TAP MTU, the room is full, the announced MAC is already
|
||||
/// taken, the room already has a gateway, or the room-id space is
|
||||
/// exhausted.
|
||||
pub fn join(&mut self, hello: &EndpointHello) -> Result<JoinAccepted, Reject> {
|
||||
hello
|
||||
.validate()
|
||||
.map_err(|error| reject_control_error(&error))?;
|
||||
|
||||
// recommended_tap_mtu is bounded by DEFAULT_TAP_MTU, so it always fits.
|
||||
let supported_tap_mtu = recommended_tap_mtu(usize::from(hello.max_datagram_size()))
|
||||
.map_err(|error| Reject::new(RejectReason::MtuTooSmall, error.to_string()))?
|
||||
as u16;
|
||||
.map(|mtu| u16::try_from(mtu).unwrap_or(u16::MAX))
|
||||
.map_err(|error| Reject::new(RejectReason::MtuTooSmall, error.to_string()))?;
|
||||
let room_code = hello.room().clone();
|
||||
|
||||
if !self.rooms.contains_key(&room_code) {
|
||||
@@ -253,10 +283,14 @@ impl RoomRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
self.rooms
|
||||
.get_mut(&room_code)
|
||||
.expect("room was inserted before lookup")
|
||||
.join(hello, supported_tap_mtu)
|
||||
let Some(room) = self.rooms.get_mut(&room_code) else {
|
||||
return Err(Reject::new(
|
||||
RejectReason::InternalError,
|
||||
"room disappeared between creation and lookup",
|
||||
));
|
||||
};
|
||||
|
||||
room.join(hello, supported_tap_mtu)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -269,6 +303,12 @@ impl RoomRegistry {
|
||||
self.rooms.get(room).map(Room::snapshot)
|
||||
}
|
||||
|
||||
/// Removes a peer from a room, deleting the room once it is empty.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ForwardingError::UnknownRoom`] or
|
||||
/// [`ForwardingError::UnknownPeer`] if the room or peer is not present.
|
||||
pub fn leave(&mut self, room: &RoomCode, peer_id: u32) -> Result<LeaveResult, ForwardingError> {
|
||||
let room_state = self
|
||||
.rooms
|
||||
@@ -284,6 +324,14 @@ impl RoomRegistry {
|
||||
Ok(LeaveResult::new(peer, room_removed))
|
||||
}
|
||||
|
||||
/// Decides where one Ethernet frame from `ingress_peer_id` should go, and
|
||||
/// accounts it against that peer's rate limits.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ForwardingError::UnknownRoom`] or
|
||||
/// [`ForwardingError::UnknownPeer`] if the room or the ingress peer is not
|
||||
/// present.
|
||||
pub fn forward_ethernet(
|
||||
&mut self,
|
||||
room: &RoomCode,
|
||||
@@ -319,7 +367,7 @@ impl RoomRegistry {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Room {
|
||||
room_id: u64,
|
||||
id: u64,
|
||||
next_peer_id: u32,
|
||||
max_clients: usize,
|
||||
effective_tap_mtu: Option<u16>,
|
||||
@@ -344,9 +392,9 @@ impl PeerEntry {
|
||||
}
|
||||
|
||||
impl Room {
|
||||
fn new(room_id: u64, max_clients: usize) -> Self {
|
||||
fn new(id: u64, max_clients: usize) -> Self {
|
||||
Self {
|
||||
room_id,
|
||||
id,
|
||||
next_peer_id: 1,
|
||||
max_clients,
|
||||
effective_tap_mtu: None,
|
||||
@@ -361,10 +409,10 @@ impl Room {
|
||||
|
||||
fn join(
|
||||
&mut self,
|
||||
hello: EndpointHello,
|
||||
hello: &EndpointHello,
|
||||
supported_tap_mtu: u16,
|
||||
) -> Result<JoinAccepted, Reject> {
|
||||
self.validate_role_capacity(&hello)?;
|
||||
self.validate_role_capacity(hello)?;
|
||||
let effective_tap_mtu = self.accept_effective_mtu(supported_tap_mtu)?;
|
||||
|
||||
let peer_id = self.allocate_peer_id()?;
|
||||
@@ -379,7 +427,7 @@ impl Room {
|
||||
Role::Gateway => Some(peer.peer_id()),
|
||||
Role::Client => self.gateway.as_ref().map(|gateway| gateway.info.peer_id()),
|
||||
};
|
||||
let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu)
|
||||
let welcome = ServerWelcome::new(self.id, peer_id, effective_tap_mtu)
|
||||
.map(|welcome| welcome.with_gateway_peer_id(gateway_peer_id))
|
||||
.map_err(|error| {
|
||||
Reject::new(
|
||||
@@ -482,7 +530,7 @@ impl Room {
|
||||
);
|
||||
|
||||
RoomSnapshot {
|
||||
room_id: self.room_id,
|
||||
room_id: self.id,
|
||||
effective_tap_mtu: self.effective_tap_mtu.unwrap_or_default(),
|
||||
gateway: self.gateway.as_ref().map(|gateway| gateway.info.clone()),
|
||||
clients,
|
||||
@@ -535,9 +583,8 @@ impl Room {
|
||||
})?;
|
||||
let ingress_role = ingress.role();
|
||||
let ingress_mac = ingress.mac();
|
||||
let frame = match EthernetFrame::parse(frame_bytes) {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => return Ok(ForwardingDecision::dropped(DropReason::Malformed)),
|
||||
let Ok(frame) = EthernetFrame::parse(frame_bytes) else {
|
||||
return Ok(ForwardingDecision::dropped(DropReason::Malformed));
|
||||
};
|
||||
|
||||
if !frame.source().is_valid_unicast() {
|
||||
@@ -748,7 +795,7 @@ fn client_total_bandwidth_limit() -> TokenBucket {
|
||||
)
|
||||
}
|
||||
|
||||
fn reject_control_error(error: ControlError) -> Reject {
|
||||
fn reject_control_error(error: &ControlError) -> Reject {
|
||||
let reason = match error {
|
||||
ControlError::UnsupportedVersion { .. } => RejectReason::UnsupportedVersion,
|
||||
ControlError::InvalidClientMac { .. } => RejectReason::InvalidMac,
|
||||
@@ -769,9 +816,10 @@ fn reject_control_error(error: ControlError) -> Reject {
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::*;
|
||||
use lanparty_proto::MAX_STANDARD_ETHERNET_FRAME_LEN;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn room() -> RoomCode {
|
||||
RoomCode::new("ABCD").unwrap()
|
||||
}
|
||||
@@ -907,8 +955,8 @@ mod tests {
|
||||
fn accepts_gateway_and_client_into_room() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let snapshot = registry.snapshot(&room()).unwrap();
|
||||
|
||||
assert_eq!(registry.room_count(), 1);
|
||||
@@ -931,7 +979,7 @@ mod tests {
|
||||
fn reports_missing_gateway_to_client_joining_first() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
assert!(!client.welcome().gateway_connected());
|
||||
assert_eq!(client.welcome().gateway_peer_id(), None);
|
||||
@@ -940,9 +988,9 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_second_gateway() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(gateway_hello()).unwrap();
|
||||
registry.join(&gateway_hello()).unwrap();
|
||||
|
||||
let reject = registry.join(gateway_hello()).unwrap_err();
|
||||
let reject = registry.join(&gateway_hello()).unwrap_err();
|
||||
|
||||
assert_eq!(reject.reason(), &RejectReason::GatewayAlreadyConnected);
|
||||
}
|
||||
@@ -950,9 +998,9 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_duplicate_client_mac() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let reject = registry.join(client_hello(1)).unwrap_err();
|
||||
let reject = registry.join(&client_hello(1)).unwrap_err();
|
||||
|
||||
assert_eq!(reject.reason(), &RejectReason::DuplicateMac);
|
||||
}
|
||||
@@ -960,9 +1008,9 @@ mod tests {
|
||||
#[test]
|
||||
fn enforces_client_limit() {
|
||||
let mut registry = RoomRegistry::new(1);
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let reject = registry.join(client_hello(2)).unwrap_err();
|
||||
let reject = registry.join(&client_hello(2)).unwrap_err();
|
||||
|
||||
assert_eq!(reject.reason(), &RejectReason::RoomFull);
|
||||
}
|
||||
@@ -971,10 +1019,10 @@ mod tests {
|
||||
fn keeps_room_mtu_stable_after_first_peer() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let first = EndpointHello::client(room(), mac(1), 1024).unwrap();
|
||||
registry.join(first).unwrap();
|
||||
registry.join(&first).unwrap();
|
||||
|
||||
let reject = registry
|
||||
.join(EndpointHello::gateway(room(), 900).unwrap())
|
||||
.join(&EndpointHello::gateway(room(), 900).unwrap())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(reject.reason(), &RejectReason::MtuTooSmall);
|
||||
@@ -985,9 +1033,9 @@ mod tests {
|
||||
fn second_peer_uses_existing_lower_room_mtu() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let first = EndpointHello::client(room(), mac(1), 1024).unwrap();
|
||||
registry.join(first).unwrap();
|
||||
registry.join(&first).unwrap();
|
||||
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
|
||||
assert_eq!(gateway.welcome().effective_tap_mtu(), 972);
|
||||
}
|
||||
@@ -995,8 +1043,8 @@ mod tests {
|
||||
#[test]
|
||||
fn removes_client_from_room_indexes() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
registry.join(client_hello(2)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(2)).unwrap();
|
||||
|
||||
let result = registry.leave(&room(), client.peer().peer_id()).unwrap();
|
||||
let snapshot = registry.snapshot(&room()).unwrap();
|
||||
@@ -1005,13 +1053,13 @@ mod tests {
|
||||
assert!(!result.room_removed());
|
||||
assert_eq!(snapshot.clients().len(), 1);
|
||||
assert_eq!(snapshot.last_seen(client.peer().peer_id()), None);
|
||||
assert!(registry.join(client_hello(1)).is_ok());
|
||||
assert!(registry.join(&client_hello(1)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_empty_room_after_last_peer_leaves() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let result = registry.leave(&room(), client.peer().peer_id()).unwrap();
|
||||
|
||||
@@ -1024,8 +1072,8 @@ mod tests {
|
||||
#[test]
|
||||
fn removes_gateway_without_removing_room_when_clients_remain() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let result = registry.leave(&room(), gateway.peer().peer_id()).unwrap();
|
||||
let snapshot = registry.snapshot(&room()).unwrap();
|
||||
@@ -1035,13 +1083,13 @@ mod tests {
|
||||
assert!(snapshot.gateway().is_none());
|
||||
assert_eq!(snapshot.last_seen(gateway.peer().peer_id()), None);
|
||||
assert_eq!(snapshot.clients().len(), 1);
|
||||
assert!(registry.join(gateway_hello()).is_ok());
|
||||
assert!(registry.join(&gateway_hello()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_unknown_peer_on_leave() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let error = registry.leave(&room(), 99).unwrap_err();
|
||||
|
||||
@@ -1057,9 +1105,9 @@ mod tests {
|
||||
#[test]
|
||||
fn forwards_unknown_client_unicast_to_gateway() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let frame = ethernet(MacAddr::new([0x00, 1, 2, 3, 4, 5]), mac(1));
|
||||
|
||||
let decision = registry
|
||||
@@ -1075,9 +1123,9 @@ mod tests {
|
||||
#[test]
|
||||
fn drops_gateway_unicast_to_unknown_remote_mac() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(2)).unwrap();
|
||||
let frame = ethernet(physical_mac(), MacAddr::new([0x00, 1, 2, 3, 4, 5]));
|
||||
|
||||
let decision = registry
|
||||
@@ -1090,9 +1138,9 @@ mod tests {
|
||||
#[test]
|
||||
fn forwards_gateway_unicast_to_matching_client() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let frame = ethernet(mac(2), MacAddr::new([0x00, 1, 2, 3, 4, 5]));
|
||||
|
||||
let decision = registry
|
||||
@@ -1107,9 +1155,9 @@ mod tests {
|
||||
#[test]
|
||||
fn floods_broadcast_without_reflecting_ingress() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let frame = ethernet(MacAddr::BROADCAST, mac(1));
|
||||
|
||||
let decision = registry
|
||||
@@ -1126,8 +1174,8 @@ mod tests {
|
||||
#[test]
|
||||
fn refreshes_peer_last_seen_after_valid_frames() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let client_seen_at = Instant::now() + Duration::from_secs(5);
|
||||
let gateway_seen_at = client_seen_at + Duration::from_secs(1);
|
||||
let client_frame = ethernet(MacAddr::BROADCAST, mac(1));
|
||||
@@ -1164,7 +1212,7 @@ mod tests {
|
||||
#[test]
|
||||
fn keeps_last_seen_unchanged_for_unauthorized_client_source() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let before = registry
|
||||
.snapshot(&room())
|
||||
.unwrap()
|
||||
@@ -1194,9 +1242,9 @@ mod tests {
|
||||
#[test]
|
||||
fn rate_limits_client_broadcast_after_burst() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let frame = ethernet(MacAddr::BROADCAST, mac(1));
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -1230,9 +1278,9 @@ mod tests {
|
||||
#[test]
|
||||
fn rate_limits_client_unknown_unicast_after_burst() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let unknown_unicast = ethernet(physical_mac(), mac(1));
|
||||
let known_unicast = ethernet(mac(2), mac(1));
|
||||
let now = Instant::now();
|
||||
@@ -1270,8 +1318,8 @@ mod tests {
|
||||
#[test]
|
||||
fn rate_limits_client_total_bandwidth_after_burst() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client_one = registry.join(client_hello(1)).unwrap();
|
||||
let client_two = registry.join(client_hello(2)).unwrap();
|
||||
let client_one = registry.join(&client_hello(1)).unwrap();
|
||||
let client_two = registry.join(&client_hello(2)).unwrap();
|
||||
let payload = vec![0; usize::from(client_one.welcome().effective_tap_mtu())];
|
||||
let frame = ethernet_with_payload(mac(2), mac(1), ETHERTYPE_IPV4, &payload);
|
||||
let frame_len = frame.len() as u64;
|
||||
@@ -1318,7 +1366,7 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_client_frames_with_forged_source_mac() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let frame = ethernet(MacAddr::BROADCAST, mac(2));
|
||||
|
||||
let decision = registry
|
||||
@@ -1331,8 +1379,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_invalid_source_macs_from_clients_and_gateway() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let client_frame = ethernet(MacAddr::BROADCAST, MacAddr::BROADCAST);
|
||||
let gateway_frame = ethernet(mac(1), MacAddr::ZERO);
|
||||
|
||||
@@ -1350,8 +1398,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_jumbo_frames() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let mut frame = ethernet(MacAddr::BROADCAST, mac(1));
|
||||
frame.resize(MAX_STANDARD_ETHERNET_FRAME_LEN + 1, 0);
|
||||
|
||||
@@ -1365,8 +1413,8 @@ mod tests {
|
||||
#[test]
|
||||
fn drops_frames_above_effective_tap_mtu() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let oversized_payload = vec![0; usize::from(client.welcome().effective_tap_mtu()) + 1];
|
||||
let client_frame = ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
@@ -1391,8 +1439,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_l2_control_plane_frames_from_clients_and_gateway() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let stp_destination = MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]);
|
||||
let client_frame = ethernet_with_payload(stp_destination, mac(1), 0x0026, &[]);
|
||||
let gateway_frame =
|
||||
@@ -1412,8 +1460,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_vlan_tagged_frames_but_allows_lan_tags() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let payload = [0, 42, 0x08, 0x00, 1, 2, 3, 4];
|
||||
let client_frame =
|
||||
ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_8021Q, &payload);
|
||||
@@ -1439,8 +1487,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_dhcp_server_replies_but_allows_lan_replies() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let payload = ipv4_udp_payload(DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT);
|
||||
let client_frame =
|
||||
ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_IPV4, &payload);
|
||||
@@ -1462,8 +1510,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_dhcpv6_server_replies_but_allows_lan_replies() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 1, 0, 2]);
|
||||
let payload =
|
||||
ipv6_udp_after_destination_options_payload(DHCPV6_SERVER_PORT, DHCPV6_CLIENT_PORT);
|
||||
@@ -1486,8 +1534,8 @@ mod tests {
|
||||
#[test]
|
||||
fn allows_remote_dhcpv4_client_requests() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let payload = ipv4_udp_payload(DHCPV4_CLIENT_PORT, DHCPV4_SERVER_PORT);
|
||||
let frame = ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_IPV4, &payload);
|
||||
|
||||
@@ -1502,8 +1550,8 @@ mod tests {
|
||||
#[test]
|
||||
fn allows_remote_dhcpv6_client_requests() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 1, 0, 2]);
|
||||
let payload = ipv6_udp_payload(DHCPV6_CLIENT_PORT, DHCPV6_SERVER_PORT);
|
||||
let frame = ethernet_with_payload(destination, mac(1), ETHERTYPE_IPV6, &payload);
|
||||
@@ -1519,8 +1567,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_ipv6_fragments_but_allows_lan_fragments() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]);
|
||||
let payload = ipv6_payload(
|
||||
IPV6_NEXT_HEADER_FRAGMENT,
|
||||
@@ -1545,8 +1593,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_ipv6_router_advertisements() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]);
|
||||
let frame = ethernet_with_payload(
|
||||
destination,
|
||||
@@ -1565,8 +1613,8 @@ mod tests {
|
||||
#[test]
|
||||
fn filters_remote_ipv6_router_advertisements_after_extension_headers() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]);
|
||||
let frame = ethernet_with_payload(
|
||||
destination,
|
||||
@@ -1585,8 +1633,8 @@ mod tests {
|
||||
#[test]
|
||||
fn allows_remote_icmpv6_that_is_not_router_advertisement() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let gateway = registry.join(&gateway_hello()).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]);
|
||||
let frame = ethernet_with_payload(
|
||||
destination,
|
||||
@@ -1606,7 +1654,7 @@ mod tests {
|
||||
#[test]
|
||||
fn drops_malformed_frames() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let client = registry.join(&client_hello(1)).unwrap();
|
||||
|
||||
let decision = registry
|
||||
.forward_ethernet(&room(), client.peer().peer_id(), &[0; 4])
|
||||
@@ -1619,7 +1667,7 @@ mod tests {
|
||||
#[test]
|
||||
fn reports_unknown_ingress_peer() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
registry.join(client_hello(1)).unwrap();
|
||||
registry.join(&client_hello(1)).unwrap();
|
||||
let frame = ethernet(MacAddr::BROADCAST, mac(1));
|
||||
|
||||
let error = registry.forward_ethernet(&room(), 99, &frame).unwrap_err();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
use clap::Parser;
|
||||
use lanparty_relay::{RelayArgs, RelayConfig, RelayServer};
|
||||
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
use std::{fs, net::SocketAddr, path::Path, sync::Arc};
|
||||
use std::{collections::HashMap, fs, net::SocketAddr, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use bytes::Bytes;
|
||||
use lanparty_ctrl::{
|
||||
CONTROL_LENGTH_PREFIX_LEN, ControlCodecError, ControlMessage, DisconnectReason, EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN, PeerInfo, RELAY_ALPN, Reject, RejectReason, Role, RoomCode,
|
||||
ServerWelcome, decode_control_frame, encode_control_message,
|
||||
CONTROL_LENGTH_PREFIX_LEN,
|
||||
ControlCodecError,
|
||||
ControlMessage,
|
||||
DisconnectReason,
|
||||
EndpointHello,
|
||||
MAX_CONTROL_MESSAGE_LEN,
|
||||
PeerInfo,
|
||||
RELAY_ALPN,
|
||||
Reject,
|
||||
RejectReason,
|
||||
Role,
|
||||
RoomCode,
|
||||
ServerWelcome,
|
||||
decode_control_frame,
|
||||
encode_control_message,
|
||||
};
|
||||
use lanparty_obs::{DropReason, FrameDirection, FrameLog, TunnelStats};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, FrameType, OVERLAY_FLAGS_NONE, decode_datagram, encode_datagram,
|
||||
EthernetFrame,
|
||||
FrameType,
|
||||
OVERLAY_FLAGS_NONE,
|
||||
decode_datagram,
|
||||
encode_datagram,
|
||||
};
|
||||
use quinn::{
|
||||
Endpoint,
|
||||
Incoming,
|
||||
RecvStream,
|
||||
SendStream,
|
||||
ServerConfig,
|
||||
TransportConfig,
|
||||
crypto::rustls::QuicServerConfig,
|
||||
};
|
||||
use quinn::crypto::rustls::QuicServerConfig;
|
||||
use quinn::{Endpoint, Incoming, RecvStream, SendStream, ServerConfig, TransportConfig};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{ForwardingDecision, RelayConfig, RoomRegistry};
|
||||
@@ -144,6 +166,13 @@ impl MalformedDatagramTracker {
|
||||
}
|
||||
|
||||
impl RelayServer {
|
||||
/// Binds the relay's QUIC endpoint and generates its development
|
||||
/// certificate.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the certificate cannot be generated or written, or
|
||||
/// the listen address cannot be bound.
|
||||
pub fn bind(config: &RelayConfig) -> Result<Self> {
|
||||
let (server_config, certificate) = development_server_config_with_certificate()?;
|
||||
if let Some(path) = config.dev_cert_der_out() {
|
||||
@@ -163,12 +192,24 @@ impl RelayServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the address the relay is actually listening on, which resolves
|
||||
/// port 0 to the port the OS picked.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the endpoint address cannot be read.
|
||||
pub fn local_addr(&self) -> Result<SocketAddr> {
|
||||
self.endpoint
|
||||
.local_addr()
|
||||
.context("failed to read relay local address")
|
||||
}
|
||||
|
||||
/// Accepts connections and serves rooms until the process is asked to shut
|
||||
/// down.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if waiting for the shutdown signal fails.
|
||||
pub async fn run_until_shutdown(self) -> Result<()> {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let rooms = Arc::clone(&self.rooms);
|
||||
@@ -629,20 +670,16 @@ fn relay_frame_log_line(
|
||||
};
|
||||
let source_mac = log
|
||||
.source_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let destination_mac = log
|
||||
.destination_mac()
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
|
||||
let ethertype_or_len = log
|
||||
.ethertype_or_len()
|
||||
.map(|value| format!("0x{value:04x}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}"));
|
||||
let drop_reason = log
|
||||
.drop_reason()
|
||||
.map(|reason| format!("{reason:?}"))
|
||||
.unwrap_or_else(|| "-".to_owned());
|
||||
.map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}"));
|
||||
|
||||
format!(
|
||||
"relay frame room={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={} targets={}",
|
||||
@@ -700,8 +737,7 @@ fn egress_budget_skip_log_line(
|
||||
max_datagram_size: usize,
|
||||
) -> String {
|
||||
format!(
|
||||
"relay egress skipped room={} peer_id={} target_peer_id={} len={} max_datagram_size={} reason=datagram_budget",
|
||||
room, ingress_peer_id, target_peer_id, datagram_len, max_datagram_size
|
||||
"relay egress skipped room={room} peer_id={ingress_peer_id} target_peer_id={target_peer_id} len={datagram_len} max_datagram_size={max_datagram_size} reason=datagram_budget"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -863,12 +899,12 @@ async fn build_handshake_response(
|
||||
};
|
||||
|
||||
let room = hello.room().clone();
|
||||
let hello = match limit_hello_to_connection(hello, connection_max_datagram_size) {
|
||||
let hello = match limit_hello_to_connection(&hello, connection_max_datagram_size) {
|
||||
Ok(hello) => hello,
|
||||
Err(reject) => return (None, ControlMessage::Reject(reject)),
|
||||
};
|
||||
let peer_max_datagram_size = usize::from(hello.max_datagram_size());
|
||||
let join = rooms.lock().await.join(hello);
|
||||
let join = rooms.lock().await.join(&hello);
|
||||
|
||||
match join {
|
||||
Ok(join) => {
|
||||
@@ -890,12 +926,12 @@ async fn build_handshake_response(
|
||||
}
|
||||
|
||||
fn limit_hello_to_connection(
|
||||
hello: EndpointHello,
|
||||
hello: &EndpointHello,
|
||||
connection_max_datagram_size: usize,
|
||||
) -> Result<EndpointHello, Reject> {
|
||||
let max_datagram_size = usize::from(hello.max_datagram_size())
|
||||
.min(connection_max_datagram_size)
|
||||
.min(usize::from(u16::MAX)) as u16;
|
||||
let max_datagram_size = u16::try_from(connection_max_datagram_size)
|
||||
.unwrap_or(u16::MAX)
|
||||
.min(hello.max_datagram_size());
|
||||
|
||||
match hello.role() {
|
||||
Role::Client => EndpointHello::client(
|
||||
@@ -907,7 +943,7 @@ fn limit_hello_to_connection(
|
||||
),
|
||||
Role::Gateway => EndpointHello::gateway(hello.room().clone(), max_datagram_size),
|
||||
}
|
||||
.map_err(crate::reject_control_error)
|
||||
.map_err(|error| crate::reject_control_error(&error))
|
||||
}
|
||||
|
||||
fn reject(reject: Reject) -> (Option<AcceptedPeer>, ControlMessage) {
|
||||
@@ -916,7 +952,7 @@ fn reject(reject: Reject) -> (Option<AcceptedPeer>, ControlMessage) {
|
||||
|
||||
fn reject_codec_error(error: ControlCodecError) -> Reject {
|
||||
match error {
|
||||
ControlCodecError::InvalidMessage(error) => crate::reject_control_error(error),
|
||||
ControlCodecError::InvalidMessage(error) => crate::reject_control_error(&error),
|
||||
ControlCodecError::FrameTooShort { .. }
|
||||
| ControlCodecError::MessageTooLarge { .. }
|
||||
| ControlCodecError::IncompletePayload { .. }
|
||||
@@ -1004,6 +1040,10 @@ fn development_server_config_with_certificate() -> Result<(ServerConfig, Certifi
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// The end-to-end tests script a whole client/gateway session pair; splitting
|
||||
// them into helpers would scatter one readable scenario across the module.
|
||||
#![allow(clippy::too_many_lines)]
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
@@ -1014,13 +1054,17 @@ mod tests {
|
||||
use lanparty_ctrl::{RoomCode, decode_control_frame, encode_control_message};
|
||||
use lanparty_gateway::{GatewayConfig, connect_gateway};
|
||||
use lanparty_proto::{
|
||||
ETHERNET_HEADER_LEN, ETHERTYPE_IPV4, FrameType, MacAddr, decode_datagram, encode_datagram,
|
||||
ETHERNET_HEADER_LEN,
|
||||
ETHERTYPE_IPV4,
|
||||
FrameType,
|
||||
MacAddr,
|
||||
decode_datagram,
|
||||
encode_datagram,
|
||||
};
|
||||
use quinn::{ClientConfig, crypto::rustls::QuicClientConfig};
|
||||
|
||||
use crate::{DEFAULT_MAX_CLIENTS_PER_ROOM, ListenEndpoint};
|
||||
|
||||
use super::*;
|
||||
use crate::{DEFAULT_MAX_CLIENTS_PER_ROOM, ListenEndpoint};
|
||||
|
||||
const ETHERTYPE_ARP: u16 = 0x0806;
|
||||
const ARP_REQUEST: u16 = 1;
|
||||
@@ -1856,8 +1900,8 @@ mod tests {
|
||||
let discover = udp_ipv4_frame(
|
||||
MacAddr::BROADCAST,
|
||||
client_mac,
|
||||
Ipv4Addr::new(0, 0, 0, 0),
|
||||
Ipv4Addr::new(255, 255, 255, 255),
|
||||
Ipv4Addr::UNSPECIFIED,
|
||||
Ipv4Addr::BROADCAST,
|
||||
DHCPV4_CLIENT_PORT,
|
||||
DHCPV4_SERVER_PORT,
|
||||
&discover_payload,
|
||||
@@ -1887,7 +1931,7 @@ mod tests {
|
||||
MacAddr::BROADCAST,
|
||||
dhcp_server_mac,
|
||||
dhcp_server_ip,
|
||||
Ipv4Addr::new(255, 255, 255, 255),
|
||||
Ipv4Addr::BROADCAST,
|
||||
DHCPV4_SERVER_PORT,
|
||||
DHCPV4_CLIENT_PORT,
|
||||
&offer_payload,
|
||||
@@ -2562,7 +2606,7 @@ mod tests {
|
||||
mac: MacAddr,
|
||||
) -> AcceptedPeer {
|
||||
let hello = EndpointHello::client(RoomCode::new("TESTROOM").unwrap(), mac, 1400).unwrap();
|
||||
let join = rooms.lock().await.join(hello).unwrap();
|
||||
let join = rooms.lock().await.join(&hello).unwrap();
|
||||
|
||||
AcceptedPeer {
|
||||
room: RoomCode::new("TESTROOM").unwrap(),
|
||||
@@ -2575,7 +2619,7 @@ mod tests {
|
||||
|
||||
async fn accepted_gateway_for_forwarding(rooms: &Arc<Mutex<RoomRegistry>>) -> AcceptedPeer {
|
||||
let hello = EndpointHello::gateway(RoomCode::new("TESTROOM").unwrap(), 1400).unwrap();
|
||||
let join = rooms.lock().await.join(hello).unwrap();
|
||||
let join = rooms.lock().await.join(&hello).unwrap();
|
||||
|
||||
AcceptedPeer {
|
||||
room: RoomCode::new("TESTROOM").unwrap(),
|
||||
@@ -2740,7 +2784,7 @@ mod tests {
|
||||
sum = (sum & 0xffff) + (sum >> 16);
|
||||
}
|
||||
|
||||
!(sum as u16)
|
||||
!u16::try_from(sum).unwrap()
|
||||
}
|
||||
|
||||
fn ethernet_frame_with_payload(
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
set positional-arguments
|
||||
|
||||
run *args:
|
||||
cargo run -- "$@"
|
||||
|
||||
build:
|
||||
cargo build
|
||||
|
||||
build-release:
|
||||
cargo build --release
|
||||
|
||||
build-production:
|
||||
cargo build --profile production
|
||||
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
tombi format
|
||||
fd -tf -e md -x prettier --write --prose-wrap always --print-width 80
|
||||
rumdl check --flavor commonmark --fix
|
||||
just --fmt
|
||||
|
||||
_fix:
|
||||
cargo fix --workspace --all-targets --all-features
|
||||
cargo clippy --fix --workspace --all-targets --all-features
|
||||
|
||||
fix: _fix fmt
|
||||
|
||||
clippy:
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
test:
|
||||
cargo test --workspace --all-targets --all-features
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
@@ -0,0 +1,3 @@
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
imports_layout = "HorizontalVertical"
|
||||
Reference in New Issue
Block a user