Yet Another DNS from DHCP Script

I wrote yet another DNS from DHCP script. Why? Several reasons:

  • I wanted to minimize writes to flash.
  • I wanted to use the DHCP network domain.
  • I wanted to handle name collisions in a stable way (first lease wins).

It seems to be working for me on 7.2.1. IPv4 only for now. Feedback/patches welcome.

https://github.com/MichaelPaddon/routeros-scripts/blob/main/syncdns

DNS RegEx for MikroTik:
http://forum.mikrotik.com/t/address-lists-downloader-dshield-spamhaus-drop-edrop-etc/133640/114

# from
:global IsValidName do={
    :local string [:tostr $1]
    :return ($string ~ "^[A-Za-z0-9-][A-Za-z0-9-]*\$" && [:len $string] < 64)
}

#to
:global IsValidName do={
    :local string [:tostr $1]
    :return ($string~"^(([a-zA-Z0-9][a-zA-Z0-9-]{0,61}){0,1}[a-zA-Z]\\.){1,9}[a-zA-Z][a-zA-Z0-9-]{0,28}[a-zA-Z]\$")
}

Instead to write everytime the path, write once for each sections, this also increase readability

# from
    :foreach network in [/ip/dhcp-server/network/find] do={
        :local netblock [/ip/dhcp-server/network/get value-name=address $network]
        :if ($ipaddr in $netblock) do={
            :return [/ip/dhcp-server/network/get value-name=domain $network]
        }
    }


# to
    /ip/dhcp-server/network/
    :foreach network in [find] do={
        :local netblock [get value-name=address $network]
        :if ($ipaddr in $netblock) do={
            :return [get value-name=domain $network]
        }
    }

Do not write path with "/" If the script can be run on also on v6
"/ip/dns/static/find" is incopmpatible on v6, but "/ip dns static find" is compatible with both v6 and v7


