Post

Avoiding Future VPN Disruptions; IP Monitoring with Python

During my vacation, I ran into a frustrating issue: my VPN connection to my home server stopped working after an ISP outage. This was because my home’s WAN IP address had changed, which meant the VPN client couldn’t reconnect to my server anymore. Since I rely heavily on my home server for automation and other services, this was an issue I didn’t want to experience again. So, after some thought, I decided to write a script that would notify me whenever my WAN IP address changes.

Lost Connection: The Problem

In our household, I’ve automated nearly everything, from lights and appliances to notifications and security. To manage all of these services, I run several servers in a hypervisor environment. Some servers handle my home automation apps, while others are used for testing purposes, like experimenting with networking, firewalls, and forensics. All of these servers are isolated from the internet and are only accessible via my local home network.

To ensure I could monitor these systems remotely, I set up a VPN server that would allow me to connect to my home network securely while I was away. The VPN client on my phone connects via the WAN IP address, routing my connection back home.

This setup had been working perfectly, until this summer. On the second day of my vacation, I realized my phone couldn’t connect to the internet when the VPN was enabled. Disabling the VPN restored connectivity, but the VPN logs showed a handshake failure whenever the client attempted to connect.

A quick Google search confirmed that my ISP had an ongoing outage. After several hours, internet service was restored, but my VPN still wouldn’t connect. I suspected that my WAN IP address had changed, but since I was far from home and had no way to access my network, I couldn’t verify this. I had to wait until I returned from vacation to check.

Sure enough, when I got home, I found that my WAN IP address had changed, which explained why the VPN failed to connect. To prevent this from happening again, I decided to create a script that would notify me whenever my WAN IP changed. I could have used an external service like DuckDNS to manage dynamic IP addresses, but I wanted full control over the entire process. Plus, I wanted to try to script in Python, and this looked like a good purpose.

IP Monitoring: The Solution

First, I set up a headless Raspberry Pi to run the script. I found a simple Python script that checked the WAN IP by sending a request to https://api.ipify.org. The script uses the requests module to fetch the current WAN IP: WanIP.py

1
2
3
4
5
import requests  

def get_wanIP():
	response = requests.get('https://api.ipify.org').text
	return response

With this working, I wrote a script that compared the current WAN IP with the one stored in a SQL database. If the IP changed, the script would log the new IP address and send me a notification via Telegram. Here’s the Python code that checks the current IP: IP_Processing.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from IPAddress import IPAddress
import requests
from IPDatabase import *

def get_currentIPAddress() -> str:
	response = requests.get('https://api.ipify.org').text
	return response

def get_latestIPAddressFromDatabase() -> str:
	IPAddress = get_latestIPAddress()
	return IPAddress.ip

def isCurrentIPChanged() -> bool:
	publicIP = get_currentIPAddress()
	latestKnowIP = get_latestIPAddressFromDatabase()

	if publicIP != latestKnowIP:
		return True
	elif publicIP == latestKnowIP:
		return False

The database management is handled with SQLite. Here’s the code for inserting and retrieving IP addresses from the database: IPDatabase.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from contextlib import nullcontext
import sqlite3
import IPAddress
import Telegram
from sqlite3 import Error
import os.path

def create_connection():
    conn = None
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    db_path = os.path.join(BASE_DIR, "IP.db")

    try:
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row
    except Error as e:
        print(e)
    print("Opened database successfully")

    return conn

def insert_newIPAddress(IPAddress):
    conn = create_connection()
    
    values_to_insert = [IPAddress.ip, IPAddress.date]

    query = "INSERT INTO main.IP(ip,date) VALUES(?,?);"

    conn.execute(query, values_to_insert)

    conn.commit()
    print("Records created successfully")
    conn.close()

def get_latestIPAddress():
    conn = create_connection()

	    IPObject = conn.execute("SELECT ID,ip,date FROM main.IP ORDER BY ID DESC;").fetchone()
    
    latest_IP = IPAddress.IPAddress()
    latest_IP.ID = IPObject[0]
    latest_IP.ip = IPObject[1]
    latest_IP.date = IPObject[2]

    if latest_IP is not None:
        return latest_IP.ip
        print("Record loaded successfully")
    else:
        print("Record loaded failed")
        return "1.1.1.1"
    conn.close()

The main script for checking if the WAN IP has changed. If it has, it logs the new IP address and sends a notification:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from datetime import datetime
import IP_Processing
from IPAddress import IPAddress
import time

import Telegram

def main():
     interval = 5

     isIPChanged = IP_Processing.isCurrentIPChanged()

     if isIPChanged == True:
	     IP = IPAddress()
         IP.ip = IP_Processing.get_currentIPAddress()
         IP.date = datetime.today().strftime('%Y-%m-%d %H:%M:%S')
         print(f"IP-address changed to {IP.ip}")

	     IP_Processing.insert_newIPAddress(IP)
	     
		 Telegram.send_telegram_message(f"IPaddress changed to {IPAddress.ip}")
     elif isIPChanged == False:
          print("IPaddress hasn't changed")
     
if __name__ == "__main__": 

