├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── periodic.yml │ ├── regression.yml │ ├── dependabot_merge.yml │ └── release.yml ├── grafana ├── screenshot.png └── export.json ├── .cargo └── config ├── example ├── softether_exporter.service └── config.toml ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── Makefile ├── Cargo.toml ├── src ├── main.rs ├── exporter.rs └── softether_reader.rs ├── README.md └── Cargo.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dalance 4 | -------------------------------------------------------------------------------- /grafana/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/softether_exporter/HEAD/grafana/screenshot.png -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-gnu] 2 | linker = "x86_64-w64-mingw32-gcc" 3 | 4 | [target.i686-pc-windows-gnu] 5 | linker = "i686-w64-mingw32-gcc" 6 | 7 | -------------------------------------------------------------------------------- /example/softether_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus SoftEther Exporter 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/softether_exporter /etc/prometheus/softether.toml 7 | User=nobody 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /example/config.toml: -------------------------------------------------------------------------------- 1 | vpncmd = "/usr/local/bin/vpncmd" # path to vpncmd binary 2 | server = "localhost:8888" # address:port of SoftEther VPN server 3 | 4 | [[hubs]] 5 | name = "HUB1" # HUB name 6 | password = "xxx" # HUB password 7 | 8 | [[hubs]] 9 | name = "HUB2" 10 | password = "yyy" 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/dalance/softether_exporter/compare/v0.2.0...Unreleased) - ReleaseDate 4 | 5 | ## [v0.2.0](https://github.com/dalance/softether_exporter/compare/v0.9.20...v0.2.0) - 2020-04-08 6 | 7 | * [Changed] command-line options 8 | * [Added] softether_user_transfer_bytes / softether_user_transfer_bytes 9 | -------------------------------------------------------------------------------- /.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 http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | #Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: hyper 11 | versions: 12 | - ">= 0.11.a, < 0.12" 13 | - dependency-name: hyper 14 | versions: 15 | - ">= 0.12.a, < 0.13" 16 | - dependency-name: hyper 17 | versions: 18 | - ">= 0.13.a, < 0.14" 19 | - dependency-name: hyper 20 | versions: 21 | - ">= 0.14.a, < 0.15" 22 | -------------------------------------------------------------------------------- /.github/workflows/periodic.yml: -------------------------------------------------------------------------------- 1 | name: Periodic 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0 * * SUN 6 | 7 | jobs: 8 | build: 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | rust: [stable, beta, nightly] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Setup Rust 19 | uses: hecrj/setup-rust-action@v1 20 | with: 21 | rust-version: ${{ matrix.rust }} 22 | - name: Checkout 23 | uses: actions/checkout@v1 24 | - name: Run tests 25 | run: | 26 | cargo update 27 | cargo test 28 | -------------------------------------------------------------------------------- /.github/workflows/regression.yml: -------------------------------------------------------------------------------- 1 | name: Regression 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macOS-latest, windows-latest] 15 | rust: [stable] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Setup Rust 21 | uses: hecrj/setup-rust-action@v1 22 | with: 23 | rust-version: ${{ matrix.rust }} 24 | - name: Checkout 25 | uses: actions/checkout@v1 26 | - name: Run tests 27 | run: cargo test 28 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.2.0 16 | with: 17 | github-token: '${{ secrets.GITHUB_TOKEN }}' 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || ( !startsWith( steps.metadata.outputs.new-version, '0.' ) && steps.metadata.outputs.update-type == 'version-update:semver-minor' ) }} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macOS-latest, windows-latest] 14 | rust: [stable] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - name: Setup Rust 20 | uses: hecrj/setup-rust-action@v1 21 | with: 22 | rust-version: ${{ matrix.rust }} 23 | - name: Checkout 24 | uses: actions/checkout@v1 25 | - name: Setup MUSL 26 | if: matrix.os == 'ubuntu-latest' 27 | run: | 28 | rustup target add x86_64-unknown-linux-musl 29 | sudo apt-get -qq install musl-tools 30 | - name: Build for linux 31 | if: matrix.os == 'ubuntu-latest' 32 | run: make release_lnx 33 | - name: Build for macOS 34 | if: matrix.os == 'macOS-latest' 35 | run: make release_mac 36 | - name: Build for Windows 37 | if: matrix.os == 'windows-latest' 38 | run: make release_win 39 | - name: Release 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | body: '[Changelog](https://github.com/dalance/procs/blob/master/CHANGELOG.md)' 43 | files: "*.zip\n*.rpm" 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(patsubst "%",%, $(word 3, $(shell grep version Cargo.toml))) 2 | BUILD_TIME = $(shell date +"%Y/%m/%d %H:%M:%S") 3 | GIT_REVISION = $(shell git log -1 --format="%h") 4 | RUST_VERSION = $(word 2, $(shell rustc -V)) 5 | LONG_VERSION = "$(VERSION) ( rev: $(GIT_REVISION), rustc: $(RUST_VERSION), build at: $(BUILD_TIME) )" 6 | BIN_NAME = softether_exporter 7 | 8 | export LONG_VERSION 9 | 10 | .PHONY: all test clean release_lnx release_win release_mac 11 | 12 | all: test 13 | 14 | test: 15 | cargo test --locked 16 | 17 | watch: 18 | cargo watch test --locked 19 | 20 | clean: 21 | cargo clean 22 | 23 | release_lnx: 24 | cargo build --locked --release --target=x86_64-unknown-linux-musl 25 | zip -j ${BIN_NAME}-v${VERSION}-x86_64-lnx.zip target/x86_64-unknown-linux-musl/release/${BIN_NAME} 26 | 27 | release_win: 28 | cargo build --locked --release --target=x86_64-pc-windows-msvc 29 | 7z a ${BIN_NAME}-v${VERSION}-x86_64-win.zip target/x86_64-pc-windows-msvc/release/${BIN_NAME}.exe 30 | 31 | release_mac: 32 | cargo build --locked --release --target=x86_64-apple-darwin 33 | zip -j ${BIN_NAME}-v${VERSION}-x86_64-mac.zip target/x86_64-apple-darwin/release/${BIN_NAME} 34 | 35 | release_rpm: 36 | mkdir -p target 37 | cargo rpm build 38 | cp target/x86_64-unknown-linux-musl/release/rpmbuild/RPMS/x86_64/* ./ 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "softether_exporter" 3 | version = "0.2.1-pre" 4 | authors = ["dalance "] 5 | repository = "https://github.com/dalance/softether_exporter" 6 | license = "MIT" 7 | readme = "README.md" 8 | description = "Prometheus expoter for SoftEther VPN server" 9 | categories = ["web-programming"] 10 | edition = "2018" 11 | exclude = ["grafana/*", "example/*"] 12 | 13 | [badges] 14 | travis-ci = { repository = "dalance/softether_exporter" } 15 | 16 | [dependencies] 17 | anyhow = "1" 18 | csv = "1" 19 | hyper = { version = "0.10", default-features = false } 20 | lazy_static = "1" 21 | prometheus = "0.14" 22 | serde = {version = "1.0", features = ["derive"]} 23 | structopt = "0.3" 24 | toml = "0.9" 25 | 26 | [package.metadata.release] 27 | dev-version-ext = "pre" 28 | pre-release-commit-message = "Prepare to v{{version}}" 29 | post-release-commit-message = "Start next development iteration v{{version}}" 30 | tag-message = "Bump version to {{version}}" 31 | tag-prefix = "" 32 | pre-release-replacements = [ 33 | {file="CHANGELOG.md", search="Unreleased", replace="v{{version}}"}, 34 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, 35 | {file="CHANGELOG.md", search="Change Log", replace="Change Log\n\n## [Unreleased](https://github.com/dalance/softether_exporter/compare/v{{version}}...Unreleased) - ReleaseDate"}, 36 | ] 37 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod exporter; 2 | mod softether_reader; 3 | 4 | use crate::exporter::{Config, Exporter}; 5 | use anyhow::Error; 6 | use std::env; 7 | use std::path::PathBuf; 8 | use structopt::{clap, StructOpt}; 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | // Opt 12 | // ------------------------------------------------------------------------------------------------- 13 | 14 | #[derive(Debug, StructOpt)] 15 | #[structopt(long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))))] 16 | #[structopt(setting(clap::AppSettings::ColoredHelp))] 17 | #[structopt(setting(clap::AppSettings::DeriveDisplayOrder))] 18 | pub struct Opt { 19 | /// Address on which to expose metrics and web interface. 20 | #[structopt(long = "web.listen-address", default_value = ":9411")] 21 | pub listen_address: String, 22 | 23 | /// Config file. 24 | #[structopt(long = "config.file")] 25 | pub config: PathBuf, 26 | 27 | /// Show verbose message 28 | #[structopt(short = "v", long = "verbose")] 29 | pub verbose: bool, 30 | } 31 | 32 | // ------------------------------------------------------------------------------------------------- 33 | // Main 34 | // ------------------------------------------------------------------------------------------------- 35 | 36 | fn run() -> Result<(), Error> { 37 | let opt = Opt::from_args(); 38 | 39 | let config = Config::from_file(&opt.config)?; 40 | 41 | Exporter::start(config, &opt.listen_address, opt.verbose)?; 42 | Ok(()) 43 | } 44 | 45 | fn main() { 46 | if let Err(x) = run() { 47 | println!("{}", x); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # softether_exporter 2 | [Prometheus](https://prometheus.io) exporter for [SoftEther VPN server](http://www.softether.org) 3 | 4 | [![Actions Status](https://github.com/dalance/softether_exporter/workflows/Regression/badge.svg)](https://github.com/dalance/softether_exporter/actions) 5 | [![Crates.io](https://img.shields.io/crates/v/softether_exporter.svg)](https://crates.io/crates/softether_exporter) 6 | 7 | ![screenshot](./grafana/screenshot.png) 8 | 9 | ## Exported Metrics 10 | 11 | | metric | description | labels | 12 | | ------------------------------------ | -------------------------------------- | ------------------------------ | 13 | | softether_up | The last query is successful | hub | 14 | | softether_online | Hub is online | hub | 15 | | softether_sessions | Number of sessions | hub | 16 | | softether_sessions_client | Number of client sessions | hub | 17 | | softether_sessions_bridge | Number of bridge sessions | hub | 18 | | softether_users | Number of users | hub | 19 | | softether_groups | Number of groups | hub | 20 | | softether_mac_tables | Number of entries in MAC table | hub | 21 | | softether_ip_tables | Number of entries in IP table | hub | 22 | | softether_logins | Number of logins | hub | 23 | | softether_outgoing_unicast_packets | Outgoing unicast transfer in packets | hub | 24 | | softether_outgoing_unicast_bytes | Outgoing unicast transfer in bytes | hub | 25 | | softether_outgoing_broadcast_packets | Outgoing broadcast transfer in packets | hub | 26 | | softether_outgoing_broadcast_bytes | Outgoing broadcast transfer in bytes | hub | 27 | | softether_incoming_unicast_packets | Incoming unicast transfer in packets | hub | 28 | | softether_incoming_unicast_bytes | Incoming unicast transfer in bytes | hub | 29 | | softether_incoming_broadcast_packets | Incoming broadcast transfer in packets | hub | 30 | | softether_incoming_broadcast_bytes | Incoming broadcast transfer in bytes | hub | 31 | | softether_build_info | softether_exporter Build information | version, revision, rustversion | 32 | | softether_user_transfer_packets | User transfer in packets | hub, user | 33 | | softether_user_transfer_bytes | User transfer in bytes | hub, user | 34 | 35 | ## Query Example 36 | 37 | Outgoing unicast packet rate of HUB1 is below. 38 | 39 | ``` 40 | rate(softether_outgoing_unicast_packets{hub="HUB1"}[1m]) 41 | ``` 42 | 43 | ## Grafana Dashboard 44 | 45 | [SoftEther VPN](https://grafana.com/grafana/dashboards/12053) 46 | 47 | ## Install 48 | Download from [release page](https://github.com/dalance/softether_exporter/releases/latest), and extract to any directory ( e.g. `/usr/local/bin` ). 49 | See the example files in `example` directory as below. 50 | 51 | | File | Description | 52 | | ---------------------------------- | ------------------------------ | 53 | | example/softether_exporter.service | systemd unit file | 54 | | example/config.toml | softether_exporter config file | 55 | 56 | 57 | If the release build doesn't fit your environment, you can build and install from source code. 58 | 59 | ``` 60 | cargo install softether_exporter 61 | ``` 62 | 63 | ## Requirement 64 | 65 | softether_exporter uses `vpncmd` or `vpncmd.exe` to access SoftEther VPN server. 66 | The binary can be got from [SoftEther VPN Download](http://www.softether-download.com/?product=softether). 67 | 68 | ## Usage 69 | 70 | ``` 71 | softether_exporter 0.1.5 72 | 73 | USAGE: 74 | softether_exporter [FLAGS] [OPTIONS] --config.file 75 | 76 | FLAGS: 77 | -v, --verbose Show verbose message 78 | -h, --help Prints help information 79 | -V, --version Prints version information 80 | 81 | OPTIONS: 82 | --web.listen-address 83 | Address on which to expose metrics and web interface [default: :9411] 84 | 85 | --config.file Config file 86 | ``` 87 | 88 | The format of `` is below. 89 | 90 | ``` 91 | vpncmd = "/usr/local/bin/vpncmd" # path to vpncmd binary 92 | server = "localhost:8888" # address:port of SoftEther VPN server 93 | 94 | [[hubs]] 95 | name = "HUB1" # HUB name 96 | password = "xxx" # HUB password 97 | 98 | [[hubs]] 99 | name = "HUB2" 100 | password = "yyy" 101 | ``` 102 | -------------------------------------------------------------------------------- /src/exporter.rs: -------------------------------------------------------------------------------- 1 | use crate::softether_reader::SoftEtherReader; 2 | use anyhow::Error; 3 | use hyper::header::ContentType; 4 | use hyper::mime::{Mime, SubLevel, TopLevel}; 5 | use hyper::server::{Request, Response, Server}; 6 | use hyper::uri::RequestUri; 7 | use lazy_static::lazy_static; 8 | use prometheus; 9 | use prometheus::{register_gauge_vec, Encoder, GaugeVec, TextEncoder}; 10 | use serde::Deserialize; 11 | use std::collections::HashMap; 12 | use std::fs::File; 13 | use std::io::Read; 14 | use std::path::Path; 15 | use toml; 16 | 17 | lazy_static! { 18 | static ref UP: GaugeVec = 19 | register_gauge_vec!("softether_up", "The last query is successful.", &["hub"]).unwrap(); 20 | static ref ONLINE: GaugeVec = 21 | register_gauge_vec!("softether_online", "Hub online.", &["hub"]).unwrap(); 22 | static ref SESSIONS: GaugeVec = 23 | register_gauge_vec!("softether_sessions", "Number of sessions.", &["hub"]).unwrap(); 24 | static ref SESSIONS_CLIENT: GaugeVec = register_gauge_vec!( 25 | "softether_sessions_client", 26 | "Number of client sessions.", 27 | &["hub"] 28 | ) 29 | .unwrap(); 30 | static ref SESSIONS_BRIDGE: GaugeVec = register_gauge_vec!( 31 | "softether_sessions_bridge", 32 | "Number of bridge sessions.", 33 | &["hub"] 34 | ) 35 | .unwrap(); 36 | static ref USERS: GaugeVec = 37 | register_gauge_vec!("softether_users", "Number of users.", &["hub"]).unwrap(); 38 | static ref GROUPS: GaugeVec = 39 | register_gauge_vec!("softether_groups", "Number of groups.", &["hub"]).unwrap(); 40 | static ref MAC_TABLES: GaugeVec = register_gauge_vec!( 41 | "softether_mac_tables", 42 | "Number of entries in MAC table.", 43 | &["hub"] 44 | ) 45 | .unwrap(); 46 | static ref IP_TABLES: GaugeVec = register_gauge_vec!( 47 | "softether_ip_tables", 48 | "Number of entries in IP table.", 49 | &["hub"] 50 | ) 51 | .unwrap(); 52 | static ref LOGINS: GaugeVec = 53 | register_gauge_vec!("softether_logins", "Number of logins.", &["hub"]).unwrap(); 54 | static ref OUTGOING_UNICAST_PACKETS: GaugeVec = register_gauge_vec!( 55 | "softether_outgoing_unicast_packets", 56 | "Outgoing unicast transfer in packets.", 57 | &["hub"] 58 | ) 59 | .unwrap(); 60 | static ref OUTGOING_UNICAST_BYTES: GaugeVec = register_gauge_vec!( 61 | "softether_outgoing_unicast_bytes", 62 | "Outgoing unicast transfer in bytes.", 63 | &["hub"] 64 | ) 65 | .unwrap(); 66 | static ref OUTGOING_BROADCAST_PACKETS: GaugeVec = register_gauge_vec!( 67 | "softether_outgoing_broadcast_packets", 68 | "Outgoing broadcast transfer in packets.", 69 | &["hub"] 70 | ) 71 | .unwrap(); 72 | static ref OUTGOING_BROADCAST_BYTES: GaugeVec = register_gauge_vec!( 73 | "softether_outgoing_broadcast_bytes", 74 | "Outgoing broadcast transfer in bytes.", 75 | &["hub"] 76 | ) 77 | .unwrap(); 78 | static ref INCOMING_UNICAST_PACKETS: GaugeVec = register_gauge_vec!( 79 | "softether_incoming_unicast_packets", 80 | "Incoming unicast transfer in packets.", 81 | &["hub"] 82 | ) 83 | .unwrap(); 84 | static ref INCOMING_UNICAST_BYTES: GaugeVec = register_gauge_vec!( 85 | "softether_incoming_unicast_bytes", 86 | "Incoming unicast transfer in bytes.", 87 | &["hub"] 88 | ) 89 | .unwrap(); 90 | static ref INCOMING_BROADCAST_PACKETS: GaugeVec = register_gauge_vec!( 91 | "softether_incoming_broadcast_packets", 92 | "Incoming broadcast transfer in packets.", 93 | &["hub"] 94 | ) 95 | .unwrap(); 96 | static ref INCOMING_BROADCAST_BYTES: GaugeVec = register_gauge_vec!( 97 | "softether_incoming_broadcast_bytes", 98 | "Incoming broadcast transfer in bytes.", 99 | &["hub"] 100 | ) 101 | .unwrap(); 102 | static ref BUILD_INFO: GaugeVec = register_gauge_vec!( 103 | "softether_build_info", 104 | "A metric with a constant '1' value labeled by version, revision and rustversion", 105 | &["version", "revision", "rustversion"] 106 | ) 107 | .unwrap(); 108 | static ref USER_TRANSFER_BYTES: GaugeVec = register_gauge_vec!( 109 | "softether_user_transfer_bytes", 110 | "User transfer in bytes.", 111 | &["hub", "user"] 112 | ) 113 | .unwrap(); 114 | static ref USER_TRANSFER_PACKETS: GaugeVec = register_gauge_vec!( 115 | "softether_user_transfer_packets", 116 | "User transfer in packets.", 117 | &["hub", "user"] 118 | ) 119 | .unwrap(); 120 | } 121 | 122 | static LANDING_PAGE: &'static str = " 123 | SoftEther Exporter 124 | 125 |

