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