Community discussions

MikroTik App
 
User avatar
Kentzo
Forum Veteran
Forum Veteran
Topic Author
Posts: 705
Joined: Mon Jan 27, 2014 3:35 pm
Location: California

Authoritative DNS Server on RouterOS with CoreDNS

Sun Apr 27, 2025 7:30 am

The up-to-date version of this guide can be found at https://gist.github.com/Kentzo/36dee5b8 ... a5e07c565f

In a Nutshell

RouterOS script that uses CoreDNS to:
  • Prevent leaks of queries for domains in Locally-Served DNS zones
  • Set up Comprehensive A/AAAA/PTR resource records for hosts
  • Set up PTR/SRV/TXT resource records for Wide-Area DNS-Based Service Discovery

Introduction

RouterOS's DNS Resolver is a very basic DNS Proxy. DNS-over-HTTPS and a very limited number of supported static resource records
is pretty much all it can do. You cannot setup Wide-Area DNS-Based Service Discovery (aka Wide-Area Bonjour),
it leaks queries for domains in IANA's Locally-Served DNS Zones, doesn't support Access Control Lists, Split-Horizon DNS, etc.

In this article I show how the HomenetDNS script and CoreDNS container address these shortcomings via plugins with custom zone files.

Why CoreDNS?

Designed specifically for a containeraized deployment, its adjustable plugin architecture allows for a very slim image. The container I use is only 5MB! Everything being compiled into a single binary makes it very easy to build and deploy. Configuration is approachable
even if you have limited knowledge of DNS. Pipeline-like processing of queries is easy to reason about.

Installation

HomenetDNS requires routeros-scripts being installed on the router. You will need a container engine (such as Docker) to prepare the CoreDNS container image.

In this setup DNS resolution looks like this:
Host -> HomenetDNS Resolver -> RouterOS Resolver

HomenetDNS Resolver is set up on hosts (via DHCPv4 and RAs), but it only answers queries for domains in locally-served zones and configured domains. The rest is forwarded to RouterOS which also handles caching.

