Here are some useful guides and resources for working with leng. Contributions welcome!

Why Leng

Reasons you would want to use Leng include:

  • Ad-blocking at the DNS level: this compliments misses browser adblockers (they use a different approach to block ads), and is especially useful in devices where ad-blockers are hard to install (like smart TVs, or non-browser apps).
  • Blocking tracking at the DNS level: vendors, especially your device's manufacturers, will often track you outside of websites (where browser ad-blockers are powerless). When using the right blocklists, leng will block this tracking for all devices that use it as their DNS provider.
  • DNS Server for self-hosted infra: by specifying your records on a config file, leng is a very easily maintanable custom DNS server deployment.
  • DNS Privacy and Security: many devices use the most basic DNS implementation, DNS over UDP. This is a bad idea because it is less private and less secure (you can read here to understand why). Leng can serve as a secure proxy so that even if your devices speak to it via UDP, it speaks to the rest of the internet via the more secure alternatives (like DoH or DoT).
  • It's small and fast
  • There are few open-source DNS servers with the above features: my motivation for forking grimd and creating leng was the need for a server that provided blocklists (like Blocky) as well as decent custom DNS records support (like CoreDNS, grimd was almost there).

For more on leveraging leng for DNS privacy, see DNS Privacy.

Alternatives Comparison

Leng overlaps with a few other solutions in terms of providing DNS sinkholing for advertisements, as well as custom DNS. This pages aims to clarify how leng compares to these other solutions.

TLDR: Leng is suitable for a simple DNS server that serves custom records and blocks ads. It is designed to be small and easily scriptable (like Blocky), whereas Adguard, PiHole, etc are more comprehensive solutions that include many more features but are not stateless, and are likely to have a larger fingerprint.

This is by all means not a comprehensive list. Note I have not tried every single of these alternatives, so some information might be outdated or plain wrong - if so please submit a PR to correct it if you find it so.

TraitLengBlockyAdguardPiHoleCoreDNS
Blocklist-basd blocking (remote fetch)❌ ish
Custom DNS records support❌ ish (only rewrites)❌ ish✅ ish via dnsmasq (no templating)
RAM footprint50MB with traffic + DoH150MB512MB250MB but depends heavily on plugins
Ease of useConfig fileConfig fileConfig file + Web UIWeb UIConfig file
Parental controlsThrough parental blocklistsThrough parental blocklists
DNS-over-HTTPS server
DNS-over-HTTPS upstream proxy
Stateless (all config as files)
Running rootless
Prometheus metrics APIsee PR❌ but exporter exists✅ via plugin
Per device config✅ via client groups✅ via plugins
DHCP Server (Assigns IPs to devices)
Fancy Web UI

DNS (overview)

Leng works by proxying your DNS requests to an upstream DNS server, and returning a useless response when the request is for a blocked domain.

Blocked domains are those that appear on a blocklist (downloaded at startup). You can see which blocklists are enabled by default and how to change them in Configuration.

Additionally, you can also configure custom responses for specific domains, indepenently of the blocklists. See more in Custom DNS.

    
sequenceDiagram 
    User --> Leng: 
    Online Blocklists -->> Leng: Download lists
    Note over Online Blocklists,Leng: At startup
    User->> +Leng: A google.com
    Leng ->> Upstream DNS: A gogle.com
    Upstream DNS ->> Leng: google.com IN A 234.213.532.12
    Leng ->> -User: google.com IN A 234.213.532.12
    User ->> +Leng: A adservice.google.com
    Leng ->> -User: adservice.google.com IN A 0.0.0.0

DNS-over-HTTP(S), aka DoH

Leng supports DNS-over-HTTPS as per RFC-8484, although it is disabled by default.

Custom DNS records will be served over DoH the same as normal DNS requests.

You can specify your key files yourself to have leng serve HTTPS traffic, or you can let leng serve HTTP traffic and have a proxy manage the HTTPS certificates.

Specifying Key files (HTTP)

[DnsOverHttpServer]
    enabled = true
    bind = "0.0.0.0:80"
    timeoutMs = 5000

    [DnsOverHttpServer.TLS]
        enabled = true
        certPath = ""
        keyPath = ""
        # if empty, system CAs will be used
        caPath = ""

Not specifying key files (TLS disabled, HTTP traffic from leng)

