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# Returns the Gateway address for the interface and type
269GetGateways() {
270  local service="$1"
271  local netdev="$2"
272
273  # We fetch both the system properties and the netdev specific properties
274  # as OpenBMC is in the process of transitioning these to the netdev object
275  # but the migration is not yet complete.
276  {
277    GetProperties "$service" '/xyz/openbmc_project/network/config' \
278      'xyz.openbmc_project.Network.SystemConfiguration'
279    GetProperties "$service" "$(EthObjRoot "$netdev")" \
280      'xyz.openbmc_project.Network.EthernetInterface'
281  } | jq -s '
282      . | map(
283        if .DefaultGateway != "" then
284          {DefaultGateway: .DefaultGateway}
285        else
286          {}
287        end +
288        if .DefaultGateway6 != "" then
289          {DefaultGateway6: .DefaultGateway6}
290        else
291          {}
292        end
293      ) | {DefaultGateway: "", DefaultGateway6: ""} + add'
294}
295
296# Adds a static IP to the system network daemon
297AddIP() {
298  local service="$1"
299  local netdev="$2"
300  local ip="$3"
301  local prefix="$4"
302
303  local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
304  if ! IsIPv4 "$ip"; then
305    protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
306  fi
307
308  busctl call \
309    "$service" \
310    "$(EthObjRoot "$netdev")" \
311    'xyz.openbmc_project.Network.IP.Create' \
312    'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
313}
314
315# Determines if two IP addresses have the same address family
316# IE: Both are IPv4 or both are IPv6
317MatchingAF() {
318  local rc1=0 rc2=0
319  IsIPv4 "$1" || rc1=$?
320  IsIPv4 "$2" || rc2=$?
321  (( rc1 == rc2 ))
322}
323
324# Checks to see if the machine has the provided IP address information
325# already configured. If not, it deletes all of the information for that
326# address family and adds the provided IP address.
327UpdateIP() {
328  local service="$1"
329  local netdev="$2"
330  local ip="$3"
331  local prefix="$4"
332
333  local should_add=1
334  local delete_services=()
335  local delete_objects=()
336  local entry
337  while read entry; do
338    eval "$(echo "$entry" | JSONToVars)" || return $?
339    eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
340    if [ "$(normalize_ip "$Address")" = "$(normalize_ip "$ip")" ] && \
341        [ "$PrefixLength" = "$prefix" ]; then
342      should_add=0
343    elif MatchingAF "$ip" "$Address"; then
344      echo "Deleting spurious IP: $Address/$PrefixLength" >&2
345      delete_services+=("$service")
346      delete_objects+=("$object")
347    fi
348  done < <(GetIPObjects "$netdev")
349
350  local i
351  for (( i=0; i<${#delete_objects[@]}; ++i )); do
352    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
353  done
354
355  if (( should_add == 0 )); then
356    echo "Not adding IP: $ip/$prefix" >&2
357  else
358    echo "Adding IP: $ip/$prefix" >&2
359    AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
360  fi
361}
362
363# Sets the system gateway property to the provided IP address if not already
364# set to the current value.
365UpdateGateway() {
366  local service="$1"
367  local ip="$2"
368
369  local object='/xyz/openbmc_project/network/config'
370  local interface='xyz.openbmc_project.Network.SystemConfiguration'
371  local property='DefaultGateway'
372  if ! IsIPv4 "$ip"; then
373    property='DefaultGateway6'
374  fi
375
376  local current_ip
377  current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
378    return $?
379  if [ -n "$current_ip" ] && \
380      [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
381    echo "Not reconfiguring gateway: $ip" >&2
382    return 0
383  fi
384
385  echo "Setting gateway: $ip" >&2
386  busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
387}
388
389# Checks to see if the machine has the provided neighbor information
390# already configured. If not, it deletes all of the information for that
391# address family and adds the provided neighbor entry.
392UpdateNeighbor() {
393  local service="$1"
394  local netdev="$2"
395  local ip="$3"
396  local mac="$4"
397
398  local should_add=1
399  local delete_services=()
400  local delete_objects=()
401  local entry
402  while read entry; do
403    eval "$(echo "$entry" | JSONToVars)" || return $?
404    eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
405    if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
406        [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
407      should_add=0
408    elif MatchingAF "$ip" "$IPAddress"; then
409      echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
410      delete_services+=("$service")
411      delete_objects+=("$object")
412    fi
413  done < <(GetNeighborObjects "$netdev" 2>/dev/null)
414
415  local i
416  for (( i=0; i<${#delete_objects[@]}; ++i )); do
417    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
418  done
419
420  if (( should_add == 0 )); then
421    echo "Not adding neighbor: $ip $mac" >&2
422  else
423    echo "Adding neighbor: $ip $mac" >&2
424    AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
425  fi
426}
427
428# Determines the ip and mac of the IPv6 router
429DiscoverRouter6() {
430  local netdev="$1"
431  local retries="$2"
432  local timeout="$3"
433  local router="${4-}"
434
435  local output
436  local st=0
437  local args=(-1 -w "$timeout" -n $router "$netdev")
438  if (( retries < 0 )); then
439    args+=(-d)
440  else
441    args+=(-r "$retries")
442  fi
443  output="$(RunInterruptible rdisc6 "${args[@]}")" || st=$?
444  if (( st != 0 )); then
445    echo "rdisc6 failed with: " >&2
446    echo "$output" >&2
447    return $st
448  fi
449
450  local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
451  local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
452  local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
453  printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
454    "$ip" "$mac" "$staddr"
455}
456