Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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()
}