add gatunad server and winforms client v1 (TCP-only)
Raw-Ethernet tunnel over ethertype 0x6969 to bypass WFP killswitches. Server (Rust, AF_PACKET + classic BPF) relays TCP to 127.0.0.1 services. Client (.NET 8 WinForms, SharpPcap/Npcap) discovers upstreams and exposes local loopback listeners. Wire protocol: 6-byte header, 7 frame types, MANIFEST with labeled upstreams. UDP types reserved, unimplemented.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
//! `gatunad` — raw-Ethernet tunnel server.
|
||||
//!
|
||||
//! Listens on an L2 interface for ethertype `0x6969` frames, announces TCP
|
||||
//! upstreams to a discovering client, and relays bidirectional TCP traffic
|
||||
//! between the client and `127.0.0.1:<port>` services.
|
||||
|
||||
mod frame;
|
||||
mod link;
|
||||
mod session;
|
||||
mod upstream;
|
||||
|
||||
use clap::Parser;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::{error, Level};
|
||||
|
||||
use crate::frame::{
|
||||
Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM,
|
||||
};
|
||||
use crate::link::Link;
|
||||
use crate::session::{spawn_pump, write_to_session, SessionHandle, SessionStore};
|
||||
use crate::upstream::build_table;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "gatunad", version, about = "raw ethernet tunnel server")]
|
||||
struct Args {
|
||||
/// L2 interface name (e.g. eth0).
|
||||
iface: String,
|
||||
/// One or more TCP upstreams as PORT[:label], relayed to 127.0.0.1:PORT.
|
||||
#[arg(num_args = 1..)]
|
||||
ports: Vec<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(Level::ERROR)
|
||||
.with_writer(|| std::io::stdout())
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let table = match build_table(&args.ports) {
|
||||
Ok(t) => Arc::new(t),
|
||||
Err(e) => {
|
||||
eprintln!("gatunad: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let link = match Link::open(&args.iface) {
|
||||
Ok(l) => Arc::new(l),
|
||||
Err(e) => {
|
||||
eprintln!("gatunad: failed to open interface {}: {e}", args.iface);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<(crate::link::MacAddr, Vec<u8>)>(1024);
|
||||
|
||||
// Tx task: sole owner of send-side writes, fed by all session pumps + rx.
|
||||
{
|
||||
let link = Arc::clone(&link);
|
||||
tokio::spawn(async move {
|
||||
while let Some((dst, frame)) = rx.recv().await {
|
||||
loop {
|
||||
let mut guard = match link.writable().await {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("writable wait: {e}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match link.send(dst, &frame) {
|
||||
Ok(()) => break,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
guard.clear_ready();
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send failed: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let store: SessionStore = Arc::new(Mutex::new(std::collections::HashMap::new()));
|
||||
let next_id = Arc::new(AtomicU32::new(1));
|
||||
let mut buf = vec![0u8; 65536];
|
||||
|
||||
loop {
|
||||
let mut guard = match link.readable().await {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("readable wait: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match link.recv(&mut buf) {
|
||||
Ok(Some((src, payload))) => {
|
||||
let frame = match Frame::parse(payload) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
error!("decode from {src:?}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
handle_frame(frame, src, &tx, &store, &next_id, &table).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Ignorable frame (outgoing/short/mismatch) or transient; do not
|
||||
// clear readiness — keep draining.
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
guard.clear_ready();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("recv: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_frame(
|
||||
frame: Frame,
|
||||
src: crate::link::MacAddr,
|
||||
tx: &mpsc::Sender<(crate::link::MacAddr, Vec<u8>)>,
|
||||
store: &SessionStore,
|
||||
next_id: &Arc<AtomicU32>,
|
||||
table: &Arc<crate::upstream::UpstreamTable>,
|
||||
) {
|
||||
match frame {
|
||||
Frame::Discover => {
|
||||
let manifest = Frame::Manifest(table.entries());
|
||||
let _ = tx.send((src, manifest.encode())).await;
|
||||
}
|
||||
Frame::Open { upstream_id } => {
|
||||
let port = table.get(upstream_id).map(|u| u.port);
|
||||
match port {
|
||||
Some(port) => {
|
||||
// Spawn so connect() doesn't block the rx loop.
|
||||
let tx = tx.clone();
|
||||
let store = Arc::clone(store);
|
||||
let next = Arc::clone(next_id);
|
||||
tokio::spawn(async move {
|
||||
match TcpStream::connect(("127.0.0.1", port)).await {
|
||||
Ok(stream) => {
|
||||
let sid = next.fetch_add(1, Ordering::Relaxed);
|
||||
let (r, w) = stream.into_split();
|
||||
let w = Arc::new(AsyncMutex::new(w));
|
||||
store.lock().expect("store poisoned").insert(
|
||||
sid,
|
||||
SessionHandle {
|
||||
upstream_id,
|
||||
write: w,
|
||||
},
|
||||
);
|
||||
let ack = Frame::OpenAck { session_id: sid, upstream_id };
|
||||
let _ = tx.send((src, ack.encode())).await;
|
||||
spawn_pump(r, sid, src, tx, store);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("connect 127.0.0.1:{port} failed: {e}");
|
||||
let nak =
|
||||
Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED };
|
||||
let _ = tx.send((src, nak.encode())).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
None => {
|
||||
let nak = Frame::OpenNak { upstream_id, reason: REASON_UNKNOWN_UPSTREAM };
|
||||
let _ = tx.send((src, nak.encode())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Frame::Data { session_id, payload } => {
|
||||
match write_to_session(store, session_id, &payload).await {
|
||||
Ok(()) => {}
|
||||
Err(session::WriteError::UnknownSession) => {
|
||||
let close = Frame::Close {
|
||||
session_id,
|
||||
reason: Some(REASON_UNKNOWN_SESSION),
|
||||
};
|
||||
let _ = tx.send((src, close.encode())).await;
|
||||
}
|
||||
Err(session::WriteError::Io) => {
|
||||
store.lock().expect("store poisoned").remove(&session_id);
|
||||
let close = Frame::Close { session_id, reason: None };
|
||||
let _ = tx.send((src, close.encode())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Frame::Close { session_id, reason: _ } => {
|
||||
store.lock().expect("store poisoned").remove(&session_id);
|
||||
}
|
||||
// Not expected from a client; ignore.
|
||||
Frame::Manifest(_) | Frame::OpenAck { .. } | Frame::OpenNak { .. } => {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user