Detect Public IP Changes

~9 min read

Jump to end ↓

Detect Public IP Changes thumbnail

Since the dawn of the Internet, this is the most classic issue of all: how to have a permanent footprint and identity on the web, without possessing a fixed public IP.


We have a dedicated server, running somewhere in our house. It is configured as a DMZ, so the router sends all incoming traffic to it. We possess a domain name pointing to our public IP. For example, let’s use deckard.fr.

Reinventing the Wheel

Of course all of this is a bit futile and has been done over and over in many ways. We could use for example ddclient and call it a day. Cloud flare has a comprehensive doc about it. But maybe ddclient does not support your Registrar or you simply do not trust the maintainers or what it does under the hood. The problem is indeed so simple, that you may be suspicious of why you need to use a complicated piece of software to do it. Use your own judgement and treat this piece of blogging as educational and let’s move on.

I am going to present a poll based solution. You server does not know its public IP. It needs to fetch it over the internet to a 3rd party server. I arbitrarily decided to do this every hours, because in theory you do not know when this IP can change.

Practically, it is not true.

Your IP is certainly going to change when your modem / router reboots, or when you lose Internet and it recovers. It is why most routers embed some type of DynDNS support; the name of what we are doing standing for Dynamic Domain Name System. For example, my router runs DD-WRT. The admin console let me inject any script on IP changes, so I could do it there, and the recovery on IP changes would certainly be faster. I think for the purpose of what we are doing here, it is less fun because the tutorial would be directly tied to some specific hardware and versions, making the point less universal.

The Plan

Let’s start with a diagram describing what we are going to do.

