server/cli/tests/support/connect_trap.rs
//! **The connect trap** (H3, shared since B2): an `LD_PRELOAD` C shim, built here with
//! `cc`, that records every `connect`, `sendto` and `getaddrinfo` of a process. Used by
//! `downstream_serves_the_build_systems.rs` and `the_default_build_dials_nothing.rs` (tests/support/: no test target of its own).
#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn text(o: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
)
}
/// An `LD_PRELOAD` shim: every `connect`/`sendto` to an IPv4 or IPv6 address and every
/// `getaddrinfo` the process makes is appended to `$HOLGER_CONNECT_LOG` (`inet <host>
/// <port>`, `dns <name>`), then done as asked. Unix sockets are not recorded.
const TRAP_C: &str = r#"
#define _GNU_SOURCE
#include <dlfcn.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
static void note(const char *line) {
const char *p = getenv("HOLGER_CONNECT_LOG");
if (!p) return;
int f = open(p, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0644);
if (f < 0) return;
int n = 0; while (line[n]) n++;
ssize_t w = write(f, line, n); (void)w;
close(f);
}
static void note_addr(const struct sockaddr *a) {
char host[INET6_ADDRSTRLEN] = "?", line[160]; int port = 0;
if (!a) return;
if (a->sa_family == AF_INET) {
const struct sockaddr_in *s = (const void *)a;
inet_ntop(AF_INET, &s->sin_addr, host, sizeof host); port = ntohs(s->sin_port);
} else if (a->sa_family == AF_INET6) {
const struct sockaddr_in6 *s = (const void *)a;
inet_ntop(AF_INET6, &s->sin6_addr, host, sizeof host); port = ntohs(s->sin6_port);
} else return;
snprintf(line, sizeof line, "inet %s %d\n", host, port);
note(line);
}
int connect(int fd, const struct sockaddr *a, socklen_t l) {
static int (*real)(int, const struct sockaddr *, socklen_t);
if (!real) real = dlsym(RTLD_NEXT, "connect");
note_addr(a);
return real(fd, a, l);
}
ssize_t sendto(int fd, const void *b, size_t n, int fl, const struct sockaddr *a, socklen_t l) {
static ssize_t (*real)(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
if (!real) real = dlsym(RTLD_NEXT, "sendto");
note_addr(a);
return real(fd, b, n, fl, a, l);
}
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) {
static int (*real)(const char *, const char *, const struct addrinfo *, struct addrinfo **);
if (!real) real = dlsym(RTLD_NEXT, "getaddrinfo");
char line[300];
snprintf(line, sizeof line, "dns %s\n", node ? node : "?");
note(line);
return real(node, service, hints, res);
}
"#;
/// The trap, built once per test process with `cc`, and proven to record: a bash
/// `/dev/tcp` connect to a listener here must show up in its log.
pub fn trap() -> &'static Path {
static TRAP: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
TRAP.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("holger-connect-trap-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (c, so) = (dir.join("trap.c"), dir.join("trap.so"));
std::fs::write(&c, TRAP_C).unwrap();
let out = Command::new("cc")
.args(["-shared", "-fPIC", "-O1", "-o"])
.arg(&so)
.arg(&c)
.arg("-ldl")
.output()
.expect("cc (the connect trap is a C shim)");
assert!(out.status.success(), "cc trap.so: {}", text(&out));
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let log = dir.join("control.log");
let out = Command::new("bash")
.args(["-c", &format!("exec 3<>/dev/tcp/127.0.0.1/{port}")])
.env("LD_PRELOAD", &so)
.env("HOLGER_CONNECT_LOG", &log)
.output()
.unwrap();
assert!(out.status.success(), "the control connect: {}", text(&out));
let seen = std::fs::read_to_string(&log).unwrap_or_default();
assert!(
seen.contains(&format!("inet 127.0.0.1 {port}")),
"the trap records nothing; it would prove nothing: {seen:?}"
);
so
})
}
/// Run `c` with the trap, recording into `log`.
pub fn trapped<'a>(c: &'a mut Command, log: &Path) -> &'a mut Command {
c.env("LD_PRELOAD", trap()).env("HOLGER_CONNECT_LOG", log)
}
/// What a trapped process dialled: the `inet`/`dns` lines, each once.
pub fn dialled(log: &Path) -> std::collections::BTreeSet<String> {
std::fs::read_to_string(log)
.unwrap_or_default()
.lines()
.map(String::from)
.collect()
}