master
in 375 lines 10 KB
Raw
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