mute dc5df5c073 fix private type leak and dead_code warnings
- LinkFd -> pub(crate) so it's visible in return types of readable/writable
- #[allow(dead_code)] on Proto enum (Udp variant reserved for v2)
2026-08-13 07:44:02 +00:00
2026-08-12 18:23:46 +00:00
2026-08-12 14:32:34 +00:00

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. 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.

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.

Protocol

See 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.
  • Sessions: OPENOPEN_ACK (or OPEN_NAK) → DATA* ↔ DATA* → CLOSE.
  • v1 ships TCP only. UDP frame types are reserved but unimplemented.

gatunad usage

gatunad IFACE PORT [PORT...]
  • IFACE — L2 interface name (e.g. eth0).
  • PORTint[: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).

Privileges

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

Logging

Errors only, to stdout. Normal lifecycle (DISCOVER/OPEN/CLOSE) is silent.

v1 limitations

  • TCP only. UDP wire types reserved, code paths stubbed.
  • No retransmit at the L2 layer. A dropped DATA frame breaks the TCP session irrecoverably because the localhost socket already ACKed the bytes. Acceptable on a healthy switched link.
  • No auth/crypto. Anyone on the same L2 segment can DISCOVER and OPEN.
  • Single server instance per interface.

License

CC0 1.0 Universal. See LICENSE.

S
Description
raw ethernet tunnel to circumvent WFP killswitches
Readme CC0-1.0 253 KiB
Languages
Rust 51.7%
C# 48.3%