Improve router provisioning workflow
This commit is contained in:
@@ -17,4 +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"
|
ssh2 = "0.9"
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -0,0 +1,216 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ENV_FILE="${1:-/tmp/router.env}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo
|
||||||
|
echo "==> $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "ERROR: $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
eval "v=\${$1:-}"
|
||||||
|
[ -n "$v" ] || die "Missing variable: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_configs() {
|
||||||
|
mkdir -p /root/litoral-backups
|
||||||
|
TS="$(date +%Y%m%d-%H%M%S || echo unknown)"
|
||||||
|
cp /etc/config/network "/root/litoral-backups/network.$TS.bak" || true
|
||||||
|
cp /etc/config/firewall "/root/litoral-backups/firewall.$TS.bak" || true
|
||||||
|
cp /etc/config/system "/root/litoral-backups/system.$TS.bak" || true
|
||||||
|
cp /etc/config/uhttpd "/root/litoral-backups/uhttpd.$TS.bak" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -f "$ENV_FILE" ] || die "Missing env file: $ENV_FILE"
|
||||||
|
. "$ENV_FILE"
|
||||||
|
|
||||||
|
require_var ROUTER_ID
|
||||||
|
require_var HOSTNAME
|
||||||
|
require_var LAN_IP
|
||||||
|
require_var LAN_NETMASK
|
||||||
|
require_var WG_IP
|
||||||
|
require_var WG_CIDR
|
||||||
|
require_var WG_SERVER_HOST
|
||||||
|
require_var WG_SERVER_PORT
|
||||||
|
require_var WG_SERVER_PUBLIC_KEY
|
||||||
|
require_var CONTROLLER_IP
|
||||||
|
require_var PLC_IP
|
||||||
|
require_var ROOT_PASSWORD
|
||||||
|
|
||||||
|
log "Starting Litoral_Regas production provisioning"
|
||||||
|
|
||||||
|
log "Verifying board and firmware"
|
||||||
|
BOARD="$(ubus call system board | jsonfilter -e '@.board_name')"
|
||||||
|
VERSION="$(ubus call system board | jsonfilter -e '@.release.version')"
|
||||||
|
|
||||||
|
[ "$BOARD" = "zbtlink,zbt-we826-16m" ] || die "Wrong board: $BOARD"
|
||||||
|
|
||||||
|
case "$VERSION" in
|
||||||
|
23.05.*) echo "OpenWrt version OK: $VERSION" ;;
|
||||||
|
*) die "Wrong OpenWrt version: $VERSION" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "Creating config backups"
|
||||||
|
backup_configs
|
||||||
|
|
||||||
|
log "Setting hostname"
|
||||||
|
uci set system.@system[0].hostname="$HOSTNAME"
|
||||||
|
uci commit system
|
||||||
|
|
||||||
|
log "Setting root password"
|
||||||
|
printf "%s\n%s\n" "$ROOT_PASSWORD" "$ROOT_PASSWORD" | passwd root
|
||||||
|
|
||||||
|
log "Configuring LAN"
|
||||||
|
uci set network.lan.ipaddr="$LAN_IP"
|
||||||
|
uci set network.lan.netmask="$LAN_NETMASK"
|
||||||
|
uci commit network
|
||||||
|
|
||||||
|
log "Preparing WireGuard keys"
|
||||||
|
mkdir -p /etc/wireguard
|
||||||
|
chmod 700 /etc/wireguard
|
||||||
|
|
||||||
|
if [ ! -f /etc/wireguard/privatekey ]; then
|
||||||
|
umask 077
|
||||||
|
wg genkey > /etc/wireguard/privatekey
|
||||||
|
cat /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROUTER_PRIVATE_KEY="$(cat /etc/wireguard/privatekey)"
|
||||||
|
ROUTER_PUBLIC_KEY="$(cat /etc/wireguard/publickey)"
|
||||||
|
|
||||||
|
log "Configuring WireGuard"
|
||||||
|
uci -q delete network.wg0 || true
|
||||||
|
uci -q delete network.wgserver || true
|
||||||
|
|
||||||
|
uci set network.wg0="interface"
|
||||||
|
uci set network.wg0.proto="wireguard"
|
||||||
|
uci set network.wg0.private_key="$ROUTER_PRIVATE_KEY"
|
||||||
|
uci add_list network.wg0.addresses="$WG_IP/$WG_CIDR"
|
||||||
|
|
||||||
|
uci set network.wgserver="wireguard_wg0"
|
||||||
|
uci set network.wgserver.description="Litoral_Regas_VPS"
|
||||||
|
uci set network.wgserver.public_key="$WG_SERVER_PUBLIC_KEY"
|
||||||
|
uci set network.wgserver.endpoint_host="$WG_SERVER_HOST"
|
||||||
|
uci set network.wgserver.endpoint_port="$WG_SERVER_PORT"
|
||||||
|
uci set network.wgserver.persistent_keepalive="25"
|
||||||
|
uci set network.wgserver.route_allowed_ips="1"
|
||||||
|
uci add_list network.wgserver.allowed_ips="198.19.0.0/16"
|
||||||
|
|
||||||
|
uci commit network
|
||||||
|
|
||||||
|
log "Configuring LuCI over WireGuard"
|
||||||
|
uci set uhttpd.main.rfc1918_filter="0"
|
||||||
|
uci commit uhttpd
|
||||||
|
|
||||||
|
log "Configuring firewall zones and forwarding"
|
||||||
|
|
||||||
|
uci -q delete firewall.vpn || true
|
||||||
|
uci -q delete firewall.vpn_lan || true
|
||||||
|
uci -q delete firewall.lan_vpn || true
|
||||||
|
|
||||||
|
uci set firewall.vpn="zone"
|
||||||
|
uci set firewall.vpn.name="vpn"
|
||||||
|
uci set firewall.vpn.input="ACCEPT"
|
||||||
|
uci set firewall.vpn.output="ACCEPT"
|
||||||
|
uci set firewall.vpn.forward="ACCEPT"
|
||||||
|
uci add_list firewall.vpn.network="wg0"
|
||||||
|
|
||||||
|
uci set firewall.vpn_lan="forwarding"
|
||||||
|
uci set firewall.vpn_lan.src="vpn"
|
||||||
|
uci set firewall.vpn_lan.dest="lan"
|
||||||
|
|
||||||
|
uci set firewall.lan_vpn="forwarding"
|
||||||
|
uci set firewall.lan_vpn.src="lan"
|
||||||
|
uci set firewall.lan_vpn.dest="vpn"
|
||||||
|
|
||||||
|
log "Configuring DNAT rules"
|
||||||
|
|
||||||
|
uci -q delete firewall.dnat_controller_vnc || true
|
||||||
|
uci -q delete firewall.dnat_controller_runtime || true
|
||||||
|
uci -q delete firewall.dnat_controller_http || true
|
||||||
|
uci -q delete firewall.dnat_plc_http || true
|
||||||
|
|
||||||
|
uci set firewall.dnat_controller_vnc="redirect"
|
||||||
|
uci set firewall.dnat_controller_vnc.name="DNAT_Controller_VNC_5900"
|
||||||
|
uci set firewall.dnat_controller_vnc.src="vpn"
|
||||||
|
uci set firewall.dnat_controller_vnc.dest="lan"
|
||||||
|
uci set firewall.dnat_controller_vnc.proto="tcp"
|
||||||
|
uci set firewall.dnat_controller_vnc.src_dport="5900"
|
||||||
|
uci set firewall.dnat_controller_vnc.dest_ip="$CONTROLLER_IP"
|
||||||
|
uci set firewall.dnat_controller_vnc.dest_port="5900"
|
||||||
|
uci set firewall.dnat_controller_vnc.target="DNAT"
|
||||||
|
|
||||||
|
uci set firewall.dnat_controller_runtime="redirect"
|
||||||
|
uci set firewall.dnat_controller_runtime.name="DNAT_Controller_20248_to_20249"
|
||||||
|
uci set firewall.dnat_controller_runtime.src="vpn"
|
||||||
|
uci set firewall.dnat_controller_runtime.dest="lan"
|
||||||
|
uci set firewall.dnat_controller_runtime.proto="tcp"
|
||||||
|
uci set firewall.dnat_controller_runtime.src_dport="20248"
|
||||||
|
uci set firewall.dnat_controller_runtime.dest_ip="$CONTROLLER_IP"
|
||||||
|
uci set firewall.dnat_controller_runtime.dest_port="20249"
|
||||||
|
uci set firewall.dnat_controller_runtime.target="DNAT"
|
||||||
|
|
||||||
|
uci set firewall.dnat_controller_http="redirect"
|
||||||
|
uci set firewall.dnat_controller_http.name="DNAT_Controller_HTTP_8000"
|
||||||
|
uci set firewall.dnat_controller_http.src="vpn"
|
||||||
|
uci set firewall.dnat_controller_http.dest="lan"
|
||||||
|
uci set firewall.dnat_controller_http.proto="tcp"
|
||||||
|
uci set firewall.dnat_controller_http.src_dport="8000"
|
||||||
|
uci set firewall.dnat_controller_http.dest_ip="$CONTROLLER_IP"
|
||||||
|
uci set firewall.dnat_controller_http.dest_port="8000"
|
||||||
|
uci set firewall.dnat_controller_http.target="DNAT"
|
||||||
|
|
||||||
|
uci set firewall.dnat_plc_http="redirect"
|
||||||
|
uci set firewall.dnat_plc_http.name="DNAT_PLC_HTTP_81"
|
||||||
|
uci set firewall.dnat_plc_http.src="vpn"
|
||||||
|
uci set firewall.dnat_plc_http.dest="lan"
|
||||||
|
uci set firewall.dnat_plc_http.proto="tcp"
|
||||||
|
uci set firewall.dnat_plc_http.src_dport="81"
|
||||||
|
uci set firewall.dnat_plc_http.dest_ip="$PLC_IP"
|
||||||
|
uci set firewall.dnat_plc_http.dest_port="81"
|
||||||
|
uci set firewall.dnat_plc_http.target="DNAT"
|
||||||
|
|
||||||
|
uci commit firewall
|
||||||
|
|
||||||
|
log "Writing provisioning markers"
|
||||||
|
cat > /etc/litoral-router <<EOF
|
||||||
|
ROUTER_ID=$ROUTER_ID
|
||||||
|
HOSTNAME=$HOSTNAME
|
||||||
|
LAN_IP=$LAN_IP
|
||||||
|
WG_IP=$WG_IP/$WG_CIDR
|
||||||
|
PROVISIONED_AT=$(date || true)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
touch /etc/litoral_provisioned
|
||||||
|
|
||||||
|
log "Restarting services"
|
||||||
|
service system reload
|
||||||
|
service uhttpd restart
|
||||||
|
service firewall restart
|
||||||
|
service network restart
|
||||||
|
|
||||||
|
log "Verification summary"
|
||||||
|
echo "Hostname: $(uci get system.@system[0].hostname)"
|
||||||
|
echo "LAN IP: $(uci get network.lan.ipaddr)"
|
||||||
|
echo "WG IP: $WG_IP/$WG_CIDR"
|
||||||
|
echo "Board: $BOARD"
|
||||||
|
echo "OpenWrt: $VERSION"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ROUTER PUBLIC KEY:"
|
||||||
|
echo "$ROUTER_PUBLIC_KEY"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Add this peer to the VPS:"
|
||||||
|
echo "[Peer]"
|
||||||
|
echo "PublicKey = $ROUTER_PUBLIC_KEY"
|
||||||
|
echo "AllowedIPs = $WG_IP/32"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Provisioning complete."
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
|
use ssh2::Session;
|
||||||
|
use std::process::Command;
|
||||||
use std::{
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{ Read, Write },
|
||||||
|
net::{ TcpStream, ToSocketAddrs },
|
||||||
|
path::Path,
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -8,118 +14,703 @@ fn delay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn detect_router(
|
pub async fn detect_router(ip: String) -> Result<bool, String> {
|
||||||
ip: String,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
delay();
|
delay();
|
||||||
|
|
||||||
if ip.trim().is_empty() {
|
if ip.trim().is_empty() {
|
||||||
return Err(
|
return Err("router IP is required".into());
|
||||||
"router IP is required".into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
fn authenticate_router(session: &Session, password: &str) -> Result<(), String> {
|
||||||
pub async fn upload_firmware(
|
if !password.trim().is_empty() {
|
||||||
ip: String,
|
if session.userauth_password("root", password).is_ok() && session.authenticated() {
|
||||||
firmware_path: String,
|
return Ok(());
|
||||||
) -> Result<String, String> {
|
}
|
||||||
delay();
|
}
|
||||||
|
|
||||||
Ok(format!(
|
if session.userauth_agent("root").is_ok() && session.authenticated() {
|
||||||
"uploaded {} to {}:/tmp/firmware.bin",
|
return Ok(());
|
||||||
firmware_path, ip,
|
}
|
||||||
))
|
|
||||||
|
Err("SSH authentication failed for root".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_system_ssh(ip: &str, command: &str) -> Result<String, String> {
|
||||||
|
let target = format!("root@{}", ip);
|
||||||
|
|
||||||
|
let output = Command::new("ssh")
|
||||||
|
.args([
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=6",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=NUL",
|
||||||
|
&target,
|
||||||
|
command,
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|error| format!("failed to run 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 failed with status {:?}:\n{}\n{}", output.status.code(), stderr, stdout)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_router_session(ip: &str, password: &str) -> Result<Session, String> {
|
||||||
|
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(8)).map_err(|error|
|
||||||
|
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = tcp.set_read_timeout(Some(Duration::from_secs(30)));
|
||||||
|
let _ = tcp.set_write_timeout(Some(Duration::from_secs(30)));
|
||||||
|
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
authenticate_router(&session, password).map_err(|error| format!("root@{}: {}", ip, error))?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scp_file_from_disk(
|
||||||
|
session: &Session,
|
||||||
|
local_path: &str,
|
||||||
|
remote_path: &str,
|
||||||
|
mode: i32
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut local_file = File::open(local_path).map_err(|error|
|
||||||
|
format!("failed to open {}: {}", local_path, error)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let file_size = local_file
|
||||||
|
.metadata()
|
||||||
|
.map_err(|error| format!("failed to read metadata for {}: {}", local_path, error))?
|
||||||
|
.len();
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
local_file
|
||||||
|
.read_to_end(&mut buffer)
|
||||||
|
.map_err(|error| format!("failed to read {}: {}", local_path, error))?;
|
||||||
|
|
||||||
|
let mut remote_file = session
|
||||||
|
.scp_send(Path::new(remote_path), mode, file_size, None)
|
||||||
|
.map_err(|error| format!("failed to start SCP upload to {}: {}", remote_path, error))?;
|
||||||
|
|
||||||
|
remote_file
|
||||||
|
.write_all(&buffer)
|
||||||
|
.map_err(|error| format!("failed to write {}: {}", remote_path, error))?;
|
||||||
|
|
||||||
|
remote_file.send_eof().ok();
|
||||||
|
remote_file.wait_eof().ok();
|
||||||
|
remote_file.close().ok();
|
||||||
|
remote_file.wait_close().ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scp_string(
|
||||||
|
session: &Session,
|
||||||
|
content: &str,
|
||||||
|
remote_path: &str,
|
||||||
|
mode: i32
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let bytes = content.as_bytes();
|
||||||
|
|
||||||
|
let mut remote_file = session
|
||||||
|
.scp_send(Path::new(remote_path), mode, bytes.len() as u64, None)
|
||||||
|
.map_err(|error| format!("failed to start SCP upload to {}: {}", remote_path, error))?;
|
||||||
|
|
||||||
|
remote_file
|
||||||
|
.write_all(bytes)
|
||||||
|
.map_err(|error| format!("failed to write {}: {}", remote_path, error))?;
|
||||||
|
|
||||||
|
remote_file.send_eof().ok();
|
||||||
|
remote_file.wait_eof().ok();
|
||||||
|
remote_file.close().ok();
|
||||||
|
remote_file.wait_close().ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ssh_command(session: &Session, command: &str) -> Result<String, String> {
|
||||||
|
let mut channel = session
|
||||||
|
.channel_session()
|
||||||
|
.map_err(|error| format!("failed to open SSH channel: {}", error))?;
|
||||||
|
|
||||||
|
channel
|
||||||
|
.exec(command)
|
||||||
|
.map_err(|error| format!("failed to run command `{}`: {}", command, error))?;
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
let _ = channel.read_to_string(&mut output);
|
||||||
|
let _ = channel.wait_close();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn flash_router(
|
pub async fn upload_firmware(ip: String, firmware_path: String) -> Result<String, String> {
|
||||||
ip: String,
|
|
||||||
remote_firmware_path: String,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
delay();
|
delay();
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!("uploaded {} to {}:/tmp/firmware.bin", firmware_path, ip))
|
||||||
"sysupgrade started on {} with {}",
|
|
||||||
ip, remote_firmware_path,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn wait_for_ssh(
|
pub async fn flash_router(ip: String, remote_firmware_path: String) -> Result<String, String> {
|
||||||
ip: String,
|
delay();
|
||||||
) -> Result<bool, String> {
|
|
||||||
|
Ok(format!("sysupgrade started on {} with {}", ip, remote_firmware_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn wait_for_ssh(ip: String) -> Result<bool, String> {
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
delay();
|
delay();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip == "198.51.100.1"
|
if ip == "198.51.100.1" || ip == "192.168.1.1" {
|
||||||
|| ip == "192.168.1.1"
|
|
||||||
{
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!("SSH timeout waiting for {}", ip))
|
||||||
"SSH timeout waiting for {}",
|
|
||||||
ip,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn upload_provisioning_bundle(
|
pub async fn run_provisioning(ip: String, password: String) -> Result<String, String> {
|
||||||
ip: String,
|
if ip.trim().is_empty() {
|
||||||
env_content: String,
|
return Err("router IP is required".into());
|
||||||
script_content: String,
|
}
|
||||||
) -> Result<String, String> {
|
|
||||||
delay();
|
|
||||||
|
|
||||||
Ok(format!(
|
let command = "cd /tmp && ./provision.sh";
|
||||||
"uploaded router.env ({} bytes) and provision.sh ({} bytes) to {}",
|
|
||||||
env_content.len(),
|
if password.trim().is_empty() {
|
||||||
script_content.len(),
|
return run_system_ssh(&ip, command);
|
||||||
ip,
|
}
|
||||||
))
|
|
||||||
|
let session = open_router_session(&ip, &password)?;
|
||||||
|
|
||||||
|
run_ssh_command(&session, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn run_provisioning(
|
pub async fn capture_wireguard_public_key(ip: String, password: String) -> Result<String, String> {
|
||||||
ip: String,
|
if ip.trim().is_empty() {
|
||||||
) -> Result<String, String> {
|
return Err("router IP is required".into());
|
||||||
delay();
|
}
|
||||||
|
|
||||||
Ok(format!(
|
let command = "wg show wg0 public-key";
|
||||||
"provision.sh completed on {}; fw4/nftables, wg0, DNAT, LuCI over WireGuard configured",
|
|
||||||
ip,
|
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 public_key = output.trim();
|
||||||
|
|
||||||
|
if public_key.is_empty() {
|
||||||
|
return Err("WireGuard public key was empty".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !public_key.ends_with('=') || public_key.len() < 40 {
|
||||||
|
return Err(format!("unexpected WireGuard public key output: {}", public_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(public_key.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn capture_wireguard_public_key(
|
pub async fn verify_router(ip: String) -> Result<bool, String> {
|
||||||
_ip: String,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
delay();
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
"MOCK_ROUTER_WIREGUARD_PUBLIC_KEY_BASE64="
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn verify_router(
|
|
||||||
ip: String,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
delay();
|
delay();
|
||||||
|
|
||||||
if ip == "198.51.100.1" {
|
if ip == "198.51.100.1" {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Err(
|
Err("router verification failed".into())
|
||||||
"router verification failed"
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn upload_firmware_to_router(ip: String, password: String) -> Result<String, String> {
|
||||||
|
let local_firmware_path =
|
||||||
|
"resources/firmware/openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin";
|
||||||
|
|
||||||
|
let remote_firmware_path = "/tmp/firmware.bin";
|
||||||
|
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err("router IP is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FACTORY / FRESH OPENWRT PATH
|
||||||
|
* Uses system scp when no password is set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if password.trim().is_empty() {
|
||||||
|
let target = format!("root@{}:{}", ip, remote_firmware_path);
|
||||||
|
|
||||||
|
let output = std::process::Command
|
||||||
|
::new("scp")
|
||||||
|
.args([
|
||||||
|
"-O",
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=10",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=accept-new",
|
||||||
|
local_firmware_path,
|
||||||
|
&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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PASSWORD SSH PATH
|
||||||
|
*/
|
||||||
|
|
||||||
|
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(8)).map_err(|error| {
|
||||||
|
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tcp
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(30)))
|
||||||
|
.map_err(|error| { format!("failed to set read timeout: {}", error) })?;
|
||||||
|
|
||||||
|
tcp
|
||||||
|
.set_write_timeout(Some(Duration::from_secs(30)))
|
||||||
|
.map_err(|error| { format!("failed to set 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 local_file = File::open(local_firmware_path).map_err(|error| {
|
||||||
|
format!("failed to open local firmware file {}: {}", local_firmware_path, 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) })?;
|
||||||
|
|
||||||
|
Ok(format!("Firmware uploaded to {}:{} ({} bytes)", ip, remote_firmware_path, firmware_size))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn flash_router_sysupgrade(ip: String, password: String) -> Result<String, String> {
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err("router IP is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let flash_command = "test -f /tmp/firmware.bin && sysupgrade -n /tmp/firmware.bin";
|
||||||
|
|
||||||
|
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 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(8)).map_err(|error| {
|
||||||
|
format!("failed to connect to SSH on {}: {}", ip, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tcp.set_read_timeout(Some(Duration::from_secs(8))).ok();
|
||||||
|
tcp.set_write_timeout(Some(Duration::from_secs(8))).ok();
|
||||||
|
|
||||||
|
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) })?;
|
||||||
|
|
||||||
|
authenticate_router(&session, &password).map_err(|error| {
|
||||||
|
format!("root@{}: {}", ip, error)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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. {}", ip, output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("sysupgrade command submitted on {}. Router should reboot shortly. {}", ip, output))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reconnect_router_after_flash(ip: String, password: String) -> Result<String, String> {
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err("router IP is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_attempts = 30;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = format!("{}:22", ip);
|
||||||
|
|
||||||
|
let socket_address = match address.to_socket_addrs() {
|
||||||
|
Ok(mut addresses) =>
|
||||||
|
match addresses.next() {
|
||||||
|
Some(socket_address) => socket_address,
|
||||||
|
None => {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tcp = match TcpStream::connect_timeout(&socket_address, Duration::from_secs(4)) {
|
||||||
|
Ok(tcp) => tcp,
|
||||||
|
Err(_) => {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = tcp.set_read_timeout(Some(Duration::from_secs(8)));
|
||||||
|
let _ = tcp.set_write_timeout(Some(Duration::from_secs(8)));
|
||||||
|
|
||||||
|
let mut session = match Session::new() {
|
||||||
|
Ok(session) => session,
|
||||||
|
Err(_) => {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
session.set_tcp_stream(tcp);
|
||||||
|
|
||||||
|
if session.handshake().is_err() {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if authenticate_router(&session, &password).is_err() {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channel = match session.channel_session() {
|
||||||
|
Ok(channel) => channel,
|
||||||
|
Err(_) => {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if channel.exec("ubus call system board").is_err() {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
if channel.read_to_string(&mut output).is_err() {
|
||||||
|
thread::sleep(wait_between_attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = channel.wait_close();
|
||||||
|
|
||||||
|
return Ok(
|
||||||
|
format!(
|
||||||
|
"Router reconnected after flash on attempt {}/{}.\n{}",
|
||||||
|
attempt,
|
||||||
|
max_attempts,
|
||||||
|
output
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(
|
||||||
|
format!(
|
||||||
|
"Router did not become reachable over SSH at {} after {} attempts. Replug Ethernet and retry.",
|
||||||
|
ip,
|
||||||
|
max_attempts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_router_after_flash(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 run_system_ssh(&ip, "ubus call system board");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(4)).map_err(|error|
|
||||||
|
format!("SSH not ready at {}: {}", ip, error)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = tcp.set_read_timeout(Some(Duration::from_secs(8)));
|
||||||
|
let _ = tcp.set_write_timeout(Some(Duration::from_secs(8)));
|
||||||
|
|
||||||
|
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: {}", error))?;
|
||||||
|
|
||||||
|
authenticate_router(&session, &password).map_err(|error| format!("root@{}: {}", ip, error))?;
|
||||||
|
|
||||||
|
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 ubus: {}", error))?;
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
channel
|
||||||
|
.read_to_string(&mut output)
|
||||||
|
.map_err(|error| format!("failed to read router info: {}", error))?;
|
||||||
|
|
||||||
|
let _ = channel.wait_close();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn upload_provisioning_bundle(
|
||||||
|
ip: String,
|
||||||
|
password: String,
|
||||||
|
env_content: String
|
||||||
|
) -> Result<String, String> {
|
||||||
|
if ip.trim().is_empty() {
|
||||||
|
return Err("router IP is required".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_script_path = "resources/provisioning/provision.sh";
|
||||||
|
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 script_upload = Command::new("scp")
|
||||||
|
.args([
|
||||||
|
"-O",
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=10",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=NUL",
|
||||||
|
local_script_path,
|
||||||
|
&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, remote_script_path, 0o755)?;
|
||||||
|
|
||||||
|
scp_string(&session, &env_content, remote_env_path, 0o600)?;
|
||||||
|
|
||||||
|
run_ssh_command(&session, "chmod +x /tmp/provision.sh")?;
|
||||||
|
|
||||||
|
Ok(format!("uploaded provision.sh and router.env to {}", ip))
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ use commands::{
|
|||||||
capture_wireguard_public_key,
|
capture_wireguard_public_key,
|
||||||
detect_router,
|
detect_router,
|
||||||
flash_router,
|
flash_router,
|
||||||
|
flash_router_sysupgrade,
|
||||||
run_provisioning,
|
run_provisioning,
|
||||||
upload_firmware,
|
upload_firmware,
|
||||||
|
upload_firmware_to_router,
|
||||||
upload_provisioning_bundle,
|
upload_provisioning_bundle,
|
||||||
|
reconnect_router_after_flash,
|
||||||
verify_router,
|
verify_router,
|
||||||
wait_for_ssh,
|
wait_for_ssh,
|
||||||
|
check_router_after_flash
|
||||||
},
|
},
|
||||||
ssh::{
|
ssh::{
|
||||||
inspect_router_with_password,
|
inspect_router_with_password,
|
||||||
@@ -33,12 +37,16 @@ pub fn run() {
|
|||||||
inspect_router_with_password,
|
inspect_router_with_password,
|
||||||
detect_router,
|
detect_router,
|
||||||
upload_firmware,
|
upload_firmware,
|
||||||
|
upload_firmware_to_router,
|
||||||
flash_router,
|
flash_router,
|
||||||
|
flash_router_sysupgrade,
|
||||||
|
reconnect_router_after_flash,
|
||||||
wait_for_ssh,
|
wait_for_ssh,
|
||||||
upload_provisioning_bundle,
|
upload_provisioning_bundle,
|
||||||
run_provisioning,
|
run_provisioning,
|
||||||
capture_wireguard_public_key,
|
capture_wireguard_public_key,
|
||||||
verify_router,
|
verify_router,
|
||||||
|
check_router_after_flash
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
+16
-16
@@ -8,13 +8,9 @@ import {
|
|||||||
|
|
||||||
import { TopBar } from '@/components/layout/TopBar';
|
import { TopBar } from '@/components/layout/TopBar';
|
||||||
import { MetricCard } from '@/components/dashboard/MetricCard';
|
import { MetricCard } from '@/components/dashboard/MetricCard';
|
||||||
import { ProvisioningWorkflow } from '@/components/dashboard/ProvisioningWorkflow';
|
|
||||||
import { IpPoolChart } from '@/components/dashboard/IpPoolChart';
|
import { IpPoolChart } from '@/components/dashboard/IpPoolChart';
|
||||||
import { NetworkTrafficChart } from '@/components/dashboard/NetworkTrafficChart';
|
import { NetworkTrafficChart } from '@/components/dashboard/NetworkTrafficChart';
|
||||||
|
|
||||||
import { VpnPeersTable } from '@/components/vpn/VpnPeersTable';
|
|
||||||
import { IpManagementPanel } from '@/components/vpn/IpManagementPanel';
|
|
||||||
|
|
||||||
import { ProvisioningWizard } from '@/components/provisioning/ProvisioningWizard';
|
import { ProvisioningWizard } from '@/components/provisioning/ProvisioningWizard';
|
||||||
import { BackendSettings } from '@/components/settings/BackendSettings';
|
import { BackendSettings } from '@/components/settings/BackendSettings';
|
||||||
import { ActivityLogs } from '@/components/activity/ActivityLogs';
|
import { ActivityLogs } from '@/components/activity/ActivityLogs';
|
||||||
@@ -40,8 +36,6 @@ export function DashboardRoute() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
try {
|
try {
|
||||||
setError('');
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
healthResponse,
|
healthResponse,
|
||||||
usedIpsResponse,
|
usedIpsResponse,
|
||||||
@@ -52,7 +46,11 @@ export function DashboardRoute() {
|
|||||||
|
|
||||||
setHealth(healthResponse);
|
setHealth(healthResponse);
|
||||||
setUsedIps(usedIpsResponse);
|
setUsedIps(usedIpsResponse);
|
||||||
|
setError('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setHealth(null);
|
||||||
|
setUsedIps(null);
|
||||||
|
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,15 +82,21 @@ export function DashboardRoute() {
|
|||||||
).toFixed(2);
|
).toFixed(2);
|
||||||
}, [usedCount, ipPoolTotal]);
|
}, [usedCount, ipPoolTotal]);
|
||||||
|
|
||||||
const vpnHealthy =
|
|
||||||
health?.wireGuardRunning ?? false;
|
|
||||||
|
|
||||||
const backendHealthy =
|
const backendHealthy =
|
||||||
health?.backend ?? !error;
|
health?.backend === true;
|
||||||
|
|
||||||
|
const vpnHealthy =
|
||||||
|
health?.wireGuardRunning === true;
|
||||||
|
|
||||||
|
const dashboardReady =
|
||||||
|
!error &&
|
||||||
|
Boolean(health) &&
|
||||||
|
backendHealthy &&
|
||||||
|
vpnHealthy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<TopBar />
|
<TopBar healthy={dashboardReady} />
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-3 rounded-xl border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-300">
|
<div className="mb-3 rounded-xl border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-300">
|
||||||
@@ -146,7 +150,7 @@ export function DashboardRoute() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 grid flex-1 grid-cols-12 grid-rows-[minmax(0,1fr)_auto] gap-4 overflow-hidden">
|
<div className="mt-4 grid min-h-0 flex-1 grid-cols-12 gap-4 overflow-hidden pb-4">
|
||||||
<div className="col-span-9 min-h-0">
|
<div className="col-span-9 min-h-0">
|
||||||
<NetworkTrafficChart />
|
<NetworkTrafficChart />
|
||||||
</div>
|
</div>
|
||||||
@@ -157,10 +161,6 @@ export function DashboardRoute() {
|
|||||||
total={ipPoolTotal}
|
total={ipPoolTotal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12">
|
|
||||||
<ProvisioningWorkflow />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ export function IpPoolChart({
|
|||||||
used,
|
used,
|
||||||
total,
|
total,
|
||||||
}: IpPoolChartProps) {
|
}: IpPoolChartProps) {
|
||||||
const available = Math.max(
|
const available = Math.max(total - used, 0);
|
||||||
total - used,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const percentage =
|
const percentage =
|
||||||
total > 0
|
total > 0
|
||||||
? ((used / total) * 100).toFixed(2)
|
? (used / total) * 100
|
||||||
: '0.00';
|
: 0;
|
||||||
|
|
||||||
|
const displayPercentage =
|
||||||
|
percentage.toFixed(2);
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
@@ -38,23 +38,28 @@ export function IpPoolChart({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full">
|
<Card className="flex h-full flex-col">
|
||||||
<h3 className="mb-4 font-semibold text-white">
|
<div>
|
||||||
IP Pool Usage
|
<h3 className="font-semibold text-white">
|
||||||
</h3>
|
IP Pool Usage
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="flex h-[calc(100%-2rem)] items-center justify-between gap-6">
|
<p className="text-xs text-slate-500">
|
||||||
<div className="h-44 w-44">
|
WireGuard allocation capacity
|
||||||
<ResponsiveContainer
|
</p>
|
||||||
width="100%"
|
</div>
|
||||||
height="100%"
|
|
||||||
>
|
<div className="flex min-h-0 flex-1 flex-col justify-center">
|
||||||
|
<div className="relative mx-auto h-56 w-56">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={data}
|
data={data}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
innerRadius={55}
|
innerRadius={76}
|
||||||
outerRadius={78}
|
outerRadius={100}
|
||||||
|
startAngle={90}
|
||||||
|
endAngle={-270}
|
||||||
paddingAngle={2}
|
paddingAngle={2}
|
||||||
>
|
>
|
||||||
{data.map((_, index) => (
|
{data.map((_, index) => (
|
||||||
@@ -70,26 +75,54 @@ export function IpPoolChart({
|
|||||||
</Pie>
|
</Pie>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<p className="text-4xl font-bold text-white">
|
||||||
|
{displayPercentage}%
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-xs text-slate-500">
|
||||||
|
pool used
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-slate-300">
|
<div className="mt-8 space-y-4">
|
||||||
<p>
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||||
<span className="text-green-300">
|
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||||
|
Used IPs
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-2xl font-bold text-green-300">
|
||||||
{used}
|
{used}
|
||||||
</span>{' '}
|
</p>
|
||||||
used IPs
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-2">
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||||
<span className="text-blue-300">
|
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||||
|
Available IPs
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-2xl font-bold text-blue-300">
|
||||||
{available}
|
{available}
|
||||||
</span>{' '}
|
</p>
|
||||||
available
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-4 text-3xl font-bold text-white">
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||||
{percentage}%
|
<div className="mb-2 flex justify-between text-xs text-slate-500">
|
||||||
</p>
|
<span>Capacity</span>
|
||||||
|
<span>{used} / {total}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-2 overflow-hidden rounded-full bg-slate-800">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-green-400"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(percentage, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function NetworkTrafficChart() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="h-72">
|
<div className="h-[calc(100%-4.5rem)] min-h-[360px]">
|
||||||
<ResponsiveContainer
|
<ResponsiveContainer
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { RefreshCw } from 'lucide-react';
|
|||||||
|
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
|
||||||
export function TopBar() {
|
type TopBarProps = {
|
||||||
|
healthy?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TopBar({
|
||||||
|
healthy = false,
|
||||||
|
}: TopBarProps) {
|
||||||
return (
|
return (
|
||||||
<header className="mb-6 flex items-center justify-between">
|
<header className="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -26,8 +32,12 @@ export function TopBar() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge>
|
<Badge
|
||||||
All Systems Operational
|
tone={healthy ? 'green' : 'red'}
|
||||||
|
>
|
||||||
|
{healthy
|
||||||
|
? 'All Systems Operational'
|
||||||
|
: 'System Issues Detected'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ const DEFAULT_ROUTER_IP = '198.51.100.1';
|
|||||||
const DEFAULT_CONTROLLER_IP = '198.51.100.10';
|
const DEFAULT_CONTROLLER_IP = '198.51.100.10';
|
||||||
const DEFAULT_PLC_IP = '198.51.100.50';
|
const DEFAULT_PLC_IP = '198.51.100.50';
|
||||||
const DEFAULT_FIRMWARE =
|
const DEFAULT_FIRMWARE =
|
||||||
'openwrt-23.05-zbt-we826-16m.bin';
|
'openwrt-23.05.5-zbt-we826-16m-litoral-golden-sysupgrade.bin';
|
||||||
|
|
||||||
export function BackendSettings() {
|
export function BackendSettings() {
|
||||||
const [settings, setSettings] =
|
const [settings, setSettings] =
|
||||||
|
|||||||
Reference in New Issue
Block a user