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)
{
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 { }
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+1 -1
View File
@@ -5,7 +5,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<AssemblyName>gatuna</AssemblyName>
<RootNamespace>gatuna</RootNamespace>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gatuna"
version = "2.0.0"
version = "2.1.0"
edition = "2021"
license = "CC0-1.0"
+12 -2
View File
@@ -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<String>,
/// 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 } => {