# -*-Shell-script-*- # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the MIT License. See the LICENSE accompanying this file # for the specific language governing permissions and limitations under # the License. # This file is not a stand-alone shell script; it provides functions # to ec2 network scripts that source it. # Set up a default search path. PATH="/sbin:/usr/sbin:/bin:/usr/bin" export PATH # metadata query requires an interface and hardware address if [ -z "${INTERFACE}" ]; then exit fi # Support alternate locations for config files in order to facilitate # testing. EC2_ETCDIR=${EC2_ETCDIR:-/etc} SYSFSDIR=${SYSFSDIR:-/sys} HWADDR=$(cat "${SYSFSDIR}"/class/net/${INTERFACE}/address 2>/dev/null) while test "${HWADDR}" = "00:00:00:00:00:00"; do sleep 0.1 HWADDR=$(cat "${SYSFSDIR}"/class/net/${INTERFACE}/address 2>/dev/null) done if [ -z "${HWADDR}" ] && [ "${ACTION}" != "remove" ]; then exit fi export HWADDR METADATA_BASEURL="http://169.254.169.254/latest" METADATA_MAC_PATH="meta-data/network/interfaces/macs" METADATA_TOKEN_PATH="api/token" config_file="$EC2_ETCDIR/sysconfig/network-scripts/ifcfg-${INTERFACE}" route_file="$EC2_ETCDIR/sysconfig/network-scripts/route-${INTERFACE}" route6_file="$EC2_ETCDIR/sysconfig/network-scripts/route6-${INTERFACE}" dhclient_file="$EC2_ETCDIR/dhcp/dhclient-${INTERFACE}.conf" MAINROUTETABLE="yes" function ip() { command "${FUNCNAME[0]}" "$@" } function ifup() { command "${FUNCNAME[0]}" "$@" } function ifdown() { command "${FUNCNAME[0]}" "$@" } function logger() { command "${FUNCNAME[0]}" "$@" } function rm() { command "${FUNCNAME[0]}" "$@" } # shellcheck source=./ec2net-functions-lib . "${EC2_INCLUDEDIR:-/etc/sysconfig/network-scripts}/ec2net-functions-lib" RTABLE=$(get_interface_rt_table "$INTERFACE") if should_sync_interface "${INTERFACE}" "${config_file}"; then if ! should_use_mainroutetable "$config_file"; then MAINROUTETABLE="no" fi else exit 0 fi # get an IMDS key for the interface being managed # parameters: # $1: key-name, e.g. local-ipv4s # $2: max query attempts (optional, default: 60) get_meta() { local key=$1 local max_attempts=${2:-60} logger --tag ec2net "[get_meta] Querying IMDS for ${METADATA_MAC_PATH}/${HWADDR}/${key}" logger --tag ec2net "[get_meta] Getting token for IMDSv2." # IMDS may have become temporarily unreachable, retry. Note that the # token query ignores the max_attempts parameter. The token query # should always be retried. local attempts=60 local imds_exitcode=1 local meta while [ "${imds_exitcode}" -gt 0 ]; do if [ "${attempts}" -eq 0 ]; then logger --tag ec2net "[get_meta] Failed to get IMDSv2 metadata token after ${max_attempts} attempts... Aborting" return $imds_exitcode fi imds_token=$(curl -s -f -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 60" ${METADATA_BASEURL}/${METADATA_TOKEN_PATH}) imds_exitcode=$? if [ "${imds_exitcode}" -gt 0 ]; then let attempts-- sleep 0.5 imds_exitcode=1 fi done # IMDS can take up to 30s to provide the information of a new ENI attempts=${max_attempts} imds_exitcode=1 while [ "${imds_exitcode}" -gt 0 ]; do if [ "${attempts}" -eq 0 ]; then logger --tag ec2net "[get_meta] Failed to get ${METADATA_BASEURL}/${METADATA_MAC_PATH}/${HWADDR}/${key}" return $imds_exitcode fi logger --tag ec2net "[get_meta] Trying to get ${METADATA_BASEURL}/${METADATA_MAC_PATH}/${HWADDR}/${key}" meta=$(curl -s -H "X-aws-ec2-metadata-token:${imds_token}" -f ${METADATA_BASEURL}/${METADATA_MAC_PATH}/${HWADDR}/${key}) imds_exitcode=$? if [ "${imds_exitcode}" -gt 0 ]; then let attempts-- sleep 0.5 imds_exitcode=1 else break fi done echo "${meta}" return $imds_exitcode } get_cidr() { cidr=$(get_meta 'subnet-ipv4-cidr-block') ec=$? echo "${cidr}" return $ec } get_ipv4s() { ipv4s=$(get_meta 'local-ipv4s') ec=$? echo "${ipv4s}" return $ec } get_primary_ipv4() { ipv4s=($(get_ipv4s)) ec=$? echo "${ipv4s[0]}" return $ec } get_secondary_ipv4s() { ipv4s=($(get_ipv4s)) ec=$? if should_provision_prefix_ips "${config_file}"; then ipv4s+=($(get_delegated_ipv4s $dpfx)) fi echo "${ipv4s[@]:1}" return $ec } get_delegated_ipv4s() { for dpfx in $(get_delegated_prefix ipv4); do get_cidr_ipv4s $dpfx done } get_cidr_ipv4s() { local ipv4_cidr=$1 local so1 so2 so3 so4 eo1 eo2 eo3 eo4 local ipv4_start=$(ipcalc -n $ipv4_cidr | cut -f2 -d=) local ipv4_end=$(ipcalc -b $ipv4_cidr | cut -f2 -d=) IFS=. read -r so1 so2 so3 so4 <<< "$ipv4_start" IFS=. read -r eo1 eo2 eo3 eo4 <<< "$ipv4_end" for ((i1=$so1; i1<=$eo1; i1++)); do for ((i2=$so2; i2<=$eo2; i2++)); do for ((i3=$so3; i3<=$eo3; i3++)); do for ((i4=$so4; i4<=$eo4; i4++)); do echo $i1.$i2.$i3.$i4 done done done done } get_delegated_prefix() { local af=$1 # IMDS provides no way of knowing definitively whether or not # there's a prefix delegated to this interface. So when probing # for prefix keys, set the get_meta max attempts parameter to 1 local prefix_tries=1 local prefixes=($(get_meta "${af}-prefix" $prefix_tries|sort -V)) ec=$? echo "${prefixes[@]}" return $ec } get_ipv6s() { ip -6 addr list dev ${INTERFACE} scope global \ | grep "inet6" \ | awk '{print $2}' | cut -d/ -f1 } get_ipv6_gateway() { # Because we start dhclient -6 immediately on interface # hotplug, it's possible we get a DHCP response before we # receive a router advertisement. The only immediate clue we # have about the gateway is the MAC address embedded in the # DHCP6 server ID. If that env var is passed to dhclient-script # we determine the router address from that; otherwise we wait # up to 10 seconds for an RA route to be added by the kernel. # Exported by dhclient: local new_dhcp6_server_id=$new_dhcp6_server_id if echo "$new_dhcp6_server_id" | grep -q "^0:3:0:1:"; then logger --tag ec2net "[get_ipv6_gateway] Using DHCP6 environment variable" octets=($(echo "$new_dhcp6_server_id" | rev | cut -d : -f -6 | rev | tr : ' ')) # The gateway's link local address is derived from the # hardware address by converting the MAC-48 to an EUI-64: # 00:00:5e : 00:53:35 # ^^ ^^^^^ ff:fe is inserted in the middle # first octet is xored with 0x2 (second LSB is flipped) # thus 02:00:5e:ff:fe:00:53:35. # # The EUI-64 is used as the last 64 bits in an fe80::/64 # address, so fe80::200:5eff:fe00:5335. declare -A quibbles # quad nibbles quibbles[0]=$(( ((0x${octets[0]} ^ 2) << 8) + 0x${octets[1]} )) quibbles[1]=$(( 0x${octets[2]}ff )) quibbles[2]=$(( 0xfe00 + 0x${octets[3]} )) quibbles[3]=$(( (0x${octets[4]} << 8) + 0x${octets[5]} )) printf "fe80::%04x:%04x:%04x:%04x\n" "${quibbles[@]}" else logger --tag ec2net "[get_ipv6_gateway] Waiting for IPv6 router advertisement" attempts=20 while true; do if [ "${attempts}" -eq 0 ]; then logger --tag ec2net "[get_ipv6_gateway] Failed to receive router advertisement" return fi gateway6=$(ip -6 route show dev "${INTERFACE}" | grep ^default | awk '{print $3}') if [ -n "${gateway6}" ]; then break else let attempts-- sleep 0.5 fi done echo "${gateway6}" fi } remove_primary() { if [ "${INTERFACE}" == "eth0" ]; then return fi logger --tag ec2net "[remove_primary] Removing configs for ${INTERFACE}" rm -f ${config_file} rm -f ${route_file} rm -f ${route6_file} rm -f ${dhclient_file} } rewrite_primary() { if [ "${INTERFACE}" == "eth0" ]; then return fi logger --tag ec2net "[rewrite_primary] Rewriting configs for ${INTERFACE}" cat <<- EOF > ${config_file} DEVICE=${INTERFACE} BOOTPROTO=dhcp ONBOOT=yes TYPE=Ethernet USERCTL=yes PEERDNS=no IPV6INIT=yes DHCPV6C=yes DHCPV6C_OPTIONS=-nw PERSISTENT_DHCLIENT=yes HWADDR=${HWADDR} DEFROUTE=no EC2SYNC=yes EC2PROVISIONPFXIPS=${EC2PROVISIONPFXIPS} MAINROUTETABLE=${MAINROUTETABLE} EOF rm -f "${route6_file}" "${route_file}" # Use broadcast address instead of unicast dhcp server address. # Works around an issue with two interfaces on the same subnet. # Unicast lease requests go out the first available interface, # and dhclient ignores the response. Broadcast requests go out # the expected interface, and dhclient accepts the lease offer. cat <<- EOF > ${dhclient_file} supersede dhcp-server-identifier 255.255.255.255; timeout 300; EOF } remove_aliases() { logger --tag ec2net "[remove_aliases] Removing aliases of ${INTERFACE}" ip -4 addr flush dev ${INTERFACE} secondary } rewrite_aliases() { # Exported by dhclient: local new_ip_address="${new_ip_address}" local reason="${reason}" if [[ "$reason" == *6 ]]; then # We're processing a DHCPv6 lease and don't use aliases in the # context of IPv6, so there's nothing to. return fi if ! subnet_supports_ipv4 "${new_ip_address}"; then logger --tag ec2net -p user.debug "[rewrite_aliases] $INTERFACE subnet is IPv6-only" return fi aliases=($(get_secondary_ipv4s)) if [ $? -gt 0 ]; then logger --tag ec2net "[rewrite_aliases] Failed to get secondary IPs from IMDS. Aborting" return fi if [ ${#aliases[*]} -eq 0 ]; then remove_aliases return fi logger --tag ec2net "[rewrite_aliases] Rewriting aliases of ${INTERFACE}" # The network prefix can be provided in the environment by # e.g. DHCP, but if it's not available then we need it to # correctly configure secondary addresses. if [ -z "${PREFIX}" ]; then cidr=$(get_cidr) PREFIX=$(echo ${cidr}|cut -d/ -f2) fi [ -n "${PREFIX##*[!0-9]*}" ] || return # Retrieve a list of secondary IP addresses on the interface. # Treat this as the stale list. For each IP address obtained # from metadata, cross it off the stale list if present, or # add it to the interface otherwise. Then, remove any address # remaining in the stale list. declare -A secondaries for secondary in $(ip -4 addr list dev ${INTERFACE} secondary \ |grep "inet .* secondary ${INTERFACE}" \ |awk '{print $2}'|cut -d/ -f1); do secondaries[${secondary}]=1 done for alias in "${aliases[@]}"; do if [[ ${secondaries[${alias}]} ]]; then unset secondaries[${alias}] else ip -4 addr add ${alias}/${PREFIX} brd + dev ${INTERFACE} fi done for secondary in "${!secondaries[@]}"; do ip -4 addr del ${secondary}/${PREFIX} dev ${INTERFACE} done } remove_rules() { if [ "${INTERFACE}" == "eth0" ]; then return fi logger --tag ec2net "[remove_rules] Removing rules for ${INTERFACE}" for rule in $(ip -4 rule list \ |grep "from .* lookup ${RTABLE}" \ |awk -F: '{print $1}'); do ip -4 rule delete pref "${rule}" done for rule in $(ip -6 rule list \ |grep "from .* lookup ${RTABLE}" \ |awk -F: '{print $1}'); do ip -6 rule delete pref "${rule}" done } rewrite_rules_v6() { if [ "${INTERFACE}" == "eth0" ]; then return fi ip6s=($(get_ipv6s)) if [ $? -gt 0 ]; then # If we get an error fetching the list of IPs from IMDS, # bail out early. logger --tag ec2net "[rewrite_rules] Could not get IPv6 addresses for ${INTERFACE} from IMDS. Aborting" return fi ip6s+=($(get_delegated_prefix ipv6)) # This is the part we would do in rewrite_primary() if we knew # the gateway address. if [ ${#ip6s[*]} -gt 0 ] && [ -z "$(ip -6 route show table ${RTABLE})" ]; then gateway6=$(get_ipv6_gateway) # Manually add the route, then add it to ${route6_file} so it # gets brought down with the rest of the interface. ip -6 route add default via ${gateway6} dev ${INTERFACE} table ${RTABLE} cat <<- EOF > ${route6_file} default via ${gateway6} dev ${INTERFACE} table ${RTABLE} EOF fi local -A kernel_rules local -a keys for rule in $(ip -6 rule list \ |grep "from .* lookup ${RTABLE}" \ |awk '{print $1$3}'); do split=(${rule/:/ }) # take care to only replace the first : kernel_rules[${split[1]}]=${split[0]} done for ip in "${ip6s[@]}"; do # keys[0] holds the rule ID # keys[1] holds the source prefix # Note that we can't perform this lookup against the # kernel_rules table we've already constructed because the # format of the PD prefixes returned by IMDS differs from the # one in `ip rule` output. keys=($(ip -6 rule list from "$ip" lookup "$RTABLE" | \ awk '{gsub(":", "", $1); print $1,$3}')) if [ -n "${keys[0]}" ]; then unset kernel_rules[${keys[1]}] else ip -6 rule add from ${ip} lookup ${RTABLE} fi done # Any rules still in the kernel_rules table no longer correspond to # addresses or prefixes assigned to this interface, so delete them: for srcprefix in "${!kernel_rules[@]}"; do ip -6 rule delete from $srcprefix pref "${kernel_rules[${srcprefix}]}" done } rewrite_rules_v4() { if [ "${INTERFACE}" == "eth0" ]; then return fi if ! subnet_supports_ipv4 "$new_ip_address"; then return fi ips=($(get_ipv4s)) if [ $? -gt 0 ]; then # If we get an error fetching the list of IPs from IMDS, # bail out early. logger --tag ec2net "[rewrite_rules] Could not get IPv4 addresses for ${INTERFACE} from IMDS. Aborting" return fi ips+=($(get_delegated_prefix ipv4)) if [ ${#ips[*]} -eq 0 ]; then remove_rules return fi logger --tag ec2net "[rewrite_rules] Rewriting rules for ${INTERFACE}" # Retrieve a list of IP rules for the route table that belongs # to this interface. Treat this as the stale list. For each IP # address obtained from metadata, cross the corresponding rule # off the stale list if present. Otherwise, add a rule sending # outbound traffic from that IP to the interface route table. # Then, remove all other rules found in the stale list. declare -A rules for rule in $(ip -4 rule list \ |grep "from .* lookup ${RTABLE}" \ |awk '{print $1$3}'); do split=(${rule//:/ }) rules[${split[1]}]=${split[0]} done for ip in "${ips[@]}"; do if [[ ${rules[${ip}]} ]]; then unset rules[${ip}] else ip -4 rule add from ${ip} lookup ${RTABLE} fi done for rule in "${!rules[@]}"; do ip -4 rule delete pref "${rules[${rule}]}" done } rewrite_rules() { # export by dhclient: local reason=$reason case "$reason" in "") logger --tag ec2net "rewrite_rules called with no reason" return 1 ;; BOUND|RENEW|REBOOT) rewrite_rules_v4 ;; BOUND6|RENEW6|REBIND6) rewrite_rules_v6 ;; esac return 0 } plug_interface() { logger --tag ec2net "[plug_interface] ${INTERFACE} plugged" rewrite_primary } unplug_interface() { logger --tag ec2net "[unplug_interface] ${INTERFACE} unplugged" remove_rules remove_aliases } activate_primary() { logger --tag ec2net "[activate_primary] Activating ${INTERFACE}" ifup ${INTERFACE} } deactivate_primary() { logger --tag ec2net "[deactivate_primary] Deactivating ${INTERFACE}" ifdown ${INTERFACE} }