feat: add automatic Windows service installation and startup with privilege elevation

Add connect_to_service helper that attempts service connection with automatic fallback to service start and installation. Add install-service-direct command for elevated service installation. Split install_windows_service into privilege-checking wrapper and install_windows_service_direct for actual installation. Add start_windows_service function using sc start command. Add is_windows_admin helper using net session to
This commit is contained in:
2026-03-18 07:12:01 +01:00
parent d72a32cce1
commit fc6969d7fb

View File

@@ -76,6 +76,7 @@ fn run() -> Result<(), String> {
return match command.as_str() { return match command.as_str() {
"service" => run_windows_service_dispatcher(), "service" => run_windows_service_dispatcher(),
"install-service" => install_windows_service(), "install-service" => install_windows_service(),
"install-service-direct" => install_windows_service_direct(),
"uninstall-service" => uninstall_windows_service(), "uninstall-service" => uninstall_windows_service(),
"status" => { "status" => {
let profile = parse_profile_arg(args)?; let profile = parse_profile_arg(args)?;
@@ -257,6 +258,32 @@ fn handle_service_client(mut stream: TcpStream) -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn install_windows_service() -> Result<(), String> { fn install_windows_service() -> Result<(), String> {
if is_windows_admin()? {
return install_windows_service_direct();
}
let helper_path = env::current_exe().map_err(|err| format!("Unable to resolve helper path: {err}"))?;
let status = Command::new("powershell")
.arg("-NoProfile")
.arg("-Command")
.arg(format!(
"Start-Process -FilePath '{}' -ArgumentList 'install-service-direct' -Verb RunAs -Wait",
helper_path.display()
))
.creation_flags(CREATE_NO_WINDOW)
.status()
.map_err(|err| format!("Unable to request elevated tunnel service install: {err}"))?;
if !status.success() {
return Err("NexaVPN tunnel service installation was cancelled or failed.".into());
}
std::thread::sleep(Duration::from_secs(2));
Ok(())
}
#[cfg(target_os = "windows")]
fn install_windows_service_direct() -> Result<(), String> {
let manager = ServiceManager::local_computer( let manager = ServiceManager::local_computer(
None::<&str>, None::<&str>,
ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
@@ -308,10 +335,7 @@ fn uninstall_windows_service() -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn windows_client_request(action: &str, profile: &Path) -> Result<(), String> { fn windows_client_request(action: &str, profile: &Path) -> Result<(), String> {
let mut stream = TcpStream::connect(IPC_BIND_ADDR).map_err(|_| { let mut stream = connect_to_service()?;
"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 { let payload = serde_json::to_string(&TunnelRequest {
action: action.to_string(), action: action.to_string(),
@@ -343,10 +367,7 @@ fn windows_client_request(action: &str, profile: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn windows_client_status(profile: &Path) -> Result<bool, String> { fn windows_client_status(profile: &Path) -> Result<bool, String> {
let mut stream = TcpStream::connect(IPC_BIND_ADDR).map_err(|_| { let mut stream = connect_to_service()?;
"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 { let payload = serde_json::to_string(&TunnelRequest {
action: "status".to_string(), action: "status".to_string(),
@@ -376,6 +397,53 @@ fn windows_client_status(profile: &Path) -> Result<bool, String> {
Ok(response.active.unwrap_or(false)) Ok(response.active.unwrap_or(false))
} }
#[cfg(target_os = "windows")]
fn connect_to_service() -> Result<TcpStream, String> {
if let Ok(stream) = TcpStream::connect(IPC_BIND_ADDR) {
return Ok(stream);
}
let _ = start_windows_service();
std::thread::sleep(Duration::from_millis(700));
if let Ok(stream) = TcpStream::connect(IPC_BIND_ADDR) {
return Ok(stream);
}
install_windows_service()?;
std::thread::sleep(Duration::from_secs(2));
if let Ok(stream) = TcpStream::connect(IPC_BIND_ADDR) {
return Ok(stream);
}
Err("NexaVPN background service is not available. Reinstall NexaVPN or install the tunnel service once as administrator.".into())
}
#[cfg(target_os = "windows")]
fn start_windows_service() -> Result<(), String> {
let status = Command::new("sc")
.arg("start")
.arg(SERVICE_NAME)
.creation_flags(CREATE_NO_WINDOW)
.status()
.map_err(|err| format!("Unable to start NexaVPN tunnel service: {err}"))?;
if status.success() {
return Ok(());
}
Err("Unable to start NexaVPN tunnel service.".into())
}
#[cfg(target_os = "windows")]
fn is_windows_admin() -> Result<bool, 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}"))?;
Ok(status.success())
}
fn connect_direct(profile: &Path) -> Result<(), String> { fn connect_direct(profile: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {