-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add None provider, path expansion, refactoring
- Add None provider and protocol for solely creating network-ready network namespace with no VPN service - Add shell path expansion to path arguments (e.g. you can use ~ in custom config path) - Refactor how CLI arguments are parsed using macro_rules
- Loading branch information
Showing
12 changed files
with
706 additions
and
567 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
// Handles using the args from either the CLI or config file | ||
|
||
use std::{net::IpAddr, path::PathBuf, str::FromStr}; | ||
|
||
use anyhow::anyhow; | ||
use config::Config; | ||
use vopono_core::{ | ||
config::{providers::VpnProvider, vpn::Protocol}, | ||
network::{ | ||
firewall::Firewall, | ||
network_interface::{get_active_interfaces, NetworkInterface}, | ||
}, | ||
util::{get_config_file_protocol, vopono_dir}, | ||
}; | ||
|
||
use crate::args::ExecCommand; | ||
|
||
macro_rules! command_else_config_option { | ||
// Get expression from command - command.expr | ||
// If None then read from Config .get("expr") | ||
// Returns None if absent in both | ||
($field_id:ident, $command:ident, $config:ident) => { | ||
$command.$field_id.clone().or_else(|| { | ||
$config | ||
.get(stringify!($field_id)) | ||
.or($config.get(&stringify!($field_id).replace('_', "-"))) | ||
.map_err(|_e| anyhow!("Failed to read config file")) | ||
.ok() | ||
}) | ||
}; | ||
} | ||
macro_rules! command_else_config_bool { | ||
// Get bool ident from command - command.expr | ||
// If None then read from Config .get("expr") | ||
// Returns false if absent in both | ||
($field_id:ident, $command:ident, $config:ident) => { | ||
$command.$field_id | ||
|| $config | ||
.get(stringify!($field_id)) | ||
.or($config.get(&stringify!($field_id).replace('_', "-"))) | ||
.map_err(|_e| anyhow!("Failed to read config file")) | ||
.ok() | ||
.unwrap_or(false) | ||
}; | ||
} | ||
|
||
macro_rules! command_else_config_option_variant { | ||
// Get enum variant ident from command - command.expr | ||
// If None then read from Config .get("expr") | ||
// Returns None if absent in both | ||
($field_id:ident, $command:ident, $config:ident) => { | ||
$command.$field_id.map(|x| x.to_variant()).or_else(|| { | ||
$config | ||
.get(stringify!($field_id)) | ||
.or($config.get(&stringify!($field_id).replace('_', "-"))) | ||
.map_err(|_e| anyhow!("Failed to read config file")) | ||
.ok() | ||
}) | ||
}; | ||
} | ||
|
||
macro_rules! error_and_bail { | ||
// log to error and bail | ||
($msg:literal) => { | ||
log::error!("{}", $msg); | ||
anyhow::bail!($msg); | ||
}; | ||
} | ||
|
||
// TODO: Generate this from procedural macro? | ||
pub struct ArgsConfig { | ||
pub provider: VpnProvider, | ||
pub protocol: Protocol, | ||
pub interface: NetworkInterface, | ||
pub server: String, | ||
pub application: String, | ||
pub user: Option<String>, | ||
pub group: Option<String>, | ||
pub working_directory: Option<String>, | ||
pub custom: Option<PathBuf>, | ||
pub dns: Option<Vec<IpAddr>>, | ||
pub hosts: Option<Vec<String>>, | ||
pub open_hosts: Option<Vec<IpAddr>>, | ||
pub no_killswitch: bool, | ||
pub keep_alive: bool, | ||
pub open_ports: Option<Vec<u16>>, | ||
pub forward: Option<Vec<u16>>, | ||
pub no_proxy: bool, | ||
pub firewall: Firewall, | ||
pub disable_ipv6: bool, | ||
pub postup: Option<String>, | ||
pub predown: Option<String>, | ||
pub custom_netns_name: Option<String>, | ||
pub allow_host_access: bool, | ||
pub port_forwarding: bool, | ||
pub custom_port_forwarding: Option<VpnProvider>, | ||
pub port_forwarding_callback: Option<String>, | ||
pub create_netns_only: bool, | ||
} | ||
|
||
impl ArgsConfig { | ||
/// Return new ExecCommand with args from config file if missing in CLI but present there | ||
/// Also handle CLI args consistency errors | ||
pub fn get_cli_or_config_args(command: ExecCommand, config: Config) -> anyhow::Result<Self> { | ||
// TODO: Automate field mapping with procedural macro over ExecCommand struct? | ||
let custom: Option<PathBuf> = command_else_config_option!(custom, command, config) | ||
.and_then(|p| { | ||
shellexpand::full(&p.to_string_lossy()) | ||
.ok() | ||
.and_then(|s| PathBuf::from_str(s.as_ref()).ok()) | ||
}); | ||
let custom_netns_name = command_else_config_option!(custom_netns_name, command, config); | ||
let open_hosts = command_else_config_option!(open_hosts, command, config); | ||
let hosts = command_else_config_option!(hosts, command, config); | ||
let open_ports = command_else_config_option!(open_ports, command, config); | ||
let forward = command_else_config_option!(forward, command, config); | ||
let postup = command_else_config_option!(postup, command, config) | ||
.and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); | ||
|
||
let predown = command_else_config_option!(predown, command, config) | ||
.and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); | ||
let group = command_else_config_option!(group, command, config); | ||
let working_directory = command_else_config_option!(working_directory, command, config) | ||
.and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); | ||
let dns = command_else_config_option!(dns, command, config); | ||
let user = command_else_config_option!(user, command, config) | ||
.or_else(|| std::env::var("SUDO_USER").ok()); | ||
let port_forwarding_callback = | ||
command_else_config_option!(port_forwarding_callback, command, config) | ||
.and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); | ||
|
||
let no_proxy = command_else_config_bool!(no_proxy, command, config); | ||
let keep_alive = command_else_config_bool!(keep_alive, command, config); | ||
let port_forwarding = command_else_config_bool!(port_forwarding, command, config); | ||
let allow_host_access = command_else_config_bool!(allow_host_access, command, config); | ||
let create_netns_only = command_else_config_bool!(create_netns_only, command, config); | ||
let disable_ipv6 = command_else_config_bool!(disable_ipv6, command, config); | ||
let no_killswitch = command_else_config_bool!(no_killswitch, command, config); | ||
|
||
let firewall = command_else_config_option_variant!(firewall, command, config) | ||
.ok_or_else(|| anyhow!("Failed to get Firewall variant from args")) | ||
.or_else(|_| vopono_core::util::get_firewall())?; | ||
let custom_port_forwarding = | ||
command_else_config_option_variant!(custom_port_forwarding, command, config); | ||
|
||
if custom_port_forwarding.is_some() && custom.is_none() { | ||
log::error!("Custom port forwarding implementation is set, but not using custom provider config file. custom-port-forwarding setting will be ignored"); | ||
} | ||
|
||
// Assign network interface from args or vopono config file | ||
// TODO: Does this work with string from config file? | ||
let interface = command_else_config_option!(interface, command, config); | ||
|
||
let interface: NetworkInterface = match interface { | ||
Some(x) => anyhow::Result::<NetworkInterface>::Ok(x), | ||
None => { | ||
let active_interfaces = get_active_interfaces()?; | ||
if active_interfaces.len() > 1 { | ||
log::warn!("Multiple network interfaces are active: {:#?}, consider specifying the interface with the -i argument. Using {}", &active_interfaces, &active_interfaces[0]); | ||
} | ||
Ok( | ||
NetworkInterface::new( | ||
active_interfaces | ||
.into_iter() | ||
.next() | ||
.ok_or_else(|| anyhow!("No active network interface - consider overriding network interface selection with -i argument"))?, | ||
)?) | ||
} | ||
}?; | ||
log::debug!("Interface: {}", &interface.name); | ||
|
||
let provider: VpnProvider; | ||
let server: String; | ||
let protocol: Protocol; | ||
|
||
// Assign protocol and server from args or vopono config file or custom config if used | ||
if let Some(path) = &custom { | ||
protocol = command | ||
.protocol | ||
.map(|x| x.to_variant()) | ||
.ok_or_else(|| anyhow!(".")) | ||
.or_else(|_| get_config_file_protocol(path))?; | ||
|
||
provider = VpnProvider::Custom; | ||
|
||
if protocol != Protocol::OpenConnect { | ||
// Encode filename with base58 so we can fit it within 16 chars for the veth pair name | ||
let sname = bs58::encode(&path.to_str().unwrap()).into_string(); | ||
|
||
server = sname[0..std::cmp::min(11, sname.len())].to_string(); | ||
} else { | ||
// For OpenConnect the server-name can be provided via the usual config or | ||
// command-line-options. Since it also can be provided via the custom-config we will | ||
// set an empty-string if it isn't provided. | ||
server = command_else_config_option!(server, command, config).unwrap_or_default(); | ||
} | ||
} else { | ||
// Get server and provider | ||
provider = command_else_config_option_variant!(provider, command, config).ok_or_else( | ||
|| { | ||
let msg = | ||
"Enter a VPN provider as a command-line argument or in the vopono config.toml file"; | ||
log::error!("{}", msg); | ||
anyhow!(msg) | ||
}, | ||
)?; | ||
|
||
if provider == VpnProvider::Custom { | ||
error_and_bail!("Must provide config file if using custom VPN Provider"); | ||
} | ||
|
||
server = command_else_config_option!(server, command, config) | ||
// Work-around for providers which do not need a server - TODO: Clean this | ||
.or_else(|| if provider == VpnProvider::Warp {Some("warp".to_owned())} else {None}) | ||
.or_else(|| if provider == VpnProvider::None {Some("none".to_owned())} else {None}) | ||
.ok_or_else(|| { | ||
let msg = "VPN server prefix must be provided as a command-line argument or in the vopono config.toml file"; | ||
log::error!("{}", msg); anyhow!(msg)})?; | ||
|
||
// Check protocol is valid for provider | ||
protocol = command_else_config_option_variant!(protocol, command, config) | ||
.unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); | ||
} | ||
|
||
if (provider == VpnProvider::Warp && protocol != Protocol::Warp) | ||
|| (provider != VpnProvider::Warp && protocol == Protocol::Warp) | ||
{ | ||
error_and_bail!("Cloudflare Warp protocol must use Warp provider"); | ||
} | ||
|
||
if provider == VpnProvider::None && custom.is_some() { | ||
error_and_bail!("Custom config cannot be set when using None provider"); | ||
} | ||
|
||
if (provider == VpnProvider::None && protocol != Protocol::None) | ||
|| (provider != VpnProvider::None && protocol == Protocol::None) | ||
{ | ||
error_and_bail!("None protocol must use None provider - will run not run any VPN service inside netns"); | ||
} | ||
|
||
Ok(Self { | ||
provider, | ||
protocol, | ||
interface, | ||
server, | ||
// TODO: Allow application to be saved in config file? - breaking change to CLI interface | ||
application: command.application, | ||
user, | ||
group, | ||
working_directory, | ||
custom, | ||
dns, | ||
hosts, | ||
open_hosts, | ||
no_killswitch, | ||
keep_alive, | ||
open_ports, | ||
forward, | ||
no_proxy, | ||
firewall, | ||
disable_ipv6, | ||
postup, | ||
predown, | ||
custom_netns_name, | ||
allow_host_access, | ||
port_forwarding, | ||
custom_port_forwarding, | ||
port_forwarding_callback, | ||
create_netns_only, | ||
}) | ||
} | ||
|
||
/// Read vopono config file to Config struct | ||
pub fn get_config_file(command: &ExecCommand) -> anyhow::Result<Config> { | ||
let config_path = command | ||
.vopono_config | ||
.clone() | ||
.ok_or_else(|| anyhow!("No config file passed")) | ||
.or_else::<anyhow::Error, _>(|_| Ok(vopono_dir()?.join("config.toml")))?; | ||
{ | ||
std::fs::OpenOptions::new() | ||
.write(true) | ||
.create(true) | ||
.truncate(false) | ||
.read(true) | ||
.open(&config_path)?; | ||
} | ||
let vopono_config_settings_builder = | ||
config::Config::builder().add_source(config::File::from(config_path.clone())); | ||
vopono_config_settings_builder.build().map_err(|e| { | ||
anyhow!( | ||
"Failed to parse config from: {} , err: {}", | ||
config_path.to_string_lossy(), | ||
e | ||
) | ||
}) | ||
} | ||
} |
Oops, something went wrong.