#!/usr/bin/env bash ######################################################################################### #### network-diag.sh — One-shot network diagnostics dump for Linux servers #### #### Shows interfaces, routes, DNS, firewall rules, listening ports, connections #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### ./network-diag.sh #### #### ./network-diag.sh --section interfaces,dns #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── SECTIONS="${SECTIONS:-all}" VERBOSE="${VERBOSE:-false}" COLOR="${COLOR:-auto}" # ── State ───────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME # ── Colors ──────────────────────────────────────────────────────────── setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" return fi if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" fi } # ── Logging ─────────────────────────────────────────────────────────── log() { echo -e "${DIM}[INFO]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; } # ── Helpers ─────────────────────────────────────────────────────────── section_header() { echo "" echo -e " ${BOLD}${CYAN}── $1 ──${RESET}" echo "" } field() { printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2" } field_color() { printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2" } should_show() { [[ "$SECTIONS" == "all" ]] || [[ ",$SECTIONS," == *",$1,"* ]] } # ══════════════════════════════════════════════════════════════════════ # INTERFACES # ══════════════════════════════════════════════════════════════════════ show_interfaces() { section_header "Network Interfaces" printf " ${BOLD}%-16s %-18s %-20s %-8s %s${RESET}\n" "INTERFACE" "IP ADDRESS" "MAC" "STATE" "MTU" printf " %s\n" "$(printf '%.0s─' {1..72})" if command -v ip &>/dev/null; then local ifaces ifaces=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | sed 's/@.*//') while IFS= read -r iface; do [[ -z "$iface" ]] && continue local ip_addr mac state mtu ip_addr=$(ip -4 -o addr show "$iface" 2>/dev/null | awk '{print $4}' | head -1) ip_addr="${ip_addr:-—}" mac=$(ip link show "$iface" 2>/dev/null | awk '/link\/ether/ {print $2}') mac="${mac:-—}" state=$(ip link show "$iface" 2>/dev/null | grep -oP 'state \K\S+' || echo "UNKNOWN") mtu=$(ip link show "$iface" 2>/dev/null | grep -oP 'mtu \K\d+' || echo "—") local state_color="$DIM" if [[ "$state" == "UP" ]]; then state_color="$GREEN" elif [[ "$state" == "DOWN" ]]; then state_color="$RED" fi printf " %-16s %-18s %-20s %b%-8s%b %s\n" \ "$iface" "$ip_addr" "$mac" "$state_color" "$state" "$RESET" "$mtu" done <<< "$ifaces" else warn "ip command not available" ifconfig 2>/dev/null || echo " No interface information available" fi # IPv6 addresses if command -v ip &>/dev/null; then local ipv6_count ipv6_count=$(ip -6 -o addr show scope global 2>/dev/null | wc -l) if [[ "$ipv6_count" -gt 0 ]]; then echo "" echo -e " ${BOLD}IPv6 Global Addresses:${RESET}" ip -6 -o addr show scope global 2>/dev/null | while IFS= read -r line; do local v6_iface v6_addr v6_iface=$(echo "$line" | awk '{print $2}') v6_addr=$(echo "$line" | awk '{print $4}') printf " %-16s %s\n" "$v6_iface" "$v6_addr" done fi fi } # ══════════════════════════════════════════════════════════════════════ # ROUTES # ══════════════════════════════════════════════════════════════════════ show_routes() { section_header "Routing" if command -v ip &>/dev/null; then # Default route local default_route default_route=$(ip route show default 2>/dev/null | head -1) if [[ -n "$default_route" ]]; then field "Default route:" "$default_route" else field_color "Default route:" "${YELLOW}None${RESET}" fi echo "" echo -e " ${BOLD}Routing Table:${RESET}" printf " %-24s %-18s %-12s %s\n" "DESTINATION" "GATEWAY" "DEVICE" "PROTO" printf " %s\n" "$(printf '%.0s─' {1..65})" ip route show 2>/dev/null | while IFS= read -r line; do local dest gw dev proto dest=$(echo "$line" | awk '{print $1}') gw=$(echo "$line" | grep -oP 'via \K\S+' || echo "—") dev=$(echo "$line" | grep -oP 'dev \K\S+' || echo "—") proto=$(echo "$line" | grep -oP 'proto \K\S+' || echo "—") printf " %-24s %-18s %-12s %s\n" "$dest" "$gw" "$dev" "$proto" done else route -n 2>/dev/null || echo " No routing information available" fi } # ══════════════════════════════════════════════════════════════════════ # DNS # ══════════════════════════════════════════════════════════════════════ show_dns() { section_header "DNS Configuration" # /etc/resolv.conf if [[ -f /etc/resolv.conf ]]; then echo -e " ${BOLD}/etc/resolv.conf:${RESET}" local nameservers search_domains nameservers=$(grep "^nameserver" /etc/resolv.conf 2>/dev/null | awk '{print $2}') search_domains=$(grep "^search" /etc/resolv.conf 2>/dev/null | sed 's/^search //') if [[ -n "$nameservers" ]]; then while IFS= read -r ns; do printf " Nameserver: %s\n" "$ns" done <<< "$nameservers" fi if [[ -n "${search_domains:-}" ]]; then printf " Search: %s\n" "$search_domains" fi # Check if resolv.conf is a symlink (systemd-resolved) if [[ -L /etc/resolv.conf ]]; then local link_target link_target=$(readlink -f /etc/resolv.conf) printf " ${DIM}(symlink → %s)${RESET}\n" "$link_target" fi fi # systemd-resolved if command -v resolvectl &>/dev/null; then echo "" echo -e " ${BOLD}systemd-resolved:${RESET}" resolvectl status 2>/dev/null | grep -E "DNS Server|DNS Domain|DNSSEC" | while IFS= read -r line; do printf " %s\n" "$line" done elif command -v systemd-resolve &>/dev/null; then echo "" echo -e " ${BOLD}systemd-resolved:${RESET}" systemd-resolve --status 2>/dev/null | grep -E "DNS Server|DNS Domain|DNSSEC" | while IFS= read -r line; do printf " %s\n" "$line" done fi } # ══════════════════════════════════════════════════════════════════════ # FIREWALL # ══════════════════════════════════════════════════════════════════════ show_firewall() { section_header "Firewall" local fw_found=false # iptables if command -v iptables &>/dev/null; then fw_found=true echo -e " ${BOLD}iptables:${RESET}" local ipt_rules ipt_rules=$(iptables -S 2>/dev/null | wc -l || echo "0") printf " Rules: %d\n" "$ipt_rules" if [[ "$VERBOSE" == "true" ]]; then iptables -L -n --line-numbers 2>/dev/null | while IFS= read -r line; do printf " %s\n" "$line" done else # Summary by chain iptables -S 2>/dev/null | awk '/^-A/ {print $2}' | sort | uniq -c | sort -rn | while IFS= read -r line; do printf " %s\n" "$line" done fi fi # nftables if command -v nft &>/dev/null; then fw_found=true echo "" echo -e " ${BOLD}nftables:${RESET}" local nft_tables nft_tables=$(nft list tables 2>/dev/null | wc -l || echo "0") printf " Tables: %d\n" "$nft_tables" if [[ "$VERBOSE" == "true" ]]; then nft list ruleset 2>/dev/null | while IFS= read -r line; do printf " %s\n" "$line" done else nft list tables 2>/dev/null | while IFS= read -r line; do printf " %s\n" "$line" done fi fi # ufw if command -v ufw &>/dev/null; then fw_found=true echo "" echo -e " ${BOLD}UFW:${RESET}" local ufw_status ufw_status=$(ufw status 2>/dev/null | head -1 || echo "Unknown") printf " %s\n" "$ufw_status" fi # firewalld if command -v firewall-cmd &>/dev/null; then fw_found=true echo "" echo -e " ${BOLD}firewalld:${RESET}" local fwd_state fwd_state=$(firewall-cmd --state 2>/dev/null || echo "not running") printf " State: %s\n" "$fwd_state" if [[ "$fwd_state" == "running" ]]; then local active_zones active_zones=$(firewall-cmd --get-active-zones 2>/dev/null | head -5) if [[ -n "$active_zones" ]]; then printf " Active zones:\n" echo "$active_zones" | while IFS= read -r line; do printf " %s\n" "$line" done fi fi fi if [[ "$fw_found" == "false" ]]; then log "No firewall tools detected (iptables, nftables, ufw, firewalld)" fi } # ══════════════════════════════════════════════════════════════════════ # LISTENING PORTS # ══════════════════════════════════════════════════════════════════════ show_ports() { section_header "Listening Ports" if command -v ss &>/dev/null; then printf " ${BOLD}%-8s %-30s %-8s %s${RESET}\n" "PROTO" "LISTEN ADDRESS" "PORT" "PROCESS" printf " %s\n" "$(printf '%.0s─' {1..70})" ss -tlnp 2>/dev/null | tail -n +2 | while IFS= read -r line; do local addr port_str proto proc_info proto="tcp" addr=$(echo "$line" | awk '{print $4}') port_str="${addr##*:}" proc_info=$(echo "$line" | grep -oP 'users:\(\("\K[^"]+' || echo "—") printf " %-8s %-30s %-8s %s\n" "$proto" "$addr" "$port_str" "$proc_info" done # UDP listeners local udp_count udp_count=$(ss -ulnp 2>/dev/null | tail -n +2 | wc -l) if [[ "$udp_count" -gt 0 ]]; then echo "" ss -ulnp 2>/dev/null | tail -n +2 | while IFS= read -r line; do local addr port_str proc_info addr=$(echo "$line" | awk '{print $4}') port_str="${addr##*:}" proc_info=$(echo "$line" | grep -oP 'users:\(\("\K[^"]+' || echo "—") printf " %-8s %-30s %-8s %s\n" "udp" "$addr" "$port_str" "$proc_info" done fi local total_tcp total_udp total_tcp=$(ss -tlnp 2>/dev/null | tail -n +2 | wc -l) total_udp=$(ss -ulnp 2>/dev/null | tail -n +2 | wc -l) echo "" field "TCP listeners:" "$total_tcp" field "UDP listeners:" "$total_udp" elif command -v netstat &>/dev/null; then netstat -tlnp 2>/dev/null | while IFS= read -r line; do printf " %s\n" "$line" done else warn "Neither ss nor netstat available" fi } # ══════════════════════════════════════════════════════════════════════ # CONNECTIONS # ══════════════════════════════════════════════════════════════════════ show_connections() { section_header "Active Connections" if command -v ss &>/dev/null; then echo -e " ${BOLD}Connections by state:${RESET}" printf " %-20s %s\n" "STATE" "COUNT" printf " %s\n" "$(printf '%.0s─' {1..30})" ss -tan 2>/dev/null | tail -n +2 | awk '{print $1}' | sort | uniq -c | sort -rn | while IFS= read -r line; do local count state count=$(echo "$line" | awk '{print $1}') state=$(echo "$line" | awk '{print $2}') local color="" case "$state" in ESTAB) color="$GREEN" ;; TIME-WAIT) color="$YELLOW" ;; CLOSE-WAIT) color="$RED" ;; *) color="" ;; esac printf " %b%-20s%b %s\n" "$color" "$state" "$RESET" "$count" done local total_conn total_conn=$(ss -tan 2>/dev/null | tail -n +2 | wc -l) echo "" field "Total connections:" "$total_conn" elif command -v netstat &>/dev/null; then netstat -tan 2>/dev/null | awk '/^tcp/ {print $6}' | sort | uniq -c | sort -rn | while IFS= read -r line; do printf " %s\n" "$line" done else warn "Neither ss nor netstat available" fi } # ══════════════════════════════════════════════════════════════════════ # USAGE # ══════════════════════════════════════════════════════════════════════ usage() { cat <&2 echo "Run ${SCRIPT_NAME} --help for usage" >&2 exit 1 ;; esac done } # ══════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════ main() { parse_args "$@" setup_colors echo "" echo -e "${BOLD}Network Diagnostics — $(hostname -f 2>/dev/null || hostname)${RESET}" echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}" should_show "interfaces" && show_interfaces should_show "routes" && show_routes should_show "dns" && show_dns should_show "firewall" && show_firewall should_show "ports" && show_ports should_show "connections" && show_connections echo "" } main "$@"