Skip to content

Commit

Permalink
Add None provider, path expansion, refactoring
Browse files Browse the repository at this point in the history
- 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
jamesmcm committed Mar 2, 2024
1 parent ebe24c5 commit 0221806
Show file tree
Hide file tree
Showing 12 changed files with 706 additions and 567 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vopono"
description = "Launch applications via VPN tunnels using temporary network namespaces"
version = "0.10.8"
version = "0.10.9"
authors = ["James McMurray <[email protected]>"]
edition = "2021"
license = "GPL-3.0-or-later"
Expand Down Expand Up @@ -31,6 +31,7 @@ config = "0.14"
basic_tcp_proxy = "0.3.2"
strum = "0.26"
strum_macros = "0.26"
shellexpand = { version = "3", features = ["full"] }

[package.metadata.rpm]
package = "vopono"
Expand Down
8 changes: 4 additions & 4 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pub struct SynchCommand {
pub struct ExecCommand {
/// VPN Provider (must be given unless using custom config)
#[clap(value_enum, long = "provider", short = 'p', ignore_case = true)]
pub vpn_provider: Option<WrappedArg<VpnProvider>>,
pub provider: Option<WrappedArg<VpnProvider>>,

/// VPN Protocol (if not given will use default)
#[clap(value_enum, long = "protocol", short = 'c', ignore_case = true)]
Expand Down Expand Up @@ -145,15 +145,15 @@ pub struct ExecCommand {

/// Custom VPN Provider - OpenVPN or Wireguard config file (will override other settings)
#[clap(long = "custom")]
pub custom_config: Option<PathBuf>,
pub custom: Option<PathBuf>,

/// DNS Server (will override provider's DNS server)
#[clap(long = "dns", short = 'd')]
pub dns: Option<Vec<IpAddr>>,

/// List of /etc/hosts entries for the network namespace (e.g. "10.0.1.10 webdav.server01.lan","10.0.1.10 vaultwarden.server01.lan"). For a local host you should also provide the open-hosts option.
#[clap(long = "hosts", use_value_delimiter = true)]
pub hosts_entries: Option<Vec<String>>,
pub hosts: Option<Vec<String>>,

/// List of host IP addresses to open on the network namespace (comma separated)
#[clap(long = "open-hosts", use_value_delimiter = true)]
Expand All @@ -174,7 +174,7 @@ pub struct ExecCommand {

/// List of ports to forward from network namespace to host - useful for running servers and daemons
#[clap(long = "forward", short = 'f')]
pub forward_ports: Option<Vec<u16>>,
pub forward: Option<Vec<u16>>,

/// Disable proxying to host machine when forwarding ports
#[clap(long = "no-proxy")]
Expand Down
298 changes: 298 additions & 0 deletions src/args_config.rs
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
)
})
}
}
Loading

0 comments on commit 0221806

Please sign in to comment.