Compare commits

..

3 Commits

Author SHA1 Message Date
mute 2f61f2bb1b move win/gatuna-client to gatuna-win, rename project to gatuna
- Directory: win/gatuna-client/ -> gatuna-win/
- Project file: gatuna-client.csproj -> gatuna.csproj
- Assembly name: gatuna-client -> gatuna
- Root namespace: gatuna_client -> gatuna
- All .cs files: namespace gatuna_client -> gatuna
- app.manifest: assemblyIdentity name -> gatuna
- README: updated all paths and references
2026-08-13 08:43:11 +00:00
mute 5bbda4c4c4 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
2026-08-13 08:40:02 +00:00
mute 094d080a95 disable maximize button and form resizing 2026-08-13 08:34:59 +00:00
10 changed files with 148 additions and 26 deletions
+24 -1
View File
@@ -40,7 +40,7 @@ emitted in v1.
| Type | Name | Direction | session_id | Payload | | Type | Name | Direction | session_id | Payload |
|------|-------------|---------------|------------|----------------------------------| |------|-------------|---------------|------------|----------------------------------|
| 0x01 | DISCOVER | C → broadcast | 0 | empty | | 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` | | 0x03 | OPEN | C → S | 0 | `upstream_id:1` |
| 0x04 | OPEN_ACK | S → C | assigned | `upstream_id:1` | | 0x04 | OPEN_ACK | S → C | assigned | `upstream_id:1` |
| 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` | | 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` |
@@ -212,3 +212,26 @@ client server
it emits `CLOSE` and exits. it emits `CLOSE` and exits.
- The server's tunnel→socket path writes `DATA` payloads to the `TcpStream` - The server's tunnel→socket path writes `DATA` payloads to the `TcpStream`
with `write_all`. On error it emits `CLOSE` and drops the session. 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 10100 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.
+113 -17
View File
@@ -16,32 +16,35 @@ pass unimpeded. See `wireguard-windows/tunnel/firewall/blocker.go:156`.
## Components ## Components
- `gatunad` — Rust server. Runs on the peer (Linux) that owns the real - **`gatunad`** — Rust server (`gatunad/`). Runs on the peer (Linux) that owns
services. Announces upstreams and relays TCP between the tunnel and the real services. Announces upstreams and relays TCP between the tunnel and
`127.0.0.1:<port>`. `127.0.0.1:<port>`.
- `gatuna` (planned) — .NET WinForms client. Runs on the killswitched Windows - **`gatuna`** — .NET 8 WinForms client (`gatuna-win/`). Runs on the
box. Discovers the server, presents its upstreams as local loopback killswitched Windows box. Discovers the server, presents its upstreams as
listeners, and hauls bytes over the same L2 protocol. local loopback listeners, and hauls bytes over the same L2 protocol. Includes
a network test (PING/PONG) for measuring latency, jitter, and loss.
This repository builds `gatunad` first.
## Transport ## Transport
- **Medium:** raw Ethernet frames on a shared L2 segment. - **Medium:** raw Ethernet frames on a shared L2 segment.
- **Ethertype:** `0x6969` (hardcoded). - **Ethertype:** `0x6969` (hardcoded).
- **No IP stack involvement.** Frames carry only our 6-byte header + payload. - **No IP stack involvement.** Frames carry only our 8-byte header + payload.
- **BPF:** the server attaches a classic BPF filter `ether proto 0x6969` to its - **BPF:** both sides filter on ethertype — the server via classic BPF on
`AF_PACKET` socket so it only wakes on our ethertype. No eBPF authoring. `AF_PACKET` (`SO_ATTACH_FILTER`), the client via Npcap's compiled filter.
No eBPF authoring.
## Protocol ## Protocol
See [`PROTOCOL.md`](PROTOCOL.md) for the full wire format. Summary: 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 ]`. - 8-byte header, big-endian:
- `version` = `1`. No length field (frame length comes from the capture). No `[ version:1 ][ type:1 ][ session_id:4 ][ payload_len:2 ][ payload:N ]`.
CRC. Ethertype discriminates our frames from everything else. - `version` = `1`. `payload_len` lets the receiver ignore Ethernet padding
- Discovery: client broadcasts `DISCOVER`; server unicasts `MANIFEST` back. (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`. - 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. - v1 ships TCP only. UDP frame types are reserved but unimplemented.
## `gatunad` usage ## `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`), 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`). `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 ## Privileges
`AF_PACKET` requires `CAP_NET_RAW`. Run as root, or grant the binary the **Server (Linux):** `AF_PACKET` requires `CAP_NET_RAW`. Run as root, or grant
capability once: the binary the capability once:
``` ```
sudo setcap cap_net_raw+ep ./target/release/gatunad 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 ## 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 ## v1 limitations
@@ -82,6 +150,34 @@ Errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is silent.
on a healthy switched link. on a healthy switched link.
- No auth/crypto. Anyone on the same L2 segment can `DISCOVER` and `OPEN`. - No auth/crypto. Anyone on the same L2 segment can `DISCOVER` and `OPEN`.
- Single server instance per interface. - 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
└── 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 ## License
@@ -1,4 +1,4 @@
namespace gatuna_client; namespace gatuna;
using System.Text; using System.Text;
@@ -1,6 +1,6 @@
using SharpPcap.LibPcap; using SharpPcap.LibPcap;
namespace gatuna_client; namespace gatuna;
public partial class MainForm : Form public partial class MainForm : Form
{ {
@@ -20,6 +20,8 @@ public partial class MainForm : Form
Text = "gatuna"; Text = "gatuna";
Width = 520; Width = 520;
Height = 420; Height = 420;
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Icon = SystemIcons.GetStockIcon(StockIconId.NetworkConnect, 32); Icon = SystemIcons.GetStockIcon(StockIconId.NetworkConnect, 32);
InitializeComponents(); InitializeComponents();
@@ -1,6 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
namespace gatuna_client; namespace gatuna;
sealed class PingTest sealed class PingTest
{ {
@@ -1,4 +1,4 @@
namespace gatuna_client; namespace gatuna;
static class Program static class Program
{ {
@@ -3,7 +3,7 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Channels; using System.Threading.Channels;
namespace gatuna_client; namespace gatuna;
sealed class SessionManager : IDisposable sealed class SessionManager : IDisposable
{ {
@@ -1,7 +1,7 @@
using SharpPcap; using SharpPcap;
using SharpPcap.LibPcap; using SharpPcap.LibPcap;
namespace gatuna_client; namespace gatuna;
sealed class TunnelLink : IDisposable sealed class TunnelLink : IDisposable
{ {
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="0.1.0.0" name="gatuna-client" /> <assemblyIdentity version="0.1.0.0" name="gatuna" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
@@ -3,7 +3,8 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>gatuna_client</RootNamespace> <AssemblyName>gatuna</AssemblyName>
<RootNamespace>gatuna</RootNamespace>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>