Provisioning halfway :P
This commit is contained in:
Generated
+57
@@ -1669,6 +1669,32 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libssh2-sys"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -1696,6 +1722,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"ssh2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
@@ -2045,6 +2072,18 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.115"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2837,6 +2876,18 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh2"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"libc",
|
||||||
|
"libssh2-sys",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -3716,6 +3767,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri-plugin-dialog = "2.7.1"
|
tauri-plugin-dialog = "2.7.1"
|
||||||
tauri-plugin-fs = "2.5.1"
|
tauri-plugin-fs = "2.5.1"
|
||||||
|
ssh2 = "0.9"
|
||||||
@@ -1,16 +1,211 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remove_known_host(
|
pub async fn remove_known_host(
|
||||||
ip: String,
|
ip: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
if ip.trim().is_empty() {
|
if ip.trim().is_empty() {
|
||||||
return Err(
|
return Err(
|
||||||
"ip address cannot be empty"
|
"ip address cannot be empty".into(),
|
||||||
.into(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!(
|
let output = Command::new("ssh-keygen")
|
||||||
"removed stale known_hosts entry for {}",
|
.args(["-R", &ip])
|
||||||
ip,
|
.output()
|
||||||
|
.map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"failed to run ssh-keygen: {}",
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(format!(
|
||||||
|
"removed stale known_hosts entry for {}",
|
||||||
|
ip,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(String::from_utf8_lossy(&output.stderr)
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn probe_router_ssh(
|
||||||
|
ip: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err(
|
||||||
|
"router IP is required".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new("ssh")
|
||||||
|
.args([
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=5",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=accept-new",
|
||||||
|
&format!("root@{}", ip),
|
||||||
|
"ubus call system board",
|
||||||
|
])
|
||||||
|
.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() {
|
||||||
|
return Ok(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if stderr.contains(
|
||||||
|
"REMOTE HOST IDENTIFICATION HAS CHANGED",
|
||||||
|
) {
|
||||||
|
return Err(format!(
|
||||||
|
"STALE_HOST_KEY: SSH host key mismatch for {}. Remove known_hosts entry and retry.",
|
||||||
|
ip,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if stderr.contains("Permission denied")
|
||||||
|
|| stderr.contains("publickey")
|
||||||
|
|| stderr.contains("password")
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"AUTH_REQUIRED: Router is reachable at {}, but SSH requires password login. Try manually: ssh root@{} then run: ubus call system board",
|
||||||
|
ip, ip,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"SSH_FAILED: {}\n{}",
|
||||||
|
stderr, stdout,
|
||||||
))
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
use ssh2::Session;
|
||||||
|
use std::{
|
||||||
|
io::Read,
|
||||||
|
net::{TcpStream, ToSocketAddrs},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn inspect_router_with_password(
|
||||||
|
ip: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err("router IP is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if password.trim().is_empty() {
|
||||||
|
return Err("router password is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = format!("{}:22", ip);
|
||||||
|
|
||||||
|
let socket_address = address
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to resolve router address: {}", error)
|
||||||
|
})?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("failed to resolve router address {}", address)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tcp = TcpStream::connect_timeout(
|
||||||
|
&socket_address,
|
||||||
|
Duration::from_secs(5),
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tcp.set_read_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to set SSH read timeout: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tcp.set_write_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to set SSH write timeout: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut session = Session::new()
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to create SSH session: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
session.set_tcp_stream(tcp);
|
||||||
|
|
||||||
|
session.handshake().map_err(|error| {
|
||||||
|
format!("SSH handshake failed for {}: {}", ip, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
session
|
||||||
|
.userauth_password("root", &password)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"SSH authentication failed for root@{}: {}",
|
||||||
|
ip, error
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !session.authenticated() {
|
||||||
|
return Err(format!(
|
||||||
|
"SSH authentication failed for root@{}",
|
||||||
|
ip
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channel = session.channel_session().map_err(|error| {
|
||||||
|
format!("failed to open SSH channel: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
channel
|
||||||
|
.exec("ubus call system board")
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to run router inspection command: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
|
||||||
|
channel
|
||||||
|
.read_to_string(&mut stdout)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("failed to read router inspection output: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
channel.wait_close().map_err(|error| {
|
||||||
|
format!("failed to close SSH channel: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let exit_status = channel.exit_status().map_err(|error| {
|
||||||
|
format!("failed to read SSH command exit status: {}", error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if exit_status != 0 {
|
||||||
|
return Err(format!(
|
||||||
|
"router inspection command failed with exit code {}",
|
||||||
|
exit_status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stdout)
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,11 @@ use commands::{
|
|||||||
verify_router,
|
verify_router,
|
||||||
wait_for_ssh,
|
wait_for_ssh,
|
||||||
},
|
},
|
||||||
ssh::remove_known_host,
|
ssh::{
|
||||||
|
inspect_router_with_password,
|
||||||
|
probe_router_ssh,
|
||||||
|
remove_known_host,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -25,6 +29,8 @@ pub fn run() {
|
|||||||
read_text_file,
|
read_text_file,
|
||||||
ping_host,
|
ping_host,
|
||||||
remove_known_host,
|
remove_known_host,
|
||||||
|
probe_router_ssh,
|
||||||
|
inspect_router_with_password,
|
||||||
detect_router,
|
detect_router,
|
||||||
upload_firmware,
|
upload_firmware,
|
||||||
flash_router,
|
flash_router,
|
||||||
|
|||||||
@@ -1,277 +1,462 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
KeyRound,
|
||||||
|
Network,
|
||||||
|
Search,
|
||||||
|
ShieldAlert,
|
||||||
|
Terminal,
|
||||||
|
Wifi,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
|
||||||
|
|
||||||
import { ProvisioningStepCard } from './ProvisioningStepCard';
|
|
||||||
import { addActivityLog } from '@/services/activityLogService';
|
import { addActivityLog } from '@/services/activityLogService';
|
||||||
|
|
||||||
import { vpnApi } from '@/services/vpnApi';
|
type DetectionStatus =
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'reachable'
|
||||||
|
| 'ssh_ok'
|
||||||
|
| 'auth_required'
|
||||||
|
| 'stale_host_key'
|
||||||
|
| 'failed';
|
||||||
|
|
||||||
import type {
|
const ROUTER_IPS = [
|
||||||
ProvisioningState,
|
'192.168.1.1',
|
||||||
ProvisionMode,
|
'198.51.100.1',
|
||||||
} from '@/types/provisioning';
|
|
||||||
|
|
||||||
const states: ProvisioningState[] = [
|
|
||||||
'IDLE',
|
|
||||||
'DETECT_ROUTER',
|
|
||||||
'UPLOAD_FIRMWARE',
|
|
||||||
'FLASHING',
|
|
||||||
'WAITING_FOR_REBOOT',
|
|
||||||
'WAITING_FOR_RECONNECT',
|
|
||||||
'UPLOAD_PROVISIONING_BUNDLE',
|
|
||||||
'RUN_PROVISIONING',
|
|
||||||
'CAPTURE_PUBLIC_KEY',
|
|
||||||
'REGISTER_PEER',
|
|
||||||
'VERIFY',
|
|
||||||
'COMPLETE',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const waitMessage =
|
function statusTone(status: DetectionStatus) {
|
||||||
'Router rebooting or network changed. Please reconnect/replug the Ethernet cable if needed. Waiting for router at 198.51.100.1...';
|
if (
|
||||||
|
status === 'reachable' ||
|
||||||
|
status === 'ssh_ok'
|
||||||
|
) {
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
function buildRouterEnv(vpnIp: string) {
|
if (
|
||||||
const routerIdParts = vpnIp.split('.');
|
status === 'auth_required' ||
|
||||||
const routerId = routerIdParts[routerIdParts.length - 1];
|
status === 'stale_host_key'
|
||||||
|
) {
|
||||||
|
return 'purple';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
if (status === 'failed') {
|
||||||
`ROUTER_ID=${routerId}`,
|
return 'red';
|
||||||
`ROUTER_HOSTNAME=Litoral_Regas_${routerId}`,
|
}
|
||||||
`ROUTER_WG_IP=${vpnIp}`,
|
|
||||||
'LAN_SUBNET=198.51.100.0/24',
|
return 'blue';
|
||||||
'ROUTER_LAN_IP=198.51.100.1',
|
|
||||||
'CONTROLLER_IP=198.51.100.10',
|
|
||||||
'PLC_IP=198.51.100.50',
|
|
||||||
'OVERLAY_ROUTE=198.19.0.0/16',
|
|
||||||
'ROUTER_PASSWORD=litoralr',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProvisioningWizard() {
|
export function ProvisioningWizard() {
|
||||||
const [mode, setMode] =
|
const [selectedIp, setSelectedIp] =
|
||||||
useState<ProvisionMode>('new');
|
useState(ROUTER_IPS[0]);
|
||||||
|
|
||||||
const [state, setState] =
|
const [status, setStatus] =
|
||||||
useState<ProvisioningState>('IDLE');
|
useState<DetectionStatus>('idle');
|
||||||
|
|
||||||
|
const [routerInfo, setRouterInfo] =
|
||||||
|
useState('');
|
||||||
|
|
||||||
const [vpnIp, setVpnIp] = useState('');
|
|
||||||
const [log, setLog] = useState<string[]>([]);
|
const [log, setLog] = useState<string[]>([]);
|
||||||
|
|
||||||
const activeIndex = useMemo(
|
function addLog(
|
||||||
() => states.indexOf(state),
|
message: string,
|
||||||
[state],
|
level:
|
||||||
);
|
| 'info'
|
||||||
|
| 'success'
|
||||||
function addLog(message: string) {
|
| 'warning'
|
||||||
|
| 'error' = 'info',
|
||||||
|
) {
|
||||||
setLog((currentLog) => [
|
setLog((currentLog) => [
|
||||||
`${new Date().toLocaleTimeString()} ${message}`,
|
`${new Date().toLocaleTimeString()} ${message}`,
|
||||||
...currentLog,
|
...currentLog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
addActivityLog({
|
addActivityLog({
|
||||||
level: 'info',
|
level,
|
||||||
source: 'desktop',
|
source: 'desktop',
|
||||||
action: state,
|
action: 'ROUTER_DETECTION',
|
||||||
message,
|
message,
|
||||||
routerIp:
|
routerIp: selectedIp,
|
||||||
state.includes('RECONNECT') ||
|
|
||||||
state.includes('PROVISION')
|
|
||||||
? '198.51.100.1'
|
|
||||||
: '192.168.1.1',
|
|
||||||
vpnIp: vpnIp || undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function runWorkflow() {
|
|
||||||
|
async function detectRouter() {
|
||||||
|
setStatus('checking');
|
||||||
|
setRouterInfo('');
|
||||||
|
|
||||||
|
addLog(
|
||||||
|
`Starting safe router detection at ${selectedIp}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState('DETECT_ROUTER');
|
await invoke<boolean>('ping_host', {
|
||||||
|
ip: selectedIp,
|
||||||
|
});
|
||||||
|
|
||||||
addLog(
|
addLog(
|
||||||
'Removing stale known_hosts entries for 192.168.1.1 and 198.51.100.1',
|
`Router responded at ${selectedIp}`,
|
||||||
|
'success',
|
||||||
);
|
);
|
||||||
|
|
||||||
await invoke('detect_router', {
|
try {
|
||||||
ip: '192.168.1.1',
|
const info = await invoke<string>(
|
||||||
});
|
'probe_router_ssh',
|
||||||
|
{
|
||||||
|
ip: selectedIp,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const selectedVpnIp =
|
setRouterInfo(info);
|
||||||
mode === 'new'
|
|
||||||
? (await vpnApi.availableIp()).vpnIp
|
|
||||||
: vpnIp;
|
|
||||||
|
|
||||||
setVpnIp(selectedVpnIp);
|
setStatus('ssh_ok');
|
||||||
addLog(`Using WireGuard IP ${selectedVpnIp}`);
|
|
||||||
|
|
||||||
setState('UPLOAD_FIRMWARE');
|
addLog(
|
||||||
|
`SSH key authentication succeeded at ${selectedIp}`,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
|
||||||
await invoke('upload_firmware', {
|
return;
|
||||||
ip: '192.168.1.1',
|
} catch (sshError) {
|
||||||
firmwarePath:
|
const sshMessage = String(sshError);
|
||||||
'./firmware/openwrt-23.05-zbt-we826-16m.bin',
|
|
||||||
});
|
|
||||||
|
|
||||||
setState('FLASHING');
|
if (
|
||||||
|
sshMessage.includes(
|
||||||
|
'STALE_HOST_KEY',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setStatus('stale_host_key');
|
||||||
|
|
||||||
await invoke('flash_router', {
|
addLog(
|
||||||
ip: '192.168.1.1',
|
`Stale SSH host key detected for ${selectedIp}`,
|
||||||
remoteFirmwarePath: '/tmp/firmware.bin',
|
'warning',
|
||||||
});
|
);
|
||||||
|
|
||||||
setState('WAITING_FOR_REBOOT');
|
return;
|
||||||
addLog(waitMessage);
|
}
|
||||||
|
|
||||||
setState('WAITING_FOR_RECONNECT');
|
if (
|
||||||
|
sshMessage.includes(
|
||||||
|
'AUTH_REQUIRED',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addLog(
|
||||||
|
`Router requires password authentication. Attempting automatic inspection...`,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
|
||||||
await invoke('wait_for_ssh', {
|
try {
|
||||||
ip: '198.51.100.1',
|
const passwordInfo =
|
||||||
});
|
await invoke<string>(
|
||||||
|
'inspect_router_with_password',
|
||||||
|
{
|
||||||
|
ip: selectedIp,
|
||||||
|
password: 'litoralr',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setState('UPLOAD_PROVISIONING_BUNDLE');
|
setRouterInfo(passwordInfo);
|
||||||
|
|
||||||
await invoke('upload_provisioning_bundle', {
|
setStatus('ssh_ok');
|
||||||
ip: '198.51.100.1',
|
|
||||||
envContent: buildRouterEnv(selectedVpnIp),
|
|
||||||
scriptContent:
|
|
||||||
'#!/bin/sh\n# future production provision.sh\n',
|
|
||||||
});
|
|
||||||
|
|
||||||
setState('RUN_PROVISIONING');
|
addLog(
|
||||||
|
`Password SSH inspection succeeded at ${selectedIp}`,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
|
||||||
await invoke('run_provisioning', {
|
return;
|
||||||
ip: '198.51.100.1',
|
} catch (passwordError) {
|
||||||
});
|
setStatus('auth_required');
|
||||||
|
|
||||||
setState('CAPTURE_PUBLIC_KEY');
|
addLog(
|
||||||
|
`Password authentication failed: ${String(
|
||||||
|
passwordError,
|
||||||
|
)}`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
|
||||||
const publicKey = await invoke<string>(
|
return;
|
||||||
'capture_wireguard_public_key',
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw sshError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = String(error);
|
||||||
|
|
||||||
|
setStatus('failed');
|
||||||
|
|
||||||
|
addLog(
|
||||||
|
`Router detection failed: ${message}`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function fixKnownHost() {
|
||||||
|
try {
|
||||||
|
addLog(
|
||||||
|
`Removing known_hosts entry for ${selectedIp}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await invoke<string>(
|
||||||
|
'remove_known_host',
|
||||||
{
|
{
|
||||||
ip: '198.51.100.1',
|
ip: selectedIp,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
addLog(
|
addLog(result, 'success');
|
||||||
`Captured public key ${publicKey.slice(
|
|
||||||
0,
|
|
||||||
12,
|
|
||||||
)}...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
setState('REGISTER_PEER');
|
setStatus('idle');
|
||||||
|
|
||||||
await vpnApi.registerPeer({
|
|
||||||
vpnIp: selectedVpnIp,
|
|
||||||
publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
setState('VERIFY');
|
|
||||||
|
|
||||||
await invoke('verify_router', {
|
|
||||||
ip: '198.51.100.1',
|
|
||||||
});
|
|
||||||
|
|
||||||
setState('COMPLETE');
|
|
||||||
|
|
||||||
addLog(
|
|
||||||
'Provisioning complete. LuCI is available over WireGuard.',
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setState('ERROR');
|
addLog(String(error), 'error');
|
||||||
addLog(String(error));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<Card>
|
<div className="mb-6 flex items-start justify-between">
|
||||||
<div className="flex flex-wrap items-end gap-4">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-bold tracking-tight text-white">
|
||||||
<h3 className="text-xl font-bold text-white">
|
Provisioning
|
||||||
Router Provisioning Wizard
|
</h1>
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="text-sm text-slate-400">
|
<p className="mt-1 text-slate-400">
|
||||||
Validated baseline: OpenWrt
|
Safe router detection and SSH
|
||||||
23.05, ZBT-WE826 16M,
|
inspection. No flashing or
|
||||||
fw4/nftables, no opkg upgrade.
|
configuration changes are performed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<select
|
|
||||||
className="rounded-xl border border-white/10 bg-ink-950 p-2 text-sm text-white"
|
|
||||||
value={mode}
|
|
||||||
onChange={(event) =>
|
|
||||||
setMode(
|
|
||||||
event.target.value as ProvisionMode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="new">
|
|
||||||
New Router
|
|
||||||
</option>
|
|
||||||
|
|
||||||
<option value="reprovision">
|
|
||||||
Reprovision existing VPN IP
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{mode === 'reprovision' && (
|
|
||||||
<input
|
|
||||||
className="rounded-xl border border-white/10 bg-ink-950 p-2 text-sm text-white"
|
|
||||||
placeholder="198.19.1.203"
|
|
||||||
value={vpnIp}
|
|
||||||
onChange={(event) =>
|
|
||||||
setVpnIp(event.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button onClick={runWorkflow}>
|
|
||||||
Start Workflow
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Badge
|
|
||||||
tone={
|
|
||||||
state === 'ERROR'
|
|
||||||
? 'red'
|
|
||||||
: state === 'COMPLETE'
|
|
||||||
? 'green'
|
|
||||||
: 'blue'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{state}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(state === 'WAITING_FOR_REBOOT' ||
|
<Badge tone={statusTone(status)}>
|
||||||
state ===
|
{status.replace(/_/g, ' ')}
|
||||||
'WAITING_FOR_RECONNECT') && (
|
</Badge>
|
||||||
<p className="mt-4 rounded-xl bg-purple-500/10 p-4 text-purple-200">
|
|
||||||
{waitMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
|
||||||
{states.map((provisioningState, index) => (
|
|
||||||
<ProvisioningStepCard
|
|
||||||
key={provisioningState}
|
|
||||||
state={provisioningState}
|
|
||||||
active={index === activeIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<div className="grid min-h-0 flex-1 grid-cols-12 gap-5 overflow-hidden">
|
||||||
<h4 className="mb-3 font-semibold text-white">
|
<Card className="col-span-7 flex flex-col">
|
||||||
Technician Log
|
<div className="mb-6 flex items-center gap-4">
|
||||||
</h4>
|
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
|
||||||
|
<Search size={24} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<pre className="max-h-72 overflow-auto whitespace-pre-wrap text-sm text-slate-300">
|
<div>
|
||||||
{log.join('\n')}
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
Safe Router Detection
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Check reachability and attempt
|
||||||
|
read-only SSH inspection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{ROUTER_IPS.map((ip) => {
|
||||||
|
const active = selectedIp === ip;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ip}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedIp(ip)}
|
||||||
|
className={`rounded-2xl border p-4 text-left transition ${active
|
||||||
|
? 'border-blue-500/40 bg-blue-500/10'
|
||||||
|
: 'border-white/10 bg-white/[0.02] hover:border-blue-500/20 hover:bg-white/[0.04]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Network
|
||||||
|
size={18}
|
||||||
|
className={
|
||||||
|
active
|
||||||
|
? 'text-blue-300'
|
||||||
|
: 'text-slate-500'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-mono text-sm font-semibold text-white">
|
||||||
|
{ip}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-xs text-slate-500">
|
||||||
|
{ip === '192.168.1.1'
|
||||||
|
? 'Factory/default OpenWrt'
|
||||||
|
: 'Provisioned Litoral Regas LAN'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 rounded-2xl border border-white/10 bg-slate-950/70 p-5">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="rounded-2xl bg-green-500/10 p-3 text-green-300">
|
||||||
|
<Wifi size={22} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-semibold text-white">
|
||||||
|
Detection Target
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 font-mono text-sm text-slate-400">
|
||||||
|
root@{selectedIp}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm text-slate-400">
|
||||||
|
Already provisioned routers
|
||||||
|
use password{' '}
|
||||||
|
<span className="font-mono text-slate-200">
|
||||||
|
litoralr
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge tone={statusTone(status)}>
|
||||||
|
{status.replace(/_/g, ' ')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={detectRouter}
|
||||||
|
disabled={status === 'checking'}
|
||||||
|
>
|
||||||
|
<Search size={16} />
|
||||||
|
Detect Router
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={fixKnownHost}
|
||||||
|
>
|
||||||
|
<ShieldAlert size={16} />
|
||||||
|
Fix Known Host
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(status === 'auth_required' ||
|
||||||
|
status === 'stale_host_key') && (
|
||||||
|
<div className="mt-6 rounded-2xl border border-purple-500/20 bg-purple-500/10 p-5">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<AlertTriangle
|
||||||
|
size={20}
|
||||||
|
className="mt-0.5 text-purple-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-purple-200">
|
||||||
|
Manual SSH may be required
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2 text-sm text-purple-100/80">
|
||||||
|
Open a terminal and run:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre className="mt-3 rounded-xl bg-slate-950 p-3 font-mono text-sm text-slate-200">
|
||||||
|
ssh root@{selectedIp}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm text-purple-100/80">
|
||||||
|
Password for provisioned
|
||||||
|
routers:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre className="mt-3 rounded-xl bg-slate-950 p-3 font-mono text-sm text-slate-200">
|
||||||
|
litoralr
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm text-purple-100/80">
|
||||||
|
Once connected, run this
|
||||||
|
read-only command:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre className="mt-3 rounded-xl bg-slate-950 p-3 font-mono text-sm text-slate-200">
|
||||||
|
ubus call system board
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="col-span-5 flex min-h-0 flex-col gap-5">
|
||||||
|
<Card>
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<div className="rounded-2xl bg-green-500/10 p-3 text-green-300">
|
||||||
|
<CheckCircle2 size={22} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-white">
|
||||||
|
Current Safety Scope
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
This screen is read-only.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 text-sm text-slate-300">
|
||||||
|
<p>✓ Ping reachability check</p>
|
||||||
|
<p>✓ SSH host key detection</p>
|
||||||
|
<p>✓ Read-only router inspection</p>
|
||||||
|
<p>✕ No firmware flashing</p>
|
||||||
|
<p>✕ No config upload</p>
|
||||||
|
<p>✕ No password changes</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="min-h-0 flex-1 overflow-hidden">
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
|
||||||
|
<Terminal size={22} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-white">
|
||||||
|
Router Info
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Output from ubus system board.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="min-h-[180px] overflow-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-slate-950 p-4 font-mono text-xs text-slate-300">
|
||||||
|
{routerInfo ||
|
||||||
|
'No router information captured yet.'}
|
||||||
|
</pre>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="mt-5 max-h-44 overflow-hidden">
|
||||||
|
<h3 className="mb-3 font-semibold text-white">
|
||||||
|
Technician Log
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<pre className="max-h-28 overflow-auto whitespace-pre-wrap text-sm text-slate-300">
|
||||||
|
{log.join('\n') ||
|
||||||
|
'No provisioning activity yet.'}
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user