xref: /openbmc/openbmc/meta-google/recipes-google/networking/gbmc-net-common/gbmc-ra.sh (revision 0f3e725f65045d495167ab80401c97b16cad6542)
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# shellcheck source=meta-google/recipes-google/networking/network-sh/lib.sh
17source /usr/share/network/lib.sh || exit
18# shellcheck source=meta-google/recipes-google/networking/gbmc-net-common/gbmc-net-lib.sh
19source /usr/share/gbmc-net-lib.sh || exit
20
21: "${RA_IF:?No RA interface set}"
22: "${IP_OFFSET=?1}"
23: "${ROUTE_METRIC:?No Metric set}"
24
25# We would prefer empty string but it's easier for associative array handling
26# to use invalid
27old_rtr=invalid
28old_mac=invalid
29old_pfx=invalid
30old_fqdn=invalid
31
32ROUTE_TABLE=$ROUTE_METRIC
33
34default_update_rtr() {
35  local rtr="$1"
36  local mac="$2"
37  local op="${3-add}"
38
39  if ip addr show | grep -q "^[ ]*inet6 $rtr/"; then
40    echo "Router is ourself, ignoring" >&2
41    return 0
42  fi
43
44  # It's important that this happens before the main table default router is configured.
45  # Otherwise, the IP source determination logic won't be able to pick the best route.
46  # Also we don't need to remove the route per table.
47  if [[ ${op} == "add" ]]; then
48    # Add additional gateway information
49    for file in /run/systemd/network/{00,}-bmc-$RA_IF.network; do
50      mkdir -p "$file.d"
51      printf '[Route]\nGateway=%s\nGatewayOnLink=true\nMetric=512\nTable=%d' \
52        "$rtr" "$ROUTE_TABLE" >"$file.d"/10-gateway-table.conf
53    done
54
55    ip -6 route replace default via "$rtr" onlink dev "$RA_IF" metric 512 table "$ROUTE_TABLE" || \
56      gbmc_net_networkd_reload "$RA_IF"
57  fi
58
59  if [[ ${op} = "add" ]]; then
60
61    # Override any existing gateway information within files
62    # Make sure we cover `00-*` and `-*` files
63    for file in /run/systemd/network/{00,}-bmc-$RA_IF.network; do
64      mkdir -p "$file.d"
65      printf '[Route]\nGateway=%s\nGatewayOnLink=true\nMetric=%d\n[Neighbor]\nMACAddress=%s\nAddress=%s' \
66        "$rtr" "$ROUTE_METRIC" "$mac" "$rtr" >"$file.d"/10-gateway.conf
67    done
68
69    # Don't force networkd to reload as this can break phosphor-networkd
70    # Fall back to reload only if ip link commands fail
71    (ip -6 route replace default via "$rtr" onlink dev "$RA_IF" metric "$ROUTE_METRIC" && \
72      ip -6 neigh replace "$rtr" dev "$RA_IF" lladdr "$mac") || \
73      gbmc_net_networkd_reload "$RA_IF" || true
74
75    echo "Set router $rtr on $RA_IF" >&2
76  elif [[ ${op} = "remove" ]]; then
77    # Override any existing gateway information within files
78    # Make sure we cover `00-*` and `-*` files
79    for file in /run/systemd/network/{00,}-bmc-$RA_IF.network.d/10-gateway.conf; do
80      rm -rf "$file"
81    done
82
83    # Fall back to reload if remove failed
84    (ip -6 route del default via "$rtr" onlink dev "$RA_IF" metric "$ROUTE_METRIC" && \
85      ip -6 neigh del "$rtr" dev "$RA_IF" lladdr "$mac") || \
86      gbmc_net_networkd_reload "$RA_IF" || true
87
88    echo "Del router $rtr on $RA_IF" >&2
89  fi
90}
91
92default_update_fqdn() {
93  local fqdn="$1"
94  [ -z "$fqdn" ] && return
95  hostnamectl set-hostname "$fqdn" || true
96  echo "Set hostname $fqdn on $RA_IF" >&2
97}
98
99# mininum retry time
100min_w=10
101# rdisc time
102r_timeout=10
103# routing expiration time
104expire_time=180
105# rs intervals
106rs_intervals=30
107declare -A rtrs
108rtrs=()
109while true; do
110  # shellcheck disable=SC2206
111  data=(${rtrs["${old_rtr}"]-})
112  next_rs=
113  if [ -z "${data[1]}" ]; then
114    next_rs=$(( min_w + SECONDS ))
115  else
116    next_rs=$(( rs_intervals + SECONDS ))
117  fi
118  args=(-m "$RA_IF" -w $(( r_timeout * 1000 )) -r 1)
119  while read -r line; do
120    # `script` terminates all lines with a CRLF, remove it
121    line="${line:0:-1}"
122    # shellcheck disable=SC2026
123    if [ -z "$line" ]; then
124      lifetime=-1
125      mac=
126      hextet=
127      pfx=
128      host=
129      domain=
130    elif [[ "$line" =~ ^Router' 'lifetime' '*:' '*([0-9]*) ]]; then
131      lifetime="${BASH_REMATCH[1]}"
132    elif [[ "$line" =~ ^Source' 'link-layer' 'address' '*:' '*([a-fA-F0-9:]*)$ ]]; then
133      mac="${BASH_REMATCH[1]}"
134    elif [[ "$line" =~ ^Prefix' '*:' '*(.*)/([0-9]+)$ ]]; then
135      t_pfx="${BASH_REMATCH[1]}"
136      t_pfx_len="${BASH_REMATCH[2]}"
137      ip_to_bytes t_pfx_b "$t_pfx" || continue
138      (( (t_pfx_len == 76 || t_pfx_len == 80) && (t_pfx_b[8] & 0xfd) == 0xfd )) || continue
139      (( t_pfx_b[9] &= 0xf0 ))
140      (( t_pfx_b[9] |= IP_OFFSET ))
141      hextet="fd$(printf '%02x' "${t_pfx_b[9]}")"
142      pfx="$(ip_bytes_to_str t_pfx_b)"
143    elif [[ "$line" =~ ^'DNS search list'' '*:' '*([^.]+)(.*[.]google[.]com)' '*$ ]]; then
144      # Ideally, we use PCRE and with lookahead and can do this in a single regex
145      #   ^([a-zA-Z0-9-]+(?=-n[a-fA-F0-9]{1,4})|[a-zA-Z0-9-]+(?!-n[a-fA-F0-9]{1,4}))[^.]*[.]((?:[a-zA-Z0-9]*[.])*google[.]com)$
146      # Instead we do multiple steps to extract the needed info
147      host="${BASH_REMATCH[1]}"
148      domain="${BASH_REMATCH[2]#.}"
149      if [[ "$host" =~ (-n[a-fA-F0-9]{1,4})$ ]]; then
150        host="${host%"${BASH_REMATCH[1]}"}"
151      fi
152    elif [[ "$line" =~ ^from' '(.*)$ ]]; then
153      rtr="${BASH_REMATCH[1]}"
154      # Only valid default routers can be considered, 0 lifetime implies
155      # a non-default router
156      (( lifetime > 0 )) || continue
157
158      dl=$((expire_time + SECONDS))
159      fqdn=
160      if [[ -n $host && -n $hextet && -n $domain ]]; then
161        fqdn="$host-n$hextet.$domain"
162      fi
163      rtrs["$rtr"]="$mac $dl $pfx $fqdn"
164      # We have some notoriously noisy lab environments with many routers being broadcast
165      # We always prefer "fe80::1" in prod and labs for routing, so prefer that gateway.
166      # We also want to take the first router we find to speed up acquisition on boot.
167      if [[ "$rtr" = "fe80::1" || "$old_rtr" = "invalid" ]]; then
168        if [[ "$rtr" != "$old_rtr" && "$mac" != "$old_mac" ]]; then
169          echo "Got defgw $rtr at $mac on $RA_IF" >&2
170          update_rtr "$rtr" "$mac" || true
171          old_mac="$mac"
172          old_rtr="$rtr"
173        fi
174      fi
175      # Only update router properties if we use this router
176      [[ "$rtr" == "$old_rtr" ]] || continue
177      if [[ $pfx != "$old_pfx" ]]; then
178        echo "Got PFX $pfx from $rtr on $RA_IF" >&2
179        old_pfx="$pfx"
180        update_pfx "$pfx" || true
181      fi
182      if [[ $fqdn != "$old_fqdn" ]]; then
183        echo "Got FQDN $fqdn from $rtr on $RA_IF" >&2
184        old_fqdn="$fqdn"
185        update_fqdn "$fqdn" || true
186      fi
187    fi
188  done < <(exec script -q -c "rdisc6 ${args[*]}" /dev/null 2>/dev/null)
189  # Purge any expired routers
190  for rtr in "${!rtrs[@]}"; do
191    # shellcheck disable=SC2206
192    data=(${rtrs["$rtr"]})
193    dl=${data[1]}
194    if (( dl <= SECONDS )); then
195      unset "rtrs[$rtr]"
196    fi
197  done
198  # Consider changing the gateway if the old one doesn't send RAs for the entire period
199  # This ensures we don't flip flop between multiple defaults if they exist.
200  if [[ "$old_rtr" != "invalid" && -z "${rtrs["$old_rtr"]-}" ]]; then
201    echo "Old router $old_rtr disappeared" >&2
202    # replace routing config
203    found="false"
204    for rtr in "${!rtrs[@]}"; do
205      # shellcheck disable=SC2206
206      data=(${rtrs["$rtr"]})
207      mac=${data[0]}
208      dl=${data[1]}
209      pfx=${data[2]}
210      fqdn=${data[3]}
211      update_rtr "$rtr" "$mac" || true
212      update_pfx "$pfx" || true
213      update_fqdn "$fqdn" || true
214      old_rtr="$rtr"
215      old_pfx="$pfx"
216      old_mac="$mac"
217      old_fqdn="$fqdn"
218      found="true"
219      break
220    done
221    # no other route exsits, removing the route
222    if [[ "$found" = "false" ]]; then
223      update_rtr "$old_rtr" "$old_mac" "remove" || true
224      old_rtr=invalid
225      old_mac=invalid
226    fi
227  fi
228
229  # If rdisc6 exits early we still want to wait for the deadline before retrying
230  (( timeout = next_rs - SECONDS ))
231  sleep $(( timeout < 0 ? 0 : timeout ))
232done
233