feat(system-info): add default network interface IP detection (#20764)
Ilya Mashchenko committed
Aug 5, 2025 at 22:02 UTC
baa1e71d3bcaa5eacf3801a16aebb557cd39e3fe
1 file changed
+201
-27
src/daemon/system-info.sh
+201
-27
@@ -15,8 +15,8 @@ ARCHITECTURE="$(uname -m)"
15
virtualization_normalize_name() {
16
vname="$1"
17
case "$vname" in
18
- "User-mode Linux") vname="uml" ;;
19
- "Windows Subsystem for Linux") vname="wsl" ;;
18
+ "User-mode Linux") vname="uml" ;;
19
+ "Windows Subsystem for Linux") vname="wsl" ;;
20
esac
21
22
echo "$vname" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g'
@@ -69,7 +69,7 @@ fi
69
70
if [ "${CONTAINER}" = "unknown" ]; then
71
if [ -f /proc/1/sched ]; then
72
- IFS='(, ' read -r process _ < /proc/1/sched
72
+ IFS='(, ' read -r process _ </proc/1/sched
73
if [ "${process}" = "netdata" ]; then
74
CONTAINER="container"
75
CONT_DETECTION="process"
@@ -77,7 +77,7 @@ if [ "${CONTAINER}" = "unknown" ]; then
77
fi
78
# ubuntu and debian supply /bin/running-in-container
79
# https://www.apt-browse.org/browse/ubuntu/trusty/main/i386/upstart/1.12.1-0ubuntu4/file/bin/running-in-container
80
- if /bin/running-in-container > /dev/null 2>&1; then
80
+ if /bin/running-in-container >/dev/null 2>&1; then
81
CONTAINER="container"
82
CONT_DETECTION="/bin/running-in-container"
83
fi
@@ -134,7 +134,7 @@ elif [ "${KERNEL_NAME}" = "FreeBSD" ]; then
134
KERNEL_VERSION=$(uname -K)
135
else
136
if [ -f "/etc/os-release" ]; then
137
- eval "$(grep -E "^(NAME|ID|ID_LIKE|VERSION|VERSION_ID)=" < /etc/os-release | sed 's/^/CONTAINER_/')"
137
+ eval "$(grep -E "^(NAME|ID|ID_LIKE|VERSION|VERSION_ID)=" </etc/os-release | sed 's/^/CONTAINER_/')"
138
CONTAINER_OS_DETECTION="/etc/os-release"
139
fi
140
@@ -149,20 +149,20 @@ else
149
DISTRIB_ID="unknown"
150
DISTRIB_RELEASE="unknown"
151
DISTRIB_CODENAME="unknown"
152
- eval "$(grep -E "^(DISTRIB_ID|DISTRIB_RELEASE|DISTRIB_CODENAME)=" < /etc/lsb-release)"
152
+ eval "$(grep -E "^(DISTRIB_ID|DISTRIB_RELEASE|DISTRIB_CODENAME)=" </etc/lsb-release)"
153
if [ "${CONTAINER_NAME}" = "unknown" ]; then CONTAINER_NAME="${DISTRIB_ID}"; fi
154
if [ "${CONTAINER_VERSION}" = "unknown" ]; then CONTAINER_VERSION="${DISTRIB_RELEASE}"; fi
155
if [ "${CONTAINER_ID}" = "unknown" ]; then CONTAINER_ID="${DISTRIB_CODENAME}"; fi
156
fi
157
- if [ -n "$(command -v lsb_release 2> /dev/null)" ]; then
157
+ if [ -n "$(command -v lsb_release 2>/dev/null)" ]; then
158
if [ "${CONTAINER_OS_DETECTION}" = "unknown" ]; then
159
CONTAINER_OS_DETECTION="lsb_release"
160
else
161
CONTAINER_OS_DETECTION="Mixed"
162
fi
163
- if [ "${CONTAINER_NAME}" = "unknown" ]; then CONTAINER_NAME="$(lsb_release -is 2> /dev/null)"; fi
164
- if [ "${CONTAINER_VERSION}" = "unknown" ]; then CONTAINER_VERSION="$(lsb_release -rs 2> /dev/null)"; fi
165
- if [ "${CONTAINER_ID}" = "unknown" ]; then CONTAINER_ID="$(lsb_release -cs 2> /dev/null)"; fi
163
+ if [ "${CONTAINER_NAME}" = "unknown" ]; then CONTAINER_NAME="$(lsb_release -is 2>/dev/null)"; fi
164
+ if [ "${CONTAINER_VERSION}" = "unknown" ]; then CONTAINER_VERSION="$(lsb_release -rs 2>/dev/null)"; fi
165
+ if [ "${CONTAINER_ID}" = "unknown" ]; then CONTAINER_ID="$(lsb_release -cs 2>/dev/null)"; fi
166
fi
167
fi
168
fi
@@ -183,7 +183,7 @@ if [ "${CONTAINER}" = "unknown" ] || [ "${CONTAINER}" = "none" ]; then
183
else
184
# Otherwise try and use a user-supplied bind-mount into the container to resolve the host details
185
if [ -e "/host/etc/os-release" ]; then
186
- eval "$(grep -E "^(NAME|ID|ID_LIKE|VERSION|VERSION_ID)=" < /host/etc/os-release | sed 's/^/HOST_/')"
186
+ eval "$(grep -E "^(NAME|ID|ID_LIKE|VERSION|VERSION_ID)=" </host/etc/os-release | sed 's/^/HOST_/')"
187
HOST_OS_DETECTION="/host/etc/os-release"
188
fi
189
if [ "${HOST_NAME}" = "unknown" ] || [ "${HOST_VERSION}" = "unknown" ] || [ "${HOST_ID}" = "unknown" ]; then
@@ -196,7 +196,7 @@ else
196
DISTRIB_ID="unknown"
197
DISTRIB_RELEASE="unknown"
198
DISTRIB_CODENAME="unknown"
199
- eval "$(grep -E "^(DISTRIB_ID|DISTRIB_RELEASE|DISTRIB_CODENAME)=" < /etc/lsb-release)"
199
+ eval "$(grep -E "^(DISTRIB_ID|DISTRIB_RELEASE|DISTRIB_CODENAME)=" </etc/lsb-release)"
200
if [ "${HOST_NAME}" = "unknown" ]; then HOST_NAME="${DISTRIB_ID}"; fi
201
if [ "${HOST_VERSION}" = "unknown" ]; then HOST_VERSION="${DISTRIB_RELEASE}"; fi
202
if [ "${HOST_ID}" = "unknown" ]; then HOST_ID="${DISTRIB_CODENAME}"; fi
@@ -204,9 +204,9 @@ else
204
fi
205
fi
206
207
-if [ -d "/etc/pve" ] && \
208
- echo "${KERNEL_VERSION}" | grep -q -- '-pve$' && \
209
- command -v pveversion > /dev/null 2>&1; then
207
+if [ -d "/etc/pve" ] &&
208
+ echo "${KERNEL_VERSION}" | grep -q -- '-pve$' &&
209
+ command -v pveversion >/dev/null 2>&1; then
210
HOST_NAME="Proxmox VE"
211
HOST_ID="proxmox"
212
HOST_ID_LIKE="proxmox"
@@ -231,8 +231,8 @@ lscpu_output=""
231
dmidecode="$(command -v dmidecode)"
232
dmidecode_output=""
233
234
-if [ -n "${lscpu}" ] && lscpu > /dev/null 2>&1; then
235
- lscpu_output="$(LC_NUMERIC=C ${lscpu} 2> /dev/null)"
234
+if [ -n "${lscpu}" ] && lscpu >/dev/null 2>&1; then
235
+ lscpu_output="$(LC_NUMERIC=C ${lscpu} 2>/dev/null)"
236
CPU_INFO_SOURCE="lscpu"
237
LCPU_COUNT="$(echo "${lscpu_output}" | grep "^CPU(s):" | cut -f 2 -d ':' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
238
CPU_VENDOR="$(echo "${lscpu_output}" | grep "^Vendor ID:" | cut -f 2 -d ':' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
@@ -248,8 +248,8 @@ if [ -n "${lscpu}" ] && lscpu > /dev/null 2>&1; then
248
possible_cpu_freq="$(echo "${lscpu_output}" | grep "^Model name:" | grep -Eo "[0-9\.]+GHz" | grep -o "^[0-9\.]*" | awk '{print int($0*1000)}')"
249
fi
250
[ -n "$possible_cpu_freq" ] && possible_cpu_freq="${possible_cpu_freq} MHz"
251
-elif [ -n "${dmidecode}" ] && dmidecode -t processor > /dev/null 2>&1; then
252
- dmidecode_output="$(${dmidecode} -t processor 2> /dev/null)"
251
+elif [ -n "${dmidecode}" ] && dmidecode -t processor >/dev/null 2>&1; then
252
+ dmidecode_output="$(${dmidecode} -t processor 2>/dev/null)"
253
CPU_INFO_SOURCE="dmidecode"
254
LCPU_COUNT="$(echo "${dmidecode_output}" | grep -F "Thread Count:" | cut -f 2 -d ':' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
255
CPU_VENDOR="$(echo "${dmidecode_output}" | grep -F "Manufacturer:" | cut -f 2 -d ':' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
@@ -262,8 +262,8 @@ else
262
elif [ "${KERNEL_NAME}" = FreeBSD ]; then
263
CPU_INFO_SOURCE="sysctl"
264
LCPU_COUNT="$(sysctl -n kern.smp.cpus)"
265
- if ! possible_cpu_freq=$(sysctl -n machdep.tsc_freq 2> /dev/null); then
266
- possible_cpu_freq=$(sysctl -n hw.model 2> /dev/null | grep -Eo "[0-9\.]+GHz" | grep -o "^[0-9\.]*" | awk '{print int($0*1000)}')
265
+ if ! possible_cpu_freq=$(sysctl -n machdep.tsc_freq 2>/dev/null); then
266
+ possible_cpu_freq=$(sysctl -n hw.model 2>/dev/null | grep -Eo "[0-9\.]+GHz" | grep -o "^[0-9\.]*" | awk '{print int($0*1000)}')
267
[ -n "$possible_cpu_freq" ] && possible_cpu_freq="${possible_cpu_freq} MHz"
268
fi
269
elif [ "${KERNEL_NAME}" = Darwin ]; then
@@ -286,7 +286,7 @@ else
286
CPU_VENDOR="Apple"
287
fi
288
echo "${CPU_INFO_SOURCE}" | grep -qv sysctl && CPU_INFO_SOURCE="${CPU_INFO_SOURCE} sysctl"
289
- elif uname --version 2> /dev/null | grep -qF 'GNU coreutils'; then
289
+ elif uname --version 2>/dev/null | grep -qF 'GNU coreutils'; then
290
CPU_INFO_SOURCE="${CPU_INFO_SOURCE} uname"
291
CPU_MODEL="$(uname -p)"
292
CPU_VENDOR="$(uname -i)"
@@ -433,14 +433,14 @@ else
433
DISK_DETECTION="sysfs"
434
DISK_SIZE="0"
435
for disk in /sys/block/*; do
436
- if [ -r "${disk}/size" ] \
437
- && (echo "${dev_major_whitelist}" | grep -q ":$(cut -f 1 -d ':' "${disk}/dev"):") \
438
- && grep -qv 1 "${disk}/removable"; then
436
+ if [ -r "${disk}/size" ] &&
437
+ (echo "${dev_major_whitelist}" | grep -q ":$(cut -f 1 -d ':' "${disk}/dev"):") &&
438
+ grep -qv 1 "${disk}/removable"; then
439
size="$(($(cat "${disk}/size") * 512))"
440
DISK_SIZE="$((DISK_SIZE + size))"
441
fi
442
done
443
- elif df --version 2> /dev/null | grep -qF "GNU coreutils"; then
443
+ elif df --version 2>/dev/null | grep -qF "GNU coreutils"; then
444
DISK_DETECTION="df"
445
DISK_SIZE=$(($(df -x tmpfs -x devtmpfs -x squashfs -l -B1 --output=source,size | tail -n +2 | sort -u -k 1 | awk '{print $2}' | tr '\n' '+' | head -c -1)))
446
else
@@ -470,7 +470,7 @@ CLOUD_TYPE="unknown"
470
CLOUD_INSTANCE_TYPE="unknown"
471
CLOUD_INSTANCE_REGION="unknown"
472
473
-if [ "${VIRTUALIZATION}" != "none" ] && command -v curl > /dev/null 2>&1; then
473
+if [ "${VIRTUALIZATION}" != "none" ] && command -v curl >/dev/null 2>&1; then
474
# Returned HTTP status codes: GCP is 200, AWS is 200, DO is 404.
475
curl --fail -s -m 1 --noproxy "*" http://169.254.169.254 >/dev/null 2>&1
476
ret=$?
@@ -509,6 +509,177 @@ if [ "${VIRTUALIZATION}" != "none" ] && command -v curl > /dev/null 2>&1; then
509
fi
510
fi
511
512
+# -------------------------------------------------------------------------------------------------
513
+# Detect the IP address of the interface on the default route
514
+
515
+get_default_interface_ip() {
516
+ DEFAULT_INTERFACE_IP="unknown"
517
+ DEFAULT_INTERFACE_NAME="unknown"
518
+ DEFAULT_INTERFACE_DETECTION="none"
519
+
520
+ # Optional parameter for IP version: "-4" (default) or "-6"
521
+ ip_version="${1:--4}"
522
+
523
+ # Check if timeout command is available
524
+ timeout_cmd=""
525
+ if command -v timeout >/dev/null 2>&1; then
526
+ timeout_cmd="timeout 2"
527
+ fi
528
+
529
+ # Find default interface based on OS
530
+ default_if=""
531
+
532
+ case "${KERNEL_NAME}" in
533
+ Linux)
534
+ # Ultra-safe: Try /proc first (never hangs - just file reading)
535
+ if [ "${ip_version}" = "-4" ] && [ -r /proc/net/route ]; then
536
+ # Default route has destination 00000000
537
+ default_if=$(awk '$2 == "00000000" && $1 != "lo" {print $1; exit}' /proc/net/route)
538
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="procfs"
539
+ elif [ "${ip_version}" = "-6" ] && [ -r /proc/net/ipv6_route ]; then
540
+ # IPv6 default route - field 10 is the interface
541
+ # Look for ::/0 route (all zeros with prefix 00) that's not on lo
542
+ default_if=$(awk '$1 == "00000000000000000000000000000000" && $2 == "00" && $10 != "lo" {print $10; exit}' /proc/net/ipv6_route)
543
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="procfs"
544
+ fi
545
+
546
+ # Fallback to ip command if /proc didn't work
547
+ if [ -z "${default_if}" ] && command -v ip >/dev/null 2>&1; then
548
+ if [ "${ip_version}" = "-4" ]; then
549
+ # Extract interface after "dev" keyword
550
+ default_if=$(${timeout_cmd} ip -o -4 route list default 2>/dev/null | \
551
+ awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
552
+ else
553
+ default_if=$(${timeout_cmd} ip -o -6 route list default 2>/dev/null | \
554
+ awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
555
+ fi
556
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="iproute2"
557
+ fi
558
+
559
+ # Last resort: netstat (if available)
560
+ if [ -z "${default_if}" ] && command -v netstat >/dev/null 2>&1; then
561
+ if [ "${ip_version}" = "-4" ]; then
562
+ default_if=$(${timeout_cmd} netstat -rn 2>/dev/null | \
563
+ awk '$1 == "0.0.0.0" && $2 != "0.0.0.0" {print $NF; exit}')
564
+ else
565
+ default_if=$(${timeout_cmd} netstat -rn -A inet6 2>/dev/null | \
566
+ awk '$1 == "::/0" {print $NF; exit}')
567
+ fi
568
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="netstat"
569
+ fi
570
+ ;;
571
+
572
+ Darwin)
573
+ # macOS specific handling
574
+ if [ "${ip_version}" = "-4" ]; then
575
+ # Try route first
576
+ default_if=$(${timeout_cmd} route -n get default 2>/dev/null | \
577
+ awk '/interface:/ {print $2; exit}')
578
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="route"
579
+
580
+ # Fallback to netstat
581
+ if [ -z "${default_if}" ]; then
582
+ default_if=$(${timeout_cmd} netstat -rnf inet 2>/dev/null | \
583
+ awk '/^default/ {print $4; exit}')
584
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="netstat"
585
+ fi
586
+ else
587
+ # IPv6 - route doesn't work, use netstat
588
+ # Note: may have multiple defaults (utun interfaces)
589
+ default_if=$(${timeout_cmd} netstat -rnf inet6 2>/dev/null | \
590
+ awk '/^default/ && $4 !~ /^utun/ {print $4; exit}')
591
+ if [ -z "${default_if}" ]; then
592
+ # If no non-utun default, take first one
593
+ default_if=$(${timeout_cmd} netstat -rnf inet6 2>/dev/null | \
594
+ awk '/^default/ {print $4; exit}')
595
+ fi
596
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="netstat"
597
+ fi
598
+ ;;
599
+
600
+ *BSD)
601
+ # FreeBSD, OpenBSD, NetBSD
602
+ inet_flag=""
603
+ [ "${ip_version}" = "-6" ] && inet_flag="-inet6"
604
+
605
+ # route with -n to prevent DNS lookups
606
+ default_if=$(${timeout_cmd} route -n get ${inet_flag} default 2>/dev/null | \
607
+ awk '/interface:/ {print $2; exit}')
608
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="route"
609
+
610
+ # Fallback to netstat
611
+ if [ -z "${default_if}" ]; then
612
+ if [ "${ip_version}" = "-4" ]; then
613
+ default_if=$(${timeout_cmd} netstat -rnf inet 2>/dev/null | \
614
+ awk '/^default/ {print $4; exit}')
615
+ else
616
+ default_if=$(${timeout_cmd} netstat -rnf inet6 2>/dev/null | \
617
+ awk '/^default/ {print $4; exit}')
618
+ fi
619
+ [ -n "${default_if}" ] && DEFAULT_INTERFACE_DETECTION="netstat"
620
+ fi
621
+ ;;
622
+ esac
623
+
624
+ # Get IP address from interface
625
+ if [ -n "${default_if}" ] && [ "${default_if}" != "lo" ]; then
626
+ # Skip if interface is down (Linux only)
627
+ if [ "${KERNEL_NAME}" = "Linux" ] && [ -r "/sys/class/net/${default_if}/operstate" ]; then
628
+ state=$(cat "/sys/class/net/${default_if}/operstate" 2>/dev/null)
629
+ if [ "${state}" = "down" ]; then
630
+ return
631
+ fi
632
+ fi
633
+
634
+ if [ "${ip_version}" = "-4" ]; then
635
+ # IPv4 handling
636
+ # Try ip command first on Linux
637
+ if [ "${KERNEL_NAME}" = "Linux" ] && command -v ip >/dev/null 2>&1; then
638
+ # With -o flag, inet is field 3, IP is field 4
639
+ ip_addr=$(${timeout_cmd} ip -o -4 addr show dev "${default_if}" 2>/dev/null | \
640
+ awk '$3 == "inet" && $0 !~ /secondary/ {gsub(/\/.*/, "", $4); print $4; exit}')
641
+ fi
642
+
643
+ # Fallback to ifconfig (if available)
644
+ if [ -z "${ip_addr}" ] && command -v ifconfig >/dev/null 2>&1; then
645
+ ip_addr=$(${timeout_cmd} ifconfig "${default_if}" 2>/dev/null | \
646
+ awk '/inet / && !/127.0.0.1/ {gsub(/addr:/, "", $2); print $2; exit}')
647
+ fi
648
+
649
+ # Validate IPv4
650
+ if [ -n "${ip_addr}" ]; then
651
+ if echo "${ip_addr}" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' && \
652
+ echo "${ip_addr}" | awk -F. '$1<=255 && $2<=255 && $3<=255 && $4<=255 {exit 0} {exit 1}'; then
653
+ DEFAULT_INTERFACE_IP="${ip_addr}"
654
+ DEFAULT_INTERFACE_NAME="${default_if}"
655
+ fi
656
+ fi
657
+ else
658
+ # IPv6 handling
659
+ if [ "${KERNEL_NAME}" = "Linux" ] && command -v ip >/dev/null 2>&1; then
660
+ # Get global scope IPv6, not link-local
661
+ ip_addr=$(${timeout_cmd} ip -o -6 addr show dev "${default_if}" scope global 2>/dev/null | \
662
+ awk '$3 == "inet6" && $0 !~ /deprecated/ {gsub(/\/.*/, "", $4); print $4; exit}')
663
+ fi
664
+
665
+ if [ -z "${ip_addr}" ] && command -v ifconfig >/dev/null 2>&1; then
666
+ # Skip fe80:: (link-local) and ::1 (loopback)
667
+ ip_addr=$(${timeout_cmd} ifconfig "${default_if}" 2>/dev/null | \
668
+ awk '/inet6 / && !/fe80:/ && !/::1/ {sub(/%.*/, "", $2); print $2; exit}')
669
+ fi
670
+
671
+ # Basic IPv6 validation
672
+ if [ -n "${ip_addr}" ] && echo "${ip_addr}" | grep -qE '^[0-9a-fA-F:]+$' && \
673
+ echo "${ip_addr}" | grep -q ':'; then
674
+ DEFAULT_INTERFACE_IP="${ip_addr}"
675
+ DEFAULT_INTERFACE_NAME="${default_if}"
676
+ fi
677
+ fi
678
+ fi
679
+}
680
+
681
+get_default_interface_ip -4
682
+
683
echo "NETDATA_CONTAINER_OS_NAME=${CONTAINER_NAME}"
684
echo "NETDATA_CONTAINER_OS_ID=${CONTAINER_ID}"
685
echo "NETDATA_CONTAINER_OS_ID_LIKE=${CONTAINER_ID_LIKE}"
@@ -542,3 +713,6 @@ echo "NETDATA_SYSTEM_DISK_DETECTION=${DISK_DETECTION}"
713
echo "NETDATA_INSTANCE_CLOUD_TYPE=${CLOUD_TYPE}"
714
echo "NETDATA_INSTANCE_CLOUD_INSTANCE_TYPE=${CLOUD_INSTANCE_TYPE}"
715
echo "NETDATA_INSTANCE_CLOUD_INSTANCE_REGION=${CLOUD_INSTANCE_REGION}"
716
+echo "NETDATA_SYSTEM_DEFAULT_INTERFACE_NAME=${DEFAULT_INTERFACE_NAME}"
717
+echo "NETDATA_SYSTEM_DEFAULT_INTERFACE_IP=${DEFAULT_INTERFACE_IP}"
718
+echo "NETDATA_SYSTEM_DEFAULT_INTERFACE_DETECTION=${DEFAULT_INTERFACE_DETECTION}"