[DnsOverHttpServer]
    enabled = true
    bind = "0.0.0.0:80"
    timeoutMs = 5000

⚠ It is not recommended to use HTTP without TLS at all directly. Your queries will be un-encrypted, so they won't be much different than normal UDP queries.

You can use DoH in most browsers.

Custom DNS Records

You can make leng return records of your choosing (which will take precedence over upstream DNS records) by setting customdnsrecords in the Configuration.

Custom DNS records are represented as Resource Record strings. Class defaults to IN and TTL defaults to 3600. Full zone file syntax is supported.

customdnsrecords = [
    "example.com.         3600 IN  A       10.10.0.1",
    "example.cname.com.        IN  CNAME   wikipedia.org",
]

CNAME Following

Leng implements following CNAME records as specified in RFC-1034 section 3.6.2, where it returns all necessary CNAME and A records to fully resolve the query (as opposed to just returning a synthetic A record, which is known as CNAME flattening).

⚠ This is the behaviour of most if not all DNS servers - Leng is only special in this in that it has to deal with cuustom DNS records, the resolvers it proxies, and blocklists. You should not need to change its default behaviour, but this page aims to leave it well-documented.

dig request example

$> dig first.example

; <<>> DiG 9.18.19 <<>> first.example

;; QUESTION SECTION:
;first.example.		IN	A

;; ANSWER SECTION:
first.example.  	300	IN	CNAME	second.example.
second.example.		300	IN	CNAME	third.example.
third.example.  	300	IN	A	139.201.133.245

Behaviour

The resolving for the downstream CNAME records is done with the same question type as the original question. That is, if you ask AAAA some-cname.com, the following CNAME queries will be AAAA questions too.

Custom records

If you have set up your own custom records, those can also be part of the CNAME chain.

This makes it easy to alias custom records to external domains:

customdnsrecords = [
  "login.vpn       IN CNAME    this.very.long.other.domain.login.login.my-company.xyz"
]

Querying login.vpn will also return the A record corresponding to this.very.long.other.domain.login.login.my-company.xyz.

Blocking

If any of the domains involved in the CNAME-following is part of a blocklist (that is, it would get blocked if it corresponded to an A response, rather than CNAME) then the entire request blocked (unless the domain is part of the custom DNS defined in the config)

For the example where we have

first.example   IN CNAME second.example
second.example  IN CNAME third.example
third.example   IN A     10.0.0.0

if any of first.example, second.example or third.example appear in a blocklist, the request for first.example will fail.

Configuration

CNAME-following is enabled by default, but you can disabled with the following:

# leng.toml

followCnameDepth = 0

Blocking DNS

There are many blocklist resources online, and by default leng is configured to use some of the more popular ones from around the internet for blocking ads and malware domains. Some services exist that will allow you to regularly get blocklist updates automatically from feeds.

Blocklists

https://github.com/StevenBlack/hosts/

DNS Privacy

Leng can enhance your DNS Privacy in several ways

As your DoH provider

DNS-over-HTTPS allows encrypted, hard-to-block DNS. You can set up DNS-over-HTTPS for most major browsers (see how here).

See how to set it up for leng at DNS-over-HTTP.

As a DoH proxy

DoH is great, but most devices use DNS-over-UDP by default, and some can't even be configured otherwise.

If you have your own private secure network, you can stop attackers from learning what websites you visit by using leng as a secure proxy:

graph TD
    subgraph Secure Network
        U("🧘 User") --> |"🔓 Insecure\nDNS-over-UDP"|L[Leng]
    end
    L --> |"🔒 Secure DoH"| Up[Upstream DNS]
    A("👿 Attacker") ---> |Cannot see contents\nof DNS requests | Up

This way you allow 'insecure' DNS, but only inside your network, and your requests are private to external attackers.

No configuration is required for this: leng will always try to resolve domains by DoH via cloudflare before falling back to other methods. You can choose the upstream DoH resolver in the Configuration.

Note that this method is only as secure as your network is! Ideally set up as many devices as possible to use DoH directly

Preserving privacy against a single upstream

If you do not trust upstream providers with your privacy, ideally you should not send all your requests to any one of them. Because of the authoritative nature of DNS, asking some upstream cannot be avoided, but the best you can do is use a fully recursive resolver like unbound. You can still use non-recursive DNS proxies (leng, blocky, or CoreDNS) and their features by using unbound as your upstream, and letting unbound resolve your queries.

