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 the DBus object root for the static neighbors of an intrerface
233function StaticNeighborObjRoot() {
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
241function GetNeighborObjects() {
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
249function GetNeighbor() {
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
257function AddNeighbor() {
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
272function GetIPObjects() {
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
280function GetIP() {
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
288function GetGateways() {
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
316function AddIP() {
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
336function MatchingAF() {
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.
346function UpdateIP() {
347    local service="$1"
348    local netdev="$2"
349    local ip="$(normalize_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")" = "$ip" ] && \
360            [ "$PrefixLength" = "$prefix" ]; then
361            should_add=0
362        elif MatchingAF "$ip" "$Address" && [[ "$Origin" == *.Static ]]; 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    # The default address is treated as a delete only request
375    if [ "$ip" = :: -o "$ip" = 0.0.0.0 ]; then
376      return
377    fi
378
379    if (( should_add == 0 )); then
380        echo "Not adding IP: $ip/$prefix" >&2
381    else
382        echo "Adding IP: $ip/$prefix" >&2
383        AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
384    fi
385}
386
387# Sets the system gateway property to the provided IP address if not already
388# set to the current value.
389function UpdateGateway() {
390    local service="$1"
391    local netdev="$2"
392    local ip="$3"
393
394    local object="$(EthObjRoot "$netdev")"
395    local interface='xyz.openbmc_project.Network.EthernetInterface'
396    local property='DefaultGateway'
397    if ! IsIPv4 "$ip"; then
398        property='DefaultGateway6'
399    fi
400
401    local current_ip
402    current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
403        return $?
404    if [ -n "$current_ip" ] && \
405        [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
406        echo "Not reconfiguring gateway: $ip" >&2
407        return 0
408    fi
409
410    echo "Setting gateway: $ip" >&2
411    busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
412}
413
414# Checks to see if the machine has the provided neighbor information
415# already configured. If not, it deletes all of the information for that
416# address family and adds the provided neighbor entry.
417function UpdateNeighbor() {
418    local service="$1"
419    local netdev="$2"
420    local ip="$3"
421    local mac="$4"
422
423    local should_add=1
424    local delete_services=()
425    local delete_objects=()
426    local entry
427    while read entry; do
428        eval "$(echo "$entry" | JSONToVars)" || return $?
429        eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
430        if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
431            [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
432            should_add=0
433        elif MatchingAF "$ip" "$IPAddress"; then
434            echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
435            delete_services+=("$service")
436            delete_objects+=("$object")
437        fi
438    done < <(GetNeighborObjects "$netdev" 2>/dev/null)
439
440    local i
441    for (( i=0; i<${#delete_objects[@]}; ++i )); do
442        DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
443    done
444
445    if (( should_add == 0 )); then
446        echo "Not adding neighbor: $ip $mac" >&2
447    else
448        echo "Adding neighbor: $ip $mac" >&2
449        AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
450    fi
451}
452
453# Determines the ip and mac of the IPv6 router
454function DiscoverRouter6() {
455    local netdev="$1"
456    local retries="$2"
457    local timeout="$3"
458    local router="${4-}"
459
460    local output
461    local st=0
462    local args=(-1 -w "$timeout" -n $router "$netdev")
463    if (( retries < 0 )); then
464        args+=(-d)
465    else
466        args+=(-r "$retries")
467    fi
468    CaptureInterruptible output rdisc6 "${args[@]}" || st=$?
469    if (( st != 0 )); then
470        echo "rdisc6 failed with: " >&2
471        echo "$output" >&2
472        return $st
473    fi
474
475    local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
476    local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
477    local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
478    printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
479        "$ip" "$mac" "$staddr"
480}
481
482# Sets the network configuration of an interface to be static
483function SetStatic() {
484    local service="$1"
485    local netdev="$2"
486
487    echo "Disabling DHCP" >&2
488    busctl set-property "$service" "$(EthObjRoot "$netdev")" \
489        xyz.openbmc_project.Network.EthernetInterface DHCPEnabled \
490        s xyz.openbmc_project.Network.EthernetInterface.DHCPConf.none
491}
492