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