diff --git a/PROTOCOL.md b/PROTOCOL.md index 138fcab..eb9472b 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -40,7 +40,7 @@ emitted in v1. | Type | Name | Direction | session_id | Payload | |------|-------------|---------------|------------|----------------------------------| | 0x01 | DISCOVER | C → broadcast | 0 | empty | -| 0x02 | MANIFEST | S → C | 0 | `id:1, proto:1, port:2` × N | +| 0x02 | MANIFEST | S → C | 0 | `hostname_len:1, hostname:N, entries...` | | 0x03 | OPEN | C → S | 0 | `upstream_id:1` | | 0x04 | OPEN_ACK | S → C | assigned | `upstream_id:1` | | 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` | @@ -212,3 +212,26 @@ client server it emits `CLOSE` and exits. - The server's tunnel→socket path writes `DATA` payloads to the `TcpStream` with `write_all`. On error it emits `CLOSE` and drops the session. + +## Network test (PING/PONG) + +``` +client server + | | + | PING { nonce } | + |-------------------------------->| + | | + | PONG { nonce } | + |<--------------------------------| + | | + | (repeated at random intervals) | + | | +``` + +- The client sends `PING` frames at random 10–100 ms intervals, each with a + unique `nonce`. +- The server echoes the nonce verbatim in a `PONG` frame. +- The client correlates `PONG` nonces with outstanding `PING` timestamps to + compute RTT, average latency, jitter (mean absolute delta of consecutive + RTTs), and drop rate (unanswered PINGs). +- PING/PONG frames use `session_id = 0`; they are independent of TCP sessions. diff --git a/README.md b/README.md index bdf1c7a..9ed1df3 100644 --- a/README.md +++ b/README.md @@ -16,32 +16,35 @@ pass unimpeded. See `wireguard-windows/tunnel/firewall/blocker.go:156`. ## Components -- `gatunad` — Rust server. Runs on the peer (Linux) that owns the real - services. Announces upstreams and relays TCP between the tunnel and +- **`gatunad`** — Rust server (`gatunad/`). Runs on the peer (Linux) that owns + the real services. Announces upstreams and relays TCP between the tunnel and `127.0.0.1:`. -- `gatuna` (planned) — .NET WinForms client. Runs on the killswitched Windows - box. Discovers the server, presents its upstreams as local loopback - listeners, and hauls bytes over the same L2 protocol. - -This repository builds `gatunad` first. +- **`gatuna-client`** — .NET 8 WinForms client (`win/gatuna-client/`). Runs on + the killswitched Windows box. Discovers the server, presents its upstreams as + local loopback listeners, and hauls bytes over the same L2 protocol. Includes + a network test (PING/PONG) for measuring latency, jitter, and loss. ## Transport - **Medium:** raw Ethernet frames on a shared L2 segment. - **Ethertype:** `0x6969` (hardcoded). -- **No IP stack involvement.** Frames carry only our 6-byte header + payload. -- **BPF:** the server attaches a classic BPF filter `ether proto 0x6969` to its - `AF_PACKET` socket so it only wakes on our ethertype. No eBPF authoring. +- **No IP stack involvement.** Frames carry only our 8-byte header + payload. +- **BPF:** both sides filter on ethertype — the server via classic BPF on + `AF_PACKET` (`SO_ATTACH_FILTER`), the client via Npcap's compiled filter. + No eBPF authoring. ## Protocol See [`PROTOCOL.md`](PROTOCOL.md) for the full wire format. Summary: -- 6-byte header, big-endian: `[ version:1 ][ type:1 ][ session_id:4 ][ payload:N ]`. -- `version` = `1`. No length field (frame length comes from the capture). No - CRC. Ethertype discriminates our frames from everything else. -- Discovery: client broadcasts `DISCOVER`; server unicasts `MANIFEST` back. +- 8-byte header, big-endian: + `[ version:1 ][ type:1 ][ session_id:4 ][ payload_len:2 ][ payload:N ]`. +- `version` = `1`. `payload_len` lets the receiver ignore Ethernet padding + (frames under 60 bytes are zero-padded by the NIC). +- Discovery: client broadcasts `DISCOVER`; server unicasts `MANIFEST` (with + hostname and upstream list) back. - Sessions: `OPEN` → `OPEN_ACK` (or `OPEN_NAK`) → `DATA`* ↔ `DATA`* → `CLOSE`. +- Network test: `PING` (with 8-byte nonce) → `PONG` (nonce echoed). - v1 ships TCP only. UDP frame types are reserved but unimplemented. ## `gatunad` usage @@ -62,17 +65,82 @@ sudo ./gatunad eth0 22 8800:site-a 8080:site-b Exposes upstreams `1→127.0.0.1:22`, `2→127.0.0.1:8800` (label `site-a`), `3→127.0.0.1:8080` (label `site-b`). +## `gatuna-client` usage + +1. Launch `gatuna-client.exe` (UAC prompt expected — Npcap requires admin). +2. Select the network adapter connected to the same L2 segment as the server. +3. Click **Discover**. The server's hostname and MAC appear; the upstream list + populates. +4. Check the upstreams you want to use. The **Mirror** column shows the local + loopback port to connect to. +5. Connect your app to `127.0.0.1:`. +6. Click **Test** to run a PING/PONG network test (latency, jitter, loss). +7. Minimize to tray; close to exit. + +### Mirror ports + +Mirror ports are deterministic: `port ^ (mac[0]<<8 | mac[5])`, clamped to +≥1024. The same server + upstream always yields the same local port, so you +know where to connect without checking the UI. If the computed port is already +in use, the client falls back to an OS-assigned port. + +## Building from source + +### Prerequisites + +- **Server:** Rust toolchain (edition 2021, MSRV 1.70+), Linux with `AF_PACKET` + support. +- **Client:** .NET 8 SDK with Windows Desktop workload, Npcap installed + (bundled with Wireshark or standalone from ). + +### Build the server (Linux) + +```sh +cd gatunad +cargo build --release +``` + +The binary is at `gatunad/target/release/gatunad`. + +### Build the client (Windows) + +```powershell +cd win\gatuna-client +dotnet build -c Release +``` + +Or run directly: +```powershell +dotnet run --project win\gatuna-client -c Release +``` + +### Npcap + +The client requires Npcap's `wpcap.dll` and `Packet.dll` in the system path. +These are installed by the Npcap installer (also bundled with Wireshark). No +additional NuGet packages or driver installs are needed — SharpPcap talks to +the existing Npcap installation. + ## Privileges -`AF_PACKET` requires `CAP_NET_RAW`. Run as root, or grant the binary the -capability once: +**Server (Linux):** `AF_PACKET` requires `CAP_NET_RAW`. Run as root, or grant +the binary the capability once: ``` sudo setcap cap_net_raw+ep ./target/release/gatunad ``` +**Client (Windows):** Npcap requires admin by default (`AdminOnly=1` in +`HKLM\SYSTEM\CurrentControlSet\Services\npcap\Parameters`). The client's +`app.manifest` requests `requireAdministrator`, so a UAC prompt appears on +launch. See the [Npcap docs](https://npcap.com/guide/npcap-dev-guide-1.html) +for details on the `AdminOnly` flag if you want to change this. + ## Logging -Errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is silent. +**Server:** errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is +silent. + +**Client:** status line in the UI. Errors are not logged to disk. ## v1 limitations @@ -82,6 +150,35 @@ Errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is silent. on a healthy switched link. - No auth/crypto. Anyone on the same L2 segment can `DISCOVER` and `OPEN`. - Single server instance per interface. +- One outstanding `OPEN` at a time on the client (serialized via queue). + +## Repository layout + +``` +gatuna/ +├── README.md +├── PROTOCOL.md +├── LICENSE # CC0 1.0 Universal +├── .gitignore +├── gatunad/ # Rust server +│ ├── Cargo.toml +│ └── src/ +│ ├── main.rs # cmdline, rx dispatch, tx task +│ ├── link.rs # AF_PACKET, classic BPF, raw Ethernet I/O +│ ├── frame.rs # wire protocol encode/decode +│ ├── upstream.rs # cmdline parsing, hostname, upstream table +│ └── session.rs # session store, socket→tunnel pump +└── win/ + └── gatuna-client/ # .NET 8 WinForms client + ├── gatuna-client.csproj + ├── app.manifest # requireAdministrator + ├── Program.cs # tray icon, entry point + ├── MainForm.cs # UI: adapter picker, discover, test, upstream list + ├── TunnelLink.cs # SharpPcap wrapper, raw Ethernet send/recv + ├── Frame.cs # wire protocol encode/decode (mirrors Rust frame.rs) + ├── SessionManager.cs # session lifecycle, listeners, OPEN serialization + └── PingTest.cs # PING/PONG network test with stats +``` ## License