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.
18function HandleTerm() {
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.
27function InitTerm() {
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()
36function SuppressTerm() {
37    SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
38}
39
40# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
41function UnsuppressTerm() {
42    SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
43}
44
45# Determines if we got a SIGTERM and should respect it
46function ShouldTerm() {
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
52function RunInterruptibleFunction() {
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
60function RunInterruptibleBg() {
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.
74function RunInterruptible() {
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
83function WaitInterruptibleBg() {
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
99function CaptureInterruptible() {
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
117function IsIPv4() {
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.
125function ParseMACFromLine() {
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
130function DetermineNeighbor4() {
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
141function DetermineNeighbor6() {
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
149function DetermineNeighbor() {
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 }.
163function GetSubTree() {
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
179function GetProperties() {
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
193function GetProperty() {
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
208function DeleteObject() {
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
221function JSONToVars() {
222    jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
223}
224
225# Returns the DBus object root for the ethernet interface
226function EthObjRoot() {
227    local netdev="$1"
228
229    echo "/xyz/openbmc_project/network/$netdev"
230}
231
232# Returns all of the neighbor { service, object } data for an interface as if
233# a call to GetSubTree() was made
234function GetNeighborObjects() {
235    local netdev="$1"
236
237    GetSubTree "$(EthObjRoot "$netdev")" 0 \
238        'xyz.openbmc_project.Network.Neighbor'
239}
240
241# Returns the neighbor properties as a JSON object
242function GetNeighbor() {
243    local service="$1"
244    local object="$2"
245
246    GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
247}
248
249# Adds a static neighbor to the system network daemon
250function AddNeighbor() {
251    local service="$1"
252    local netdev="$2"
253    local ip="$3"
254    local mac="$4"
255
256    busctl call \
257        "$service" \
258        "$(EthObjRoot "$netdev")" \
259        'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
260        'Neighbor' ss "$ip" "$mac" >/dev/null
261}
262
263# Returns all of the IP { service, object } data for an interface as if
264# a call to GetSubTree() was made
265function GetIPObjects() {
266    local netdev="$1"
267
268    GetSubTree "$(EthObjRoot "$netdev")" 0 \
269        'xyz.openbmc_project.Network.IP'
270}
271
272# Returns the IP properties as a JSON object
273function GetIP() {
274    local service="$1"
275    local object="$2"
276
277    GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
278}
279
280# Returns the Gateway address for the interface and type
281function GetGateways() {
282    local service="$1"
283    local netdev="$2"
284
285    # We fetch both the system properties and the netdev specific properties
286    # as OpenBMC is in the process of transitioning these to the netdev object
287    # but the migration is not yet complete.
288    {
289        GetProperties "$service" '/xyz/openbmc_project/network/config' \
290            'xyz.openbmc_project.Network.SystemConfiguration'
291        GetProperties "$service" "$(EthObjRoot "$netdev")" \
292            'xyz.openbmc_project.Network.EthernetInterface'
293    } | jq -s '
294      . | map(
295        if .DefaultGateway != "" then
296          {DefaultGateway: .DefaultGateway}
297        else
298          {}
299        end +
300        if .DefaultGateway6 != "" then
301          {DefaultGateway6: .DefaultGateway6}
302        else
303          {}
304        end
305    ) | {DefaultGateway: "", DefaultGateway6: ""} + add'
306}
307
308# Adds a static IP to the system network daemon
309function AddIP() {
310    local service="$1"
311    local netdev="$2"
312    local ip="$3"
313    local prefix="$4"
314
315    local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
316    if ! IsIPv4 "$ip"; then
317        protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
318    fi
319
320    busctl call \
321        "$service" \
322        "$(EthObjRoot "$netdev")" \
323        'xyz.openbmc_project.Network.IP.Create' \
324        'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
325}
326
327# Determines if two IP addresses have the same address family
328# IE: Both are IPv4 or both are IPv6
329function MatchingAF() {
330    local rc1=0 rc2=0
331    IsIPv4 "$1" || rc1=$?
332    IsIPv4 "$2" || rc2=$?
333    (( rc1 == rc2 ))
334}
335
336# Checks to see if the machine has the provided IP address information
337# already configured. If not, it deletes all of the information for that
338# address family and adds the provided IP address.
339function UpdateIP() {
340    local service="$1"
341    local netdev="$2"
342    local ip="$(normalize_ip $3)"
343    local prefix="$4"
344
345    local should_add=1
346    local delete_services=()
347    local delete_objects=()
348    local entry
349    while read entry; do
350        eval "$(echo "$entry" | JSONToVars)" || return $?
351        eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
352        if [ "$(normalize_ip "$Address")" = "$ip" ] && \
353            [ "$PrefixLength" = "$prefix" ]; then
354            should_add=0
355        elif MatchingAF "$ip" "$Address" && [[ "$Origin" == *.Static ]]; then
356            echo "Deleting spurious IP: $Address/$PrefixLength" >&2
357            delete_services+=("$service")
358            delete_objects+=("$object")
359        fi
360    done < <(GetIPObjects "$netdev")
361
362    local i
363    for (( i=0; i<${#delete_objects[@]}; ++i )); do
364        DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
365    done
366
367    # The default address is treated as a delete only request
368    if [ "$ip" = :: -o "$ip" = 0.0.0.0 ]; then
369      return
370    fi
371
372    if (( should_add == 0 )); then
373        echo "Not adding IP: $ip/$prefix" >&2
374    else
375        echo "Adding IP: $ip/$prefix" >&2
376        AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
377    fi
378}
379
380# Sets the system gateway property to the provided IP address if not already
381# set to the current value.
382function UpdateGateway() {
383    local service="$1"
384    local netdev="$2"
385    local ip="$3"
386
387    local object="$(EthObjRoot "$netdev")"
388    local interface='xyz.openbmc_project.Network.EthernetInterface'
389    local property='DefaultGateway'
390    if ! IsIPv4 "$ip"; then
391        property='DefaultGateway6'
392    fi
393
394    local current_ip
395    current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
396        return $?
397    if [ -n "$current_ip" ] && \
398        [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
399        echo "Not reconfiguring gateway: $ip" >&2
400        return 0
401    fi
402
403    echo "Setting gateway: $ip" >&2
404    busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
405}
406
407# Checks to see if the machine has the provided neighbor information
408# already configured. If not, it deletes all of the information for that
409# address family and adds the provided neighbor entry.
410function UpdateNeighbor() {
411    local service="$1"
412    local netdev="$2"
413    local ip="$3"
414    local mac="$4"
415
416    local should_add=1
417    local delete_services=()
418    local delete_objects=()
419    local entry
420    while read entry; do
421        eval "$(echo "$entry" | JSONToVars)" || return $?
422        eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
423        if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
424            [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
425            should_add=0
426        elif MatchingAF "$ip" "$IPAddress"; then
427            echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
428            delete_services+=("$service")
429            delete_objects+=("$object")
430        fi
431    done < <(GetNeighborObjects "$netdev" 2>/dev/null)
432
433    local i
434    for (( i=0; i<${#delete_objects[@]}; ++i )); do
435        DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
436    done
437
438    if (( should_add == 0 )); then
439        echo "Not adding neighbor: $ip $mac" >&2
440    else
441        echo "Adding neighbor: $ip $mac" >&2
442        AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
443    fi
444}
445
446# Determines the ip and mac of the IPv6 router
447function DiscoverRouter6() {
448    local netdev="$1"
449    local retries="$2"
450    local timeout="$3"
451    local router="${4-}"
452
453    local output
454    local st=0
455    local args=(-1 -w "$timeout" -n $router "$netdev")
456    if (( retries < 0 )); then
457        args+=(-d)
458    else
459        args+=(-r "$retries")
460    fi
461    CaptureInterruptible output rdisc6 "${args[@]}" || st=$?
462    if (( st != 0 )); then
463        echo "rdisc6 failed with: " >&2
464        echo "$output" >&2
465        return $st
466    fi
467
468    local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
469    local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
470    local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
471    printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
472        "$ip" "$mac" "$staddr"
473}
474
475# Sets the network configuration of an interface to be static
476function SetStatic() {
477    local service="$1"
478    local netdev="$2"
479
480    echo "Disabling DHCP" >&2
481    busctl set-property "$service" "$(EthObjRoot "$netdev")" \
482        xyz.openbmc_project.Network.EthernetInterface DHCPEnabled \
483        s xyz.openbmc_project.Network.EthernetInterface.DHCPConf.none
484}
485