# rscamper
A Rust interface to [scamper](https://www.caida.org/catalog/software/scamper/)
network measurement tool.
`rscamper` wraps the scamper C libraries (`libscamperfile` and `libscamperctrl`) through Rust
FFI, providing idiomatic Rust types for all measurement data structures and a friendly
control loop.
## Features
- **File I/O** — read and write scamper warts/JSON files via `ScamperFile`
- **Control loop** — connect to a running scamper daemon and issue measurements via `ScamperCtrl`
- **All measurement types** — `trace`, `ping`, `tracelb`, `dealias`, `host`, `http`, `tbit`,
`sniff`, `udpprobe`, `owamp`, `sting`, `neighbourdisc`
- **Mux / VP support** — enumerate and connect to vantage points via `ScamperMux` and `ScamperVp`
## Prerequisites
`rscamper` links against `libscamperfile` and `libscamperctrl`, which are part of the scamper
distribution. Build and install scamper from source following the
[official instructions](https://www.caida.org/catalog/software/scamper/).
By default, the libraries are searched for in `/usr/local/lib` and `/usr/lib`. If you installed
scamper to a custom prefix, set the `SCAMPER_LIB_DIR` environment variable at build time:
```sh
SCAMPER_LIB_DIR=/opt/scamper/lib cargo build
```
## Installation
Run the following Cargo command in your project directory:
```sh
cargo add rscamper
```
## Usage
### Reading a warts file
```rust
use rscamper::{ScamperFile, ScamperObject};
let file = ScamperFile::open("results.warts", 'r', None).unwrap();
for obj in file {
if let ScamperObject::Ping(ping) = obj {
println!("{}", ping.to_json().unwrap_or_default());
}
}
```
### One-shot measurement
Schedule a single measurement, wait for the result, and save it to disk.
`inst.done()` is a required lifecycle signal — without it `responses()` and `poll()` block forever.
```rust
use rscamper::{ScamperCtrl, ScamperFile, ScamperObject};
let mut ctrl = ScamperCtrl::new(false, None).unwrap();
let inst = ctrl.add_inet(31337, None).unwrap();
ctrl.do_ping(
&inst, "1.1.1.1",
None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None,
None, None, None, None, None,
).unwrap();
inst.done(); // no more commands on this instance
// Pull exactly one result — blocks until it is ready.
// Equivalent: ctrl.responses(None).next().unwrap()
let item = ctrl.poll().unwrap();
let mut f = ScamperFile::open("result.warts", 'w', Some("warts")).unwrap();
f.write(&item.obj).unwrap();
f.close();
```
### Streaming multiple measurements
Schedule several measurements and process each result the moment it arrives,
without waiting for the full batch to finish.
```rust
use std::time::Duration;
use rscamper::{ScamperCtrl, ScamperObject};
let mut ctrl = ScamperCtrl::new(false, None).unwrap();
let inst = ctrl.add_inet(31337, None).unwrap();
for dst in &["1.1.1.1", "8.8.8.8"] {
ctrl.do_ping(
&inst, dst,
None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None,
None, None, None, None, None,
).unwrap();
}
inst.done();
// poll() blocks until one result is ready, then returns it immediately.
// The loop ends when all tasks are complete.
while let Some(item) = ctrl.poll() {
if let ScamperObject::Ping(ping) = &item.obj {
if let Some(stats) = ping.stats() {
println!("min RTT: {:?}", stats.min_rtt());
}
}
}
```
### Iterator style
Use `responses()` when you want a `for` loop. The argument controls whether
the iterator has a timeout:
| `responses(None)` | Waits indefinitely — ends only when all tasks are done |
| `responses(Some(duration))` | Stops early if the deadline elapses, even if tasks are still running |
`None` is safe when you trust scamper to finish every measurement (e.g. each
`do_*` call has its own `wait_timeout`). `Some(duration)` is a safety net for
the whole batch — useful when you cannot predict how long the full set will
take.
### Choosing between `poll()` and `responses()`
Both deliver results one at a time as they arrive. The difference is timeout
control and syntax:
| Syntax | `while let Some(r) = ctrl.poll()` | `for r in ctrl.responses(Some(d))` |
| Per-result wait | Indefinite — blocks until one result is ready | Same |
| Batch timeout | **None** — no way to stop the loop early | **Yes** — stops the whole iteration if the deadline elapses |
| Use when | You control timeouts per measurement via `wait_timeout` | You need a hard cap on total wall-clock time for the batch |
Use `poll()` when each `do_*` call already has a `wait_timeout` — scamper
will time out individual measurements and `poll()` will eventually return
`None`. Use `responses(Some(duration))` when you want a single deadline that
covers the entire batch regardless of how individual measurements behave.
```rust
use std::time::Duration;
use rscamper::{ScamperCtrl, ScamperObject};
let mut ctrl = ScamperCtrl::new(false, None).unwrap();
let inst = ctrl.add_inet(31337, None).unwrap();
ctrl.do_ping(
&inst, "1.1.1.1",
None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None,
None, None, None, None, None,
).unwrap();
inst.done();
// No timeout — wait as long as it takes.
for item in ctrl.responses(None) {
if let ScamperObject::Ping(ping) = &item.obj {
if let Some(stats) = ping.stats() {
println!("min RTT: {:?}", stats.min_rtt());
}
}
}
// With a 10-second safety timeout on the whole batch.
for item in ctrl.responses(Some(Duration::from_secs(10))) {
if let ScamperObject::Ping(ping) = &item.obj {
if let Some(stats) = ping.stats() {
println!("min RTT: {:?}", stats.min_rtt());
}
}
}
```
### Mux / vantage points
```rust
use rscamper::ScamperCtrl;
let mut ctrl = ScamperCtrl::new(false, None).unwrap();
let _mux_inst = ctrl.add_inet(31337, None).unwrap();
for vp in ctrl.vps() {
let inst = ctrl.add_vp(&vp).unwrap();
// issue measurements on inst ...
inst.done();
}
```
## License
GPL-3.0-only — see the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
for details.