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