8

I want to limit access to a server to certain IPs using iptables but:

  • One of the IPs is dynamic, a normal ISP home connection which changes from time to time.
  • A subdomain e.g. dynamic.example.org is automatically updated when the IP changes using a similar service to dyndns.

Is it possible to have IPtables allow access to a port if dynamic.example.org resolves to that IP?

My current idea is to set up a systemd unit that periodically resolves dynamic.example.org and adjusts iptables accordingly. However, this also requires knowing the old IP address (so storing it somewhere) to remove it from the whitelist.

Is there a simpler way to do this already built in to iptables?

2
  • 2
    Use two ipsets and swap them. Oct 25, 2018 at 20:03
  • 1
    @MichaelHampton's answer, with some embellishment, should be THE answer, IMHO. Create an ipset, and reference that in your iptables rules. Your iptables rules will never change, but you can have an asynchronous process (i.e. a script run out of cron, or similar) that periodically looks up the address, and updates the ipset as needed. Updates in this way require no reloading of iptables rules - ipsets can be updated on-the-fly.
    – guzzijason
    Oct 25, 2018 at 20:40

3 Answers 3

4

iptables works on IP addresses, not on hostnames. You can use hostnames as arguments, but they will be resolved at the time the command is entered. Doing a DNS lookup for each passing packet would be much too slow.

Your idea to adjust the rules is therefore the only approach. This can be either at a regular schedule, controlled by a program like systemd or cron, or better if you can manage to receive a notification whenever the IP address changes.

You don't have to store the old address, just make an iptables chain for your rule and replace the rule. See the -R option to iptables. To have a rule to replace on the first check, just add a dummy rule so that there will be a rule to replace when the first check runs.

You can also avoid the extra chain and replace a rule at a specific position in INPUT or FORWARD, but that is much more work to maintain, as the position number changes whenever you add or delete rules.

3
  • 1
    Saying it would be much too slow is actually understating just how bad it is. It would cost a roundtrip to the DNS server for every packet being processed. That extra roundtrip is bad enough in itself. But it's also introducing cyclic dependencies. You'd have the IP layer depending on responses from the application layer. What if those DNS requests/responses themselves were to go through rules which rely on DNS lookups? And even worse, what if the DNS requests/responses were to go through more than one such rule? You could end up with an exponential explosion of the number of packets.
    – kasperd
    Oct 25, 2018 at 21:36
  • 2
    And where does the packet go while waiting for the DNS lookup? This layer of the stack is not supposed to do that kind of buffering. And if a buffer was to get introduced at this layer, how is it going to deal with DNS requests or responses getting lost? By the time iptables notice the DNS response isn't coming back, the packet which triggered the DNS lookup in the first place may very well have been retransmitted. So updating the rules is a much better path to take. Of course that still leaves open the question on whether you trust DNS lookups enough to let them change your firewall rules.
    – kasperd
    Oct 25, 2018 at 21:39
  • 1
    Thanks, yes it would be slow but I'd expect some caching of hostname/ips if this existed as a feature in iptables. As for trust, obviously it's an additional layer of security and not entirely secure by itself. I'm using this for limiting MySQL and SSH access, even if someone can gain access to adjust the DNS record, they still need the username/password. Completely secure? No, but it stops anyone from even seeing the service is running unless they have first adjusted the dns record. More secure than leaving the service visible to everyone, at least.
    – Tom B
    Oct 26, 2018 at 8:58
7

The way I do this is:

  • Run a script every x minutes from crontab to update an "ipset"
  • Have IPtables use the ipset

Assuming you have only 1 IP address in this ipset, the following script would do:

#!/bin/bash
# Update ipset to let my dynamic IP in
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

set=whitelist
host=myhost.dynamic.example.com

me=$(basename "$0")

ip=$(dig +short $host)

if [ -z "$ip" ]; then
    logger -t "$me" "IP for '$host' not found"
    exit 1
fi

# make sure the set exists
ipset -exist create $set hash:ip

if ipset -q test $set $ip; then
    logger -t "$me" "IP '$ip' already in set '$set'."
else 
    logger -t "$me" "Adding IP '$ip' to set '$set'."
    ipset flush $set
    ipset add $set $ip
fi

In crontab I call this script every 5 minutes :

*/5 * * * * root /usr/local/bin/ipset-update-dyn

In iptables, the rule using the ipset looks like this :

-A INPUT -p tcp -m set --match-set whitelist src -m state --state NEW -j ACCEPT
0

Even simpler using dig:

iptables -I INPUT -p tcp -s $(dig +short yourdomain.ddns.net) -m state --state NEW -m tcp -j ACCEPT

Explanation:

The dig +short yourdomain.ddns.net is launched in a subprocess returning your IPv4 IP. The output of this subprocess is passed on to the iptables binary adding a new rule.

Note that we used -I to prepend the rule to all others, if you want to append use -A

iptables -A INPUT -p tcp -s $(dig +short yourdomain.ddns.net) -m state --state NEW -m tcp -j ACCEPT

Update:

As MartinV states, if you keep on adding rules you could end up having a lot of duplicate rules and also reach the rule limit capacity.

The best approach would be to create a chain to hold your DDNS rules and just flush the custom chain before adding your new rule.

Create a custom chain if it doesn't exist, prepend your custom chain myDDNS to the INPUT chain to prevent being blocked by the final general REJECT in the INPUT chain and flush the chain:

iptables -N myDDNS 2>/dev/null;iptables -I INPUT -j myDDNS;iptables -F myDDNS

If the chain exists an error will be raised but the custom chain won't be duplicated. Redirecting the error to /dev/null will prevent the error from being thrown on STDIN.

Prepend your rule to your custom chain:

iptables -I myDDNS -p tcp -s $(dig +short yourdomain.ddns.net) -m state --state NEW -m tcp -j ACCEPT

Just put everything together in a one liner and call the script from the cron every N minutes.

iptables -N myDDNS 2>/dev/null;iptables -I INPUT -j myDDNS;iptables -F myDDNS && iptables -I myDDNS -p tcp -s $(dig +short yourdomain.ddns.net) -m state --state NEW -m tcp -j ACCEPT

Adding && between the iptables -F (flush) and the rule prepending will make sure that the rule is added only if the flush command returns 0

1
  • It works but not really in the OP's use case. Repeated, periodic, iterations will fill up the iptables chain and it doesn't provide a way to retire expired addresses.
    – MartinV
    Nov 29, 2021 at 12:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .