Compare commits

...

2 Commits

Author SHA1 Message Date
mute ee6b121370 bump version to 2.1.0
--verbose flag and TCP RST are feature additions within wire
protocol v2 (no protocol changes, MAJOR stays at 2).
2026-08-14 14:01:30 +00:00
mute f476f6b145 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).
2026-08-14 13:59:15 +00:00
5 changed files with 31 additions and 7 deletions
+16 -2
View File
@@ -85,7 +85,7 @@ sealed class SessionManager : IDisposable
lock (_openLock) lock (_openLock)
{ {
if (_pending != null) if (_pending != null)
_pending.Client.Dispose(); NetUtil.RstClose(_pending.Client);
} }
ProcessQueue(); ProcessQueue();
break; break;
@@ -546,7 +546,21 @@ sealed class Session(
_cts.Cancel(); _cts.Cancel();
_deliverChannel.Writer.TryComplete(); _deliverChannel.Writer.TryComplete();
SendClose(); SendClose();
try { client.Dispose(); } catch { } NetUtil.RstClose(client);
_cts.Dispose(); _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 { }
}
}
+1 -1
View File
@@ -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="2.0.0.0" name="gatuna" /> <assemblyIdentity version="2.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">
+1 -1
View File
@@ -5,7 +5,7 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<AssemblyName>gatuna</AssemblyName> <AssemblyName>gatuna</AssemblyName>
<RootNamespace>gatuna</RootNamespace> <RootNamespace>gatuna</RootNamespace>
<Version>2.0.0</Version> <Version>2.1.0</Version>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gatuna" name = "gatuna"
version = "2.0.0" version = "2.1.0"
edition = "2021" edition = "2021"
license = "CC0-1.0" license = "CC0-1.0"
+12 -2
View File
@@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex as AsyncMutex;
use tracing::{error, Level}; use tracing::{error, info, Level};
use crate::frame::{ use crate::frame::{
Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM, 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. /// One or more TCP upstreams as PORT[:label], relayed to 127.0.0.1:PORT.
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
ports: Vec<String>, ports: Vec<String>,
/// Verbose logging (lifecycle events to stdout).
#[arg(short, long)]
verbose: bool,
} }
#[tokio::main] #[tokio::main]
async fn main() -> ExitCode { async fn main() -> ExitCode {
let level = if args.verbose { Level::INFO } else { Level::ERROR };
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_max_level(Level::ERROR) .with_max_level(level)
.with_writer(|| std::io::stdout()) .with_writer(|| std::io::stdout())
.init(); .init();
@@ -141,13 +145,16 @@ async fn handle_frame(
) { ) {
match frame { match frame {
Frame::Discover => { Frame::Discover => {
info!("DISCOVER from {src:?}");
let manifest = Frame::Manifest { let manifest = Frame::Manifest {
hostname: (**hostname).clone(), hostname: (**hostname).clone(),
entries: table.entries(), entries: table.entries(),
}; };
let _ = tx.send((src, manifest.encode())).await; let _ = tx.send((src, manifest.encode())).await;
info!("MANIFEST sent to {src:?} ({} upstreams)", table.0.len());
} }
Frame::Open { upstream_id, proto: _ } => { Frame::Open { upstream_id, proto: _ } => {
info!("OPEN upstream {upstream_id} from {src:?}");
let upstream = table.get(upstream_id); let upstream = table.get(upstream_id);
match upstream { match upstream {
Some(upstream) => { Some(upstream) => {
@@ -176,10 +183,12 @@ async fn handle_frame(
); );
let ack = Frame::OpenAck { session_id: sid, upstream_id, proto }; let ack = Frame::OpenAck { session_id: sid, upstream_id, proto };
let _ = tx.send((src, ack.encode())).await; let _ = tx.send((src, ack.encode())).await;
info!("session {sid} established (upstream {upstream_id})");
spawn_pump(r, sid, proto, src, tx, store); spawn_pump(r, sid, proto, src, tx, store);
} }
Err(e) => { Err(e) => {
error!("connect 127.0.0.1:{port} failed: {e}"); error!("connect 127.0.0.1:{port} failed: {e}");
info!("OPEN_NAK upstream {upstream_id} (connect_failed) to {src:?}");
let nak = let nak =
Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED }; Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED };
let _ = tx.send((src, nak.encode())).await; let _ = tx.send((src, nak.encode())).await;
@@ -215,6 +224,7 @@ async fn handle_frame(
} }
} }
Frame::Close { session_id, reason: _ } => { Frame::Close { session_id, reason: _ } => {
info!("CLOSE session {session_id} from {src:?}");
store.lock().expect("store poisoned").remove(&session_id); store.lock().expect("store poisoned").remove(&session_id);
} }
Frame::Ping { nonce } => { Frame::Ping { nonce } => {