1# Copyright 2021 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Internal handler used for signalling child processes that they should
16# terminate.
17HandleTerm() {
18  GOT_TERM=1
19  if ShouldTerm && (( ${#CHILD_PIDS[@]} > 0 )); then
20    kill "${!CHILD_PIDS[@]}"
21  fi
22}
23
24# Sets up the signal handler and global variables needed to run interruptible
25# services that can be killed gracefully.
26InitTerm() {
27  declare -g -A CHILD_PIDS=()
28  declare -g GOT_TERM=0
29  declare -g SUPPRESS_TERM=0
30  trap HandleTerm TERM
31}
32
33# Used to suppress the handling of SIGTERM for critical components that should
34# not respect SIGTERM. To finish suppressing, use UnsuppressTerm()
35SuppressTerm() {
36  SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
37}
38
39# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
40UnsuppressTerm() {
41  SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
42}
43
44# Determines if we got a SIGTERM and should respect it
45ShouldTerm() {
46  (( GOT_TERM == 1 && SUPPRESS_TERM == 0 ))
47}
48
49# Internal, ensures that functions called in a subprocess properly initialize
50# their SIGTERM handling logic
51RunInterruptibleFunction() {
52  CHILD_PIDS=()
53  trap HandleTerm TERM
54  "$@"
55}
56
57# Runs the provided commandline in the background, and passes any received
58# SIGTERMS to the child. Can be waited on using WaitInterruptibleBg
59RunInterruptibleBg() {
60  if ShouldTerm; then
61    return 143
62  fi
63  if [ "$(type -t "$1")" = "function" ]; then
64    RunInterruptibleFunction "$@" &
65  else
66    "$@" &
67  fi
68  CHILD_PIDS["$!"]=1
69}
70
71# Runs the provided commandline to completion, and passes any received
72# SIGTERMS to the child.
73RunInterruptible() {
74  RunInterruptibleBg "$@" || return
75  local child_pid="$!"
76  wait "$child_pid" || true
77  unset CHILD_PIDS["$child_pid"]
78  wait "$child_pid"
79}
80
81# Waits until all of the RunInterruptibleBg() jobs have terminated
82WaitInterruptibleBg() {
83  local wait_on=("${!CHILD_PIDS[@]}")
84  if (( ${#wait_on[@]} > 0 )); then
85    wait "${wait_on[@]}" || true
86    CHILD_PIDS=()
87    local rc=0
88    local id
89    for id in "${wait_on[@]}"; do
90      wait "$id" || rc=$?
91    done
92    return $rc
93  fi
94}
95
96# Determines if an address could be a valid IPv4 address
97# NOTE: this doesn't sanitize invalid IPv4 addresses
98IsIPv4() {
99  local ip="$1"
100
101  [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
102}
103
104# Takes lines of text from an application on stdin and parses out a single
105# MAC address per line of input.
106ParseMACFromLine() {
107  sed -n 's,.*\(\([0-9a-fA-F]\{2\}:\)\{5\}[0-9a-fA-F]\{2\}\).*,\1,p'
108}
109
110# Looks up the MAC address of the IPv4 neighbor using ARP
111DetermineNeighbor4() {
112  local netdev="$1"
113  local ip="$2"
114
115  # Grep intentionally prevented from returning an error to preserve the error
116  # value of arping
117  RunInterruptible arping -f -c 5 -w 5 -I "$netdev" "$ip" | \
118    { grep 'reply from' || true; } | ParseMACFromLine
119}
120
121# Looks up the MAC address of the IPv6 neighbor using ICMPv6 ND
122DetermineNeighbor6() {
123  local netdev="$1"
124  local ip="$2"
125
126  RunInterruptible ndisc6 -1 -r 5 -w 1000 -q "$ip" "$netdev"
127}
128
129# Looks up the MAC address of the neighbor regardless of type
130DetermineNeighbor() {
131  local netdev="$1"
132  local ip="$2"
133
134  if IsIPv4 "$ip"; then
135    DetermineNeighbor4 "$netdev" "$ip"
136  else
137    DetermineNeighbor6 "$netdev" "$ip"
138  fi
139}
140
141# Performs a mapper call to get the subroot for the object root
142# with a maxdepth and list of required interfaces. Returns a streamed list
143# of JSON objects that contain an { object, service }.
144GetSubTree() {
145  local root="$1"
146  shift
147  local max_depth="$1"
148  shift
149
150  busctl --json=short call \
151      'xyz.openbmc_project.ObjectMapper' \
152      '/xyz/openbmc_project/object_mapper' \
153      'xyz.openbmc_project.ObjectMapper' \
154      'GetSubTree' sias "$root" "$max_depth" "$#" "$@" | \
155    jq -c '.data[0] | to_entries[] | { object: .key, service: (.value | keys[0]) }'
156}
157
158# Returns all of the properties for a DBus interface on an object as a JSON
159# object where the keys are the property names
160GetProperties() {
161  local service="$1"
162  local object="$2"
163  local interface="$3"
164
165  busctl --json=short call \
166      "$service" \
167      "$object" \
168      'org.freedesktop.DBus.Properties' \
169      'GetAll' s "$interface" | \
170    jq -c '.data[0] | with_entries({ key, value: .value.data })'
171}
172
173# Returns the property for a DBus interface on an object
174GetProperty() {
175  local service="$1"
176  local object="$2"
177  local interface="$3"
178  local property="$4"
179
180  busctl --json=short call \
181      "$service" \
182      "$object" \
183      'org.freedesktop.DBus.Properties' \
184      'Get' ss "$interface" "$property" | \
185    jq -r '.data[0].data'
186}
187
188# Deletes any OpenBMC DBus object from a service
189DeleteObject() {
190  local service="$1"
191  local object="$2"
192
193  busctl call \
194    "$service" \
195    "$object" \
196    'xyz.openbmc_project.Object.Delete' \
197    'Delete'
198}
199
200# Transforms the given JSON dictionary into bash local variable
201# statements that can be directly evaluated by the interpreter
202JSONToVars() {
203  jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
204}
205
206# Returns the DBus object root for the ethernet interface
207EthObjRoot() {
208  local netdev="$1"
209
210  echo "/xyz/openbmc_project/network/$netdev"
211}
212
213# Returns the DBus object root for the static neighbors of an intrerface
214StaticNeighborObjRoot() {
215  local netdev="$1"
216
217  echo "$(EthObjRoot "$netdev")/static_neighbor"
218}
219
220# Returns all of the neighbor { service, object } data for an interface as if
221# a call to GetSubTree() was made
222GetNeighborObjects() {
223  local netdev="$1"
224
225  GetSubTree "$(StaticNeighborObjRoot "$netdev")" 0 \
226    'xyz.openbmc_project.Network.Neighbor'
227}
228
229# Returns the neighbor properties as a JSON object
230GetNeighbor() {
231  local service="$1"
232  local object="$2"
233
234  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
235}
236
237# Adds a static neighbor to the system network daemon
238AddNeighbor() {
239  local service="$1"
240  local netdev="$2"
241  local ip="$3"
242  local mac="$4"
243
244  busctl call \
245    "$service" \
246    "$(EthObjRoot "$netdev")" \
247    'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
248    'Neighbor' ss "$ip" "$mac" >/dev/null
249}
250
251# Returns all of the IP { service, object } data for an interface as if
252# a call to GetSubTree() was made
253GetIPObjects() {
254  local netdev="$1"
255
256  GetSubTree "$(EthObjRoot "$netdev")" 0 \
257    'xyz.openbmc_project.Network.IP'
258}
259
260# Returns the IP properties as a JSON object
261GetIP() {
262  local service="$1"
263  local object="$2"
264
265  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
266}
267
268# Adds a static IP to the system network daemon
269AddIP() {
270  local service="$1"
271  local netdev="$2"
272  local ip="$3"
273  local prefix="$4"
274
275  local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
276  if ! IsIPv4 "$ip"; then
277    protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
278  fi
279
280  busctl call \
281    "$service" \
282    "$(EthObjRoot "$netdev")" \
283    'xyz.openbmc_project.Network.IP.Create' \
284    'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
285}
286
287# Determines if two IP addresses have the same address family
288# IE: Both are IPv4 or both are IPv6
289MatchingAF() {
290  local rc1=0 rc2=0
291  IsIPv4 "$1" || rc1=$?
292  IsIPv4 "$2" || rc2=$?
293  (( rc1 == rc2 ))
294}
295
296# Checks to see if the machine has the provided IP address information
297# already configured. If not, it deletes all of the information for that
298# address family and adds the provided IP address.
299UpdateIP() {
300  local service="$1"
301  local netdev="$2"
302  local ip="$3"
303  local prefix="$4"
304
305  local should_add=1
306  local delete_services=()
307  local delete_objects=()
308  local entry
309  while read entry; do
310    eval "$(echo "$entry" | JSONToVars)" || return $?
311    eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
312    if [ "$(normalize_ip "$Address")" = "$(normalize_ip "$ip")" ] && \
313        [ "$PrefixLength" = "$prefix" ]; then
314      should_add=0
315    elif MatchingAF "$ip" "$Address"; then
316      echo "Deleting spurious IP: $Address/$PrefixLength" >&2
317      delete_services+=("$service")
318      delete_objects+=("$object")
319    fi
320  done < <(GetIPObjects "$netdev")
321
322  local i
323  for (( i=0; i<${#delete_objects[@]}; ++i )); do
324    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || return $?
325  done
326
327  if (( should_add == 0 )); then
328    echo "Not adding IP: $ip/$prefix" >&2
329  else
330    echo "Adding IP: $ip/$prefix" >&2
331    AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
332  fi
333}
334
335# Sets the system gateway property to the provided IP address if not already
336# set to the current value.
337UpdateGateway() {
338  local service="$1"
339  local ip="$2"
340
341  local object='/xyz/openbmc_project/network/config'
342  local interface='xyz.openbmc_project.Network.SystemConfiguration'
343  local property='DefaultGateway'
344  if ! IsIPv4 "$ip"; then
345    property='DefaultGateway6'
346  fi
347
348  local current_ip
349  current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
350    return $?
351  if [ -n "$current_ip" ] && \
352      [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
353    echo "Not reconfiguring gateway: $ip" >&2
354    return 0
355  fi
356
357  echo "Setting gateway: $ip" >&2
358  busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
359}
360
361# Checks to see if the machine has the provided neighbor information
362# already configured. If not, it deletes all of the information for that
363# address family and adds the provided neighbor entry.
364UpdateNeighbor() {
365  local service="$1"
366  local netdev="$2"
367  local ip="$3"
368  local mac="$4"
369
370  local should_add=1
371  local delete_services=()
372  local delete_objects=()
373  local entry
374  while read entry; do
375    eval "$(echo "$entry" | JSONToVars)" || return $?
376    eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
377    if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
378        [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
379      should_add=0
380    elif MatchingAF "$ip" "$IPAddress"; then
381      echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
382      delete_services+=("$service")
383      delete_objects+=("$object")
384    fi
385  done < <(GetNeighborObjects "$netdev" 2>/dev/null)
386
387  local i
388  for (( i=0; i<${#delete_objects[@]}; ++i )); do
389    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || return $?
390  done
391
392  if (( should_add == 0 )); then
393    echo "Not adding neighbor: $ip $mac" >&2
394  else
395    echo "Adding neighbor: $ip $mac" >&2
396    AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
397  fi
398}
399