graph LR

you(("You")) --> leng(leng) --> unbound(unbound) -.-> u1["upstream A"] & u2["upstream B"] & u3["upstream C"]

If leng.toml is not found the default configuration will be used. If it is found, fields that are set will act as overrides.

Quick Start

If you are happy to use Cloudflare as your upstream DNS provider and just want to generally block tracking and advertising, the following minimal config should be enough.

If you want to tweak more settings, keep scrolling down!

# address to bind to for the DNS server
bind = "0.0.0.0:53"

# address to bind to for the API server
api = "127.0.0.1:8080"

# manual custom dns entries - comments for reference
customdnsrecords = [
    # "example.mywebsite.tld      IN A       10.0.0.1",
]

[Metrics]
    enabled = false

[Blocking]
    # manual whitelist entries - comments for reference
    whitelist = [
        # "getsentry.com",
    ]

Default configuration

# log configuration
# format: comma separated list of options, where options is one of 
#   file:<filename>@<loglevel>
#   stderr>@<loglevel>
#   syslog@<loglevel>
# loglevel: 0 = errors and important operations, 1 = dns queries, 2 = debug
# e.g. logconfig = "file:leng.log@2,syslog@1,stderr@2"
logconfig = "stderr@2"

# apidebug enables the debug mode of the http api library
apidebug = false

# address to bind to for the DNS server
bind = "0.0.0.0:53"

# address to bind to for the API server
api = "127.0.0.1:8080"

# concurrency interval for lookups in miliseconds
interval = 200

# question cache capacity, 0 for infinite but not recommended (this is used for storing logs)
questioncachecap = 5000

# manual custom dns entries - comments for reference
customdnsrecords = [
    # "example.mywebsite.tld      IN A       10.0.0.1",
    # "example.other.tld          IN CNAME   wikipedia.org"
]

[Blocking]
    # response to blocked queries with a NXDOMAIN
    nxdomain = false
    # ipv4 address to forward blocked queries to
    nullroute = "0.0.0.0"
    # ipv6 address to forward blocked queries to
    nullroutev6 = "0:0:0:0:0:0:0:0"
    # manual blocklist entries
    blocklist = []
    # list of sources to pull blocklists from, stores them in ./sources
    sources = [
        "https://mirror1.malwaredomains.com/files/justdomains",
        "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
        "https://sysctl.org/cameleon/hosts",
        "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
        "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
        "https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-blocklist.txt"
    ]
    # list of locations to recursively read blocklists from (warning, every file found is assumed to be a hosts-file or domain list)
    sourcedirs = ["./sources"]
    sourcesStore = "./sources"
    # manual whitelist entries - comments for reference
    whitelist = [
        # "getsentry.com",
        # "www.getsentry.com"
    ]



[Upstream]
    # Dns over HTTPS provider to use.
    DoH = "https://cloudflare-dns.com/dns-query"
    # nameservers to forward queries to
    nameservers = ["1.1.1.1:53", "1.0.0.1:53"]
    # query timeout for dns lookups in seconds
    timeout_s = 5
    # cache entry lifespan in seconds
    expire = 600
    # cache capacity, 0 for infinite
    maxcount = 0

# Prometheus metrics
[Metrics]
    enabled = false
    path = "/metrics"
    # see https://cottand.github.io/leng/Prometheus-Metrics.html
    highCardinalityEnabled = false
    histogramsEnabled = false
    resetPeriodMinutes = 60

[DnsOverHttpServer]
    enabled = false
    bind = "0.0.0.0:80"
    timeoutMs = 5000

# TLS config is not required for DoH if you have some proxy (ie, caddy, nginx, traefik...) manage HTTPS for you
    [DnsOverHttpServer.TLS]
        enabled = false
        certPath = ""
        keyPath = ""
        # if empty, system CAs will be used
        caPath = ""

The most up-to-date version can be found on config.go

Prometheus metrics

The HTTP API has a /metrics endpoint that exposes Go runtime metrics as well as things including:

  • downstream DNS requests, broken down by type
  • upstream DNS requests
  • upstream DNS-over-HTTPS success rate
  • downstream DNS-over-HTTPS success rate

No grafana dashboards exist for leng yet. If you make one, please make a PR!

High cardinality metrics

