From b3f56a2729bf2dcc722f5237fa08931bb97a8b08 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Wed, 25 Mar 2026 00:12:11 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Tools=20R2=20=E2=80=94=20DNS,=20Whois,?= =?UTF-8?q?=20Bandwidth,=20Subnet=20Calculator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 new tools with full backend + popup UIs: DNS Lookup: - dig/nslookup/host fallback chain on remote host - Record type selector (A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR) Whois: - Remote whois query, first 80 lines - Works for domains and IP addresses Bandwidth Test (2 modes): - iperf3: LAN speed test between remote host and iperf server - Internet: speedtest-cli / curl-based Cloudflare test fallback Subnet Calculator: - Pure Rust, no SSH needed - CIDR input with quick-select buttons (/8 through /32) - Displays: network, broadcast, netmask, wildcard, host range, total/usable hosts, class, private/public Tools menu now has 11 items across 3 sections. Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/tools_commands_r2.rs | 201 ++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/components/tools/BandwidthTest.vue | 44 +++++ src/components/tools/DnsLookup.vue | 31 +++ src/components/tools/SubnetCalc.vue | 49 +++++ src/components/tools/ToolWindow.vue | 8 + src/components/tools/WhoisTool.vue | 26 +++ src/layouts/MainLayout.vue | 30 ++- 9 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/commands/tools_commands_r2.rs create mode 100644 src/components/tools/BandwidthTest.vue create mode 100644 src/components/tools/DnsLookup.vue create mode 100644 src/components/tools/SubnetCalc.vue create mode 100644 src/components/tools/WhoisTool.vue diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 634f985..41c6658 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -10,3 +10,4 @@ pub mod pty_commands; pub mod mcp_commands; pub mod scanner_commands; pub mod tools_commands; +pub mod tools_commands_r2; diff --git a/src-tauri/src/commands/tools_commands_r2.rs b/src-tauri/src/commands/tools_commands_r2.rs new file mode 100644 index 0000000..5438525 --- /dev/null +++ b/src-tauri/src/commands/tools_commands_r2.rs @@ -0,0 +1,201 @@ +//! Tauri commands for Tools Round 2: DNS, Whois, Bandwidth, Subnet Calculator. + +use tauri::State; +use serde::Serialize; + +use crate::AppState; + +// ── DNS Lookup ─────────────────────────────────────────────────────────────── + +#[tauri::command] +pub async fn tool_dns_lookup( + session_id: String, + domain: String, + record_type: Option, + state: State<'_, AppState>, +) -> Result { + let session = state.ssh.get_session(&session_id) + .ok_or_else(|| format!("SSH session {} not found", session_id))?; + let rtype = record_type.unwrap_or_else(|| "A".to_string()); + let cmd = format!( + r#"dig {} {} +short 2>/dev/null || nslookup -type={} {} 2>/dev/null || host -t {} {} 2>/dev/null"#, + domain, rtype, rtype, domain, rtype, domain + ); + exec_on_session(&session.handle, &cmd).await +} + +// ── Whois ──────────────────────────────────────────────────────────────────── + +#[tauri::command] +pub async fn tool_whois( + session_id: String, + target: String, + state: State<'_, AppState>, +) -> Result { + let session = state.ssh.get_session(&session_id) + .ok_or_else(|| format!("SSH session {} not found", session_id))?; + let cmd = format!("whois {} 2>&1 | head -80", target); + exec_on_session(&session.handle, &cmd).await +} + +// ── Bandwidth Test ─────────────────────────────────────────────────────────── + +#[tauri::command] +pub async fn tool_bandwidth_iperf( + session_id: String, + server: String, + duration: Option, + state: State<'_, AppState>, +) -> Result { + let session = state.ssh.get_session(&session_id) + .ok_or_else(|| format!("SSH session {} not found", session_id))?; + let dur = duration.unwrap_or(5); + let cmd = format!( + "iperf3 -c {} -t {} --json 2>/dev/null || iperf3 -c {} -t {} 2>&1 || echo 'iperf3 not installed — run: apt install iperf3 / brew install iperf3'", + server, dur, server, dur + ); + exec_on_session(&session.handle, &cmd).await +} + +#[tauri::command] +pub async fn tool_bandwidth_speedtest( + session_id: String, + state: State<'_, AppState>, +) -> Result { + let session = state.ssh.get_session(&session_id) + .ok_or_else(|| format!("SSH session {} not found", session_id))?; + // Try multiple speedtest tools in order of preference + let cmd = r#" +if command -v speedtest-cli >/dev/null 2>&1; then + speedtest-cli --simple 2>&1 +elif command -v speedtest >/dev/null 2>&1; then + speedtest --simple 2>&1 +elif command -v curl >/dev/null 2>&1; then + echo "=== Download speed (curl) ===" + curl -o /dev/null -w "Download: %{speed_download} bytes/sec (%{size_download} bytes in %{time_total}s)\n" https://speed.cloudflare.com/__down?bytes=25000000 2>/dev/null + echo "=== Upload speed (curl) ===" + dd if=/dev/zero bs=1M count=10 2>/dev/null | curl -X POST -o /dev/null -w "Upload: %{speed_upload} bytes/sec (%{size_upload} bytes in %{time_total}s)\n" -d @- https://speed.cloudflare.com/__up 2>/dev/null +else + echo "No speedtest tool found. Install: pip install speedtest-cli" +fi +"#; + exec_on_session(&session.handle, cmd).await +} + +// ── Subnet Calculator ──────────────────────────────────────────────────────── + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubnetInfo { + pub cidr: String, + pub network: String, + pub broadcast: String, + pub netmask: String, + pub wildcard: String, + pub first_host: String, + pub last_host: String, + pub total_hosts: u64, + pub usable_hosts: u64, + pub prefix_length: u8, + pub class: String, + pub is_private: bool, +} + +/// Pure Rust subnet calculator — no SSH session needed. +#[tauri::command] +pub fn tool_subnet_calc( + cidr: String, +) -> Result { + let parts: Vec<&str> = cidr.split('/').collect(); + if parts.len() != 2 { + return Err("Expected CIDR notation: e.g. 192.168.1.0/24".to_string()); + } + + let ip_str = parts[0]; + let prefix: u8 = parts[1].parse() + .map_err(|_| format!("Invalid prefix length: {}", parts[1]))?; + + if prefix > 32 { + return Err(format!("Prefix length must be 0-32, got {}", prefix)); + } + + let octets: Vec = ip_str.split('.') + .map(|o| o.parse::()) + .collect::, _>>() + .map_err(|_| format!("Invalid IP address: {}", ip_str))?; + + if octets.len() != 4 { + return Err(format!("Invalid IP address: {}", ip_str)); + } + + let ip: u32 = (octets[0] as u32) << 24 + | (octets[1] as u32) << 16 + | (octets[2] as u32) << 8 + | (octets[3] as u32); + + let mask: u32 = if prefix == 0 { 0 } else { !0u32 << (32 - prefix) }; + let wildcard = !mask; + let network = ip & mask; + let broadcast = network | wildcard; + let first_host = if prefix >= 31 { network } else { network + 1 }; + let last_host = if prefix >= 31 { broadcast } else { broadcast - 1 }; + let total: u64 = 1u64 << (32 - prefix as u64); + let usable = if prefix >= 31 { total } else { total - 2 }; + + let class = match octets[0] { + 0..=127 => "A", + 128..=191 => "B", + 192..=223 => "C", + 224..=239 => "D (Multicast)", + _ => "E (Reserved)", + }; + + let is_private = matches!( + (octets[0], octets[1]), + (10, _) | (172, 16..=31) | (192, 168) + ); + + Ok(SubnetInfo { + cidr: format!("{}/{}", to_ip(network), prefix), + network: to_ip(network), + broadcast: to_ip(broadcast), + netmask: to_ip(mask), + wildcard: to_ip(wildcard), + first_host: to_ip(first_host), + last_host: to_ip(last_host), + total_hosts: total, + usable_hosts: usable, + prefix_length: prefix, + class: class.to_string(), + is_private, + }) +} + +fn to_ip(val: u32) -> String { + format!("{}.{}.{}.{}", val >> 24, (val >> 16) & 0xFF, (val >> 8) & 0xFF, val & 0xFF) +} + +// ── Helper ─────────────────────────────────────────────────────────────────── + +async fn exec_on_session( + handle: &std::sync::Arc>>, + cmd: &str, +) -> Result { + let mut channel = { + let h = handle.lock().await; + h.channel_open_session().await.map_err(|e| format!("Exec channel failed: {}", e))? + }; + channel.exec(true, cmd).await.map_err(|e| format!("Exec failed: {}", e))?; + let mut output = String::new(); + loop { + match channel.wait().await { + Some(russh::ChannelMsg::Data { ref data }) => { + if let Ok(text) = std::str::from_utf8(data.as_ref()) { output.push_str(text); } + } + Some(russh::ChannelMsg::Eof) | Some(russh::ChannelMsg::Close) | None => break, + Some(russh::ChannelMsg::ExitStatus { .. }) => {} + _ => {} + } + } + Ok(output) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ffd1436..4bc27f8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -142,6 +142,7 @@ pub fn run() { commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan, commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password, + commands::tools_commands_r2::tool_dns_lookup, commands::tools_commands_r2::tool_whois, commands::tools_commands_r2::tool_bandwidth_iperf, commands::tools_commands_r2::tool_bandwidth_speedtest, commands::tools_commands_r2::tool_subnet_calc, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/tools/BandwidthTest.vue b/src/components/tools/BandwidthTest.vue new file mode 100644 index 0000000..c1edcc6 --- /dev/null +++ b/src/components/tools/BandwidthTest.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/tools/DnsLookup.vue b/src/components/tools/DnsLookup.vue new file mode 100644 index 0000000..51d9491 --- /dev/null +++ b/src/components/tools/DnsLookup.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/tools/SubnetCalc.vue b/src/components/tools/SubnetCalc.vue new file mode 100644 index 0000000..9c8f361 --- /dev/null +++ b/src/components/tools/SubnetCalc.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/tools/ToolWindow.vue b/src/components/tools/ToolWindow.vue index 8ef53d1..51a2882 100644 --- a/src/components/tools/ToolWindow.vue +++ b/src/components/tools/ToolWindow.vue @@ -5,6 +5,10 @@ + + + +
@@ -19,6 +23,10 @@ import PortScanner from "./PortScanner.vue"; import PingTool from "./PingTool.vue"; import TracerouteTool from "./TracerouteTool.vue"; import WakeOnLan from "./WakeOnLan.vue"; +import DnsLookup from "./DnsLookup.vue"; +import WhoisTool from "./WhoisTool.vue"; +import BandwidthTest from "./BandwidthTest.vue"; +import SubnetCalc from "./SubnetCalc.vue"; import SshKeyGen from "./SshKeyGen.vue"; import PasswordGen from "./PasswordGen.vue"; diff --git a/src/components/tools/WhoisTool.vue b/src/components/tools/WhoisTool.vue new file mode 100644 index 0000000..eaf7976 --- /dev/null +++ b/src/components/tools/WhoisTool.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 628e445..5c0e477 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -88,6 +88,30 @@ > Traceroute + + + +