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:
@@ -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")]
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user