use std::io::{BufRead as _, Read as _};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, TcpStream};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
fn foreign_peers(pid: u32) -> Vec<String> {
let mut out = Vec::new();
for table in ["tcp", "tcp6", "udp", "udp6"] {
let Ok(text) = std::fs::read_to_string(format!("/proc/{pid}/net/{table}")) else {
continue;
};
for line in text.lines().skip(1) {
let Some(remote) = line.split_whitespace().nth(2) else {
continue;
};
let Some((addr, _port)) = remote.split_once(':') else {
continue;
};
let ip = match addr.len() {
8 => u32::from_str_radix(addr, 16)
.ok()
.map(|v| IpAddr::V4(Ipv4Addr::from(v.swap_bytes()))),
32 => u128::from_str_radix(addr, 16).ok().map(|v| {
let b = v.to_be_bytes();
let mut o = [0u8; 16];
for w in 0..4 {
for i in 0..4 {
o[w * 4 + i] = b[w * 4 + 3 - i];
}
}
IpAddr::V6(Ipv6Addr::from(o))
}),
_ => None,
};
let Some(ip) = ip else { continue };
let local = ip.is_unspecified()
|| ip.is_loopback()
|| matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some_and(|v4| v4.is_loopback() || v4.is_unspecified()));
if !local {
out.push(format!("{table} {ip}"));
}
}
}
out
}
#[test]
fn the_default_server_starts_serves_and_dials_nothing() {
let data = tempfile::tempdir().expect("a data root");
let home = tempfile::tempdir().expect("a home");
let log_path = data.path().join("server.log");
let log = std::fs::File::create(&log_path).expect("the log");
let mut child = Command::new(env!("CARGO_BIN_EXE_gunnar-server"))
.arg("--data-root")
.arg(data.path().join("root"))
.args([
"serve",
"--ssh-bind",
"127.0.0.1:0",
"--grpc-bind",
"127.0.0.1:0",
])
.env_clear()
.env("HOME", home.path())
.env("PATH", "/usr/bin:/bin")
.env("RUST_LOG", "info")
.stdout(Stdio::piped())
.stderr(Stdio::from(log))
.spawn()
.expect("the server starts");
let pid = child.id();
let mut reader = std::io::BufReader::new(child.stdout.take().expect("piped"));
let (mut ssh, mut grpc) = (None, None);
let deadline = Instant::now() + Duration::from_secs(60);
while Instant::now() < deadline && (ssh.is_none() || grpc.is_none()) {
let mut line = String::new();
if matches!(reader.read_line(&mut line), Ok(0) | Err(_)) {
break;
}
let word = |p: &str| {
line.strip_prefix(p)
.and_then(|r| r.split_whitespace().next())
.map(str::to_owned)
};
ssh = ssh.or_else(|| word("ssh "));
grpc = grpc.or_else(|| word("grpc "));
}
std::thread::spawn(move || {
let mut sink = String::new();
let _ = reader.read_to_string(&mut sink);
});
let (ssh, grpc) = (
ssh.expect("the banner names the ssh listener"),
grpc.expect("the banner names the gRPC listener"),
);
let mut s = TcpStream::connect(&ssh).expect("the ssh port accepts");
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
let mut banner = [0u8; 8];
s.read_exact(&mut banner).expect("an ssh banner");
assert_eq!(&banner, b"SSH-2.0-", "the git port speaks SSH");
TcpStream::connect(&grpc).expect("the gRPC port accepts");
let mut seen = Vec::new();
for _ in 0..30 {
seen.extend(foreign_peers(pid));
std::thread::sleep(Duration::from_millis(100));
}
let _ = child.kill();
let _ = child.wait();
let text = std::fs::read_to_string(&log_path).unwrap_or_default();
assert!(
seen.is_empty(),
"the default server opened a socket to another machine: {seen:?}\n{text}"
);
for tell in [
"Network is unreachable",
"network unreachable",
"failed to lookup address",
"dns error",
] {
assert!(
!text.contains(tell),
"the log shows an outbound attempt ({tell}):\n{text}"
);
}
}