20 March 2023
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.
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:
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 0.0.0.0/0
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 -- 109.158.126.154 0.0.0.0/0
output = `iptables -nL WHITELIST-IP -t filter`
unless $?.success?
raise "failed to run iptables command successfully"
end
output.each_line do |l|
if l =~ /^ACCEPT/
parts = l.split(/\s+/, 5)
return parts[3]
end
end
nil
end
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"
end
begin
current_ip = Resolv.getaddress(hostname)
whitelist_ip = existing_iptables_address
if whitelist_ip.nil? || current_ip != whitelist_ip
update_iptables current_ip
end
rescue Resolv::ResolvError => e
puts "Failed to get the address: #{e}"
end