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
16source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
17
18NCSI_IF="$1"
19
20# We would prefer empty string but it's easier for associative array handling
21# to use invalid
22old_rtr=invalid
23old_mac=
24
25function set_rtr() {
26    if ! ip -6 route show | grep -q '^default'; then
27        echo 'default route missing, reconfiguring...' >&2
28        old_rtr=invalid
29        old_mac=
30    fi
31    [ "$rtr" != "$old_rtr" -a "$mac" != "$old_mac" ] || return
32
33    echo "Setting default router: $rtr at $mac" >&2
34
35    # Delete and static gateways and neighbors
36    while read entry; do
37        eval "$(echo "$entry" | JSONToVars)" || return
38        echo "Deleting neighbor $object"
39        DeleteObject "$service" "$object" || true
40    done < <(GetNeighborObjects "$netdev" 2>/dev/null)
41
42    busctl set-property xyz.openbmc_project.Network "$(EthObjRoot "$NCSI_IF")" \
43        xyz.openbmc_project.Network.EthernetInterface DefaultGateway6 s "" || true
44
45    # In case we don't have a base network file, make one
46    net_file=/run/systemd/network/00-bmc-$NCSI_IF.network
47    printf '[Match]\nName=%s\n[Network]\nDHCP=false\nIPv6AcceptRA=false\nLinkLocalAddressing=yes' \
48        "$NCSI_IF" >$net_file
49
50    # Override any existing gateway info
51    mkdir -p $net_file.d
52    printf '[Network]\nGateway=%s\n[Neighbor]\nMACAddress=%s\nAddress=%s' \
53        "$rtr" "$mac" "$rtr" >$net_file.d/10-gateway.conf
54
55    # Don't force networkd to reload as this can break phosphor-networkd
56    # Fall back to reload only if ip link commands fail
57    (ip -6 route replace default via "$rtr" dev "$NCSI_IF" && \
58        ip -6 neigh replace "$rtr" dev "$NCSI_IF" lladdr "$mac") || \
59        (networkctl reload && networkctl reconfigure "$NCSI_IF") || true
60
61    retries=-1
62    old_mac="$mac"
63    old_rtr="$rtr"
64}
65
66retries=1
67min_w=10
68declare -A rtrs
69rtrs=()
70while true; do
71    data=(${rtrs["${old_rtr}"]-})
72    curr_dl="${data[1]-$min_w}"
73    args=(-m "$NCSI_IF" -w $(( (curr_dl - SECONDS) * 1000 )))
74    if (( retries > 0 )); then
75        args+=(-r "$retries")
76    else
77        args+=(-d)
78    fi
79    while read line; do
80        # `script` terminates all lines with a CRLF, remove it
81        line="${line:0:-1}"
82        if [ -z "$line" ]; then
83            lifetime=-1
84            mac=
85        elif [[ "$line" =~ ^Router' 'lifetime' '*:' '*([0-9]*) ]]; then
86            lifetime="${BASH_REMATCH[1]}"
87        elif [[ "$line" =~ ^Source' 'link-layer' 'address' '*:' '*([a-fA-F0-9:]*)$ ]]; then
88            mac="${BASH_REMATCH[1]}"
89        elif [[ "$line" =~ ^from' '(.*)$ ]]; then
90            rtr="${BASH_REMATCH[1]}"
91            # Only valid default routers can be considered, 0 lifetime implies
92            # a non-default router
93            if (( lifetime > 0 )); then
94                dl=$((lifetime + SECONDS))
95                rtrs["$rtr"]="$mac $dl"
96                # We have some notoriously noisy lab environments with many routers being broadcast
97                # We always prefer "fe80::1" in prod and labs for routing, so prefer that gateway.
98                # We also want to take the first router we find to speed up acquisition on boot.
99                if [ "$rtr" = "fe80::1" -o -z "$old_rtr" ]; then
100                    set_rtr || true
101                fi
102            fi
103            lifetime=-1
104            mac=
105        fi
106    done < <(exec script -q -c "rdisc6 ${args[*]}" /dev/null 2>/dev/null)
107    # Purge any expired routers
108    for rtr in "${!rtrs[@]}"; do
109        data=(${rtrs["$rtr"]})
110        dl=${data[1]}
111        if (( dl <= SECONDS )); then
112            unset rtrs["$rtr"]
113        fi
114    done
115    # Consider changing the gateway if the old one doesn't send RAs for the entire period
116    # This ensures we don't flip flop between multiple defaults if they exist.
117    if [ -z "${rtrs["$old_rtr"]-}" ]; then
118        echo "Old router $old_rtr disappeared" >&2
119        for rtr in "${!rtrs[@]}"; do
120            data=(${rtrs["$rtr"]})
121            mac=${data[0]}
122            dl=${data[1]}
123            set_rtr && break
124        done
125    fi
126
127    # If rdisc6 exits early we still want to wait for the deadline before retrying
128    (( timeout = curr_dl - SECONDS ))
129    sleep $(( timeout < 0 ? 0 : timeout ))
130done
131