Last active
February 25, 2022 20:04
-
-
Save akorn/7b96e78c7d1b3ca70e35261f9b1a2f2b to your computer and use it in GitHub Desktop.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/zsh | |
# | |
# Copyright (c) 2019-2022 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 | |
typeset -A logged_time # When was each message last logged? If within the last 5 seconds, we suppress it on the console. | |
zmodload zsh/datetime | |
[[ -S /dev/log ]] || USE_SYSLOG=0 | |
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? | |
((USE_SYSLOG)) && logger --tag "${me:-$0}" --id=$$ --priority daemon.$level -- "$@" | |
if [[ $[EPOCHSECONDS-0$logged_time[$@]] -gt 5 ]]; then | |
echo "$level: $@" >&2 | |
logged_time[$@]=$EPOCHSECONDS | |
fi | |
fi | |
} | |
function count_hosts() { | |
if if_exists $target; then | |
log notice "count_hosts: $target found by different thread; investigation of $iface exiting early" | |
return 1 # target interface already came into existence; we can exit early | |
fi | |
if_exists $iface || { echo 0; log info "count_hosts: $iface doesn't exist; skipping"; return 1 } # doesn't exist | |
is_up $iface || return 2 # { 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 if_exists $target; then | |
log notice "can_ping: $target found by different thread; investigation of $iface exiting early" | |
return 1 # target interface already came into existence; we can exit early | |
fi | |
if_exists $iface || { log info "can_ping: $iface doesn't exist; skipping"; return 1 } # doesn't exist | |
is_up $iface || return 2 # { 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 if_exists $target; then | |
log notice "sees_traffic_from: $target found by different thread; investigation of $iface exiting early" | |
return 1 # target interface already came into existence; we can exit early | |
fi | |
if_exists $iface || { log info "sees_traffic_from: $iface doesn't exist; skipping"; return 1 } # doesn't exist | |
is_up $iface || return 2 # { 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 | |
if if_exists $target; then | |
log notice "sees_traffic_from: $target found by different thread; investigation of $iface exiting early" | |
else | |
log info "sees_traffic_from: tcpdump on $iface timed out without receiving traffic matching '$pcap_filter'; $iface is probably not $target" | |
fi | |
return 1 | |
fi | |
} | |
function has_link() { | |
if_exists $iface || { log info "has_link: $iface doesn't exist; skipping"; return 1 } | |
is_up $iface || return 2 # { 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 | |
[[ "$errmsg" = "RTNETLINK answers: Cannot assign requested address" ]] && return 0 # the IP wasn't there to begin with | |
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 debug "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 if_exists $target; then | |
log notice "wait_for_link: $target found by different thread; investigation of $iface exiting early" | |
return 1 # target interface already came into existence; we can exit early | |
fi | |
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 | |
zselect -t 50 | |
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