stamparm/maltrail · error

bind

Error message

bind

What it means

The metrics test harness spawn() binds a TcpListener on 127.0.0.1:0 (an OS-assigned port) and .expect("bind") panics if binding fails. With port 0, failure usually means the loopback interface is unavailable, resource limits are exhausted, or the network stack is restricted in the test environment.

Solutions

  1. Retry the spawn once after a short delay if the failure is transient resource exhaustion
  2. Verify loopback networking is available in the test environment (ulimit -n, container --network settings)
  3. Surface the io::Error from bind instead of a bare expect for diagnosis
  4. Raise the file-descriptor limit (ulimit -n) if sockets are being leaked by other tests

Example fix

// before
let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now()).expect("bind");
// after
let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now())
    .unwrap_or_else(|e| panic!("metrics listener bind failed: {e}"));
Defensive patterns

Strategy: retry

Validate before calling

std::net::TcpListener::bind("127.0.0.1:0").map(|l| drop(l)).map_err(|e| format!("loopback bind unavailable: {e}"))?;

Try / catch

let bound = spawn("127.0.0.1:0", registry, now).unwrap_or_else(|e| panic!("bind failed: {e}"));

Prevention

When it happens

Trigger: Calling spawn("127.0.0.1:0", registry, now) in the test a_scrape_returns_the_metrics_over_http when TcpListener::bind returns Err — e.g. no loopback, file-descriptor/socket exhaustion, or sandboxed CI without network permissions.

Common situations: CI containers with networking disabled; hitting the open-file limit after many tests leak listeners; hardened sandboxes (seccomp) blocking socket creation.

Related errors


AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/7ca61b2380914ad8. Report an issue: GitHub.

Appendix: source

Thrown at sensor/src/stats.rs:336

            "maltrail_worker_packets_total",
        ] {
            assert!(text.contains(&format!("# HELP {name} ")), "missing HELP for {name}:\n{text}");
            assert!(text.contains(&format!("# TYPE {name} ")), "missing TYPE for {name}");
        }
        assert!(text.contains("maltrail_trails 1505265"), "{text}");
        assert!(text.contains("maltrail_workers 2"), "{text}");
        assert!(text.contains("maltrail_worker_packets_total{worker=\"1\"}"), "{text}");
        // Monotonic counters must end in _total so `rate()` works.
        for line in text.lines().filter(|l| l.starts_with("# TYPE ") && l.ends_with(" counter")) {
            let name = line.split_whitespace().nth(2).unwrap();
            assert!(name.ends_with("_total"), "counter {name} must end in _total");
        }
    }

    #[test]
    fn a_scrape_returns_the_metrics_over_http() {
        let registry = Arc::new(Registry::new(1));
        let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now()).expect("bind");
        let mut stream = std::net::TcpStream::connect(bound).expect("connect");
        stream.write_all(b"GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n").unwrap();
        let mut response = String::new();
        stream.read_to_string(&mut response).unwrap();
        assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
        assert!(response.contains("text/plain; version=0.0.4"), "{response}");
        assert!(response.contains("maltrail_up 1"), "{response}");
    }

    #[test]
    fn a_bad_address_is_reported_not_fatal() {
        let registry = Arc::new(Registry::new(1));
        assert!(spawn("300.300.300.300:1", registry, std::time::Instant::now()).is_err());
    }
}

View on GitHub (pinned to 77cfb06d76)