Tags can be added to some metrics (upstream_request, request_total) so that they include information such as the name of the DNS request (ie, example.com.) or the IP of host making the request.

If leng is left to run for a few hours (and you have enough traffic), the cardinality of these metrics will grow, to the point the size of the /metrics response will grow to be so big the metrics stop being updated. While resetting the counters periodically can help (and you can tweak that with the config Metrics.resetPeriodMinutes) you might still see issues depending on your traffic. You can read this SO post to learn more.

High cardinality metrics can also compromise your privacy by exposing in the metrics endpoint what domains clients are querying as well as their IPs.

For these reasons, high cardinality metrics are disabled by default. You can enable them with the following config:

[Metrics]
enabled = true
path = "/metrics"
highCardinalityEnabled = true

Histogram metrics

Histogram metrics are not unbounded and usually will not be as high-cardinality as the metrics discussed above, but you should still expect them to have some impact on leng's the memory footprint.

You can enable them with:

[Metrics]
enabled = true
path = "/metrics"
histogramsEnabled = true

Signals (config reload)

Leng will listen for SIGUSR1 signals in order to reload the config file at run-time and apply the new config.

Currently, the only field of the config file that supports reloading is customdnsrecords, which specifies custom DNS records served. Please make an issue if you have a use-case where you would like some other config fields to also be able to be reloaded.

In the meantime, for all fields, it is safe to simply change the config file while leng is running, and restart it.

Note on Nomad deployments

Nomad is able to send signals rather than restarting a task when a template (like the config file) changes.

