Dumb AP - Associated Stations Resolver

Hi everyone,

I have created a script for myself that would populate both /etc/hosts and /etc/ethers reliably so the Associated Stations listings would always contain at least an IP and a hostname if possible.

This is what it looks like:

Requirements

To use it, it requires the package

arp-scan

to be installed. It will use pretty basic commands, which should be available on even the tiniest of devices, otherwise. The arp-scan-database is not required for this script.

How it works

It uses arp-scan to get a list of stations on your entire network, both LAN and WLAN, to fill up /etc/ethers. It then uses the contents of that file to question your (local) DNS server for hostname information using nslookup.

NOTE: If you don't have a local DNS server on your network, this script will fail to find any FQDN's on the hosts in your network by default. Not having DHCP addresses registered with the local DNS will also make it impossible to resolve anything on an IP, expect the hosts file to remain unchanged if that happens...

Install using the CLI

Copy the following into a file, save it to /root/. I would suggest a name like 'stationinfo.sh', I will be using it in this example. Type 'vi stationinfo.sh' right after logging in. All commands I give should be followed by an enter, obviously.

#!/bin/sh

if [ ! -e "/tmp/etc/ethers" ]; then
	if [ -L "/etc/ethers" ]; then
		touch /tmp/etc/ethers
	else
		mv /etc/ethers /tmp/etc/ethers
	fi
	ln -s /tmp/etc/ethers /etc/ethers
fi

if [ ! -e "/tmp/etc/hosts" ]; then
	if [ -L "/etc/hosts" ]; then
		touch /tmp/etc/hosts
	else
		mv /etc/hosts /tmp/etc/hosts
	fi
	ln -s /tmp/etc/hosts /etc/hosts
fi

# Any custom hosts need to be added here
cat > /etc/hosts <<- EOM
# NOTE: THIS HOSTS FILE WILL BE OVERWRITTEN BY $(readlink -f $0)
127.0.0.1 localhost

::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

# Automatically added adresses will be added below:
EOM

LANST=$(ifstatus lan)
LANADDR=`echo "$LANST" |  jsonfilter -e '@["ipv4-address"][0].address'`
INTERFACE=`echo "$LANST" |  jsonfilter -e '@.device'`
LANNET=`ip r | grep "link\s\+src $LANIP" | awk -F ' ' '{print $1}'`
DNSSERVER=`echo "$LANST" |  jsonfilter -e '@["dns-server"][0]'`

arp-scan -q -x -I $INTERFACE $LANNET | awk -F' ' '{print $2 " " $1}' > /etc/ethers

for IPADDR in `cat /etc/ethers | awk -F' ' '{print $2}'`; do

	FQDN=`nslookup $IPADDR $DNSSERVER | grep -m 1 arpa | awk -F' ' '{print $4}'`
	if [ $FQDN != "find" ]; then
		echo "$IPADDR	$FQDN" >> /etc/hosts
	fi

done

Make it executable by typing 'chmod +x stationinfo.sh'.

Now we need to make it run, every once in a while. Type 'crontab -e' in the CLI. Add the following in the editor:

*/5 * * * * /root/stationinfo.sh

This will make it run every 5 minutes.
You can change it to */15 for every 15 minutes or */30 for every 30 minutes. You can also change */5 to a simple 0, this will make it run every hour, on the hour.

It will need to run once during startup so it can create the necessary files and symlinks so it will not be wearing an unrecoverable hole in your ROM storage. Add the following in the editor after typing 'vi /etc/rc.local':

/root/stationinfo.sh

Make sure you type this above the 'exit 0' line.

The vi editor, daunting if you are not accustomed to it, is surprisingly easy to use, once you know how to. When you open a file it will open it in a 'view' mode. It will accept some commands but in general you will not be able to type or paste anything in the console. Hit the letter 'i' to put it in 'insert mode' to be able to type or paste. Hit Esc to escape to command mode again and in the command mode type ':wq!' to save and quit.

EDIT: UPDATED SCRIPT 6th of March 2023. Making use of ifstatus. :innocent: Enjoy folks!

12 Likes

I'm using cron:

arp-scan -qxlN -I br-lan | awk '{print $1}' | xargs fping -q -c1

Is there difference between your script and this code?

2 Likes

Depends on if and how you store this anywhere.

My script fills up /etc/ethers and /etc/hosts and with that it just works.

1 Like

I just tried your one liner script, I see it requires another package (fping) to be installed while mine does not.

It may not be a problem for you, but I have a device with really limited space so I guess that may be the biggest difference and advantage?

2 Likes

Thanks for this!

My Associated Stations list always does have the hostname and IP. When doesn't it have this information?

If you have a 'Dumb AP' with DNS and DHCP disabled, relying on them being provided by your router as example.

That is when the crucial information is not available for luci to display this.

3 Likes

Ah! Hence the "Dumb AP: part of the thread title Ta da! :bulb: The clue train arrives!

Great idea, nice work!

A month later, the NAND Flash is broken due to so many writes.

