2026-08-12 14:32:34 +00:00
|
|
|
|
# gatuna
|
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- **`gatunad`** — Rust server (`gatunad/`). Runs on the peer (Linux) that owns
|
|
|
|
|
|
the real services. Announces upstreams and relays TCP between the tunnel and
|
2026-08-12 18:20:51 +00:00
|
|
|
|
`127.0.0.1:<port>`.
|
2026-08-13 08:43:11 +00:00
|
|
|
|
- **`gatuna`** — .NET 8 WinForms client (`gatuna-win/`). Runs on the
|
|
|
|
|
|
killswitched Windows box. Discovers the server, presents its upstreams as
|
2026-08-13 08:40:02 +00:00
|
|
|
|
local loopback listeners, and hauls bytes over the same L2 protocol. Includes
|
|
|
|
|
|
a network test (PING/PONG) for measuring latency, jitter, and loss.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## Transport
|
|
|
|
|
|
|
|
|
|
|
|
- **Medium:** raw Ethernet frames on a shared L2 segment.
|
|
|
|
|
|
- **Ethertype:** `0x6969` (hardcoded).
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- **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.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## Protocol
|
|
|
|
|
|
|
|
|
|
|
|
See [`PROTOCOL.md`](PROTOCOL.md) for the full wire format. Summary:
|
|
|
|
|
|
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- 8-byte header, big-endian:
|
|
|
|
|
|
`[ version:1 ][ type:1 ][ session_id:4 ][ payload_len:2 ][ payload:N ]`.
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- `version` = `2`. `payload_len` lets the receiver ignore Ethernet padding
|
2026-08-13 08:40:02 +00:00
|
|
|
|
(frames under 60 bytes are zero-padded by the NIC).
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- 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.
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- Discovery: client broadcasts `DISCOVER`; server unicasts `MANIFEST` (with
|
|
|
|
|
|
hostname and upstream list) back.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
- Sessions: `OPEN` → `OPEN_ACK` (or `OPEN_NAK`) → `DATA`* ↔ `DATA`* → `CLOSE`.
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- Network test: `PING` (with 8-byte nonce) → `PONG` (nonce echoed).
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- TCP only. UDP frame types are reserved but unimplemented.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## `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`).
|
|
|
|
|
|
|
2026-08-13 08:43:11 +00:00
|
|
|
|
## `gatuna` usage
|
2026-08-13 08:40:02 +00:00
|
|
|
|
|
2026-08-13 08:43:11 +00:00
|
|
|
|
1. Launch `gatuna.exe` (UAC prompt expected — Npcap requires admin).
|
2026-08-13 08:40:02 +00:00
|
|
|
|
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
|
2026-08-13 08:43:11 +00:00
|
|
|
|
cd gatuna-win
|
2026-08-13 08:40:02 +00:00
|
|
|
|
dotnet build -c Release
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Or run directly:
|
|
|
|
|
|
```powershell
|
2026-08-13 08:43:11 +00:00
|
|
|
|
dotnet run --project gatuna-win -c Release
|
2026-08-13 08:40:02 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
|
## Privileges
|
|
|
|
|
|
|
2026-08-13 08:40:02 +00:00
|
|
|
|
**Server (Linux):** `AF_PACKET` requires `CAP_NET_RAW`. Run as root, or grant
|
|
|
|
|
|
the binary the capability once:
|
2026-08-12 18:20:51 +00:00
|
|
|
|
```
|
|
|
|
|
|
sudo setcap cap_net_raw+ep ./target/release/gatunad
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-13 08:40:02 +00:00
|
|
|
|
**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.
|
|
|
|
|
|
|
2026-08-12 18:20:51 +00:00
|
|
|
|
## Logging
|
|
|
|
|
|
|
2026-08-13 08:40:02 +00:00
|
|
|
|
**Server:** errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is
|
|
|
|
|
|
silent.
|
|
|
|
|
|
|
|
|
|
|
|
**Client:** status line in the UI. Errors are not logged to disk.
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
2026-08-13 09:08:05 +00:00
|
|
|
|
## Limitations
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
- 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.
|
2026-08-13 08:40:02 +00:00
|
|
|
|
- One outstanding `OPEN` at a time on the client (serialized via queue).
|
2026-08-13 09:08:05 +00:00
|
|
|
|
- L2 retransmit caps at 10 retries × 5 ms = 50 ms. A permanently dead link
|
|
|
|
|
|
closes the session with `reason = max_retries`.
|
2026-08-13 08:40:02 +00:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-08-13 08:43:11 +00:00
|
|
|
|
└── 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
|
2026-08-13 08:40:02 +00:00
|
|
|
|
```
|
2026-08-12 18:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
## License
|
|
|
|
|
|
|
|
|
|
|
|
CC0 1.0 Universal. See [`LICENSE`](LICENSE).
|