1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/// valley-free is a library that builds AS-level topology using CAIDA's
/// AS-relationship data file and run path exploration using valley-free routing
/// principle.
use std::{
    collections::{HashMap, HashSet},
    io::BufRead,
};
use std::{fs::File, io::BufReader};

use bzip2::read::BzDecoder;
use pyo3::prelude::*;
use pyo3::{exceptions::PyIOError, wrap_pyfunction};

/// AS relationship types: CUSTOMER, PROVIDER, and PEER
pub enum RelType {
    CUSTOMER,
    PROVIDER,
    PEER,
}

/// Direction of where the current current propagation going.
pub enum Direction {
    /// Propagating to a provider AS
    UP,
    /// Propagating to a customer AS
    DOWN,
    /// Propagating to a peer AS
    PEER,
}

/// Define type alias Path as Vec<u32>
pub type Path = Vec<u32>;

/// Definiton of AS struct
#[pyclass]
#[derive(Clone)]
pub struct As {
    /// Autonomous system number
    #[pyo3(get, set)]
    pub asn: u32,

    /// Set of customer ASes
    #[pyo3(get, set)]
    pub customers: HashSet<u32>,

    /// Set of provider ASes
    #[pyo3(get, set)]
    pub providers: HashSet<u32>,

    /// Set of peer ASes
    #[pyo3(get, set)]
    pub peers: HashSet<u32>,
}

impl As {
    /// Insert another AS as related AS
    fn insert_rel(&mut self, rel_type: RelType, other: u32) {
        match rel_type {
            RelType::CUSTOMER => {
                self.customers.insert(other);
            }
            RelType::PEER => {
                self.peers.insert(other);
            }
            RelType::PROVIDER => {
                self.providers.insert(other);
            }
        }
    }
}

/// Definition of Topology
#[pyclass]
#[derive(FromPyObject)]
pub struct Topology {
    /// Hashmap of ASes: ASN (u32) to [As]
    #[pyo3(get)]
    pub ases_map: HashMap<u32, As>,
}

impl Topology {
    /// Constructor of the Topology struct
    pub fn new() -> Topology {
        Topology {
            ases_map: HashMap::new(),
        }
    }

    fn get_or_insert(&mut self, asn: u32) -> u32 {
        match self.ases_map.entry(asn) {
            std::collections::hash_map::Entry::Occupied(o) => o.into_mut(),
            std::collections::hash_map::Entry::Vacant(v) => v.insert(As {
                asn: asn,
                customers: HashSet::new(),
                providers: HashSet::new(),
                peers: HashSet::new(),
            }),
        }
        .asn
    }

    fn add_rel(&mut self, asn1: u32, asn2: u32, rel_type: RelType) {
        let as1 = self.ases_map.get_mut(&asn1).unwrap();
        as1.insert_rel(rel_type, asn2);
    }

    /// Build the Topology using [CAIDA AS-relationship][asrel] datafile.
    ///
    /// This function takes an reader that implements [BufRead] trait, read
    /// lines from the reader, and parse the input.
    ///
    /// The CAIDA's AS-relationship data is formatted as follows:
    /// ```example
    /// ## A FEW LINES OF COMMENT
    /// ## A FEW LINES OF COMMENT
    /// ## A FEW LINES OF COMMENT
    /// 1|7470|0
    /// 1|9931|-1
    /// 1|11537|0
    /// 1|25418|0
    /// 2|35000|0
    /// 2|263686|0
    /// ...
    /// ```
    ///
    /// The data format is:
    /// ```example
    /// <provider-as>|<customer-as>|-1
    /// <peer-as>|<peer-as>|0
    /// ```
    ///
    /// [asrel]: https://www.caida.org/data/as-relationships/
    pub fn build_topology(&mut self, reader: impl BufRead) -> Result<(), String> {
        for line in reader.lines() {
            let line: String = match line {
                Ok(l) => l,
                Err(e) => return Err(e.to_string()),
            };
            if line.starts_with("#") {
                continue;
            }
            let fields = line.split("|").collect::<Vec<&str>>();
            let asn1 = fields[0].parse::<u32>().unwrap();
            let asn2 = fields[1].parse::<u32>().unwrap();
            let rel = fields[2].parse::<i32>().unwrap();

            let asn1 = self.get_or_insert(asn1);
            let asn2 = self.get_or_insert(asn2);

            match rel {
                0 => {
                    // asn1 and asn2 are peers
                    self.add_rel(asn1, asn2, RelType::PEER);
                    self.add_rel(asn2, asn1, RelType::PEER);
                }
                -1 => {
                    // asn1 is the provider of asn2
                    self.add_rel(asn1, asn2, RelType::CUSTOMER);
                    self.add_rel(asn2, asn1, RelType::PROVIDER);
                }
                _ => return Err(format!("unknown relationship type {} in {}", rel, line)),
            }
        }

        Ok(())
    }

