Synchronising DHCP and DNS on Mikrotik routers

One limitation of Mikrotik‘s DNS and DHCP implementations is that you cannot easily sync hostnames from DHCP requests into DNS. This is a standard feature of many routers. On OpenWRT routers (which use dnsmasq), if a PC called “pc1” requests an IP address, any other PC can then “ping pc1”. Fortunately, we can script this behaviour on Mikrotik.

Several Mikrotik users have written scripts to do this. However, all lacked something I needed:

  1. Operating on only one DHCP server, to only one zone (domain suffix). I have routers with multiple interfaces and DHCP servers, each of which should be synchronised to only one zone.
  2. Avoid operating on manually-created static DNS entries. All scripts I found can potentially delete static records you create.
  3. Name their variables using correct jargon for the protocols involved, such as DNS “records” and DHCP “leases”. This isn’t necessary, but it sure makes the script easier to understand.

The Script

Here is my script to sync DHCP and DNS on Mikrotik routers.

# Creates static DNS entres for DHCP clients in the named DHCP server.
# Hostnames passed to DHCP are appended with the zone.

# Set the first two variables according to your installation.
:local dhcpserver "dhcp1"
:local zone "home.example.com"

# Set the TTL to the scheduler frequency for this script.
:local ttl "00:05:00"

# Clear old static DNS entries matching the zone and TTL.
/ip dns static
:foreach dnsrecord in=[find where name ~ (".*\\.".$zone) ] do={
	:local fqdn [ get $dnsrecord name ]
	:local hostname [ :pick $fqdn 0 ( [ :len $fqdn ] - ( [ :len $zone ] + 1 ) ) ]
	:local recordttl [get $dnsrecord ttl]
	:if ( $recordttl != $ttl ) do={
		:log debug ("Ignoring DNS record $fqdn with TTL $recordttl")
	} else={
		/ip dhcp-server lease
		:local dhcplease [ find where host-name=$hostname and server="$dhcpserver"]
		:if ( [ :len $dhcplease ] > 0) do={
			:log debug ("DHCP lease exists for $hostname in $dhcpserver, keeping DNS record $fqdn")
		} else={
			:log info ("DHCP lease expired for $hostname, deleting DNS record $fqdn")
			/ip dns static remove $dnsrecord
		}
	}
}

# Create or update static DNS entries from DHCP server leases.
/ip dhcp-server lease
:foreach dhcplease in=[find where server ~ ("$dhcpserver")] do={
	:local hostname [ get $dhcplease host-name ]
	:if ( [ :len $hostname ] > 0) do={
		:local dhcpip [ get $dhcplease address ]
		:local fqdn ( $hostname . "." . $zone )
		/ip dns static
		:local dnsrecord [ find where name=$fqdn ]
		:if ( [ :len $dnsrecord ] > 0 ) do={
			:local dnsip [ get $dnsrecord address ]
			:if ( $dnsip = $dhcpip ) do={
				:log debug ("DNS record for $fqdn to $dhcpip is up to date")
			} else={
				:log info ("Updating DNS record for $fqdn to $dhcpip")
				/ip dns static remove $dnsrecord
				/ip dns static add name=$fqdn address=$dhcpip ttl=$ttl
			}
		} else={
			:log info ("Creating DNS record for $fqdn to $dhcpip")
			/ip dns static add name=$fqdn address=$dhcpip ttl=$ttl
		}
	}
}

Installation

To use this script, install it as a new script in System -> Scripts. Edit the first two variables with the name of your DHCP server and the matching DNS zone you want to create, and save the script. Finally, create a job in System -> Scheduler to run the script every five minutes.

How does it work?

The script has two main loops. The first loop operates on DNS records, clearing any expired records it previously created. For each DNS record with TTL of 5 minutes, it checks to see if a corresponding DHCP lease exists. If not, it deletes the record.

The second loop does the real work. It operates on DHCP leases from the named DHCP server. For each lease, it checks that lease has a corresponding DNS record matching the zone. If yes, it checks that the DHCP lease IP matches the DNS record and updates it. If it didn’t have a matching DNS record, it creates a new one.

All DNS records made by this script have the DNS zone appended, and have a TTL of 5 minutes. To ensure a static DNS entry is ignored by this script, just ensure it has any other TTL, or doesn’t match the zone. Otherwise it will try to manage it.

This script is reasonably fast on my test routers. I’ve attempted to add additional logic to sanitise hostnames from DHCP (which can have invalid characters in Mikrotik’s DHCP server), but the loops required caused the script to run much slower. This is a limitation of Mikrotik script language, which practically requires you write inefficient loops and string construction functions for anything complicated.

Debugging

To watch the script in action, turn on logging:

/system logging
add topics=script

Updates

The latest version will always be available on Github.