It is not recommended to use this approach with leng, because there are instances in which Nomad can try to send a signal even if the task is not running (see hashicorp/nomad#5459, which was unresolved at the time of writing).

Instead, set the template's change_mode = "restart" and rely on Nomad restarting the task. Due to leng's fast startup time and tiny image size, it should take seconds even when redownloading the image.

In order to still mitigate this downtime, rolling/canary deployments can be used so there is always an instance of leng up to serve traffic.

Docker

Leng is also distributed as a Docker image. You can find published images here. The image is small (v1.3.1 is under 13MB).

Supported architectures are linux AMD64, ARM64, ARMv6, ARMv7.

If you think leng ought to support more OSs or architectures, please make an issue.

Running

With the default configuration:

docker run -d \
  -p 53:53/udp \
  -p 53:53/tcp \
  -p 8080:8080/tcp \
  ghcr.io/cottand/leng

With a specific leng.toml:

docker run -d \
  -p 53:53/udp \
  -p 53:53/tcp \
  -p 8080:8080/tcp \
  -v leng.toml:/leng.toml \
  ghcr.io/cottand/leng \
  -config /leng.toml

See Configuration for the full config.

Deploying on Debian

Installing leng

Installing leng is the easiest when you simply download a release from the releases page. Go ahead and copy the link for leng_linux_x64 and run the following in your terminal.

mkdir ~/grim
cd ~/grim
wget <leng release>

This will download the binary to ~/grim which will be leng's working directory. First, lets setup file permissions for leng, by running the following.

chmod a+x ./leng_linux_x64

Setup is pretty much complete, the only thing left to do is run leng and let it generate the default configuration and download the blocklists, but lets set it up as a systemd service so it automatically restarts and updates when starting.

Setting up the service

Create the leng service by running the following,

nano /etc/systemd/system/leng.service

Now paste in the code for the service below,

[Unit]
Description=leng dns proxy
Documentation=https://github.com/looterz/leng
After=network.target

[Service]
User=root
WorkingDirectory=/root/grim
LimitNOFILE=4096
PIDFile=/var/run/leng/leng.pid
ExecStart=/root/grim/leng_linux_x64 -update
Restart=always
StartLimitInterval=30

[Install]
WantedBy=multi-user.target

Save, and now you can start, stop, restart and run status commands on the leng service like follows

systemctl start leng  # start
systemctl enable leng # start on boot

The only thing left to do is setup your clients to use your leng dns server.

Security

Now that leng is setup on your droplet, it's recommended to secure the installation from non-whitelisted clients.

Securing on Linux

The recommended way to harden access to the leng server is to only allow connections from clients you trust, mainly because public dns servers are hit by penetration testers and hackers regularly to scout for vulnerabilities.

Installing Requirements

Let's grab ufw to allow for easy editing of iptables.

apt-get install ufw -y

Firewall Setup

Now let's whitelist our dns clients IP address or range, and block access from everywhere else by default using ufw.

ufw deny 53
ufw allow from <ip or range> to any port 53
ufw reload

Now only the client(s) you whitelisted can access the dns server.

⚠ For Docker deployments, keep in mind ufw will not stop outside connections to your containers if you bind their ports. See the Docker docs about the issue.

Nix

Leng is also packaged as a Nix flake.

Running

You can simply run nix run github:cottand/leng to run latest master.

Installing in NixOS via a Module

The leng flake also exports a NixOS module for easy deployment on NixOS machines.

Please refer to Configuration for the options you can use under services.leng.configuration. = ....

In your flake

{
  inputs = {
    # pinned version for safety
    leng.url = "github:cottand/leng/v1.6.0"; 
    leng.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, leng, ... }: {
    # Use in your outputs
    nixosConfigurations."this-is-a-server-innit" = nixpkgs.lib.nixosSystem {
      modules = [ 
        ./configuration.nix
        leng.nixosModules.default #  <- import leng module
        {
          services.leng = {       # <-- now you can use services.leng!
            enable = true;
            configuration = {
              api = "127.0.0.1:8080";
              metrics.enabled = true;
              blocking.sourcesStore = "/var/lib/leng-sources";
            };
          };
        }
      ];
    };
  };
}

Legacy Nix

Add the following inside your configuration.nix:

{pkgs, lib, ... }: {
  imports = [
    # import leng module
    (builtins.getFlake "github:cottand/leng/v1.6.0").nixosModules.default 
  ];
    
  # now you can use services.leng!
  services.leng = {       
    enable = true;
    configuration = {
      api = "127.0.0.1:8080";
      metrics.enabled = true;
      blocking.sourcesStore = "/var/lib/leng-sources";
    };
  };
  
}

Using leng for local DNS

If you want to use leng to resolve the DNS queries of the machine you are installing it on, you should also add it to the local nameservers:

networking.nameservers = [ "127.0.0.1" ];

Developing

The flake's development shell simply includes Go 1.21+ and a fish shell. You can enter it with nix develop.

Deploying on Nomad

Example Job file

You can deploy leng on Nomad. The following job file should serve as a starting point. It includes

  • ports bound to a host network YOUR_VPN
  • services for metrics and DNS, including DoH

It is strongly recommended that

  • you do not expose your DNS ports to the outer internet (as you will make yourself vulnerable to DNS amplification and DoS attacks). I recommend you use Nomad's host_network feature to select what interface to bind the ports to.
  • you bind the dns port to 53 in order to make it reachable. Other ports can be reached through your preferred method of service discovery.
Drop down for Job file
job "dns" {
  group "leng-dns" {
    network {
      mode = "bridge"
      port "dns" {
        static = 53
      }
      port "metrics" {}
      port "http_doh" {}
    }
    update {
      canary           = 1
      min_healthy_time = "30s"
      healthy_deadline = "5m"
      auto_revert      = true
      auto_promote     = true
    }

    service {
      name     = "dns-metrics"
      provider = "nomad"
      port     = "metrics"
      tags     = ["metrics"]
    }
    service {
      name     = "dns"
      provider = "nomad"
      port     = "dns"
    }
    service {
      name     = "doh"
      provider = "nomad"
      port     = "http_doh"
    }
    task "leng-dns" {
      driver = "docker"
      config {
        image = "ghcr.io/cottand/leng:sha-6b2e265"
        args = [
          "--config", "${NOMAD_ALLOC_DIR}/config.toml",
          "--update",
        ]
        ports = ["dns", "metrics"]
      }
      resources {
        cpu    = 80
        memory = 80
      }
      template {
        destination = "${NOMAD_ALLOC_DIR}/config.toml"
        change_mode = "restart"
        data = <<EOF
logconfig = "stderr@1"

# address to bind to for the DNS server
bind = "0.0.0.0:{{ env "NOMAD_PORT_dns"  }}"

# address to bind to for the API server
api = "0.0.0.0:{{ env "NOMAD_PORT_metrics"  }}"

# concurrency interval for lookups in miliseconds
interval = 200

metrics.enabled = true

[Upstream]
  nameservers = ["1.1.1.1:53", "1.0.0.1:53"]
  DoH = "https://cloudflare-dns.com/dns-query"

[DnsOverHttpServer]
	enabled = true
	bind = "0.0.0.0:{{ env "NOMAD_PORT_http_doh" }}"
	timeoutMs = 5000

	[DnsOverHttpServer.TLS]
		enabled = false

EOF
      }
    }
  }
}

Service Discovery

Leng can be used as a sort of Consul replacement to implement DNS-based service discovery - meaning you can address your Nomad services by DNS. See the following templated config file (you would use it inside the template above, as a replacemenet for the provided config.toml file).

Example diagram for discovering a grafana service (SRV record omitted):

graph TD
    J("🐳 Nomad Job (grafana)\n at 10.0.0.3") -->|" registers `grafana` service "| N[Nomad]
    N -->|renders template| L["⚡ Leng \n `grafana.nomad. IN A 10.10.0.3` "]
    U("You (or some container)") -->|queries DNS| L
    L -->|reaches| J
Templated TOML config for leng
  • creates A record per Nomad client
  • creates A record per service
  • creates SRV record per service pointing to (port, client)
logconfig = "stderr@1"

# address to bind to for the DNS server
bind = "0.0.0.0:{{ env "NOMAD_PORT_dns"  }}"

# address to bind to for the API server
api = "0.0.0.0:{{ env "NOMAD_PORT_metrics"  }}"

metrics.enabled = true

customdnsrecords = [

    # example for your hardcoded service:
    {{ range $i, $s := nomadService "my-service" }}
    "myservice.mytld            3600  IN  A   {{ .Address }}",
    {{ end }}
    
    ## start - generation for every registered nomad service" ##

    {{ $rr_a := sprig_list -}}
    {{- $rr_srv := sprig_list -}}
    {{- $base_domain := ".nomad" -}} {{- /* Change this field for a diferent tld! */ -}}
    {{- $ttl := 3600 -}}             {{- /* Change this field for a diferent ttl! */ -}}

    {{- /* Iterate over all of the registered Nomad services */ -}}
    {{- range nomadServices -}}
        {{ $service := . }}

        {{- /* Iterate over all of the instances of a services */ -}}
        {{- range nomadService $service.Name -}}
            {{ $svc := . }}


            {{- /* Generate a uniq label for IP */ -}}
            {{- $node := $svc.Address | md5sum | sprig_trunc 8 }}

            {{- /* Record A & SRV RRs */ -}}
            {{- $rr_a = sprig_append $rr_a (sprig_list $svc.Name $svc.Address) -}}
            {{- $rr_a = sprig_append $rr_a (sprig_list $node $svc.Address) -}}
            {{- $rr_srv = sprig_append $rr_srv (sprig_list $svc.Name $svc.Port $node) -}}
        {{- end -}}
    {{- end -}}

    {{- /* Iterate over lists and print everything */ -}}

    {{- /* Only the latest record will get returned - see https://github.com/looterz/grimd/issues/114 */ -}}
    {{ range $rr_srv -}}
    "{{ printf "%-45s %s %s %d %d %6d %s" (sprig_nospace (sprig_cat (index . 0) $base_domain ".srv")) "IN" "SRV" 0 0 (index . 1) (sprig_nospace (sprig_cat (index . 2) $base_domain )) }}",
    {{ end -}}

    {{- range $rr_a | sprig_uniq -}}
    "{{ printf "%-45s %4d %s %4s %s" (sprig_nospace (sprig_cat (index . 0) $base_domain)) $ttl "IN" "A" (sprig_last . ) }}",
    {{ end -}}

Templating works very well with leng because of its fast startup and small Docker image. When Nomad restarts the task because of a change in the template, leng will be back up in seconds or less.

Proxy discovery with DNS-based routing

You can tweak this further to direct DNS to your ingress proxy, rather than directly to each container.

For example, if you are using traefik, you could:

  1. Add services to traefik by default (with defaultRule setting) for example:
# in traefik.toml
providers.nomad.defaultRule = "Host(`{{"{{ .Name }}"}}.traefik`)"`)
  1. Add a DNS A record pointing to traefik for each service:
customdnsrecords = [
    {{- $ttl := 3600 -}}             {{- /* Change this field for a diferent ttl! */ -}}
    {{- $traefik_ip := "10.10.4.1" -}}             {{- /* Change this field to the IP of your traefik! */ -}}
    {{- range nomadServices -}}
        {{ $service := . }}

        {{- /* Iterate over all of the instances of a services */ -}}
        {{- range nomadService $service.Name -}}
            {{- /* A records to traefik IP: */ -}}
            "{{ printf "%-45s %4d %s %4s %s" (sprig_nospace (sprig_cat .Name ".traefik")) $ttl "IN" "A" $traefik_ip }}",
        {{ end }}
    {{ end }}
]

The end result: if you visit grafana.traefik, you will get directed to the instance where traefik is running, and traefik will proxy your request to the actual instance where the grafana service is running!

Example diagram for discovering a grafana service:

graph TD
    J("🐳 Nomad Job (grafana)\n at 10.0.0.3") -->|" registers `grafana` service "| N[Nomad]
    U("You (or some container)") -->|queries DNS| L
    T -->|discovers services from| N
    T("🛣 Traefik Job at 10.0.0.1") -->|proxies for| J
    N -->|renders template| L["⚡ Leng \n `grafana.traefik. IN A 10.10.0.1` "]
    L -->|reaches| T

Acknowledgments

The templating are modified versions of this gist by m1keil.

Some parameters may require your attention, such as path and executable name.

systemd

Create /etc/systemd/services/leng.service and paste in the following.

[Unit]
Description=leng dns proxy
Documentation=https://github.com/looterz/leng
After=network.target

[Service]
User=root
WorkingDirectory=/root/grim
LimitNOFILE=4096
PIDFile=/var/run/leng/leng.pid
ExecStart=/root/grim/leng_linux_x64 -update
Restart=always
StartLimitInterval=30

[Install]
WantedBy=multi-user.target

init.d

Create /etc/init.d/leng and paste in the following.

#!/bin/bash
# leng daemon
# chkconfig: 345 20 80
# description: leng daemon
# processname: leng

DAEMON_PATH="/root/grim"

DAEMON=leng
DAEMONOPTS="-update"

NAME=leng
DESC="https://github.com/looterz/leng"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

case "$1" in
start)
	printf "%-50s" "Starting $NAME..."
	cd $DAEMON_PATH
	PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!`
	#echo "Saving PID" $PID " to " $PIDFILE
        if [ -z $PID ]; then
            printf "%s\n" "Fail"
        else
            echo $PID > $PIDFILE
            printf "%s\n" "Ok"
        fi
;;
status)
        printf "%-50s" "Checking $NAME..."
        if [ -f $PIDFILE ]; then
            PID=`cat $PIDFILE`
            if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then
                printf "%s\n" "Process dead but pidfile exists"
            else
                echo "Running"
            fi
        else
            printf "%s\n" "Service not running"
        fi
;;
stop)
        printf "%-50s" "Stopping $NAME"
            PID=`cat $PIDFILE`
            cd $DAEMON_PATH
        if [ -f $PIDFILE ]; then
            kill -HUP $PID
            printf "%s\n" "Ok"
            rm -f $PIDFILE
        else
            printf "%s\n" "pidfile not found"
        fi
