20 March 2023

Whitelisting a Dynamic IP in with iptables

I recently got a Shelly EM to monitor the electric usage at my house. Shelly sends data over MQTT, and I intend to run a MQTT server on a small cloud server to receive it. However, I am not comfortable with an MQTT server open to the wider internet. To avoid this, I'd like to configure iptables to allow only traffic from my home ip address.


My home IP address is dynamic and can change at any time. No IP to the rescue! No IP provides a service which maps a domain name to the dynamic IP. It does this with a short TTL DNS record, but also provides an API to update the address when it changes. My home router has a built in integration with No IP, so it can automatically update the IP when it changes.

Updating iptables

Iptables works on IP addresses, not hostnames, so when the IP changes, iptables needs to be updated. In an earlier post I described how to setup iptables to filter traffic for Docker containers. In short, we have a iptables chain called WHITELIST-IP. The intention, is for this chain to hold a single IP and have it ACCEPT the traffic. To make things work, we need to do 3 things:

  1. Resolve the no ip hostname to give the current IP of my home internet service.
  2. Query iptables to find the current whitelist IP if any.
  3. If there is no whitelisted IP or the current IP does not match the one in iptables, flush the chain and add a new entry.

The first is a simple DNS lookup.

The second, can be achieved by running iptables -nL WHITELIST-IP -t filter and looking for lines like:

ACCEPT     all  --  109.xxx.xxx.xxx

Finally, update iptables:

iptables -F WHITELIST-IP
iptables -A WHITELIST-IP -s #{new_address} -j ACCEPT

Putting this all together in a short Ruby script looks like below. Simply schedule this in cron, and the dynamic IP will be whitelisted in iptables anytime it changes.

require "resolv"

hostname = "hidden.ddns.net"

def existing_iptables_address
  # iptables -nL WHITELIST-IP -t filter
  # Chain WHITELIST-IP (1 references)
  # target     prot opt source               destination
  # ACCEPT     all  --
  output = `iptables -nL WHITELIST-IP -t filter`
  unless $?.success?
    raise "failed to run iptables command successfully"

  output.each_line do |l|
    if l =~ /^ACCEPT/
      parts = l.split(/\s+/, 5)
      return parts[3]

def update_iptables(new_address)
  puts "Switching the whitelist IP to #{new_address}"
  system "iptables -F WHITELIST-IP"
  system "iptables -A WHITELIST-IP -s #{new_address} -j ACCEPT"

  current_ip = Resolv.getaddress(hostname)
  whitelist_ip = existing_iptables_address
  if whitelist_ip.nil? || current_ip != whitelist_ip
    update_iptables current_ip
rescue Resolv::ResolvError => e
  puts "Failed to get the address: #{e}"
blog comments powered by Disqus