use std::io::Write; use std::net::ToSocketAddrs; use std::process; use log::debug; use clap::Clap; use serde::{Deserialize, Serialize}; const BASE_URL: &str = "https://api.azirevpn.com/v1"; #[derive(Clap, Debug)] struct Opts { #[clap(short, long)] json: bool, #[clap(subcommand)] command: Command, } #[derive(Clap, Debug)] struct ConfigOpts { location: String, username: String, token: String, } #[derive(Clap, Debug)] enum Command { Endpoints, Config(ConfigOpts), Check, } #[derive(Serialize, Deserialize, Debug)] struct Endpoints { wireguard: String, } #[derive(Serialize, Deserialize, Debug)] struct Location { name: String, city: String, country: String, iso: String, endpoints: Endpoints, } #[derive(Serialize, Deserialize, Debug)] struct Locations { locations: Vec, } #[derive(Serialize, Deserialize, Debug)] struct CheckResult { connected: bool, ip: String, // XXX location: String, } #[derive(Serialize, Deserialize, Debug)] struct WireguardConfig { status: String, data: WireguardConfigData, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] struct WireguardConfigData { public_key: String, address: String, endpoint: String, } #[derive(Debug)] struct WireguardKeyPair { public_key: String, private_key: String, } fn main() -> Result<(), anyhow::Error> { env_logger::init(); let opts = Opts::parse(); match &opts.command { Command::Endpoints => list_endpoints(&opts)?, Command::Config(get_config_opts) => get_config(&opts, &get_config_opts)?, Command::Check => check(&opts)?, } Ok(()) } fn list_endpoints(opts: &Opts) -> Result<(), anyhow::Error> { let locations: Locations = get_locations()?; if opts.json { println!("{}", serde_json::to_string(&locations)?); } else { for (i, location) in locations.locations.iter().enumerate() { if i > 0 { println!() }; println!("Name: {}", location.name); println!("City: {}", location.city); println!("Country: {}", location.country); println!("Country code: {}", location.iso); println!("WireGuard endpoint: {}", location.endpoints.wireguard); } } Ok(()) } fn get_config(_opts: &Opts, config_opts: &ConfigOpts) -> Result<(), anyhow::Error> { let locations = get_locations()?; let location = locations .locations .iter() .find(|location| location.name == config_opts.location) .ok_or_else(|| anyhow::anyhow!("no such location: {}", config_opts.location))?; debug!("location = {:?}", &location); let keys = generage_keys()?; debug!("keys = {:?}", &keys); let config: WireguardConfig = ureq::post(&location.endpoints.wireguard) .send_form(&[ ("username", &config_opts.username), ("token", &config_opts.token), ("pubkey", &keys.public_key), ])? .into_json()?; debug!("config = {:?}", &config); let mut endpoint_addrs = config.data.endpoint.to_socket_addrs()?; let endpoint_addr = endpoint_addrs .next() .ok_or_else(|| anyhow::anyhow!("no endpoint address received"))?; debug!("endpoint_addr = {:?}", &endpoint_addr); println!( r"[Interface] PrivateKey = {} Address = {} [Peer] PublicKey = {} Endpoint = {} AllowedIPs = 0.0.0.0/0 #, ::/0", &keys.private_key, &config.data.address, &config.data.public_key, &endpoint_addr ); Ok(()) } fn check(opts: &Opts) -> Result<(), anyhow::Error> { let url = format!("{}/check", BASE_URL); let result: CheckResult = ureq::get(&url).call()?.into_json()?; if opts.json { println!("{}", serde_json::to_string(&result)?); } else { println!("Connected: {:?}", result.connected); println!("IP: {}", result.ip); println!("Location: {}", result.location); } Ok(()) } fn get_locations() -> Result { let url = format!("{}/locations", BASE_URL); let locations: Locations = ureq::get(&url).call()?.into_json()?; Ok(locations) } fn generage_keys() -> Result { let privkey = process::Command::new("wg").arg("genkey").output()?.stdout; let mut pubkey_cmd = process::Command::new("wg") .arg("pubkey") .stdin(process::Stdio::piped()) .stdout(process::Stdio::piped()) .spawn()?; pubkey_cmd .stdin .as_mut() .expect("no stdin") .write_all(&privkey)?; let pubkey = pubkey_cmd.wait_with_output()?.stdout; Ok(WireguardKeyPair { private_key: String::from_utf8(privkey)?.trim_end().to_string(), public_key: String::from_utf8(pubkey)?.trim_end().to_string(), }) }