Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f61f2bb1b | |||
| 5bbda4c4c4 | |||
| 094d080a95 |
+24
-1
@@ -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.
|
||||
|
||||
@@ -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`** — .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 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` 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
|
||||
|
||||
`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,34 @@ 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
|
||||
└── 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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
using System.Text;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using SharpPcap.LibPcap;
|
||||
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
@@ -20,6 +20,8 @@ public partial class MainForm : Form
|
||||
Text = "gatuna";
|
||||
Width = 520;
|
||||
Height = 420;
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Icon = SystemIcons.GetStockIcon(StockIconId.NetworkConnect, 32);
|
||||
InitializeComponents();
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
sealed class PingTest
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
static class Program
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
sealed class SessionManager : IDisposable
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using SharpPcap;
|
||||
using SharpPcap.LibPcap;
|
||||
|
||||
namespace gatuna_client;
|
||||
namespace gatuna;
|
||||
|
||||
sealed class TunnelLink : IDisposable
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
@@ -3,7 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>gatuna_client</RootNamespace>
|
||||
<AssemblyName>gatuna</AssemblyName>
|
||||
<RootNamespace>gatuna</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
Reference in New Issue
Block a user