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 -s TERM "${!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# Runs the provided commandline to completion, capturing stdout
97# into a variable
98CaptureInterruptible() {
99  local var="$1"
100  shift
101  if ShouldTerm; then
102    return 143
103  fi
104  coproc "$@" || return
105  local child_pid="$COPROC_PID"
106  CHILD_PIDS["$child_pid"]=1
107  exec {COPROC[1]}>&-
108  read -d $'\0' -ru "${COPROC[0]}" "$var" || true
109  wait "$child_pid" || true
110  unset CHILD_PIDS[$child_pid]
111  wait "$child_pid"
112}
113
114# Determines if an address could be a valid IPv4 address
115# NOTE: this doesn't sanitize invalid IPv4 addresses
116IsIPv4() {
117  local ip="$1"
118
119  [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
120}
121
122# Takes lines of text from an application on stdin and parses out a single
123# MAC address per line of input.
124ParseMACFromLine() {
125  sed -n 's,.*\(\([0-9a-fA-F]\{2\}:\)\{5\}[0-9a-fA-F]\{2\}\).*,\1,p'
126}
127
128# Looks up the MAC address of the IPv4 neighbor using ARP
129DetermineNeighbor4() {
130  local netdev="$1"
131  local ip="$2"
132
133  # Grep intentionally prevented from returning an error to preserve the error
134  # value of arping
135  RunInterruptible arping -f -c 5 -w 5 -I "$netdev" "$ip" | \
136    { grep 'reply from' || true; } | ParseMACFromLine
137}
138
139# Looks up the MAC address of the IPv6 neighbor using ICMPv6 ND
140DetermineNeighbor6() {
141  local netdev="$1"
142  local ip="$2"
143
144  RunInterruptible ndisc6 -1 -r 5 -w 1000 -q "$ip" "$netdev"
145}
146
147# Looks up the MAC address of the neighbor regardless of type
148DetermineNeighbor() {
149  local netdev="$1"
150  local ip="$2"
151
152  if IsIPv4 "$ip"; then
153    DetermineNeighbor4 "$netdev" "$ip"
154  else
155    DetermineNeighbor6 "$netdev" "$ip"
156  fi
157}
158
159# Performs a mapper call to get the subroot for the object root
160# with a maxdepth and list of required interfaces. Returns a streamed list
161# of JSON objects that contain an { object, service }.
162GetSubTree() {
163  local root="$1"
164  shift
165  local max_depth="$1"
166  shift
167
168  busctl --json=short call \
169      'xyz.openbmc_project.ObjectMapper' \
170      '/xyz/openbmc_project/object_mapper' \
171      'xyz.openbmc_project.ObjectMapper' \
172      'GetSubTree' sias "$root" "$max_depth" "$#" "$@" | \
173    jq -c '.data[0] | to_entries[] | { object: .key, service: (.value | keys[0]) }'
174}
175
176# Returns all of the properties for a DBus interface on an object as a JSON
177# object where the keys are the property names
178GetProperties() {
179  local service="$1"
180  local object="$2"
181  local interface="$3"
182
183  busctl --json=short call \
184      "$service" \
185      "$object" \
186      'org.freedesktop.DBus.Properties' \
187      'GetAll' s "$interface" | \
188    jq -c '.data[0] | with_entries({ key, value: .value.data })'
189}
190
191# Returns the property for a DBus interface on an object
192GetProperty() {
193  local service="$1"
194  local object="$2"
195  local interface="$3"
196  local property="$4"
197
198  busctl --json=short call \
199      "$service" \
200      "$object" \
201      'org.freedesktop.DBus.Properties' \
202      'Get' ss "$interface" "$property" | \
203    jq -r '.data[0].data'
204}
205
206# Deletes any OpenBMC DBus object from a service
207DeleteObject() {
208  local service="$1"
209  local object="$2"
210
211  busctl call \
212    "$service" \
213    "$object" \
214    'xyz.openbmc_project.Object.Delete' \
215    'Delete'
216}
217
218# Transforms the given JSON dictionary into bash local variable
219# statements that can be directly evaluated by the interpreter
220JSONToVars() {
221  jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
222}
223
224# Returns the DBus object root for the ethernet interface
225EthObjRoot() {
226  local netdev="$1"
227
228  echo "/xyz/openbmc_project/network/$netdev"
229}
230
231# Returns the DBus object root for the static neighbors of an intrerface
232StaticNeighborObjRoot() {
233  local netdev="$1"
234
235  echo "$(EthObjRoot "$netdev")/static_neighbor"
236}
237
238# Returns all of the neighbor { service, object } data for an interface as if
239# a call to GetSubTree() was made
240GetNeighborObjects() {
241  local netdev="$1"
242
243  GetSubTree "$(StaticNeighborObjRoot "$netdev")" 0 \
244    'xyz.openbmc_project.Network.Neighbor'
245}
246
247# Returns the neighbor properties as a JSON object
248GetNeighbor() {
249  local service="$1"
250  local object="$2"
251
252  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
253}
254
255# Adds a static neighbor to the system network daemon
256AddNeighbor() {
257  local service="$1"
258  local netdev="$2"
259  local ip="$3"
260  local mac="$4"
261
262  busctl call \
263    "$service" \
264    "$(EthObjRoot "$netdev")" \
265    'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
266    'Neighbor' ss "$ip" "$mac" >/dev/null
267}
268
269# Returns all of the IP { service, object } data for an interface as if
270# a call to GetSubTree() was made
271GetIPObjects() {
272  local netdev="$1"
273
274  GetSubTree "$(EthObjRoot "$netdev")" 0 \
275    'xyz.openbmc_project.Network.IP'
276}
277
278# Returns the IP properties as a JSON object
279GetIP() {
280  local service="$1"
281  local object="$2"
282
283  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
284}
285
286# Returns the Gateway address for the interface and type
287GetGateways() {
288  local service="$1"
289  local netdev="$2"
290
291  # We fetch both the system properties and the netdev specific properties
292  # as OpenBMC is in the process of transitioning these to the netdev object
293  # but the migration is not yet complete.
294  {
295    GetProperties "$service" '/xyz/openbmc_project/network/config' \
296      'xyz.openbmc_project.Network.SystemConfiguration'
297    GetProperties "$service" "$(EthObjRoot "$netdev")" \
298      'xyz.openbmc_project.Network.EthernetInterface'
299  } | jq -s '
300      . | map(
301        if .DefaultGateway != "" then
302          {DefaultGateway: .DefaultGateway}
303        else
304          {}
305        end +
306        if .DefaultGateway6 != "" then
307          {DefaultGateway6: .DefaultGateway6}
308        else
309          {}
310        end
311      ) | {DefaultGateway: "", DefaultGateway6: ""} + add'
312}
313
314# Adds a static IP to the system network daemon
315AddIP() {
316  local service="$1"
317  local netdev="$2"
318  local ip="$3"
319  local prefix="$4"
320
321  local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
322  if ! IsIPv4 "$ip"; then
323    protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
324  fi
325
326  busctl call \
327    "$service" \
328    "$(EthObjRoot "$netdev")" \
329    'xyz.openbmc_project.Network.IP.Create' \
330    'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
331}
332
333# Determines if two IP addresses have the same address family
334# IE: Both are IPv4 or both are IPv6
335MatchingAF() {
336  local rc1=0 rc2=0
337  IsIPv4 "$1" || rc1=$?
338  IsIPv4 "$2" || rc2=$?
339  (( rc1 == rc2 ))
340}
341
342# Checks to see if the machine has the provided IP address information
343# already configured. If not, it deletes all of the information for that
344# address family and adds the provided IP address.
345UpdateIP() {
346  local service="$1"
347  local netdev="$2"
348  local ip="$3"
349  local prefix="$4"
350
351  local should_add=1
352  local delete_services=()
353  local delete_objects=()
354  local entry
355  while read entry; do
356    eval "$(echo "$entry" | JSONToVars)" || return $?
357    eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
358    if [ "$(normalize_ip "$Address")" = "$(normalize_ip "$ip")" ] && \
359        [ "$PrefixLength" = "$prefix" ]; then
360      should_add=0
361    elif MatchingAF "$ip" "$Address"; then
362      echo "Deleting spurious IP: $Address/$PrefixLength" >&2
363      delete_services+=("$service")
364      delete_objects+=("$object")
365    fi
366  done < <(GetIPObjects "$netdev")
367
368  local i
369  for (( i=0; i<${#delete_objects[@]}; ++i )); do
370    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
371  done
372
373  if (( should_add == 0 )); then
374    echo "Not adding IP: $ip/$prefix" >&2
375  else
376    echo "Adding IP: $ip/$prefix" >&2
377    AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
378  fi
379}
380
381# Sets the system gateway property to the provided IP address if not already
382# set to the current value.
383UpdateGateway() {
384  local service="$1"
385  local netdev="$2"
386  local ip="$3"
387
388  local object="$(EthObjRoot "$netdev")"
389  local interface='xyz.openbmc_project.Network.EthernetInterface'
390  local property='DefaultGateway'
391  if ! IsIPv4 "$ip"; then
392    property='DefaultGateway6'
393  fi
394
395  local current_ip
396  current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
397    return $?
398  if [ -n "$current_ip" ] && \
399      [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
400    echo "Not reconfiguring gateway: $ip" >&2
401    return 0
402  fi
403
404  echo "Setting gateway: $ip" >&2
405  busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
406}
407
408# Checks to see if the machine has the provided neighbor information
409# already configured. If not, it deletes all of the information for that
410# address family and adds the provided neighbor entry.
411UpdateNeighbor() {
412  local service="$1"
413  local netdev="$2"
414  local ip="$3"
415  local mac="$4"
416
417  local should_add=1
418  local delete_services=()
419  local delete_objects=()
420  local entry
421  while read entry; do
422    eval "$(echo "$entry" | JSONToVars)" || return $?
423    eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
424    if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
425        [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
426      should_add=0
427    elif MatchingAF "$ip" "$IPAddress"; then
428      echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
429      delete_services+=("$service")
430      delete_objects+=("$object")
431    fi
432  done < <(GetNeighborObjects "$netdev" 2>/dev/null)
433
434  local i
435  for (( i=0; i<${#delete_objects[@]}; ++i )); do
436    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
437  done
438
439  if (( should_add == 0 )); then
440    echo "Not adding neighbor: $ip $mac" >&2
441  else
442    echo "Adding neighbor: $ip $mac" >&2
443    AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
444  fi
445}
446
447# Determines the ip and mac of the IPv6 router
448DiscoverRouter6() {
449  local netdev="$1"
450  local retries="$2"
451  local timeout="$3"
452  local router="${4-}"
453
454  local output
455  local st=0
456  local args=(-1 -w "$timeout" -n $router "$netdev")
457  if (( retries < 0 )); then
458    args+=(-d)
459  else
460    args+=(-r "$retries")
461  fi
462  CaptureInterruptible output rdisc6 "${args[@]}" || st=$?
463  if (( st != 0 )); then
464    echo "rdisc6 failed with: " >&2
465    echo "$output" >&2
466    return $st
467  fi
468
469  local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
470  local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
471  local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
472  printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
473    "$ip" "$mac" "$staddr"
474}
475
476# Sets the network configuration of an interface to be static
477SetStatic() {
478  local service="$1"
479  local netdev="$2"
480
481  echo "Disabling DHCP" >&2
482  busctl set-property "$service" "$(EthObjRoot "$netdev")" \
483    xyz.openbmc_project.Network.EthernetInterface DHCPEnabled \
484    s xyz.openbmc_project.Network.EthernetInterface.DHCPConf.none
485}
486