Browse Source

Use mrt-rs and flate2 for gz decompression (#6)

Parse bgp attributes, use flate2 for gz, mrt-rs to parse RIB entries

* Use gzdecoder, mrt-rs

* Parse AS path from BGP attributes

* Write Address and associated bottleneck to file

* Use structopt for cli args

* Add logging

* Create Bottleneck struct

* Handle ipv6 as well as ipv4
pull/13/head
RJ Rybarczyk 4 years ago
committed by GitHub
parent
commit
73980f2af0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 1381
      Cargo.lock
  3. 13
      Cargo.toml
  4. 86
      README.md
  5. 200
      data/dump_01_2019-12-28
  6. 42
      justfile
  7. 23
      src/address.rs
  8. 313
      src/as_path_parser.rs
  9. 90
      src/bgp_path.rs
  10. 22
      src/common.rs
  11. 55
      src/error.rs
  12. 343
      src/find_bottleneck.rs
  13. 58
      src/main.rs
  14. 38
      src/opt.rs
  15. 93
      src/subcommand.rs

4
.gitignore

@ -1,2 +1,6 @@
/target
**/*.rs.bk
data/
notes
dump/
bottleneck/

1381
Cargo.lock

File diff suppressed because it is too large

13
Cargo.toml

@ -3,3 +3,16 @@ name = "asmap-rs"
version = "0.1.0"
authors = ["Gleb Naumenko <naumenko.gs@gmail.com>", "RJ Rybarczyk <rj@rybar.tech>"]
edition = "2018"
[dependencies]
flate2 = { version = "1.0", features = ["zlib"], default-features = false }
mrt-rs = "1.1.3"
structopt = "0.3.9"
url = "2.1.1"
log = "0.4.8"
pretty_env_logger = "0.4.0"
[dependencies.reqwest]
version = "0.10.1"
features = ["blocking"]

86
README.md

@ -0,0 +1,86 @@
# asmap-rs
A tool to assist the [asmap](https://github.com/sipa/asmap) project read and parse RIS raw data from the [RIPE NCC](https://www.ripe.net/analyse/internet-measurements/routing-information-service-ris/ris-raw-data).
It may be extended to support other data sources.
The data is collected using Quagga routing software and stored in MRT format.
## Run
```
Parse mrt formatted files and find asn bottleneck
USAGE:
asmap-rs <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
find-bottleneck Reads and decompresses the MRT gz files, parses the AS Paths, determines the AS bottleneck, saves result
download Downloads and saves the MRT formatted gz files
help Prints this message or the help of the given subcommand(s)
```
### Download RIS Raw Data
```
asmap-rs-download 0.1.0
Downloads and saves the MRT formatted gz files
USAGE:
asmap-rs download [OPTIONS]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-o, --out <OUT> Directory to write MRT formatted gz files [default: dump]
-n, --ripe_collector_number <RIPE_COLLECTOR_NUMBER>...
Range of specific RIS files to download [default: [00, 24]]
```
#### Download RIS Raw Data Examples
Download all files from RIPE NCC (`rrc00-latest-bview.gz` through `rrc24-latest-bview.gz`) and saves the MRT formatted gz files to default `dump` directory.
```
cargo run --release download
```
Download `rrc03-latest-bview.gz` and `rrc14-latest-bview.gz` files from RIPE NCC and save the MRT formatted gz files to `dump-dir` directory.
Will create `dump-dir` if dne.
```
cargo run --release download -n 3,14 -o dump-dir
```
Download `rrc03-latest-bview.gz` file from RIPE NCC and save the MRT formatted gz files to default `dump` directory.
```
cargo run --release download -n 3
```
### Find ASN Bottleneck
```
asmap-rs find-bottleneck 0.1.0
Reads and decompresses the MRT gz files, parses the AS Paths, determines the AS bottleneck, saves result
USAGE:
asmap-rs find-bottleneck [OPTIONS]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-d, --dir <DIRECTORY> Directory path of the MRT formatted gz files to find bottleneck of
-o, --out <OUT> Directory to write result [default: print to stdout]
```
### Find Bottleneck ASN Example
Finds bottleneck from the data located in the `dump` and prints bottleneck results to stdout.
```
$ cargo run --release find-bottleneck -d dump
```
Finds bottleneck from the data located in the `dump` directory and writes the bottleneck results to `bottleneck/bottleneck.<epoch>.txt`.
```
$ cargo run --release find-bottleneck -d dump -o bottleneck
```

200
data/dump_01_2019-12-28

@ -1,200 +0,0 @@
223.255.245.0/24|31742 174 6453 4755 45820 45954 45954 45954 45954
223.255.245.0/24|35266 2914 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|8607 3356 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|2914 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|286 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|25160 3356 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|328145 174 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|64271 62240 1299 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|42473 1299 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|8896 3356 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|56730 3356 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|12779 2914 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|60501 3257 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|39122 174 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|207044 3356 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|57695 62240 1299 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|57111 6762 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|14630 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|51185 174 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|8218 6461 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|6894 174 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|31742 174 6453 4755 45820 45954 45954 45954 45954
223.255.246.0/24|35266 2914 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|8607 2914 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|2914 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|286 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|25160 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|328145 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|64271 62240 1299 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|42473 1299 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|8896 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|56730 3356 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|12779 2914 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|60501 3257 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|39122 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|207044 3356 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|57695 62240 1299 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|57111 6762 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|14630 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|51185 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|8218 6461 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|6894 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|31742 174 6453 4755 45820 45954 45954 45954 45954
223.255.247.0/24|35266 2914 6453 4755 45820 45954 45954 45954 45954
223.255.248.0/24|8607 2914 3491 63199
223.255.248.0/24|2914 6453 63199
223.255.248.0/24|13030 3491 63199
223.255.248.0/24|286 3491 63199
223.255.248.0/24|25160 174 6453 63199
223.255.248.0/24|328145 174 3491 63199
223.255.248.0/24|64271 62240 1299 3491 63199
223.255.248.0/24|42473 1299 3491 63199
223.255.248.0/24|8896 174 6453 63199
223.255.248.0/24|56730 3356 3491 63199
223.255.248.0/24|12779 2914 6453 63199
223.255.248.0/24|60501 3257 6453 63199
223.255.248.0/24|39122 174 3491 63199
223.255.248.0/24|207044 3356 3491 63199
223.255.248.0/24|57695 62240 1299 3491 63199
223.255.248.0/24|57111 6762 6453 63199
223.255.248.0/24|14630 8220 63199
223.255.248.0/24|51185 3356 6453 63199
223.255.248.0/24|8218 6461 6453 63199
223.255.248.0/24|6894 3257 6453 63199
223.255.248.0/24|31742 174 3491 63199
223.255.248.0/24|35266 2914 3491 63199
223.255.249.0/24|8607 2914 3491 63199
223.255.249.0/24|2914 6453 63199
223.255.249.0/24|13030 3491 63199
223.255.249.0/24|286 3491 63199
223.255.249.0/24|25160 174 6453 63199
223.255.249.0/24|328145 174 3491 63199
223.255.249.0/24|64271 62240 1299 3491 63199
223.255.249.0/24|42473 1299 3491 63199
223.255.249.0/24|8896 174 6453 63199
223.255.249.0/24|56730 3356 3491 63199
223.255.249.0/24|12779 2914 6453 63199
223.255.249.0/24|60501 3257 6453 63199
223.255.249.0/24|39122 174 3491 63199
223.255.249.0/24|207044 3356 3491 63199
223.255.249.0/24|57695 62240 1299 3491 63199
223.255.249.0/24|57111 6762 6453 63199
223.255.249.0/24|14630 8220 63199
223.255.249.0/24|51185 3356 6453 63199
223.255.249.0/24|8218 6461 6453 63199
223.255.249.0/24|6894 3257 6453 63199
223.255.249.0/24|31742 174 3491 63199
223.255.249.0/24|35266 2914 3491 63199
223.255.250.0/24|8607 2914 3491 63199
223.255.250.0/24|2914 6453 63199
223.255.250.0/24|13030 3491 63199
223.255.250.0/24|286 3491 63199
223.255.250.0/24|25160 174 6453 63199
223.255.250.0/24|328145 174 3491 63199
223.255.250.0/24|64271 62240 1299 3491 63199
223.255.250.0/24|42473 1299 3491 63199
223.255.250.0/24|8896 174 6453 63199
223.255.250.0/24|56730 3356 3491 63199
223.255.250.0/24|12779 2914 6453 63199
223.255.250.0/24|60501 3257 6453 63199
223.255.250.0/24|39122 174 3491 63199
223.255.250.0/24|207044 3356 3491 63199
223.255.250.0/24|57695 62240 1299 3491 63199
223.255.250.0/24|57111 6762 6453 63199
223.255.250.0/24|14630 8220 63199
223.255.250.0/24|51185 3356 6453 63199
223.255.250.0/24|8218 6461 6453 63199
223.255.250.0/24|6894 3257 6453 63199
223.255.250.0/24|31742 174 3491 63199
223.255.250.0/24|35266 2914 3491 63199
223.255.251.0/24|8607 2914 3491 63199
223.255.251.0/24|2914 6453 63199
223.255.251.0/24|13030 3491 63199
223.255.251.0/24|286 3491 63199
223.255.251.0/24|25160 174 6453 63199
223.255.251.0/24|328145 174 3491 63199
223.255.251.0/24|64271 62240 1299 3491 63199
223.255.251.0/24|42473 1299 3491 63199
223.255.251.0/24|8896 174 6453 63199
223.255.251.0/24|56730 3356 3491 63199
223.255.251.0/24|12779 2914 6453 63199
223.255.251.0/24|60501 3257 6453 63199
223.255.251.0/24|39122 174 3491 63199
223.255.251.0/24|207044 3356 3491 63199
223.255.251.0/24|57695 62240 1299 3491 63199
223.255.251.0/24|57111 6762 6453 63199
223.255.251.0/24|14630 8220 63199
223.255.251.0/24|51185 3356 6453 63199
223.255.251.0/24|8218 6461 6453 63199
223.255.251.0/24|6894 3257 6453 63199
223.255.251.0/24|31742 174 3491 63199
223.255.251.0/24|35266 2914 3491 63199
223.255.252.0/24|8607 2914 4134 58519
223.255.252.0/24|2914 4134 58519
223.255.252.0/24|286 4134 58519
223.255.252.0/24|25160 174 4134 58519
223.255.252.0/24|328145 174 4134 58519
223.255.252.0/24|64271 62240 1299 4134 58519
223.255.252.0/24|42473 1299 4134 58519
223.255.252.0/24|8896 174 4134 58519
223.255.252.0/24|56730 174 4134 58519
223.255.252.0/24|12779 174 4134 58519
223.255.252.0/24|60501 4134 58519
223.255.252.0/24|39122 1299 4134 58519
223.255.252.0/24|207044 3257 4134 58519
223.255.252.0/24|57111 6762 4134 58519
223.255.252.0/24|14630 6461 4134 58519
223.255.252.0/24|51185 1273 4134 58519
223.255.252.0/24|8218 4134 58519
223.255.252.0/24|6894 174 4134 58519
223.255.252.0/24|31742 174 4134 58519
223.255.252.0/24|35266 2914 4134 58519
223.255.253.0/24|8607 2914 4134 58519
223.255.253.0/24|2914 4134 58519
223.255.253.0/24|286 4134 58519
223.255.253.0/24|25160 1299 4134 58519
223.255.253.0/24|328145 9498 6762 4134 58519
223.255.253.0/24|64271 62240 1299 4134 58519
223.255.253.0/24|42473 1299 4134 58519
223.255.253.0/24|8896 1299 4134 58519
223.255.253.0/24|56730 51945 1299 4134 58519
223.255.253.0/24|12779 2914 4134 58519
223.255.253.0/24|60501 4134 58519
223.255.253.0/24|39122 1299 4134 58519
223.255.253.0/24|207044 3257 4134 58519
223.255.253.0/24|57111 6762 4134 58519
223.255.253.0/24|14630 6461 4134 58519
223.255.253.0/24|51185 1273 4134 58519
223.255.253.0/24|8218 4134 58519
223.255.253.0/24|6894 3257 4134 58519
223.255.253.0/24|31742 1299 4134 58519
223.255.253.0/24|35266 2914 4134 58519
223.255.254.0/24|8607 7473 3758 55415
223.255.254.0/24|28792 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|2914 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|13030 7473 3758 55415
223.255.254.0/24|4637 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|286 7473 3758 55415
223.255.254.0/24|6461 7473 3758 55415
223.255.254.0/24|25160 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|328145 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|64271 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|42473 1299 7473 3758 55415
223.255.254.0/24|8896 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|37721 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|56730 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|12779 6461 7473 3758 55415
223.255.254.0/24|60501 6461 7473 3758 55415
223.255.254.0/24|39122 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|207044 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|57695 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|57111 174 3257 7473 3758 55415
223.255.254.0/24|14630 6461 7473 3758 55415
223.255.254.0/24|37680 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|51185 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|28792 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|8218 6461 7473 3758 55415
223.255.254.0/24|6894 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|31742 4657 55415 55415 55415 55415 55415 55415 55415
223.255.254.0/24|35266 4657 55415 55415 55415 55415 55415 55415 55415

42
justfile

@ -1,26 +1,38 @@
test:
cargo watch --clear --exec test
alias b := build
alias br := build-release
alias c := check
alias t := test
alias tc := test-crate
alias rc := run-cmd
build:
cargo watch --clear --exec run
build-release:
cargo build --release
check:
cargo watch --clear --exec check
cargo watch --clear --exec check
test-print:
cargo test -- --nocapture
test:
cargo watch --clear --ignore dump --shell "cargo test -- --nocapture"
build:
cargo build --release
test-crate CRATE:
RUST_LOG=debug cargo watch --clear --shell "cargo test {{CRATE}} -- --nocapture"
run:
cargo watch --clear --exec run
run-cmd CMD:
cargo watch --clear --ignore dump --ignore data --shell "cargo run {{CMD}}"
run-cmd-r CMD:
cargo watch --clear --ignore dump --ignore data --shell "cargo run --release {{CMD}}"
# clean up feature branch BRANCH
done BRANCH:
git checkout master
git diff --no-ext-diff --quiet --exit-code
git pull --rebase origin master
git diff --no-ext-diff --quiet --exit-code {{BRANCH}}
git branch -D {{BRANCH}}
git checkout master
git diff --no-ext-diff --quiet --exit-code
git pull --rebase origin master
git diff --no-ext-diff --quiet --exit-code {{BRANCH}}
git branch -D {{BRANCH}}
publish:
cargo build

23
src/address.rs

@ -1,6 +1,6 @@
pub(crate) use crate::common::*;
use crate::common::*;
#[derive(Debug, PartialEq, Eq, Hash)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub(crate) struct Address {
pub(crate) ip: IpAddr,
pub(crate) mask: u8,
@ -36,7 +36,7 @@ mod tests {
use super::*;
#[test]
fn address_from_str() -> Result<(), Error> {
fn address_from_str_ipv4() -> Result<(), Error> {
let ip = "127.0.0.1";
let mask = 23;
let ip_and_mask = format!("{}/{}", ip, mask);
@ -51,4 +51,21 @@ mod tests {
Ok(())
}
#[test]
fn address_from_str_ipv6() -> Result<(), Error> {
let ip = "2001::";
let mask = 32;
let ip_and_mask = format!("{}/{}", ip, mask);
let want = Address {
ip: IpAddr::from_str(ip).unwrap(),
mask,
};
let have = Address::from_str(&ip_and_mask).unwrap();
assert_eq!(have, want);
Ok(())
}
}

313
src/as_path_parser.rs

@ -0,0 +1,313 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
pub(crate) struct AsPathParser<'buffer> {
buffer: &'buffer [u8],
next: usize,
}
impl<'buffer> AsPathParser<'buffer> {
/// Given a `buffer` with lifetime `'buffer`, constructs a new `AsPathParser` and parses the
/// attributes.
pub(crate) fn parse(buffer: &'buffer [u8]) -> Result<Vec<u32>> {
if buffer.is_empty() {
info!("Error::MissingPathAttribute, buffer: {:?}", buffer);
return Err(Error::MissingPathAttribute {
missing_attribute: "all attributes".to_string(),
});
}
Self::new(buffer).parse_attributes()
}
/// Given a `buffer` with lifetime `'buffer`, constructs a new `AsPathParser`
fn new(buffer: &'buffer [u8]) -> AsPathParser {
AsPathParser { next: 0, buffer }
}
/// Advances forward one in the buffer and returns that byte. Error if `buffer` is already exhausted.
fn advance(&mut self) -> Result<u8> {
if self.done() {
info!("Error::UnexpectedEndOfBuffer {:?}", &self.buffer);
Err(Error::UnexpectedEndOfBuffer)
} else {
let byte = self.buffer[self.next];
self.next += 1;
Ok(byte)
}
}
/// Advances forward four in the buffer and returns the fours bytes as a u32.
fn parse_u32(&mut self) -> Result<u32> {
let a = self.advance()?;
let b = self.advance()?;
let c = self.advance()?;
let d = self.advance()?;
Ok(u32::from_be_bytes([a, b, c, d]))
}
/// Returns true if buffer is exhausted, false otherwise.
fn done(&self) -> bool {
self.next == self.buffer.len()
}
fn parse_attributes(mut self) -> Result<Vec<u32>> {
let mut paths = Vec::new();
while !self.done() {
if let Some(path) = self.parse_attribute()? {
// if there are no asn's in the as path
if path.is_empty() {
info!("Error::NoAsPathInAttributePath {:?}", &self.buffer);
return Err(Error::NoAsPathInAttributePath);
}
paths.push(path);
}
}
if paths.len() > 1 {
// Too many asn paths in path attributes
info!("Error::MultipleAsPaths {:?}", &self.buffer);
Err(Error::MultipleAsPaths)
} else if let Some(path) = paths.pop() {
Ok(path)
} else {
info!("Error::NoAsPathInAttributePath{:?}", &self.buffer);
Err(Error::NoAsPathInAttributePath)
}
}
/// Parses all attributes. If attribute is AS_PATH, p
fn parse_attribute(&mut self) -> Result<Option<Vec<u32>>> {
let flag = self.advance()?;
let type_code = self.advance()?;
let mut attribute_length: u16 = self.advance()?.into();
if (flag >> 4) & 1 == 1 {
attribute_length <<= 8;
attribute_length |= self.advance()? as u16;
}
if type_code == 2 {
let asn_attr_position_end = &self.next + attribute_length as usize;
let asn_path = self.parse_as_path();
if asn_attr_position_end != self.next {
let leftover_attr_count = asn_attr_position_end - self.next;
for _ in 0..leftover_attr_count {
self.advance()?;
}
}
asn_path
} else {
for _ in 0..attribute_length {
self.advance()?;
}
Ok(None)
}
}
/// Takes self and returns a vec of asn
fn parse_as_path(&mut self) -> Result<Option<Vec<u32>>> {
let as_set_indicator = self.advance()?;
// Determine if asn's are listed as an unordered AS_SET (1) or an ordered AS_SEQUENCE (2)
// Only add asn's to as_path vector if they are listed in an ordered AS_SEQUENCE
match as_set_indicator {
1 => {
println!("AS_SET's are not factored into the bottleneck calculation.");
let num_asn = self.advance()?;
for _ in 0..num_asn {
self.parse_u32()?;
}
Ok(None)
}
2 => {
let mut as_path = Vec::new();
let num_asn = self.advance()?;
for _ in 0..num_asn {
as_path.push(self.parse_u32()?);
}
Ok(Some(as_path))
}
_ => Err(Error::UnknownAsValue {
unknown_as_value: as_set_indicator,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_new_as_path_parser() -> Result<()> {
let buffer = &[0, 1, 2, 3, 4];
let want = AsPathParser {
buffer: buffer,
next: 0,
};
let have = AsPathParser::new(buffer);
assert_eq!(want, have);
Ok(())
}
#[test]
fn returns_true_if_buffer_exhausted() -> Result<()> {
let want = true;
let have = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 5,
}
.done();
assert_eq!(want, have);
Ok(())
}
#[test]
fn returns_false_if_buffer_has_contents() -> Result<()> {
let have = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 1,
}
.done();
let want = false;
assert_eq!(want, have);
Ok(())
}
#[test]
fn advances_buffer_one_forward() -> Result<()> {
let want = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 1,
};
let mut have = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 0,
};
let next_byte = have.advance()?;
assert_eq!(have, want);
assert_eq!(0, next_byte);
Ok(())
}
#[test]
fn advances_buffer_four_forward_returns_u32() -> Result<()> {
let want = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 4,
};
let mut have = AsPathParser {
buffer: &[0, 1, 2, 3, 4],
next: 0,
};
let new_u32 = have.parse_u32()?;
assert_eq!(have, want);
assert_eq!(66051, new_u32);
Ok(())
}
#[test]
fn parses_attributes_0() -> Result<()> {
let bgp_attributes = &[
64, 1, 1, 0, 80, 2, 0, 10, 2, 2, 0, 0, 251, 15, 0, 0, 243, 32, 64, 3, 4, 195, 66, 225,
77,
];
let have = AsPathParser::parse(bgp_attributes)?;
let want = &[64271u32, 62240u32];
assert_eq!(have, want);
Ok(())
}
#[test]
fn parses_attributes_1() -> Result<(), Error> {
let bgp_attributes = &[
64, 1, 1, 0, 64, 2, 14, 2, 3, 0, 0, 12, 231, 0, 0, 50, 74, 0, 3, 49, 30, 64, 3, 4, 195,
66, 224, 110, 192, 8, 28, 12, 231, 3, 232, 12, 231, 3, 238, 12, 231, 3, 252, 12, 231,
12, 21, 50, 74, 2, 188, 50, 74, 3, 243, 50, 74, 11, 210,
];
let have = AsPathParser::parse(bgp_attributes)?;
let want = &[3303, 12874, 209182];
assert_eq!(have, want);
Ok(())
}
#[test]
fn parses_attributes_2() -> Result<(), Error> {
let bgp_attributes = &[
64, 1, 1, 0, 64, 2, 10, 2, 2, 0, 0, 165, 233, 0, 0, 5, 19, 64, 3, 4, 195, 66, 226, 113,
128, 4, 4, 0, 0, 0, 0, 192, 8, 24, 184, 43, 5, 222, 184, 43, 7, 208, 184, 43, 8, 64,
184, 43, 8, 252, 184, 43, 9, 112, 184, 43, 10, 40,
];
let have = AsPathParser::parse(bgp_attributes)?;
let want = &[42473u32, 1299u32];
assert_eq!(have, want);
Ok(())
}
#[test]
fn parses_attributes_3() -> Result<(), Error> {
let bgp_attributes = &[
64, 1, 1, 0, 80, 2, 0, 10, 2, 2, 0, 2, 1, 149, 0, 0, 229, 255, 64, 3, 4, 103, 102, 5,
1, 192, 16, 8, 2, 2, 0, 2, 1, 149, 0, 200,
];
let have = AsPathParser::parse(bgp_attributes)?;
let want = &[131477u32, 58879u32];
assert_eq!(have, want);
Ok(())
}
#[test]
fn parse_long_as_path_attr() -> Result<(), Error> {
let bgp_attributes = &[
64, 1, 1, 2, 64, 2, 24, 2, 4, 0, 0, 58, 59, 0, 0, 11, 98, 0, 0, 25, 53, 0, 0, 50, 49,
1, 1, 0, 0, 50, 49, 64, 3, 4, 27, 111, 228, 186, 192, 7, 8, 0, 0, 50, 49, 213, 57, 3,
1, 192, 8, 32, 11, 98, 1, 164, 11, 98, 5, 125, 11, 98, 9, 102, 11, 98, 13, 72, 25, 53,
7, 208, 25, 53, 8, 52, 25, 53, 8, 57, 58, 59, 0, 4,
];
// 0, 0, 58, 59, 0, 0, 11, 98, 0, 0, 25, 53, 0, 0, 50, 49,
let have = AsPathParser::parse(bgp_attributes)?;
let want = &[14907u32, 2914u32, 6453u32, 12849u32];
assert_eq!(have, want);
Ok(())
}
#[ignore]
#[test]
fn returns_err_if_buffer_empty() -> Result<()> {
let _have = match AsPathParser::parse(&[]) {
Ok(_) => panic!("exepceted errr"),
Err(e) => e,
};
let _want = Error::MissingPathAttribute {
missing_attribute: "all attributes".to_string(),
};
// assert_eq!(want, have);
Ok(())
}
}

90
src/bgp_path.rs

@ -1,90 +0,0 @@
pub(crate) use crate::common::*;
#[derive(Debug, PartialEq)]
pub(crate) struct BGPPath {
pub addr: Address,
pub as_path: Vec<u32>,
}
impl FromStr for BGPPath {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Self::Err> {
match text.find('|') {
Some(_) => (),
None => {
return Err(Error::NoPipe {
bad_quagga: text.to_owned(),
})
}
};
//
// TODO: Count number of fields and if num fields is wrong throw error instead of splitting
// first and then counting vector length
// Get Address from IP and mask str
let record_vec: Vec<&str> = text.split('|').collect();
if record_vec.len() != 2 {
panic!("TODO: handle err for could not parse line correctly");
}
let addr = Address::from_str(record_vec[0])?;
// Get BGPPath from ASN array
let as_vec_str: Vec<&str> = record_vec[1].split(' ').collect();
let mut as_path: Vec<u32> = as_vec_str
.into_iter()
.map(|s| {
s.parse().map_err(|e| Error::ParseInt {
bad_num: s.to_string(),
error: e,
})
})
.collect::<Result<Vec<u32>, Error>>()?;
as_path.dedup();
Ok(BGPPath { addr, as_path })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
// "223.255.245.0/24|31742 174 6453 4755 45820 45954 45954 45954 45954"
fn bgp_path_from_str() -> Result<(), Error> {
let ip = "223.255.245.0";
let mask = 24;
let asn_list = "31742 174 6453 4755 45820 45954 45954 45954 45954";
let text = format!("{}/{}|{}", ip, mask, asn_list);
let have = BGPPath::from_str(&text).unwrap();
let addr = Address {
ip: IpAddr::from_str(ip).unwrap(),
mask,
};
let asn_list_dedup: Vec<u32> = vec![31742, 174, 6453, 4755, 45820, 45954];
let want = BGPPath {
addr,
as_path: asn_list_dedup,
};
assert_eq!(have, want);
Ok(())
}
#[test]
fn parse_int() {
let text = "223.255.245.0/24|31742 174 4294967296 4755 45820 45954 45954 45954 45954";
let have = BGPPath::from_str(&text).unwrap_err();
match have {
Error::ParseInt { .. } => {}
_ => panic!("Expected BGPPath ParseInt Error type"),
};
}
}

22
src/common.rs

@ -1,9 +1,23 @@
pub(crate) use crate::{address::Address, bgp_path::BGPPath, error::Error};
pub(crate) use std::collections::{HashMap, HashSet};
pub(crate) use std::fs::File;
pub(crate) use std::io::{BufRead, BufReader};
pub(crate) use std::{
collections::{HashMap, HashSet},
fmt::{self, Display, Formatter},
fs::{self, File},
io::{self, prelude::*, BufReader, BufWriter},
net::IpAddr,
path::{Path, PathBuf},
str::FromStr,
time::SystemTime,
};
pub(crate) use flate2::read::GzDecoder;
pub(crate) use log::*;
pub(crate) use mrt_rs::{tabledump::TABLE_DUMP_V2, Reader, Record};
pub(crate) use pretty_env_logger;
pub(crate) use structopt::StructOpt;
pub(crate) use crate::{
address::Address, as_path_parser::AsPathParser, error::Error, find_bottleneck::FindBottleneck,
opt::Opt, subcommand::Subcommand,
};
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;

55
src/error.rs

@ -1,13 +1,10 @@
pub(crate) use crate::common::*;
use crate::common::*;
#[derive(Debug)]
pub(crate) enum Error {
pub enum Error {
IoError {
io_error: std::io::Error,
},
ParseInt {
bad_num: String,
error: std::num::ParseIntError,
path: PathBuf,
},
AddrParse {
addr_parse: std::net::AddrParseError,
@ -16,32 +13,56 @@ pub(crate) enum Error {
NoSlash {
bad_addr: String,
},
NoPipe {
bad_quagga: String,
Reqwest {
url: String,
reqwest_error: reqwest::Error,
},
MissingPathAttribute {
missing_attribute: String,
},
UnknownAsValue {
unknown_as_value: u8,
},
UnexpectedEndOfBuffer,
MultipleAsPaths,
NoAsPathInAttributePath,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Error::*;
match self {
Self::ParseInt { bad_num, error } => {
write!(f, "Failed to parse u32 `{}`: {}", bad_num, error)
}
Self::AddrParse {
AddrParse {
addr_parse,
bad_addr,
} => write!(f, "Invalid address, {}: {}", addr_parse, bad_addr),
Self::NoSlash { bad_addr } => write!(
NoSlash { bad_addr } => write!(
f,
"Invalid IP and mask: {}. Missing `/`, expected format `IP/mask`",
bad_addr
),
Self::NoPipe { bad_quagga } => write!(
IoError { io_error, path } => {
write!(f, "I/O error at `{}`: {}", path.display(), io_error)
}
Reqwest { url, reqwest_error } => {
write!(f, "Failed request for {}. {}", url, reqwest_error)
}
MissingPathAttribute { missing_attribute } => {
write!(f, "Invalid mrt entry. Missing {}.", missing_attribute)
}
UnknownAsValue { unknown_as_value } => write!(
f,
"Did not recognize as path value `{}`, expected AS_SET (1) or AS_SEQUENCE (2).",
unknown_as_value
),
UnexpectedEndOfBuffer => write!(f, "Expected another byte but buffer is exhausted."),
NoAsPathInAttributePath => {
write!(f, "Expected an AS_PATH attribute in BGP Attribute Path.")
}
MultipleAsPaths => write!(
f,
"Invalid quagga data line: {}. Missing `|`, expected format `IP/mask|<asn list>`",
bad_quagga
"Expected one AS_PATH attribute in BGP Attribute Path, found multiple."
),
Self::IoError { io_error } => write!(f, "I/O error: {}", io_error),
}
}
}

343
src/find_bottleneck.rs

@ -0,0 +1,343 @@
use crate::common::*;
/// Contains the mapping of each prefix to its bottleneck asn.
#[derive(Debug, PartialEq)]
pub(crate) struct FindBottleneck {
prefix_asn: HashMap<Address, u32>,
}
impl FindBottleneck {
/// Creates a new `FindBottleneck`, reads and parses mrt files, locates prefix and asn bottleneck
pub(crate) fn locate(dir: &PathBuf) -> Result<Self> {
let mut mrt_hm = HashMap::new();
// Walk the directory and read its contents
if dir.is_dir() {
for entry in fs::read_dir(dir).map_err(|io_error| Error::IoError {
io_error,
path: "path".into(),
})? {
let entry = entry.map_err(|io_error| Error::IoError {
io_error,
path: "path".into(),
})?;
let path = entry.path();
println!("Reading in and parsing `{}`", &path.display());
let buffer =
BufReader::new(File::open(&path).map_err(|io_error| Error::IoError {
io_error,
path: path.into(),
})?);
let mut decoder = GzDecoder::new(buffer);
Self::parse_mrt(&mut decoder, &mut mrt_hm)?;
}
}
let mut bottleneck = FindBottleneck {
prefix_asn: HashMap::new(),
};
bottleneck.find_as_bottleneck(&mut mrt_hm)?;
Ok(bottleneck)
}
/// Creates a mapping between a prefix and all of its asn paths, gets the common asns from
/// those paths, and considers the last asn (the asn farthest from the originating hop) from
/// the common asns to be the bottleneck.
fn find_as_bottleneck(
&mut self,
mrt_hm: &mut HashMap<Address, HashSet<Vec<u32>>>,
) -> Result<(), Error> {
// In the vector value, the first element is the final AS (so the actual AS of the IP,
// not some AS on the path). The last element is the critical AS on the path that
// determines the bottleneck.
let mut prefix_to_common_suffix: HashMap<Address, Vec<u32>> = HashMap::new();
Self::find_common_suffix(mrt_hm, &mut prefix_to_common_suffix)?;
for (addr, mut as_path) in prefix_to_common_suffix {
let asn = match as_path.pop() {
Some(a) => a,
None => panic!("ERROR: No ASN"), // TODO: Handle error
};
self.prefix_asn.insert(addr, asn);
}
Ok(())
}
/// Logic that finds the mapping of each prefix and the asns common to all of the prefix's asn
/// paths.
fn find_common_suffix(
mrt_hm: &mut HashMap<Address, HashSet<Vec<u32>>>,
prefix_to_common_suffix: &mut HashMap<Address, Vec<u32>>,
) -> Result<(), Error> {
'outer: for (prefix, as_paths) in mrt_hm.iter() {
let mut as_paths_sorted: Vec<&Vec<u32>> = as_paths.iter().collect();
as_paths_sorted.sort_by(|a, b| a.len().cmp(&b.len())); // descending
let mut rev_common_suffix: Vec<u32> = as_paths_sorted[0].to_vec();
rev_common_suffix.reverse();
for as_path in as_paths_sorted.iter().skip(1) {
// first one is already in rev_common_suffix
let mut rev_as_path: Vec<u32> = as_path.to_vec();
rev_as_path.reverse();
// Every IP should always belong to only one AS
if rev_common_suffix.first() != rev_as_path.first() {
warn!(
"Every IP should belong to one AS. Prefix: `{:?}` has anomalous AS paths: `{:?}`.",
&prefix, &as_paths
);
continue 'outer;
}
// first element is already checked
for i in 1..rev_common_suffix.len() {
if rev_as_path[i] != rev_common_suffix[i] {
rev_common_suffix.truncate(i);
break;
}
}
}
// rev_common_suffix.reverse();
prefix_to_common_suffix
.entry(*prefix)
.or_insert(rev_common_suffix);
}
Ok(())
}
/// Parses the mrt formatted data, extracting the pertinent `PEER_INDEX_TABLE` values
/// containing the prefix and associated as paths.
fn parse_mrt(
reader: &mut dyn Read,
mrt_hm: &mut HashMap<Address, HashSet<Vec<u32>>>,
) -> Result<()> {
let mut reader = Reader { stream: reader };
loop {
match reader.read() {
Ok(header_record) => match header_record {
Some((_, record)) => match record {
Record::TABLE_DUMP_V2(tdv2_entry) => match tdv2_entry {
TABLE_DUMP_V2::RIB_IPV4_UNICAST(entry) => {
let ip = Self::format_ip(&entry.prefix, true)?;
let mask = entry.prefix_length;
Self::match_rib_entry(entry.entries, ip, mask, mrt_hm)?;
}
TABLE_DUMP_V2::RIB_IPV6_UNICAST(entry) => {
let ip = Self::format_ip(&entry.prefix, false)?;
let mask = entry.prefix_length;
Self::match_rib_entry(entry.entries, ip, mask, mrt_hm)?;
}
_ => continue,
},
_ => continue,
},
None => break,
},
Err(e) => match e.kind() {
std::io::ErrorKind::InvalidInput => {
println!("Invalid gzip header. Skipping file.")
}
other_error => println!(
"Problem with gzip mrt file. `{:?}`. Skipping file.",
other_error
),
},
}
}
Ok(())
}
/// Format IPV4 and IPV6 from slice.
fn format_ip(ip: &[u8], is_ipv4: bool) -> Result<IpAddr> {
let pad = &[0; 17];
let ip = [ip, pad].concat();
if is_ipv4 {
Ok(IpAddr::V4(std::net::Ipv4Addr::new(
ip[0], ip[1], ip[2], ip[3],
)))
} else {
Ok(IpAddr::V6(std::net::Ipv6Addr::new(
u16::from_be_bytes([ip[0], ip[1]]),
u16::from_be_bytes([ip[2], ip[3]]),
u16::from_be_bytes([ip[4], ip[5]]),
u16::from_be_bytes([ip[7], ip[8]]),
u16::from_be_bytes([ip[9], ip[10]]),
u16::from_be_bytes([ip[11], ip[12]]),
u16::from_be_bytes([ip[13], ip[14]]),
u16::from_be_bytes([ip[15], ip[16]]),
)))
}
}
/// Parse each RIB Entry.
fn match_rib_entry(
entries: Vec<mrt_rs::records::tabledump::RIBEntry>,
ip: IpAddr,
mask: u8,
mrt_hm: &mut HashMap<Address, HashSet<Vec<u32>>>,
) -> Result<()> {
let addr = Address { ip, mask };
for rib_entry in entries {
match AsPathParser::parse(&rib_entry.attributes) {
Ok(mut as_path) => {
as_path.dedup();
mrt_hm
.entry(addr)
.or_insert_with(HashSet::new)
.insert(as_path);
}
Err(e) => info!("ERROR: {:?}. ", e), // TODO: Handle error
};
}
Ok(())
}
/// Writes the asn bottleneck result to a stdout or a time stamped file
pub(crate) fn write(self, out: Option<&Path>) -> Result<()> {
if let Some(path) = out {
let epoch = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let now = epoch.as_secs();
let dst = path.join(format!("bottleneck.{}.txt", now));
let mut file = File::create(&dst).map_err(|io_error| Error::IoError {
io_error,
path: dst.to_path_buf(),
})?;
self.write_bottleneck(&mut file)?;
} else {
self.write_bottleneck(&mut io::stdout())?;
};
Ok(())
}
/// Helper write function
fn write_bottleneck(self, out: &mut dyn Write) -> Result<(), Error> {
for (key, value) in self.prefix_asn {
let text = format!("{}/{}|{:?}", key.ip, key.mask, value);
writeln!(out, "{}", &text).unwrap();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_mrt_hm() -> Result<HashMap<Address, HashSet<Vec<u32>>>, Error> {
let mut mrt_hm: HashMap<Address, HashSet<Vec<u32>>> = HashMap::new();
let ip_str = "1.0.139.0";
let addr = Address {
ip: IpAddr::from_str(ip_str).map_err(|addr_parse| Error::AddrParse {
addr_parse,
bad_addr: ip_str.to_string(),
})?,
mask: 24,
};
let mut asn_paths = HashSet::new();
asn_paths.insert(vec![2497, 38040, 23969]);
asn_paths.insert(vec![25152, 6939, 4766, 38040, 23969]);
asn_paths.insert(vec![4777, 6939, 4766, 38040, 23969]);
mrt_hm.insert(addr, asn_paths);
let ip_str = "1.0.204.0";
let addr = Address {
ip: IpAddr::from_str(ip_str).map_err(|addr_parse| Error::AddrParse {
addr_parse,
bad_addr: ip_str.to_string(),
})?,
mask: 22,
};
let mut asn_paths = HashSet::new();
asn_paths.insert(vec![2497, 38040, 23969]);
asn_paths.insert(vec![4777, 6939, 4766, 38040, 23969]);
asn_paths.insert(vec![25152, 2914, 38040, 23969]);
mrt_hm.insert(addr, asn_paths);
let ip_str = "1.0.6.0";
let addr = Address {
ip: IpAddr::from_str(ip_str).map_err(|addr_parse| Error::AddrParse {
addr_parse,
bad_addr: ip_str.to_string(),
})?,
mask: 24,
};
let mut asn_paths = HashSet::new();
asn_paths.insert(vec![2497, 4826, 38803, 56203]);
asn_paths.insert(vec![25152, 6939, 4826, 38803, 56203]);
asn_paths.insert(vec![4777, 6939, 4826, 38803, 56203]);
mrt_hm.insert(addr, asn_paths);
Ok(mrt_hm)
}
#[test]
fn finds_common_suffix_from_mrt_hashmap() -> Result<(), Error> {
let mut want: HashMap<Address, Vec<u32>> = HashMap::new();
want.insert(Address::from_str("1.0.139.0/24")?, vec![23969, 38040]);
want.insert(Address::from_str("1.0.204.0/22")?, vec![23969, 38040]);
want.insert(Address::from_str("1.0.6.0/24")?, vec![56203, 38803, 4826]);
let mut mrt_hm = setup_mrt_hm()?;
let mut have: HashMap<Address, Vec<u32>> = HashMap::new();
assert_eq!(
FindBottleneck::find_common_suffix(&mut mrt_hm, &mut have)?,
()
);
assert_eq!(have, want);
Ok(())
}
#[test]
fn finds_as_bottleneck_from_mrt_hashmap() -> Result<(), Error> {
let mut want = FindBottleneck {
prefix_asn: HashMap::new(),
};
want.prefix_asn
.insert(Address::from_str("1.0.139.0/24")?, 38040);
want.prefix_asn
.insert(Address::from_str("1.0.204.0/22")?, 38040);
want.prefix_asn
.insert(Address::from_str("1.0.6.0/24")?, 4826);
let mut have = FindBottleneck {
prefix_asn: HashMap::new(),
};
let mut mrt_hm = setup_mrt_hm()?;
have.find_as_bottleneck(&mut mrt_hm)?;
assert_eq!(have, want);
Ok(())
}
#[test]
fn ipaddr_from_ipv6_short() -> Result<(), Error> {
let have = FindBottleneck::format_ip(&[32, 1, 3, 24], false)?;
assert_eq!("2001:318::".parse(), Ok(have));
Ok(())
}
#[test]
fn ipaddr_from_ipv6_long() -> Result<(), Error> {
let have = FindBottleneck::format_ip(&[32, 1, 2, 248, 16, 8], false)?;
assert_eq!("2001:2f8:1008::".parse(), Ok(have));
Ok(())
}
}

58
src/main.rs

@ -1,53 +1,13 @@
mod address;
mod bgp_path;
mod as_path_parser;
mod common;
mod error;
pub(crate) use crate::common::*;
fn main() -> Result<(), Error> {
let path = "./data/dump_01_2019-12-28";
// let input = File::open(path).unwrap();
let input = File::open(path).map_err(|error| Error::IoError { io_error: error })?;
let buffered = BufReader::new(input);
let mut prefix_to_as_paths: HashMap<Address, HashSet<Vec<u32>>> = HashMap::new();
for line in buffered.lines() {
let l = line.unwrap();
let bgp_line = BGPPath::from_str(&l)?;
prefix_to_as_paths
.entry(bgp_line.addr)
.or_insert_with(HashSet::new)
.insert(bgp_line.as_path);
}
let mut prefix_to_common_suffix: HashMap<&Address, Vec<u32>> = HashMap::new();
for (prefix, as_paths) in prefix_to_as_paths.iter() {
let mut as_paths_sorted: Vec<&Vec<u32>> = as_paths.iter().collect();
as_paths_sorted.sort_by(|a, b| a.len().cmp(&b.len())); // descending
let mut rev_common_suffix: Vec<u32> = as_paths_sorted[0].to_vec();
rev_common_suffix.reverse();
for as_path in as_paths_sorted.iter().skip(1) {
// first one is already in rev_common_suffix
let mut rev_as_path: Vec<u32> = as_path.to_vec();
rev_as_path.reverse();
// every IP should always belong to only one AS
assert!(rev_common_suffix.first() == rev_as_path.first());
// first element is already checked
for i in 1..rev_common_suffix.len() {
if rev_as_path[i] != rev_common_suffix[i] {
rev_common_suffix.truncate(i);
break;
}
}
}
rev_common_suffix.reverse();
prefix_to_common_suffix
.entry(prefix)
.or_insert(rev_common_suffix);
}
Ok(())
mod find_bottleneck;
mod opt;
mod subcommand;
use crate::common::*;
fn main() -> Result<()> {
pretty_env_logger::init();
Opt::from_args().run()
}

38
src/opt.rs

@ -0,0 +1,38 @@
use crate::common::*;
#[derive(Debug, PartialEq, StructOpt)]
#[structopt(
name = "asmap",
about = "Parse mrt formatted files and find asn bottleneck"
)]
pub(crate) struct Opt {
#[structopt(subcommand)]
pub(crate) cmd: Subcommand,
}
impl Opt {
pub(crate) fn run(self) -> Result<(), Error> {
self.cmd.run()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_download_basic() -> Result<(), structopt::clap::Error> {
let have =
Opt::from_iter_safe(vec!["asmap", "download", "--ripe_collector_number", "1,2"])?;
let want = Opt {
cmd: Subcommand::Download {
out: "dump".into(),
ripe_collector_number: vec![1, 2],
},
};
assert_eq!(have, want);
Ok(())
}
}

93
src/subcommand.rs

@ -0,0 +1,93 @@
use crate::common::*;
#[derive(Debug, PartialEq, StructOpt)]
pub(crate) enum Subcommand {
/// Downloads and saves the MRT formatted gz files
Download {
/// Range of specific RIS files to download [default: [00, 24]]
#[structopt(
name = "RIPE_COLLECTOR_NUMBER",
long = "ripe_collector_number",
short = "n",
use_delimiter(true)
)]
ripe_collector_number: Vec<u32>,
/// Directory to write MRT formatted gz files
#[structopt(name = "OUT", long = "out", short = "o", default_value = "dump")]
out: PathBuf,
},
/// Reads and decompresses the MRT gz files, parses the AS Paths, determines the AS bottleneck, saves result
FindBottleneck {
/// Directory path of the MRT formatted gz files to find bottleneck of
#[structopt(name = "DIRECTORY", long = "dir", short = "d")]
dir: PathBuf,
/// Directory to write result [default: print to stdout]
#[structopt(name = "OUT", long = "out", short = "o")]
out: Option<PathBuf>,
},
}
impl Subcommand {
pub(crate) fn run(self) -> Result<(), Error> {
match self {
Self::Download {
out,
ripe_collector_number,
} => Self::download(&out, &ripe_collector_number),
Self::FindBottleneck { dir, out } => Self::find_bottleneck(&dir, out.as_deref()),
}
}
/// Downloads the gz file from data.ris.ripe.net and save to the corresponding directory.
fn download(out: &Path, ripe_collector_number: &[u32]) -> Result<()> {
// Create target directory
fs::create_dir_all(out).map_err(|io_error| Error::IoError {
io_error,
path: out.into(),
})?;
if ripe_collector_number.is_empty() {
for i in 0..=24 {
Self::download_file(out, i)?;
}
} else {
for i in ripe_collector_number {
Self::download_file(out, *i)?;
}
}
Ok(())
}
fn download_file(out: &Path, number: u32) -> Result<()> {
let url = format!("http://data.ris.ripe.net/rrc{:02}/latest-bview.gz", number);
let mut res = reqwest::blocking::get(&url).map_err(|reqwest_error| Error::Reqwest {
url: url.to_string(),
reqwest_error,
})?;
let dst = out.join(format!("rrc{:02}-latest-bview.gz", number));
let file = File::create(&dst).map_err(|io_error| Error::IoError {
io_error,
path: dst.to_path_buf(),
})?;
let mut buf_write = BufWriter::new(file);
io::copy(&mut res, &mut buf_write).map_err(|io_error| Error::IoError {
io_error,
path: out.to_path_buf(),
})?;
Ok(())
}
/// Reads gz mrt data from urls defined by range, decompresses them, parses mrt output, finds bottleneck.
fn find_bottleneck(dump: &PathBuf, out: Option<&Path>) -> Result<()> {
let bottleneck = FindBottleneck::locate(dump)?;
bottleneck.write(out)?;
Ok(())
}
}
Loading…
Cancel
Save