See [configuration examples](#configuration-examples) for other deployment scenarios.

Prepare CoreDNS Container

FROM --platform=$BUILDPLATFORM golang:1.23.8 AS build
WORKDIR /src
ADD https://github.com/coredns/coredns.git#v1.12.1 /src
COPY <<EOF /src/plugin.cfg
reload:reload
errors:errors
log:log
cache:cache
rewrite:rewrite
auto:auto
forward:forward
template:template
EOF
RUN sh -c 'GOFLAGS="-buildvcs=false" make gen && GOFLAGS="-buildvcs=false" make'

FROM --platform=$TARGETPLATFORM gcr.io/distroless/static-debian12
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /src/coredns /bin/coredns
USER root
WORKDIR /etc/coredns
EXPOSE 53 53/udp
ENTRYPOINT ["/bin/coredns"]

This is mainly based on CoreDNS's Dockerfile. Few important differences:
  • plugin.cfg: List of plugins is trimmed to reduce binary size and the order of plugins (i.e. order of query processing) is altered from rewrite->template->auto->forward to rewrite->auto->forward->template, this allows to handle queries of domains in locally-served zones (via auto) before they are forwarded to the upstream
  • USER: /bin/coredns is run by root, this is RouterOS's requirement to bind on the 53 privileged port
  • WORKDIR: changed to /etc/coredns, this is where a directory with the HomenetDNS configuration is mounted at

Build, export and upload the image:
$ docker build -t routeros_coredns:latest --platform linux/arm64 -f Dockerfile . 
$ docker save routeros/coredns:latest | gzip > routeros_coredns.tar.gz
$ scp routeros_coredns.tar.gz <router-ip>:/

Install Container on RouterOS

Add the network interface:
> /interface/veth/add address=192.0.2.53/31,2001:db8:53::1/127 gateway=192.0.2.52 gateway6=2001:db8:53:: name=veth-coredns
> /ip/address/add address=192.0.2.52/31 interface=veth-coredns
> /ipv6/address/add address=2001:db8:53::/127 advertise=no interface=veth-coredns no-dad=yes

When picking IP addresses for the container, consider:
  • Does the container need to be dual-stack and run on both IPv4 and IPv6?
  • Does the container need access to WAN?

Then add a mount for the directory where HomenetDNS generates zones files and CoreDNS configuration. It's best to use an attached disk for the mount:
> /container/mounts/add dst=/etc/coredns/ name=coredns src=/usb1-part2/coredns/config

Finally, add the container using from the built image:
> /container/add file=routeros_coredns.tar.gz interface=veth-coredns root-dir=usb1-part2/coredns/root mounts=coredns workdir=/ logging=yes start-on-boot=yes

Install HomenetDNS

Download the scripts:
> $ScriptInstallUpdate ({
      "mod/kentzo-functions";
      "mod/ipv4-structured";
      "mod/ipv6-structured";
      "mod/homenet-dns";
      "setup-homenet-dns";
    }) "base-url=https://raw.githubusercontent.com/Kentzo/routeros-scripts-custom/main/"

Optionally, schedule to re-generate zone files daily:
> /system/scheduler/add name=update-homenet-dns interval=24h start-time=03:00:00 on-event=setup-homenet-dns policy=read,write,sensitive

Usage

Refer to the mod/homenet-dns.rsc for the up-to-date configuration reference.

HomenetDNS is configured by setting the HomenetDNSConfig in the global-config-overlay script:
:global HomenetDNSConfig ({
    # (str): Regex-escaped unique ID of the managed objects
    "managedID"="01234567-1337-dead-beef-0123456789ab";

    # (str): Name of the CoreDNS container
    "nsContainer"="...";

    # (str): Optional path to the CoreDNS working directory that will be mounted into the container; defaults to the container's first mount
    # "nsRoot"="";

    # (ip, str): Optional IPv4 address of the DNS server in zone files; defaults to the container's first IPv4 address
    # "nsIPAddress"=;

    #(ip, str): Optional IPv6 address of the DNS server in zone files; defaults to the container's first IPv6 address
    # "nsIP6Address"=;

    # (str): Optional default domain name for hosts and services; defaults to global-config's $Domain, if present, otherwise "home.arpa."
    # "domain"="home.arpa.";

    # (num): Optional TTL in seconds for DNS resource records; defaults to 3600
    # "ttl"=3600;

    # (array): Optional array of hosts for address resource records; defaults to empty
    #   - name: Hostname (and subdomain) relative to the "domain"
    #   - [domain]: Optional domain of the host; defaults to config's "domain"
    #   - addresses: An array of IPv4, IPv6 or MAC addresses
    # "hosts"={
    #     {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1}};
    # };

    # (array): Optional array of DNS-SD services (RFC 6763); defaults to empty
    #   - name: <Instance> of Service Instance Name, encoded and escaped, e.g. "Home\\ Media"
    #   - service: <Service> of Service Instance Name, e.g. "_smb._tcp"
    #   - [domain]: Optional <Domain> of Service Instance Name, e.g. "home.arpa."; defaults to config's "domain"
    #   - host: Hostname that provides service, e.g. "gateway" (relative to "domain") or "gateway.example." (absolute)
    #   - port: Port on the "host" where the service is available, e.g. "445"
    #   - [txt]: Optional TXT record(s) associated with the service instance
    #     - {str}: one TXT record with multiple values, e.g. {"path"="/usb1-part2/media" ; "u=guest"} -> TXT ("path=/usb1-part2/media" "u=guest")
    #     - {{str}}: Multiple TXT records where each follows the rule above
    # "services"={
    #     {"name"="Home\\ Media" ; "service"="_smb._tcp" ; "host"="gateway" ; "port"=445 ; txt={"path=/media" ; "u"="guest"}};
    # };

    # (bool): Optional flag to control whether /ip/dns/forwdarder for all configured zones is set up; defaults to `/ip/dns/get value-name=allow-remote-requests`
    # "useDNSForwarder"=yes;

    # (bool, num, time): Optional flag or refresh interval to control whether CoreDNS uses zero-downtime deployment; defaults to no
    #   - yes or >0 interval causes CoreDNS to re-read configuration and zones every so often; defaults to 60s
    #   - no or <=0 interval disables zero-downtime behavior; instead the container gets restarted on changes
    # "useZeroDowntime"=no;

    # (str): Optional regex to filter IPv4 ARP when resolving hosts; defaults to "(permanent|reachable|stale)"
    # "ipARPStatusRegex"="(permanent|reachable|stale)";

    # (str): Optional regex to filter IPv6 neighbors when resolving hosts; defaults to "(noarp|reachable|stale)"
    # "ip6NeighborStatusRegex"="(noarp|reachable|stale)";

    # (str): Optional regex to filter interfaces when resolving addresses of hosts and delegated networks; defaults to ".*"
    # "interfacesRegex"=".*";

    # (array): Optional array of additional IPv4 networks delegated to the router; defaults to advertised prefixes
    # "ipNetworksExtra"={};

    # (array): Optional array of additional IPv6 networks delegated to the router; defaults to delegated and advertised prefixes
    # "ip6NetworksExtra"={};

    # (array): Optional array of additional domains delegated to the router
    # "domainsExtra"={};

    # (array): Optional array of additional resource records
    #   - key: Zone domain name
    #   - value: An array of additional resource records to append to the zone file
    # "zonesExtra"={
    #     "example."={
    #         "samba CNAME gateway";
    #     }
    # };

    # (str): Optional additional CoreDNS configuration for the main server block, passed verbatim
    # "corefileExtra"="";

    # (str): Optional CoreDNS configuration override, passed verbatim; contents of the default main server block is available via the "homenet-dns-default" snippet
    # "corefileOverride"="";
})

You start by setting managedID and nsContainer which completes basic CoreDNS setup that authoritively answers queries for domains in locally-served zones with the NXDOMAIN rcode.

For each record in hosts you provide a hostname and at least one IP address (at most one IPv4 and one IPv6 will be used for A and AAAA resource records, respectively). The script does one extra step: it uses these IPs to look up MACs and then uses these MACs to look up all IPs assigned to the host. This comprehensive list of addresses populates PTR resource records. The search can be assisted by [providing additional IPs and MACs](#address-resource-records) in the array. The ipARPStatusRegex, ip6NeighborStatusRegex and interfacesRegex options further narrow down which addresses and interfaces are considered.

services is interesting. macOS (iOS, iPadOS, tvOS), Linux distros and Windows all support DNS-Based Service Discovery. Most of the time it happens over multicast via mDNS, but unicast DNS is also supported via specially crafted PTR, SRV and TXT resource records. Very handy when a service (say, an SMB share) on a separate VLAN, but you still want apps on your devices to find it automatically. Just set it here and let the magic happen. The dns-sd -B command on macOS and avahi-browse command on Linux can help discover available services.

useDNSForwarder configures a DNS forwarder on RouterOS for zones handled by HomenetDNS.

The script is conservative when it comes to disk usage. Each run only reads one file (< 100KB) and writes are avoided unless a change is detected. CoreDNS's zero-downtime deployment is disabled by default and the container is restarted instead. Set the useZeroDowntime option to yes or a positive interval in seconds to avoid downtime by make CoreDNS to re-read configuration every so often.

In addition to locally-served zones the script considers IPv6 prefixes advertised by Neighbor Discovery as well as managed by DHCPv6 Server. Additional networks and domains can be provided via the ipNetworksExtra, ip6NetworksExtra and domainsExtra options.

Finally, if generated Corefile is not to your liking:
  • corefileExtra: add additional plugins to the main server block
  • corefileOverride: completely override Corefile which useful when you need additional server blocks for Split-Horizon DNS

Configuration Examples

Authoritative-only Server

                             -> HomenetDNS Server
                            |
Host -> RouterOS Resolver -> 
                            |
                             -> Internet DNS Server
.
:global HomenetDNSConfig {
    "managedID"="...";
    "nsContainer"="...";
    "useDNSForwarder"=yes;
    "hosts"={
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1}};
    };
    "services"={
        {"name"="Media" ; "service"="_smb._tcp" ; "host"="samba" ; "port"=445 ; txt={"path=/media" ; "u"="guest"}};
    };
    "zonesExtra"={
        "example."={
            "samba CNAME gateway";
        };
    };
}

