use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
fn main() {
exit_when_orphaned();
let mode = env_str("FAKE_SSH_MODE", "sleep");
if mode == "hang" {
ignore_sigterm();
}
let start = record_start();
start_tunnel();
match mode.as_str() {
"hang" => sleep_forever(),
"exit" => {
thread::sleep(Duration::from_millis(env_num("FAKE_SSH_DELAY_MS", 50)));
std::process::exit(exit_code_for(start));
}
_ => sleep_forever(),
}
}
enum Forward {
Tcp { listen: u16, target: u16 },
Unix { listen: PathBuf, target: PathBuf },
}
fn forward(flag: &str) -> Option<Forward> {
let argv: Vec<String> = std::env::args().collect();
let spec = argv
.iter()
.position(|a| a == flag)
.and_then(|i| argv.get(i + 1))?;
if spec.starts_with('/') {
let (listen, target) = spec.split_once(':')?;
return Some(Forward::Unix {
listen: PathBuf::from(listen),
target: PathBuf::from(target),
});
}
let (listen, rest) = spec.split_once(':')?;
let (_host, target) = rest.rsplit_once(':')?;
Some(Forward::Tcp {
listen: listen.parse().ok()?,
target: target.parse().ok()?,
})
}
fn start_tunnel() {
let left = forward("-L");
let right = forward("-R");
let mode = env_str("FAKE_SSH_TUNNEL", "auto");
let mode = match mode.as_str() {
"auto" if left.is_some() && right.is_some() => "loop",
"auto" if left.is_some() => "echo",
"auto" => "none",
other => other,
};
match (left, right) {
(Some(Forward::Tcp { listen, .. }), r) => {
let back = match r {
Some(Forward::Tcp { target, .. }) => Some(target),
_ => None,
};
tcp_tunnel(mode, listen, back);
}
(Some(Forward::Unix { listen, .. }), r) => {
let back = match r {
Some(Forward::Unix { target, .. }) => Some(target),
_ => None,
};
unix_tunnel(mode, listen, back);
}
(None, _) => {}
}
}
fn tcp_tunnel(mode: &str, listen: u16, back_to: Option<u16>) {
match mode {
"loop" => {
let Some(target) = back_to else { return };
spawn_server(
move || TcpListener::bind(("127.0.0.1", listen)),
format!("127.0.0.1:{listen}"),
move |mut inbound| {
let Ok(mut outbound) = TcpStream::connect(("127.0.0.1", target)) else {
return;
};
splice(&mut inbound, &mut outbound);
},
);
}
"echo" => spawn_server(
move || TcpListener::bind(("127.0.0.1", listen)),
format!("127.0.0.1:{listen}"),
echo,
),
"blackhole" => spawn_server(
move || TcpListener::bind(("127.0.0.1", listen)),
format!("127.0.0.1:{listen}"),
blackhole,
),
_ => {}
}
}
fn unix_tunnel(mode: &str, listen: PathBuf, back_to: Option<PathBuf>) {
let _ = std::fs::remove_file(&listen);
let name = listen.display().to_string();
let bind = {
let listen = listen.clone();
move || UnixListener::bind(&listen)
};
match mode {
"loop" => {
let Some(target) = back_to else { return };
spawn_server(bind, name, move |mut inbound| {
let Ok(mut outbound) = UnixStream::connect(&target) else {
return;
};
splice(&mut inbound, &mut outbound);
});
}
"echo" => spawn_server(bind, name, echo),
"blackhole" => spawn_server(bind, name, blackhole),
_ => {}
}
}
fn splice<S: Splittable>(inbound: &mut S, outbound: &mut S) {
let (Ok(mut a), Ok(mut b)) = (inbound.dup(), outbound.dup()) else {
return;
};
thread::spawn(move || {
let _ = std::io::copy(&mut a, &mut b);
});
let _ = std::io::copy(outbound, inbound);
}
fn echo<S: Splittable>(mut s: S) {
let Ok(mut back) = s.dup() else { return };
let _ = std::io::copy(&mut back, &mut s);
}
fn blackhole<S: Splittable>(s: S) {
thread::sleep(Duration::from_secs(3600));
drop(s);
}
trait Splittable: std::io::Read + std::io::Write + Send + Sized + 'static {
fn dup(&self) -> std::io::Result<Self>;
}
impl Splittable for TcpStream {
fn dup(&self) -> std::io::Result<Self> {
self.try_clone()
}
}
impl Splittable for UnixStream {
fn dup(&self) -> std::io::Result<Self> {
self.try_clone()
}
}
fn spawn_server<L, S, B, H>(bind: B, name: String, handle: H)
where
L: Listener<Conn = S>,
S: Send + 'static,
B: FnOnce() -> std::io::Result<L> + Send + 'static,
H: Fn(S) + Send + Clone + 'static,
{
thread::spawn(move || {
let listener = match bind() {
Ok(l) => l,
Err(e) => {
eprintln!("fake-ssh: cannot bind {name}: {e}");
std::process::exit(70);
}
};
while let Ok(conn) = listener.take() {
let handle = handle.clone();
thread::spawn(move || handle(conn));
}
});
}
trait Listener: Send + 'static {
type Conn;
fn take(&self) -> std::io::Result<Self::Conn>;
}
impl Listener for TcpListener {
type Conn = TcpStream;
fn take(&self) -> std::io::Result<TcpStream> {
self.accept().map(|(s, _)| s)
}
}
impl Listener for UnixListener {
type Conn = UnixStream;
fn take(&self) -> std::io::Result<UnixStream> {
self.accept().map(|(s, _)| s)
}
}
fn record_start() -> usize {
let Ok(path) = std::env::var("FAKE_SSH_STATE") else {
return 1;
};
let before = OpenOptions::new()
.read(true)
.open(&path)
.map(|f| BufReader::new(f).lines().count())
.unwrap_or(0);
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&path) {
let argv: Vec<String> = std::env::args().skip(1).collect();
let _ = writeln!(f, "{}", argv.join(" "));
let _ = f.flush();
}
before + 1
}
fn exit_code_for(start: usize) -> i32 {
let codes = env_str("FAKE_SSH_EXIT_CODES", "0");
let codes: Vec<i32> = codes
.split(',')
.filter_map(|c| c.trim().parse().ok())
.collect();
if codes.is_empty() {
return 0;
}
codes[(start - 1).min(codes.len() - 1)]
}
fn exit_when_orphaned() {
let parent = unsafe { libc::getppid() };
thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(250));
if unsafe { libc::getppid() } != parent {
std::process::exit(0);
}
}
});
}
fn ignore_sigterm() {
unsafe { libc::signal(libc::SIGTERM, libc::SIG_IGN) };
}
fn sleep_forever() -> ! {
loop {
thread::sleep(Duration::from_secs(3600));
}
}
fn env_str(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_owned())
}
fn env_num(key: &str, default: u64) -> u64 {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}