finalise docs: accurate header size, PING/PONG, build instructions, repo layout

README:
- Fix header size (8 bytes, not 6) and payload_len field
- Mark client as built (not 'planned')
- Add PING/PONG to protocol summary
- Add client usage section (Discover, mirror ports, Test button, tray)
- Add building from source section (Rust + .NET)
- Add Npcap prerequisite explanation
- Add Windows privilege/UAC note
- Add full repository layout tree

PROTOCOL:
- Fix MANIFEST payload description in frame types table (hostname + entries)
- Add PING/PONG flow diagram and semantics section
This commit is contained in:
2026-08-13 08:40:02 +00:00
parent 094d080a95
commit 5bbda4c4c4
2 changed files with 138 additions and 18 deletions
+114 -17
View File
@@ -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:<port>`.
- `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:<mirror_port>`.
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 <https://npcap.com>).
### 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