|
| 1 | +#!/bin/sh |
| 2 | +# DHCPv6 client state update script for odhcp6c |
| 3 | +# This script expects a system with resolvconf (openresolv) and iproute2 |
| 4 | + |
| 5 | +[ -z "$1" ] && echo "Error: should be called from odhcp6c" && exit 1 |
| 6 | + |
| 7 | +interface="$1" |
| 8 | +state="$2" |
| 9 | +RESOLV_CONF="/run/resolvconf/interfaces/${interface}-ipv6.conf" |
| 10 | +NTPFILE="/run/chrony/dhcp-sources.d/${interface}-ipv6.sources" |
| 11 | + |
| 12 | +[ -n "$metric" ] || metric=5 |
| 13 | + |
| 14 | +log() |
| 15 | +{ |
| 16 | + logger -I $$ -t odhcp6c -p user.notice "${interface}: $*" |
| 17 | +} |
| 18 | + |
| 19 | +dbg() |
| 20 | +{ |
| 21 | + logger -I $$ -t odhcp6c -p user.debug "${interface}: $*" |
| 22 | +} |
| 23 | + |
| 24 | +err() |
| 25 | +{ |
| 26 | + logger -I $$ -t odhcp6c -p user.err "${interface}: $*" |
| 27 | +} |
| 28 | + |
| 29 | +teardown_interface() |
| 30 | +{ |
| 31 | + ip -6 route flush dev "$interface" |
| 32 | + ip -6 address flush dev "$interface" scope global |
| 33 | +} |
| 34 | + |
| 35 | +setup_interface() |
| 36 | +{ |
| 37 | + # Merge RA addresses with DHCP addresses |
| 38 | + for entry in $RA_ADDRESSES; do |
| 39 | + duplicate=0 |
| 40 | + addr="${entry%%/*}" |
| 41 | + for dentry in $ADDRESSES; do |
| 42 | + daddr="${dentry%%/*}" |
| 43 | + [ "$addr" = "$daddr" ] && duplicate=1 |
| 44 | + done |
| 45 | + [ "$duplicate" = "0" ] && ADDRESSES="$ADDRESSES $entry" |
| 46 | + done |
| 47 | + |
| 48 | + # Add addresses |
| 49 | + for entry in $ADDRESSES; do |
| 50 | + addr="${entry%%,*}" |
| 51 | + entry="${entry#*,}" |
| 52 | + preferred="${entry%%,*}" |
| 53 | + entry="${entry#*,}" |
| 54 | + valid="${entry%%,*}" |
| 55 | + |
| 56 | + ip -6 address add "$addr" dev "$interface" preferred_lft "$preferred" valid_lft "$valid" proto dhcp |
| 57 | + log "assigned address $addr (preferred=$preferred, valid=$valid)" |
| 58 | + done |
| 59 | + |
| 60 | + # Add routes from RA |
| 61 | + for entry in $RA_ROUTES; do |
| 62 | + addr="${entry%%,*}" |
| 63 | + entry="${entry#*,}" |
| 64 | + gw="${entry%%,*}" |
| 65 | + entry="${entry#*,}" |
| 66 | + valid="${entry%%,*}" |
| 67 | + entry="${entry#*,}" |
| 68 | + metric="${entry%%,*}" |
| 69 | + |
| 70 | + if [ -n "$gw" ]; then |
| 71 | + ip -6 route add "$addr" via "$gw" metric "$metric" dev "$interface" from "::/128" |
| 72 | + else |
| 73 | + ip -6 route add "$addr" metric "$metric" dev "$interface" |
| 74 | + fi |
| 75 | + |
| 76 | + # Add routes for delegated prefixes |
| 77 | + for prefix in $PREFIXES; do |
| 78 | + paddr="${prefix%%,*}" |
| 79 | + [ -n "$gw" ] && ip -6 route add "$addr" via "$gw" metric "$metric" dev "$interface" from "$paddr" |
| 80 | + done |
| 81 | + done |
| 82 | +} |
| 83 | + |
| 84 | +handle_prefixes() |
| 85 | +{ |
| 86 | + # $PREFIXES format: "prefix/len,preferred,valid[,class=N][,excluded=...] ..." |
| 87 | + for entry in $PREFIXES; do |
| 88 | + addr="${entry%%,*}" |
| 89 | + entry="${entry#*,}" |
| 90 | + preferred="${entry%%,*}" |
| 91 | + entry="${entry#*,}" |
| 92 | + valid="${entry%%,*}" |
| 93 | + |
| 94 | + log "received delegated prefix $addr (preferred=$preferred, valid=$valid)" |
| 95 | + |
| 96 | + # Add unreachable route to prevent routing loops |
| 97 | + ip -6 route add unreachable "$addr" 2>/dev/null |
| 98 | + |
| 99 | + # Future: Distribute to downstream interfaces |
| 100 | + done |
| 101 | +} |
| 102 | + |
| 103 | +handle_dns() |
| 104 | +{ |
| 105 | + truncate -s 0 "$RESOLV_CONF" |
| 106 | + |
| 107 | + # Combine DHCPv6 DNS ($RDNSS) and RA DNS ($RA_DNS), deduplicating |
| 108 | + all_dns="" |
| 109 | + for server in $RDNSS $RA_DNS; do |
| 110 | + # Simple deduplication: only add if not already in list |
| 111 | + case " $all_dns " in |
| 112 | + *" $server "*) ;; |
| 113 | + *) all_dns="$all_dns $server" ;; |
| 114 | + esac |
| 115 | + done |
| 116 | + |
| 117 | + # Domain search list (DHCPv6 option 24) |
| 118 | + if [ -n "$DOMAINS" ]; then |
| 119 | + dbg "adding search domains: $DOMAINS" |
| 120 | + echo "search $DOMAINS # $interface" >> "$RESOLV_CONF" |
| 121 | + fi |
| 122 | + |
| 123 | + # DNS servers |
| 124 | + for server in $all_dns; do |
| 125 | + [ -z "$server" ] && continue |
| 126 | + dbg "adding dns $server" |
| 127 | + echo "nameserver $server # $interface" >> "$RESOLV_CONF" |
| 128 | + done |
| 129 | + |
| 130 | + if [ -n "$all_dns" ]; then |
| 131 | + resolvconf -u |
| 132 | + fi |
| 133 | +} |
| 134 | + |
| 135 | +handle_ntp() |
| 136 | +{ |
| 137 | + # DHCPv6 option 56 (NTP server) is provided as $OPTION_56 in hex format |
| 138 | + # Format: sub-option-code (2 bytes) + length (2 bytes) + data |
| 139 | + # Sub-option 1 = NTP server address (16 bytes IPv6) |
| 140 | + # |
| 141 | + # This is complex to parse in shell. For now, we attempt basic parsing |
| 142 | + # and fall back to logging a warning if the format is unexpected. |
| 143 | + |
| 144 | + if [ -n "$OPTION_56" ]; then |
| 145 | + # Remove all non-hex characters (spaces, colons, etc.) and convert to lowercase |
| 146 | + hex=$(echo "$OPTION_56" | tr -d '[:space:]:-' | tr '[:upper:]' '[:lower:]') |
| 147 | + |
| 148 | + truncate -s 0 "$NTPFILE" |
| 149 | + ntp_found=0 |
| 150 | + |
| 151 | + # Parse option 56: iterate through sub-options |
| 152 | + # Each sub-option: 2 bytes code + 2 bytes length + data |
| 153 | + pos=0 |
| 154 | + while [ $pos -lt ${#hex} ]; do |
| 155 | + # Need at least 4 hex chars (2 bytes) for sub-option code |
| 156 | + [ $((${#hex} - pos)) -lt 4 ] && break |
| 157 | + |
| 158 | + # Extract sub-option code (2 bytes = 4 hex chars) |
| 159 | + subopt_code=$(echo "$hex" | cut -c $((pos+1))-$((pos+4))) |
| 160 | + pos=$((pos + 4)) |
| 161 | + |
| 162 | + # Need 4 more hex chars for length |
| 163 | + [ $((${#hex} - pos)) -lt 4 ] && break |
| 164 | + |
| 165 | + # Extract length (2 bytes = 4 hex chars) |
| 166 | + subopt_len_hex=$(echo "$hex" | cut -c $((pos+1))-$((pos+4))) |
| 167 | + subopt_len=$(printf "%d" "0x$subopt_len_hex") |
| 168 | + pos=$((pos + 4)) |
| 169 | + |
| 170 | + # Sub-option 1 = NTP server address (should be 16 bytes for IPv6) |
| 171 | + if [ "$subopt_code" = "0001" ] && [ "$subopt_len" -eq 16 ]; then |
| 172 | + # Extract 16 bytes (32 hex chars) for IPv6 address |
| 173 | + addr_hex=$(echo "$hex" | cut -c $((pos+1))-$((pos+32))) |
| 174 | + |
| 175 | + # Convert hex to IPv6 address format |
| 176 | + # Format: 0123456789abcdef0123456789abcdef -> 0123:4567:89ab:cdef:0123:4567:89ab:cdef |
| 177 | + ipv6=$(echo "$addr_hex" | sed 's/\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)/\1:\2:\3:\4:\5:\6:\7:\8/') |
| 178 | + |
| 179 | + dbg "got NTP server $ipv6" |
| 180 | + echo "server $ipv6 iburst" >> "$NTPFILE" |
| 181 | + ntp_found=1 |
| 182 | + fi |
| 183 | + |
| 184 | + # Skip this sub-option's data |
| 185 | + pos=$((pos + subopt_len * 2)) |
| 186 | + done |
| 187 | + |
| 188 | + if [ "$ntp_found" -eq 1 ]; then |
| 189 | + chronyc reload sources >/dev/null 2>&1 |
| 190 | + else |
| 191 | + dbg "option 56 received but no NTP server addresses found (consider using option 31/SNTP)" |
| 192 | + fi |
| 193 | + fi |
| 194 | +} |
| 195 | + |
| 196 | +log "state: $state" |
| 197 | + |
| 198 | +( |
| 199 | + flock 9 |
| 200 | + case "$state" in |
| 201 | + started) |
| 202 | + # Initial state - clean up any stale config |
| 203 | + teardown_interface |
| 204 | + ;; |
| 205 | + |
| 206 | + bound) |
| 207 | + # Fresh lease - tear down and set up from scratch |
| 208 | + teardown_interface |
| 209 | + setup_interface |
| 210 | + handle_prefixes |
| 211 | + handle_dns |
| 212 | + handle_ntp |
| 213 | + ;; |
| 214 | + |
| 215 | + informed|updated|rebound|ra-updated) |
| 216 | + # Update existing configuration |
| 217 | + setup_interface |
| 218 | + [ -n "$PREFIXES" ] && handle_prefixes |
| 219 | + handle_dns |
| 220 | + handle_ntp |
| 221 | + ;; |
| 222 | + |
| 223 | + unbound|stopped) |
| 224 | + # Lost server or client stopped |
| 225 | + teardown_interface |
| 226 | + rm -f "$RESOLV_CONF" |
| 227 | + rm -f "$NTPFILE" |
| 228 | + resolvconf -u |
| 229 | + chronyc reload sources >/dev/null 2>&1 |
| 230 | + ;; |
| 231 | + esac |
| 232 | +) 9>/tmp/odhcp6c.lock.${interface} |
| 233 | +rm -f /tmp/odhcp6c.lock.${interface} |
| 234 | + |
| 235 | +# Run hooks |
| 236 | +HOOK_DIR="/usr/libexec/odhcp6c.d" |
| 237 | +for hook in "${HOOK_DIR}/"*; do |
| 238 | + [ -f "${hook}" -a -x "${hook}" ] || continue |
| 239 | + "${hook}" "$interface" "$state" |
| 240 | +done |
| 241 | + |
| 242 | +exit 0 |
0 commit comments