SoftEther Exporter

126 |

Metrics

127 | 128 | "; 129 | 130 | static VERSION: &'static str = env!("CARGO_PKG_VERSION"); 131 | static GIT_REVISION: Option<&'static str> = option_env!("GIT_REVISION"); 132 | static RUST_VERSION: Option<&'static str> = option_env!("RUST_VERSION"); 133 | 134 | #[derive(Debug, Deserialize)] 135 | pub struct Config { 136 | vpncmd: Option, 137 | server: Option, 138 | hubs: Vec, 139 | } 140 | 141 | #[derive(Debug, Deserialize, Clone)] 142 | pub struct Hub { 143 | name: Option, 144 | password: Option, 145 | } 146 | 147 | impl Config { 148 | pub fn from_file(file: &Path) -> Result { 149 | let mut f = File::open(file)?; 150 | let mut s = String::new(); 151 | let _ = f.read_to_string(&mut s); 152 | let config: Config = toml::from_str(&s)?; 153 | Ok(config) 154 | } 155 | } 156 | 157 | pub struct Exporter; 158 | 159 | impl Exporter { 160 | pub fn start(config: Config, listen_address: &str, _verbose: bool) -> Result<(), Error> { 161 | let encoder = TextEncoder::new(); 162 | let vpncmd = config.vpncmd.unwrap_or(String::from("vpncmd")); 163 | let server = config.server.unwrap_or(String::from("localhost")); 164 | let hubs = config.hubs; 165 | 166 | let addr = if listen_address.starts_with(":") { 167 | format!("0.0.0.0{}", listen_address) 168 | } else { 169 | String::from(listen_address) 170 | }; 171 | 172 | println!("Server started: {}", addr); 173 | 174 | Server::http(addr)?.handle(move |req: Request, mut res: Response| { 175 | if req.uri == RequestUri::AbsolutePath("/metrics".to_string()) { 176 | for hub in hubs.clone() { 177 | let name = hub.name.unwrap_or(String::from("")); 178 | let password = hub.password.unwrap_or(String::from("")); 179 | let status = 180 | match SoftEtherReader::hub_status(&vpncmd, &server, &name, &password) { 181 | Ok(x) => x, 182 | Err(x) => { 183 | UP.with_label_values(&[&name]).set(0.0); 184 | println!("Hub status read failed: {}", x); 185 | continue; 186 | } 187 | }; 188 | 189 | let sessions = 190 | match SoftEtherReader::hub_sessions(&vpncmd, &server, &name, &password) { 191 | Ok(x) => x, 192 | Err(x) => { 193 | UP.with_label_values(&[&name]).set(0.0); 194 | println!("Hub sessions read failed: {}", x); 195 | continue; 196 | } 197 | }; 198 | 199 | UP.with_label_values(&[&status.name]).set(1.0); 200 | ONLINE 201 | .with_label_values(&[&status.name]) 202 | .set(if status.online { 1.0 } else { 0.0 }); 203 | SESSIONS 204 | .with_label_values(&[&status.name]) 205 | .set(status.sessions); 206 | SESSIONS_CLIENT 207 | .with_label_values(&[&status.name]) 208 | .set(status.sessions_client); 209 | SESSIONS_BRIDGE 210 | .with_label_values(&[&status.name]) 211 | .set(status.sessions_bridge); 212 | USERS.with_label_values(&[&status.name]).set(status.users); 213 | GROUPS.with_label_values(&[&status.name]).set(status.groups); 214 | MAC_TABLES 215 | .with_label_values(&[&status.name]) 216 | .set(status.mac_tables); 217 | IP_TABLES 218 | .with_label_values(&[&status.name]) 219 | .set(status.ip_tables); 220 | LOGINS.with_label_values(&[&status.name]).set(status.logins); 221 | OUTGOING_UNICAST_PACKETS 222 | .with_label_values(&[&status.name]) 223 | .set(status.outgoing_unicast_packets); 224 | OUTGOING_UNICAST_BYTES 225 | .with_label_values(&[&status.name]) 226 | .set(status.outgoing_unicast_bytes); 227 | OUTGOING_BROADCAST_PACKETS 228 | .with_label_values(&[&status.name]) 229 | .set(status.outgoing_broadcast_packets); 230 | OUTGOING_BROADCAST_BYTES 231 | .with_label_values(&[&status.name]) 232 | .set(status.outgoing_broadcast_bytes); 233 | INCOMING_UNICAST_PACKETS 234 | .with_label_values(&[&status.name]) 235 | .set(status.incoming_unicast_packets); 236 | INCOMING_UNICAST_BYTES 237 | .with_label_values(&[&status.name]) 238 | .set(status.incoming_unicast_bytes); 239 | INCOMING_BROADCAST_PACKETS 240 | .with_label_values(&[&status.name]) 241 | .set(status.incoming_broadcast_packets); 242 | INCOMING_BROADCAST_BYTES 243 | .with_label_values(&[&status.name]) 244 | .set(status.incoming_broadcast_bytes); 245 | 246 | let mut transfer_bytes = HashMap::new(); 247 | let mut transfer_packets = HashMap::new(); 248 | for session in sessions { 249 | if let Some(val) = transfer_bytes.get(&session.user) { 250 | let val = val + session.transfer_bytes; 251 | transfer_bytes.insert(session.user.clone(), val); 252 | } else { 253 | let val = session.transfer_bytes; 254 | transfer_bytes.insert(session.user.clone(), val); 255 | } 256 | if let Some(val) = transfer_packets.get(&session.user) { 257 | let val = val + session.transfer_packets; 258 | transfer_packets.insert(session.user.clone(), val); 259 | } else { 260 | let val = session.transfer_packets; 261 | transfer_packets.insert(session.user.clone(), val); 262 | } 263 | } 264 | for (user, bytes) in &transfer_bytes { 265 | USER_TRANSFER_BYTES 266 | .with_label_values(&[&status.name, user]) 267 | .set(*bytes); 268 | } 269 | for (user, packets) in &transfer_packets { 270 | USER_TRANSFER_PACKETS 271 | .with_label_values(&[&status.name, user]) 272 | .set(*packets); 273 | } 274 | } 275 | 276 | let git_revision = GIT_REVISION.unwrap_or(""); 277 | let rust_version = RUST_VERSION.unwrap_or(""); 278 | BUILD_INFO 279 | .with_label_values(&[&VERSION, &git_revision, &rust_version]) 280 | .set(1.0); 281 | 282 | let metric_familys = prometheus::gather(); 283 | let mut buffer = vec![]; 284 | encoder.encode(&metric_familys, &mut buffer).unwrap(); 285 | res.headers_mut() 286 | .set(ContentType(encoder.format_type().parse::().unwrap())); 287 | res.send(&buffer).unwrap(); 288 | } else { 289 | res.headers_mut() 290 | .set(ContentType(Mime(TopLevel::Text, SubLevel::Html, vec![]))); 291 | res.send(LANDING_PAGE.as_bytes()).unwrap(); 292 | } 293 | })?; 294 | 295 | Ok(()) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /grafana/export.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS", 5 | "label": "DS", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "panel", 15 | "id": "bargauge", 16 | "name": "Bar Gauge", 17 | "version": "" 18 | }, 19 | { 20 | "type": "grafana", 21 | "id": "grafana", 22 | "name": "Grafana", 23 | "version": "6.7.2" 24 | }, 25 | { 26 | "type": "panel", 27 | "id": "graph", 28 | "name": "Graph", 29 | "version": "" 30 | }, 31 | { 32 | "type": "datasource", 33 | "id": "prometheus", 34 | "name": "Prometheus", 35 | "version": "1.0.0" 36 | }, 37 | { 38 | "type": "panel", 39 | "id": "stat", 40 | "name": "Stat", 41 | "version": "" 42 | } 43 | ], 44 | "annotations": { 45 | "list": [ 46 | { 47 | "$$hashKey": "object:700", 48 | "builtIn": 1, 49 | "datasource": "-- Grafana --", 50 | "enable": true, 51 | "hide": true, 52 | "iconColor": "rgba(0, 211, 255, 1)", 53 | "name": "Annotations & Alerts", 54 | "type": "dashboard" 55 | } 56 | ] 57 | }, 58 | "editable": true, 59 | "gnetId": null, 60 | "graphTooltip": 0, 61 | "id": null, 62 | "links": [], 63 | "panels": [ 64 | { 65 | "cacheTimeout": null, 66 | "datasource": "${DS}", 67 | "gridPos": { 68 | "h": 4, 69 | "w": 12, 70 | "x": 0, 71 | "y": 0 72 | }, 73 | "id": 10, 74 | "links": [], 75 | "options": { 76 | "colorMode": "value", 77 | "fieldOptions": { 78 | "calcs": [ 79 | "mean" 80 | ], 81 | "defaults": { 82 | "mappings": [ 83 | { 84 | "$$hashKey": "object:1441", 85 | "id": 0, 86 | "op": "=", 87 | "text": "Down", 88 | "type": 1, 89 | "value": "0" 90 | }, 91 | { 92 | "$$hashKey": "object:1443", 93 | "id": 1, 94 | "op": "=", 95 | "text": "Up", 96 | "type": 1, 97 | "value": "1" 98 | } 99 | ], 100 | "nullValueMode": "connected", 101 | "thresholds": { 102 | "mode": "absolute", 103 | "steps": [ 104 | { 105 | "color": "#299c46", 106 | "value": null 107 | }, 108 | { 109 | "color": "rgba(237, 129, 40, 0.89)", 110 | "value": 1.2 111 | }, 112 | { 113 | "color": "#d44a3a", 114 | "value": 1.8 115 | } 116 | ] 117 | }, 118 | "unit": "none" 119 | }, 120 | "overrides": [], 121 | "values": false 122 | }, 123 | "graphMode": "area", 124 | "justifyMode": "auto", 125 | "orientation": "auto" 126 | }, 127 | "pluginVersion": "6.7.2", 128 | "targets": [ 129 | { 130 | "expr": "avg(softether_online) by (hub)", 131 | "interval": "", 132 | "legendFormat": "{{hub}}", 133 | "refId": "A" 134 | } 135 | ], 136 | "timeFrom": null, 137 | "timeShift": null, 138 | "title": "Hub Status", 139 | "type": "stat" 140 | }, 141 | { 142 | "aliasColors": {}, 143 | "bars": false, 144 | "dashLength": 10, 145 | "dashes": false, 146 | "datasource": "${DS}", 147 | "fill": 1, 148 | "fillGradient": 0, 149 | "gridPos": { 150 | "h": 8, 151 | "w": 12, 152 | "x": 12, 153 | "y": 0 154 | }, 155 | "hiddenSeries": false, 156 | "id": 4, 157 | "legend": { 158 | "avg": false, 159 | "current": false, 160 | "max": false, 161 | "min": false, 162 | "show": true, 163 | "total": false, 164 | "values": false 165 | }, 166 | "lines": true, 167 | "linewidth": 1, 168 | "nullPointMode": "null", 169 | "options": { 170 | "dataLinks": [] 171 | }, 172 | "percentage": false, 173 | "pointradius": 2, 174 | "points": false, 175 | "renderer": "flot", 176 | "seriesOverrides": [], 177 | "spaceLength": 10, 178 | "stack": false, 179 | "steppedLine": false, 180 | "targets": [ 181 | { 182 | "expr": "sum(softether_sessions_client) by (hub)", 183 | "interval": "", 184 | "legendFormat": "{{hub}}", 185 | "refId": "A" 186 | } 187 | ], 188 | "thresholds": [], 189 | "timeFrom": null, 190 | "timeRegions": [], 191 | "timeShift": null, 192 | "title": "Sessions", 193 | "tooltip": { 194 | "shared": true, 195 | "sort": 0, 196 | "value_type": "individual" 197 | }, 198 | "type": "graph", 199 | "xaxis": { 200 | "buckets": null, 201 | "mode": "time", 202 | "name": null, 203 | "show": true, 204 | "values": [] 205 | }, 206 | "yaxes": [ 207 | { 208 | "$$hashKey": "object:769", 209 | "format": "short", 210 | "label": null, 211 | "logBase": 1, 212 | "max": null, 213 | "min": null, 214 | "show": true 215 | }, 216 | { 217 | "$$hashKey": "object:770", 218 | "format": "short", 219 | "label": null, 220 | "logBase": 1, 221 | "max": null, 222 | "min": null, 223 | "show": true 224 | } 225 | ], 226 | "yaxis": { 227 | "align": false, 228 | "alignLevel": null 229 | } 230 | }, 231 | { 232 | "cacheTimeout": null, 233 | "datasource": "${DS}", 234 | "gridPos": { 235 | "h": 4, 236 | "w": 6, 237 | "x": 0, 238 | "y": 4 239 | }, 240 | "id": 6, 241 | "links": [], 242 | "options": { 243 | "displayMode": "lcd", 244 | "fieldOptions": { 245 | "calcs": [ 246 | "mean" 247 | ], 248 | "defaults": { 249 | "mappings": [], 250 | "thresholds": { 251 | "mode": "absolute", 252 | "steps": [ 253 | { 254 | "color": "green", 255 | "value": null 256 | }, 257 | { 258 | "color": "red", 259 | "value": 300 260 | } 261 | ] 262 | } 263 | }, 264 | "overrides": [], 265 | "values": false 266 | }, 267 | "orientation": "horizontal", 268 | "showUnfilled": true 269 | }, 270 | "pluginVersion": "6.7.2", 271 | "targets": [ 272 | { 273 | "$$hashKey": "object:1029", 274 | "aggregation": "Last", 275 | "decimals": 2, 276 | "displayAliasType": "Warning / Critical", 277 | "displayType": "Regular", 278 | "displayValueWithAlias": "Never", 279 | "expr": "sum(softether_mac_tables) by (hub)", 280 | "interval": "", 281 | "legendFormat": "{{hub}}", 282 | "refId": "A", 283 | "units": "none", 284 | "valueHandler": "Number Threshold" 285 | } 286 | ], 287 | "timeFrom": null, 288 | "timeShift": null, 289 | "title": "MAC Table", 290 | "type": "bargauge" 291 | }, 292 | { 293 | "cacheTimeout": null, 294 | "datasource": "${DS}", 295 | "gridPos": { 296 | "h": 4, 297 | "w": 6, 298 | "x": 6, 299 | "y": 4 300 | }, 301 | "id": 7, 302 | "links": [], 303 | "options": { 304 | "displayMode": "lcd", 305 | "fieldOptions": { 306 | "calcs": [ 307 | "mean" 308 | ], 309 | "defaults": { 310 | "mappings": [], 311 | "thresholds": { 312 | "mode": "absolute", 313 | "steps": [ 314 | { 315 | "color": "green", 316 | "value": null 317 | }, 318 | { 319 | "color": "red", 320 | "value": 300 321 | } 322 | ] 323 | } 324 | }, 325 | "overrides": [], 326 | "values": false 327 | }, 328 | "orientation": "horizontal", 329 | "showUnfilled": true 330 | }, 331 | "pluginVersion": "6.7.2", 332 | "targets": [ 333 | { 334 | "$$hashKey": "object:1029", 335 | "aggregation": "Last", 336 | "decimals": 2, 337 | "displayAliasType": "Warning / Critical", 338 | "displayType": "Regular", 339 | "displayValueWithAlias": "Never", 340 | "expr": "sum(softether_ip_tables) by (hub)", 341 | "interval": "", 342 | "legendFormat": "{{hub}}", 343 | "refId": "A", 344 | "units": "none", 345 | "valueHandler": "Number Threshold" 346 | } 347 | ], 348 | "timeFrom": null, 349 | "timeShift": null, 350 | "title": "IP Table", 351 | "type": "bargauge" 352 | }, 353 | { 354 | "aliasColors": {}, 355 | "bars": false, 356 | "dashLength": 10, 357 | "dashes": false, 358 | "datasource": "${DS}", 359 | "fill": 1, 360 | "fillGradient": 0, 361 | "gridPos": { 362 | "h": 8, 363 | "w": 24, 364 | "x": 0, 365 | "y": 8 366 | }, 367 | "hiddenSeries": false, 368 | "id": 2, 369 | "legend": { 370 | "avg": false, 371 | "current": false, 372 | "max": false, 373 | "min": false, 374 | "show": true, 375 | "total": false, 376 | "values": false 377 | }, 378 | "lines": true, 379 | "linewidth": 1, 380 | "nullPointMode": "null", 381 | "options": { 382 | "dataLinks": [] 383 | }, 384 | "percentage": false, 385 | "pointradius": 2, 386 | "points": false, 387 | "renderer": "flot", 388 | "seriesOverrides": [], 389 | "spaceLength": 10, 390 | "stack": false, 391 | "steppedLine": false, 392 | "targets": [ 393 | { 394 | "expr": "sum(rate(softether_incoming_broadcast_bytes[1m]) + rate(softether_incoming_unicast_bytes[1m])) by (hub)", 395 | "interval": "", 396 | "legendFormat": "incoming-{{hub}}", 397 | "refId": "A" 398 | }, 399 | { 400 | "expr": "sum(rate(softether_outgoing_broadcast_bytes[1m]) + rate(softether_outgoing_unicast_bytes[1m])) by (hub)", 401 | "interval": "", 402 | "legendFormat": "outgoing-{{hub}}", 403 | "refId": "B" 404 | } 405 | ], 406 | "thresholds": [], 407 | "timeFrom": null, 408 | "timeRegions": [], 409 | "timeShift": null, 410 | "title": "Hub Traffic", 411 | "tooltip": { 412 | "shared": true, 413 | "sort": 0, 414 | "value_type": "individual" 415 | }, 416 | "type": "graph", 417 | "xaxis": { 418 | "buckets": null, 419 | "mode": "time", 420 | "name": null, 421 | "show": true, 422 | "values": [] 423 | }, 424 | "yaxes": [ 425 | { 426 | "$$hashKey": "object:769", 427 | "format": "Bps", 428 | "label": null, 429 | "logBase": 1, 430 | "max": null, 431 | "min": null, 432 | "show": true 433 | }, 434 | { 435 | "$$hashKey": "object:770", 436 | "format": "short", 437 | "label": null, 438 | "logBase": 1, 439 | "max": null, 440 | "min": null, 441 | "show": true 442 | } 443 | ], 444 | "yaxis": { 445 | "align": false, 446 | "alignLevel": null 447 | } 448 | }, 449 | { 450 | "aliasColors": {}, 451 | "bars": false, 452 | "dashLength": 10, 453 | "dashes": false, 454 | "datasource": "${DS}", 455 | "fill": 1, 456 | "fillGradient": 0, 457 | "gridPos": { 458 | "h": 8, 459 | "w": 24, 460 | "x": 0, 461 | "y": 16 462 | }, 463 | "hiddenSeries": false, 464 | "id": 3, 465 | "legend": { 466 | "avg": false, 467 | "current": false, 468 | "max": false, 469 | "min": false, 470 | "show": true, 471 | "total": false, 472 | "values": false 473 | }, 474 | "lines": true, 475 | "linewidth": 1, 476 | "nullPointMode": "null", 477 | "options": { 478 | "dataLinks": [] 479 | }, 480 | "percentage": false, 481 | "pointradius": 2, 482 | "points": false, 483 | "renderer": "flot", 484 | "seriesOverrides": [], 485 | "spaceLength": 10, 486 | "stack": false, 487 | "steppedLine": false, 488 | "targets": [ 489 | { 490 | "expr": "sum(rate(softether_user_transfer_bytes{user!=\"Local Bridge\", user !=\"SecureNAT\"}[1m])) by (user)", 491 | "interval": "", 492 | "legendFormat": "{{user}}", 493 | "refId": "A" 494 | } 495 | ], 496 | "thresholds": [], 497 | "timeFrom": null, 498 | "timeRegions": [], 499 | "timeShift": null, 500 | "title": "User Traffic", 501 | "tooltip": { 502 | "shared": true, 503 | "sort": 0, 504 | "value_type": "individual" 505 | }, 506 | "type": "graph", 507 | "xaxis": { 508 | "buckets": null, 509 | "mode": "time", 510 | "name": null, 511 | "show": true, 512 | "values": [] 513 | }, 514 | "yaxes": [ 515 | { 516 | "$$hashKey": "object:769", 517 | "format": "Bps", 518 | "label": null, 519 | "logBase": 1, 520 | "max": null, 521 | "min": null, 522 | "show": true 523 | }, 524 | { 525 | "$$hashKey": "object:770", 526 | "format": "short", 527 | "label": null, 528 | "logBase": 1, 529 | "max": null, 530 | "min": null, 531 | "show": true 532 | } 533 | ], 534 | "yaxis": { 535 | "align": false, 536 | "alignLevel": null 537 | } 538 | } 539 | ], 540 | "schemaVersion": 22, 541 | "style": "dark", 542 | "tags": [], 543 | "templating": { 544 | "list": [] 545 | }, 546 | "time": { 547 | "from": "now-6h", 548 | "to": "now" 549 | }, 550 | "timepicker": { 551 | "refresh_intervals": [ 552 | "5s", 553 | "10s", 554 | "30s", 555 | "1m", 556 | "5m", 557 | "15m", 558 | "30m", 559 | "1h", 560 | "2h", 561 | "1d" 562 | ] 563 | }, 564 | "timezone": "", 565 | "title": "SoftEther VPN", 566 | "uid": "IRZvDTCZz", 567 | "variables": { 568 | "list": [] 569 | }, 570 | "version": 1 571 | } -------------------------------------------------------------------------------- /src/softether_reader.rs: -------------------------------------------------------------------------------- 1 | use csv; 2 | use std::error::Error; 3 | use std::fmt; 4 | use std::io::Write; 5 | use std::process::{Command, Stdio}; 6 | 7 | #[derive(Debug)] 8 | pub struct SoftEtherError { 9 | msg: String, 10 | } 11 | 12 | impl fmt::Display for SoftEtherError { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | write!(f, "{}", self.msg) 15 | } 16 | } 17 | 18 | impl Error for SoftEtherError { 19 | fn description(&self) -> &str { 20 | &self.msg 21 | } 22 | } 23 | 24 | pub struct SoftEtherReader; 25 | 26 | impl SoftEtherReader { 27 | pub fn hub_status( 28 | vpncmd: &str, 29 | server: &str, 30 | hub: &str, 31 | password: &str, 32 | ) -> Result> { 33 | let mut child = Command::new(vpncmd) 34 | .arg(server) 35 | .arg("/SERVER") 36 | .arg(format!("/HUB:{}", hub)) 37 | .arg(format!("/PASSWORD:{}", password)) 38 | .arg("/CSV") 39 | .arg("/CMD") 40 | .arg("StatusGet") 41 | .stdin(Stdio::piped()) 42 | .stdout(Stdio::piped()) 43 | .spawn()?; 44 | 45 | { 46 | let stdin = child.stdin.as_mut().unwrap(); 47 | // Input Ctrl-D to interrupt password prompt 48 | stdin.write_all(&[4])?; 49 | } 50 | 51 | let output = child.wait_with_output()?; 52 | 53 | if !output.status.success() { 54 | let msg = String::from_utf8_lossy(output.stdout.as_slice()); 55 | return Err(Box::new(SoftEtherError { 56 | msg: String::from(format!("vpncmd failed ( {} )", msg)), 57 | })); 58 | } 59 | 60 | SoftEtherReader::decode_hub_status(&output.stdout) 61 | } 62 | 63 | pub fn hub_sessions( 64 | vpncmd: &str, 65 | server: &str, 66 | hub: &str, 67 | password: &str, 68 | ) -> Result, Box> { 69 | let mut child = Command::new(vpncmd) 70 | .arg(server) 71 | .arg("/SERVER") 72 | .arg(format!("/HUB:{}", hub)) 73 | .arg(format!("/PASSWORD:{}", password)) 74 | .arg("/CSV") 75 | .arg("/CMD") 76 | .arg("SessionList") 77 | .stdin(Stdio::piped()) 78 | .stdout(Stdio::piped()) 79 | .spawn()?; 80 | 81 | { 82 | let stdin = child.stdin.as_mut().unwrap(); 83 | // Input Ctrl-D to interrupt password prompt 84 | stdin.write_all(&[4])?; 85 | } 86 | 87 | let output = child.wait_with_output()?; 88 | 89 | if !output.status.success() { 90 | let msg = String::from_utf8_lossy(output.stdout.as_slice()); 91 | return Err(Box::new(SoftEtherError { 92 | msg: String::from(format!("vpncmd failed ( {} )", msg)), 93 | })); 94 | } 95 | 96 | SoftEtherReader::decode_hub_sessions(&output.stdout) 97 | } 98 | 99 | fn decode_hub_status(src: &[u8]) -> Result> { 100 | let mut rdr = csv::Reader::from_reader(src); 101 | let mut status = HubStatus::new(); 102 | 103 | for entry in rdr.records() { 104 | let entry = entry?; 105 | let key = entry.get(0).unwrap_or(""); 106 | let val = entry.get(1).unwrap_or(""); 107 | match key.as_ref() { 108 | "仮想 HUB 名" => status.name = String::from(val), 109 | "状態" => { 110 | status.online = if val == "オンライン" { 111 | true 112 | } else { 113 | false 114 | } 115 | } 116 | "SecureNAT 機能" => { 117 | status.secure_nat = if val == "無効" { false } else { true } 118 | } 119 | "セッション数" => status.sessions = val.parse()?, 120 | "セッション数 (クライアント)" => { 121 | status.sessions_client = val.parse()? 122 | } 123 | "セッション数 (ブリッジ)" => status.sessions_bridge = val.parse()?, 124 | "アクセスリスト数" => status.access_lists = val.parse()?, 125 | "ユーザー数" => status.users = val.parse()?, 126 | "グループ数" => status.groups = val.parse()?, 127 | "MAC テーブル数" => status.mac_tables = val.parse()?, 128 | "IP テーブル数" => status.ip_tables = val.parse()?, 129 | "ログイン回数" => status.logins = val.parse()?, 130 | "送信ユニキャストパケット数" => { 131 | status.outgoing_unicast_packets = SoftEtherReader::decode_packets(val)? 132 | } 133 | "送信ユニキャスト合計サイズ" => { 134 | status.outgoing_unicast_bytes = SoftEtherReader::decode_bytes(val)? 135 | } 136 | "送信ブロードキャストパケット数" => { 137 | status.outgoing_broadcast_packets = SoftEtherReader::decode_packets(val)? 138 | } 139 | "送信ブロードキャスト合計サイズ" => { 140 | status.outgoing_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 141 | } 142 | "受信ユニキャストパケット数" => { 143 | status.incoming_unicast_packets = SoftEtherReader::decode_packets(val)? 144 | } 145 | "受信ユニキャスト合計サイズ" => { 146 | status.incoming_unicast_bytes = SoftEtherReader::decode_bytes(val)? 147 | } 148 | "受信ブロードキャストパケット数" => { 149 | status.incoming_broadcast_packets = SoftEtherReader::decode_packets(val)? 150 | } 151 | "受信ブロードキャスト合計サイズ" => { 152 | status.incoming_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 153 | } 154 | "Virtual Hub Name" => status.name = String::from(val), 155 | "Status" => status.online = if val == "Online" { true } else { false }, 156 | "SecureNAT" => status.secure_nat = if val == "Disabled" { false } else { true }, 157 | "Sessions" => status.sessions = val.parse()?, 158 | "Sessions (Client)" => status.sessions_client = val.parse()?, 159 | "Sessions (Bridge)" => status.sessions_bridge = val.parse()?, 160 | "Access Lists" => status.access_lists = val.parse()?, 161 | "Users" => status.users = val.parse()?, 162 | "Groups" => status.groups = val.parse()?, 163 | "MAC Tables" => status.mac_tables = val.parse()?, 164 | "IP Tables" => status.ip_tables = val.parse()?, 165 | "Num Logins" => status.logins = val.parse()?, 166 | "Outgoing Unicast Packets" => { 167 | status.outgoing_unicast_packets = SoftEtherReader::decode_packets(val)? 168 | } 169 | "Outgoing Unicast Total Size" => { 170 | status.outgoing_unicast_bytes = SoftEtherReader::decode_bytes(val)? 171 | } 172 | "Outgoing Broadcast Packets" => { 173 | status.outgoing_broadcast_packets = SoftEtherReader::decode_packets(val)? 174 | } 175 | "Outgoing Broadcast Total Size" => { 176 | status.outgoing_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 177 | } 178 | "Incoming Unicast Packets" => { 179 | status.incoming_unicast_packets = SoftEtherReader::decode_packets(val)? 180 | } 181 | "Incoming Unicast Total Size" => { 182 | status.incoming_unicast_bytes = SoftEtherReader::decode_bytes(val)? 183 | } 184 | "Incoming Broadcast Packets" => { 185 | status.incoming_broadcast_packets = SoftEtherReader::decode_packets(val)? 186 | } 187 | "Incoming Broadcast Total Size" => { 188 | status.incoming_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 189 | } 190 | "虚拟 HUB 名称" => status.name = String::from(val), 191 | "状态" => status.online = if val == "在线" { true } else { false }, 192 | "SecureNAT 机能" => { 193 | status.secure_nat = if val == "无效" { false } else { true } 194 | } 195 | "会话数" => status.sessions = val.parse()?, 196 | "会话数 (客户端)" => status.sessions_client = val.parse()?, 197 | "会话数 (网桥)" => status.sessions_bridge = val.parse()?, 198 | "访问列表" => status.access_lists = val.parse()?, 199 | "用户数" => status.users = val.parse()?, 200 | "组数" => status.groups = val.parse()?, 201 | "MAC 表数" => status.mac_tables = val.parse()?, 202 | "IP 表数" => status.ip_tables = val.parse()?, 203 | "登录次数" => status.logins = val.parse()?, 204 | "发送单播数据包" => { 205 | status.outgoing_unicast_packets = SoftEtherReader::decode_packets(val)? 206 | } 207 | "发送单播总量" => { 208 | status.outgoing_unicast_bytes = SoftEtherReader::decode_bytes(val)? 209 | } 210 | "发送广播数据包" => { 211 | status.outgoing_broadcast_packets = SoftEtherReader::decode_packets(val)? 212 | } 213 | "发送广播总量" => { 214 | status.outgoing_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 215 | } 216 | "接收单播数据包" => { 217 | status.incoming_unicast_packets = SoftEtherReader::decode_packets(val)? 218 | } 219 | "接收单播总量" => { 220 | status.incoming_unicast_bytes = SoftEtherReader::decode_bytes(val)? 221 | } 222 | "接收广播数据包" => { 223 | status.incoming_broadcast_packets = SoftEtherReader::decode_packets(val)? 224 | } 225 | "接收广播总量" => { 226 | status.incoming_broadcast_bytes = SoftEtherReader::decode_bytes(val)? 227 | } 228 | _ => (), 229 | } 230 | } 231 | Ok(status) 232 | } 233 | 234 | fn decode_hub_sessions(src: &[u8]) -> Result, Box> { 235 | let mut rdr = csv::Reader::from_reader(src); 236 | let mut sessions = Vec::new(); 237 | 238 | for entry in rdr.records() { 239 | let entry = entry?; 240 | let name = entry.get(0).unwrap_or(""); 241 | let vlan_id = entry.get(1).unwrap_or(""); 242 | let location = entry.get(2).unwrap_or(""); 243 | let user = entry.get(3).unwrap_or(""); 244 | let source = entry.get(4).unwrap_or(""); 245 | let connections = entry.get(5).unwrap_or(""); 246 | let transfer_bytes = entry.get(6).unwrap_or(""); 247 | let transfer_packets = entry.get(7).unwrap_or(""); 248 | 249 | let connections = SoftEtherReader::decode_connections(connections)?; 250 | let transfer_bytes = SoftEtherReader::decode_bytes(transfer_bytes)?; 251 | let transfer_packets = SoftEtherReader::decode_bytes(transfer_packets)?; 252 | 253 | let session = HubSession { 254 | name: String::from(name), 255 | vlan_id: String::from(vlan_id), 256 | location: String::from(location), 257 | user: String::from(user), 258 | source: String::from(source), 259 | connections, 260 | transfer_bytes, 261 | transfer_packets, 262 | }; 263 | 264 | sessions.push(session); 265 | } 266 | 267 | Ok(sessions) 268 | } 269 | 270 | fn decode_packets(src: &str) -> Result> { 271 | let ret = String::from(src) 272 | .replace(",", "") 273 | .replace(" パケット", "") 274 | .replace(" packets", "") 275 | .replace(" 数据包", "") 276 | .parse()?; 277 | Ok(ret) 278 | } 279 | 280 | fn decode_bytes(src: &str) -> Result> { 281 | let ret = String::from(src) 282 | .replace(",", "") 283 | .replace(" バイト", "") 284 | .replace(" bytes", "") 285 | .replace(" 字节", "") 286 | .parse()?; 287 | Ok(ret) 288 | } 289 | 290 | fn decode_connections(src: &str) -> Result<(f64, f64), Box> { 291 | if !src.contains('/') { 292 | Ok((0.0, 0.0)) 293 | } else { 294 | let src: Vec<_> = src.split('/').collect(); 295 | let ret0: f64 = src[0].trim().parse()?; 296 | let ret1: f64 = src[1].trim().parse()?; 297 | Ok((ret0, ret1)) 298 | } 299 | } 300 | } 301 | 302 | #[derive(Debug)] 303 | pub struct HubStatus { 304 | pub name: String, 305 | pub online: bool, 306 | pub secure_nat: bool, 307 | pub sessions: f64, 308 | pub sessions_client: f64, 309 | pub sessions_bridge: f64, 310 | pub access_lists: f64, 311 | pub users: f64, 312 | pub groups: f64, 313 | pub mac_tables: f64, 314 | pub ip_tables: f64, 315 | pub logins: f64, 316 | pub outgoing_unicast_packets: f64, 317 | pub outgoing_unicast_bytes: f64, 318 | pub outgoing_broadcast_packets: f64, 319 | pub outgoing_broadcast_bytes: f64, 320 | pub incoming_unicast_packets: f64, 321 | pub incoming_unicast_bytes: f64, 322 | pub incoming_broadcast_packets: f64, 323 | pub incoming_broadcast_bytes: f64, 324 | } 325 | 326 | impl HubStatus { 327 | pub fn new() -> HubStatus { 328 | HubStatus { 329 | name: String::from(""), 330 | online: false, 331 | secure_nat: false, 332 | sessions: 0.0, 333 | sessions_client: 0.0, 334 | sessions_bridge: 0.0, 335 | access_lists: 0.0, 336 | users: 0.0, 337 | groups: 0.0, 338 | mac_tables: 0.0, 339 | ip_tables: 0.0, 340 | logins: 0.0, 341 | outgoing_unicast_packets: 0.0, 342 | outgoing_unicast_bytes: 0.0, 343 | outgoing_broadcast_packets: 0.0, 344 | outgoing_broadcast_bytes: 0.0, 345 | incoming_unicast_packets: 0.0, 346 | incoming_unicast_bytes: 0.0, 347 | incoming_broadcast_packets: 0.0, 348 | incoming_broadcast_bytes: 0.0, 349 | } 350 | } 351 | } 352 | 353 | #[derive(Debug)] 354 | pub struct HubSession { 355 | pub name: String, 356 | pub vlan_id: String, 357 | pub location: String, 358 | pub user: String, 359 | pub source: String, 360 | pub connections: (f64, f64), 361 | pub transfer_bytes: f64, 362 | pub transfer_packets: f64, 363 | } 364 | 365 | #[cfg(test)] 366 | mod tests { 367 | use super::*; 368 | 369 | #[test] 370 | fn test_hub_status() { 371 | let src = r#"項目,値 372 | 仮想 HUB 名,DEFAULT 373 | 状態,オンライン 374 | 種類,スタンドアロン 375 | SecureNAT 機能,無効 376 | セッション数,4 377 | セッション数 (クライアント),3 378 | セッション数 (ブリッジ),0 379 | アクセスリスト数,0 380 | ユーザー数,1 381 | グループ数,0 382 | MAC テーブル数,134 383 | IP テーブル数,211 384 | ログイン回数,18965 385 | 最終ログイン日時,2020-04-08 09:25:49 386 | 最終通信日時,2020-04-08 11:31:43 387 | 作成日時,2018-01-16 10:04:05 388 | 送信ユニキャストパケット数,"7,262,679,895 パケット" 389 | 送信ユニキャスト合計サイズ,"4,153,388,417,848 バイト" 390 | 送信ブロードキャストパケット数,"1,756,889,863 パケット" 391 | 送信ブロードキャスト合計サイズ,"256,781,466,202 バイト" 392 | 受信ユニキャストパケット数,"8,840,585,104 パケット" 393 | 受信ユニキャスト合計サイズ,"4,676,951,155,757 バイト" 394 | 受信ブロードキャストパケット数,"976,264,699 パケット" 395 | 受信ブロードキャスト合計サイズ,"138,170,046,309 バイト""#; 396 | 397 | let status = SoftEtherReader::decode_hub_status(src.as_bytes()).unwrap(); 398 | assert_eq!(status.name, String::from("DEFAULT")); 399 | assert_eq!(status.online, true); 400 | assert_eq!(status.secure_nat, false); 401 | assert_eq!(status.sessions, 4.0); 402 | assert_eq!(status.sessions_client, 3.0); 403 | assert_eq!(status.sessions_bridge, 0.0); 404 | assert_eq!(status.access_lists, 0.0); 405 | assert_eq!(status.users, 1.0); 406 | assert_eq!(status.groups, 0.0); 407 | assert_eq!(status.mac_tables, 134.0); 408 | assert_eq!(status.ip_tables, 211.0); 409 | assert_eq!(status.logins, 18965.0); 410 | assert_eq!(status.outgoing_unicast_packets, 7262679895.0); 411 | assert_eq!(status.outgoing_unicast_bytes, 4153388417848.0); 412 | assert_eq!(status.outgoing_broadcast_packets, 1756889863.0); 413 | assert_eq!(status.outgoing_broadcast_bytes, 256781466202.0); 414 | assert_eq!(status.incoming_unicast_packets, 8840585104.0); 415 | assert_eq!(status.incoming_unicast_bytes, 4676951155757.0); 416 | assert_eq!(status.incoming_broadcast_packets, 976264699.0); 417 | assert_eq!(status.incoming_broadcast_bytes, 138170046309.0); 418 | } 419 | 420 | #[test] 421 | fn test_hub_session() { 422 | let src = r#"セッション名,VLAN ID,場所,ユーザー名,接続元ホスト名,TCP コネクション,転送バイト数,転送パケット数 423 | SID-LOCALBRIDGE-1,-,ローカルセッション,Local Bridge,Ethernet ブリッジ,なし,"294,035,917,956","1,380,393,323" 424 | SID-XXXX-1047,-,ローカルセッション,xxxx,xxx.example.com,2 / 2,"82,691,861","322,784""#; 425 | 426 | let sessions = SoftEtherReader::decode_hub_sessions(src.as_bytes()).unwrap(); 427 | assert_eq!(sessions[0].name, String::from("SID-LOCALBRIDGE-1")); 428 | assert_eq!(sessions[0].vlan_id, String::from("-")); 429 | assert_eq!(sessions[0].location, String::from("ローカルセッション")); 430 | assert_eq!(sessions[0].user, String::from("Local Bridge")); 431 | assert_eq!(sessions[0].source, String::from("Ethernet ブリッジ")); 432 | assert_eq!(sessions[0].connections, (0.0, 0.0)); 433 | assert_eq!(sessions[0].transfer_bytes, 294035917956.0); 434 | assert_eq!(sessions[0].transfer_packets, 1380393323.0); 435 | assert_eq!(sessions[1].name, String::from("SID-XXXX-1047")); 436 | assert_eq!(sessions[1].vlan_id, String::from("-")); 437 | assert_eq!(sessions[1].location, String::from("ローカルセッション")); 438 | assert_eq!(sessions[1].user, String::from("xxxx")); 439 | assert_eq!(sessions[1].source, String::from("xxx.example.com")); 440 | assert_eq!(sessions[1].connections, (2.0, 2.0)); 441 | assert_eq!(sessions[1].transfer_bytes, 82691861.0); 442 | assert_eq!(sessions[1].transfer_packets, 322784.0); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.100" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi 0.1.19", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.5.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 36 | 37 | [[package]] 38 | name = "base64" 39 | version = "0.9.3" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" 42 | dependencies = [ 43 | "byteorder", 44 | "safemem", 45 | ] 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "1.3.2" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "2.9.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 58 | 59 | [[package]] 60 | name = "byteorder" 61 | version = "1.5.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 70 | 71 | [[package]] 72 | name = "clap" 73 | version = "2.34.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 76 | dependencies = [ 77 | "ansi_term", 78 | "atty", 79 | "bitflags 1.3.2", 80 | "strsim", 81 | "textwrap", 82 | "unicode-width", 83 | "vec_map", 84 | ] 85 | 86 | [[package]] 87 | name = "csv" 88 | version = "1.4.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" 91 | dependencies = [ 92 | "csv-core", 93 | "itoa", 94 | "ryu", 95 | "serde_core", 96 | ] 97 | 98 | [[package]] 99 | name = "csv-core" 100 | version = "0.1.12" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" 103 | dependencies = [ 104 | "memchr", 105 | ] 106 | 107 | [[package]] 108 | name = "equivalent" 109 | version = "1.0.2" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 112 | 113 | [[package]] 114 | name = "fnv" 115 | version = "1.0.7" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 118 | 119 | [[package]] 120 | name = "hashbrown" 121 | version = "0.15.5" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 124 | 125 | [[package]] 126 | name = "heck" 127 | version = "0.3.3" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 130 | dependencies = [ 131 | "unicode-segmentation", 132 | ] 133 | 134 | [[package]] 135 | name = "hermit-abi" 136 | version = "0.1.19" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 139 | dependencies = [ 140 | "libc", 141 | ] 142 | 143 | [[package]] 144 | name = "hermit-abi" 145 | version = "0.5.2" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 148 | 149 | [[package]] 150 | name = "httparse" 151 | version = "1.10.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 154 | 155 | [[package]] 156 | name = "hyper" 157 | version = "0.10.16" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" 160 | dependencies = [ 161 | "base64", 162 | "httparse", 163 | "language-tags", 164 | "log 0.3.9", 165 | "mime", 166 | "num_cpus", 167 | "time", 168 | "traitobject", 169 | "typeable", 170 | "unicase", 171 | "url", 172 | ] 173 | 174 | [[package]] 175 | name = "idna" 176 | version = "0.1.5" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" 179 | dependencies = [ 180 | "matches", 181 | "unicode-bidi", 182 | "unicode-normalization", 183 | ] 184 | 185 | [[package]] 186 | name = "indexmap" 187 | version = "2.11.4" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 190 | dependencies = [ 191 | "equivalent", 192 | "hashbrown", 193 | ] 194 | 195 | [[package]] 196 | name = "itoa" 197 | version = "1.0.15" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 200 | 201 | [[package]] 202 | name = "language-tags" 203 | version = "0.2.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" 206 | 207 | [[package]] 208 | name = "lazy_static" 209 | version = "1.5.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 212 | 213 | [[package]] 214 | name = "libc" 215 | version = "0.2.175" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 218 | 219 | [[package]] 220 | name = "lock_api" 221 | version = "0.4.13" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 224 | dependencies = [ 225 | "autocfg", 226 | "scopeguard", 227 | ] 228 | 229 | [[package]] 230 | name = "log" 231 | version = "0.3.9" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 234 | dependencies = [ 235 | "log 0.4.27", 236 | ] 237 | 238 | [[package]] 239 | name = "log" 240 | version = "0.4.27" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 243 | 244 | [[package]] 245 | name = "matches" 246 | version = "0.1.10" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 249 | 250 | [[package]] 251 | name = "memchr" 252 | version = "2.7.5" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 255 | 256 | [[package]] 257 | name = "mime" 258 | version = "0.2.6" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" 261 | dependencies = [ 262 | "log 0.3.9", 263 | ] 264 | 265 | [[package]] 266 | name = "num_cpus" 267 | version = "1.17.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 270 | dependencies = [ 271 | "hermit-abi 0.5.2", 272 | "libc", 273 | ] 274 | 275 | [[package]] 276 | name = "once_cell" 277 | version = "1.21.3" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 280 | 281 | [[package]] 282 | name = "parking_lot" 283 | version = "0.12.4" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 286 | dependencies = [ 287 | "lock_api", 288 | "parking_lot_core", 289 | ] 290 | 291 | [[package]] 292 | name = "parking_lot_core" 293 | version = "0.9.11" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 296 | dependencies = [ 297 | "cfg-if", 298 | "libc", 299 | "redox_syscall", 300 | "smallvec", 301 | "windows-targets", 302 | ] 303 | 304 | [[package]] 305 | name = "percent-encoding" 306 | version = "1.0.1" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" 309 | 310 | [[package]] 311 | name = "proc-macro-error" 312 | version = "1.0.4" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 315 | dependencies = [ 316 | "proc-macro-error-attr", 317 | "proc-macro2", 318 | "quote", 319 | "syn 1.0.109", 320 | "version_check 0.9.5", 321 | ] 322 | 323 | [[package]] 324 | name = "proc-macro-error-attr" 325 | version = "1.0.4" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 328 | dependencies = [ 329 | "proc-macro2", 330 | "quote", 331 | "version_check 0.9.5", 332 | ] 333 | 334 | [[package]] 335 | name = "proc-macro2" 336 | version = "1.0.97" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" 339 | dependencies = [ 340 | "unicode-ident", 341 | ] 342 | 343 | [[package]] 344 | name = "prometheus" 345 | version = "0.14.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" 348 | dependencies = [ 349 | "cfg-if", 350 | "fnv", 351 | "lazy_static", 352 | "memchr", 353 | "parking_lot", 354 | "protobuf", 355 | "thiserror 2.0.14", 356 | ] 357 | 358 | [[package]] 359 | name = "protobuf" 360 | version = "3.7.2" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" 363 | dependencies = [ 364 | "once_cell", 365 | "protobuf-support", 366 | "thiserror 1.0.69", 367 | ] 368 | 369 | [[package]] 370 | name = "protobuf-support" 371 | version = "3.7.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" 374 | dependencies = [ 375 | "thiserror 1.0.69", 376 | ] 377 | 378 | [[package]] 379 | name = "quote" 380 | version = "1.0.40" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 383 | dependencies = [ 384 | "proc-macro2", 385 | ] 386 | 387 | [[package]] 388 | name = "redox_syscall" 389 | version = "0.5.17" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 392 | dependencies = [ 393 | "bitflags 2.9.1", 394 | ] 395 | 396 | [[package]] 397 | name = "ryu" 398 | version = "1.0.20" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 401 | 402 | [[package]] 403 | name = "safemem" 404 | version = "0.3.3" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 407 | 408 | [[package]] 409 | name = "scopeguard" 410 | version = "1.2.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 413 | 414 | [[package]] 415 | name = "serde" 416 | version = "1.0.228" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 419 | dependencies = [ 420 | "serde_core", 421 | "serde_derive", 422 | ] 423 | 424 | [[package]] 425 | name = "serde_core" 426 | version = "1.0.228" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 429 | dependencies = [ 430 | "serde_derive", 431 | ] 432 | 433 | [[package]] 434 | name = "serde_derive" 435 | version = "1.0.228" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 438 | dependencies = [ 439 | "proc-macro2", 440 | "quote", 441 | "syn 2.0.104", 442 | ] 443 | 444 | [[package]] 445 | name = "serde_spanned" 446 | version = "1.0.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 449 | dependencies = [ 450 | "serde_core", 451 | ] 452 | 453 | [[package]] 454 | name = "smallvec" 455 | version = "1.15.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 458 | 459 | [[package]] 460 | name = "softether_exporter" 461 | version = "0.2.1-pre" 462 | dependencies = [ 463 | "anyhow", 464 | "csv", 465 | "hyper", 466 | "lazy_static", 467 | "prometheus", 468 | "serde", 469 | "structopt", 470 | "toml", 471 | ] 472 | 473 | [[package]] 474 | name = "strsim" 475 | version = "0.8.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 478 | 479 | [[package]] 480 | name = "structopt" 481 | version = "0.3.26" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 484 | dependencies = [ 485 | "clap", 486 | "lazy_static", 487 | "structopt-derive", 488 | ] 489 | 490 | [[package]] 491 | name = "structopt-derive" 492 | version = "0.4.18" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 495 | dependencies = [ 496 | "heck", 497 | "proc-macro-error", 498 | "proc-macro2", 499 | "quote", 500 | "syn 1.0.109", 501 | ] 502 | 503 | [[package]] 504 | name = "syn" 505 | version = "1.0.109" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 508 | dependencies = [ 509 | "proc-macro2", 510 | "quote", 511 | "unicode-ident", 512 | ] 513 | 514 | [[package]] 515 | name = "syn" 516 | version = "2.0.104" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 519 | dependencies = [ 520 | "proc-macro2", 521 | "quote", 522 | "unicode-ident", 523 | ] 524 | 525 | [[package]] 526 | name = "textwrap" 527 | version = "0.11.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 530 | dependencies = [ 531 | "unicode-width", 532 | ] 533 | 534 | [[package]] 535 | name = "thiserror" 536 | version = "1.0.69" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 539 | dependencies = [ 540 | "thiserror-impl 1.0.69", 541 | ] 542 | 543 | [[package]] 544 | name = "thiserror" 545 | version = "2.0.14" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" 548 | dependencies = [ 549 | "thiserror-impl 2.0.14", 550 | ] 551 | 552 | [[package]] 553 | name = "thiserror-impl" 554 | version = "1.0.69" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 557 | dependencies = [ 558 | "proc-macro2", 559 | "quote", 560 | "syn 2.0.104", 561 | ] 562 | 563 | [[package]] 564 | name = "thiserror-impl" 565 | version = "2.0.14" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" 568 | dependencies = [ 569 | "proc-macro2", 570 | "quote", 571 | "syn 2.0.104", 572 | ] 573 | 574 | [[package]] 575 | name = "time" 576 | version = "0.1.45" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 579 | dependencies = [ 580 | "libc", 581 | "wasi", 582 | "winapi", 583 | ] 584 | 585 | [[package]] 586 | name = "tinyvec" 587 | version = "1.9.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 590 | dependencies = [ 591 | "tinyvec_macros", 592 | ] 593 | 594 | [[package]] 595 | name = "tinyvec_macros" 596 | version = "0.1.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 599 | 600 | [[package]] 601 | name = "toml" 602 | version = "0.9.10+spec-1.1.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" 605 | dependencies = [ 606 | "indexmap", 607 | "serde_core", 608 | "serde_spanned", 609 | "toml_datetime", 610 | "toml_parser", 611 | "toml_writer", 612 | "winnow", 613 | ] 614 | 615 | [[package]] 616 | name = "toml_datetime" 617 | version = "0.7.5+spec-1.1.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 620 | dependencies = [ 621 | "serde_core", 622 | ] 623 | 624 | [[package]] 625 | name = "toml_parser" 626 | version = "1.0.6+spec-1.1.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 629 | dependencies = [ 630 | "winnow", 631 | ] 632 | 633 | [[package]] 634 | name = "toml_writer" 635 | version = "1.0.6+spec-1.1.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 638 | 639 | [[package]] 640 | name = "traitobject" 641 | version = "0.1.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "04a79e25382e2e852e8da874249358d382ebaf259d0d34e75d8db16a7efabbc7" 644 | 645 | [[package]] 646 | name = "typeable" 647 | version = "0.1.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" 650 | 651 | [[package]] 652 | name = "unicase" 653 | version = "1.4.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" 656 | dependencies = [ 657 | "version_check 0.1.5", 658 | ] 659 | 660 | [[package]] 661 | name = "unicode-bidi" 662 | version = "0.3.18" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 665 | 666 | [[package]] 667 | name = "unicode-ident" 668 | version = "1.0.18" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 671 | 672 | [[package]] 673 | name = "unicode-normalization" 674 | version = "0.1.24" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 677 | dependencies = [ 678 | "tinyvec", 679 | ] 680 | 681 | [[package]] 682 | name = "unicode-segmentation" 683 | version = "1.12.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 686 | 687 | [[package]] 688 | name = "unicode-width" 689 | version = "0.1.14" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 692 | 693 | [[package]] 694 | name = "url" 695 | version = "1.7.2" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" 698 | dependencies = [ 699 | "idna", 700 | "matches", 701 | "percent-encoding", 702 | ] 703 | 704 | [[package]] 705 | name = "vec_map" 706 | version = "0.8.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 709 | 710 | [[package]] 711 | name = "version_check" 712 | version = "0.1.5" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 715 | 716 | [[package]] 717 | name = "version_check" 718 | version = "0.9.5" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 721 | 722 | [[package]] 723 | name = "wasi" 724 | version = "0.10.0+wasi-snapshot-preview1" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 727 | 728 | [[package]] 729 | name = "winapi" 730 | version = "0.3.9" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 733 | dependencies = [ 734 | "winapi-i686-pc-windows-gnu", 735 | "winapi-x86_64-pc-windows-gnu", 736 | ] 737 | 738 | [[package]] 739 | name = "winapi-i686-pc-windows-gnu" 740 | version = "0.4.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 743 | 744 | [[package]] 745 | name = "winapi-x86_64-pc-windows-gnu" 746 | version = "0.4.0" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 749 | 750 | [[package]] 751 | name = "windows-targets" 752 | version = "0.52.6" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 755 | dependencies = [ 756 | "windows_aarch64_gnullvm", 757 | "windows_aarch64_msvc", 758 | "windows_i686_gnu", 759 | "windows_i686_gnullvm", 760 | "windows_i686_msvc", 761 | "windows_x86_64_gnu", 762 | "windows_x86_64_gnullvm", 763 | "windows_x86_64_msvc", 764 | ] 765 | 766 | [[package]] 767 | name = "windows_aarch64_gnullvm" 768 | version = "0.52.6" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 771 | 772 | [[package]] 773 | name = "windows_aarch64_msvc" 774 | version = "0.52.6" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 777 | 778 | [[package]] 779 | name = "windows_i686_gnu" 780 | version = "0.52.6" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 783 | 784 | [[package]] 785 | name = "windows_i686_gnullvm" 786 | version = "0.52.6" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 789 | 790 | [[package]] 791 | name = "windows_i686_msvc" 792 | version = "0.52.6" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 795 | 796 | [[package]] 797 | name = "windows_x86_64_gnu" 798 | version = "0.52.6" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 801 | 802 | [[package]] 803 | name = "windows_x86_64_gnullvm" 804 | version = "0.52.6" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 807 | 808 | [[package]] 809 | name = "windows_x86_64_msvc" 810 | version = "0.52.6" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 813 | 814 | [[package]] 815 | name = "winnow" 816 | version = "0.7.13" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 819 | --------------------------------------------------------------------------------