Do not mix "styles", or use "/" or not use:

    :foreach entry in [/ip/dns/static/find where comment=$Magic] do={
        :local fqdn [/ip dns static get value-name=name $entry]

If possible, make easier the scripting, this is also more readable

:local fqdn ($hostname . "." . $domain) ==>> :local fqdn "$hostname.$domain"

Some comments......

record already present?

must be like

exact same record already present?

record exists but needs updating?

must be like

if exist update record (regardless of any other conditions)

no record of that name at all?

must be like

if not exist add one

Thanks for all the feedback! I’ll update the script accordingly!

How to include hostnames with _ character? I’ve always used this font with no problems and this script is great if it would include these names

# syncdns version: 1.0.0
#
# A RouterOS script to synchronise DNS with DHCP leases.
# This may be run regularly or whenever a DHCP event occurs.
#
# Copyright 2022 by Michael Paddon.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.

# default domain for DNS records
:global DefaultDomain "localdomain"

# TTL for DNS records
:global DnsTtl 5m

# magic comment for DNS records
:global Magic "automatic-from-dhcp"

# return the domain associated with an address
:global GetDomain do={
    :global DefaultDomain
    :local ipaddr [:toip $1]
    /ip dhcp-server network
    :foreach network in [find] do={
        :local netblock [get value-name=address $network]
        :if ($ipaddr in $netblock) do={
            :return [get value-name=domain $network]
        }
    }

    :return $DefaultDomain
}

# is a string a valid DNS name?
:global IsValidName do={
    :local string [:tostr $1]
    :return ($string ~ "^[A-Za-z]([A-Za-z0-9-_]{0,61}[A-Za-z0-9])?\$")
}

# return an array of fqdn=ipaddr items
:global GetFqdns do={
    :global GetDomain
    :global IsValidName

    /ip dhcp-server lease
    :local fqdns [:toarray ""]
    :foreach lease in [find] do={
        :local hostname [get value-name=host-name $lease]
        :if ([$IsValidName $hostname]) do={
            :local ipaddr [get value-name=address $lease]
            :local domain [$GetDomain $ipaddr]
            :local fqdn "$hostname.$domain"
            :if ([:len ($fqdns->$fqdn)] = 0) do={
                :set ($fqdns->$fqdn) $ipaddr
            }
        }
    }

    :return $fqdns
}

# update a DNS record, returning true if a change was made
:global Update do={
    :global DnsTtl
    :global Magic

    :local fqdn [:tostr $1]
    :local ipaddr [:toip $2]

    # correct record already present?
    /ip dns static
    :if ([:len [find where name=$fqdn address=$ipaddr ttl=$DnsTtl comment=$Magic]] > 0) do={
        :return false
    }

    # if record exists it must be updated
    :local exists [find where name=$fqdn comment=$Magic]
    :if ([:len $exists] > 0) do={
        :log info "syncdns: update $fqdn -> $ipaddr"
        set address=$ipaddr ttl=$DnsTtl $exists
        :return true
    }

    # if no record of that name exists at all, create record
    # note: a manually created record may exist which MUST NOT be touched
    :if ([:len [find where name=$fqdn]] = 0) do={
        :log info "syncdns: add $fqdn -> $ipaddr"
        add name=$fqdn address=$ipaddr ttl=$DnsTtl comment=$Magic
        :return true
    }

    :return false
}

# synchronize DNS
:global SyncDns do={
    :global GetFqdns
    :global Magic
    :global Update

    # update dns records
    :local fqdns [$GetFqdns]
    :foreach fqdn,ipaddr in=$fqdns do={
        [$Update $fqdn $ipaddr]
    }

    # remove obsolete dns records
    /ip dns static
    :foreach entry in [find where comment=$Magic] do={
        :local fqdn [get value-name=name $entry]
        :if ([:len ($fqdns->$fqdn)] = 0) do={
            :log info "syncdns: remove $fqdn"
            remove $entry
        }
    } 
}

:log info "syncdns: synchronize"
[$SyncDns]

I see that all the scripts posted here are super complicated and designed to be run on a schedule.

Why not just use something simple as a lease script in the dhcp server to have instantaneous changes?




:local ttl 600s
:local debug 0

:if ($leaseBound = 1) do={
    :local HostName [/ip dhcp-server lease get number=[find where mac-address=$leaseActMAC] host-name]
    :if ([:len $HostName] > 0) do={
        :local Domain [/ip dhcp-server network get number=[find where mac-address=$leaseActMAC] domain]
        /ip dns static remove numbers=[find where name=($HostName . "." . $Domain)]
        /ip dns static add name=($HostName . "." . $Domain) address=$leaseActIP ttl=$ttl
    }
} else={
    /ip dns static remove numbers=[find where address=$leaseActIP ttl=$ttl]
}

:if ($debug = 1) do={
    :log info message=("leaseBound = " . $leaseBound)
    :log info message=("leaseServerName = " . $leaseServerName)
    :log info message=("leaseActMAC = " . $leaseActMAC)
    :log info message=("leaseActIP = " . $leaseActIP)
    :log info message=("lease-hostname = " . $lease-hostname)
    :log info message=("lease-options = " . $lease-options)
}

I haven’t tried it, but it works by accident, because there is a big error in the script:

/ip dhcp-server network get number=[find where mac-address=$leaseActMAC] domain

The DHCP Networks do not have “mac-address”

In fact valerio’s script correctly searches the name among the networks, using the IP…

:global GetDomain do={
    :global DefaultDomain
    :local ipaddr [:toip $1]
    /ip dhcp-server network
    :foreach network in [find] do={
        :local netblock [get value-name=address $network]
        :if ($ipaddr in $netblock) do={
            :return [get value-name=domain $network]
        }
    }

    :return $DefaultDomain
}

Also is useless search and read the hostname with

:local HostName [/ip dhcp-server lease get number=[find where mac-address=$leaseActMAC] host-name]

When already exist on purpose the variable lease-hostname (but probably you are unable to use it for next point)
Just use $“lease-hostname”


And also the “debug” part is wrong, because both lease-hostname and lease-options must be surrounded by quotes or do not work correctly and broken the script

    :log info message=("lease-hostname = " . $lease-hostname)
    :log info message=("lease-options = " . $lease-options)

correct are $“lease-hostname” and $“lease-options”



Instead, regarding the fact that it is more logical to be done by the DHCP script, instead of the scheduler, it is more logical and correct.

I really appreciate your pointers. I was able to simplify that lease script and add your dns validation regex and OP’s GetDomain function.

:local ttl 600s

:local GetDomain do={
    :local ipaddr [:toip $1]
    /ip dhcp-server network
    :foreach network in [find] do={
        :local netblock [get value-name=address $network]
        :if ($ipaddr in $netblock) do={
            :return [get value-name=domain $network]
        }
    }
}

:local IsValidFQDN do={
    :local string [:tostr $1]
    :return ($string~"^(([a-zA-Z0-9][a-zA-Z0-9-]{0,61}){0,1}[a-zA-Z]\\.){1,9}[a-zA-Z][a-zA-Z0-9-]{0,28}[a-zA-Z]\$")
}

/ip dns static
:if ($leaseBound = 1) do={
    :local FQDN "$($"lease-hostname").$[$GetDomain $leaseActIP]"
    :if ([$IsValidFQDN $FQDN]) do={
        remove numbers=[find where name=$FQDN]
        add name=$FQDN address=$leaseActIP ttl=$ttl
    }
} else={
    remove numbers=[find where address=$leaseActIP ttl=$ttl]
}