    /// Recursively calculate path propagation results.
    ///
    /// Recursion breaking conditions:
    /// 1. loop detected;
    /// 2. previously propagated from the AS;
    /// 3. all connected ASes have been called
    ///
    /// Propagation logic:
    /// 1. if the current path is propagated from a customer, then it can
    /// propagate to all of its' customers, providers, and peers
    /// 2. if the current path is propagated from a provider or a peer, it can
    /// only propagate to its customers (thus achiving valley-free)
    ///
    /// Full example: to explore all paths toward AS123, call
    /// ```no_run
    /// use std::{fs::File, io::BufReader, collections::HashSet};
    /// use valley_free::*;
    /// use bzip2::read::BzDecoder;
    ///
    /// let mut topo = Topology::new();
    /// let file = match File::open("20161101.as-rel.txt.bz2") {
    ///     Ok(f) => f,
    ///     Err(_) => panic!("cannot open file"),
    /// };
    /// let reader = BufReader::new(BzDecoder::new(&file));
    /// let res = topo.build_topology(reader);
    ///
    /// let mut all_paths = vec![];
    /// let mut seen = HashSet::new();
    /// topo.propagate_paths(&mut all_paths, 15169, Direction::UP, vec![], &mut seen);
    /// dbg!(all_paths.len());
    /// ```
    ///
    /// Arguments:
    /// - `all_paths`: a muttable Vector of [Path]s passed in to store explored paths
    /// - `asn`: the current ASN that will propagate paths
    /// - `dir`: the [Direction] of the propagation
    /// - `path`: the path so far(not including the current AS)
    /// - `seen`: a [HashSet] of ASes that it has explored already
    pub fn propagate_paths(
        &self,
        all_paths: &mut Vec<Path>,
        asn: u32,
        dir: Direction,
        path: Path,
        seen: &mut HashSet<u32>,
    ) {
        if path.contains(&asn) {
            // detected a loop
            return;
        }

        if seen.contains(&asn) {
            // in current dps search, we've already gone through this AS from other paths
            // for example:
            // 1,2 -> 1,2,3 -> 1,4
            // 1,4,2 will be triggered here
            // NOTE: this might produce some false negative
            return;
        }

        // reaching here means the path + current as should be good to use
        let mut new_path = path.clone();
        new_path.push(asn);
        all_paths.push(new_path.clone());
        seen.insert(asn);

        let as_ptr = self.ases_map.get(&asn).unwrap();
        match dir {
            Direction::UP => {
                // we are propagating up in the current step, thus we can
                // continue to all directions
                for customer_asn in &as_ptr.customers {
                    self.propagate_paths(
                        all_paths,
                        *customer_asn,
                        Direction::DOWN,
                        new_path.clone(),
                        seen,
                    );
                }

                for peer_asn in &as_ptr.peers {
                    self.propagate_paths(
                        all_paths,
                        *peer_asn,
                        Direction::PEER,
                        new_path.clone(),
                        seen,
                    );
                }

                for provider_asn in &as_ptr.providers {
                    self.propagate_paths(
                        all_paths,
                        *provider_asn,
                        Direction::UP,
                        new_path.clone(),
                        seen,
                    );
                }
            }
            Direction::DOWN | Direction::PEER => {
                for customer_asn in &as_ptr.customers {
                    self.propagate_paths(
                        all_paths,
                        *customer_asn,
                        Direction::DOWN,
                        new_path.clone(),
                        seen,
                    );
                }
            }
        }
    }
}

/// Formats the sum of two numbers as string.
#[pyfunction]
fn load_topology<'a>(py: Python, file_path: String) -> PyResult<&PyCell<Topology>> {
    let mut topo = Topology::new();
    let file = match File::open(&file_path) {
        Ok(f) => f,
        Err(_) => panic!("cannot open file"),
    };
    let reader = BufReader::new(BzDecoder::new(&file));
    match topo.build_topology(reader) {
        Ok(_) => {
            let topo_cell = PyCell::new(py, topo).unwrap();
            Ok(topo_cell)
        }
        Err(_) => Err(PyIOError::new_err("cannot load topology file")),
    }
}

#[pyfunction]
fn propagate_paths<'a>(topo: &Topology, asn: u32) -> PyResult<Vec<Vec<u32>>> {
    let mut all_paths = vec![];
    let mut seen = HashSet::new();
    topo.propagate_paths(&mut all_paths, asn, Direction::UP, vec![], &mut seen);
    Ok(all_paths)
}

/// A Python module implemented in Rust.
#[pymodule]
fn valley_free(_: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(load_topology, m)?)?;
    m.add_function(wrap_pyfunction!(propagate_paths, m)?)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::{fs::File, io::BufReader};

    use crate::*;
    use bzip2::read::BzDecoder;

    #[test]
    fn build_topology() {
        let mut topo = Topology::new();
        let file = match File::open("20161101.as-rel.txt.bz2") {
            Ok(f) => f,
            Err(_) => panic!("cannot open file"),
        };
        let reader = BufReader::new(BzDecoder::new(&file));
        let res = topo.build_topology(reader);

        assert!(res.is_ok());
        assert_eq!(topo.ases_map.len(), 55809);
    }

    #[test]
    fn propagate() {
        let mut topo = Topology::new();
        let file = match File::open("20161101.as-rel.txt.bz2") {
            Ok(f) => f,
            Err(_) => panic!("cannot open file"),
        };
        let reader = BufReader::new(BzDecoder::new(&file));
        let res = topo.build_topology(reader);
        assert!(res.is_ok());
        assert_eq!(topo.ases_map.len(), 55809);

        let mut all_paths = vec![];
        let mut seen = HashSet::new();
        topo.propagate_paths(&mut all_paths, 15169, Direction::UP, vec![], &mut seen);
        dbg!(all_paths.len());
    }
}