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 netdev="$2" 368 local ip="$3" 369 370 local object="$(EthObjRoot "$netdev")" 371 local interface='xyz.openbmc_project.Network.EthernetInterface' 372 local property='DefaultGateway' 373 if ! IsIPv4 "$ip"; then 374 property='DefaultGateway6' 375 fi 376 377 local current_ip 378 current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \ 379 return $? 380 if [ -n "$current_ip" ] && \ 381 [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then 382 echo "Not reconfiguring gateway: $ip" >&2 383 return 0 384 fi 385 386 echo "Setting gateway: $ip" >&2 387 busctl set-property "$service" "$object" "$interface" "$property" s "$ip" 388} 389 390# Checks to see if the machine has the provided neighbor information 391# already configured. If not, it deletes all of the information for that 392# address family and adds the provided neighbor entry. 393UpdateNeighbor() { 394 local service="$1" 395 local netdev="$2" 396 local ip="$3" 397 local mac="$4" 398 399 local should_add=1 400 local delete_services=() 401 local delete_objects=() 402 local entry 403 while read entry; do 404 eval "$(echo "$entry" | JSONToVars)" || return $? 405 eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $? 406 if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \ 407 [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then 408 should_add=0 409 elif MatchingAF "$ip" "$IPAddress"; then 410 echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2 411 delete_services+=("$service") 412 delete_objects+=("$object") 413 fi 414 done < <(GetNeighborObjects "$netdev" 2>/dev/null) 415 416 local i 417 for (( i=0; i<${#delete_objects[@]}; ++i )); do 418 DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true 419 done 420 421 if (( should_add == 0 )); then 422 echo "Not adding neighbor: $ip $mac" >&2 423 else 424 echo "Adding neighbor: $ip $mac" >&2 425 AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $? 426 fi 427} 428 429# Determines the ip and mac of the IPv6 router 430DiscoverRouter6() { 431 local netdev="$1" 432 local retries="$2" 433 local timeout="$3" 434 local router="${4-}" 435 436 local output 437 local st=0 438 local args=(-1 -w "$timeout" -n $router "$netdev") 439 if (( retries < 0 )); then 440 args+=(-d) 441 else 442 args+=(-r "$retries") 443 fi 444 output="$(RunInterruptible rdisc6 "${args[@]}")" || st=$? 445 if (( st != 0 )); then 446 echo "rdisc6 failed with: " >&2 447 echo "$output" >&2 448 return $st 449 fi 450 451 local ip="$(echo "$output" | grep 'from' | awk '{print $2}')" 452 local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)" 453 local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')" 454 printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \ 455 "$ip" "$mac" "$staddr" 456} 457 458# Sets the network configuration of an interface to be static 459SetStatic() { 460 local service="$1" 461 local netdev="$2" 462 463 echo "Disabling DHCP" >&2 464 busctl set-property "$service" "$(EthObjRoot "$netdev")" \ 465 xyz.openbmc_project.Network.EthernetInterface DHCPEnabled \ 466 s xyz.openbmc_project.Network.EthernetInterface.DHCPConf.none 467} 468