├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md └── src └── main.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: bgpkit 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | release-format-check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Run format check 17 | run: cargo fmt --check 18 | 19 | create-release: 20 | needs: release-format-check 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: taiki-e/create-gh-release-action@v1 25 | with: 26 | changelog: CHANGELOG.md 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | cargo-publish: 30 | needs: create-release 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Publish to crates.io 35 | run: > 36 | cargo publish 37 | --all-features 38 | --verbose 39 | --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 40 | 41 | upload-assets: 42 | needs: create-release 43 | strategy: 44 | matrix: 45 | include: 46 | - target: aarch64-unknown-linux-gnu 47 | os: ubuntu-latest 48 | - target: x86_64-unknown-linux-gnu 49 | os: ubuntu-latest 50 | - target: universal-apple-darwin 51 | os: macos-latest 52 | runs-on: ${{ matrix.os }} 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: taiki-e/upload-rust-binary-action@v1 56 | with: 57 | bin: ris-live-reader 58 | checksum: sha256 59 | target: ${{ matrix.target }} 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | branches: [ main ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Build 22 | run: cargo build --verbose -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v0.3.0 - 2024-10-16 6 | 7 | ### Highlights 8 | 9 | * Update RIS live message parsing from `bgpkit-parser` to `0.10.11-beta.1`. 10 | * Remove RIS live messages parsing functionalities, instead using `bgpkit-parser` directly. 11 | * remove library, provides only CLI tool now 12 | * automated release on tagging -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ris-live-rs" 3 | version = "0.3.0" 4 | edition = "2018" 5 | authors = ["Mingwei Zhang "] 6 | readme = "README.md" 7 | license = "MIT" 8 | repository = "https://github.com/bgpkit/ris-live-rs" 9 | documentation = "https://docs.rs/ris-live-rs" 10 | description = """ 11 | RIS-Live real-time BGP data stream crate. 12 | """ 13 | keywords = ["bgp"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [[bin]] 18 | name="ris-live-reader" 19 | path="src/main.rs" 20 | 21 | [dependencies] 22 | 23 | serde={version="1.0", features=["derive"]} 24 | serde_json = "1.0" 25 | bgpkit-parser = { version = "0.10.11-beta.1", default-features = false, features = ["rislive"]} 26 | ipnet = "2.10" 27 | 28 | # cli-tool dependencies 29 | tungstenite="0.24.0" 30 | structopt = "0.3" 31 | 32 | [package.metadata.binstall] 33 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.tar.gz" 34 | pkg-fmt = "tgz" 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # select build image 2 | FROM rust:1.46 as build 3 | 4 | # create a new empty shell project 5 | RUN USER=root cargo new --bin ris_live_rs 6 | WORKDIR /ris_live_rs 7 | 8 | # copy your source tree 9 | COPY ./src ./src 10 | COPY ./Cargo.toml ./Cargo.toml 11 | 12 | # build for release 13 | RUN cargo build --release 14 | 15 | # our final base 16 | FROM debian:buster-slim 17 | LABEL maintainer="mingwei@bgpkit.com" 18 | LABEL org.opencontainers.image.source="https://github.com/bgpkit/ris-live-rs" 19 | LABEL org.opencontainers.image.description="ris-live-reader is a commandline tool that reads real-time bgp messagse from RIPE RIS Live websocket stream." 20 | 21 | RUN DEBIAN=NONINTERACTIVE apt update; apt install -y libssl-dev libpq-dev ca-certificates tzdata tini; rm -rf /var/lib/apt/lists/* 22 | 23 | # copy the build artifact from the build stage 24 | COPY --from=build /ris_live_rs/target/release/ris-live-reader /usr/local/bin 25 | 26 | 27 | # set the startup command to run your binary 28 | ENTRYPOINT ["tini", "--", "ris-live-reader"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 BGPKIT 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 | # ris-live-rs 2 | 3 | `ris-live-reader` is a CLI tool that builds up top of [`bgpkit-parser`](https://github.com/bgpkit/bgpkit-parser) 4 | library's RIS live message parsing functionalities to provide a commandline interface to stream BGP data from the 5 | [RIS-Live project](https://ris-live.ripe.net/). 6 | 7 | [![asciicast](https://asciinema.org/a/zAxCUmUko9H7T8KM9qFY77uPo.svg)](https://asciinema.org/a/zAxCUmUko9H7T8KM9qFY77uPo) 8 | 9 | Full command-line options are: 10 | 11 | ``` 12 | ris-live-reader 0.3.0 13 | ris-live-reader is a simple cli tool that can stream BGP data from RIS-Live project with websocket. Check out 14 | https://ris-live.ripe.net/ for more data source information 15 | 16 | USAGE: 17 | ris-live-reader [FLAGS] [OPTIONS] 18 | 19 | FLAGS: 20 | -h, --help Prints help information 21 | --json Output as JSON objects 22 | --less-specific Match prefixes that are less specific (contain) `prefix` 23 | --more-specific Match prefixes that are more specific (part of) `prefix` 24 | --pretty Pretty-print JSON output 25 | --raw Print out raw message without parsing 26 | -V, --version Prints version information 27 | 28 | OPTIONS: 29 | --client client name to identify the stream [default: ris-live-rs] 30 | --host Filter by RRC host: e.g. rrc01. Use "all" for the firehose [default: rrc21] 31 | --msg-type Only include messages of a given BGP or RIS type: UPDATE, OPEN, NOTIFICATION, 32 | KEEPALIVE, or RIS_PEER_STATE 33 | --path ASN or pattern to match against the AS PATH attribute 34 | --peer Only include messages sent by the given BGP peer 35 | --prefix Filter UPDATE messages by prefixes in announcements or withdrawals 36 | --require Only include messages containing a given key 37 | --update-type Only a given BGP update type: announcement (a) or withdrawal (w) 38 | ``` 39 | 40 | ### Installation 41 | 42 | Install via cargo by: 43 | 44 | ```bash 45 | cargo install ris-live-rs 46 | ``` 47 | 48 | Or checkout the repo and run: 49 | 50 | ```bash 51 | cargo install --path . 52 | ``` 53 | 54 | The program `ris-live-reader` will be installed to your `$CARGO_HOME/bin` (e.g. `~/.cargo/bin`). 55 | 56 | ### Run with Docker 57 | 58 | ```bash 59 | docker run --rm -it bgpkit/ris-live-reader --help 60 | ``` 61 | 62 | ## Built with ❤️ by BGPKIT Team 63 | 64 | https://bgpkit.com/favicon.ico 65 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | use bgpkit_parser::models::ElemType; 4 | use bgpkit_parser::parse_ris_live_message; 5 | use bgpkit_parser::rislive::error::ParserRisliveError; 6 | use bgpkit_parser::rislive::messages::ris_subscribe::RisSubscribeType; 7 | use bgpkit_parser::rislive::messages::{RisLiveClientMessage, RisSubscribe}; 8 | use ipnet::IpNet; 9 | use std::net::IpAddr; 10 | use std::str::FromStr; 11 | use structopt::StructOpt; 12 | use tungstenite::{connect, Message}; 13 | 14 | const RIS_LIVE_URL_BASE: &str = "ws://ris-live.ripe.net/v1/ws/"; 15 | 16 | /// ris-live-reader is a simple cli tool that can stream BGP data from RIS-Live project with websocket. 17 | /// Check out https://ris-live.ripe.net/ for more data source information. 18 | #[derive(StructOpt, Debug)] 19 | #[structopt(name = "ris-live-reader")] 20 | struct Opts { 21 | /// client name to identify the stream 22 | #[structopt(long, default_value = "ris-live-rs")] 23 | client: String, 24 | 25 | /// Filter by RRC host: e.g. rrc01. Use "all" for the firehose. 26 | #[structopt(long, default_value = "rrc21")] 27 | host: String, 28 | 29 | /// Only include messages of a given BGP or RIS type: UPDATE, OPEN, NOTIFICATION, KEEPALIVE, or RIS_PEER_STATE 30 | #[structopt(long)] 31 | msg_type: Option, 32 | 33 | /// Only a given BGP update type: announcement (a) or withdrawal (w) 34 | #[structopt(long)] 35 | update_type: Option, 36 | 37 | /// Only include messages containing a given key 38 | #[structopt(long)] 39 | require: Option, 40 | 41 | /// Only include messages sent by the given BGP peer 42 | #[structopt(long)] 43 | peer: Option, 44 | 45 | /// Filter UPDATE messages by prefixes in announcements or withdrawals 46 | #[structopt(long)] 47 | prefix: Option, 48 | 49 | /// Match prefixes that are more specific (part of) `prefix` 50 | #[structopt(long, parse(from_flag = std::ops::Not::not))] 51 | more_specific: bool, 52 | 53 | /// Match prefixes that are less specific (contain) `prefix` 54 | #[structopt(long)] 55 | less_specific: bool, 56 | 57 | /// ASN or pattern to match against the AS PATH attribute 58 | #[structopt(long)] 59 | path: Option, 60 | 61 | /// Output as JSON objects 62 | #[structopt(long)] 63 | json: bool, 64 | 65 | /// Pretty-print JSON output 66 | #[structopt(long)] 67 | pretty: bool, 68 | 69 | /// Print out raw message without parsing 70 | #[structopt(long)] 71 | raw: bool, 72 | } 73 | 74 | /// This is an example of subscribing to RIS-Live's streaming data. 75 | /// 76 | /// For more RIS-Live details, check out their documentation at https://ris-live.ripe.net/manual/ 77 | fn main() { 78 | let opts: Opts = Opts::from_args(); 79 | 80 | let url = format!("{}?client={}", RIS_LIVE_URL_BASE, opts.client); 81 | // connect to RIPE RIS Live websocket server 82 | let (mut socket, _response) = 83 | connect(url.as_str()).expect("Can't connect to RIS Live websocket server"); 84 | 85 | let mut subscribe_msg = RisSubscribe::new(); 86 | if opts.host == "all" { 87 | subscribe_msg.host = None; 88 | } else { 89 | subscribe_msg.host = Some(opts.host.clone()); 90 | } 91 | if let Some(msg_type) = &opts.msg_type { 92 | subscribe_msg.data_type = match msg_type.as_str() { 93 | "UPDATE" => Some(RisSubscribeType::UPDATE), 94 | "OPEN" => Some(RisSubscribeType::OPEN), 95 | "NOTIFICATION" => Some(RisSubscribeType::NOTIFICATION), 96 | "KEEPALIVE" => Some(RisSubscribeType::KEEPALIVE), 97 | "RIS_PEER_STATE" => Some(RisSubscribeType::RIS_PEER_STATE), 98 | _ => None, 99 | }; 100 | } 101 | 102 | if let Some(require) = &opts.require { 103 | subscribe_msg.require = Some(require.to_string()); 104 | } 105 | if let Some(peer) = &opts.peer { 106 | subscribe_msg.peer = Some(IpAddr::from_str(peer).unwrap()); 107 | } 108 | if let Some(prefix) = &opts.prefix { 109 | subscribe_msg.prefix = Some(IpNet::from_str(prefix).unwrap()); 110 | } 111 | if let Some(path) = &opts.path { 112 | subscribe_msg.path = Some(path.to_string()); 113 | } 114 | if opts.more_specific { 115 | subscribe_msg.more_specific = Some(true); 116 | } 117 | if opts.less_specific { 118 | subscribe_msg.less_specific = Some(true); 119 | } 120 | socket 121 | .send(Message::Text(subscribe_msg.to_json_string())) 122 | .unwrap(); 123 | 124 | loop { 125 | let msg = socket.read().expect("Error reading message").to_string(); 126 | if msg.is_empty() { 127 | continue; 128 | } 129 | if opts.raw { 130 | println!("{}", msg.as_str()); 131 | continue; 132 | } 133 | match parse_ris_live_message(msg.as_str()) { 134 | Ok(elems) => { 135 | for e in elems { 136 | if let Some(t) = &opts.update_type { 137 | match t.to_lowercase().chars().next().unwrap() { 138 | 'a' => match e.elem_type { 139 | ElemType::ANNOUNCE => {} 140 | ElemType::WITHDRAW => continue, 141 | }, 142 | 'w' => match e.elem_type { 143 | ElemType::ANNOUNCE => continue, 144 | ElemType::WITHDRAW => { 145 | dbg!("withdrawal appeared"); 146 | } 147 | }, 148 | _ => { 149 | panic!("the update types can only be announce or withdrawal") 150 | } 151 | } 152 | } 153 | 154 | if opts.json { 155 | if opts.pretty { 156 | println!("{}", serde_json::to_string_pretty(&e).unwrap()); 157 | } else { 158 | println!("{}", serde_json::json!(e)); 159 | } 160 | } else { 161 | println!("{}", e); 162 | } 163 | } 164 | } 165 | Err(error) => { 166 | if let ParserRisliveError::ElemEndOfRibPrefix = error { 167 | println!("{:?}", &error); 168 | println!("{}", msg); 169 | continue; 170 | } 171 | break; 172 | } 173 | } 174 | } 175 | } 176 | --------------------------------------------------------------------------------