1#!/bin/bash
2# shellcheck disable=SC2034
3# shellcheck disable=SC2317
4# Copyright 2021 Google LLC
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18# A list of functions which get executed for each netlink event received.
19# These are configured by the files included below.
20GBMC_IP_MONITOR_HOOKS=()
21
22# Load configurations from a known location in the filesystem to populate
23# hooks that are executed after each event.
24shopt -s nullglob
25for conf in /usr/share/gbmc-ip-monitor/*.sh; do
26  # shellcheck source=/dev/null
27  source "$conf"
28done
29
30gbmc_ip_monitor_run_hooks() {
31  local hook
32  for hook in "${GBMC_IP_MONITOR_HOOKS[@]}"; do
33    "$hook" || continue
34  done
35}
36
37gbmc_ip_monitor_generate_init() {
38  ip link | sed 's,^[^ ],[LINK]\0,'
39  local intf=
40  local line
41  while read -r line; do
42    [[ "$line" =~ ^([0-9]+:[[:space:]][^:]+) ]] && intf="${BASH_REMATCH[1]}"
43    [[ "$line" =~ ^[[:space:]]*inet ]] && echo "[ADDR]$intf $line"
44  done < <(ip addr)
45  ip -4 route | sed 's,^,[ROUTE],'
46  ip -6 route | sed 's,^,[ROUTE],'
47  echo '[INIT]'
48}
49
50GBMC_IP_MONITOR_DEFER_OUTSTANDING=
51gbmc_ip_monitor_defer_() {
52  sleep 1
53  printf '[DEFER]\n' >&"$GBMC_IP_MONITOR_DEFER"
54}
55gbmc_ip_monitor_defer() {
56  [ -z "$GBMC_IP_MONITOR_DEFER_OUTSTANDING" ] || return 0
57  gbmc_ip_monitor_defer_ &
58  GBMC_IP_MONITOR_DEFER_OUTSTANDING=1
59}
60
61gbmc_ip_monitor_parse_line() {
62  local line="$1"
63  if [[ "$line" == '[INIT]'* ]]; then
64    change=init
65    echo "Initialized" >&2
66  elif [[ "$line" == '[ADDR]'* ]]; then
67    change=addr
68    action=add
69    pfx_re='^\[ADDR\](Deleted )?[0-9]+:[[:space:]]*'
70    intf_re='([^ ]+)[[:space:]]+'
71    fam_re='([^ ]+)[[:space:]]+'
72    addr_re='([^/]+)/[0-9]+[[:space:]]+'
73    metric_re='(metric[[:space:]]+[^ ]+[[:space:]]+)?'
74    brd_re='(brd[[:space:]]+[^ ]+[[:space:]]+)?'
75    scope_re='scope[[:space:]]+([^ ]+)[[:space:]]*(.*)'
76    combined_re="${pfx_re}${intf_re}${fam_re}${addr_re}${metric_re}${brd_re}${scope_re}"
77    if ! [[ "$line" =~ ${combined_re} ]]; then
78      echo "Failed to parse addr: $line" >&2
79      return 1
80    fi
81    if [ -n "${BASH_REMATCH[1]}" ]; then
82      action=del
83    fi
84    intf="${BASH_REMATCH[2]}"
85    fam="${BASH_REMATCH[3]}"
86    ip="${BASH_REMATCH[4]}"
87    scope="${BASH_REMATCH[7]}"
88    flags="${BASH_REMATCH[8]}"
89  elif [[ "$line" == '[ROUTE]'* ]]; then
90    line="${line#[ROUTE]}"
91    change=route
92    action=add
93    if ! [[ "$line" =~ ^\[ROUTE\](Deleted )?(.*)$ ]]; then
94      echo "Failed to parse link: $line" >&2
95      return 1
96    fi
97    if [ -n "${BASH_REMATCH[1]}" ]; then
98      action=del
99    fi
100    route="${BASH_REMATCH[2]}"
101  elif [[ "$line" == '[LINK]'* ]]; then
102    change='link'
103    action=add
104    pfx_re='^\[LINK\](Deleted )?[0-9]+:[[:space:]]*'
105    intf_re='([^:]+):[[:space:]]+'
106    if ! [[ "$line" =~ ${pfx_re}${intf_re} ]]; then
107      echo "Failed to parse link: $line" >&2
108      return 1
109    fi
110    if [ -n "${BASH_REMATCH[1]}" ]; then
111      action=del
112    fi
113    intf="${BASH_REMATCH[2]}"
114    read -ra data || return
115    mac="${data[1]}"
116  elif [[ "$line" == '[DEFER]'* ]]; then
117    GBMC_IP_MONITOR_DEFER_OUTSTANDING=
118    change=defer
119  else
120    return 2
121  fi
122}
123
124return 0 2>/dev/null
125
126cleanup() {
127  local st="$?"
128  trap - HUP INT QUIT ABRT TERM EXIT
129  jobs -l -p | xargs -r kill || true
130  exit "$st"
131}
132trap cleanup HUP INT QUIT ABRT TERM EXIT
133
134FIFODIR="$(mktemp -d)"
135mkfifo "$FIFODIR"/fifo
136exec {GBMC_IP_MONITOR_DEFER}<>"$FIFODIR"/fifo
137rm -rf "$FIFODIR"
138
139while read -r line; do
140  gbmc_ip_monitor_parse_line "$line" || continue
141  gbmc_ip_monitor_run_hooks || continue
142  if [ "$change" = 'init' ]; then
143    systemd-notify --ready
144  fi
145done < <(gbmc_ip_monitor_generate_init; ip monitor link addr route label & cat <&"$GBMC_IP_MONITOR_DEFER")
146