adds old rsa support
This commit is contained in:
+292
-374
@@ -1,5 +1,5 @@
|
||||
use ssh2::Session;
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
@@ -46,22 +46,30 @@ fn authenticate_router(session: &Session, password: &str) -> Result<(), String>
|
||||
Err("SSH authentication failed for root".into())
|
||||
}
|
||||
|
||||
fn system_ssh_base_args(connect_timeout: &str) -> Vec<String> {
|
||||
vec![
|
||||
"-o".into(),
|
||||
"BatchMode=yes".into(),
|
||||
"-o".into(),
|
||||
format!("ConnectTimeout={}", connect_timeout),
|
||||
"-o".into(),
|
||||
"StrictHostKeyChecking=accept-new".into(),
|
||||
"-o".into(),
|
||||
"HostKeyAlgorithms=+ssh-rsa".into(),
|
||||
"-o".into(),
|
||||
"PubkeyAcceptedAlgorithms=+ssh-rsa".into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
|
||||
let target = format!("root@{}", ip);
|
||||
let mut args = system_ssh_base_args("6");
|
||||
|
||||
args.push(target);
|
||||
args.push(command.to_string());
|
||||
|
||||
let output = Command::new("ssh")
|
||||
.args([
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=6",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
&target,
|
||||
command,
|
||||
])
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run system ssh: {}", error))?;
|
||||
|
||||
@@ -80,6 +88,196 @@ fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
|
||||
))
|
||||
}
|
||||
|
||||
fn run_system_ssh_with_stdin(ip: &str, command: &str, input: &[u8]) -> Result<String, String> {
|
||||
let target = format!("root@{}", ip);
|
||||
let mut args = system_ssh_base_args("10");
|
||||
|
||||
args.push(target);
|
||||
args.push(command.to_string());
|
||||
|
||||
let mut child = Command::new("ssh")
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|error| format!("failed to start system ssh: {}", error))?;
|
||||
|
||||
{
|
||||
let stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| "failed to open ssh stdin".to_string())?;
|
||||
|
||||
stdin
|
||||
.write_all(input)
|
||||
.map_err(|error| format!("failed to write ssh stdin: {}", error))?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|error| format!("failed waiting for system ssh: {}", error))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(format!("{}{}", stdout, stderr));
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"system ssh stdin command failed with status {:?}:\n{}\n{}",
|
||||
output.status.code(),
|
||||
stderr,
|
||||
stdout
|
||||
))
|
||||
}
|
||||
|
||||
fn shell_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
||||
}
|
||||
|
||||
fn upload_bytes_via_system_ssh(
|
||||
ip: &str,
|
||||
content: &[u8],
|
||||
remote_path: &str,
|
||||
mode: i32,
|
||||
) -> Result<String, String> {
|
||||
let remote_path_quoted = shell_quote(remote_path);
|
||||
let command = format!(
|
||||
"cat > {} && chmod {:o} {}",
|
||||
remote_path_quoted, mode, remote_path_quoted
|
||||
);
|
||||
|
||||
run_system_ssh_with_stdin(ip, &command, content)
|
||||
}
|
||||
|
||||
fn upload_file_via_system_ssh(
|
||||
ip: &str,
|
||||
local_path: &Path,
|
||||
remote_path: &str,
|
||||
mode: i32,
|
||||
) -> Result<String, String> {
|
||||
let mut local_file = File::open(local_path).map_err(|error| {
|
||||
format!(
|
||||
"failed to open local file {}: {}",
|
||||
local_path.display(),
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
local_file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|error| format!("failed to read local file {}: {}", local_path.display(), error))?;
|
||||
|
||||
upload_bytes_via_system_ssh(ip, &buffer, remote_path, mode)
|
||||
}
|
||||
|
||||
fn run_router_command(ip: &str, password: &str, command: &str) -> Result<String, String> {
|
||||
if !password.trim().is_empty() {
|
||||
match open_router_session(ip, password) {
|
||||
Ok(session) => return run_ssh_command(&session, command),
|
||||
Err(error) => {
|
||||
if !is_ssh_auth_failure(&error) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// The OpenSSH client may have a usable agent/key even when libssh2 cannot use it
|
||||
// on Windows. Fall back to system ssh for key-based auth.
|
||||
return run_system_ssh(ip, command).map_err(|system_error| {
|
||||
format!(
|
||||
"password/libssh2 auth failed: {}; system ssh fallback failed: {}",
|
||||
error, system_error
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_system_ssh(ip, command)
|
||||
}
|
||||
|
||||
fn upload_file_to_router_dynamic(
|
||||
ip: &str,
|
||||
password: &str,
|
||||
local_path: &Path,
|
||||
remote_path: &str,
|
||||
mode: i32,
|
||||
) -> Result<String, String> {
|
||||
if !password.trim().is_empty() {
|
||||
match open_router_session(ip, password) {
|
||||
Ok(session) => {
|
||||
scp_file_from_disk(&session, local_path.to_string_lossy().as_ref(), remote_path, mode)?;
|
||||
return Ok(format!("uploaded {} to {}", local_path.display(), remote_path));
|
||||
}
|
||||
Err(error) => {
|
||||
if !is_ssh_auth_failure(&error) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Fall back to OpenSSH agent/key auth and stdin upload. This also avoids Windows
|
||||
// scp parsing \\?\C:\... as a remote hostname.
|
||||
return upload_file_via_system_ssh(ip, local_path, remote_path, mode).map(|_| {
|
||||
format!(
|
||||
"uploaded {} to {} using system ssh fallback",
|
||||
local_path.display(),
|
||||
remote_path
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upload_file_via_system_ssh(ip, local_path, remote_path, mode).map(|_| {
|
||||
format!(
|
||||
"uploaded {} to {} using system ssh",
|
||||
local_path.display(),
|
||||
remote_path
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn upload_string_to_router_dynamic(
|
||||
ip: &str,
|
||||
password: &str,
|
||||
content: &str,
|
||||
remote_path: &str,
|
||||
mode: i32,
|
||||
) -> Result<String, String> {
|
||||
if !password.trim().is_empty() {
|
||||
match open_router_session(ip, password) {
|
||||
Ok(session) => {
|
||||
scp_string(&session, content, remote_path, mode)?;
|
||||
return Ok(format!("uploaded string to {}", remote_path));
|
||||
}
|
||||
Err(error) => {
|
||||
if !is_ssh_auth_failure(&error) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
return upload_bytes_via_system_ssh(ip, content.as_bytes(), remote_path, mode).map(|_| {
|
||||
format!("uploaded string to {} using system ssh fallback", remote_path)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upload_bytes_via_system_ssh(ip, content.as_bytes(), remote_path, mode).map(|_| {
|
||||
format!("uploaded string to {} using system ssh", remote_path)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn is_ssh_auth_failure(error: &str) -> bool {
|
||||
let lowered = error.to_lowercase();
|
||||
|
||||
lowered.contains("permission denied")
|
||||
|| lowered.contains("publickey,password")
|
||||
|| lowered.contains("authentication failed")
|
||||
}
|
||||
|
||||
fn open_router_session(ip: &str, password: &str) -> Result<Session, String> {
|
||||
let address = format!("{}:22", ip);
|
||||
|
||||
@@ -214,15 +412,7 @@ pub async fn run_provisioning(ip: String, password: String) -> Result<String, St
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let command = "cd /tmp && ./provision.sh";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return run_system_ssh(&ip, command);
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
run_ssh_command(&session, command)
|
||||
run_router_command(&ip, &password, "cd /tmp && ./provision.sh")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -231,15 +421,7 @@ pub async fn capture_wireguard_public_key(ip: String, password: String) -> Resul
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let command = "wg show wg0 public-key";
|
||||
|
||||
let output = if password.trim().is_empty() {
|
||||
run_system_ssh(&ip, command)?
|
||||
} else {
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
run_ssh_command(&session, command)?
|
||||
};
|
||||
|
||||
let output = run_router_command(&ip, &password, "wg show wg0 public-key")?;
|
||||
let public_key = output.trim();
|
||||
|
||||
if public_key.is_empty() {
|
||||
@@ -281,92 +463,17 @@ pub async fn upload_firmware_to_router(
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_firmware_path);
|
||||
let local_firmware_path_string = local_firmware_path.to_string_lossy().to_string();
|
||||
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
"-O",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
&local_firmware_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run system scp: {}", error))?;
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(format!(
|
||||
"Firmware uploaded to {}:{} using system scp",
|
||||
ip, remote_firmware_path
|
||||
));
|
||||
}
|
||||
|
||||
return Err(format!(
|
||||
"system scp failed:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
let mut local_file = File::open(&local_firmware_path).map_err(|error| {
|
||||
format!(
|
||||
"failed to open local firmware file {}: {}",
|
||||
local_firmware_path.display(),
|
||||
error
|
||||
)
|
||||
})?;
|
||||
|
||||
let firmware_size = local_file
|
||||
.metadata()
|
||||
.map_err(|error| format!("failed to read firmware metadata: {}", error))?
|
||||
.len();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
local_file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|error| format!("failed to read firmware file: {}", error))?;
|
||||
|
||||
let mut remote_file = session
|
||||
.scp_send(Path::new(remote_firmware_path), 0o644, firmware_size, None)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to start SCP upload to {}: {}",
|
||||
remote_firmware_path, error
|
||||
)
|
||||
})?;
|
||||
|
||||
remote_file
|
||||
.write_all(&buffer)
|
||||
.map_err(|error| format!("failed to upload firmware via SCP: {}", error))?;
|
||||
|
||||
remote_file
|
||||
.send_eof()
|
||||
.map_err(|error| format!("failed to send SCP EOF: {}", error))?;
|
||||
|
||||
remote_file
|
||||
.wait_eof()
|
||||
.map_err(|error| format!("failed waiting for SCP EOF: {}", error))?;
|
||||
|
||||
remote_file
|
||||
.close()
|
||||
.map_err(|error| format!("failed closing SCP channel: {}", error))?;
|
||||
|
||||
remote_file
|
||||
.wait_close()
|
||||
.map_err(|error| format!("failed waiting for SCP close: {}", error))?;
|
||||
upload_file_to_router_dynamic(
|
||||
&ip,
|
||||
&password,
|
||||
&local_firmware_path,
|
||||
remote_firmware_path,
|
||||
0o644,
|
||||
)?;
|
||||
|
||||
Ok(format!(
|
||||
"Firmware uploaded to {}:{} ({} bytes)",
|
||||
ip, remote_firmware_path, firmware_size
|
||||
"Firmware uploaded to {}:{}",
|
||||
ip, remote_firmware_path
|
||||
))
|
||||
}
|
||||
|
||||
@@ -376,69 +483,40 @@ pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<Str
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let flash_command = "test -f /tmp/firmware.bin && sysupgrade -n /tmp/firmware.bin";
|
||||
let flash_command = "test -f /tmp/firmware.bin && sysupgrade -F -n /tmp/firmware.bin";
|
||||
let output = run_router_command(&ip, &password, flash_command);
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let output = run_system_ssh(&ip, flash_command);
|
||||
|
||||
return match output {
|
||||
Ok(output) => Ok(format!(
|
||||
"sysupgrade command submitted on {} using system ssh. {}",
|
||||
ip, output
|
||||
)),
|
||||
Err(error) => {
|
||||
let lowered = error.to_lowercase();
|
||||
|
||||
if lowered.contains("commencing upgrade")
|
||||
|| lowered.contains("closing all shell sessions")
|
||||
|| lowered.contains("connection failed")
|
||||
|| lowered.contains("connection reset")
|
||||
|| lowered.contains("broken pipe")
|
||||
|| lowered.contains("closed")
|
||||
|| lowered.contains("disconnect")
|
||||
|| lowered.contains("sysupgrade")
|
||||
{
|
||||
Ok(format!(
|
||||
"sysupgrade started on {}; SSH disconnected as expected.",
|
||||
ip
|
||||
))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
let mut channel = session
|
||||
.channel_session()
|
||||
.map_err(|error| format!("failed to open SSH channel: {}", error))?;
|
||||
|
||||
channel
|
||||
.exec(flash_command)
|
||||
.map_err(|error| format!("failed to start sysupgrade: {}", error))?;
|
||||
|
||||
let mut output = String::new();
|
||||
let _ = channel.read_to_string(&mut output);
|
||||
let _ = channel.wait_close();
|
||||
|
||||
let lowered = output.to_lowercase();
|
||||
|
||||
if lowered.contains("commencing upgrade")
|
||||
|| lowered.contains("closing all shell sessions")
|
||||
|| lowered.contains("sysupgrade")
|
||||
{
|
||||
return Ok(format!(
|
||||
"sysupgrade started on {}; router should reboot shortly. {}",
|
||||
match output {
|
||||
Ok(output) => Ok(format!(
|
||||
"sysupgrade command submitted on {}. {}",
|
||||
ip, output
|
||||
));
|
||||
}
|
||||
)),
|
||||
Err(error) => {
|
||||
if is_ssh_auth_failure(&error) {
|
||||
return Err(format!(
|
||||
"AUTH_REQUIRED: Firmware was uploaded, but sysupgrade was NOT started on {} because SSH authentication failed. Enter the router password and run flash again.",
|
||||
ip
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"sysupgrade command submitted on {}. Router should reboot shortly. {}",
|
||||
ip, output
|
||||
))
|
||||
let lowered = error.to_lowercase();
|
||||
|
||||
if lowered.contains("commencing upgrade")
|
||||
|| lowered.contains("closing all shell sessions")
|
||||
|| lowered.contains("connection reset")
|
||||
|| lowered.contains("broken pipe")
|
||||
|| lowered.contains("disconnect")
|
||||
|| lowered.contains("sysupgrade")
|
||||
{
|
||||
Ok(format!(
|
||||
"sysupgrade started on {}; SSH disconnected as expected.",
|
||||
ip
|
||||
))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -451,32 +529,23 @@ pub async fn reconnect_router_after_flash(ip: String, password: String) -> Resul
|
||||
let wait_between_attempts = Duration::from_secs(5);
|
||||
|
||||
for attempt in 1..=max_attempts {
|
||||
if password.trim().is_empty() {
|
||||
match run_system_ssh(&ip, "ubus call system board") {
|
||||
Ok(output) => {
|
||||
return Ok(format!(
|
||||
"Router reconnected after flash on attempt {}/{} using system ssh.\n{}",
|
||||
attempt, max_attempts, output
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
thread::sleep(wait_between_attempts);
|
||||
continue;
|
||||
}
|
||||
match run_router_command(&ip, &password, "ubus call system board") {
|
||||
Ok(output) => {
|
||||
return Ok(format!(
|
||||
"Router reconnected after flash on attempt {}/{}.\n{}",
|
||||
attempt, max_attempts, output
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
match open_router_session(&ip, &password) {
|
||||
Ok(session) => match run_ssh_command(&session, "ubus call system board") {
|
||||
Ok(output) => {
|
||||
return Ok(format!(
|
||||
"Router reconnected after flash on attempt {}/{}.\n{}",
|
||||
attempt, max_attempts, output
|
||||
Err(error) => {
|
||||
if is_ssh_auth_failure(&error) {
|
||||
return Err(format!(
|
||||
"AUTH_REQUIRED: Router is reachable at {} over SSH, but authentication failed. Enter the router password before continuing.",
|
||||
ip
|
||||
));
|
||||
}
|
||||
Err(_) => thread::sleep(wait_between_attempts),
|
||||
},
|
||||
Err(_) => thread::sleep(wait_between_attempts),
|
||||
|
||||
thread::sleep(wait_between_attempts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,12 +561,19 @@ pub async fn check_router_after_flash(ip: String, password: String) -> Result<St
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return run_system_ssh(&ip, "ubus call system board");
|
||||
match run_router_command(&ip, &password, "ubus call system board") {
|
||||
Ok(output) => Ok(output),
|
||||
Err(error) => {
|
||||
if is_ssh_auth_failure(&error) {
|
||||
Err(format!(
|
||||
"AUTH_REQUIRED: Router is reachable at {} over SSH, but authentication failed. Enter the router password before continuing.",
|
||||
ip
|
||||
))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
run_ssh_command(&session, "ubus call system board")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -515,64 +591,12 @@ pub async fn upload_provisioning_bundle(
|
||||
let remote_script_path = "/tmp/provision.sh";
|
||||
let remote_env_path = "/tmp/router.env";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let script_target = format!("root@{}:{}", ip, remote_script_path);
|
||||
let local_script_path_string = local_script_path.to_string_lossy().to_string();
|
||||
|
||||
let script_upload = Command::new("scp")
|
||||
.args([
|
||||
"-O",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
&local_script_path_string,
|
||||
&script_target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run scp for provision.sh: {}", error))?;
|
||||
|
||||
if !script_upload.status.success() {
|
||||
return Err(format!(
|
||||
"failed to upload provision.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&script_upload.stderr),
|
||||
String::from_utf8_lossy(&script_upload.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
let env_command = format!(
|
||||
"cat > {} <<'EOF'\n{}\nEOF\nchmod +x {}",
|
||||
remote_env_path, env_content, remote_script_path
|
||||
);
|
||||
|
||||
run_system_ssh(&ip, &env_command)?;
|
||||
|
||||
return Ok(format!(
|
||||
"uploaded provision.sh and router.env to {} using system ssh/scp",
|
||||
ip
|
||||
));
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_script_path.to_string_lossy().as_ref(),
|
||||
remote_script_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
scp_string(&session, &env_content, remote_env_path, 0o600)?;
|
||||
|
||||
run_ssh_command(&session, "chmod +x /tmp/provision.sh")?;
|
||||
upload_file_to_router_dynamic(&ip, &password, &local_script_path, remote_script_path, 0o755)?;
|
||||
upload_string_to_router_dynamic(&ip, &password, &env_content, remote_env_path, 0o600)?;
|
||||
run_router_command(&ip, &password, "chmod +x /tmp/provision.sh")?;
|
||||
|
||||
Ok(format!("uploaded provision.sh and router.env to {}", ip))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_udp2raw_setup_script(
|
||||
app: AppHandle,
|
||||
@@ -586,50 +610,8 @@ pub async fn upload_udp2raw_setup_script(
|
||||
let local_script_path = resource_path(&app, "udp2raw/setup_udp2raw.sh")?;
|
||||
let remote_script_path = "/tmp/setup_udp2raw.sh";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_script_path);
|
||||
let local_script_path_string = local_script_path.to_string_lossy().to_string();
|
||||
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
"-O",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
&local_script_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run scp for setup_udp2raw.sh: {}", error))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"failed to upload setup_udp2raw.sh:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
run_system_ssh(&ip, "chmod +x /tmp/setup_udp2raw.sh")?;
|
||||
|
||||
return Ok(format!("uploaded setup_udp2raw.sh to {}", ip));
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_script_path.to_string_lossy().as_ref(),
|
||||
remote_script_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
run_ssh_command(&session, "chmod +x /tmp/setup_udp2raw.sh")?;
|
||||
upload_file_to_router_dynamic(&ip, &password, &local_script_path, remote_script_path, 0o755)?;
|
||||
run_router_command(&ip, &password, "chmod +x /tmp/setup_udp2raw.sh")?;
|
||||
|
||||
Ok(format!("uploaded setup_udp2raw.sh to {}", ip))
|
||||
}
|
||||
@@ -640,15 +622,7 @@ pub async fn run_udp2raw_setup(ip: String, password: String) -> Result<String, S
|
||||
return Err("router IP is required".into());
|
||||
}
|
||||
|
||||
let command = "sh /tmp/setup_udp2raw.sh";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return run_system_ssh(&ip, command);
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
run_ssh_command(&session, command)
|
||||
run_router_command(&ip, &password, "sh /tmp/setup_udp2raw.sh")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -703,13 +677,7 @@ pub async fn check_udp2raw_router_status(ip: String, password: String) -> Result
|
||||
wg show wg0 2>/dev/null | grep -A8 '^peer:' || echo "wg0 unavailable"
|
||||
"#;
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return run_system_ssh(&ip, command);
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
run_ssh_command(&session, command)
|
||||
run_router_command(&ip, &password, command)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -752,17 +720,11 @@ pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String,
|
||||
echo "UDP2RAW tunnel test completed"
|
||||
"#;
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return run_system_ssh(&ip, command);
|
||||
}
|
||||
|
||||
let mut last_error = String::new();
|
||||
|
||||
for attempt in 1..=5 {
|
||||
match open_router_session(&ip, &password) {
|
||||
Ok(session) => {
|
||||
return run_ssh_command(&session, command);
|
||||
}
|
||||
match run_router_command(&ip, &password, command) {
|
||||
Ok(output) => return Ok(output),
|
||||
Err(error) => {
|
||||
last_error = format!("SSH attempt {}/5 failed: {}", attempt, error);
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
@@ -786,54 +748,10 @@ pub async fn upload_udp2raw_binary(
|
||||
let local_binary_path = resource_path(&app, "udp2raw/udp2raw")?;
|
||||
let remote_binary_path = "/usr/bin/udp2raw";
|
||||
|
||||
if password.trim().is_empty() {
|
||||
let target = format!("root@{}:{}", ip, remote_binary_path);
|
||||
let local_binary_path_string = local_binary_path.to_string_lossy().to_string();
|
||||
|
||||
let output = Command::new("scp")
|
||||
.args([
|
||||
"-O",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=NUL",
|
||||
&local_binary_path_string,
|
||||
&target,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run scp for udp2raw binary: {}", error))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"failed to upload udp2raw binary:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
|
||||
run_system_ssh(
|
||||
&ip,
|
||||
"chmod +x /usr/bin/udp2raw && /usr/bin/udp2raw --help >/dev/null 2>&1 || true",
|
||||
)?;
|
||||
|
||||
return Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into());
|
||||
}
|
||||
|
||||
let session = open_router_session(&ip, &password)?;
|
||||
|
||||
scp_file_from_disk(
|
||||
&session,
|
||||
local_binary_path.to_string_lossy().as_ref(),
|
||||
remote_binary_path,
|
||||
0o755,
|
||||
)?;
|
||||
|
||||
run_ssh_command(
|
||||
&session,
|
||||
upload_file_to_router_dynamic(&ip, &password, &local_binary_path, remote_binary_path, 0o755)?;
|
||||
run_router_command(
|
||||
&ip,
|
||||
&password,
|
||||
"chmod +x /usr/bin/udp2raw && ls -l /usr/bin/udp2raw",
|
||||
)?;
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ pub async fn probe_router_ssh(
|
||||
"ConnectTimeout=5",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
|
||||
// Legacy OpenWrt/Dropbear routers that only offer ssh-rsa.
|
||||
"-o",
|
||||
"HostKeyAlgorithms=+ssh-rsa",
|
||||
"-o",
|
||||
"PubkeyAcceptedAlgorithms=+ssh-rsa",
|
||||
|
||||
&format!("root@{}", ip),
|
||||
"ubus call system board",
|
||||
])
|
||||
@@ -97,6 +104,44 @@ pub async fn probe_router_ssh(
|
||||
))
|
||||
}
|
||||
|
||||
fn system_ssh_probe(ip: &str, command: &str) -> Result<String, String> {
|
||||
let output = Command::new("ssh")
|
||||
.args([
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
"-o",
|
||||
"HostKeyAlgorithms=+ssh-rsa",
|
||||
"-o",
|
||||
"PubkeyAcceptedAlgorithms=+ssh-rsa",
|
||||
&format!("root@{}", ip),
|
||||
command,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run ssh command: {}", error))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
if output.status.success() {
|
||||
Ok(stdout)
|
||||
} else {
|
||||
Err(format!("SSH_FAILED: {}\n{}", stderr, stdout))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ssh_auth_failure(error: &str) -> bool {
|
||||
let lowered = error.to_lowercase();
|
||||
|
||||
lowered.contains("permission denied")
|
||||
|| lowered.contains("publickey,password")
|
||||
|| lowered.contains("authentication failed")
|
||||
}
|
||||
|
||||
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::Read,
|
||||
@@ -158,20 +203,26 @@ pub async fn inspect_router_with_password(
|
||||
format!("SSH handshake failed for {}: {}", ip, error)
|
||||
})?;
|
||||
|
||||
session
|
||||
.userauth_password("root", &password)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"SSH authentication failed for root@{}: {}",
|
||||
ip, error
|
||||
)
|
||||
})?;
|
||||
let auth_result = session.userauth_password("root", &password);
|
||||
|
||||
if !session.authenticated() {
|
||||
return Err(format!(
|
||||
"SSH authentication failed for root@{}",
|
||||
ip
|
||||
));
|
||||
if auth_result.is_err() || !session.authenticated() {
|
||||
let error = auth_result
|
||||
.err()
|
||||
.map(|error| error.to_string())
|
||||
.unwrap_or_else(|| format!("SSH authentication failed for root@{}", ip));
|
||||
|
||||
// If password auth fails but the local OpenSSH agent has a working key,
|
||||
// still allow detection to succeed. This handles Windows/libssh2 agent gaps.
|
||||
return system_ssh_probe(&ip, "ubus call system board").map_err(|system_error| {
|
||||
if is_ssh_auth_failure(&system_error) {
|
||||
format!(
|
||||
"SSH authentication failed for root@{}: {}; system ssh fallback also failed: {}",
|
||||
ip, error, system_error
|
||||
)
|
||||
} else {
|
||||
system_error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut channel = session.channel_session().map_err(|error| {
|
||||
|
||||
Reference in New Issue
Block a user