#!/bin/sh # vim: set ts=4 et: # This script should be installed as symlinks in # /etc/udhcpc//eth-eni-hook # :- # post-bound - after udhcpc binds an IP to the interface # post-renew - after udhcpc renews the lease for the IP # # udhcpc provides via ENV... # IFACE - eth0, etc. # mask - bits in IPv4 subnet mask set -e HOOK="$(basename "$(dirname "$0")")" DEBUG= log() { [ -z "$DEBUG" ] && [ "$1" = "debug" ] && return FACILITY="daemon.$1" shift logger -s -p "$FACILITY" -t "udhcpc/${HOOK}[$$]" -- "$@" } if [ -z "$IFACE" ] || [ -z "$mask" ]; then log err "Missing 'IFACE' or 'mask' ENV from udhcpc" exit 1 fi # route table number RTABLE="${IFACE#eth}" let RTABLE+=1000 IMDS=X-aws-ec2-metadata-token IMDS_IP=169.254.169.254 IMDS_MAC="http://$IMDS_IP/latest/meta-data/network/interfaces/macs/$( cat "/sys/class/net/$IFACE/address")" get_imds_token() { IMDS_TOKEN="$(echo -ne \ "PUT /latest/api/token HTTP/1.0\r\n$IMDS-ttl-seconds: 60\r\n\r\n" | nc "$IMDS_IP" 80 | tail -n 1)" } mac_meta() { wget -qO - --header "$IMDS: $IMDS_TOKEN" "$IMDS_MAC/$1" 2>/dev/null \ || true # when no ipv6s attached (yet), IMDS returns 404 error } ip() { FAIL_OK= if [ "$1" = '+F' ]; then FAIL_OK=1 shift fi v=-4 if [ "$1" = '-4' ] || [ "$1" = '-6' ]; then v="$1" shift fi OP="$2" [ "$OP" = show ] && LEV=debug || LEV=info if /sbin/ip "$v" "$@" || [ -n "$FAIL_OK" ]; then log "$LEV" "OK: ip $v $*" else log err "FAIL: ip $v $*" fi } iface_ip4s() { ip -4 addr show "$IFACE" secondary | sed -E -e '/inet /!d' -e 's/.*inet ([0-9.]+).*/\1/' } iface_ip6s() { ip -6 addr show "$IFACE" scope global | sed -E -e '/inet6/!d' -e 's/.*inet6 ([0-9a-f:]+).*/\1/' } ec2_ip4s() { get_imds_token # NOTE: metadata for ENI arrives later than hotplug events TRIES=60 while true; do IP4="$(mac_meta local-ipv4s)" [ -n "$IP4" ] && break let TRIES-- if [ "$TRIES" -eq 0 ]; then log err "Unable to get IPv4 addresses for $IFACE after 30s" exit 1 fi sleep 0.5 done IP4S="$(echo "$IP4" | tail +2)" # secondary IPs (udhcpc handles primary) # non-eth0 interfaces need custom route tables # if [ "$IFACE" != eth0 ] && [ -n "$IP4S" ] && [ -z "$(ip +F -4 route show table "$RTABLE")" ]; then IP4P="$(echo "$IP4" | head -1)" # primary IP IP4_CIDR="$(mac_meta subnet-ipv4-cidr-block)" IP4_GW="$(echo "$IP4_CIDR" | cut -d/ -f1 | awk -F. '{ print $1"."$2"."$3"."$4+1 }')" ip -4 route add default via "$IP4_GW" dev "$IFACE" table "$RTABLE" ip -4 route add "$IP4_CIDR" dev "$IFACE" proto kernel scope link \ src "$IP4P" table "$RTABLE" fi echo "$IP4S" } ec2_ip6s() { get_imds_token # NOTE: IPv6 metadata (if any) may arrive later than IPv4 metadata TRIES=60 while true; do IP6S="$(mac_meta ipv6s)" [ -n "$IP6S" ] && break let TRIES-- if [ "$TRIES" -eq 0 ]; then log warn "Unable to get IPv6 addresses for $IFACE after 30s" break fi sleep 0.5 done # non-eth0 interfaces need custom route tables # # NOTE: busybox iproute2 doesn't do 'route show table' properly for IPv6, # so iproute2-minimal package is required! # if [ "$IFACE" != eth0 ] && [ -n "$IP6S" ] && [ -z "$(ip +F -6 route show table "$RTABLE")" ]; then TRIES=20 while true; do GW="$(ip -6 route show dev "$IFACE" default | awk '{ print $3 }')" [ -n "$GW" ] && break let TRIES-- if [ "$TRIES" -eq 0 ]; then log warn "Unable to get IPv6 gateway RA after 10s" break fi sleep 0.5 done ip -6 route add default via "$GW" dev "$IFACE" table "$RTABLE" fi echo "$IP6S" } in_list() { echo "$2" | grep -q "^$1$" } # ip_addr {4|6} {add|del} ip_addr() { [ "$1" -eq 6 ] && MASK=128 || MASK="$mask" # IP6s are always /128 ip -"$1" addr "$2" "$3/$MASK" dev "$IFACE" [ "$IFACE" = eth0 ] && return # non-eth0 interfaces get rules associating IPs with route tables ip -"$1" rule "$2" from "$3" lookup "$RTABLE" } # sync_ips {4|6} "" "" sync_ips() { # remove extra IPs for i in $3; do in_list "$i" "$2" || ip_addr "$1" del "$i" done # add missing IPs for i in $2; do in_list "$i" "$3" || ip_addr "$1" add "$i" done } log info "STARTING: $IFACE" if [ "$HOOK" != post-bound ] && [ "$HOOK" != post-renew ]; then log err "Unhandled udhcpc hook '$HOOK'" exit 1 fi sync_ips 4 "$(ec2_ip4s)" "$(iface_ip4s)" sync_ips 6 "$(ec2_ip6s)" "$(iface_ip6s)" log info "FINISHED: $IFACE"