eb8994d1e1
DATA frames now carry seq:4 and ack_seq:4 in a 16-byte extended header. Both sides maintain per-session send/recv state: Sender: - Monotonic seq counter, retransmit buffer (seq -> frame bytes) - Retransmit timer: 5ms timeout, 10 max retries -> CLOSE - Window advances on cumulative ACK Receiver: - In-order delivery to TCP socket (expected_seq) - Out-of-order buffering (SortedList by seq) - Duplicate detection (seq < expected -> discard + re-ACK) - Pure ACK frames (empty-payload DATA) for duplicate/OOO responses This prevents lost Ethernet frames from permanently corrupting TCP sessions, which was the key v1 limitation. The local kernel TCP stack ACKs data before we chunk it into DATA frames; without L2 reliability a dropped frame creates an unrecoverable gap. Version bumped to 2. Both sides must speak v2; no negotiation. Updated: PROTOCOL.md (full v2 spec), README.md, Rust frame.rs/ session.rs/main.rs, C# Frame.cs/SessionManager.cs/TunnelLink.cs.
187 lines
6.9 KiB
Markdown
187 lines
6.9 KiB
Markdown
# gatuna
|
||
|
||
A raw-Ethernet tunnel for reaching services on a peer machine when a WFP
|
||
killswitch on the local machine blocks normal IP traffic. Two programs carry
|
||
bytes between their respective loopbacks over a private L2 protocol, bypassing
|
||
the IP stack (and therefore the killswitch) entirely.
|
||
|
||
## Why this works
|
||
|
||
WireGuard-for-Windows installs its killswitch as WFP filters at the ALE layers
|
||
only (`ALE_AUTH_CONNECT_V4/V6`, `ALE_AUTH_RECV_ACCEPT_V4/V6`). It never installs
|
||
MAC-layer callouts — the L2 code in `tunnel/firewall/` is commented out. Raw
|
||
Ethernet frames sent via a packet driver (Npcap on Windows, `AF_PACKET` on
|
||
Linux) never enter the IP stack, so they never reach the ALE classify path and
|
||
pass unimpeded. See `wireguard-windows/tunnel/firewall/blocker.go:156`.
|
||
|
||
## Components
|
||
|
||
- **`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`** — .NET 8 WinForms client (`gatuna-win/`). 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 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:
|
||
|
||
- 8-byte header, big-endian:
|
||
`[ version:1 ][ type:1 ][ session_id:4 ][ payload_len:2 ][ payload:N ]`.
|
||
- `version` = `2`. `payload_len` lets the receiver ignore Ethernet padding
|
||
(frames under 60 bytes are zero-padded by the NIC).
|
||
- DATA frames carry an extended 16-byte header with `seq` and `ack_seq` fields
|
||
for L2-level reliability (retransmit + in-order delivery), preventing lost
|
||
Ethernet frames from corrupting TCP sessions.
|
||
- 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).
|
||
- TCP only. UDP frame types are reserved but unimplemented.
|
||
|
||
## `gatunad` usage
|
||
|
||
```
|
||
gatunad IFACE PORT [PORT...]
|
||
```
|
||
|
||
- `IFACE` — L2 interface name (e.g. `eth0`).
|
||
- `PORT` — `int[:label]`; a TCP upstream relayed to `127.0.0.1:int`. The label
|
||
is optional, max 255 bytes, carried in the MANIFEST for the client to display.
|
||
At least one required. Upstream ID = 1-based positional index in cmdline order.
|
||
|
||
Example:
|
||
```
|
||
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` usage
|
||
|
||
1. Launch `gatuna.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 gatuna-win
|
||
dotnet build -c Release
|
||
```
|
||
|
||
Or run directly:
|
||
```powershell
|
||
dotnet run --project gatuna-win -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
|
||
|
||
**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
|
||
|
||
**Server:** errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is
|
||
silent.
|
||
|
||
**Client:** status line in the UI. Errors are not logged to disk.
|
||
|
||
## Limitations
|
||
|
||
- TCP only. UDP wire types reserved, code paths stubbed.
|
||
- 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).
|
||
- L2 retransmit caps at 10 retries × 5 ms = 50 ms. A permanently dead link
|
||
closes the session with `reason = max_retries`.
|
||
|
||
## 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
|
||
└── gatuna-win/ # .NET 8 WinForms client
|
||
├── gatuna.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
|
||
|
||
CC0 1.0 Universal. See [`LICENSE`](LICENSE).
|