Tags: ,

  1. Ian’s avatar

    Thanks for taking the time to write this, it’s just what I was looking for :-) As a side-note, where/what are you using for this blog-site (I really like the layout).

    Reply

    1. Tyler Wagner’s avatar

      Hi Ian,

      I’m glad I could help.

      This is WordPress, with my own theme.

      Reply

    2. Gerald’s avatar

      Hi, thanks for the script.
      I have a query. I have 3x interfaces with a different dhcp range on each interface. This means there are 3x “dhcp servers”. How do I get the script to manage each dhcp server’s ip’s?
      I created 3x scripts (one for each server), but each script removes entries from the other scripts, resulting in only entries for one dhcp server.
      Hopefully I am doing something wrong.

      Thanks in advance.

      Reply

      1. Tyler Wagner’s avatar

        Yes, you do have three DHCP servers. A normal design would have three servers, each with a different interface, IP subnet, and zone (domain name). IE:

        ether1 – 192.168.1.0/24 – home1.example.com
        ether2 – 192.168.2.0/24 – home2.example.com
        ether3 – 192.168.3.0/24 – home3.example.com

        If you have that, the three scripts will work. If you are trying to put them all into the same zone, then you will have a problem. Each script will clear the records created by the other two.

        If you want to use one zone across multiple interfaces or subnets, you may need to rewrite it as one script which works on all three interfaces but only does one very complicated “clear old static DNS entries” step.

        Or, try setting three different TTL values. 05:01, 05:02, and 05:03 should be OK. The script only manages entries matching its known zone and TTL.

        Reply

      2. Corey’s avatar

        I have this installed and running, but it doesn’t seem to be doing anything. I also turned on debugging but nothing interesting appears in teh logs.

        The schedule run count is incrementing, so I know it’s running.

        I’m on 6.25 if that makes a difference.

        Thanks in advance for any info!

        Reply

        1. Tyler Wagner’s avatar

          Try running it from the CLI and see what error it gives.

          Reply

          1. Corey’s avatar

            Hi Tyler,

            I did that and found some junk characters from when I pasted it into the script. I fixed all that. No i get no errors when I run it from CLI, there is nothing in the log, and no new DNS entries appear.

            Reply

            1. Tyler Wagner’s avatar

              The two loops output to the log when they find DNS or DHCP entries to act on. If you are seeing nothing, then check that the first two variables with the name of your DHCP server and the matching DNS zone are correct.

              Reply

              1. Corey’s avatar

                It was the DHCP server name. I had used the router’s host name, not the dhcp server name.

                Looks like it’s working now!

                Reply

              2. Brane’s avatar

                Thumbs up for a very nicely cleaned up script!

                Now for anyone who has computers with multiple network interfaces on the network, a little modification:

                ...snip...
                # Create or update static DNS entries from DHCP server leases.
                /ip dhcp-server lease
                :foreach dhcplease in=[find where server ~ ("$dhcpserver")] do={
                	:local hostname [ get $dhcplease host-name ]
                	:if ( [ :len $hostname ] > 0) do={
                		:local dhcpip [ get $dhcplease address ]
                		:local fqdn ( $hostname . "." . $zone )
                		:local dhcpstatus [ get $dhcplease status ]
                		/ip dns static
                		:local dnsrecord [ find where name=$fqdn ]
                		:if ( [ :len $dnsrecord ] > 0 ) do={
                			:local dnsip [ get $dnsrecord address ]
                			:if ( $dnsip = $dhcpip ) do={
                				:log debug ("DNS record for $fqdn to $dhcpip is up to date")
                			} else={
                				:if ( $dhcpstatus="bound" ) do={
                					/ip dhcp-server lease
                					:if ( [ get [ find where address=$dnsip ] status ] ="bound" ) do={
                						:log debug ("The first found IP ($dnsip) for hostname ($hostname) in DNS  is bound. That means that $dhcpip probably belongs to a different interface on the same host, or you have 2+ hosts with the same name on the network!")
                					} else={
                						:log debug ("Updating DNS record for $fqdn to $dhcpip")
                						/ip dns static remove $dnsrecord
                						/ip dns static add name=$fqdn address=$dhcpip ttl=$ttl
                					}
                				}
                			}
                		} else={
                			:log info ("Creating DNS record for $fqdn to $dhcpip")
                			/ip dns static add name=$fqdn address=$dhcpip ttl=$ttl
                		}
                	}
                }
                ...snip...
                

                This also “solves” a problem, when two devices on the network have the same hostname. Well, solves, that’s a stretch. It only prevents the script changing static DNS entry when a host with the same name as one that’s already active gets on your network.

                Reply

                1. Brane’s avatar

                  Well, it seems I can’t edit my comment, so would you, Tyler, kindly fix my mistake and remove

                  tags from the code?

                  Reply

                  1. Tyler Wagner’s avatar

                    Done! Thanks for the update.

                    Reply

                  2. Peter B’s avatar

                    Perhaps a dumb question – but is it possible to use your script without using a local domain? i.e. setting :local zone “” ?

                    Reply

                    1. Tyler Wagner’s avatar

                      The easiest way is know is to test.

                      It should work. But make sure that any manual records you create that you don’t want this script to manage have a TTL that is not 5 minutes, or the script will delete them.

                      Reply

                    2. Erik’s avatar

                      Hi,

                      So I’m sorry for this stupid question, but how can I find or Setup the :local zone? When I lock in the DNS options I have no point to setup this one.

                      I hope someone helps me :-)

                      Reply

                    3. Milan Kerslager’s avatar

                      Your script could be run by command: /system script run YourScriptName
                      This command should be placed to the input box “On event:” in System->Scheduler also.

                      Section Installation could be updated if author wants to.

                      Reply

                    4. Harald’s avatar

                      Thanks a lot for the script. I am playing currently heavily with Ansible and VirtualBox, and as such I provision and destroy plenty VMs. With unique MAC addresses. And plenty hostnames. While I have a script to grab the IP from each VM, with the help of this script I can finally use DNS to connect to all those VMs I create! So much nicer.

                      Reply

                    5. ArshanskiyAV’s avatar

                      Thanks, I tested this script on my RB4011 with ROS 6.49.10 and it works fine.

                      Reply

Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.