diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/connect.rs | 2 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/disconnect.rs | 2 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/reconnect.rs | 2 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/status.rs | 26 | ||||
| -rw-r--r-- | mullvad-cli/src/format.rs | 200 |
6 files changed, 137 insertions, 96 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4413af951f..2892b02b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Line wrap the file at 100 chars. Th - Make login field keep previous value when submitting an incorrect account number in desktop app. - Decrease the time it takes to connect to WireGuard relays by sending an ICMP packet immediately. - Pause API interactions when the daemon has not been used for 3 days. +- Simplified output of `mullvad status` command. ### Fixed - Fix the sometimes incorrect time added text after adding time to the account. diff --git a/mullvad-cli/src/cmds/connect.rs b/mullvad-cli/src/cmds/connect.rs index a184e38244..3a879fc6a8 100644 --- a/mullvad-cli/src/cmds/connect.rs +++ b/mullvad-cli/src/cmds/connect.rs @@ -34,7 +34,7 @@ impl Command for Connect { if let Some(mut receiver) = receiver_option { while let Some(state) = receiver.next().await { let state = state?; - format::print_state(&state); + format::print_state(&state, false); match state.state.unwrap() { State::Connected(_) => return Ok(()), State::Error(_) => return Err(Error::CommandFailed("connect")), diff --git a/mullvad-cli/src/cmds/disconnect.rs b/mullvad-cli/src/cmds/disconnect.rs index 9ac051ca57..a906eed22d 100644 --- a/mullvad-cli/src/cmds/disconnect.rs +++ b/mullvad-cli/src/cmds/disconnect.rs @@ -34,7 +34,7 @@ impl Command for Disconnect { if let Some(mut receiver) = receiver_option { while let Some(state) = receiver.next().await { let state = state?; - format::print_state(&state); + format::print_state(&state, false); match state.state.unwrap() { Disconnected(_) => return Ok(()), _ => {} diff --git a/mullvad-cli/src/cmds/reconnect.rs b/mullvad-cli/src/cmds/reconnect.rs index c2a577a212..53ca666482 100644 --- a/mullvad-cli/src/cmds/reconnect.rs +++ b/mullvad-cli/src/cmds/reconnect.rs @@ -34,7 +34,7 @@ impl Command for Reconnect { if let Some(mut receiver) = receiver_option { while let Some(state) = receiver.next().await { let state = state?; - format::print_state(&state); + format::print_state(&state, false); match state.state.unwrap() { State::Connected(_) => return Ok(()), State::Error(_) => return Err(Error::CommandFailed("reconnect")), diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index 3d5cfc8b7d..71f31f5fef 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -15,6 +15,11 @@ impl Command for Status { clap::App::new(self.name()) .about("View the state of the VPN tunnel") .arg( + clap::Arg::new("verbose") + .short('v') + .help("Enables verbose output"), + ) + .arg( clap::Arg::new("location") .long("location") .short('l') @@ -24,13 +29,15 @@ impl Command for Status { clap::Arg::new("debug") .long("debug") .global(true) - .help("Enables verbose output"), + .help("Enables debug output"), ) .subcommand(clap::App::new("listen").about("Listen for VPN tunnel state changes")) } async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { let debug = matches.is_present("debug"); + let verbose = matches.is_present("verbose"); + let show_full_location = matches.is_present("location"); let mut rpc = new_rpc_client().await?; let state = rpc.get_tunnel_state(()).await?.into_inner(); @@ -38,10 +45,10 @@ impl Command for Status { if debug { println!("Tunnel state: {:#?}", state); } else { - format::print_state(&state); + format::print_state(&state, verbose); } - if matches.is_present("location") { + if show_full_location { print_location(&mut rpc).await?; } @@ -54,13 +61,13 @@ impl Command for Status { if debug { println!("New tunnel state: {:#?}", new_state); } else { - format::print_state(&new_state); + format::print_state(&new_state, verbose); } use mullvad_management_interface::types::tunnel_state::State::*; match new_state.state.unwrap() { Connected(..) | Disconnected(..) => { - if matches.is_present("location") { + if show_full_location { print_location(&mut rpc).await?; } } @@ -113,9 +120,6 @@ async fn print_location(rpc: &mut ManagementServiceClient) -> Result<()> { } } }; - if !location.hostname.is_empty() { - println!("Relay: {}", location.hostname); - } if !location.ipv4.is_empty() { println!("IPv4: {}", location.ipv4); } @@ -123,12 +127,6 @@ async fn print_location(rpc: &mut ManagementServiceClient) -> Result<()> { println!("IPv6: {}", location.ipv6); } - print!("Location: "); - if !location.city.is_empty() { - print!("{}, ", location.city); - } - println!("{}", location.country); - println!( "Position: {:.5}°N, {:.5}°W", location.latitude, location.longitude diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs index 13e084707a..8a4fc95742 100644 --- a/mullvad-cli/src/format.rs +++ b/mullvad-cli/src/format.rs @@ -5,107 +5,149 @@ use mullvad_management_interface::types::{ }, tunnel_state, tunnel_state::State::*, - ErrorState, ObfuscationType, ProxyType, TransportProtocol, TunnelEndpoint, TunnelState, + ErrorState, ObfuscationType, ProxyType, TransportProtocol, TunnelState, TunnelStateRelayInfo, TunnelType, }; use mullvad_types::auth_failed::AuthFailed; -use std::fmt::Write; -pub fn print_state(state: &TunnelState) { - print!("Tunnel status: "); +pub fn print_state(state: &TunnelState, verbose: bool) { match state.state.as_ref().unwrap() { Error(error) => print_error_state(error.error_state.as_ref().unwrap()), Connected(tunnel_state::Connected { relay_info }) => { - let endpoint = relay_info - .as_ref() - .unwrap() - .tunnel_endpoint - .as_ref() - .unwrap(); - println!("Connected to {}", format_endpoint(&endpoint)); + println!( + "Connected to {}", + format_relay_connection(relay_info.as_ref().unwrap(), verbose) + ); } Connecting(tunnel_state::Connecting { relay_info }) => { - let endpoint = relay_info - .as_ref() - .unwrap() - .tunnel_endpoint - .as_ref() - .unwrap(); - println!("Connecting to {}...", format_endpoint(&endpoint)); + let ellipsis = if !verbose { "..." } else { "" }; + println!( + "Connecting to {}{ellipsis}", + format_relay_connection(relay_info.as_ref().unwrap(), verbose) + ); } Disconnected(_) => println!("Disconnected"), Disconnecting(_) => println!("Disconnecting..."), } } -fn format_endpoint(endpoint: &TunnelEndpoint) -> String { - let tunnel_type = TunnelType::from_i32(endpoint.tunnel_type).expect("invalid tunnel protocol"); - let mut out = format!( - "{} {}/{}", - match tunnel_type { +fn format_relay_connection(relay_info: &TunnelStateRelayInfo, verbose: bool) -> String { + let endpoint = relay_info.tunnel_endpoint.as_ref().unwrap(); + let location = &relay_info.location.as_ref().unwrap(); + + let prefix_separator = if verbose { "\n\t" } else { " " }; + let mut obfuscator_overlaps = false; + + let exit_endpoint = { + let mut address = endpoint.address.as_str(); + let mut protocol = endpoint.protocol; + if let Some(obfuscator) = endpoint.obfuscation.as_ref() { + if location.hostname == location.obfuscator_hostname { + obfuscator_overlaps = true; + address = &obfuscator.address; + protocol = obfuscator.protocol; + } + }; + + let exit = format_endpoint( + &location.hostname, + protocol, + Some(address).filter(|_| verbose), + ); + format!("{exit} in {}, {}", &location.city, &location.country) + }; + + let first_hop = endpoint.entry_endpoint.as_ref().map(|entry| { + let mut address = entry.address.as_str(); + let mut protocol = entry.protocol; + if let Some(obfuscator) = endpoint.obfuscation.as_ref() { + obfuscator_overlaps = true; + if location.entry_hostname == location.obfuscator_hostname { + address = &obfuscator.address; + protocol = obfuscator.protocol; + } + }; + + let endpoint = format_endpoint( + &location.entry_hostname, + protocol, + Some(address).filter(|_| verbose), + ); + format!("{prefix_separator}via {endpoint}") + }); + + let obfuscator = endpoint.obfuscation.as_ref().map(|obfuscator| { + if !obfuscator_overlaps { + let endpoint_str = format_endpoint( + &location.obfuscator_hostname, + obfuscator.protocol, + Some(obfuscator.address.as_str()).filter(|_| verbose), + ); + format!("{prefix_separator}obfuscated via {endpoint_str}") + } else { + String::new() + } + }); + + let bridge = endpoint.proxy.as_ref().map(|proxy| { + let proxy_endpoint = format_endpoint( + &location.bridge_hostname, + proxy.protocol, + Some(proxy.address.as_str()).filter(|_| verbose), + ); + + format!("{prefix_separator}via {proxy_endpoint}") + }); + let tunnel_type = if verbose { + let tunnel = match TunnelType::from_i32(endpoint.tunnel_type).expect("invalid tunnel type") + { TunnelType::Wireguard => "WireGuard", TunnelType::Openvpn => "OpenVPN", - }, - endpoint.address, - format_protocol( - TransportProtocol::from_i32(endpoint.protocol).expect("invalid transport protocol") - ), - ); + }; - match tunnel_type { - TunnelType::Openvpn => { - if let Some(ref proxy) = endpoint.proxy { - write!( - &mut out, - " via {} {}/{}", - match ProxyType::from_i32(proxy.proxy_type).expect("invalid proxy type") { - ProxyType::Shadowsocks => "Shadowsocks", - ProxyType::Custom => "custom bridge", - }, - proxy.address, - format_protocol( - TransportProtocol::from_i32(proxy.protocol) - .expect("invalid transport protocol") - ), - ) - .unwrap(); - } + format!("\nTunnel type: {tunnel}") + } else { + String::new() + }; + + let mut bridge_type = String::new(); + let mut obfuscator_type = String::new(); + if verbose { + if let Some(bridge) = endpoint.proxy.as_ref() { + let bridge = match ProxyType::from_i32(bridge.proxy_type).expect("invalid proxy type") { + ProxyType::Shadowsocks => "Shadowsocks", + ProxyType::Custom => "custom bridge", + }; + bridge_type = format!("\nBridge type: {}", bridge); } - TunnelType::Wireguard => { - if let Some(ref entry_endpoint) = endpoint.entry_endpoint { - write!( - &mut out, - " via {}/{}", - entry_endpoint.address, - format_protocol( - TransportProtocol::from_i32(entry_endpoint.protocol) - .expect("invalid transport protocol") - ) - ) - .unwrap(); - } - if let Some(ref obfuscation) = endpoint.obfuscation { - write!( - &mut out, - " via {} {}:{}/{}", - match ObfuscationType::from_i32(obfuscation.obfuscation_type) - .expect("invalid obfuscation type") - { - ObfuscationType::Udp2tcp => "Udp2Tcp", - }, - obfuscation.address, - obfuscation.port, - format_protocol( - TransportProtocol::from_i32(obfuscation.protocol) - .expect("invalid transport protocol") - ) - ) - .unwrap(); - } + if let Some(obfuscator) = endpoint.obfuscation.as_ref() { + let obfuscation = convert_obfuscator_type(obfuscator.obfuscation_type); + obfuscator_type = format!("\nObfuscator: {obfuscation}"); } } - out + format!( + "{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{bridge_type}{obfuscator_type}", + first_hop = first_hop.unwrap_or(String::new()), + bridge = bridge.unwrap_or(String::new()), + obfuscator = obfuscator.unwrap_or(String::new()), + ) +} + +fn convert_obfuscator_type(obfuscator: i32) -> &'static str { + match ObfuscationType::from_i32(obfuscator).expect("invalid obfuscator type") { + ObfuscationType::Udp2tcp => "Udp2Tcp", + } +} + +fn format_endpoint(hostname: &String, protocol_enum: i32, addr: Option<&str>) -> String { + let protocol = format_protocol( + TransportProtocol::from_i32(protocol_enum).expect("invalid transport protocol"), + ); + let sockaddr_suffix = addr + .map(|addr| format!(" ({addr}/{protocol})")) + .unwrap_or_else(String::new); + format!("{hostname}{sockaddr_suffix}") } fn print_error_state(error_state: &ErrorState) { |
