|
|
|
@@ -1,15 +1,61 @@
|
|
|
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::{
|
|
|
|
|
env,
|
|
|
|
|
io::{BufRead, BufReader, Write},
|
|
|
|
|
net::TcpStream,
|
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
|
process::{Command, ExitCode},
|
|
|
|
|
process::{Command, ExitCode, Output},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use std::{
|
|
|
|
|
ffi::OsString,
|
|
|
|
|
net::TcpListener,
|
|
|
|
|
sync::mpsc,
|
|
|
|
|
time::Duration,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use windows_service::{
|
|
|
|
|
define_windows_service,
|
|
|
|
|
service::{
|
|
|
|
|
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
|
|
|
|
|
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
|
|
|
|
|
},
|
|
|
|
|
service_control_handler::{self, ServiceControlHandlerResult},
|
|
|
|
|
service_dispatcher,
|
|
|
|
|
service_manager::{ServiceManager, ServiceManagerAccess},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
const SERVICE_NAME: &str = "NexaVPNTunnelService";
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
const SERVICE_DISPLAY_NAME: &str = "NexaVPN Tunnel Service";
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
const IPC_BIND_ADDR: &str = "127.0.0.1:53189";
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
struct TunnelRequest {
|
|
|
|
|
action: String,
|
|
|
|
|
profile: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
struct TunnelResponse {
|
|
|
|
|
ok: bool,
|
|
|
|
|
error: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() -> ExitCode {
|
|
|
|
|
match run() {
|
|
|
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
|
|
@@ -23,23 +69,253 @@ fn main() -> ExitCode {
|
|
|
|
|
fn run() -> Result<(), String> {
|
|
|
|
|
let mut args = env::args().skip(1);
|
|
|
|
|
let command = args.next().ok_or_else(|| "missing command".to_string())?;
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
return match command.as_str() {
|
|
|
|
|
"service" => run_windows_service_dispatcher(),
|
|
|
|
|
"install-service" => install_windows_service(),
|
|
|
|
|
"uninstall-service" => uninstall_windows_service(),
|
|
|
|
|
"connect" | "disconnect" => {
|
|
|
|
|
let profile = parse_profile_arg(args)?;
|
|
|
|
|
windows_client_request(command.as_str(), &profile)
|
|
|
|
|
}
|
|
|
|
|
_ => Err("unsupported command".into()),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
{
|
|
|
|
|
let profile = parse_profile_arg(args)?;
|
|
|
|
|
match command.as_str() {
|
|
|
|
|
"connect" => connect_direct(&profile),
|
|
|
|
|
"disconnect" => disconnect_direct(&profile),
|
|
|
|
|
_ => Err("unsupported command".into()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_profile_arg(mut args: impl Iterator<Item = String>) -> Result<PathBuf, String> {
|
|
|
|
|
let flag = args.next().ok_or_else(|| "missing --profile flag".to_string())?;
|
|
|
|
|
if flag != "--profile" {
|
|
|
|
|
return Err("expected --profile flag".into());
|
|
|
|
|
}
|
|
|
|
|
let profile = PathBuf::from(args.next().ok_or_else(|| "missing profile path".to_string())?);
|
|
|
|
|
Ok(PathBuf::from(
|
|
|
|
|
args.next().ok_or_else(|| "missing profile path".to_string())?,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match command.as_str() {
|
|
|
|
|
"connect" => connect(&profile),
|
|
|
|
|
"disconnect" => disconnect(&profile),
|
|
|
|
|
_ => Err("unsupported command".into()),
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn run_windows_service_dispatcher() -> Result<(), String> {
|
|
|
|
|
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
|
|
|
|
.map_err(|err| format!("Unable to start NexaVPN Windows service dispatcher: {err}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
define_windows_service!(ffi_service_main, service_main);
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn service_main(_arguments: Vec<OsString>) {
|
|
|
|
|
if let Err(err) = run_service_loop() {
|
|
|
|
|
eprintln!("{err}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn connect(profile: &Path) -> Result<(), String> {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn run_service_loop() -> Result<(), String> {
|
|
|
|
|
let (shutdown_tx, shutdown_rx) = mpsc::channel();
|
|
|
|
|
let status_handle = service_control_handler::register(SERVICE_NAME, move |control_event| match control_event {
|
|
|
|
|
ServiceControl::Stop => {
|
|
|
|
|
let _ = shutdown_tx.send(());
|
|
|
|
|
ServiceControlHandlerResult::NoError
|
|
|
|
|
}
|
|
|
|
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
|
|
|
|
_ => ServiceControlHandlerResult::NotImplemented,
|
|
|
|
|
})
|
|
|
|
|
.map_err(|err| format!("Unable to register NexaVPN service handler: {err}"))?;
|
|
|
|
|
|
|
|
|
|
status_handle
|
|
|
|
|
.set_service_status(ServiceStatus {
|
|
|
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
|
|
|
current_state: ServiceState::Running,
|
|
|
|
|
controls_accepted: ServiceControlAccept::STOP,
|
|
|
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
|
|
|
checkpoint: 0,
|
|
|
|
|
wait_hint: Duration::default(),
|
|
|
|
|
process_id: None,
|
|
|
|
|
})
|
|
|
|
|
.map_err(|err| format!("Unable to publish service status: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let listener = TcpListener::bind(IPC_BIND_ADDR)
|
|
|
|
|
.map_err(|err| format!("Unable to bind NexaVPN service IPC socket: {err}"))?;
|
|
|
|
|
listener
|
|
|
|
|
.set_nonblocking(true)
|
|
|
|
|
.map_err(|err| format!("Unable to configure IPC socket: {err}"))?;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
if shutdown_rx.try_recv().is_ok() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match listener.accept() {
|
|
|
|
|
Ok((stream, _)) => {
|
|
|
|
|
let _ = handle_service_client(stream);
|
|
|
|
|
}
|
|
|
|
|
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
|
|
|
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
return Err(format!("IPC accept failed: {err}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
status_handle
|
|
|
|
|
.set_service_status(ServiceStatus {
|
|
|
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
|
|
|
current_state: ServiceState::Stopped,
|
|
|
|
|
controls_accepted: ServiceControlAccept::empty(),
|
|
|
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
|
|
|
checkpoint: 0,
|
|
|
|
|
wait_hint: Duration::default(),
|
|
|
|
|
process_id: None,
|
|
|
|
|
})
|
|
|
|
|
.map_err(|err| format!("Unable to publish stopped service state: {err}"))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn handle_service_client(mut stream: TcpStream) -> Result<(), String> {
|
|
|
|
|
let mut reader = BufReader::new(
|
|
|
|
|
stream
|
|
|
|
|
.try_clone()
|
|
|
|
|
.map_err(|err| format!("Unable to clone IPC socket: {err}"))?,
|
|
|
|
|
);
|
|
|
|
|
let mut line = String::new();
|
|
|
|
|
reader
|
|
|
|
|
.read_line(&mut line)
|
|
|
|
|
.map_err(|err| format!("Unable to read IPC request: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let request = serde_json::from_str::<TunnelRequest>(&line)
|
|
|
|
|
.map_err(|err| format!("Unable to decode IPC request: {err}"))?;
|
|
|
|
|
let response = match request.action.as_str() {
|
|
|
|
|
"connect" => match connect_direct(Path::new(&request.profile)) {
|
|
|
|
|
Ok(()) => TunnelResponse { ok: true, error: None },
|
|
|
|
|
Err(err) => TunnelResponse {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: Some(err),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"disconnect" => match disconnect_direct(Path::new(&request.profile)) {
|
|
|
|
|
Ok(()) => TunnelResponse { ok: true, error: None },
|
|
|
|
|
Err(err) => TunnelResponse {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: Some(err),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
_ => TunnelResponse {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: Some("unsupported tunnel action".into()),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let payload = serde_json::to_vec(&response).map_err(|err| format!("Unable to encode IPC response: {err}"))?;
|
|
|
|
|
stream
|
|
|
|
|
.write_all(&payload)
|
|
|
|
|
.and_then(|_| stream.write_all(b"\n"))
|
|
|
|
|
.map_err(|err| format!("Unable to write IPC response: {err}"))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn install_windows_service() -> Result<(), String> {
|
|
|
|
|
let manager = ServiceManager::local_computer(
|
|
|
|
|
None::<&str>,
|
|
|
|
|
ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|err| format!("Unable to connect to Service Control Manager: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let helper_path = env::current_exe().map_err(|err| format!("Unable to resolve helper path: {err}"))?;
|
|
|
|
|
let service_info = ServiceInfo {
|
|
|
|
|
name: OsString::from(SERVICE_NAME),
|
|
|
|
|
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
|
|
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
|
|
|
start_type: ServiceStartType::Automatic,
|
|
|
|
|
error_control: ServiceErrorControl::Normal,
|
|
|
|
|
executable_path: helper_path,
|
|
|
|
|
launch_arguments: vec![OsString::from("service")],
|
|
|
|
|
dependencies: vec![],
|
|
|
|
|
account_name: None,
|
|
|
|
|
account_password: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let service = manager
|
|
|
|
|
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START | ServiceAccess::QUERY_STATUS)
|
|
|
|
|
.or_else(|_| {
|
|
|
|
|
manager.open_service(
|
|
|
|
|
SERVICE_NAME,
|
|
|
|
|
ServiceAccess::CHANGE_CONFIG | ServiceAccess::START | ServiceAccess::QUERY_STATUS,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.map_err(|err| format!("Unable to install NexaVPN tunnel service: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let _ = service.start(&[]);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn uninstall_windows_service() -> Result<(), String> {
|
|
|
|
|
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
|
|
|
|
.map_err(|err| format!("Unable to connect to Service Control Manager: {err}"))?;
|
|
|
|
|
let service = manager
|
|
|
|
|
.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE)
|
|
|
|
|
.map_err(|err| format!("Unable to open NexaVPN tunnel service: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let _ = service.stop();
|
|
|
|
|
service
|
|
|
|
|
.delete()
|
|
|
|
|
.map_err(|err| format!("Unable to delete NexaVPN tunnel service: {err}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn windows_client_request(action: &str, profile: &Path) -> Result<(), String> {
|
|
|
|
|
let mut stream = TcpStream::connect(IPC_BIND_ADDR).map_err(|_| {
|
|
|
|
|
"NexaVPN background service is not available. Reinstall NexaVPN or install the tunnel service once as administrator."
|
|
|
|
|
.to_string()
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let payload = serde_json::to_string(&TunnelRequest {
|
|
|
|
|
action: action.to_string(),
|
|
|
|
|
profile: profile.display().to_string(),
|
|
|
|
|
})
|
|
|
|
|
.map_err(|err| format!("Unable to encode tunnel request: {err}"))?;
|
|
|
|
|
|
|
|
|
|
stream
|
|
|
|
|
.write_all(payload.as_bytes())
|
|
|
|
|
.and_then(|_| stream.write_all(b"\n"))
|
|
|
|
|
.map_err(|err| format!("Unable to send tunnel request: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let mut reader = BufReader::new(stream);
|
|
|
|
|
let mut line = String::new();
|
|
|
|
|
reader
|
|
|
|
|
.read_line(&mut line)
|
|
|
|
|
.map_err(|err| format!("Unable to read tunnel service response: {err}"))?;
|
|
|
|
|
|
|
|
|
|
let response = serde_json::from_str::<TunnelResponse>(&line)
|
|
|
|
|
.map_err(|err| format!("Unable to decode tunnel service response: {err}"))?;
|
|
|
|
|
if response.ok {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Err(response
|
|
|
|
|
.error
|
|
|
|
|
.unwrap_or_else(|| "NexaVPN tunnel service reported an unknown error".into()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn connect_direct(profile: &Path) -> Result<(), String> {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
ensure_windows_admin()?;
|
|
|
|
|
let wireguard = find_windows_wireguard()?;
|
|
|
|
|
let output = Command::new(wireguard)
|
|
|
|
|
.arg("/installtunnelservice")
|
|
|
|
@@ -71,10 +347,9 @@ fn connect(profile: &Path) -> Result<(), String> {
|
|
|
|
|
Err("unsupported platform".into())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn disconnect(profile: &Path) -> Result<(), String> {
|
|
|
|
|
fn disconnect_direct(profile: &Path) -> Result<(), String> {
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
ensure_windows_admin()?;
|
|
|
|
|
let wireguard = find_windows_wireguard()?;
|
|
|
|
|
let tunnel_name = profile
|
|
|
|
|
.file_stem()
|
|
|
|
@@ -124,22 +399,7 @@ fn find_windows_wireguard() -> Result<PathBuf, String> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn ensure_windows_admin() -> Result<(), String> {
|
|
|
|
|
let status = Command::new("net")
|
|
|
|
|
.arg("session")
|
|
|
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
|
|
|
.status()
|
|
|
|
|
.map_err(|err| format!("unable to determine Windows privilege level: {err}"))?;
|
|
|
|
|
|
|
|
|
|
if status.success() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Err("Administrator rights are required to activate the VPN tunnel on Windows. Start NexaVPN as Administrator for now.".into())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn format_windows_runtime_error(action: &str, output: &std::process::Output) -> String {
|
|
|
|
|
fn format_windows_runtime_error(action: &str, output: &Output) -> String {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
let details = if !stderr.trim().is_empty() {
|
|
|
|
|