Libraries Beneath the Tools
Parts 1–5 of this series covered the user-facing binaries: workspace setup, recon,
crawling, fuzzing, and the terminal dashboard. But six of those binaries call the
same two library crates on every request: http-client and llm-client.
Neither is glamorous. Both are load-bearing.
http-client: Controlled HTTP at the Foundation
Every tool that touches the network — subdomain-enum, mg-scan, mg-crawl,
corpus-builder, mg-fuzz — imports http-client instead of calling reqwest
directly. Three reasons:
UA rotation. Tools that make hundreds of requests to the same target need to
avoid looking like a scanner. The client cycles through six realistic browser
strings on every request — Chrome on Windows, Chrome on Mac, Chrome on Linux,
Firefox on Windows, Firefox on Mac, Safari on Mac — in a deterministic round-robin
using an AtomicUsize:
static USER_AGENTS: &[&str] = &[
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...",
// ...
];
The counter is atomic so concurrent tasks each advance it without locking.
Rate limiting. Every client instance holds a Mutex<Instant> for the last
request time. The throttle() method computes elapsed time and sleeps the
difference before allowing the next request:
async fn throttle(&self) {
let Some(min_ms) = self.config.rate_limit_ms else { return; };
let min = Duration::from_millis(min_ms);
let mut last = self.last_req.lock().await;
let elapsed = last.elapsed();
if elapsed < min {
tokio::time::sleep(min - elapsed).await;
}
*last = Instant::now();
}
Rate limiting is per-instance, so a tool can create separate clients with different budgets — one conservative client for target probing, one faster client for public APIs like crt.sh.
Retry with jittered backoff. Transient failures — timeouts, TCP resets, brief
502s — shouldn’t abort a long recon run. The client retries up to max_retries
times with exponential base delay and 200ms of uniform jitter:
let base_ms = 300u64 * (1u64 << attempt);
let jitter_ms = fastrand::u64(0..200);
tokio::time::sleep(Duration::from_millis(base_ms + jitter_ms)).await;
The jitter matters when many concurrent tasks hit the same transient failure. Without it, all retries wake up at the same instant and flood the target again.
The public surface is small: get, post_json, get_json, get_text,
post_json_text. Callers configure a ClientConfig struct and get a Client.
That’s it.
llm-client: One Interface, Two Backends
Several tools need to call a language model — ai-prioritize, mg-report,
mg-recopilot, mg-exploitgen. The choice of model (local vs. remote, which
provider) is an operator decision that shouldn’t be baked into each tool.
llm-client exposes a single enum with two variants:
pub enum LlmClient {
Ollama(OllamaClient),
Anthropic(AnthropicClient),
}
impl LlmClient {
pub fn ollama(model: impl Into<String>) -> Result<Self, LlmError> { ... }
pub fn anthropic(api_key: impl Into<String>, model: impl Into<String>) -> Result<Self, LlmError> { ... }
pub async fn complete(&self, system: &str, user: &str) -> Result<String, LlmError> { ... }
}
Every tool that calls the LLM checks ANTHROPIC_API_KEY at startup. If it’s set,
you get AnthropicClient pointing at Claude Sonnet 4.6. If not, you get
OllamaClient pointing at a local Llama model. The same complete(system, user)
call works either way.
This matters for the offline mode that every AI tool supports. When you pass
--offline, the tool skips the LLM call entirely and generates a deterministic
placeholder. No API key, no network, reproducible output for testing. The
offline/online branching happens in each tool’s lib; llm-client doesn’t need
to know about it.
Why Shared Rather Than Inline
The alternative was duplicating the retry logic in each binary or calling reqwest
directly from mg-crawl, corpus-builder, etc. That would work until it didn’t:
the first time a target started returning 429s and one tool handled it differently
from another, or UA rotation was only in some tools and not others.
Shared libraries make policy uniform. If rate limiting needs to be tightened,
one change in http-client applies everywhere. If a new Anthropic model replaces
the current one, one change in llm-client is enough.
Part 7 covers corpus-builder: mining certificate transparency logs and the
Wayback Machine to reconstruct a target’s historical attack surface before any
active probing begins.