peer_stats/
lib.rs

1#![allow(dead_code)]
2use anyhow::Result;
3use bgpkit_parser::BgpkitParser;
4use ipnet::{IpNet, Ipv4Net, Ipv6Net};
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8use std::net::IpAddr;
9
10#[derive(Debug, Clone, Serialize)]
11pub struct RibPeerInfo {
12    pub project: String,
13    pub collector: String,
14    pub rib_dump_url: String,
15    pub peers: HashMap<IpAddr, PeerInfo>,
16}
17
18#[derive(Debug, Clone, Serialize)]
19pub struct PeerInfo {
20    pub ip: IpAddr,
21    pub asn: u32,
22    pub num_v4_pfxs: usize,
23    pub num_v6_pfxs: usize,
24    pub num_connected_asns: usize,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Prefix2As {
29    pub project: String,
30    pub collector: String,
31    pub rib_dump_url: String,
32    /// prefix to as mapping: <prefix, <asn, count>>
33    pub pfx2as: Vec<Prefix2AsCount>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Prefix2AsCount {
38    pub prefix: String,
39    pub asn: u32,
40    pub count: usize,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct As2Rel {
45    pub project: String,
46    pub collector: String,
47    pub rib_dump_url: String,
48    /// prefix to as mapping: <prefix, <asn, count>>
49    pub as2rel: Vec<As2RelCount>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct As2RelCount {
54    pub asn1: u32,
55    pub asn2: u32,
56    /// 1 - asn1 is upstream of asn2, 2 - peer, 0 - unknown
57    pub rel: u8,
58    /// number of paths having this relationship
59    pub paths_count: usize,
60    /// number of peers seeing this relationship
61    pub peers_count: usize,
62}
63
64const TIER1: [u32; 17] = [
65    6762, 12956, 2914, 3356, 6453, 1239, 701, 6461, 3257, 1299, 3491, 7018, 3320, 5511, 6830, 174,
66    6939,
67];
68
69const TIER1_V4: [u32; 17] = [
70    6762, 12956, 2914, 3356, 6453, 1239, 701, 6461, 3257, 1299, 3491, 7018, 3320, 5511, 6830, 174,
71    0,
72];
73
74const TIER1_V6: [u32; 17] = [
75    6762, 12956, 2914, 3356, 6453, 1239, 701, 6461, 3257, 1299, 3491, 7018, 3320, 5511, 6830, 174,
76    6939,
77];
78
79fn dedup_path(path: Vec<u32>) -> Vec<u32> {
80    if path.len() <= 1 {
81        return path;
82    }
83
84    let mut new_path = vec![path[0]];
85
86    for (asn1, asn2) in path.into_iter().tuple_windows::<(u32, u32)>() {
87        if asn1 != asn2 {
88            new_path.push(asn2)
89        }
90    }
91    new_path
92}
93
94fn update_as2rel_map(
95    peer_ip: IpAddr,
96    tier1: &[u32],
97    data_map: &mut HashMap<(u32, u32, u8), (usize, HashSet<IpAddr>)>,
98    // input AS path must be from collector ([0]) to origin ([last])
99    original_as_path: &[u32],
100) {
101    let mut as_path = original_as_path.to_vec();
102
103    // counting peer relationships
104    for (asn1, asn2) in as_path.iter().tuple_windows::<(&u32, &u32)>() {
105        let (msg_count, peers) = data_map
106            .entry((*asn1, *asn2, 0))
107            .or_insert((0, HashSet::new()));
108        *msg_count += 1;
109        peers.insert(peer_ip);
110    }
111
112    // counting provider-customer relationships
113    as_path.reverse();
114    let contains_tier1 = as_path.iter().any(|x| tier1.contains(x));
115    if contains_tier1 {
116        let mut first_tier1: usize = usize::MAX;
117        for (i, asn) in as_path.iter().enumerate() {
118            if tier1.contains(asn) && first_tier1 == usize::MAX {
119                first_tier1 = i;
120                break;
121            }
122        }
123
124        // origin to first tier 1
125        if first_tier1 < as_path.len() - 1 {
126            for i in 0..first_tier1 {
127                let (asn1, asn2) = (as_path.get(i).unwrap(), as_path.get(i + 1).unwrap());
128                let (msg_count, peers) = data_map
129                    .entry((*asn2, *asn1, 1))
130                    .or_insert((0, HashSet::new()));
131                *msg_count += 1;
132                peers.insert(peer_ip);
133            }
134        }
135    }
136}
137
138fn compile_as2rel_count(
139    data_map: &HashMap<(u32, u32, u8), (usize, HashSet<IpAddr>)>,
140) -> Vec<As2RelCount> {
141    data_map
142        .iter()
143        .map(|((asn1, asn2, rel), (msg_count, peers))| As2RelCount {
144            asn1: *asn1,
145            asn2: *asn2,
146            rel: *rel,
147            paths_count: *msg_count,
148            peers_count: peers.len(),
149        })
150        .collect()
151}
152
153/// collect information from a provided RIB file
154///
155/// Info to collect:
156/// - `project`
157/// - `collector`
158/// - `dump_url`
159/// - `peer_ip`
160/// - `peer_asn`
161/// - `num_v4_pfxs`
162/// - `num_v6_pfxs`
163pub fn parse_rib_file(
164    file_url: &str,
165    project: &str,
166    collector: &str,
167) -> Result<(RibPeerInfo, Prefix2As, (As2Rel, As2Rel, As2Rel))> {
168    // peer-stats
169    let mut peer_asn_map: HashMap<IpAddr, u32> = HashMap::new();
170    let mut peer_connection: HashMap<IpAddr, HashSet<u32>> = HashMap::new();
171    let mut peer_v4_pfxs_map: HashMap<IpAddr, HashSet<Ipv4Net>> = HashMap::new();
172    let mut peer_v6_pfxs_map: HashMap<IpAddr, HashSet<Ipv6Net>> = HashMap::new();
173
174    // pfx2as
175    let mut pfx2as_map: HashMap<(String, u32), usize> = HashMap::new();
176
177    // as2rel
178    let mut as2rel_map: HashMap<(u32, u32, u8), (usize, HashSet<IpAddr>)> = HashMap::new();
179    let mut as2rel_v4_map: HashMap<(u32, u32, u8), (usize, HashSet<IpAddr>)> = HashMap::new();
180    let mut as2rel_v6_map: HashMap<(u32, u32, u8), (usize, HashSet<IpAddr>)> = HashMap::new();
181
182    for elem in BgpkitParser::new(file_url)? {
183        peer_asn_map
184            .entry(elem.peer_ip)
185            .or_insert(elem.peer_asn.to_u32());
186        if let Some(as_path) = elem.as_path.clone() {
187            if let Some(u32_path) = as_path.to_u32_vec_opt(true) {
188                // peer-stats
189                match u32_path.get(1) {
190                    None => {}
191                    Some(asn) => {
192                        peer_connection
193                            .entry(elem.peer_ip)
194                            .or_default()
195                            .insert(*asn);
196                    }
197                };
198
199                // pfx2as
200                match u32_path.last() {
201                    None => {}
202                    Some(asn) => {
203                        let prefix = elem.prefix.to_string();
204                        let count = pfx2as_map.entry((prefix, *asn)).or_insert(0);
205                        *count += 1;
206                    }
207                }
208
209                // do a global and a v4/v6 specific as2rel
210                for is_global in [true, false] {
211                    // get tier-1 ASes list and the corresponding as2rel_map
212                    let (tier1, data_map) = match is_global {
213                        true => (TIER1.to_vec(), &mut as2rel_map),
214                        false => match elem.prefix.prefix {
215                            IpNet::V4(_) => (TIER1_V4.to_vec(), &mut as2rel_v4_map),
216                            IpNet::V6(_) => (TIER1_V6.to_vec(), &mut as2rel_v6_map),
217                        },
218                    };
219                    // update as2rel_map
220                    update_as2rel_map(elem.peer_ip, &tier1, data_map, &u32_path);
221                }
222            }
223        }
224
225        match elem.prefix.prefix {
226            IpNet::V4(net) => {
227                peer_v4_pfxs_map
228                    .entry(elem.peer_ip)
229                    .or_default()
230                    .insert(net);
231            }
232            IpNet::V6(net) => {
233                peer_v6_pfxs_map
234                    .entry(elem.peer_ip)
235                    .or_default()
236                    .insert(net);
237            }
238        }
239        drop(elem);
240    }
241
242    let mut peer_info_map: HashMap<IpAddr, PeerInfo> = HashMap::new();
243    for (ip, asn) in peer_asn_map {
244        let num_v4_pfxs = peer_v4_pfxs_map.entry(ip).or_default().len();
245        let num_v6_pfxs = peer_v6_pfxs_map.entry(ip).or_default().len();
246        let num_connected_asn = peer_connection.entry(ip).or_default().len();
247        let ip_clone = ip;
248        let asn_clone = asn;
249        peer_info_map.insert(
250            ip_clone,
251            PeerInfo {
252                ip: ip_clone,
253                asn: asn_clone,
254                num_v4_pfxs,
255                num_v6_pfxs,
256                num_connected_asns: num_connected_asn,
257            },
258        );
259    }
260
261    let pfx2as = pfx2as_map
262        .into_iter()
263        .map(|((prefix, asn), count)| Prefix2AsCount { prefix, asn, count })
264        .collect();
265
266    let as2rel_global = compile_as2rel_count(&as2rel_map);
267    let as2rel_v4 = compile_as2rel_count(&as2rel_v4_map);
268    let as2rel_v6 = compile_as2rel_count(&as2rel_v6_map);
269
270    Ok((
271        RibPeerInfo {
272            project: project.to_string(),
273            collector: collector.to_string(),
274            rib_dump_url: file_url.to_string(),
275            peers: peer_info_map,
276        },
277        Prefix2As {
278            project: project.to_string(),
279            collector: collector.to_string(),
280            rib_dump_url: file_url.to_string(),
281            pfx2as,
282        },
283        (
284            As2Rel {
285                project: project.to_string(),
286                collector: collector.to_string(),
287                rib_dump_url: file_url.to_string(),
288                as2rel: as2rel_global,
289            },
290            As2Rel {
291                project: project.to_string(),
292                collector: collector.to_string(),
293                rib_dump_url: file_url.to_string(),
294                as2rel: as2rel_v4,
295            },
296            As2Rel {
297                project: project.to_string(),
298                collector: collector.to_string(),
299                rib_dump_url: file_url.to_string(),
300                as2rel: as2rel_v6,
301            },
302        ),
303    ))
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use serde_json::json;
310    use std::fs::File;
311    use tracing::{info, Level};
312
313    #[test]
314    fn test_read_rib() {
315        tracing_subscriber::fmt()
316            // filter spans/events with level TRACE or higher.
317            .with_max_level(Level::INFO)
318            .init();
319        info!("start");
320        let (peer_stats, pfx2as, as2rel) = parse_rib_file("http://archive.routeviews.org/route-views.soxrs/bgpdata/2022.08/RIBS/rib.20220808.1400.bz2",
321        "route-views", "route-views.sg").unwrap();
322        serde_json::to_writer_pretty(
323            &File::create("peer_info_example.json").unwrap(),
324            &json!(peer_stats),
325        )
326        .unwrap();
327        serde_json::to_writer_pretty(
328            &File::create("pfx2as_example.json").unwrap(),
329            &json!(pfx2as),
330        )
331        .unwrap();
332        serde_json::to_writer_pretty(
333            &File::create("as2rel_example.json").unwrap(),
334            &json!(as2rel),
335        )
336        .unwrap();
337        info!("finished");
338    }
339
340    #[test]
341    fn test_dedup() {
342        let empty: Vec<u32> = vec![];
343        assert_eq!(dedup_path(empty.clone()), empty);
344        assert_eq!(dedup_path(vec![1]), vec![1]);
345        assert_eq!(dedup_path(vec![0, 1, 2, 3, 3, 3, 3]), vec![0, 1, 2, 3]);
346        assert_eq!(
347            dedup_path(vec![0, 1, 2, 3, 3, 3, 3, 4]),
348            vec![0, 1, 2, 3, 4]
349        );
350        assert_eq!(
351            dedup_path(vec![0, 1, 2, 3, 3, 3, 3, 4, 4, 4, 4]),
352            vec![0, 1, 2, 3, 4]
353        );
354    }
355}