Hosts query RouterOS which in turn decides whether to use HomenetDNS or forward to an internet DNS server. HomenetDNS configures a DNS forwarder, queries for domains in its own zones are answered authoritatively, all other queries get the REFUSED rcode for compatibility.

For a 3rd party resolver set "useDNSForwarder"=no and configure forwarding manually.

Authoritative Server and DNS Forwarder

Host -> HomenetDNS Resolver -> RouterOS Resolver
.
:global HomenetDNSConfig {
    "managedID"="...";
    "nsContainer"="...";
    "useDNSForwarder"=no;
    "hosts"={
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1}};
    };
    "services"={
        {"name"="Media" ; "service"="_smb._tcp" ; "host"="samba" ; "port"=445 ; txt={"path=/media" ; "u"="guest"}};
    };
    "zonesExtra"={
        "example."={
            "samba CNAME gateway";
        };
    };
    "corefileExtra"="\
forward . dns://<router-ip-on-veth-coredns> {\n\
    max_fails 0\n\
}\n\
";
}

Hosts query HomenetDNS. Queries for domains in its own zones are answered authoritatively, all other queries are forwarded to RouterOS which in turn forwards to the Internet resolver and handles caching.
Host -> HomenetDNS Resolver -> Internet Resolver
.
:global HomenetDNSConfig {
    "managedID"="...";
    "nsContainer"="...";
    "useDNSForwarder"=no;
    "hosts"={
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1}};
    };
    "services"={
        {"name"="Media" ; "service"="_smb._tcp" ; "host"="samba" ; "port"=445 ; txt={"path=/media" ; "u"="guest"}};
    };
    "zonesExtra"={
        "example."={
            "samba CNAME gateway";
        };
    };
    "corefileExtra"="\
cache\n\
forward . https://dns.quad9.net/dns-query\n\
";
}

