Learn in Public unlocks on Jan 1, 2026
This lesson will be public then. Admins can unlock early with a password.
Build Your First Security Tool in Rust (Beginner-Friendly 2026 Guide)
Step-by-step tutorial to build a simple Rust security scanner using Tokio and Reqwest, then ship it like a pro.
Write and ship a real Rust security tool end to end: a safe URL liveness checker with concurrency limits, JSON logs, validation, and cleanup.
What You’ll Build
- A small Rust CLI that checks URLs concurrently with strict timeouts.
- JSON output for SOC ingestion and predictable, tagged User-Agent.
- Safe defaults (low concurrency, delays) to avoid accidental abuse.
Prerequisites
- macOS or Linux with Rust 1.80+ (
rustc --version). - Python 3.10+ (optional) to host a local test page.
- Run only against authorized URLs; use localhost for practice.
Safety and Legal
- Do not scan third-party sites without written approval.
- Keep concurrency low; stop on rate limits or complaints.
- Never hardcode secrets; keep configs outside binaries.
Step 1) Prepare test targets (local)
Click to view commands
mkdir -p mock_web
echo "ok" > mock_web/index.html
python3 -m http.server 8020 --directory mock_web > mock_web/server.log 2>&1 &
Common fix: If port 8020 is used, change to a free port and reuse it below.
Step 2) Create the Rust project
Click to view commands
cargo new rustsec-checker
cd rustsec-checker
Step 3) Add dependencies
Replace Cargo.toml with:
Click to view toml code
[package]
name = "rustsec-checker"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.40", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
futures = "0.3"
indicatif = "0.17"
anyhow = "1.0"
Step 4) Implement the tool
Replace src/main.rs with:
Click to view Rust code
use clap::Parser;
use futures::stream::{self, StreamExt};
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde::Serialize;
use std::time::Duration;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// File containing URLs (one per line)
#[arg(long)]
file: String,
/// Max concurrent requests
#[arg(long, default_value_t = 5)]
concurrency: usize,
/// Delay between tasks in ms
#[arg(long, default_value_t = 50)]
delay_ms: u64,
/// Request timeout in seconds
#[arg(long, default_value_t = 5)]
timeout: u64,
}
#[derive(Serialize)]
struct ResultRow {
url: String,
status: Option<u16>,
ok: bool,
elapsed_ms: u128,
}
async fn check_url(client: &Client, url: &str, delay_ms: u64) -> ResultRow {
let start = std::time::Instant::now();
let resp = client.get(url).send().await;
let (status, ok) = match resp {
Ok(r) => (Some(r.status().as_u16()), r.status().is_success()),
Err(_) => (None, false),
};
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
ResultRow {
url: url.to_string(),
status,
ok,
elapsed_ms: start.elapsed().as_millis(),
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let body = std::fs::read_to_string(&args.file)?;
let targets: Vec<String> = body.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).map(|s| s.to_string()).collect();
let client = Client::builder()
.user_agent("rustsec-checker (+you@example.com)")
.timeout(Duration::from_secs(args.timeout))
.build()?;
let pb = ProgressBar::new(targets.len() as u64);
pb.set_style(
ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] {pos}/{len} {msg}")?
.progress_chars("#>-"),
);
let results: Vec<ResultRow> = stream::iter(targets)
.map(|url| {
let client = client.clone();
let delay = args.delay_ms;
async move {
let row = check_url(&client, &url, delay).await;
row
}
})
.buffer_unordered(args.concurrency)
.inspect(|_| pb.inc(1))
.collect()
.await;
pb.finish_with_message("done");
println!("{}", serde_json::to_string_pretty(&results)?);
Ok(())
}
Click to view commands
echo -e "http://127.0.0.1:8020/\nhttps://example.com" > urls.txt
cargo run -- --file urls.txt --concurrency 3 --delay-ms 25
Common fixes:
connection refused: ensure the local server is running.- Timeouts: increase
--timeoutfor slow targets or reduce concurrency.
Step 5) Ship responsibly
- Default to safe values: low concurrency, small timeouts, optional jitter.
- Log JSON to stdout; pipe to files for audits:
... | tee results.json. - Add
--retryonly for idempotent GETs; never hammer login pages. - Sign binaries or distribute checksums; document usage and authorized scopes.
Cleanup
Click to view commands
cd ..
pkill -f "http.server 8020" || true
rm -rf rustsec-checker mock_web
Quick Reference
- Keep scans in-scope, low and slow, with clear User-Agent tagging.
- Validate after each change; capture JSON for SOC ingestion.
- Avoid secrets in code; keep configs external and rotated.