#!/bin/bash ################################################################################ # Script Name: network-info-exporter.sh # Description: Prometheus exporter for Linux network metrics # # Collects interface statistics, connection states, routing info, firewall # rules, DNS configuration, protocol statistics, and latency measurements. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # Version: 3.0 # # Usage: # # Output to stdout # ./network-info-exporter.sh # # # Textfile collector mode (atomic write) # ./network-info-exporter.sh --textfile # # # Custom output file # ./network-info-exporter.sh -o /path/to/metrics.prom # ################################################################################ # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HOSTNAME=$(hostname) PING_TARGETS="${PING_TARGETS:-8.8.8.8 1.1.1.1 google.com}" PING_COUNT="${PING_COUNT:-5}" PING_TIMEOUT="${PING_TIMEOUT:-2}" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } # ============================================================================ # METRIC GENERATION # ============================================================================ generate_metrics() { local START_TIME START_TIME=$(date +%s.%N) # --- System info --- echo "# HELP network_exporter_info Exporter and system information" echo "# TYPE network_exporter_info gauge" local kernel arch os_name os_version kernel=$(uname -r) arch=$(uname -m) if [[ -f /etc/os-release ]]; then . /etc/os-release os_name="${NAME:-unknown}" os_version="${VERSION_ID:-unknown}" else os_name="unknown" os_version="unknown" fi echo "network_exporter_info{hostname=\"${HOSTNAME}\",kernel=\"${kernel}\",arch=\"${arch}\",os=\"${os_name}\",version=\"${os_version}\"} 1" echo "" # --- Interface counters from /proc/net/dev --- if [[ -r /proc/net/dev ]]; then awk ' NR > 2 { sub(/:/, " ") iface = $1 if (iface == "lo") next devices[count++] = iface rx_bytes[iface] = $2; rx_packets[iface] = $3 rx_errors[iface] = $4; rx_dropped[iface] = $5 tx_bytes[iface] = $10; tx_packets[iface] = $11 tx_errors[iface] = $12; tx_dropped[iface] = $13 } END { metrics["receive_bytes"] = "Total bytes received"; t["receive_bytes"] = "counter" metrics["receive_packets"] = "Total packets received"; t["receive_packets"] = "counter" metrics["receive_errors"] = "Total receive errors"; t["receive_errors"] = "counter" metrics["receive_dropped"] = "Total receive drops"; t["receive_dropped"] = "counter" metrics["transmit_bytes"] = "Total bytes transmitted"; t["transmit_bytes"] = "counter" metrics["transmit_packets"] = "Total packets transmitted"; t["transmit_packets"] = "counter" metrics["transmit_errors"] = "Total transmit errors"; t["transmit_errors"] = "counter" metrics["transmit_dropped"] = "Total transmit drops"; t["transmit_dropped"] = "counter" order[0]="receive_bytes"; order[1]="receive_packets" order[2]="receive_errors"; order[3]="receive_dropped" order[4]="transmit_bytes"; order[5]="transmit_packets" order[6]="transmit_errors"; order[7]="transmit_dropped" for (m = 0; m < 8; m++) { key = order[m] name = "network_" key "_total" printf "# HELP %s %s\n", name, metrics[key] printf "# TYPE %s %s\n", name, t[key] for (i = 0; i < count; i++) { d = devices[i] if (key == "receive_bytes") v = rx_bytes[d] else if (key == "receive_packets") v = rx_packets[d] else if (key == "receive_errors") v = rx_errors[d] else if (key == "receive_dropped") v = rx_dropped[d] else if (key == "transmit_bytes") v = tx_bytes[d] else if (key == "transmit_packets") v = tx_packets[d] else if (key == "transmit_errors") v = tx_errors[d] else if (key == "transmit_dropped") v = tx_dropped[d] printf "%s{device=\"%s\",hostname=\"%s\"} %s\n", name, d, ENVIRON["HOSTNAME"], v } print "" } } ' /proc/net/dev fi # --- Interface status, MTU, queue length, speed --- echo "# HELP network_interface_up Interface operational status (1=up, 0=down)" echo "# TYPE network_interface_up gauge" echo "# HELP network_interface_mtu Interface MTU size" echo "# TYPE network_interface_mtu gauge" echo "# HELP network_interface_tx_queue_length Interface transmit queue length" echo "# TYPE network_interface_tx_queue_length gauge" echo "# HELP network_interface_speed_mbps Interface link speed in Mbps" echo "# TYPE network_interface_speed_mbps gauge" for iface_path in /sys/class/net/*; do iface=$(basename "$iface_path") [[ "$iface" == "lo" ]] && continue # status local operstate operstate=$(cat "$iface_path/operstate" 2>/dev/null) || operstate="unknown" if [[ "$operstate" == "up" ]]; then echo "network_interface_up{device=\"${iface}\",hostname=\"${HOSTNAME}\"} 1" else echo "network_interface_up{device=\"${iface}\",hostname=\"${HOSTNAME}\"} 0" fi # mtu local mtu mtu=$(cat "$iface_path/mtu" 2>/dev/null) || mtu=0 echo "network_interface_mtu{device=\"${iface}\",hostname=\"${HOSTNAME}\"} ${mtu}" # tx queue length local qlen qlen=$(cat "$iface_path/tx_queue_len" 2>/dev/null) || qlen=0 echo "network_interface_tx_queue_length{device=\"${iface}\",hostname=\"${HOSTNAME}\"} ${qlen}" # speed (only for physical interfaces with valid speed) if [[ -r "$iface_path/speed" ]]; then local speed speed=$(cat "$iface_path/speed" 2>/dev/null) || speed=-1 if [[ "$speed" =~ ^[0-9]+$ ]] && [[ "$speed" -gt 0 ]]; then echo "network_interface_speed_mbps{device=\"${iface}\",hostname=\"${HOSTNAME}\"} ${speed}" fi fi done echo "" # --- Routing --- echo "# HELP network_routes_total Route count by protocol" echo "# TYPE network_routes_total gauge" if command -v ip &>/dev/null; then local ipv4_routes ipv6_routes default_routes ipv4_routes=$(ip route show 2>/dev/null | wc -l) ipv6_routes=$(ip -6 route show 2>/dev/null | wc -l) default_routes=$(ip route show default 2>/dev/null | wc -l) echo "network_routes_total{family=\"ipv4\",hostname=\"${HOSTNAME}\"} ${ipv4_routes}" echo "network_routes_total{family=\"ipv6\",hostname=\"${HOSTNAME}\"} ${ipv6_routes}" echo "network_routes_total{type=\"default\",hostname=\"${HOSTNAME}\"} ${default_routes}" fi echo "" # --- DNS nameservers --- echo "# HELP network_dns_nameservers_configured Number of configured DNS nameservers" echo "# TYPE network_dns_nameservers_configured gauge" local ns_count=0 if [[ -r /etc/resolv.conf ]]; then ns_count=$(grep -c "^nameserver" /etc/resolv.conf 2>/dev/null) || ns_count=0 fi echo "network_dns_nameservers_configured{hostname=\"${HOSTNAME}\"} ${ns_count}" echo "" # --- Firewall rules --- echo "# HELP network_firewall_rules_total Firewall rule count" echo "# TYPE network_firewall_rules_total gauge" local ipt_rules=0 ip6t_rules=0 if command -v iptables &>/dev/null; then ipt_rules=$(iptables -S 2>/dev/null | grep -c '^-A' 2>/dev/null) || ipt_rules=0 fi if command -v ip6tables &>/dev/null; then ip6t_rules=$(ip6tables -S 2>/dev/null | grep -c '^-A' 2>/dev/null) || ip6t_rules=0 fi echo "network_firewall_rules_total{family=\"ipv4\",hostname=\"${HOSTNAME}\"} ${ipt_rules}" echo "network_firewall_rules_total{family=\"ipv6\",hostname=\"${HOSTNAME}\"} ${ip6t_rules}" echo "" # --- Network service status --- if command -v systemctl &>/dev/null; then echo "# HELP network_service_active Network service status (1=active, 0=inactive)" echo "# TYPE network_service_active gauge" for svc in NetworkManager networking systemd-networkd dhcpcd wpa_supplicant; do local status=0 systemctl is-active "$svc" &>/dev/null && status=1 echo "network_service_active{service=\"${svc}\",hostname=\"${HOSTNAME}\"} ${status}" done echo "" fi # --- Socket stats from /proc/net/sockstat --- if [[ -r /proc/net/sockstat ]]; then echo "# HELP network_sockets_inuse Sockets currently in use by protocol" echo "# TYPE network_sockets_inuse gauge" local tcp_inuse udp_inuse raw_inuse tcp_inuse=$(awk '/^TCP:/ {print $3}' /proc/net/sockstat 2>/dev/null) || tcp_inuse=0 udp_inuse=$(awk '/^UDP:/ {print $3}' /proc/net/sockstat 2>/dev/null) || udp_inuse=0 raw_inuse=$(awk '/^RAW:/ {print $3}' /proc/net/sockstat 2>/dev/null) || raw_inuse=0 echo "network_sockets_inuse{protocol=\"tcp\",hostname=\"${HOSTNAME}\"} ${tcp_inuse:-0}" echo "network_sockets_inuse{protocol=\"udp\",hostname=\"${HOSTNAME}\"} ${udp_inuse:-0}" echo "network_sockets_inuse{protocol=\"raw\",hostname=\"${HOSTNAME}\"} ${raw_inuse:-0}" echo "" fi # --- TCP retransmissions and listen overflows from /proc/net/netstat --- if [[ -r /proc/net/netstat ]]; then local tcp_retrans=0 listen_overflows=0 listen_drops=0 # Parse TcpExt header+values pair local headers values headers=$(grep "^TcpExt:" /proc/net/netstat | head -1) values=$(grep "^TcpExt:" /proc/net/netstat | tail -1) if [[ -n "$headers" && -n "$values" ]]; then tcp_retrans=$(paste <(echo "$headers" | tr ' ' '\n') <(echo "$values" | tr ' ' '\n') | awk -F'\t' '$1=="TCPRetransSegs" {print $2}') listen_overflows=$(paste <(echo "$headers" | tr ' ' '\n') <(echo "$values" | tr ' ' '\n') | awk -F'\t' '$1=="ListenOverflows" {print $2}') listen_drops=$(paste <(echo "$headers" | tr ' ' '\n') <(echo "$values" | tr ' ' '\n') | awk -F'\t' '$1=="ListenDrops" {print $2}') fi echo "# HELP network_tcp_retransmit_segments_total TCP segments retransmitted" echo "# TYPE network_tcp_retransmit_segments_total counter" echo "network_tcp_retransmit_segments_total{hostname=\"${HOSTNAME}\"} ${tcp_retrans:-0}" echo "" echo "# HELP network_tcp_listen_overflows_total TCP listen queue overflows" echo "# TYPE network_tcp_listen_overflows_total counter" echo "network_tcp_listen_overflows_total{hostname=\"${HOSTNAME}\"} ${listen_overflows:-0}" echo "" echo "# HELP network_tcp_listen_drops_total TCP listen queue drops" echo "# TYPE network_tcp_listen_drops_total counter" echo "network_tcp_listen_drops_total{hostname=\"${HOSTNAME}\"} ${listen_drops:-0}" echo "" fi # --- ARP table --- if [[ -r /proc/net/arp ]]; then echo "# HELP network_arp_entries_total ARP table entries" echo "# TYPE network_arp_entries_total gauge" local arp_count arp_count=$(($(wc -l < /proc/net/arp) - 1)) [[ $arp_count -lt 0 ]] && arp_count=0 echo "network_arp_entries_total{hostname=\"${HOSTNAME}\"} ${arp_count}" echo "" fi # --- UDP sockets --- if [[ -r /proc/net/udp ]]; then echo "# HELP network_udp_sockets_total UDP sockets" echo "# TYPE network_udp_sockets_total gauge" local udp_sockets udp_sockets=$(($(wc -l < /proc/net/udp) - 1)) [[ $udp_sockets -lt 0 ]] && udp_sockets=0 echo "network_udp_sockets_total{hostname=\"${HOSTNAME}\"} ${udp_sockets}" echo "" fi # --- ICMP stats from /proc/net/snmp --- if [[ -r /proc/net/snmp ]]; then local icmp_header icmp_values icmp_header=$(grep "^Icmp:" /proc/net/snmp | head -1) icmp_values=$(grep "^Icmp:" /proc/net/snmp | tail -1) if [[ -n "$icmp_header" && -n "$icmp_values" ]]; then local icmp_in icmp_out icmp_err_in icmp_err_out icmp_in=$(paste <(echo "$icmp_header" | tr ' ' '\n') <(echo "$icmp_values" | tr ' ' '\n') | awk -F'\t' '$1=="InMsgs" {print $2}') icmp_out=$(paste <(echo "$icmp_header" | tr ' ' '\n') <(echo "$icmp_values" | tr ' ' '\n') | awk -F'\t' '$1=="OutMsgs" {print $2}') icmp_err_in=$(paste <(echo "$icmp_header" | tr ' ' '\n') <(echo "$icmp_values" | tr ' ' '\n') | awk -F'\t' '$1=="InErrors" {print $2}') icmp_err_out=$(paste <(echo "$icmp_header" | tr ' ' '\n') <(echo "$icmp_values" | tr ' ' '\n') | awk -F'\t' '$1=="OutErrors" {print $2}') echo "# HELP network_icmp_messages_total ICMP messages by direction" echo "# TYPE network_icmp_messages_total counter" echo "network_icmp_messages_total{direction=\"in\",hostname=\"${HOSTNAME}\"} ${icmp_in:-0}" echo "network_icmp_messages_total{direction=\"out\",hostname=\"${HOSTNAME}\"} ${icmp_out:-0}" echo "" echo "# HELP network_icmp_errors_total ICMP errors by direction" echo "# TYPE network_icmp_errors_total counter" echo "network_icmp_errors_total{direction=\"in\",hostname=\"${HOSTNAME}\"} ${icmp_err_in:-0}" echo "network_icmp_errors_total{direction=\"out\",hostname=\"${HOSTNAME}\"} ${icmp_err_out:-0}" echo "" fi fi # --- TCP connections by state --- if [[ -r /proc/net/tcp ]]; then echo "# HELP network_tcp_connections TCP connections by state" echo "# TYPE network_tcp_connections gauge" # hex state codes: 01=ESTABLISHED, 06=TIME_WAIT, 0A=LISTEN local tcp_estab tcp_listen tcp_tw tcp_close_wait tcp_syn_recv tcp_estab=$(awk '$4=="01" {c++} END {print c+0}' /proc/net/tcp 2>/dev/null) tcp_listen=$(awk '$4=="0A" {c++} END {print c+0}' /proc/net/tcp 2>/dev/null) tcp_tw=$(awk '$4=="06" {c++} END {print c+0}' /proc/net/tcp 2>/dev/null) tcp_close_wait=$(awk '$4=="08" {c++} END {print c+0}' /proc/net/tcp 2>/dev/null) tcp_syn_recv=$(awk '$4=="03" {c++} END {print c+0}' /proc/net/tcp 2>/dev/null) echo "network_tcp_connections{state=\"established\",hostname=\"${HOSTNAME}\"} ${tcp_estab}" echo "network_tcp_connections{state=\"listen\",hostname=\"${HOSTNAME}\"} ${tcp_listen}" echo "network_tcp_connections{state=\"time_wait\",hostname=\"${HOSTNAME}\"} ${tcp_tw}" echo "network_tcp_connections{state=\"close_wait\",hostname=\"${HOSTNAME}\"} ${tcp_close_wait}" echo "network_tcp_connections{state=\"syn_recv\",hostname=\"${HOSTNAME}\"} ${tcp_syn_recv}" echo "" fi # --- Conntrack --- if [[ -r /proc/sys/net/netfilter/nf_conntrack_count ]]; then echo "# HELP network_nf_conntrack_entries Conntrack table entries" echo "# TYPE network_nf_conntrack_entries gauge" echo "network_nf_conntrack_entries{hostname=\"${HOSTNAME}\"} $(cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null || echo 0)" fi if [[ -r /proc/sys/net/netfilter/nf_conntrack_max ]]; then echo "# HELP network_nf_conntrack_entries_limit Conntrack table maximum" echo "# TYPE network_nf_conntrack_entries_limit gauge" echo "network_nf_conntrack_entries_limit{hostname=\"${HOSTNAME}\"} $(cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null || echo 0)" echo "" fi # --- Latency (parallel ping) --- if command -v ping &>/dev/null; then read -ra targets <<< "$PING_TARGETS" local tmp_files=() pids=() labels=() for target in "${targets[@]}"; do [[ -z "$target" ]] && continue local label label=$(echo "$target" | tr -c 'a-zA-Z0-9._-' '_' | sed 's/_*$//') labels+=("$label") local tmp tmp=$(mktemp) tmp_files+=("$tmp") ( set +e timeout $((PING_COUNT * PING_TIMEOUT + 5)) ping -c "$PING_COUNT" -W "$PING_TIMEOUT" "$target" > "$tmp" 2>&1 echo "EXIT=$?" >> "$tmp" ) & pids+=($!) done [[ ${#pids[@]} -gt 0 ]] && wait "${pids[@]}" # Collect parsed results local all_min=() all_avg=() all_max=() all_stddev=() all_loss=() for i in "${!tmp_files[@]}"; do local tmp="${tmp_files[$i]}" local exit_code min_v=0 avg_v=0 max_v=0 stddev_v=0 loss_v=100 if [[ -f "$tmp" ]]; then exit_code=$(grep "^EXIT=" "$tmp" | tail -1 | cut -d= -f2) if [[ "${exit_code:-1}" -eq 0 ]]; then local stats stats=$(grep -E 'rtt min/avg/max/(mdev|stddev)' "$tmp" | head -1) if [[ -n "$stats" ]]; then local vals vals=$(echo "$stats" | cut -d= -f2 | awk '{print $1}') min_v=$(echo "$vals" | cut -d/ -f1) avg_v=$(echo "$vals" | cut -d/ -f2) max_v=$(echo "$vals" | cut -d/ -f3) stddev_v=$(echo "$vals" | cut -d/ -f4) fi loss_v=$(grep -oP '\d+(?=% packet loss)' "$tmp" | head -1) fi rm -f "$tmp" fi all_min+=("${min_v:-0}") all_avg+=("${avg_v:-0}") all_max+=("${max_v:-0}") all_stddev+=("${stddev_v:-0}") all_loss+=("${loss_v:-100}") done echo "# HELP network_ping_rtt_min_milliseconds Minimum ping RTT" echo "# TYPE network_ping_rtt_min_milliseconds gauge" for i in "${!labels[@]}"; do echo "network_ping_rtt_min_milliseconds{target=\"${labels[$i]}\",hostname=\"${HOSTNAME}\"} ${all_min[$i]}" done echo "" echo "# HELP network_ping_rtt_avg_milliseconds Average ping RTT" echo "# TYPE network_ping_rtt_avg_milliseconds gauge" for i in "${!labels[@]}"; do echo "network_ping_rtt_avg_milliseconds{target=\"${labels[$i]}\",hostname=\"${HOSTNAME}\"} ${all_avg[$i]}" done echo "" echo "# HELP network_ping_rtt_max_milliseconds Maximum ping RTT" echo "# TYPE network_ping_rtt_max_milliseconds gauge" for i in "${!labels[@]}"; do echo "network_ping_rtt_max_milliseconds{target=\"${labels[$i]}\",hostname=\"${HOSTNAME}\"} ${all_max[$i]}" done echo "" echo "# HELP network_ping_rtt_stddev_milliseconds Ping RTT standard deviation (jitter)" echo "# TYPE network_ping_rtt_stddev_milliseconds gauge" for i in "${!labels[@]}"; do echo "network_ping_rtt_stddev_milliseconds{target=\"${labels[$i]}\",hostname=\"${HOSTNAME}\"} ${all_stddev[$i]}" done echo "" echo "# HELP network_ping_packet_loss_percent Packet loss percentage" echo "# TYPE network_ping_packet_loss_percent gauge" for i in "${!labels[@]}"; do echo "network_ping_packet_loss_percent{target=\"${labels[$i]}\",hostname=\"${HOSTNAME}\"} ${all_loss[$i]}" done echo "" fi # --- Exporter timing --- local END_TIME DURATION END_TIME=$(date +%s.%N) DURATION=$(echo "$END_TIME - $START_TIME" | bc) echo "# HELP network_exporter_duration_seconds Time to generate all metrics" echo "# TYPE network_exporter_duration_seconds gauge" echo "network_exporter_duration_seconds{hostname=\"${HOSTNAME}\"} ${DURATION}" echo "" echo "# HELP network_exporter_last_run_timestamp Unix timestamp of last successful run" echo "# TYPE network_exporter_last_run_timestamp gauge" echo "network_exporter_last_run_timestamp{hostname=\"${HOSTNAME}\"} $(date +%s)" } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { parse_args "$@" if [[ $EUID -ne 0 ]]; then echo "Error: This script must be run as root" >&2 exit 1 fi if [ -n "$OUTPUT_FILE" ]; then local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" local temp_file temp_file=$(mktemp "${output_dir}/.network_info.XXXXXX") if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 exit 1 fi local file_lines file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0) if [ "$file_lines" -lt 10 ]; then rm -f "$temp_file" echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2 exit 1 fi chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else generate_metrics fi } main "$@"