Skip to content

Instantly share code, notes, and snippets.

@akorn

akorn/find-interface

Last active May 19, 2020
Embed
What would you like to do?
A script to be called from `pre-up` in `interfaces(5)`: it examines a set of configured candidate interfaces to find the one that's plugged into the network you want, then renames it to a descriptive name you've chosen. Use case: you have many similar physical interfaces and don't want to keep track of which cable you plug into which.
#!/bin/zsh
#
# Copyright (c) 2019-2020 András Korn. License: GPLv3
#
# Take a set of network interfaces and determine which one is plugged into a specific network. Then rename the interface.
#
# This script supplies some generic functions for that purpose; the actual logic comes from configuration (you need to override the detect() function and perhaps the last_resort() function).
#
# The script is concurrency capable. It processes candidate interfaces simultaneously, obtaining a lock for each; other instances skip locked interfaces and add them to the end of their queue.
# Several instances can run concurrently with overlapping candidate POOLs.
#
# Usage: find-interface target-name
#
# Functions provided (all implement some heuristic that can help determine what network the interface is plugged into):
#
# * count_hosts() runs nmap -sn (ping scan) against the subnet of the IP address specified for the interface and prints the number of hosts that replied to a ping
# * count_hosts_between(a b) returns true if the number returned by count_hosts was in the range [a-b] (inclusive)
# * can_ping(IP) returns true if $iface receives ping replies from $IP. Uses oping.
# * sees_traffic_from(addr1 [ ... ]) returns true if $iface receives ethernet packets with any of the given MACs or IPv4 IPs as the source address. Uses tcpdump.
# * has_link() returns true if $iface detects the link as 'up' ('/sys/class/net/$iface/carrier' is '1')
# * link_speed_is(speed) returns true if $iface's link speed is $speed (e.g. 10, 100, 1000)
#
# All functions rely on the global variable $iface being set to the name of the current candidate interface to be examined. While it'd be arguably cleaner to pass the name of the interface in as an argument, it would make the configuration unnecessarily verbose.
# can_ping and count_hosts add an IP (specified in configuration) to the candidate interface if it doesn't have one.
#
# The script exits successfully if it found the requisite interface and renamed it; it exists unsuccessfully if it couldn't find the interface.
#
# It can be used from /etc/network/interfaces as follows (the example assumes you don't have an interface called 'uplink' yet, but want to):
#
# auto uplink
# iface uplink inet static
# pre-up find-interface uplink
# address ...
#
#
# Configuration (/etc/default/find-interface) example:
######################################################
# POOL=(eth0 eth1 ... ) # array of candidate interfaces; if you can, you should rearrange the list in decreasing order of likelihood based on $target
# PING_TIMEOUT=0.5 # how many seconds to wait for ping replies
# TCPDUMP_TIMEOUT=10 # how many seconds to wait for traffic to come (from the set of configured MACs/IPs)
# SKIP_CONFIGURED=1 # if 1, skip interfaces that already have IPs assigned. If 0, reconfigure them as needed
# LINK_TIMEOUT=13 # wait up to this many seconds for the interfaces to acquire a link
#
# ip_addr[intra]=192.168.0.111/24 # will be used like "ip addr add ${=ip_addr[intra]} dev current-if-being-tried" in can_ping and count_hosts
# ip_addr[uplink]="1.2.3.4 peer 4.5.6.7" # example of a point-to-point address specification
# force_speed[intra]=1000 # use ethtool to force speed to 1000 (WARNING: will set this speed on all interfaces it tries before finding 'intra')
# wait_for_link[intra]=1 # wait for link to go up before attempting further tests?
# wait_for_link[uplink]=1
#
# function detect() { # this is the actual logic; should return 0 if $iface matches $target
# # at this point $iface exists, is up and has link (if wait_for_link[$target]=1)
# case $target in
# uplink) has_link && can_ping 4.5.6.7;;
# intra) count_hosts_between 4 10 || sees_traffic_from 11:22:33:44:55:66 aa:bb:cc:dd:ee:ff 192.168.0.42;;
# *) return 1;;
# esac
# }
#
# function last_resort() { # called if no interface was matched using the heuristics so you can implement some desperate guess, such as relying on nameif(8) and the MAC address
# case $target in
# uplink) nameif 12:34:56:78:90:ab uplink;;
# intra) nameif de:ad:be:ef:d0:0d intra;;
# *) return 1;;
# esac
# }
######################################################
target=$1 # what interface we're looking for
CONFIG=/etc/default/find-interface
POOL=($(cd /sys/class/net; echo eth[0-9](N) en*(N) em*(N)))
PING_TIMEOUT=0.5 # how many seconds to wait for ping replies
TCPDUMP_TIMEOUT=10 # how many seconds to wait for traffic to come (from the set of configured MACs)
SKIP_CONFIGURED=1 # if 1, skip interfaces that already have IPs assigned. If 0, reconfigure them as needed
LINK_TIMEOUT=13 # wait up to this many seconds for the interface to acquire a link
LOG_LEVEL_NAMES=(emerg alert crit err warning notice info debug) # we also use these in syslog messages, so we have to use these specific level names
LOG_LEVEL=debug
USE_SYSLOG=1
me=${0:t}:$1
typeset -A ip_addr
typeset -A force_speed
typeset -A wait_for_link
function log() { # Usage: log <level> <message>. Consults $USE_SYSLOG.
# Prints message on stderr and optionally logs it to syslog.
# Depends on "$me" being set to the name of the script being executed.
# When logging to syslog, a facility of "daemon" is currently hardcoded.
local level=$1
local level_index=${LOG_LEVEL_NAMES[(ie)$level]}
shift # $@ holds the message now
if ((${LOG_LEVEL_NAMES[(ie)$LOG_LEVEL]}>=level_index)); then # is the message of high enough priority to be logged?
if ((USE_SYSLOG)); then
logger --stderr --tag "${me:-$0}" --id=$$ --priority daemon.$level -- "$@"
else
echo "$level: $@" >&2
fi
fi
}
function count_hosts() {
if_exists $iface || { echo 0; log info "count_hosts: $iface doesn't exist; skipping"; return 1 } # doesn't exist
is_up $iface || { echo 0; log info "count_hosts: $iface is not up; skipping"; return 2 } # not up
has_ipv4 $iface || add_ip # no IP; add if we can
local count=$(nmap -sn -oG - $(netmask $(ifdata -pN $iface)/$(ifdata -pn $iface)) | grep -c 'Status: Up')
log info "count_hosts: counted $count hosts up on $iface."
echo $count
}
function count_hosts_between() { local count=$(count_hosts $iface); [[ $1 -le $count ]] && [[ $2 -ge $count ]] }
function can_ping() {
local dest=$1
if_exists $iface || { log info "can_ping: $iface doesn't exist; skipping"; return 1 } # doesn't exist
is_up $iface || { log info "can_ping: $iface is not up; skipping"; return 2 } # not up
has_ipv4 $iface || add_ip
if oping -Z0 -D $iface -c1 -w${PING_TIMEOUT:-0.5} $dest >/dev/null 2>/dev/null; then
log notice "can_ping: successfully pinged $dest on $iface."
return 0
else
log info "can_ping: no ping response from $dest on $iface."
return 1
fi
}
function sees_traffic_from() {
if_exists $iface || { log info "sees_traffic_from: $iface doesn't exist; skipping"; return 1 } # doesn't exist
is_up $iface || { log info "sees_traffic_from: $iface is not up; skipping"; return 2 } # not up
local -a macs ips
local pcap_filter i
while [[ -n $1 ]]; do
[[ $1 =~ : ]] && macs=($macs[@] ${=1}) || ips=($ips[@] ${=1})
shift
done
for i in $macs[@]; do
[[ -z "$pcap_filter[@]" ]] && pcap_filter="(ether src host $i)" && continue
pcap_filter="$pcap_filter or (ether src host $i)"
done
for i in $ips[@]; do
[[ -z "$pcap_filter[@]" ]] && pcap_filter="(ip src host $i)" && continue
pcap_filter="$pcap_filter or (ip src host $i)"
done
log debug "sees_traffic_from: using pcap filter '$pcap_filter'"
if timeout --signal=TERM --kill-after=1 ${TCPDUMP_TIMEOUT:-10} tcpdump -c1 -i $iface "$pcap_filter" >/dev/null 2>/dev/null; then
log notice "sees_traffic_from: $iface saw traffic matching '$pcap_filter', is likely $target"
return 0
else
log info "sees_traffic_from: tcpdump on $iface timed out without receiving traffic matching '$pcap_filter'; $iface is probably not $target"
return 1
fi
}
function has_link() {
if_exists $iface || { log info "has_link: $iface doesn't exist; skipping"; return 1 }
is_up $iface || { log info "has_link: $iface is not up; skipping"; return 2 }
if (($(</sys/class/net/$iface/carrier))); then
log notice "$iface has link"
return 0
else
log debug "$iface doesn't have link"
return 1
fi
}
function link_speed_is() {
if_exists $iface || { log info "link_speed_is: $iface doesn't exist; skipping"; return 1 }
local speed=$(</sys/class/net/$iface/speed)
if [[ $speed = $1 ]]; then
log notice "link_speed_is: $iface link speed is $speed, as expected for $target"
return 0
else
log info "link_speed_is: $iface link speed is $speed; expected $1 for $target"
fi
}
function add_ip() {
local errmsg
if [[ -n "${=ip_addr[$target]}" ]]; then
if errmsg=$(ip addr add ${=ip_addr[$target]} dev $iface 2>&1); then
log info "add_ip: 'ip addr add ${=ip_addr[$target]} dev $iface' succeeded"
return 0
else
log err "add_ip: 'ip addr add ${=ip_addr[$target]} dev $iface' failed with '$errmsg'"
return 1
fi
else
log info "add_ip: No IP given in configuration for $target. You might want to add a line like ip_addr[$target]=192.168.42.42/24 to $CONFIG unless you know you don't need one."
return 1
fi
}
function remove_ip() {
local errmsg
if [[ -n "${=ip_addr[$target]}" ]]; then
if errmsg=$(ip addr del ${=ip_addr[$target]} dev $iface 2>&1); then
log info "remove_ip: 'ip addr del ${=ip_addr[$target]} dev $iface' succeeded"
return 0
else
log err "remove_ip: 'ip addr del ${=ip_addr[$target]} dev $iface' failed with '$errmsg'"
return 1
fi
else
return 0 # no IP to remove; make the call to remove_ip a no-op
fi
}
function is_up() {
local iplink
if_exists $1 || { log info "is_up: $1 doesn't exist; skipping"; return 1 }
iplink=($(ip link sh dev $1))
if [[ $iplink =~ ,UP ]]; then
log info "is_up: $1 appears to be up."
return 0
else
log info "is_up: $1 doesn't seem to be up. 'ip link show dev $1' says: '$iplink'."
return 1
fi
}
function if_exists() { [[ -e /sys/class/net/$1 ]] }
function has_ipv4() {
local ifdata
if_exists $1 || { log info "has_ipv4: $1 doesn't exist; skipping"; return 1 }
ifdata=$(ifdata -pa $1)
if [[ $ifdata =~ ^[0-9.]+$ ]]; then
log info "has_ipv4: $1 seems to have an ipv4 address ('$ifdata')."
return 0
else
log info "has_ipv4: $1 doesn't seem to have an ipv4 address; 'ifdata -pa $1' says '$ifdata'."
return 1
fi
}
function bring_up() { if_exists $iface || return 1; ip link set up dev $iface } # most interfaces will need some time to really be up
function bring_down() { if_exists $iface || return 0; ip link set down dev $iface; remove_ip }
function wait_for_link() {
local count=0
if_exists $iface || { log info "wait_for_link: $iface doesn't exist; skipping"; return 1 }
log info "wait_for_link: waiting up to $LINK_TIMEOUT seconds for $iface to establish link."
[[ -n $force_speed[$target] ]] && {
ethtool -s $iface autoneg off
sleep 0.5
ethtool -s $iface speed $force_speed[$target]
}
while ! has_link && (((count++)/2<=LINK_TIMEOUT)) && ! if_exists $target; do
zselect -t 50
done
if if_exists $target; then # another thread found the target, we can exit
log notice "wait_for_link: $target already exists; not waiting for link to appear on $iface any longer"
has_link
return $?
elif has_link; then # ensure we return link status
[[ -n $force_speed[$target] ]] && { # with some drivers, these settings need to be readjusted after link beat detection
ethtool -s $iface autoneg off
sleep 0.5
ethtool -s $iface speed $force_speed[$target]
}
log info "wait_for_link: returning success as $iface has link."
return 0
else
log info "wait_for_link: $iface still doesn't have link after waiting $LINK_TIMEOUT seconds."
return 1
fi
}
function renameif() { if_exists $1 && ip link set name $2 dev $1 }
function detect() { # override this from config; the script is useless until you do. See beginning for example.
log warning "detect(): not overridden in $CONFIG, returning failure."
return 1
}
function last_resort() { # if none of the heuristic detections returned a positive result, last_resort is called. You could call nameif(8) from it, for example, to make a choice based on the MAC address.
log info "last_resort(): not overridden in $CONFIG, returning failure."
return 1
}
[[ -r $CONFIG ]] && . $CONFIG
zmodload zsh/zselect # we use zselect to sleep instead of /bin/sleep
zmodload zsh/system # for locking
if if_exists $target; then # target interface already exists, nothing to do
log notice "interface $target already exists. Nothing to do; exiting."
exit 0
fi
while [[ -n "$POOL[1]" ]]; do
iface=$POOL[1]
shift POOL
if_exists $iface || continue
log info "Considering $iface to see if it's $target."
LOCKFILE=/run/lock/find-interface.$iface.lock
: >>$LOCKFILE
if zsystem flock -f lockfd -t 0 "$LOCKFILE" 2>/dev/null; then
if_exists $target && exit 0 # target interface already came into existence, nothing to do
is_up $iface && has_ipv4 $iface && ((SKIP_CONFIGURED)) && continue # don't mess with configured interfaces
bring_up
{
if ((wait_for_link[$target])); then
if ! wait_for_link; then
log info "$iface failed to come up; skipping."
bring_down
continue # if still no link, we skip this interface
fi
fi
if if_exists $target; then # target interface found by other thread, exit cleanly
log info "$target found by other thread; exiting thread processing $iface"
bring_down
zsystem flock -u $lockfd
exit 0
fi
if detect; then
log info "Success! $iface is $target. Renaming."
bring_down
renameif=$(renameif $iface $target 2>&1)
zsystem flock -u $lockfd
ret=$?
if ((ret)); then
log err "Failed to rename $iface to $target. Renameif said '$renameif' and returned $ret."
exit $ret
else
log notice "$iface renamed to $target. All done; exiting."
exit 0
fi
else
bring_down
log info "$iface is not $target. Moving on to next interface."
fi
rm $LOCKFILE
zsystem flock -u $lockfd
} &
else
zselect -t 50 # avoid spinning in a busy loop if there are several instances of us waiting for the same interface(s)
POOL=$(POOL[@] $iface) # couldn't obtain lock as another find-interface instance was working on the interface; re-add it at the end of the queue.
fi
done
wait
if_exists $target && exit 0
# not reached if one of the heuristics succeeded
log notice "Heuristic detection failed. Calling last_resort() to obtain $target."
last_resort
if if_exists $target; then
log notice "last_resort() produced $target. Exiting."
exit 0
fi
# not reached if last_resort succeeded
log err "Not even last_resort() produced $target. Exiting."
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment