├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── build.yaml ├── examples ├── read_caida_as.rs ├── all_paths.rs ├── all_paths_between_two_ases.rs ├── shortest_path_between_two_ases.rs └── draw_path_graph.rs ├── Cargo.toml ├── LICENSE ├── README.md ├── images ├── path_topology.svg └── base_topology.svg └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: bgpkit 4 | -------------------------------------------------------------------------------- /examples/read_caida_as.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use valley_free::Topology; 4 | 5 | fn main() { 6 | let file = File::open("20231201.as-rel.txt").unwrap(); 7 | let topo = Topology::from_caida(file).unwrap(); 8 | 9 | println!("Number of ases: {}", topo.graph.node_count()); 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Build default 21 | run: cargo build 22 | 23 | - name: Run test for lib feature 24 | run: cargo test 25 | 26 | - name: Run format check 27 | run: cargo fmt --check 28 | -------------------------------------------------------------------------------- /examples/all_paths.rs: -------------------------------------------------------------------------------- 1 | use valley_free::{RelType, Topology}; 2 | 3 | fn main() { 4 | let topo = Topology::from_edges(vec![ 5 | (1, 2, RelType::ProviderToCustomer), 6 | (1, 3, RelType::ProviderToCustomer), 7 | (2, 3, RelType::PeerToPeer), 8 | (2, 4, RelType::ProviderToCustomer), 9 | (3, 4, RelType::ProviderToCustomer), 10 | ]); 11 | 12 | let topo = topo.valley_free_of(4); 13 | 14 | for path in topo.path_to_all_ases().unwrap() { 15 | println!("{:?}", path); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/all_paths_between_two_ases.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use valley_free::Topology; 4 | 5 | fn main() { 6 | let file = File::open("20231201.as-rel.txt").unwrap(); 7 | let topo = Topology::from_caida(file).unwrap(); 8 | 9 | let university_of_twente_asn = 1133; 10 | let universidade_de_sao_paulo_asn = 28571; 11 | let ut_path = topo.valley_free_of(university_of_twente_asn); 12 | 13 | println!("Paths from UT to USP:"); 14 | for path in ut_path.all_paths_to(universidade_de_sao_paulo_asn).unwrap() { 15 | println!(" {:?}", path); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/shortest_path_between_two_ases.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use valley_free::Topology; 4 | 5 | fn main() { 6 | let file = File::open("20231201.as-rel.txt").unwrap(); 7 | let topo = Topology::from_caida(file).unwrap(); 8 | 9 | let university_of_twente_asn = 1133; 10 | let universidade_de_sao_paulo_asn = 28571; 11 | let ut_path = topo.valley_free_of(university_of_twente_asn); 12 | 13 | // Use A* to find the shortest path between two nodes 14 | let path = ut_path 15 | .shortest_path_to(universidade_de_sao_paulo_asn) 16 | .unwrap(); 17 | 18 | println!("Path from UT to USP: {:?}", path); 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valley-free" 3 | description = "BGP valley-free routing AS path exploration library" 4 | keywords = ["bgp", "valley-free", "network", "simulation"] 5 | repository = "https://github.com/bgpkit/valley-free" 6 | documentation = "https://docs.rs/valley-free" 7 | version = "0.3.1" 8 | authors = ["Mingwei Zhang ", "Guilherme Salustiano "] 9 | edition = "2018" 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | petgraph = "0.6.4" 17 | 18 | [dev-dependencies] 19 | rayon = "1.8.1" 20 | bzip2 = "0.4.4" 21 | reqwest = { version = "0.11", features = ["blocking"] } 22 | -------------------------------------------------------------------------------- /examples/draw_path_graph.rs: -------------------------------------------------------------------------------- 1 | use petgraph::dot::Dot; 2 | use valley_free::{RelType, Topology}; 3 | 4 | fn main() { 5 | let topo = Topology::from_edges(vec![ 6 | (1, 2, RelType::ProviderToCustomer), 7 | (1, 3, RelType::ProviderToCustomer), 8 | (2, 4, RelType::ProviderToCustomer), 9 | (2, 5, RelType::ProviderToCustomer), 10 | (2, 3, RelType::PeerToPeer), 11 | (3, 5, RelType::ProviderToCustomer), 12 | (3, 6, RelType::ProviderToCustomer), 13 | ]); 14 | 15 | println!("Basic topology"); 16 | println!("{:?}", Dot::new(&topo.graph)); 17 | 18 | let topo_path = topo.valley_free_of(4); 19 | println!("Path topology"); 20 | println!("{:?}", Dot::new(&topo_path.graph)); 21 | 22 | // You can visualize the graphs online at https://dreampuf.github.io/GraphvizOnline/ 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Mingwei Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Valley Free Explorer 2 | 3 | [![Rust](https://github.com/bgpkit/valley-free/actions/workflows/build.yaml/badge.svg)](https://github.com/bgpkit/valley-free/actions/workflows/build.yaml) 4 | [![Crates.io](https://img.shields.io/crates/v/valley-free)](https://crates.io/crates/valley-free) 5 | [![Docs.rs](https://docs.rs/valley-free/badge.svg)](https://docs.rs/valley-free) 6 | [![License](https://img.shields.io/crates/l/valley-free)](https://raw.githubusercontent.com/bgpkit/valley-free/main/LICENSE) 7 | 8 | `valley-free` crate is a Rust package that reads CAIDA's [AS-relationship data][asrel] 9 | and explores AS-level paths using `valley-free` model. 10 | 11 | [asrel]: https://www.caida.org/data/as-relationships/ 12 | 13 | ## Core Ideas 14 | 15 | ### Topology Building 16 | 17 | The first step for doing `valley-free` paths simulation is to obtain AS-level 18 | topology and inter-AS relationships. Here in this library, we utilize CAIDA's 19 | [AS-relationship data][asrel] data to obtain both the AS relationships and the 20 | topology. 21 | 22 | The CAIDA's AS-relationship data is formatted as follows: 23 | ``` 24 | ## A FEW LINES OF COMMENT 25 | ## A FEW LINES OF COMMENT 26 | ## A FEW LINES OF COMMENT 27 | 1|7470|0 28 | 1|9931|-1 29 | 1|11537|0 30 | 1|25418|0 31 | 2|35000|0 32 | 2|263686|0 33 | ... 34 | ``` 35 | 36 | The data format is: 37 | ```example 38 | ||-1 39 | ||0 40 | ``` 41 | 42 | A non-comment row in the dataset means: 43 | - there is a AS-level link between the two ASes 44 | - the relationships are either peer-to-peer (0) or provider-to-customer (-1) 45 | 46 | ### Path Propagation 47 | 48 | It generate a graph simulating the AS paths propagation from the origin and 49 | creating a graph of all the possible paths in the way of the propagation. 50 | 51 | For exemplo for the following topology: 52 | 53 | ![](images/base_topology.svg) 54 | 55 | It start from the AS4 and form a direct graph with all next hops that confom 56 | wih valley-free routing (i.e. the path with the next hop is stil valley-free), 57 | and keeps propagate until generate a direct acyclic graph (DAG) with all with the 58 | "valley-free view" of the AS4 to the network. 59 | 60 | ![](images/path_topology.svg) 61 | 62 | And then you can use this DAG with all the classic graph methods to analyze it. 63 | For example, you can find the [length of all shortest paths](https://docs.rs/petgraph/latest/petgraph/algo/k_shortest_path/fn.k_shortest_path.html), 64 | or even [all the paths](https://docs.rs/petgraph/latest/petgraph/algo/simple_paths/fn.all_simple_paths.html). 65 | 66 | ## Usage 67 | 68 | ### Rust 69 | 70 | #### Install 71 | ``` toml 72 | [dependencies] 73 | valley_free="0.3" 74 | ``` 75 | 76 | #### Examples 77 | To use the examples expect the [CAIDA-as 2023-12-01 dataset](https://publicdata.caida.org/datasets/as-relationships/serial-1/20231201.as-rel.txt.bz2) 78 | on the root directory. 79 | 80 | The examples are available in the [`examples/`](examples/) direction. 81 | 82 | You can run it with `cargo run --example=`. -------------------------------------------------------------------------------- /images/path_topology.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | %0 9 | 10 | 11 | 12 | 0 13 | 14 | 2 15 | 16 | 17 | 18 | 2 19 | 20 | 5 21 | 22 | 23 | 24 | 0->2 25 | 26 | 27 | ProviderToCustomer 28 | 29 | 30 | 31 | 3 32 | 33 | 3 34 | 35 | 36 | 37 | 0->3 38 | 39 | 40 | PeerToPeer 41 | 42 | 43 | 44 | 5 45 | 46 | 1 47 | 48 | 49 | 50 | 0->5 51 | 52 | 53 | CustomerToProvider 54 | 55 | 56 | 57 | 1 58 | 59 | 6 60 | 61 | 62 | 63 | 3->1 64 | 65 | 66 | ProviderToCustomer 67 | 68 | 69 | 70 | 3->2 71 | 72 | 73 | ProviderToCustomer 74 | 75 | 76 | 77 | 4 78 | 79 | 4 80 | 81 | 82 | 83 | 4->0 84 | 85 | 86 | CustomerToProvider 87 | 88 | 89 | 90 | 5->3 91 | 92 | 93 | ProviderToCustomer 94 | 95 | 96 | -------------------------------------------------------------------------------- /images/base_topology.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | %0 9 | 10 | 11 | 12 | 0 13 | 14 | 5 15 | 16 | 17 | 18 | 1 19 | 20 | 2 21 | 22 | 23 | 24 | 1->0 25 | 26 | 27 | ProviderToCustomer 28 | 29 | 30 | 31 | 2 32 | 33 | 4 34 | 35 | 36 | 37 | 1->2 38 | 39 | 40 | ProviderToCustomer 41 | 42 | 43 | 44 | 5 45 | 46 | 3 47 | 48 | 49 | 50 | 1->5 51 | 52 | 53 | PeerToPeer 54 | 55 | 56 | 57 | 3 58 | 59 | 6 60 | 61 | 62 | 63 | 3->5 64 | 65 | 66 | CustomerToProvider 67 | 68 | 69 | 70 | 4 71 | 72 | 1 73 | 74 | 75 | 76 | 4->1 77 | 78 | 79 | ProviderToCustomer 80 | 81 | 82 | 83 | 4->5 84 | 85 | 86 | ProviderToCustomer 87 | 88 | 89 | 90 | 5->0 91 | 92 | 93 | ProviderToCustomer 94 | 95 | 96 | 97 | 5->1 98 | 99 | 100 | PeerToPeer 101 | 102 | 103 | 104 | 5->3 105 | 106 | 107 | ProviderToCustomer 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// valley-free is a library that builds AS-level topology using CAIDA's 2 | /// AS-relationship data file and run path exploration using valley-free routing 3 | /// principle. 4 | use std::{ 5 | collections::{HashMap, HashSet}, 6 | io, 7 | }; 8 | 9 | use petgraph::{ 10 | algo::{all_simple_paths, astar}, 11 | graph::{DiGraph, NodeIndex}, 12 | visit::EdgeRef, 13 | Direction, 14 | }; 15 | 16 | #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] 17 | pub enum RelType { 18 | CustomerToProvider, 19 | PeerToPeer, 20 | ProviderToCustomer, 21 | } 22 | 23 | // Required to work as a edge 24 | impl Default for RelType { 25 | fn default() -> Self { 26 | RelType::ProviderToCustomer 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct Topology { 32 | pub graph: DiGraph, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct ValleyFreeTopology { 37 | pub graph: DiGraph, 38 | pub source: u32, 39 | } 40 | 41 | pub type TopologyPath = Vec; 42 | type TopologyPathIndex = Vec; 43 | 44 | #[derive(Debug)] 45 | pub enum TopologyError { 46 | IoError(io::Error), 47 | ParseAsnError(std::num::ParseIntError), 48 | ParseError(String), 49 | } 50 | 51 | pub trait TopologyExt { 52 | fn asn_of(&self, index: NodeIndex) -> u32; 53 | fn index_of(&self, asn: u32) -> Option; 54 | fn all_asns(&self) -> HashSet; 55 | fn providers_of(&self, asn: u32) -> Option>; 56 | fn customers_of(&self, asn: u32) -> Option>; 57 | fn peers_of(&self, asn: u32) -> Option>; 58 | fn has_connection(&self, asn1: u32, asn2: u32) -> bool; 59 | } 60 | 61 | trait TopologyPathExt { 62 | fn paths_graph(&self, asn: u32) -> DiGraph; 63 | } 64 | 65 | impl TopologyExt for DiGraph { 66 | fn asn_of(&self, index: NodeIndex) -> u32 { 67 | *self.node_weight(index).unwrap() 68 | } 69 | 70 | fn index_of(&self, asn: u32) -> Option { 71 | self.node_indices().find(|&index| self.asn_of(index) == asn) 72 | } 73 | 74 | fn all_asns(&self) -> HashSet { 75 | self.raw_nodes().iter().map(|node| node.weight).collect() 76 | } 77 | 78 | fn providers_of(&self, asn: u32) -> Option> { 79 | let incoming = self 80 | .edges_directed(self.index_of(asn)?, Direction::Incoming) 81 | .filter(|edge| edge.weight() == &RelType::ProviderToCustomer) // could be PeerToPeer 82 | .map(|edge| edge.source()); 83 | 84 | let outgoing = self 85 | .edges_directed(self.index_of(asn)?, Direction::Outgoing) 86 | .filter(|edge| edge.weight() == &RelType::CustomerToProvider) 87 | .map(|edge| edge.target()); 88 | 89 | Some( 90 | incoming 91 | .chain(outgoing) 92 | .map(|asn| self.asn_of(asn)) 93 | .collect(), 94 | ) 95 | } 96 | 97 | fn customers_of(&self, asn: u32) -> Option> { 98 | let outgoing = self 99 | .edges_directed(self.index_of(asn)?, Direction::Outgoing) 100 | .filter(|edge| edge.weight() == &RelType::ProviderToCustomer) // could be PeerToPeer 101 | .map(|edge| edge.target()); 102 | 103 | let incoming = self 104 | .edges_directed(self.index_of(asn)?, Direction::Incoming) 105 | .filter(|edge| edge.weight() == &RelType::CustomerToProvider) 106 | .map(|edge| edge.source()); 107 | 108 | Some( 109 | outgoing 110 | .chain(incoming) 111 | .map(|asn| self.asn_of(asn)) 112 | .collect(), 113 | ) 114 | } 115 | 116 | fn peers_of(&self, asn: u32) -> Option> { 117 | let outgoing = self 118 | .edges_directed(self.index_of(asn)?, Direction::Outgoing) 119 | .filter(|edge| edge.weight() == &RelType::PeerToPeer) 120 | .map(|edge| edge.target()); 121 | 122 | let incoming = self 123 | .edges_directed(self.index_of(asn)?, Direction::Incoming) 124 | .filter(|edge| edge.weight() == &RelType::PeerToPeer) 125 | .map(|edge| edge.source()); 126 | 127 | Some( 128 | outgoing 129 | .chain(incoming) 130 | .map(|asn| self.asn_of(asn)) 131 | .collect(), 132 | ) 133 | } 134 | 135 | fn has_connection(&self, asn1: u32, asn2: u32) -> bool { 136 | self.index_of(asn1) 137 | .map(|asn1| self.index_of(asn2).map(|asn2| self.find_edge(asn1, asn2))) 138 | .flatten() 139 | .flatten() 140 | .is_some() 141 | } 142 | } 143 | 144 | impl TopologyPathExt for DiGraph { 145 | /* 146 | * Given the following topology: 147 | * 148 | * ┌─────┐ 149 | * │ │ 150 | * └──┬──┘ 151 | * ┌──────┴─────┐ 152 | * ┌──▼──┐ ┌──▼──┐ 153 | * │ ◄──────► │ 154 | * └──┬──┘ └──┬──┘ 155 | * ┌─────┴────┐ ┌────┴────┐ 156 | * ┌──▼──┐ ┌─▼──▼┐ ┌──▼──┐ 157 | * │ │ │ │ │ │ 158 | * └─────┘ └─────┘ └─────┘ 159 | * 160 | * This method generate a DAG with all paths from the given AS to all other AS-relationship 161 | * following the valley-free principle. 162 | * 163 | * ┌─────┐ 164 | * │ │ 165 | * └──▲──┘ 166 | * ┌──────┴─────┐ 167 | * ┌──┴──┐ ┌──▼──┐ 168 | * │ ├──────► │ 169 | * └──▲──┘ └──┬──┘ 170 | * ┌─────┴────┐ ┌────┴────┐ 171 | * ┌──┴──┐ ┌─▼──▼┐ ┌──▼──┐ 172 | * │ │ │ │ │ │ 173 | * └─────┘ └─────┘ └─────┘ 174 | * 175 | * You can use this graph to calculate the shortest path or even list all paths using the 176 | * petgraph library. 177 | */ 178 | fn paths_graph(&self, asn: u32) -> DiGraph { 179 | let mut graph = DiGraph::new(); 180 | 181 | let get_or_create = |graph: &mut DiGraph, asn: u32| { 182 | graph.index_of(asn).unwrap_or_else(|| graph.add_node(asn)) 183 | }; 184 | 185 | let add_edge = |graph: &mut DiGraph, asn1: u32, asn2: u32, rel: RelType| { 186 | let asn1 = get_or_create(graph, asn1); 187 | let asn2 = get_or_create(graph, asn2); 188 | graph.add_edge(asn1, asn2, rel); 189 | }; 190 | 191 | let mut up_path_queue = Vec::new(); 192 | let mut up_seen = Vec::new(); 193 | 194 | // add first 195 | graph.add_node(asn); 196 | up_path_queue.push(asn); 197 | 198 | while !up_path_queue.is_empty() { 199 | let asn = up_path_queue.pop().unwrap(); // While check if has elements 200 | up_seen.push(asn); 201 | 202 | for provider_asn in self.providers_of(asn).unwrap() { 203 | if up_seen.contains(&provider_asn) { 204 | continue; 205 | } 206 | up_path_queue.push(provider_asn); 207 | 208 | add_edge(&mut graph, asn, provider_asn, RelType::CustomerToProvider); 209 | } 210 | } 211 | 212 | let mut peer_seen = Vec::new(); 213 | // Iterate over all ASes reach by UP 214 | // They can only do one PEAR, so we don't need a queue 215 | // In order to avoid cycle, we need to first iterate with was first acess by UP 216 | for asn in up_seen.clone().into_iter() { 217 | for peer_asn in self.peers_of(asn).unwrap() { 218 | peer_seen.push(peer_asn); 219 | 220 | if !graph.has_connection(peer_asn, asn) { 221 | add_edge(&mut graph, asn, peer_asn, RelType::PeerToPeer); 222 | } 223 | } 224 | } 225 | 226 | let mut down_seen = Vec::new(); 227 | 228 | let mut down_path_queue: Vec<_> = up_seen 229 | .into_iter() 230 | .chain(peer_seen.into_iter()) 231 | .rev() // down propagate fisrt up then peer 232 | .collect(); 233 | 234 | while !down_path_queue.is_empty() { 235 | let asn = down_path_queue.pop().unwrap(); 236 | 237 | for customer_asn in self.customers_of(asn).unwrap() { 238 | if !graph.has_connection(customer_asn, asn) 239 | && !graph.has_connection(asn, customer_asn) 240 | { 241 | add_edge(&mut graph, asn, customer_asn, RelType::ProviderToCustomer); 242 | } 243 | 244 | if !down_seen.contains(&customer_asn) && !down_path_queue.contains(&customer_asn) { 245 | down_seen.push(customer_asn); 246 | down_path_queue.push(customer_asn); 247 | } 248 | } 249 | } 250 | 251 | // assert!(!is_cyclic_directed(&graph)); 252 | graph 253 | } 254 | } 255 | 256 | impl TopologyExt for Topology { 257 | fn asn_of(&self, index: NodeIndex) -> u32 { 258 | self.graph.asn_of(index) 259 | } 260 | 261 | fn index_of(&self, asn: u32) -> Option { 262 | self.graph.index_of(asn) 263 | } 264 | 265 | fn all_asns(&self) -> HashSet { 266 | self.graph.all_asns() 267 | } 268 | 269 | fn providers_of(&self, asn: u32) -> Option> { 270 | self.graph.providers_of(asn) 271 | } 272 | 273 | fn customers_of(&self, asn: u32) -> Option> { 274 | self.graph.customers_of(asn) 275 | } 276 | 277 | fn peers_of(&self, asn: u32) -> Option> { 278 | self.graph.peers_of(asn) 279 | } 280 | 281 | fn has_connection(&self, asn1: u32, asn2: u32) -> bool { 282 | self.graph.has_connection(asn1, asn2) 283 | } 284 | } 285 | 286 | impl TopologyExt for ValleyFreeTopology { 287 | fn asn_of(&self, index: NodeIndex) -> u32 { 288 | self.graph.asn_of(index) 289 | } 290 | 291 | fn index_of(&self, asn: u32) -> Option { 292 | self.graph.index_of(asn) 293 | } 294 | 295 | fn all_asns(&self) -> HashSet { 296 | self.graph.all_asns() 297 | } 298 | 299 | fn providers_of(&self, asn: u32) -> Option> { 300 | self.graph.providers_of(asn) 301 | } 302 | 303 | fn customers_of(&self, asn: u32) -> Option> { 304 | self.graph.customers_of(asn) 305 | } 306 | 307 | fn peers_of(&self, asn: u32) -> Option> { 308 | self.graph.peers_of(asn) 309 | } 310 | 311 | fn has_connection(&self, asn1: u32, asn2: u32) -> bool { 312 | self.graph.has_connection(asn1, asn2) 313 | } 314 | } 315 | 316 | impl Topology { 317 | pub fn from_edges(edges: Vec<(u32, u32, RelType)>) -> Self { 318 | let mut graph = DiGraph::new(); 319 | 320 | let nodes: HashSet = edges 321 | .iter() 322 | .flat_map(|(asn1, asn2, _)| vec![*asn1, *asn2]) 323 | .collect(); 324 | 325 | let asn2index: HashMap = nodes 326 | .into_iter() 327 | .map(|asn| (asn, graph.add_node(asn))) 328 | .collect(); 329 | 330 | graph.extend_with_edges(edges.into_iter().map(|(asn1, asn2, rel)| { 331 | ( 332 | *asn2index.get(&asn1).unwrap(), 333 | *asn2index.get(&asn2).unwrap(), 334 | rel, 335 | ) 336 | })); 337 | 338 | Topology { graph } 339 | } 340 | 341 | pub fn from_caida(reader: impl std::io::Read) -> Result { 342 | let content = reader 343 | .bytes() 344 | .collect::, _>>() 345 | .map_err(TopologyError::IoError)?; 346 | 347 | let content = String::from_utf8(content).map_err(|e| { 348 | TopologyError::ParseError(format!("invalid UTF-8 in AS relationship file: {}", e)) 349 | })?; 350 | 351 | let edges = content 352 | .lines() 353 | .filter(|line| !line.starts_with("#")) 354 | .map(|line| { 355 | let fields = line.split("|").collect::>(); 356 | let asn1 = fields[0] 357 | .parse::() 358 | .map_err(TopologyError::ParseAsnError)?; 359 | let asn2 = fields[1] 360 | .parse::() 361 | .map_err(TopologyError::ParseAsnError)?; 362 | let rel = fields[2] 363 | .parse::() 364 | .map_err(TopologyError::ParseAsnError)?; 365 | 366 | match rel { 367 | // asn1 and asn2 are peers 368 | 0 => Ok((asn1, asn2, RelType::PeerToPeer)), 369 | 370 | // asn1 is a provider of asn2 371 | -1 => Ok((asn1, asn2, RelType::ProviderToCustomer)), 372 | 373 | _ => Err(TopologyError::ParseError(format!( 374 | "unknown relationship type {} in {}", 375 | rel, line 376 | ))), 377 | } 378 | }) 379 | .collect::, _>>()?; 380 | 381 | Ok(Topology::from_edges(edges)) 382 | } 383 | 384 | pub fn valley_free_of(&self, asn: u32) -> ValleyFreeTopology { 385 | ValleyFreeTopology { 386 | graph: self.graph.paths_graph(asn), 387 | source: asn, 388 | } 389 | } 390 | } 391 | 392 | impl ValleyFreeTopology { 393 | pub fn shortest_path_to(&self, target: u32) -> Option { 394 | let source_index = self.index_of(self.source)?; 395 | let target_index = self.index_of(target)?; 396 | 397 | // Use A* to find the shortest path between two nodes 398 | let (_len, path) = astar( 399 | &self.graph, 400 | source_index, 401 | |finish| finish == target_index, 402 | |edge| match edge.weight() { 403 | // priorize pearing 404 | RelType::PeerToPeer => 0, 405 | RelType::ProviderToCustomer => 1, 406 | RelType::CustomerToProvider => 2, 407 | }, 408 | |_| 0, 409 | ) 410 | .unwrap(); 411 | 412 | let path = path.iter().map(|node| self.asn_of(*node)).collect(); 413 | 414 | Some(path) 415 | } 416 | 417 | pub fn all_paths_to(&self, target: u32) -> Option + '_> { 418 | let source_index = self.index_of(self.source)?; 419 | let target_index = self.index_of(target)?; 420 | 421 | let paths = all_simple_paths::( 422 | &self.graph, 423 | source_index, 424 | target_index, 425 | 0, 426 | None, 427 | ); 428 | 429 | let paths = paths.map(move |path| { 430 | path.iter() 431 | .map(|node| self.asn_of(*node)) 432 | .collect::>() 433 | }); 434 | 435 | Some(paths) 436 | } 437 | 438 | pub fn path_to_all_ases(&self) -> Option> { 439 | let source_index = self.index_of(self.source)?; 440 | 441 | let mut stack: Vec<(NodeIndex, TopologyPathIndex)> = 442 | vec![(source_index, vec![source_index])]; 443 | let mut visited: Vec = vec![]; 444 | let mut all_paths: Vec = vec![]; 445 | 446 | while !stack.is_empty() { 447 | let (node_idx, path) = stack.pop().unwrap(); 448 | 449 | if visited.contains(&node_idx) { 450 | continue; 451 | } 452 | 453 | visited.push(node_idx); 454 | all_paths.push(path.clone()); 455 | 456 | let childrens = self 457 | .graph 458 | .neighbors_directed(node_idx, petgraph::Direction::Outgoing) 459 | .map(|child_idx| { 460 | let mut path = path.clone(); 461 | path.push(child_idx); 462 | (child_idx, path) 463 | }); 464 | stack.extend(childrens); 465 | } 466 | 467 | let all_paths = all_paths 468 | .into_iter() 469 | .map(|path| path.iter().map(|node| self.asn_of(*node)).collect()) 470 | .collect(); 471 | 472 | Some(all_paths) 473 | } 474 | } 475 | 476 | impl From for Topology { 477 | fn from(valley_free: ValleyFreeTopology) -> Self { 478 | Topology { 479 | graph: valley_free.graph, 480 | } 481 | } 482 | } 483 | 484 | #[cfg(test)] 485 | mod test { 486 | use std::{env, fs::File}; 487 | 488 | use bzip2::read::BzDecoder; 489 | use petgraph::{algo::is_cyclic_directed, dot::Dot}; 490 | use rayon::iter::{IntoParallelIterator, ParallelIterator}; 491 | 492 | use super::*; 493 | 494 | /* ┌───────┐ 495 | * │ 1 │ 496 | * └──┬─┬──┘ 497 | * ┌────┘ └────┐ 498 | * ┌───▼───┐ ┌───▼───┐ 499 | * │ 2 ◄───► 3 │ 500 | * └───┬───┘ └───┬───┘ 501 | * └────┐ ┌────┘ 502 | * ┌──▼─▼──┐ 503 | * │ 4 │ 504 | * └───────┘ 505 | */ 506 | fn diamond_topology() -> Topology { 507 | Topology::from_edges(vec![ 508 | (1, 2, RelType::ProviderToCustomer), 509 | (1, 3, RelType::ProviderToCustomer), 510 | (3, 2, RelType::PeerToPeer), 511 | (3, 4, RelType::ProviderToCustomer), 512 | (2, 4, RelType::ProviderToCustomer), 513 | ]) 514 | } 515 | 516 | /* ┌─────┐ 517 | * │ 1 │ 518 | * └──┬──┘ 519 | * ┌──────┴─────┐ 520 | * ┌──▼──┐ ┌──▼──┐ 521 | * │ 2 │ │ 3 │ 522 | * └──┬──┘ └──┬──┘ 523 | * ┌─────┴────┐ ┌────┴────┐ 524 | * ┌──▼──┐ ┌─▼──▼─┐ ┌──▼──┐ 525 | * │ 4 │ │ 05 │ │ 6 │ 526 | * └─────┘ └──────┘ └─────┘ 527 | */ 528 | fn piramid_topology() -> Topology { 529 | Topology::from_edges(vec![ 530 | (1, 2, RelType::ProviderToCustomer), 531 | (1, 3, RelType::ProviderToCustomer), 532 | (2, 4, RelType::ProviderToCustomer), 533 | (2, 5, RelType::ProviderToCustomer), 534 | (3, 5, RelType::ProviderToCustomer), 535 | (3, 6, RelType::ProviderToCustomer), 536 | ]) 537 | } 538 | 539 | fn get_caida_data() -> impl std::io::Read { 540 | let cachefile = env::temp_dir().join("20231201.as-rel.txt.bz2"); 541 | if cachefile.exists() { 542 | return BzDecoder::new(File::open(cachefile).unwrap()); 543 | } 544 | 545 | let url = "https://publicdata.caida.org/datasets/as-relationships/serial-1/20231201.as-rel.txt.bz2"; 546 | let mut response = reqwest::blocking::get(url).unwrap(); 547 | 548 | response 549 | .copy_to(&mut File::create(cachefile.clone()).unwrap()) 550 | .unwrap(); 551 | 552 | BzDecoder::new(File::open(cachefile).unwrap()) 553 | } 554 | 555 | #[test] 556 | fn test_all_asns() { 557 | let topo = diamond_topology(); 558 | 559 | assert_eq!(topo.all_asns(), [1, 2, 3, 4].into()); 560 | } 561 | 562 | #[test] 563 | fn test_providers() { 564 | let topo = diamond_topology(); 565 | 566 | assert_eq!(topo.providers_of(1), Some([].into())); 567 | assert_eq!(topo.providers_of(2), Some([1].into())); 568 | assert_eq!(topo.providers_of(3), Some([1].into())); 569 | assert_eq!(topo.providers_of(4), Some([2, 3].into())); 570 | } 571 | 572 | #[test] 573 | fn test_customers() { 574 | let topo = diamond_topology(); 575 | 576 | assert_eq!(topo.customers_of(1), Some([3, 2].into())); 577 | assert_eq!(topo.customers_of(2), Some([4].into())); 578 | assert_eq!(topo.customers_of(3), Some([4].into())); 579 | assert_eq!(topo.customers_of(4), Some([].into())); 580 | } 581 | 582 | #[test] 583 | fn test_peers() { 584 | let topo = diamond_topology(); 585 | 586 | assert_eq!(topo.peers_of(1), Some([].into())); 587 | assert_eq!(topo.peers_of(2), Some([3].into())); 588 | assert_eq!(topo.peers_of(3), Some([2].into())); 589 | assert_eq!(topo.peers_of(4), Some([].into())); 590 | } 591 | 592 | #[test] 593 | fn test_from_caida() { 594 | let test_rel = r#"# xxx 595 | 1|2|-1 596 | 1|3|-1 597 | 2|4|-1 598 | 3|4|-1"#; 599 | let topo = Topology::from_caida(test_rel.as_bytes()); 600 | 601 | assert!(topo.is_ok()); 602 | } 603 | 604 | #[test] 605 | fn test_from_real_caida() { 606 | let topo = Topology::from_caida(get_caida_data()); 607 | 608 | assert!(topo.is_ok()); 609 | } 610 | 611 | #[test] 612 | /* Input: 613 | * ┌─────┐ 614 | * │ 1 │ 615 | * └──┬──┘ 616 | * ┌──────┴─────┐ 617 | * ┌──▼──┐ ┌──▼──┐ 618 | * │ 2 ◄──────► 3 │ 619 | * └──┬──┘ └──┬──┘ 620 | * ┌─────┴────┐ ┌────┴────┐ 621 | * ┌──▼──┐ ┌─▼──▼─┐ ┌──▼──┐ 622 | * │ 4 │ │ 05 │ │ 6 │ 623 | * └─────┘ └──────┘ └─────┘ 624 | * 625 | * Expected output: 626 | * ┌─────┐ 627 | * │ 1 │ 628 | * └──▲──┘ 629 | * ┌──────┴─────┐ 630 | * ┌──┴──┐ ┌──▼──┐ 631 | * │ 2 ├──────► 3 │ 632 | * └──▲──┘ └──┬──┘ 633 | * ┌─────┴────┐ ┌────┴────┐ 634 | * ┌──┴──┐ ┌─▼──▼─┐ ┌──▼──┐ 635 | * │ 4 │ │ 05 │ │ 6 │ 636 | * └─────┘ └──────┘ └─────┘ 637 | * 638 | */ 639 | fn test_path_graph() { 640 | let topo = Topology::from_edges(vec![ 641 | (1, 2, RelType::ProviderToCustomer), 642 | (1, 3, RelType::ProviderToCustomer), 643 | (2, 4, RelType::ProviderToCustomer), 644 | (2, 5, RelType::ProviderToCustomer), 645 | (2, 3, RelType::PeerToPeer), 646 | (3, 5, RelType::ProviderToCustomer), 647 | (3, 6, RelType::ProviderToCustomer), 648 | ]); 649 | 650 | let topo = topo.valley_free_of(4); 651 | 652 | let has_edge = |asn1: u32, asn2: u32| topo.has_connection(asn1, asn2); 653 | 654 | assert!(has_edge(4, 2)); 655 | 656 | assert!(has_edge(2, 1)); 657 | assert!(has_edge(2, 3)); 658 | assert!(has_edge(2, 5)); 659 | 660 | assert!(has_edge(1, 3)); 661 | 662 | assert!(has_edge(3, 5)); 663 | assert!(has_edge(3, 6)); 664 | 665 | assert_eq!(topo.graph.edge_count(), 7); 666 | assert!(!is_cyclic_directed(&topo.graph)); 667 | } 668 | 669 | #[test] 670 | fn test_shortest_path_to() { 671 | let topo = piramid_topology(); 672 | let topo = topo.valley_free_of(4); 673 | 674 | let path = topo.shortest_path_to(6).unwrap(); 675 | assert_eq!(path, vec![4, 2, 1, 3, 6]); 676 | } 677 | 678 | #[test] 679 | fn test_all_paths_to() { 680 | let topo = piramid_topology(); 681 | let topo = topo.valley_free_of(4); 682 | 683 | let paths = topo.all_paths_to(5).unwrap().collect::>(); 684 | 685 | assert!(paths.contains(&[4, 2, 5].into())); 686 | assert!(paths.contains(&[4, 2, 1, 3, 5].into())); 687 | assert_eq!(paths.len(), 2); 688 | } 689 | 690 | #[test] 691 | fn test_path_to_all_ases() { 692 | let topo = piramid_topology(); 693 | let topo = topo.valley_free_of(4); 694 | 695 | let paths = topo.path_to_all_ases().unwrap(); 696 | 697 | assert!(paths.contains(&[4].into())); 698 | assert!(paths.contains(&[4, 2].into())); 699 | assert!(paths.contains(&[4, 2, 5].into()) || paths.contains(&[4, 2, 1, 3, 5].into())); 700 | assert!(paths.contains(&[4, 2, 1].into())); 701 | assert!(paths.contains(&[4, 2, 1, 3].into())); 702 | assert!(paths.contains(&[4, 2, 1, 3, 6].into())); 703 | assert_eq!(paths.len(), 6); 704 | } 705 | 706 | /* One possible expected output 707 | * ┌───────┐ 708 | * │ 1 │ 709 | * └──▲─┬──┘ 710 | * ┌────┘ └────┐ 711 | * ┌───┴───┐ ┌───▼───┐ 712 | * │ 2 ├───► 3 │ 713 | * └───▲───┘ └───▲───┘ 714 | * └────┐ ┌────┘ 715 | * ┌──┴─┴──┐ 716 | * │ 4 │ 717 | * └───────┘ 718 | */ 719 | #[test] 720 | fn test_path_graph_with_ciclic() { 721 | let topo = diamond_topology(); 722 | let topo = topo.valley_free_of(4); 723 | 724 | let has_edge = |asn1: u32, asn2: u32| topo.has_connection(asn1, asn2); 725 | 726 | println!("{:?}", Dot::new(&topo.graph)); 727 | 728 | assert!(!is_cyclic_directed(&topo.graph)); 729 | assert!(has_edge(4, 2)); 730 | assert!(has_edge(4, 3)); 731 | 732 | if has_edge(2, 3) { 733 | assert!(!has_edge(3, 2)); 734 | assert!(has_edge(2, 1)); 735 | assert!(has_edge(1, 3)); 736 | } else if has_edge(3, 2) { 737 | assert!(!has_edge(2, 3)); 738 | assert!(has_edge(3, 1)); 739 | assert!(has_edge(1, 2)); 740 | } else { 741 | panic!("should have edge between 2 and 3"); 742 | } 743 | } 744 | 745 | #[test] 746 | #[ignore] 747 | fn test_path_graph_never_generate_ciclic() { 748 | let topo = Topology::from_caida(get_caida_data()).unwrap(); 749 | 750 | topo.all_asns().into_par_iter().for_each(|asn| { 751 | let topo = topo.valley_free_of(asn); 752 | assert!(!is_cyclic_directed(&topo.graph)); 753 | }); 754 | } 755 | } 756 | --------------------------------------------------------------------------------