Automated Notifications via Telegram

The script uses the python-telegram-bot library to send notifications to my phone whenever the WAN IP address changes. This ensures that no matter where I am, I’ll be alerted if the IP address changes: Telegram.py

1
2
3
4
5
6
7
8
9
10
from telegram import Bot
import asyncio

chat_id = "<CHAT-ID>"
TOKEN = "<BOT-TOKEN>"

bot = Bot(TOKEN)

def send_telegram_message(message):
    asyncio.run(bot.send_message(chat_id, text=message))

Running the script

As this script doesn’t need to run all the time, I used cron to run it every two hours as the local user. Al the logging is discarded.

1
0 */2 * * *  python3 /home/<USER>/IPmonitor/Main.py >/dev/null 2>&1

Overengineering with a Telegram Bot

In true overengineering fashion, I also created a Telegram bot used the echobot example from python-telegram-bot. This allows me to manually check the current WAN IP by sending a simple /ip message. Here’s how it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
import logging

from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters

from WanIP import get_public_ip

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)

logging.getLogger("httpx").setLevel(logging.WARNING)

logger = logging.getLogger(__name__)

async def ip(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text("Current WAN-ip is " + get_public_ip())

def main() -> None:
    application = Application.builder().token("<BOT-TOKEN>").build()
    application.add_handler(CommandHandler("ip", ip))
    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

Running the Telegram-bot script as a service

To make the Telegram bot script run continuously and respond to requests, I needed to set it up as a systemd service. The idea was that the script should always be active and ready to notify me whenever I requested the current WAN IP address. After some research, I found a great guide on running a program at startup. The relevant section for my setup was “Installing a Service Per User.”

Enabling User Linger

First things first, I needed to enable “linger” for my user account, allowing it to run background services even when the account wasn’t actively logged in. This was a simple command: sudo loginctl enable-linger <USER>

With that done, the local user account was permitted to keep services running persistently, even when logged out, as described in the loginctl documentation.

Creating the .service Directory

Next, I created the directory to hold the service configuration files: mkdir -p ~/.config/systemd/user

Writing the .service File

The service itself is defined in a .service file. Here’s what I wrote to configure the systemd service:

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Telegrambot to monitor WAN IP-address
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/home/monitor/TelegramBot/telegrambot.py
Restart=always

[Install]
WantedBy=default.target
WantedBy=multi-user.target

This file holds the information that systemd needs to operate the service in different sections. The [Unit] section specifies that the bot should start only after the network is fully online. This ensures the bot can communicate via Telegram from the moment the system boots. The [Service] section tells systemd to start the Python script that runs the bot, and to automatically restart it if it crashes. Finally, the [Install] section configures systemd to include this service in the boot process.

Deploying the Service

With the .service file ready, I copied it into the user-specific system directory: $ cp telegrambot.service ~/.config/systemd/user/

Then, I needed to reload systemd’s internal state so it would recognize the new service: $ systemctl --user daemon-reload

Enabling and Starting the Service

Once reloaded, I enabled the service to start at boot, and then manually started it to test the setup: $ systemctl --user enable telegrambot.service $ systemctl --user start telegrambot.service

Checking the status of the service showed that it was up and running smoothly:

1
2
3
4
5
6
7
8
9
10
11
● telegrambot.service - Telegrambot to monitor WAN IP-address  
Loaded: loaded (/home/<USER>/.config/systemd/user/telegrambot.service; enabled; preset: enabled)  
Active: active (running) since Mon 2054-01-01 10:11:12 CEST; 5s ago  
Main PID: 12345 (python3)  
Tasks: 2 (limit: 762)  
CPU: 1.079s  
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/telegrambot.service  
└─12345 python3 /home/<USER>/TelegramBot/telegrambot.py  
  
Jan 01 10:11:12 monitor systemd[12098]: Started telegrambot.service - Telegrambot to monitor WAN IP-address.  
Jan 01 10:11:12 monitor python3[12346]: 2054-01-01 10:11:12,345 - telegram.ext.Application - INFO - Application started

Everything was looking good—my bot was running and waiting for commands!

The Multi-User Target Issue

However, when I rebooted the Raspberry Pi, I realized the script wasn’t running as expected. After some troubleshooting, I found that the WantedBy=multi-user.target line in the .service file was causing the issue. It turns out that services defined with --user don’t work with multi-user.target as they would for system-level services.

1
2
[Install]
WantedBy=default.target

After another reboot, I checked again, and this time, the Telegram bot was running as expected, automatically starting with the system and happily messaging back in the Telegram chat. telegram Fig.1 Telegram messages

Finally

With the service now running reliably at startup, my over-engineered solution was finally complete. The Telegram bot automatically monitors and responds to WAN IP requests, even after system reboots. No more wondering if I’ll lose access to my servers again due to ISP outages! All I have to do is message the bot with /ip, and it promptly delivers the updated IP address. A perfect blend of automation and control. And perhaps a little more than strictly necessary, but that’s how I like it.

Cover Photo by blog.jdriven.com

This post is licensed under CC BY 4.0 by the author.