# Utility functions for communicating with IPv6 neighbors on the local # LAN # Usage: llping # # Send ping to all-hosts group, with link-local scope, on , # suppressing loopbacked packets. llping() { local iface="$1" shift ping -L "$@" ff02::1%$iface } # Usage: llpeer # # Return the address of the first IPv6 neighbor to respond on # 's local LAN. It is thus typically only useful in scenarios # where only one neighbor exists llpeer() { local iface="$1" shift llping $iface -c1 "$@" | \ awk '/bytes from/ { sub(/:$/, "", $4); print($4); exit(0); }' } llssh_expand() { local old_ifs="$IFS" local orig="$1" local fmt="$2" local peer= local user= local iface= IFS=@ set $1 IFS="$old_ifs" case $# in 1) user="$LLSSH_USER" iface="$1" ;; 2) user="$1" iface="$2" ;; *) echo "Error: Invalid peer \"$orig1\"" >&2 return 1 ;; esac if [ -d "/sys/class/net/$iface" ]; then peer="$(llpeer $iface -w3)" else peer="$iface" fi if [ ! "$peer" ]; then echo "Error: No peer responded on $iface" >&2 return 1 fi case "$fmt" in ssh) ;; scp) peer="[$peer]" ;; *) echo "Internal error: Invalid format \"$fmt\"" >&2 return 1 ;; esac [ "$user" ] && peer="$user@$peer" echo $peer } # Usage: llssh [] # # ssh(1) to , but if the host matches an existing # interface name, expand it to a neighboring host on that interface. llssh() { local remote="$1" shift local peer="$(llssh_expand $remote ssh)" [ "$peer" ] || return 1 local sshpasscmd= [ "$LLSSH_PASS" ] && sshpasscmd="sshpass -p$LLSSH_PASS $LLSSHPASS_OPTS" $sshpasscmd ssh $LLSSH_OPTS $peer "$@" } llscp_expand() { local orig="$1" local old_ifs="$IFS" local peer= IFS=: set $@ IFS="$old_ifs" case $# in 1) echo "$1" ;; 2) peer="$(llssh_expand $1 scp)" [ "$peer" ] || return 1 echo "$peer:$2" ;; *) echo "Error: Invalid location \"$orig1\"" >&2 return 1 ;; esac } # Usage: llscp # # scp(1) from to , but expand any host specified in either # or , which matches an existing interface name, to a # neighboring host on that interface. llscp() { local src="$(llscp_expand $1)" local dst="$(llscp_expand $2)" [ "$src" -a "$dst" ] || return 1 shift 2 local sshpasscmd= [ "$LLSSH_PASS" ] && sshpasscmd="sshpass -p$LLSSH_PASS $LLSSHPASS_OPTS" $sshpasscmd scp $LLSCP_OPTS "$src" "$dst" } # Usage: llscan [-a] [] # # Discover Infix devices (or all mDNS devices with -a) on the LAN. # Prefers mDNS-SD via avahi-browse; falls back to IPv6 link-local # multicast ping if avahi-browse is not available and an interface # is specified. llscan() { local all=0 local iface= while [ $# -gt 0 ]; do case "$1" in -a) all=1 shift ;; *) iface="$1" shift ;; esac done if [ "$iface" ] && [ ! -d "/sys/class/net/$iface" ]; then echo "Error: Interface \"$iface\" does not exist." >&2 return 1 fi if command -v avahi-browse >/dev/null 2>&1; then llscan_mdns "$all" elif [ "$iface" ]; then echo "Note: avahi-browse not found, falling back to link-local scan." >&2 llscan_ll "$iface" else cat >&2 <<-EOF Error: avahi-browse not found. Install avahi-utils to scan for Infix devices via mDNS: sudo apt install avahi-utils Or specify an interface to use IPv6 link-local fallback: $(basename $0) scan EOF return 1 fi } llscan_mdns() { local all="$1" local flags="-tarp" local tmpfile i c pid if avahi-browse --help 2>&1 | grep -q -- '-k'; then flags="-tarpk" fi tmpfile=$(mktemp) avahi-browse $flags 2>/dev/null >"$tmpfile" & pid=$! if [ -t 2 ]; then i=0 while kill -0 "$pid" 2>/dev/null; do case $((i % 4)) in 0) c='-';; 1) c="\\";; 2) c='|';; 3) c='/';; esac printf "\r[%s] Scanning, please wait ..." "$c" >&2 i=$((i + 1)) sleep 0.2 done printf "\r\033[2K" >&2 fi wait "$pid" awk -F';' -v show_all="$all" ' $1 == "=" { host = $7 proto = $3 addr = $8 stype = $5 txt = $10 on = ""; ov = ""; product = ""; devid = ""; vv = "" n = split(txt, parts, "\" \"") for (i = 1; i <= n; i++) { gsub(/"/, "", parts[i]) if (parts[i] ~ /^on=/) { split(parts[i], kv, "="); on = kv[2] } else if (parts[i] ~ /^ov=/) { split(parts[i], kv, "="); ov = kv[2] } else if (parts[i] ~ /^product=/) { split(parts[i], kv, "="); product = kv[2] } else if (parts[i] ~ /^deviceid=/) { split(parts[i], kv, "="); devid = kv[2] } else if (parts[i] ~ /^vv=/) { split(parts[i], kv, "="); vv = kv[2] } } # vv=1 is the platform marker set by confd/services.c, survives OS rebranding. # We require it together with a management service type to avoid false # positives from Apple devices, which also use vv=1 in AirPlay/RAOP records. # on=Infix is kept as a fallback for older firmware that predates vv=1. mgmt = (stype == "_ssh._tcp" || stype == "_sftp-ssh._tcp" || \ stype == "_https._tcp" || stype == "_http._tcp" || \ stype == "_netconf-ssh._tcp" || stype == "_restconf-tls._tcp") if (!show_all && !(vv == "1" && mgmt) && on != "Infix") next # Use deviceid (MAC) as unique key; fall back to hostname key = devid ? devid : host if (!product) product = on ? on : "-" if (!ov) ov = "-" # Prefer IPv4 for display address if (proto == "IPv4") { ipv4[key] = addr } else if (proto == "IPv6" && !ipv6[key]) { ipv6[key] = addr } if (!seen[key]++) { keys[++ndevs] = key hosts[key] = host products[key] = product versions[key] = ov } else if (length(host) > length(hosts[key])) { # Prefer the unique hostname (e.g., infix-c0-ff-ee.local) # over the generic one (e.g., infix.local) hosts[key] = host } } END { if (ndevs == 0) { if (show_all) print "No mDNS devices found." | "cat >&2" else print "No Infix devices found. Use -a to show all mDNS devices." | "cat >&2" exit 1 } fmt = "%-26s %-18s %-24s %-30s\n" hdr = sprintf(fmt, "HOSTNAME", "ADDRESS", "PRODUCT", "VERSION") sub(/\n$/, "", hdr) printf "\033[7m%s\033[0m\n", hdr for (i = 1; i <= ndevs; i++) { k = keys[i] a = (ipv4[k] ? ipv4[k] : (ipv6[k] ? ipv6[k] : "-")) p = products[k]; if (length(p) > 23) p = substr(p, 1, 22) "~" v = versions[k]; if (length(v) > 21) v = substr(v, 1, 20) "~" printf fmt, hosts[k], a, p, v } printf "\n%d device(s) found.\n", ndevs } ' "$tmpfile" rm -f "$tmpfile" } llscan_ll() { local iface="$1" printf "Scanning %s for IPv6 link-local neighbors ...\n\n" "$iface" printf "\033[7m%-6s %s\033[0m\n" "#" "ADDRESS" llping "$iface" -c3 -w3 2>/dev/null | awk ' /bytes from/ { sub(/:$/, "", $4) addr = $4 if (!seen[addr]++) { printf "%-6d %s\n", ++n, addr } } END { printf "\n%d neighbor(s) found on '"$iface"'.\n", n+0 } ' }