Keeping up with IP number changes

Last weekend I was at a hotel working on my iPad and launched Prompt to connect to my iMac at home and run a command. It wouldn’t connect. I launched Transmit (which still works well, despite its zombie status) and couldn’t connect there, either. Obviously, my service provider had changed my home IP number. This happens rarely,1 but I didn’t want to be caught like this again.

My first thought was to write a script that would check the IP number periodically and save it in a file on the leancrew.com server. But a bit of searching led me to this script by Charlie Hawker, which checks for the IP number and sends an email if it’s changed, a much better idea.2

There were, however, two things about the script I didn’t like:

1. It’s written in PHP. I don’t disklike PHP—I use it to build this blog—but it’s not a language I’m deeply familiar with, and I’d prefer a Python script so I can easily make changes if necessary.
2. It gets the IP number by loading a website (ip6.me) and scraping it. I wanted something a little more direct.

The solution to the second problem was to use dig, the domain information groper (a program that was undoubtedly named by a man). Here’s a command that does it by returning information from OpenDNS3

dig +short myip.opendns.com @resolver1.opendns.com


With that command in mind, the script, checkip, was easy to write:

python:
1:  #!/usr/bin/python
2:
3:  import smtplib
4:  import subprocess as sb
5:  import os
6:  import sys
7:
8:  # Parameters
9:  ipFile = '{}/.ipnumber'.format(os.environ['HOME'])
10:  mailFrom = 'user@gmail.com'
11:  mailTo = 'user@gmail.com'
12:  gmailUser = 'user'
14:  cmd = '/usr/bin/dig +short myip.opendns.com @resolver1.opendns.com'
15:  msgTemplate = '''From: {}
16:  To: {}
17:  Subject: Home IP number has changed
18:  Content-Type: text/plain
19:
20:  {}
21:  '''
22:
23:  # Get the current IP number and the filed IP number
24:  dig = sb.check_output(cmd.split(), universal_newlines=True)
25:  currentIP = dig.strip()
26:  with open(ipFile, 'r') as ip:
28:
29:  # Update the file and send an email if they differ
30:  if currentIP != oldIP:
31:    with open(ipFile, 'w') as ip:
32:      ip.write(currentIP)
33:
34:    msg = msgTemplate.format(mailFrom, mailTo, currentIP)
35:    smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
36:    smtp.ehlo()
38:    smtp.sendmail(mailFrom, mailTo, msg)


Lines 9–21 set the parameters that are used later in the script. As you can see from Line 9, I save the IP number in the hidden file .ipnumber in my home directory. Line 14 is the dig command we just talked about. Lines 15–21 is the template for the email that the script sends when the IP number change, and Lines 10–13 are the personal settings needed to send the email.

Lines 24–25 run the dig command4 and store its result (after stripping the trailing newline) in the currentIP variable. Lines 26–27 read the .ipnumber file, which contains the IP number from the last time the script was run, and store the contents in oldIP.

Line 30 then compares the two numbers and continues the script only if they differ. Lines 31–32 puts the new IP number into the .ipnumber file, and Lines 34–38 build an email message and send it via GMail. This script works only if you have a GMail account—if you don’t, you’ll have to tweak these lines to get your email service to send the message.

The email it sends is nothing fancy: a subject line telling me the IP number has changed and a body with the new number (which I’ve obscured in this screenshot).

To avoid an error the first time this script is run, create the .ipnumber file and put any old IP number in it. I seeded mine with an incorrect value, 11.11.11.11, so I could make sure the file changed and the email was sent when the script ran.

I decided to have the script run every hour. If I were using Linux, I’d set this up through cron, but the (more complicated) Mac way is with launchd. Here’s the plist file that controls the running of the script. It’s called com.leancrew.checkip.plist and is saved in my ~/Library/LaunchAgents directory:

xml:
1:  <?xml version="1.0" encoding="UTF-8"?>
2:  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3:  <plist version="1.0">
4:  <dict>
5:    <key>Label</key>
6:    <string>com.leancrew.checkip</string>
7:    <key>ProgramArguments</key>
8:    <array>
9:      <string>/Users/drang/Dropbox/bin/checkip</string>
10:    </array>
11:    <key>StartCalendarInterval</key>
12:    <array>
13:      <dict>
14:        <key>Minute</key>
15:        <integer>12</integer>
16:      </dict>
17:    </array>
18:  </dict>
19:  </plist>


The key entries here are Line 9, which gives the full path to the checkip script, and Lines 11–17, which tell launchd to run the script at 12 minutes past the hour.5 Because there are no Hour, Day, or Month entries in this section, it runs every hour of every day of every month.

Because of where it’s stored, this Launch Agent gets loaded into the launchd system every time I log in. To get it to load without logging out and logging back it, I ran these two commands:6

launchctl load ~/Library/LaunchAgents/com.leancrew.checkip.plist
launchctl start com.leancrew.checkip


With it in the system, the agent will continue to run as long as I’m logged in. For me, this is essentially forever, as I keep this computer running all the time specifically so I can log into it whenever I need to.

I’ve thought of improving the script by adding an error-handling section to alert me when the dig command fails. This would let me know, for example, if OpenDNS has stopped running its service. But the possibility of being inundated by false positive emails has kept me from trying that out.

1. I managed to get the command run by connecting to my work computer, where the IP number is fixed.

2. Yes, I know there are services that will do this sort of thing. I don’t want to rely on them.

3. This does rely on OpenDNS, but if it ever shuts down or curtails this service, there will always be another similar way to get my IP number. I’ll just change my script to use that instead.

4. Note: I typically write scripts in Python 3 nowadays, where the subprocess module is considerably different. This script was easier to do in the Python 2 that comes with macOS because it gets run by the launchd system, which uses an environment that doesn’t know where my installation of Python 3 is. Rather than telling it, I decided to just write it for Python 2. You could call this the \$PATH of least resistance.

5. Why 12 minutes? No particular reason. Any number under 60 would work just as well.

6. The load and start subcommands are now considered legacy features of launchctl and will probably be removed in the future. But for now they still work, so I’ll worry about learning the new subcommands later.