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