;;

restart)
  	$0 stop
  	$0 start
;;

*)
        echo "Usage: $0 {status|start|stop|restart}"
        exit 1
esac

rc.d

Create /etc/rc.d/leng and paste in the following.

#!/bin/sh
#
# $FreeBSD$
#
# PROVIDE: leng
# REQUIRE: NETWORKING SYSLOG
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf to enable leng:
#
#leng_enable="YES"

. /etc/rc.subr

name="leng"
rcvar="leng_enable"

load_rc_config $name

: ${leng_user:="root"}
: ${leng_enable:="NO"}
: ${leng_directory:="/root/grim"}

command="${leng_directory}/leng -update"

pidfile="${leng_directory}/${name}.pid"

start_cmd="export USER=${leng_user}; export HOME=${leng_directory}; /usr/sbin/daemon -f -u ${leng_user} -p ${pidfile} $command"

#stop_cmd="kill $(cat $pidfile)"
stop_cmd="${name}_stop"
leng_stop() {
	if [ ! -f $pidfile ]; then
		echo "leng PID File not found. Maybe leng is not running?"
	else
		kill $(cat $pidfile)
	fi
}

run_rc_command "$1"

or

#
# OpenBSD
#
daemon="<path_to_daemon>"  

. /etc/rc.d/rc.subr

rc_bg=YES
rc_reload=NO

rc_cmd $1