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}");
}
}