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).
This commit is contained in:
2026-08-14 13:59:15 +00:00
parent a700514849
commit f476f6b145
2 changed files with 28 additions and 4 deletions
+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 } => {