Don't complicate things and use this method because it works with dnsmasq and odhcpd + Unbound.

  1. Install these two packages only on your APs:

    • arp-scan
    • fping
  2. Add this cron job only on your APs:
    (/etc/crontabs/root or LuCI → System → Scheduled Task)

    # Making hostnames visible across multiple APs every 30 minutes
    */30 * * * * arp-scan -qxlN -I br-lan | awk '{print $1}' | xargs fping -q -c1
    
  3. If you want to suppress cron error message and br-lan entered/left promiscuous mode messages:

    uci set system.@system[0].cronloglevel="9"
    uci set network.@device[0].promisc="1"
    uci commit
    reload_config
    

Thanks to @ronisaacson.

2 Likes

If that is a serious concern, you can move the actual files to tmp and symlink the files to etc. Problem solved.

1 Like

@xNUTx Thanks for the script. FWIW, you can extract the LAN IP easily by using uci show network.lan.ipaddr.

Never too old to learn :sweat_smile:

Thanks for the pointer. I'll look into integrating it.

2 Likes

Sorry, the right call is get instead of show:

# uci get network.lan.ipaddr
10.0.0.1/24
1 Like

Just in case it can help anyone else, I'm using the following script to update three devices connected via WDS to the main AP. It runs with cron every 10 minutes. It only deploy the new files when there is any change.

#!/bin/ash
#shellcheck shell=ash

# Init variables by default
CHECKSUM_DHCP_LEASES="0000"
CHECKSUM_DHCP_LEASES_OLD="ffff"

LEASES_FILE="/tmp/dhcp.leases"
CHECKSUM_FILE="/tmp/dhcp.leases.checksum"
ETHERS_DEPLOY_FILE="/tmp/ethers.deploy"
ETHERS_REMOTE_FILE="/etc/ethers"

REMOTE_USER="root"
REMOTE_SERVERS="nanohd-downstairs.lan nanohd-upstairs.lan re450.lan"

# Checksums dhcp.leases if exits otherwise exit
[ -e $LEASES_FILE ] || exit 1
CHECKSUM_DHCP_LEASES=$(md5sum $LEASES_FILE | cut -d ' ' -f 1)
[ -e $CHECKSUM_FILE ] && CHECKSUM_DHCP_LEASES_OLD=$(cat $CHECKSUM_FILE)

# Verify checksums, if different update ethers files in APs
[ "$CHECKSUM_DHCP_LEASES" != "$CHECKSUM_DHCP_LEASES_OLD" ] && {
    echo "$CHECKSUM_DHCP_LEASES" > $CHECKSUM_FILE
    awk '{print $2" "$4"\n"$2" "$3}' < $LEASES_FILE > $ETHERS_DEPLOY_FILE

    # Deploy files to APs
    for remote_server in $REMOTE_SERVERS; do
        scp -O $ETHERS_DEPLOY_FILE $REMOTE_USER@"$remote_server":$ETHERS_REMOTE_FILE

        if [ $? -eq 0 ]; then
            logger -t "update_ap_ethers_file" "Deployed ethers file to $remote_server"
        else
            logger -t "update_ap_ethers_file" "Failed deploying ethers file to $remote_server"
        fi
    done
}

exit 0
4 Likes

I have found a caveat in using that command: you will need to be using a static address on your lan interface... if you use a dhcp assigned IP (like I have, through static leases) it will fail:

root@WAX206:~# uci get network.lan.ipaddr
uci: Entry not found
root@WAX206:~# ip addr show dev br-lan
10: br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 80:cc:9c:eb:fd:f7 brd ff:ff:ff:ff:ff:ff
    inet 192.168.xxx.xxx/24 brd 192.168.128.255 scope global br-lan
       valid_lft forever preferred_lft forever

So I won't be changing my script, as the current way of doing it makes sure it works either way.

1 Like

Because I did not think of this as a possible issue with my script. I have modified it to make use of the /tmp filesystem (for those who do not know, it is a filesystem kept in RAM instead of storage).

The consequence is that it will require the script to run once during bootup using rc.local to make sure the target files exist and everything works as you would expect.

I will be update the OP in the next couple of minutes.

OP Updated.

1 Like

Please consider this:

LANST=$(ifstatus lan)
echo "$LANST" |  jsonfilter -e '@["ipv4-address"][0].address' # gives LAN IP
echo "$LANST" |  jsonfilter -e '@["ipv4-address"][0].mask' # gives LAN netmask
echo "$LANST" |  jsonfilter -e '@.device' # gives INTERFACE
...
4 Likes

Interesting approach. Too bad it does not also have a 'network' based on the IP and MASK, that would complete all the info I would need without 'complicated' modifications of output...

For LANNET you need to do some math, putting 0 at the end is not enough.
As a quick workaround I can propose this:
ip r | grep "link\s\+ src $LANIP" | awk -F ' ' '{print $1}'

4 Likes

For arp-scan replacing the 4th position with a 0 is enough, as long as the netmask is right. But I love your solution, the chance of it failing is smaller then the way I have done it.

Thank you for these suggestions. I will include them in my script.

EDIT: OP script updated, integrated the suggestions by @AndrewZ

2 Likes