diff --git a/README.md b/README.md index 9de88ee..ebf0795 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ vopono includes built-in killswitches for both Wireguard and OpenVPN. Currently Mullvad, AzireVPN, MozillaVPN, ProtonVPN, iVPN, NordVPN, AirVPN, HMA (HideMyAss) and PrivateInternetAccess are supported directly, with custom configuration files also supported with the `--custom` argument. +Cloudflare Warp is also supported. For custom connections the OpenConnect and OpenFortiVPN protocols are also supported (e.g. for enterprise VPNs). See the [vopono User Guide](USERGUIDE.md) for more details. @@ -34,6 +35,7 @@ lynx all running through different VPN connections: | NordVPN | ✅ | ❌ | | HMA (HideMyAss) | ✅ | ❌ | | AirVPN | ✅ | ❌ | +| Cloudflare Warp\*\*\* | ❌ | ❌ | \* For ProtonVPN you can generate and download specific Wireguard config files, and use them as a custom provider config. See the [User Guide](USERGUIDE.md) @@ -41,6 +43,12 @@ for details - note that port forwarding is currently not supported for ProtonVPN \*\* Port forwarding is not currently supported for PrivateInternetAccess. +\*\*\* Cloudflare Warp uses its own protocol. Set both the provider and +protocol to `warp`. Note you must first register with `sudo warp-cli +register` and then run it once with `sudo warp-svc` and `sudo warp-cli +connect` outside of vopono. Please verify this works first before trying +it with vopono. + ## Usage Set up VPN provider configuration files: diff --git a/USERGUIDE.md b/USERGUIDE.md index a848e53..4c696ac 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -453,6 +453,21 @@ the `vopono sync` process in order to copy the `AUTH-*` cookie to access the OpenVPN configuration files, and the OpenVPN specific credentials to use them. +Cloudflare Warp users must first register with Warp via the CLI client: +``` +$ sudo warp-cli register +``` +And then run Warp once to enable automatic connection on service +availability: +``` +$ sudo warp-svc +$ sudo warp-cli connect +``` +You can then kill `warp-svc` and run it via vopono: +``` +$ vopono -v exec --no-killswitch --provider warp --protocol warp firefox-developer-edition +``` + ### VPN Provider limitations #### ProtonVPN diff --git a/src/exec.rs b/src/exec.rs index 1d40032..4afdf27 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -259,11 +259,18 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); } - if provider != VpnProvider::Custom { + if (provider == VpnProvider::Warp && protocol != Protocol::Warp) + || (provider != VpnProvider::Warp && protocol == Protocol::Warp) + { + bail!("Cloudflare Warp protocol must use Warp provider"); + } + + if provider != VpnProvider::Custom && protocol != Protocol::Warp { // Check config files exist for provider let cdir = match protocol { Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(), Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(), + Protocol::Warp => unreachable!("Unreachable, Warp must use Warp provider"), Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"), Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"), }?; @@ -332,12 +339,15 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> }?; debug!("Interface: {}", &interface.name); - let config_file = if provider != VpnProvider::Custom { + let config_file = if protocol == Protocol::Warp { + None + } else if provider != VpnProvider::Custom { let cdir = match protocol { Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(), Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(), Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"), Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"), + Protocol::Warp => unreachable!(), }?; Some(get_config_from_alias(&cdir, &server_name)?) } else { @@ -389,6 +399,11 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> )?; _sysctl = SysCtl::enable_ipv4_forwarding(); match protocol { + Protocol::Warp => ns.run_warp( + command.open_ports.as_ref(), + command.forward_ports.as_ref(), + firewall, + )?, Protocol::OpenVpn => { // Handle authentication check let auth_file = if provider != VpnProvider::Custom { diff --git a/src/list_configs.rs b/src/list_configs.rs index 4ba93be..4959cfc 100644 --- a/src/list_configs.rs +++ b/src/list_configs.rs @@ -21,6 +21,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> { let cdir = match protocol { Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(), Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(), + Protocol::Warp => bail!("Config listing not implemented for Cloudflare Warp"), Protocol::OpenConnect => bail!("Config listing not implemented for OpenConnect"), Protocol::OpenFortiVpn => bail!("Config listing not implemented for OpenFortiVPN"), }?; diff --git a/src/sync.rs b/src/sync.rs index 6b5c19e..7d1556d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -39,6 +39,7 @@ pub fn synch( protocol: Option, uiclient: &dyn UiClient, ) -> anyhow::Result<()> { + // TODO: Separate availability from functionality, so we can filter disabled protocols from the UI match protocol { Some(Protocol::OpenVpn) => { info!("Starting OpenVPN configuration..."); @@ -57,6 +58,9 @@ pub fn synch( Some(Protocol::OpenFortiVpn) => { error!("vopono sync not supported for OpenFortiVpn protocol"); } + Some(Protocol::Warp) => { + error!("vopono sync not supported for Cloudflare Warp protocol"); + } // TODO: Fix this asking for same credentials twice None => { if let Ok(p) = provider.get_dyn_wireguard_provider() { diff --git a/vopono_core/src/config/providers/mod.rs b/vopono_core/src/config/providers/mod.rs index d4871d0..b3cd19f 100644 --- a/vopono_core/src/config/providers/mod.rs +++ b/vopono_core/src/config/providers/mod.rs @@ -8,6 +8,7 @@ mod nordvpn; mod pia; mod protonvpn; mod ui; +mod warp; use crate::config::vpn::Protocol; use crate::util::vopono_dir; @@ -40,6 +41,7 @@ pub enum VpnProvider { IVPN, NordVPN, HMA, + Warp, Custom, } @@ -56,6 +58,7 @@ impl VpnProvider { Self::IVPN => Box::new(ivpn::IVPN {}), Self::NordVPN => Box::new(nordvpn::NordVPN {}), Self::HMA => Box::new(hma::HMA {}), + Self::Warp => Box::new(warp::Warp {}), Self::Custom => unimplemented!("Custom provider uses separate logic"), } } @@ -70,6 +73,7 @@ impl VpnProvider { Self::IVPN => Ok(Box::new(ivpn::IVPN {})), Self::NordVPN => Ok(Box::new(nordvpn::NordVPN {})), Self::HMA => Ok(Box::new(hma::HMA {})), + Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), Self::MozillaVPN => Err(anyhow!("MozillaVPN only supports Wireguard!")), Self::Custom => Err(anyhow!("Custom provider uses separate logic")), } @@ -83,6 +87,7 @@ impl VpnProvider { Self::AzireVPN => Ok(Box::new(azirevpn::AzireVPN {})), Self::IVPN => Ok(Box::new(ivpn::IVPN {})), Self::Custom => Err(anyhow!("Custom provider uses separate logic")), + Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), _ => Err(anyhow!("Wireguard not implemented")), } } @@ -91,6 +96,7 @@ impl VpnProvider { match self { Self::Mullvad => Ok(Box::new(mullvad::Mullvad {})), Self::Custom => Err(anyhow!("Start Shadowsocks manually for custom provider")), + Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), _ => Err(anyhow!("Shadowsocks not supported")), } } diff --git a/vopono_core/src/config/providers/warp/mod.rs b/vopono_core/src/config/providers/warp/mod.rs new file mode 100644 index 0000000..887ba72 --- /dev/null +++ b/vopono_core/src/config/providers/warp/mod.rs @@ -0,0 +1,18 @@ +use super::Provider; +use crate::config::vpn::Protocol; + +pub struct Warp {} + +impl Provider for Warp { + fn alias(&self) -> String { + "warp".to_string() + } + + fn alias_2char(&self) -> String { + "wp".to_string() + } + + fn default_protocol(&self) -> Protocol { + Protocol::Warp + } +} diff --git a/vopono_core/src/config/vpn.rs b/vopono_core/src/config/vpn.rs index 58c149e..aa18f67 100644 --- a/vopono_core/src/config/vpn.rs +++ b/vopono_core/src/config/vpn.rs @@ -74,6 +74,7 @@ pub enum Protocol { Wireguard, OpenConnect, OpenFortiVpn, + Warp, } #[derive(Serialize, Deserialize)] diff --git a/vopono_core/src/network/mod.rs b/vopono_core/src/network/mod.rs index d4dfc8f..f286795 100644 --- a/vopono_core/src/network/mod.rs +++ b/vopono_core/src/network/mod.rs @@ -10,4 +10,5 @@ pub mod openvpn; pub mod shadowsocks; pub mod sysctl; pub mod veth_pair; +pub mod warp; pub mod wireguard; diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index d37dd75..0f79fd7 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -7,6 +7,7 @@ use super::openfortivpn::OpenFortiVpn; use super::openvpn::OpenVpn; use super::shadowsocks::Shadowsocks; use super::veth_pair::VethPair; +use super::warp::Warp; use super::wireguard::Wireguard; use crate::config::providers::{UiClient, VpnProvider}; use crate::config::vpn::Protocol; @@ -36,6 +37,7 @@ pub struct NetworkNamespace { pub veth_pair_ips: Option, pub openconnect: Option, pub openfortivpn: Option, + pub warp: Option, pub provider: VpnProvider, pub protocol: Protocol, pub firewall: Firewall, @@ -93,6 +95,7 @@ impl NetworkNamespace { veth_pair_ips: None, openconnect: None, openfortivpn: None, + warp: None, provider, protocol, firewall, @@ -353,6 +356,16 @@ impl NetworkNamespace { Ok(()) } + pub fn run_warp( + &mut self, + open_ports: Option<&Vec>, + forward_ports: Option<&Vec>, + firewall: Firewall, + ) -> anyhow::Result<()> { + self.warp = Some(Warp::run(self, open_ports, forward_ports, firewall)?); + Ok(()) + } + pub fn run_shadowsocks( &mut self, config_file: &Path, diff --git a/vopono_core/src/network/warp.rs b/vopono_core/src/network/warp.rs new file mode 100644 index 0000000..add954d --- /dev/null +++ b/vopono_core/src/network/warp.rs @@ -0,0 +1,64 @@ +use super::firewall::Firewall; +use super::netns::NetworkNamespace; +use anyhow::{anyhow, Context}; +use log::{debug, error, info}; +use serde::{Deserialize, Serialize}; + +// Cloudflare Warp + +#[derive(Serialize, Deserialize, Debug)] +pub struct Warp { + pid: u32, +} + +impl Warp { + #[allow(clippy::too_many_arguments)] + pub fn run( + netns: &NetworkNamespace, + open_ports: Option<&Vec>, + forward_ports: Option<&Vec>, + firewall: Firewall, + ) -> anyhow::Result { + // TODO: Add Killswitch using - https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/firewall/ + + if let Err(x) = which::which("warp-svc") { + error!("Cloudflare Warp warp-svc not found. Is warp-svc installed and on PATH?"); + return Err(anyhow!( + "warp-svc not found. Is warp-svc installed and on PATH?: {:?}", + x + )); + } + + info!("Launching Warp..."); + + let handle = netns + .exec_no_block(&["warp-svc"], None, None, false, false, false, None) + .context("Failed to launch warp-svc - is waro-svc installed?")?; + + let id = handle.id(); + + // Allow input to and output from open ports (for port forwarding in tunnel) + if let Some(opens) = open_ports { + crate::util::open_ports(netns, opens.as_slice(), firewall)?; + } + + // Allow input to and output from forwarded ports + if let Some(forwards) = forward_ports { + crate::util::open_ports(netns, forwards.as_slice(), firewall)?; + } + + Ok(Self { pid: id }) + } +} + +impl Drop for Warp { + fn drop(&mut self) { + match nix::sys::signal::kill( + nix::unistd::Pid::from_raw(self.pid as i32), + nix::sys::signal::Signal::SIGKILL, + ) { + Ok(_) => debug!("Killed warp-svc (pid: {})", self.pid), + Err(e) => error!("Failed to kill warp-svc (pid: {}): {:?}", self.pid, e), + } + } +}