[ 3 4 5 6 7 . . . . . / O ( ( h G U ( U ` n M 1 2 o e p f p g [ ( - i . i . m t d o d i R p r i p e a r a t S e u r p T l E / P t t e c s o S [ l r o x u u e d e p l e h r e o i g e s b e u f i i r d S g g g c e l C c I s - v h n v e y g g e u r i l k P h H e o g e c s e e r t / c o a ` o s o r k t r r . e i u r f s k a a e . s s s p I d d i ( t l c A r m t e l P f . l t e o t t c d d i H r S o l f e o d c r i t . m o v c g A a r a i o i f ] e u i r g d r ) & l G l g n o r r r c i e d e o i g n l e p r r ` c t p e s y t . e D c a e u r ) ) s s N o l a s s h s S m h h m o ] ) ] i s t t ` ) [ [ ( [ E I D x n C N ( t t l S R e e o e r r u R G p n n d e i o a e f c t l t l o H i a r u s S I r d b e P e . S r U c y v S A p o n i e P d m c c r I a e e v t d s i e ) c ) e ] ] ]

1. Systemd Timer

We need to run our script periodically and I do not know if you heard, but Cron is not cool anymore. I can appreciate the formalism of Systemd. The syntax is easy to understand and it is not too verbose, so let’s use it. It is also a bit more portable for a Arch based system, as Cron is not installed by default.

I decided to run this script every hours, we could do more, we could do less., I do not know of any rate limits rules on the 3rd party website we are going to use, so in my good conscience, it is what I decided was fair.

Let’s see what’s inside iplogger.timer. I will run all systemd as system (opposed to user), as apparently there is less edge-case to run those script on boot.

[Unit]
Description=Run iplogger.service every hour

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

Self explanatory, no particular gotcha.

2. Systemd Service

Timers need a service to run things, so here we go:

[Unit]
Description=Discover and log public IP

[Service]
Type=oneshot
User=gitea
WorkingDirectory=/home/gitea/git/publiciplogger/
ExecStart=/bin/bash iplogger.sh
ProtectHome=false

[Install]
WantedBy=default.target

There are two little things to know about Systemd here. The first is the ProtectHome setting. By default this setting is set to true. It will prevent you from executing anything in the /home directories. You can spend all the time you want setting and crafting carefully access rights, if this flag is not turned off, nothing will execute.

The second is the WorkingDirectory. I recommend warmly to set it up to avoid any surprises when executing scripts using relative paths (they usually always do!).

Use those 3 commands to check everything is setup correctly:

systemctl daemon-reload
systemctl enable iplogger.service
systemctl enable iplogger.timer
systemctl start iplogger.timer

You can find more doc about the timers on Arch. The most useful commands are:

systemctl list-timers

NEXT                                 LEFT LAST                           PASSED UNIT                             ACTIVATES       >
Sat 2025-08-23 13:00:00 PDT         37min Sat 2025-08-23 12:00:08 PDT 22min ago iplogger.timer                   iplogger.service

Showing you all the timers, when it ran and when it will run again.

Then,

systemctl status iplogger.service

○ iplogger.service - Discover and log public IP
     Loaded: loaded (/etc/systemd/system/iplogger.service; enabled; preset: disabled)
     Active: inactive (dead) since Sat 2025-08-23 12:00:15 PDT; 23min ago
 Invocation: 4226f14d3f914fb895772bc4454f19d1
TriggeredBy: ● iplogger.timer
    Process: 61309 ExecStart=/bin/bash iplogger.sh (code=exited, status=0/SUCCESS)
   Main PID: 61309 (code=exited, status=0/SUCCESS)
   Mem peak: 2.6M
        CPU: 17ms

Aug 23 12:00:08 star-killer systemd[1]: Starting Discover and log public IP...
Aug 23 12:00:08 star-killer bash[61309]: Running public ip check
Aug 23 12:00:15 star-killer bash[61309]: Nothing changed XXX.XXX.XXX.XXX
Aug 23 12:00:15 star-killer systemd[1]: iplogger.service: Deactivated successfully.
Aug 23 12:00:15 star-killer systemd[1]: Finished Discover and log public IP.

This last one is a must use. It shows you the status of a service or a timer, including the logs if you need to debug. Particularly useful to trace access rights issues.

3. Get the Public IP Address

This is the easiest part. The script is written in Bash. I have come to peace with it. After hating it for years, and preferring to write my scripts in Python, the lord spoke to me and I saw the light. Bash is portable and the syntax grew in me. Plus I will not need to manage dependencies and versions for something so simple as doing http calls.

I can show you 3 ways of doing it with wget or dig:

wget -q -O - http://checkip.dyndns.org|sed -e 's/.*Current IP Address: //' -e 's/<.*$//'
wget -q -O - https://checkip.amazonaws.com
wget -q -O - https://icanhazip.com/
dig @1.1.1.1 ch txt whoami.cloudflare +short

This is mostly just for fun: the three examples are public APIs, exposed to the web. The first example show you how to trim the text from the response, to only keep the IP. I am using dyndns in my script because it has been around the longest.

The last example is just here to showcase dig. This little piece of software is very useful to query DNS servers, a bit like nslookup on Windows.

4. Update Cloudflare DNS

We are going to use Cloudflare because it is what I use. But most registrar have an API. It always works the same way: you have some kind of REST interface, a token and here you go. Just craft the request with wget.

From the Cloudflare documentation using Curl:

curl https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID \
    -X PUT \
    -H 'Content-Type: application/json' \
    -H "X-Auth-Email: $CLOUDFLARE_EMAIL" \
    -H "X-Auth-Key: $CLOUDFLARE_API_KEY" \
    -d '{
          "name": "example.com",
          "ttl": 3600,
          "type": "A",
          "comment": "Domain verification record",
          "content": "198.51.100.4",
          "proxied": true
        }'

There is also a batch endpoint.

5. 6. 7. Git workflow

In my script, I save the IP in a file. The script runs inside a git depot. The IP is overwritten every time the script runs, but git will detect a change only if the IP actually changed. If it changed, I commit the change, and I push it back to the origin.

The master depot is hosted using Gitea on the same machine. It allows me to track the history of IP change and the time. This way no need to care about extra logging. Gitea has an extra feature that is awesome: it can mirror git depot to other providers. In this case, I mirror this locally run depot to GitHub.

Indeed, if for some reasons the Cloudflare API rejected my updates (token expired for example, the API changed, etc), I can still recover the IP through Github and update the records manually.

Conclusion


#!/bin/bash

echo "Running public ip check"

OLD_IP=$(<publicip)

# wget -qO- https://checkip.amazonaws.com >./publicip
wget -q -O - http://checkip.dyndns.org | sed -e 's/.*Current IP Address: //' -e 's/<.*$//' >./publicip

NEW_IP=$(<publicip)

if [[ -z "${NEW_IP}" ]]; then
	echo "IP resolve certainly failed because returned empty string. Aborting."
	exit 1
fi

if [[ "$OLD_IP" == "$NEW_IP" ]]; then
	echo "Nothing changed $OLD_IP"
	exit
fi

echo "New IP detected: $OLD_IP -> $NEW_IP."

echo "Updating git"
git add .
git commit -m "New ip detected"
git push

echo "Updating cloud flare"

Just copy paste at the end the Curl call for your DNS provider. The only subtlety in this script is to check for empty NEW_IP which means the resolver failed.

Using Git allows you to trigger a lot of fun stuff from the push hooks. For example I have in my back pocket the idea of doing this clock, or I could plug an alarm of some sorts, sending emails and so on.

So now you know, in order of efficiency go for:

  1. DynDNS on the router
  2. ddclient
  3. Run your own script as I do, just be cool 😎