Skip to content

Instantly share code, notes, and snippets.

@bouroo
Last active December 19, 2025 02:59
Show Gist options
  • Select an option

  • Save bouroo/d263e0f5b7a855a2a58c0c56e12ce498 to your computer and use it in GitHub Desktop.

Select an option

Save bouroo/d263e0f5b7a855a2a58c0c56e12ce498 to your computer and use it in GitHub Desktop.
bulk ping test
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
}
//! 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)),
}
}
#!/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
#!/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