How to run a script when there is a change in your local IP?

Before marking as a duplicate: this is not about dynamic DNS or similar things.

I know how to run a script every time my networks goes up; it is a simple matter of adding a script in the directory /etc/NetworkManager/dispatcher.d like this one:

#!/bin/bash

IF=$1
STATUS=$2

case "$2" in
        up)
        logger -s "NM Script up $IF triggered"
        su rmano -c /home/romano/bin/myscript 
        ;;
        down)
        logger -s "NM Script down $IF triggered"
        ;;
        *)
        ;;
esac

In my case, myscript is a very simple ifconfig -a > ~/Dropbox/myifconfig.txt --- I use it because I need to know my local IP from everywhere in the University, and it will change often.

So far so good, the system works ok. But unfortunately the DHCP here is configured so that the IP sometime is changed without a down/up of the interface. In that case the script is (logically) not run, and the file is not updated.

I cannot use a DynDNS approach because the change is in the local IP, not the externally visible one.

I could poll, by simply putting the script in cron and execute it every minute or write a slightly more complex one (...if the IP is changed write the file otherwise do nothing) and putting it again as a background task, but it is not elegant. So the question is:

Is there any way to trigger a script when my local IP changes?

UPDATE 1

I tried to put a script in /etc/dhcp/dhclient-enter-hooks.d/ , based on the existing /etc/dhcp/dhclient-enter-hooks.d/resolvconf, but it will not be triggered. I suspect (confirmation needed) that NM (network manager) is doing the dhcp negotiation by itself, without calling the dhcp command...


Solution 1:

According to the man page for NetmorkManager, one of the events is

dhcp4-change
          The DHCPv4 lease has changed (renewed, rebound, etc).

I think you can simply change

up) 

to

dhcp4-change|up)

Solution 2:

I am providing a script that listens on dbus signals, which will allow you to react faster than if you were to poll for changes on your current network configuration. It helps on systems where scripts /etc/ are not executed when you would like them to (like on my 14.04 system).

my enter/exit hooks.d don't work

NetworkManager starts dhclient with the flag -sf /usr/lib/NetworkManager/nm-dhcp-client.action which seems to override the normal enter/exit hook behaviour. The default behavior with dhclient is to call scripts in /etc/dhcp/dhclient-{enter,exit}-hooks.d. Those don't get called at all on my system.

my NetworkManager dispatcher.d scripts don't work either

NM does however invoke a different set of scripts, in /etc/NetworkManager/dispatcher.d, to inform of various events. The NetworkManager (8) man page defines dhcp4-change and dhcp6-change actions which would seem to do exactly what you want. Despite what the manpage says, on my system at least, only up and down actions get invoked. I can't get those scripts to fire on anything else. So this is not a great avenue to monitor IP changes either.

so, snoop directly on dbus signals emitted by NM

nm-dhcp-client.action (source), from the command line, simply converts all the environment variables set by dhclient into a dbus signal. Those environment variables are defined in man dhclient-script (8). One of particular interest is $new_ip_address. What you could do, as suggested by @Bernhard, is to monitor the signal and act accordingly based on its contents.

Here is a program that will snoop all event data signalled by that binary:

#!/bin/bash -e

#
# This script listens for the org.freedesktop.nm_dhcp_client signal.
# The signal is emitted every time dhclient-script would execute.
# It has the same contents as the environment passed to
# dhclient-script (8). Refer to manpage for variables of interest.
#

# "org.freedesktop.nm_dhcp_client" is an undocumented signal name,
# as far as I could tell. it is emitted by nm-dhcp-client.action,
# which is from the NetworkManager package source code.
# 

# detail: todo cleanup subprocess on exit. if the parent exits, 
#       the subprocess will linger until it tries to print
#       at which point it will get SIGPIPE and clean itself.
#       trap on bash's EXIT signal to do proper cleanup.


mkfifo /tmp/monitor-nm-change

(
    dbus-monitor --system "type='signal',interface='org.freedesktop.nm_dhcp_client'"
) > /tmp/monitor-nm-change &

exec </tmp/monitor-nm-change
rm /tmp/monitor-nm-change

while read EVENT; do
    #change this condition to the event you're interested in
    if echo "$EVENT" | grep -q BOUND6; then
        # do something interesting
        echo "current ipv6 addresses:"
        ip addr show | grep inet6
    fi
done

The output of dbus-monitor is not straightforward to parse in scripts. Perhaps it is easier to trigger on the presence of a certain keyword(s), e.g. new_ip_address, and from there use different tools to get the information that changed (e.g. ip or ifconfig).

# example output data from dbus-monitor for that signal
...
dict entry(
string "new_routers"
variant             array of bytes "192.168.2.11"
)
dict entry(
string "new_subnet_mask"
variant             array of bytes "255.255.255.0"
)
dict entry(
string "new_network_number"
variant             array of bytes "192.168.2.0"
)
dict entry(
string "new_ip_address"
variant             array of bytes "192.168.2.4"
)
dict entry(
string "pid"
variant             array of bytes "12114"
)
dict entry(
string "reason"
variant             array of bytes "REBOOT"
)
dict entry(
string "interface"
variant             array of bytes "eth0"
)
...

Give it a shot!

Solution 3:

Polling approach with python script. Basic idea is to continuously parse output of ip -4 -o add show <INTERFACE> and compare current result with previous iteration

#!/usr/bin/env python3
import subprocess
import sys

def get_ip():
    # Simple function that parses output
    # of ip command and returns interface ip
    # replace wlan7 with your interface
    command = 'ip -4 -o addr show wlan7'.split()
    ip = None
    try:
        ip = subprocess.check_output(command).decode().split()[3]
    except IndexError:
        return
    finally:
        if ip:
           return ip

def main():
    # do while loop
    # Exits only when change occurs
    address = get_ip()
    while address == get_ip():
          address = get_ip()

    # Trigger script once we're out of loop
    subprocess.call(['zenity','--info','--text','IP CHANGED'])


if __name__ == '__main__':
    # use while loop if yout want this script to run
    # continuously
    while True:
        try:
            main()
        except KeyboardInterrupt:
            sys.exit()

Solution 4:

Although NetworkManager is using dhclient, it provides its own binaries as a replacement for the dhclient-scripts. (For reference: you can find the NM binary at /usr/lib/NetworkManager/nm-dhcp-client.action).

Maybe you could take a different approach: NM is issuing an DBus signal on all events. You could listen on the system DBus for the appropiate event and trigger your script based on this...