Skip to content

Commit

Permalink
feat: implement rtt filtering, add more cli flags, add abstractions
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Feb 26, 2024
1 parent 02df66c commit 1f42e61
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 62 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ CLI that helps to filter Mullvad servers and pick the closest one.

## Features

- [x] Ping matching Mullvad relays and print the results.
- [x] Filter servers by:
- [ ] Ping round-trip time;
- [x] Ping round-trip time;
- [x] Used protocol: OpenVPN or WireGuard;
- [x] Distance from the current location;
- [x] Ping matching Mullvad relays.
- [ ] Print the results in a table.

Pinging is done using TCP, not ICMP. Reasons:

Expand Down
16 changes: 10 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,25 @@ pub struct Cli {

/// Filter servers by maximum rtt (in ms).
#[arg(short, long)]
pub rtt: Option<usize>,
pub rtt: Option<u64>,

/// How many pings to send for each relay.
#[arg(short, long, default_value_t = 4)]
/// Set pings count to perform.
#[arg(short, long, default_value_t = 8)]
pub count: usize,

/// Specify ping timeout (in ms).
/// Set ping timeout (in ms).
#[arg(long, default_value_t = 750)]
pub timeout: u64,

/// Specify the latitude.
/// Set ping interval (in ms).
#[arg(long, default_value_t = 1000)]
pub interval: u64,

/// Set the latitude.
#[arg(long, requires = "longitude")]
pub latitude: Option<f64>,

/// Specify the longitude.
/// Set the longitude.
#[arg(long, requires = "latitude")]
pub longitude: Option<f64>,
}
Expand Down
50 changes: 46 additions & 4 deletions src/filters.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::fmt::Debug;
use std::time::Duration;

use crate::coord::Coord;
use crate::pinger::RelayTimed;
use crate::relays::{Protocol, Relay};

#[derive(PartialEq)]
Expand All @@ -13,17 +15,21 @@ pub enum FilterStage {

/// Filter trait to dynamically dispatch filters.
pub trait Filter: Debug {
type Item;

/// Returns the stage of the filter.
fn stage(&self) -> FilterStage;

/// Filter predicate.
fn matches(&self, relay: &Relay) -> bool;
fn matches(&self, item: &Self::Item) -> bool;
}

/// Filter by distance. The distance is in kilometers.
#[derive(Debug)]
pub struct FilterByDistance {
/// Current coordinates.
coord: Coord,
/// Distance in kilometers.
distance: f64,
}

Expand All @@ -34,18 +40,21 @@ impl FilterByDistance {
}

impl Filter for FilterByDistance {
type Item = Relay;

fn stage(&self) -> FilterStage {
FilterStage::Load
}

fn matches(&self, relay: &Relay) -> bool {
fn matches(&self, relay: &Self::Item) -> bool {
relay.coord.distance_to(&self.coord) < self.distance
}
}

/// Filter by protocol. `None` means any protocol.
/// Filter by protocol.
#[derive(Debug)]
pub struct FilterByProtocol {
/// Protocol to compare with. `None` means any protocol.
protocol: Option<Protocol>,
}

Expand All @@ -56,14 +65,47 @@ impl FilterByProtocol {
}

impl Filter for FilterByProtocol {
type Item = Relay;

fn stage(&self) -> FilterStage {
FilterStage::Load
}

fn matches(&self, relay: &Relay) -> bool {
fn matches(&self, relay: &Self::Item) -> bool {
self
.protocol
.as_ref()
.map_or(true, |protocol| relay.protocol == *protocol)
}
}

/// Filter by Round-Trip Time.
#[derive(Debug)]
pub struct FilterByRTT {
/// RTT value to compare with. `None` means any RTT.
rtt: Option<Duration>,
}

impl FilterByRTT {
pub fn new(rtt: Option<Duration>) -> Self {
Self { rtt }
}
}

impl Filter for FilterByRTT {
type Item = RelayTimed;

fn stage(&self) -> FilterStage {
FilterStage::Ping
}

fn matches(&self, timings: &Self::Item) -> bool {
// If `rtt` is `None`, then it means any RTT, so we then default to `true`.
self.rtt.map_or(true, |filter_rtt| {
// Otherwise, we compare the measured RTT with the filter RTT, but here we default to `false`.
timings
.rtt()
.map_or(false, |relay_rtt| relay_rtt <= filter_rtt)
})
}
}
47 changes: 31 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use clap::Parser;
use pingmole::cli::{Cli, Spinner};
use pingmole::coord::Coord;
use pingmole::filters::{FilterByDistance, FilterByProtocol};
use pingmole::pinger::RelayPinger;
use pingmole::filters::{FilterByDistance, FilterByProtocol, FilterByRTT};
use pingmole::pinger::{RelayPingerConfig, RelaysPinger};
use pingmole::relays::RelaysLoader;

#[tokio::main]
Expand All @@ -15,6 +16,7 @@ async fn main() -> anyhow::Result<()> {

// -----------------------------------------------------------------------------------------------
// 1. Get the current location, either via arguments or via Mullvad API.

spinner.set_message("Getting current location");

let user = match cli.latitude.zip(cli.longitude) {
Expand All @@ -26,6 +28,7 @@ async fn main() -> anyhow::Result<()> {

// -----------------------------------------------------------------------------------------------
// 2. Load relays from file and filter them.

spinner.set_message("Loading relays");

let loader = RelaysLoader::new(
Expand All @@ -42,29 +45,41 @@ async fn main() -> anyhow::Result<()> {

// -----------------------------------------------------------------------------------------------
// 3. Ping relays.
spinner.set_message("Pinging relays");

let mut tasks = Vec::new();
let mut timings = Vec::new();

for relay in relays {
let mut pinger = RelayPinger::new(relay);
spinner.set_message("Pinging relays");

pinger.set_count(cli.count);
pinger.set_timeout(Duration::from_millis(cli.timeout));
let config = Arc::new(
RelayPingerConfig::new()
.set_count(cli.count)
.set_timeout(Duration::from_millis(cli.timeout))
.set_interval(Duration::from_millis(cli.interval)),
);

tasks.push(tokio::spawn(pinger.execute()));
}
let pinger = RelaysPinger::new(
relays,
config,
vec![Box::new(FilterByRTT::new(
cli.rtt.map(Duration::from_millis),
))],
);

for task in tasks {
timings.push(task.await?);
}
let timings = pinger.ping().await?;

// -----------------------------------------------------------------------------------------------
// 4. Print results.

spinner.stop();

dbg!(timings);
for timed in timings {
let relay = timed.relay();
let rtt = timed.rtt().unwrap_or(Duration::default());
let rtt_median = timed.rtt_median().unwrap_or(Duration::default());

println!(
"{} rtt={:.2?} median={:.2?} ({} pings)",
relay.ip, rtt, rtt_median, cli.count
);
}

Ok(())
}
Loading

0 comments on commit 1f42e61

Please sign in to comment.