| 1 | #!/usr/bin/env bash |
| 2 | # SPDX-License-Identifier: GPL-3.0-or-later |
| 3 | # |
| 4 | # cgroup-network-helper.sh |
| 5 | # detect container and virtual machine interfaces |
| 6 | # |
| 7 | # |
| 8 | # This script is called as root (by cgroup-network), with either a pid, or a cgroup path. |
| 9 | # It tries to find all the network interfaces that belong to the same cgroup. |
| 10 | # |
| 11 | # It supports several method for this detection: |
| 12 | # |
| 13 | # 1. cgroup-network (the binary father of this script) detects veth network interfaces, |
| 14 | # by examining iflink and ifindex IDs and switching namespaces |
| 15 | # (it also detects the interface name as it is used by the container). |
| 16 | # |
| 17 | # 2. this script, uses /proc/PID/fdinfo to find tun/tap network interfaces. |
| 18 | # |
| 19 | # 3. this script, calls virsh to find libvirt network interfaces. |
| 20 | # |
| 21 | # shellcheck disable=SC1117 |
| 22 | |
| 23 | # ----------------------------------------------------------------------------- |
| 24 | |
| 25 | # the system path is cleared by cgroup-network |
| 26 | # shellcheck source=/dev/null |
| 27 | [ -f /etc/profile ] && source /etc/profile |
| 28 | export PATH="${PATH}:@sbindir_POST@" |
| 29 | |
| 30 | export LC_ALL=C |
| 31 | |
| 32 | cmd_line="'${0}' $(printf "'%s' " "${@}")" |
| 33 | |
| 34 | # ----------------------------------------------------------------------------- |
| 35 | # logging |
| 36 | |
| 37 | PROGRAM_NAME="$(basename "${0}")" |
| 38 | |
| 39 | # these should be the same with syslog() priorities |
| 40 | NDLP_EMERG=0 # system is unusable |
| 41 | NDLP_ALERT=1 # action must be taken immediately |
| 42 | NDLP_CRIT=2 # critical conditions |
| 43 | NDLP_ERR=3 # error conditions |
| 44 | NDLP_WARN=4 # warning conditions |
| 45 | NDLP_NOTICE=5 # normal but significant condition |
| 46 | NDLP_INFO=6 # informational |
| 47 | NDLP_DEBUG=7 # debug-level messages |
| 48 | |
| 49 | # the max (numerically) log level we will log |
| 50 | LOG_LEVEL=$NDLP_INFO |
| 51 | |
| 52 | set_log_min_priority() { |
| 53 | case "${NETDATA_LOG_LEVEL,,}" in |
| 54 | "emerg" | "emergency") |
| 55 | LOG_LEVEL=$NDLP_EMERG |
| 56 | ;; |
| 57 | |
| 58 | "alert") |
| 59 | LOG_LEVEL=$NDLP_ALERT |
| 60 | ;; |
| 61 | |
| 62 | "crit" | "critical") |
| 63 | LOG_LEVEL=$NDLP_CRIT |
| 64 | ;; |
| 65 | |
| 66 | "err" | "error") |
| 67 | LOG_LEVEL=$NDLP_ERR |
| 68 | ;; |
| 69 | |
| 70 | "warn" | "warning") |
| 71 | LOG_LEVEL=$NDLP_WARN |
| 72 | ;; |
| 73 | |
| 74 | "notice") |
| 75 | LOG_LEVEL=$NDLP_NOTICE |
| 76 | ;; |
| 77 | |
| 78 | "info") |
| 79 | LOG_LEVEL=$NDLP_INFO |
| 80 | ;; |
| 81 | |
| 82 | "debug") |
| 83 | LOG_LEVEL=$NDLP_DEBUG |
| 84 | ;; |
| 85 | esac |
| 86 | } |
| 87 | |
| 88 | set_log_min_priority |
| 89 | |
| 90 | log() { |
| 91 | local level="${1}" |
| 92 | shift 1 |
| 93 | |
| 94 | [[ -n "$level" && -n "$LOG_LEVEL" && "$level" -gt "$LOG_LEVEL" ]] && return |
| 95 | |
| 96 | systemd-cat-native --log-as-netdata <<EOFLOG |
| 97 | INVOCATION_ID=${NETDATA_INVOCATION_ID} |
| 98 | SYSLOG_IDENTIFIER=${PROGRAM_NAME} |
| 99 | PRIORITY=${level} |
| 100 | THREAD_TAG=cgroup-network-helper |
| 101 | ND_LOG_SOURCE=collector |
| 102 | ND_REQUEST=${cmd_line} |
| 103 | MESSAGE=${*//$'\n'/\\n} |
| 104 | |
| 105 | EOFLOG |
| 106 | # AN EMPTY LINE IS NEEDED ABOVE |
| 107 | } |
| 108 | |
| 109 | info() { |
| 110 | log "$NDLP_INFO" "${@}" |
| 111 | } |
| 112 | |
| 113 | warning() { |
| 114 | log "$NDLP_WARN" "${@}" |
| 115 | } |
| 116 | |
| 117 | error() { |
| 118 | log "$NDLP_ERR" "${@}" |
| 119 | } |
| 120 | |
| 121 | fatal() { |
| 122 | log "$NDLP_ALERT" "${@}" |
| 123 | exit 1 |
| 124 | } |
| 125 | |
| 126 | debug() { |
| 127 | log "$NDLP_DEBUG" "${@}" |
| 128 | } |
| 129 | |
| 130 | debug=0 |
| 131 | if [ "${NETDATA_CGROUP_NETWORK_HELPER_DEBUG-0}" = "1" ]; then |
| 132 | debug=1 |
| 133 | LOG_LEVEL=$NDLP_DEBUG |
| 134 | fi |
| 135 | |
| 136 | # ----------------------------------------------------------------------------- |
| 137 | # check for BASH v4+ (required for associative arrays) |
| 138 | |
| 139 | if [ ${BASH_VERSINFO[0]} -lt 4 ]; then |
| 140 | echo >&2 "BASH version 4 or later is required (this is ${BASH_VERSION})." |
| 141 | exit 1 |
| 142 | fi |
| 143 | |
| 144 | # ----------------------------------------------------------------------------- |
| 145 | # parse the arguments |
| 146 | |
| 147 | pid= |
| 148 | cgroup= |
| 149 | while [ -n "${1}" ] |
| 150 | do |
| 151 | case "${1}" in |
| 152 | --cgroup) cgroup="${2}"; shift 1;; |
| 153 | --pid|-p) pid="${2}"; shift 1;; |
| 154 | --debug|debug) |
| 155 | debug=1 |
| 156 | LOG_LEVEL=$NDLP_DEBUG |
| 157 | ;; |
| 158 | *) fatal "Cannot understand argument '${1}'";; |
| 159 | esac |
| 160 | |
| 161 | shift |
| 162 | done |
| 163 | |
| 164 | if [ -z "${pid}" ] && [ -z "${cgroup}" ] |
| 165 | then |
| 166 | fatal "Either --pid or --cgroup is required" |
| 167 | fi |
| 168 | |
| 169 | # ----------------------------------------------------------------------------- |
| 170 | |
| 171 | set_source() { |
| 172 | [ ${debug} -eq 1 ] && echo "SRC ${*}" |
| 173 | } |
| 174 | |
| 175 | |
| 176 | # ----------------------------------------------------------------------------- |
| 177 | # veth interfaces via cgroup |
| 178 | |
| 179 | # cgroup-network can detect veth interfaces by itself (written in C). |
| 180 | # If you seek for a shell version of what it does, check this: |
| 181 | # https://github.com/netdata/netdata/issues/474#issuecomment-317866709 |
| 182 | |
| 183 | |
| 184 | # ----------------------------------------------------------------------------- |
| 185 | # tun/tap interfaces via /proc/PID/fdinfo |
| 186 | |
| 187 | # find any tun/tap devices linked to a pid |
| 188 | proc_pid_fdinfo_iff() { |
| 189 | local p="${1}" # the pid |
| 190 | |
| 191 | debug "Searching for tun/tap interfaces for pid ${p}..." |
| 192 | set_source "fdinfo" |
| 193 | grep "^iff:.*" "${NETDATA_HOST_PREFIX}/proc/${p}/fdinfo"/* 2>/dev/null | cut -f 2 |
| 194 | } |
| 195 | |
| 196 | find_tun_tap_interfaces_for_cgroup() { |
| 197 | local c="${1}" # the cgroup path |
| 198 | [ -d "${c}/emulator" ] && c="${c}/emulator" # check for 'emulator' subdirectory |
| 199 | c="${c}/cgroup.procs" # make full path |
| 200 | |
| 201 | # for each pid of the cgroup |
| 202 | # find any tun/tap devices linked to the pid |
| 203 | if [ -f "${c}" ] |
| 204 | then |
| 205 | local p |
| 206 | for p in $(< "${c}" ) |
| 207 | do |
| 208 | proc_pid_fdinfo_iff "${p}" |
| 209 | done |
| 210 | else |
| 211 | debug "Cannot find file '${c}', not searching for tun/tap interfaces." |
| 212 | fi |
| 213 | } |
| 214 | |
| 215 | |
| 216 | # ----------------------------------------------------------------------------- |
| 217 | # virsh domain network interfaces |
| 218 | |
| 219 | virsh_cgroup_to_domain_name() { |
| 220 | local c="${1}" # the cgroup path |
| 221 | |
| 222 | debug "extracting a possible virsh domain from cgroup ${c}..." |
| 223 | |
| 224 | # extract for the cgroup path |
| 225 | sed -n -e "s|.*/machine-qemu\\\\x2d[0-9]\+\\\\x2d\(.*\)\.scope$|\1|p" \ |
| 226 | -e "s|.*/machine/qemu-[0-9]\+-\(.*\)\.libvirt-qemu$|\1|p" \ |
| 227 | -e "s|.*/machine/\(.*\)\.libvirt-qemu$|\1|p" \ |
| 228 | <<EOF |
| 229 | ${c} |
| 230 | EOF |
| 231 | } |
| 232 | |
| 233 | virsh_find_all_interfaces_for_cgroup() { |
| 234 | local c="${1}" # the cgroup path |
| 235 | |
| 236 | # the virsh command |
| 237 | local virsh |
| 238 | # shellcheck disable=SC2230 |
| 239 | virsh="$(which virsh 2>/dev/null || command -v virsh 2>/dev/null)" |
| 240 | |
| 241 | if [ -n "${virsh}" ] |
| 242 | then |
| 243 | local d |
| 244 | d="$(virsh_cgroup_to_domain_name "${c}")" |
| 245 | # convert hex to character |
| 246 | # e.g.: vm01\x2dweb => vm01-web (https://github.com/netdata/netdata/issues/11088#issuecomment-832618149) |
| 247 | d="$(printf '%b' "${d}")" |
| 248 | |
| 249 | if [ -n "${d}" ] |
| 250 | then |
| 251 | debug "running: virsh domiflist ${d}; to find the network interfaces" |
| 252 | |
| 253 | # 'virsh -r domiflist <domain>' example output |
| 254 | # Interface Type Source Model MAC |
| 255 | #-------------------------------------------------------------- |
| 256 | # vnet3 bridge br0 virtio 52:54:00:xx:xx:xx |
| 257 | # vnet4 network default virtio 52:54:00:yy:yy:yy |
| 258 | |
| 259 | # match only 'network' interfaces from virsh output |
| 260 | set_source "virsh" |
| 261 | "${virsh}" -r domiflist "${d}" |\ |
| 262 | sed -n \ |
| 263 | -e "s|^[[:space:]]\?\([^[:space:]]\+\)[[:space:]]\+network[[:space:]]\+\([^[:space:]]\+\)[[:space:]]\+[^[:space:]]\+[[:space:]]\+[^[:space:]]\+$|\1 \1_\2|p" \ |
| 264 | -e "s|^[[:space:]]\?\([^[:space:]]\+\)[[:space:]]\+bridge[[:space:]]\+\([^[:space:]]\+\)[[:space:]]\+[^[:space:]]\+[[:space:]]\+[^[:space:]]\+$|\1 \1_\2|p" |
| 265 | else |
| 266 | debug "no virsh domain extracted from cgroup ${c}" |
| 267 | fi |
| 268 | else |
| 269 | debug "virsh command is not available" |
| 270 | fi |
| 271 | } |
| 272 | |
| 273 | # ----------------------------------------------------------------------------- |
| 274 | # netnsid detected interfaces |
| 275 | |
| 276 | netnsid_find_all_interfaces_for_pid() { |
| 277 | local pid="${1}" |
| 278 | [ -z "${pid}" ] && return 1 |
| 279 | |
| 280 | local nsid |
| 281 | nsid=$(lsns -t net -p "${pid}" -o NETNSID -nr 2>/dev/null) |
| 282 | if [ -z "${nsid}" ] || [ "${nsid}" = "unassigned" ]; then |
| 283 | return 1 |
| 284 | fi |
| 285 | |
| 286 | set_source "netnsid" |
| 287 | ip link show |\ |
| 288 | grep -B 1 -E " link-netnsid ${nsid}($| )" |\ |
| 289 | sed -n -e "s|^[[:space:]]*[0-9]\+:[[:space:]]\+\([A-Za-z0-9_]\+\)\(@[A-Za-z0-9_]\+\)*:[[:space:]].*$|\1|p" |
| 290 | } |
| 291 | |
| 292 | netnsid_find_all_interfaces_for_cgroup() { |
| 293 | local c="${1}" # the cgroup path |
| 294 | |
| 295 | if [ -f "${c}/cgroup.procs" ]; then |
| 296 | netnsid_find_all_interfaces_for_pid "$(head -n 1 "${c}/cgroup.procs" 2>/dev/null)" |
| 297 | else |
| 298 | debug "Cannot find file '${c}/cgroup.procs', not searching for netnsid interfaces." |
| 299 | fi |
| 300 | } |
| 301 | |
| 302 | # ----------------------------------------------------------------------------- |
| 303 | |
| 304 | find_all_interfaces_of_pid_or_cgroup() { |
| 305 | local p="${1}" c="${2}" # the pid and the cgroup path |
| 306 | |
| 307 | if [ -n "${pid}" ] |
| 308 | then |
| 309 | # we have been called with a pid |
| 310 | |
| 311 | proc_pid_fdinfo_iff "${p}" |
| 312 | netnsid_find_all_interfaces_for_pid "${p}" |
| 313 | |
| 314 | elif [ -n "${c}" ] |
| 315 | then |
| 316 | # we have been called with a cgroup |
| 317 | |
| 318 | info "searching for network interfaces of cgroup '${c}'" |
| 319 | |
| 320 | find_tun_tap_interfaces_for_cgroup "${c}" |
| 321 | virsh_find_all_interfaces_for_cgroup "${c}" |
| 322 | netnsid_find_all_interfaces_for_cgroup "${c}" |
| 323 | |
| 324 | else |
| 325 | |
| 326 | error "Either a pid or a cgroup path is needed" |
| 327 | return 1 |
| 328 | |
| 329 | fi |
| 330 | |
| 331 | return 0 |
| 332 | } |
| 333 | |
| 334 | # ----------------------------------------------------------------------------- |
| 335 | |
| 336 | # an associative array to store the interfaces |
| 337 | # the index is the interface name as seen by the host |
| 338 | # the value is the interface name as seen by the guest / container |
| 339 | declare -A devs=() |
| 340 | |
| 341 | # store all interfaces found in the associative array |
| 342 | # this will also give the unique devices, as seen by the host |
| 343 | last_src= |
| 344 | # shellcheck disable=SC2162 |
| 345 | while read host_device guest_device |
| 346 | do |
| 347 | [ -z "${host_device}" ] && continue |
| 348 | |
| 349 | [ "${host_device}" = "SRC" ] && last_src="${guest_device}" && continue |
| 350 | |
| 351 | # the default guest_device is the host_device |
| 352 | [ -z "${guest_device}" ] && guest_device="${host_device}" |
| 353 | |
| 354 | # when we run in debug, show the source |
| 355 | debug "Found host device '${host_device}', guest device '${guest_device}', detected via '${last_src}'" |
| 356 | |
| 357 | if [ -z "${devs[${host_device}]}" ] || [ "${devs[${host_device}]}" = "${host_device}" ]; then |
| 358 | devs[${host_device}]="${guest_device}" |
| 359 | fi |
| 360 | |
| 361 | done < <( find_all_interfaces_of_pid_or_cgroup "${pid}" "${cgroup}" ) |
| 362 | |
| 363 | # print the interfaces found, in the format netdata expects them |
| 364 | found=0 |
| 365 | for x in "${!devs[@]}" |
| 366 | do |
| 367 | found=$((found + 1)) |
| 368 | echo "${x} ${devs[${x}]}" |
| 369 | done |
| 370 | |
| 371 | debug "found ${found} network interfaces for pid '${pid}', cgroup '${cgroup}', run as ${USER}, ${UID}" |
| 372 | |
| 373 | # let netdata know if we found any |
| 374 | [ ${found} -eq 0 ] && exit 1 |
| 375 | exit 0 |