Last active
December 19, 2025 02:59
-
-
Save bouroo/d263e0f5b7a855a2a58c0c56e12ce498 to your computer and use it in GitHub Desktop.
bulk ping test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "encoding/csv" | |
| "flag" | |
| "fmt" | |
| "net" | |
| "os" | |
| "os/exec" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "time" | |
| ) | |
| // main executes a network connectivity test tool that reads IP addresses | |
| // from a CSV file and tests their availability using multiple methods. | |
| func main() { | |
| ipColumn := flag.Int("ip-column", 1, "Column number containing IP addresses (1-based indexing)") | |
| delimiter := flag.String("delimiter", ",", "CSV delimiter") | |
| timeout := flag.Int("timeout", 2, "Connection timeout in seconds") | |
| flag.Parse() | |
| args := flag.Args() | |
| if len(args) == 0 { | |
| fmt.Printf("Usage: %s <csv_file> [options]\n", os.Args[0]) | |
| fmt.Println("Options:") | |
| fmt.Printf(" --ip-column=N Specify which column contains IP addresses (default: %d)\n", *ipColumn) | |
| fmt.Printf(" --delimiter=D Specify CSV delimiter (default: '%s')\n", *delimiter) | |
| fmt.Printf(" --timeout=T Connection timeout in seconds (default: %d)\n", *timeout) | |
| fmt.Println("\nExamples:") | |
| fmt.Printf(" %s servers.csv\n", os.Args[0]) | |
| fmt.Printf(" %s servers.csv --ip-column=2\n", os.Args[0]) | |
| fmt.Printf(" %s servers.csv --delimiter='\\t' --ip-column=3\n", os.Args[0]) | |
| os.Exit(1) | |
| } | |
| csvFile := args[0] | |
| if _, err := os.Stat(csvFile); os.IsNotExist(err) { | |
| fmt.Printf("Error: File '%s' not found\n", csvFile) | |
| os.Exit(1) | |
| } | |
| fmt.Printf("Starting network connectivity test for servers listed in '%s'\n", csvFile) | |
| fmt.Printf("Using IP column: %d with delimiter: '%s'\n", *ipColumn, *delimiter) | |
| fmt.Println(strings.Repeat("=", 60)) | |
| // Counters for statistics | |
| totalIPs := 0 | |
| onlineCount := 0 | |
| offlineCount := 0 | |
| skippedCount := 0 | |
| tcpFallbackCount := 0 | |
| synFallbackCount := 0 | |
| file, err := os.Open(csvFile) | |
| if err != nil { | |
| fmt.Printf("Error: Cannot open file '%s': %v\n", csvFile, err) | |
| os.Exit(1) | |
| } | |
| defer file.Close() | |
| reader := csv.NewReader(file) | |
| if *delimiter != "," { | |
| reader.Comma = rune((*delimiter)[0]) | |
| } | |
| for { | |
| record, err := reader.Read() | |
| if err != nil { | |
| if err.Error() == "EOF" { | |
| break | |
| } | |
| fmt.Printf("Error reading CSV: %v\n", err) | |
| continue | |
| } | |
| if len(record) == 0 { | |
| continue | |
| } | |
| if len(record) < *ipColumn { | |
| continue | |
| } | |
| ip := strings.TrimSpace(record[*ipColumn-1]) | |
| if ip == "" { | |
| continue | |
| } | |
| if validateIP(ip) { | |
| totalIPs++ | |
| fmt.Printf("Testing connectivity to %s... ", ip) | |
| pingTime, pingError := testSystemPing(ip, *timeout) | |
| if pingError == nil { | |
| fmt.Printf("\033[0;32m✓ ONLINE\033[0m - Server is responding via ICMP (%v)\n", pingTime) | |
| onlineCount++ | |
| continue | |
| } | |
| synTime, synError := testSYNConnection(ip, 80, *timeout) | |
| if synError == nil { | |
| fmt.Printf("\033[0;36m✓ ONLINE\033[0m - Server is responding via SYN check (%v)\n", synTime) | |
| onlineCount++ | |
| synFallbackCount++ | |
| continue | |
| } | |
| tcpTime, tcpError := testTCPConnection(ip, 80, *timeout) | |
| if tcpError == nil { | |
| fmt.Printf("\033[0;33m✓ ONLINE\033[0m - Server is responding via TCP/80 (%v)\n", tcpTime) | |
| onlineCount++ | |
| tcpFallbackCount++ | |
| } else { | |
| fmt.Printf("\033[0;31m✗ OFFLINE\033[0m - No response received via ICMP, SYN check or TCP/80\n") | |
| offlineCount++ | |
| } | |
| } else { | |
| fmt.Printf("\033[1;33m⚠ SKIPPING\033[0m - Invalid IP format: '%s'\n", ip) | |
| skippedCount++ | |
| } | |
| } | |
| fmt.Println(strings.Repeat("=", 60)) | |
| fmt.Println("\033[0;34mNetwork connectivity test completed\033[0m") | |
| fmt.Println(strings.Repeat("-", 60)) | |
| fmt.Println("Summary of results:") | |
| fmt.Printf(" • Total IP addresses processed: %d\n", totalIPs) | |
| fmt.Printf(" • \033[0;32mServers responding: %d\033[0m\n", onlineCount) | |
| fmt.Printf(" • \033[0;31mServers not responding: %d\033[0m\n", offlineCount) | |
| fmt.Printf(" • \033[1;33mInvalid IPs skipped: %d\033[0m\n", skippedCount) | |
| if tcpFallbackCount > 0 { | |
| fmt.Printf(" • \033[0;33mServers found via TCP/80 fallback: %d\033[0m\n", tcpFallbackCount) | |
| } | |
| if synFallbackCount > 0 { | |
| fmt.Printf(" • \033[0;36mServers found via SYN check fallback: %d\033[0m\n", synFallbackCount) | |
| } | |
| fmt.Println(strings.Repeat("-", 60)) | |
| if totalIPs > 0 { | |
| successRate := (onlineCount * 100) / totalIPs | |
| fmt.Printf("Success rate: \033[0;32m%d%%\033[0m of valid servers are online\n", successRate) | |
| } | |
| } | |
| // validateIP checks if the given string is a valid IPv4 address. | |
| func validateIP(ip string) bool { | |
| parts := strings.Split(ip, ".") | |
| if len(parts) != 4 { | |
| return false | |
| } | |
| for _, part := range parts { | |
| if part == "" { | |
| return false | |
| } | |
| num, err := strconv.Atoi(part) | |
| if err != nil || num < 0 || num > 255 { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| // testSystemPing attempts to ping an IP using the system's ping command. | |
| // It returns the ping duration and error if any. | |
| func testSystemPing(ip string, timeoutSeconds int) (time.Duration, error) { | |
| start := time.Now() | |
| var cmd *exec.Cmd | |
| if runtime.GOOS == "windows" { | |
| cmd = exec.Command("ping", "-n", "1", "-w", fmt.Sprintf("%d", timeoutSeconds*1000), ip) | |
| } else { | |
| cmd = exec.Command("ping", "-c", "1", "-W", fmt.Sprintf("%d", timeoutSeconds), ip) | |
| } | |
| output, err := cmd.CombinedOutput() | |
| if err != nil { | |
| return 0, fmt.Errorf("ping command failed: %v", err) | |
| } | |
| if runtime.GOOS == "windows" { | |
| if strings.Contains(string(output), "TTL=") { | |
| lines := strings.Split(string(output), "\n") | |
| for _, line := range lines { | |
| if strings.Contains(line, "time=") || strings.Contains(line, "time<") { | |
| parts := strings.Split(line, "=") | |
| if len(parts) >= 2 { | |
| timeStr := strings.TrimSpace(parts[1]) | |
| if idx := strings.Index(timeStr, "ms"); idx != -1 { | |
| timeStr = timeStr[:idx] | |
| } | |
| if duration, err := time.ParseDuration(timeStr + "ms"); err == nil { | |
| return duration, nil | |
| } | |
| } | |
| } | |
| } | |
| return time.Since(start), nil | |
| } | |
| } else { | |
| if strings.Contains(string(output), "1 packets transmitted, 1 received") || | |
| strings.Contains(string(output), "1 packets transmitted, 1 packets received") || | |
| strings.Contains(string(output), "1 received") { | |
| lines := strings.Split(string(output), "\n") | |
| for _, line := range lines { | |
| if strings.Contains(line, "time=") { | |
| parts := strings.Split(line, "time=") | |
| if len(parts) >= 2 { | |
| timeStr := strings.TrimSpace(parts[1]) | |
| if idx := strings.Index(timeStr, " "); idx != -1 { | |
| timeStr = timeStr[:idx] | |
| } | |
| if duration, err := time.ParseDuration(timeStr + "ms"); err == nil { | |
| return duration, nil | |
| } | |
| } | |
| } | |
| } | |
| return time.Since(start), nil | |
| } | |
| } | |
| return 0, fmt.Errorf("ping failed: no response received") | |
| } | |
| // testTCPConnection tests connectivity to a specific TCP port. | |
| // It returns the connection duration and error if any. | |
| func testTCPConnection(ip string, port int, timeoutSeconds int) (time.Duration, error) { | |
| start := time.Now() | |
| address := fmt.Sprintf("%s:%d", ip, port) | |
| timeout := time.Duration(timeoutSeconds) * time.Second | |
| conn, err := net.DialTimeout("tcp", address, timeout) | |
| if err != nil { | |
| return 0, fmt.Errorf("TCP connection to %s failed: %v", address, err) | |
| } | |
| defer conn.Close() | |
| return time.Since(start), nil | |
| } | |
| // testSYNConnection performs a half-open connection test (SYN scan). | |
| // It returns the connection duration and error if any. | |
| // Note: This is a simplified implementation that uses a TCP connect scan as a fallback. | |
| // True SYN scanning would require raw socket access and elevated privileges. | |
| func testSYNConnection(ip string, port int, timeoutSeconds int) (time.Duration, error) { | |
| start := time.Now() | |
| address := fmt.Sprintf("%s:%d", ip, port) | |
| timeout := time.Duration(timeoutSeconds) * time.Second | |
| conn, err := net.DialTimeout("tcp", address, timeout) | |
| if err != nil { | |
| return 0, fmt.Errorf("SYN connection to %s failed: %v", address, err) | |
| } | |
| _ = conn.Close() | |
| return time.Since(start), nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! Network Connectivity Tester | |
| //! | |
| //! A Rust application that tests network connectivity to servers listed in a CSV file. | |
| //! It performs three-stage connectivity testing: | |
| //! 1. ICMP ping using system command | |
| //! 2. SYN connection check (half-open) | |
| //! 3. TCP/80 connection as fallback | |
| //! | |
| //! The application processes CSV files with customizable IP column position and delimiter. | |
| use clap::{Arg, Command}; | |
| use csv::ReaderBuilder; | |
| use std::fs::File; | |
| use std::io::{self, BufRead}; | |
| use std::net::{IpAddr, SocketAddr, TcpStream}; | |
| use std::process::Command; | |
| use std::str::FromStr; | |
| use std::time::{Duration, Instant}; | |
| fn main() -> io::Result<()> { | |
| let matches = Command::new("network-connectivity-test") | |
| .version("1.0") | |
| .about("Tests network connectivity to servers listed in a CSV file") | |
| .arg( | |
| Arg::new("file") | |
| .help("CSV file containing server information") | |
| .required(true) | |
| .index(1), | |
| ) | |
| .arg( | |
| Arg::new("ip-column") | |
| .long("ip-column") | |
| .value_name("N") | |
| .help("Column number containing IP addresses (1-based indexing)") | |
| .default_value("1"), | |
| ) | |
| .arg( | |
| Arg::new("delimiter") | |
| .long("delimiter") | |
| .value_name("D") | |
| .help("CSV delimiter") | |
| .default_value(","), | |
| ) | |
| .arg( | |
| Arg::new("timeout") | |
| .long("timeout") | |
| .value_name("T") | |
| .help("Connection timeout in seconds") | |
| .default_value("2"), | |
| ) | |
| .get_matches(); | |
| let csv_file = matches.get_one::<String>("file").unwrap(); | |
| let ip_column: usize = matches.get_one::<String>("ip-column").unwrap().parse().unwrap_or(1); | |
| let delimiter = matches.get_one::<String>("delimiter").unwrap().chars().next().unwrap_or(','); | |
| let timeout_seconds: u64 = matches.get_one::<String>("timeout").unwrap().parse().unwrap_or(2); | |
| // Check if file exists | |
| if !std::path::Path::new(csv_file).exists() { | |
| eprintln!("Error: File '{}' not found", csv_file); | |
| std::process::exit(1); | |
| } | |
| println!("Starting network connectivity test for servers listed in '{}'", csv_file); | |
| println!("Using IP column: {} with delimiter: '{}'", ip_column, delimiter); | |
| println!("{}", "=".repeat(60)); | |
| // Counters for statistics | |
| let mut total_ips = 0; | |
| let mut online_count = 0; | |
| let mut offline_count = 0; | |
| let mut skipped_count = 0; | |
| let mut tcp_fallback_count = 0; | |
| let mut syn_fallback_count = 0; | |
| // Open CSV file | |
| let file = File::open(csv_file)?; | |
| let mut reader = ReaderBuilder::new() | |
| .delimiter(delimiter as u8) | |
| .from_reader(file); | |
| // Process CSV records | |
| for result in reader.records() { | |
| match result { | |
| Ok(record) => { | |
| // Skip empty records and records with insufficient columns | |
| if record.is_empty() || record.len() < ip_column { | |
| continue; | |
| } | |
| // Extract IP from the specified column | |
| let ip = record.get(ip_column - 1).unwrap_or("").trim(); | |
| if ip.is_empty() { | |
| continue; | |
| } | |
| // Validate and test IP address | |
| if validate_ip(ip) { | |
| total_ips += 1; | |
| print!("Testing connectivity to {}... ", ip); | |
| // Stage 1: Test ICMP ping | |
| match test_system_ping(ip, timeout_seconds) { | |
| Ok(ping_time) => { | |
| println!("\x1b[0;32m✓ ONLINE\x1b[0m - Server is responding via ICMP ({})", ping_time); | |
| online_count += 1; | |
| continue; // Move to next IP if ICMP succeeds | |
| } | |
| Err(_) => {} // Continue to next test method | |
| } | |
| // Stage 2: Test SYN connection (half-open) | |
| match test_syn_connection(ip, 80, timeout_seconds) { | |
| Ok(syn_time) => { | |
| println!("\x1b[0;36m✓ ONLINE\x1b[0m - Server is responding via SYN check ({})", syn_time); | |
| online_count += 1; | |
| syn_fallback_count += 1; | |
| continue; // Move to next IP if SYN check succeeds | |
| } | |
| Err(_) => {} // Continue to next test method | |
| } | |
| // Stage 3: Test TCP/80 connection | |
| match test_tcp_connection(ip, 80, timeout_seconds) { | |
| Ok(tcp_time) => { | |
| println!("\x1b[0;33m✓ ONLINE\x1b[0m - Server is responding via TCP/80 ({})", tcp_time); | |
| online_count += 1; | |
| tcp_fallback_count += 1; | |
| } | |
| Err(_) => { | |
| println!("\x1b[0;31m✗ OFFLINE\x1b[0m - No response received via ICMP, SYN check or TCP/80"); | |
| offline_count += 1; | |
| } | |
| } | |
| } else { | |
| println!("\x1b[1;33m⚠ SKIPPING\x1b[0m - Invalid IP format: '{}'", ip); | |
| skipped_count += 1; | |
| } | |
| } | |
| Err(err) => { | |
| eprintln!("Error reading CSV: {}", err); | |
| continue; | |
| } | |
| } | |
| } | |
| // Print summary | |
| println!("{}", "=".repeat(60)); | |
| println!("\x1b[0;34mNetwork connectivity test completed\x1b[0m"); | |
| println!("{}", "-".repeat(60)); | |
| println!("Summary of results:"); | |
| println!(" • Total IP addresses processed: {}", total_ips); | |
| println!(" • \x1b[0;32mServers responding: {}\x1b[0m", online_count); | |
| println!(" • \x1b[0;31mServers not responding: {}\x1b[0m", offline_count); | |
| println!(" • \x1b[1;33mInvalid IPs skipped: {}\x1b[0m", skipped_count); | |
| if tcp_fallback_count > 0 { | |
| println!(" • \x1b[0;33mServers found via TCP/80 fallback: {}\x1b[0m", tcp_fallback_count); | |
| } | |
| if syn_fallback_count > 0 { | |
| println!(" • \x1b[0;36mServers found via SYN check fallback: {}\x1b[0m", syn_fallback_count); | |
| } | |
| println!("{}", "-".repeat(60)); | |
| // Calculate success rate | |
| if total_ips > 0 { | |
| let success_rate = (online_count * 100) / total_ips; | |
| println!("Success rate: \x1b[0;32m{}%\x1b[0m of valid servers are online", success_rate); | |
| } | |
| Ok(()) | |
| } | |
| /// Validates if a string is a valid IPv4 address | |
| fn validate_ip(ip: &str) -> bool { | |
| match IpAddr::from_str(ip) { | |
| Ok(IpAddr::V4(_)) => true, | |
| _ => false, | |
| } | |
| } | |
| /// Tests connectivity using system ping command | |
| /// Returns the round-trip time if successful, otherwise an error | |
| fn test_system_ping(ip: &str, timeout_seconds: u64) -> Result<Duration, String> { | |
| let start = Instant::now(); | |
| // Execute ping command based on OS | |
| let output = if cfg!(target_os = "windows") { | |
| Command::new("ping") | |
| .args(&["-n", "1", "-w", &format!("{}", timeout_seconds * 1000), ip]) | |
| .output() | |
| .map_err(|e| format!("Failed to execute ping command: {}", e))? | |
| } else { | |
| Command::new("ping") | |
| .args(&["-c", "1", "-W", &format!("{}", timeout_seconds), ip]) | |
| .output() | |
| .map_err(|e| format!("Failed to execute ping command: {}", e))? | |
| }; | |
| let output_str = String::from_utf8_lossy(&output.stdout); | |
| // Check for success and parse response time | |
| if cfg!(target_os = "windows") { | |
| if output_str.contains("TTL=") { | |
| extract_ping_time(&output_str, "time=", "ms").unwrap_or_else(|| Ok(start.elapsed())) | |
| } else { | |
| Err("Ping failed: no response received".to_string()) | |
| } | |
| } else { | |
| if output_str.contains("1 packets transmitted, 1 received") || | |
| output_str.contains("1 packets transmitted, 1 packets received") || | |
| output_str.contains("1 received") { | |
| extract_ping_time(&output_str, "time=", " ").unwrap_or_else(|| Ok(start.elapsed())) | |
| } else { | |
| Err("Ping failed: no response received".to_string()) | |
| } | |
| } | |
| } | |
| /// Helper function to extract ping time from ping output | |
| fn extract_ping_time(output: &str, prefix: &str, suffix: &str) -> Result<Duration, String> { | |
| for line in output.lines() { | |
| if line.contains(prefix) { | |
| if let Some(time_part) = line.split(prefix).nth(1) { | |
| let time_str = time_part.trim().split(suffix).next().unwrap_or(time_part); | |
| if let Ok(time_ms) = time_str.parse::<f64>() { | |
| return Ok(Duration::from_millis(time_ms as u64)); | |
| } | |
| } | |
| } | |
| } | |
| Err("Could not extract ping time".to_string()) | |
| } | |
| /// Tests TCP connection to a specific port | |
| /// Returns the connection time if successful, otherwise an error | |
| fn test_tcp_connection(ip: &str, port: u16, timeout_seconds: u64) -> Result<Duration, String> { | |
| let start = Instant::now(); | |
| let address = format!("{}:{}", ip, port); | |
| let timeout = Duration::from_secs(timeout_seconds); | |
| match TcpStream::connect_timeout(&address.parse().unwrap(), timeout) { | |
| Ok(_) => Ok(start.elapsed()), | |
| Err(e) => Err(format!("TCP connection to {} failed: {}", address, e)), | |
| } | |
| } | |
| /// Tests connectivity using a half-open SYN connection | |
| /// Returns the connection time if successful, otherwise an error | |
| /// | |
| /// Note: This implementation uses TCP connect as a fallback since true SYN scanning | |
| /// requires raw socket access with elevated privileges | |
| fn test_syn_connection(ip: &str, port: u16, timeout_seconds: u64) -> Result<Duration, String> { | |
| let start = Instant::now(); | |
| let address = format!("{}:{}", ip, port); | |
| let timeout = Duration::from_secs(timeout_seconds); | |
| match TcpStream::connect_timeout(&address.parse().unwrap(), timeout) { | |
| Ok(_) => Ok(start.elapsed()), | |
| Err(e) => Err(format!("SYN connection to {} failed: {}", address, e)), | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # === CONFIGURATION === | |
| IP_COLUMN=1 | |
| DELIMITER="," | |
| TIMEOUT=2 | |
| # === ARGUMENT PARSING === | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --ip-column=*) IP_COLUMN="${1#*=}" ;; | |
| --delimiter=*) DELIMITER="${1#*=}" ;; | |
| --timeout=*) TIMEOUT="${1#*=}" ;; | |
| --help) | |
| echo "Usage: $0 <csv_file> [options]" | |
| echo "Options:" | |
| echo " --ip-column=N Specify which column contains IP addresses (default: $IP_COLUMN)" | |
| echo " --delimiter=D Specify CSV delimiter (default: '$DELIMITER')" | |
| echo " --timeout=T Connection timeout in seconds (default: $TIMEOUT)" | |
| echo "" | |
| echo "Examples:" | |
| echo " $0 servers.csv" | |
| echo " $0 servers.csv --ip-column=2" | |
| echo " $0 servers.csv --delimiter='\\t' --ip-column=3" | |
| exit 0 | |
| ;; | |
| -*) echo "Unknown option: $1" && exit 1 ;; | |
| *) | |
| if [ -z "$CSV_FILE" ]; then CSV_FILE="$1" | |
| else echo "Error: Multiple CSV files specified" && exit 1 | |
| fi | |
| ;; | |
| esac | |
| shift | |
| done | |
| # === VALIDATION === | |
| if [ -z "$CSV_FILE" ]; then | |
| echo "Usage: $0 <csv_file> [options]" | |
| echo "Options:" | |
| echo " --ip-column=N Specify which column contains IP addresses (default: $IP_COLUMN)" | |
| echo " --delimiter=D Specify CSV delimiter (default: '$DELIMITER')" | |
| echo " --timeout=T Connection timeout in seconds (default: $TIMEOUT)" | |
| echo "" | |
| echo "Examples:" | |
| echo " $0 servers.csv" | |
| echo " $0 servers.csv --ip-column=2" | |
| echo " $0 servers.csv --delimiter='\\t' --ip-column=3" | |
| exit 1 | |
| fi | |
| if [ ! -f "$CSV_FILE" ]; then | |
| echo "Error: File '$CSV_FILE' not found" | |
| exit 1 | |
| fi | |
| # === INITIALIZATION === | |
| echo "Starting network connectivity test for servers listed in '$CSV_FILE'" | |
| echo "Using IP column: $IP_COLUMN with delimiter: '$DELIMITER'" | |
| echo "$(printf '=%.0s' {1..60})" | |
| TOTAL_IPS=0 | |
| ONLINE_COUNT=0 | |
| OFFLINE_COUNT=0 | |
| SKIPPED_COUNT=0 | |
| TCP_FALLBACK_COUNT=0 | |
| SYN_FALLBACK_COUNT=0 | |
| # === PLATFORM DETECTION === | |
| OS_TYPE=$(uname) | |
| PING_CMD="ping" | |
| PING_OPTS="" | |
| case "$OS_TYPE" in | |
| "Darwin"|"Linux") PING_OPTS="-c 1 -W $TIMEOUT" ;; | |
| "CYGWIN"*|"MINGW"*|"MSYS"*) PING_OPTS="-n 1 -w $((TIMEOUT * 1000))" ;; | |
| esac | |
| # === UTILITY FUNCTIONS === | |
| validate_ip() { | |
| local ip="$1" | |
| [[ ! "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 1 | |
| IFS='.' read -ra ADDR <<< "$ip" | |
| for i in "${ADDR[@]}"; do | |
| [[ $i -gt 255 ]] || [[ $i -lt 0 ]] && return 1 | |
| done | |
| return 0 | |
| } | |
| test_ping() { | |
| local ip="$1" | |
| local start=$(date +%s.%N) | |
| local output=$($PING_CMD $PING_OPTS "$ip" 2>&1) | |
| local exit_code=$? | |
| local end=$(date +%s.%N) | |
| local duration=$(echo "$end - $start" | bc -l) | |
| if [[ $exit_code -eq 0 ]]; then | |
| local ping_time="" | |
| [[ "$output" =~ time=([0-9.]+) ]] && ping_time="${BASH_REMATCH[1]}ms" | |
| [[ "$output" =~ time<([0-9.]+) ]] && ping_time="<${BASH_REMATCH[1]}ms" | |
| echo "${ping_time:-${duration}s}" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| test_tcp() { | |
| local ip="$1" port="$2" | |
| local start=$(date +%s.%N) | |
| if timeout "$TIMEOUT" bash -c "</dev/tcp/$ip/$port" 2>/dev/null; then | |
| local end=$(date +%s.%N) | |
| local duration=$(echo "$end - $start" | bc -l) | |
| echo "${duration}s" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| test_syn() { | |
| local ip="$1" port="$2" | |
| local start=$(date +%s.%N) | |
| if nc -z -w "$TIMEOUT" "$ip" "$port" 2>/dev/null; then | |
| local end=$(date +%s.%N) | |
| local duration=$(echo "$end - $start" | bc -l) | |
| echo "${duration}s" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # === MAIN PROCESSING LOOP === | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| local ip=$(echo "$line" | cut -d "$DELIMITER" -f "$IP_COLUMN" | xargs) | |
| [[ -z "$ip" ]] && continue | |
| if validate_ip "$ip"; then | |
| TOTAL_IPS=$((TOTAL_IPS + 1)) | |
| printf "Testing connectivity to %s... " "$ip" | |
| if ping_time=$(test_ping "$ip"); then | |
| printf "\\e[0;32m✓ ONLINE\\e[0m - Server is responding via ICMP (%s)\\n" "$ping_time" | |
| ONLINE_COUNT=$((ONLINE_COUNT + 1)) | |
| continue | |
| fi | |
| if syn_time=$(test_syn "$ip" 80); then | |
| printf "\\e[0;36m✓ ONLINE\\e[0m - Server is responding via SYN check (%s)\\n" "$syn_time" | |
| ONLINE_COUNT=$((ONLINE_COUNT + 1)) | |
| SYN_FALLBACK_COUNT=$((SYN_FALLBACK_COUNT + 1)) | |
| continue | |
| fi | |
| if tcp_time=$(test_tcp "$ip" 80); then | |
| printf "\\e[0;33m✓ ONLINE\\e[0m - Server is responding via TCP/80 (%s)\\n" "$tcp_time" | |
| ONLINE_COUNT=$((ONLINE_COUNT + 1)) | |
| TCP_FALLBACK_COUNT=$((TCP_FALLBACK_COUNT + 1)) | |
| else | |
| printf "\\e[0;31m✗ OFFLINE\\e[0m - No response received via ICMP, SYN check or TCP/80\\n" | |
| OFFLINE_COUNT=$((OFFLINE_COUNT + 1)) | |
| fi | |
| else | |
| printf "\\e[1;33m⚠ SKIPPING\\e[0m - Invalid IP format: '%s'\\n" "$ip" | |
| SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) | |
| fi | |
| done < "$CSV_FILE" | |
| # === SUMMARY REPORTING === | |
| echo "$(printf '=%.0s' {1..60})" | |
| echo -e "\\e[0;34mNetwork connectivity test completed\\e[0m" | |
| echo "$(printf '-%.0s' {1..60})" | |
| echo "Summary of results:" | |
| echo -e " • Total IP addresses processed: $TOTAL_IPS" | |
| echo -e " • \\e[0;32mServers responding: $ONLINE_COUNT\\e[0m" | |
| echo -e " • \\e[0;31mServers not responding: $OFFLINE_COUNT\\e[0m" | |
| echo -e " • \\e[1;33mInvalid IPs skipped: $SKIPPED_COUNT\\e[0m" | |
| if [ "$TCP_FALLBACK_COUNT" -gt 0 ]; then | |
| echo -e " • \\e[0;33mServers found via TCP/80 fallback: $TCP_FALLBACK_COUNT\\e[0m" | |
| fi | |
| if [ "$SYN_FALLBACK_COUNT" -gt 0 ]; then | |
| echo -e " • \\e[0;36mServers found via SYN check fallback: $SYN_FALLBACK_COUNT\\e[0m" | |
| fi | |
| echo "$(printf '-%.0s' {1..60})" | |
| # === FINAL STATISTICS === | |
| if [ "$TOTAL_IPS" -gt 0 ]; then | |
| SUCCESS_RATE=$((ONLINE_COUNT * 100 / TOTAL_IPS)) | |
| echo -e "Success rate: \\e[0;32m${SUCCESS_RATE}%\\e[0m of valid servers are online" | |
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bun | |
| /** | |
| * Network Connectivity Checker | |
| * Tests connectivity to servers listed in a CSV file using multiple methods: | |
| * 1. ICMP ping | |
| * 2. SYN connection check | |
| * 3. TCP port check | |
| */ | |
| import { parse } from "csv-parse/sync"; | |
| import { spawn } from "child_process"; | |
| import { connect } from "net"; | |
| import { readFile, stat } from "fs/promises"; | |
| import { promisify } from "util"; | |
| /** | |
| * Defines a test result with connection method and response time | |
| */ | |
| interface TestResult { | |
| method: "ICMP" | "SYN" | "TCP"; | |
| time: number; | |
| } | |
| /** | |
| * Defines statistics for tracking test results | |
| */ | |
| interface Stats { | |
| total: number; | |
| online: number; | |
| offline: number; | |
| skipped: number; | |
| tcpFallback: number; | |
| synFallback: number; | |
| } | |
| /** | |
| * Command line options for the network checker | |
| */ | |
| const options = { | |
| ipColumn: 1, // Default: first column | |
| delimiter: ",", // Default: comma | |
| timeout: 2, // Default: 2 seconds | |
| }; | |
| /** | |
| * Parses command line arguments to extract options and file path | |
| * @returns Object with file path if valid arguments are provided, null otherwise | |
| */ | |
| function parseArguments(): { filePath: string } | null { | |
| const args = process.argv.slice(2); | |
| for (let i = 0; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (arg === "--help" || arg === "-h") { | |
| printUsage(); | |
| return null; | |
| } | |
| if (arg.startsWith("--ip-column=")) { | |
| options.ipColumn = parseInt(arg.split("=")[1]) || 1; | |
| } else if (arg.startsWith("--delimiter=")) { | |
| options.delimiter = arg.split("=")[1]; | |
| } else if (arg.startsWith("--timeout=")) { | |
| options.timeout = parseInt(arg.split("=")[1]) || 2; | |
| } else if (!arg.startsWith("--")) { | |
| return { filePath: arg }; | |
| } | |
| } | |
| printUsage(); | |
| return null; | |
| } | |
| /** | |
| * Prints usage information for the script | |
| */ | |
| function printUsage(): void { | |
| const scriptName = process.argv[1]; | |
| console.log(`Usage: ${scriptName} <csv_file> [options]`); | |
| console.log("Options:"); | |
| console.log(` --ip-column=N Specify which column contains IP addresses (default: ${options.ipColumn})`); | |
| console.log(` --delimiter=D Specify CSV delimiter (default: '${options.delimiter}')`); | |
| console.log(` --timeout=T Connection timeout in seconds (default: ${options.timeout})`); | |
| console.log("\nExamples:"); | |
| console.log(` ${scriptName} servers.csv`); | |
| console.log(` ${scriptName} servers.csv --ip-column=2`); | |
| console.log(` ${scriptName} servers.csv --delimiter='\\t' --ip-column=3`); | |
| } | |
| /** | |
| * Validates if a string is a valid IPv4 address | |
| * @param ip - The string to validate | |
| * @returns True if the string is a valid IPv4 address, false otherwise | |
| */ | |
| function validateIP(ip: string): boolean { | |
| const parts = ip.split("."); | |
| if (parts.length !== 4) { | |
| return false; | |
| } | |
| for (const part of parts) { | |
| if (part === "") { | |
| return false; | |
| } | |
| const num = parseInt(part, 10); | |
| if (isNaN(num) || num < 0 || num > 255) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Tests ICMP ping using system ping command | |
| * @param ip - The IP address to test | |
| * @param timeoutSeconds - Timeout in seconds for the ping test | |
| * @returns Promise that resolves with test result | |
| * @throws Error if ping fails | |
| */ | |
| async function testSystemPing(ip: string, timeoutSeconds: number): Promise<TestResult> { | |
| const startTime = Date.now(); | |
| const isWindows = process.platform === "win32"; | |
| const args = isWindows | |
| ? ["-n", "1", "-w", (timeoutSeconds * 1000).toString(), ip] | |
| : ["-c", "1", "-W", timeoutSeconds.toString(), ip]; | |
| const result = await new Promise<{ success: boolean; time: number }>((resolve) => { | |
| const ping = spawn("ping", args); | |
| let output = ""; | |
| ping.stdout.on("data", (data) => { | |
| output += data.toString(); | |
| }); | |
| ping.stderr.on("data", () => { | |
| // Ignore stderr output | |
| }); | |
| ping.on("close", (code) => { | |
| const success = code === 0; | |
| let time = 0; | |
| if (success) { | |
| const timeRegex = isWindows | |
| ? /time[=<](\d+)ms/ | |
| : /time=(\d+(\.\d+)?) ms/; | |
| const match = output.match(timeRegex); | |
| if (match) { | |
| time = parseFloat(match[1]); | |
| } else { | |
| time = Date.now() - startTime; | |
| } | |
| } | |
| resolve({ success, time }); | |
| }); | |
| }); | |
| if (!result.success) { | |
| throw new Error(`Ping to ${ip} failed`); | |
| } | |
| return { | |
| method: "ICMP", | |
| time: result.time | |
| }; | |
| } | |
| /** | |
| * Tests TCP connection to a specific port | |
| * @param ip - The IP address to test | |
| * @param port - The port number to test | |
| * @param timeoutSeconds - Timeout in seconds for the connection | |
| * @returns Promise that resolves with test result | |
| * @throws Error if connection fails or times out | |
| */ | |
| async function testTCPConnection(ip: string, port: number, timeoutSeconds: number): Promise<TestResult> { | |
| const startTime = Date.now(); | |
| return new Promise((resolve, reject) => { | |
| const socket = connect({ host: ip, port, timeout: timeoutSeconds * 1000 }); | |
| socket.on("connect", () => { | |
| const time = Date.now() - startTime; | |
| socket.destroy(); | |
| resolve({ method: "TCP", time }); | |
| }); | |
| socket.on("timeout", () => { | |
| socket.destroy(); | |
| reject(new Error(`TCP connection to ${ip}:${port} timed out`)); | |
| }); | |
| socket.on("error", (err) => { | |
| reject(new Error(`TCP connection to ${ip}:${port} failed: ${err.message}`)); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Tests SYN connection (simplified implementation) | |
| * This implementation uses TCP connection with immediate close | |
| * since raw sockets require elevated privileges | |
| * @param ip - The IP address to test | |
| * @param port - The port number to test | |
| * @param timeoutSeconds - Timeout in seconds for the connection | |
| * @returns Promise that resolves with test result | |
| * @throws Error if connection fails or times out | |
| */ | |
| async function testSYNConnection(ip: string, port: number, timeoutSeconds: number): Promise<TestResult> { | |
| const startTime = Date.now(); | |
| return new Promise((resolve, reject) => { | |
| const socket = connect({ host: ip, port, timeout: timeoutSeconds * 1000 }); | |
| socket.on("connect", () => { | |
| const time = Date.now() - startTime; | |
| socket.destroy(); | |
| resolve({ method: "SYN", time }); | |
| }); | |
| socket.on("timeout", () => { | |
| socket.destroy(); | |
| reject(new Error(`SYN connection to ${ip}:${port} timed out`)); | |
| }); | |
| socket.on("error", (err) => { | |
| reject(new Error(`SYN connection to ${ip}:${port} failed: ${err.message}`)); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Main function that orchestrates the network connectivity tests | |
| * Parses command line arguments, reads CSV file, and tests each IP | |
| */ | |
| async function main(): Promise<void> { | |
| const args = parseArguments(); | |
| if (!args) { | |
| process.exit(1); | |
| } | |
| const { filePath } = args; | |
| // Check if file exists | |
| try { | |
| await stat(filePath); | |
| } catch (error) { | |
| console.error(`Error: File '${filePath}' not found`); | |
| process.exit(1); | |
| } | |
| console.log(`Starting network connectivity test for servers listed in '${filePath}'`); | |
| console.log(`Using IP column: ${options.ipColumn} with delimiter: '${options.delimiter}'`); | |
| console.log("=".repeat(60)); | |
| // Initialize counters | |
| const stats: Stats = { | |
| total: 0, | |
| online: 0, | |
| offline: 0, | |
| skipped: 0, | |
| tcpFallback: 0, | |
| synFallback: 0 | |
| }; | |
| // Read and parse CSV file | |
| try { | |
| const fileContent = await readFile(filePath, "utf8"); | |
| const records = parse(fileContent, { | |
| delimiter: options.delimiter, | |
| relax_column_count: true | |
| }); | |
| for (const record of records) { | |
| // Skip empty records | |
| if (!record || record.length === 0) { | |
| continue; | |
| } | |
| // Check if we have enough columns | |
| if (record.length < options.ipColumn) { | |
| continue; | |
| } | |
| // Extract IP from the specified column | |
| const ip = record[options.ipColumn - 1].trim(); | |
| if (ip === "") { | |
| continue; | |
| } | |
| // Validate IP address format | |
| if (validateIP(ip)) { | |
| stats.total++; | |
| // Test connectivity | |
| process.stdout.write(`Testing connectivity to ${ip}... `); | |
| try { | |
| // Stage 1: Test ICMP ping | |
| try { | |
| const pingResult = await testSystemPing(ip, options.timeout); | |
| console.log(`\x1b[0;32m✓ ONLINE\x1b[0m - Server is responding via ICMP (${pingResult.time}ms)`); | |
| stats.online++; | |
| continue; | |
| } catch (pingError) { | |
| // Continue to next test method | |
| } | |
| // Stage 2: Test SYN connection | |
| try { | |
| const synResult = await testSYNConnection(ip, 80, options.timeout); | |
| console.log(`\x1b[0;36m✓ ONLINE\x1b[0m - Server is responding via SYN check (${synResult.time}ms)`); | |
| stats.online++; | |
| stats.synFallback++; | |
| continue; | |
| } catch (synError) { | |
| // Continue to next test method | |
| } | |
| // Stage 3: Test TCP/80 connection | |
| try { | |
| const tcpResult = await testTCPConnection(ip, 80, options.timeout); | |
| console.log(`\x1b[0;33m✓ ONLINE\x1b[0m - Server is responding via TCP/80 (${tcpResult.time}ms)`); | |
| stats.online++; | |
| stats.tcpFallback++; | |
| } catch (tcpError) { | |
| console.log(`\x1b[0;31m✗ OFFLINE\x1b[0m - No response received via ICMP, SYN check or TCP/80`); | |
| stats.offline++; | |
| } | |
| } catch (error) { | |
| console.log(`\x1b[0;31m✗ OFFLINE\x1b[0m - Error testing connectivity: ${(error as Error).message}`); | |
| stats.offline++; | |
| } | |
| } else { | |
| console.log(`\x1b[1;33m⚠ SKIPPING\x1b[0m - Invalid IP format: '${ip}'`); | |
| stats.skipped++; | |
| } | |
| } | |
| } catch (error) { | |
| console.error(`Error: Cannot process file '${filePath}': ${(error as Error).message}`); | |
| process.exit(1); | |
| } | |
| // Print summary | |
| console.log("=".repeat(60)); | |
| console.log("\x1b[0;34mNetwork connectivity test completed\x1b[0m"); | |
| console.log("-".repeat(60)); | |
| console.log("Summary of results:"); | |
| console.log(` • Total IP addresses processed: ${stats.total}`); | |
| console.log(` • \x1b[0;32mServers responding: ${stats.online}\x1b[0m`); | |
| console.log(` • \x1b[0;31mServers not responding: ${stats.offline}\x1b[0m`); | |
| console.log(` • \x1b[1;33mInvalid IPs skipped: ${stats.skipped}\x1b[0m`); | |
| if (stats.tcpFallback > 0) { | |
| console.log(` • \x1b[0;33mServers found via TCP/80 fallback: ${stats.tcpFallback}\x1b[0m`); | |
| } | |
| if (stats.synFallback > 0) { | |
| console.log(` • \x1b[0;36mServers found via SYN check fallback: ${stats.synFallback}\x1b[0m`); | |
| } | |
| console.log("-".repeat(60)); | |
| // Calculate success rate if any valid IPs were found | |
| if (stats.total > 0) { | |
| const successRate = Math.floor((stats.online * 100) / stats.total); | |
| console.log(`Success rate: \x1b[0;32m${successRate}%\x1b[0m of valid servers are online`); | |
| } | |
| } | |
| // Run the main function | |
| main().catch(error => { | |
| console.error("An unexpected error occurred:", error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment