feat(proxy): add SSH TCP passthrough and graceful HTTP-only startup

Add optional [ssh] config block that proxies port 22 → Gitea SSH pod,
running on a dedicated thread/runtime matching the cert-watcher pattern.

Also start HTTP-only on first deploy when the TLS cert file doesn't exist
yet — once ACME challenge completes and the cert watcher writes the file,
a graceful upgrade adds the TLS listener without downtime.

Fix ACME watcher to handle InitApply events (kube-runtime v3+) so
Ingresses that existed before the proxy started are picked up correctly.

Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
This commit is contained in:
2026-03-10 23:38:19 +00:00
parent 10de00990c
commit e5b6802107
5 changed files with 89 additions and 4 deletions

41
src/ssh.rs Normal file
View File

@@ -0,0 +1,41 @@
use tokio::io::copy_bidirectional;
use tokio::net::{TcpListener, TcpStream};
/// Listens on `listen` and proxies every TCP connection to `backend`.
/// Runs forever; intended to be spawned on a dedicated OS thread + Tokio runtime,
/// matching the pattern used for the cert/ingress watcher.
pub async fn run_tcp_proxy(listen: &str, backend: &str) {
let listener = match TcpListener::bind(listen).await {
Ok(l) => {
tracing::info!(%listen, %backend, "SSH TCP proxy listening");
l
}
Err(e) => {
tracing::error!(error = %e, %listen, "SSH TCP proxy: bind failed");
return;
}
};
loop {
match listener.accept().await {
Ok((mut socket, peer_addr)) => {
let backend = backend.to_string();
tokio::spawn(async move {
match TcpStream::connect(&backend).await {
Ok(mut upstream) => {
if let Err(e) = copy_bidirectional(&mut socket, &mut upstream).await {
tracing::debug!(error = %e, %peer_addr, "ssh: session ended");
}
}
Err(e) => {
tracing::error!(error = %e, %peer_addr, %backend, "ssh: upstream connect failed");
}
}
});
}
Err(e) => {
tracing::error!(error = %e, "ssh: accept failed");
}
}
}
}