From f476f6b14565bf9c0ec7c356923251bfe1f8b103 Mon Sep 17 00:00:00 2001 From: Mute Date: Fri, 14 Aug 2026 13:59:15 +0000 Subject: [PATCH] add --verbose flag and TCP RST on connection failure Server (gatunad): - New --verbose/-v flag: logs lifecycle events at info level (DISCOVER, MANIFEST sent, OPEN, session established, CLOSE, OPEN_NAK). Without the flag, errors only as before. Client (gatuna): - OPEN_NAK and session CLOSE now send TCP RST to the local app instead of a graceful FIN. LingerOption(true, 0) causes Winsock to emit RST on close. The local app (e.g. ssh) sees a broken connection instead of a clean close, which is more honest about what happened (upstream was unreachable). --- gatuna-win/SessionManager.cs | 18 ++++++++++++++++-- gatunad/src/main.rs | 14 ++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/gatuna-win/SessionManager.cs b/gatuna-win/SessionManager.cs index bebc73b..560a646 100644 --- a/gatuna-win/SessionManager.cs +++ b/gatuna-win/SessionManager.cs @@ -85,7 +85,7 @@ sealed class SessionManager : IDisposable lock (_openLock) { if (_pending != null) - _pending.Client.Dispose(); + NetUtil.RstClose(_pending.Client); } ProcessQueue(); break; @@ -546,7 +546,21 @@ sealed class Session( _cts.Cancel(); _deliverChannel.Writer.TryComplete(); SendClose(); - try { client.Dispose(); } catch { } + NetUtil.RstClose(client); _cts.Dispose(); } } + +/// Close a TcpClient with a TCP RST instead of a FIN. +static partial class NetUtil +{ + public static void RstClose(TcpClient c) + { + try + { + c.LingerState = new LingerOption(true, 0); + c.Close(); + } + catch { } + } +} diff --git a/gatunad/src/main.rs b/gatunad/src/main.rs index a661f04..5e39e2c 100644 --- a/gatunad/src/main.rs +++ b/gatunad/src/main.rs @@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex}; use tokio::net::TcpStream; use tokio::sync::mpsc; use tokio::sync::Mutex as AsyncMutex; -use tracing::{error, Level}; +use tracing::{error, info, Level}; use crate::frame::{ Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM, @@ -33,12 +33,16 @@ struct Args { /// One or more TCP upstreams as PORT[:label], relayed to 127.0.0.1:PORT. #[arg(num_args = 1..)] ports: Vec, + /// Verbose logging (lifecycle events to stdout). + #[arg(short, long)] + verbose: bool, } #[tokio::main] async fn main() -> ExitCode { + let level = if args.verbose { Level::INFO } else { Level::ERROR }; tracing_subscriber::fmt() - .with_max_level(Level::ERROR) + .with_max_level(level) .with_writer(|| std::io::stdout()) .init(); @@ -141,13 +145,16 @@ async fn handle_frame( ) { match frame { Frame::Discover => { + info!("DISCOVER from {src:?}"); let manifest = Frame::Manifest { hostname: (**hostname).clone(), entries: table.entries(), }; let _ = tx.send((src, manifest.encode())).await; + info!("MANIFEST sent to {src:?} ({} upstreams)", table.0.len()); } Frame::Open { upstream_id, proto: _ } => { + info!("OPEN upstream {upstream_id} from {src:?}"); let upstream = table.get(upstream_id); match upstream { Some(upstream) => { @@ -176,10 +183,12 @@ async fn handle_frame( ); let ack = Frame::OpenAck { session_id: sid, upstream_id, proto }; let _ = tx.send((src, ack.encode())).await; + info!("session {sid} established (upstream {upstream_id})"); spawn_pump(r, sid, proto, src, tx, store); } Err(e) => { error!("connect 127.0.0.1:{port} failed: {e}"); + info!("OPEN_NAK upstream {upstream_id} (connect_failed) to {src:?}"); let nak = Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED }; let _ = tx.send((src, nak.encode())).await; @@ -215,6 +224,7 @@ async fn handle_frame( } } Frame::Close { session_id, reason: _ } => { + info!("CLOSE session {session_id} from {src:?}"); store.lock().expect("store poisoned").remove(&session_id); } Frame::Ping { nonce } => {