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