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