Similar to the configuration above but this time HomenetDNS forwards request to the Internet resolver and handles caching.

Address Resource Records

Add address resources records for hosts":
:global HomenetDNSConfig {
    ...
    "hosts"={
        # Both A and AAAA resource records for the gateway host in the default domain
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1}};

        # Same as above + additional MAC address to create PTR resource records
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 2001:db8::1 ; "00:53:00:AA:BB:CC"}};

        # One A resource record for 192.0.2.1; remaining addresses are used to create PTR resource records
        {"name"="gateway" ; "addresses"={192.0.2.1 ; 10.0.0.1 ; 192.168.0.1}};

        # A and AAAA resource records for gateway.example. rather than in the default domain
        {"name"="gateway" ; "domain"="example." ; "addresses"={192.0.2.1 ; 2001:db8::1}};
    };
    ...
}

Narrow down which interfaces are considered:
:global HomenetDNSConfig {
    ...
    "hosts"={ ... };

    # Only consider the vlan-main and vlan-services interfaces when making PTR resource records
    "interfacesRegex"="(vlan-main|vlan-services)";
    ...
}

DNS-SD Resource Records

Advertise the "Home Media._smb._tcp" samba share and "Printer._ipp._tcp" printer via unicast DNS:
:global HomenetDNSConfig ({
    ...
    "services"=({
        # Shows as "Home Media" in apps that can access SMB shares
        {"name"="Home\\ Media" ; "service"="_smb._tcp" ; "host"="gateway" ; "port"=445 ; txt={"path=/media" ; "u"="guest"}};

        # Shows as "Printer" in the printing dialogs of apps that support AirPrint
        {"name"="Printer" ; "service"="_ipp._tcp" ; "host"="printer" ; "port"=631 ; txt=({
            "txtvers=1";
            "qtotal=1";
            "rp=printers/HP_Color_LaserJet_9500";
            "ty=HP Color LaserJet 9500 MFP";
            "adminurl=...";
            "note=Shared HP CLJ 9500; In DA7/4 Near Howard";
            "priority=0";
            "product=(HP color LaserJet 9500 MFP)";
            "printer-state=3";
            "printer-type=0xC0B0DE";
            "Transparent=T";
            "Binary=T";
            "Fax=F";
            "Color=T";
            "Duplex=T";
            "Staple=F";
            "Copies=T";
            "Collate=T";
            "Punch=F";
            "Bind=F";
            "Sort=F";
            "Scan=F";
            "pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/urf";
            "air=username,password";
            "URF=W8,SRGB24,CP255,RS600,DM1";
        })};
    });
    ...
})

Verbatim Resource Records

Add the CNAME resource record to redirect samba.example. -> gateway.example.:
:global HomenetDNSConfig ({
    ...
    "zonesExtra"=({
        # Add the CNAME resource record to the example. zone
        "example."={
            "samba CNAME gateway";
        }
    });
    ...
})

Verbatim CoreDNS Configuration

Enable caching and verbose logging:
:global HomenetDNSConfig ({
    ...
    # Customize the main server block
    "corefileExtra": "\
log\n\
cache\n\
"
    ...
})

Use Split-Horizon DNS for custom processing of queries from the 10.13.37.0/24 subnet:
:global HomenetDNSConfig ({
    ...
    # Add Split-Horizon DNS with custom processing for hosts from 10.13.37.0/24
    "corefileOverride": "\
. {\n\
    view IPsec {\n\
        expr incidr(client_ip(), '10.13.37.0/24')\n\
    }\n\
    ...\n\
}\n\
\n\
. {\n\
    import homenet-dns-default\n\
}\n\
"
})

Normative and Informational References