DEV Community

Cover image for Setting Up a Local DNS Resolver with Custom Domains on Fedora Using Unbound
Caleb Ajibade
Caleb Ajibade

Posted on AI-assisted

Setting Up a Local DNS Resolver with Custom Domains on Fedora Using Unbound

I recently gave myself a simple networking task:

Set up a local DNS resolver and add custom domains.

Before jumping into Unbound and configuration files, it's worth understanding what we're actually building.

If you've used the internet, you've used DNS constantly — usually without realizing it.

What Is DNS?

DNS (Domain Name System) is the system that translates human-readable domain names into IP addresses.

Computers communicate over networks using IP addresses.

For example, a server might have an address like:

142.250.151.100
Enter fullscreen mode Exit fullscreen mode

Humans, however, don't want to remember IP addresses for every website they visit.

We'd much rather type:

google.com
Enter fullscreen mode Exit fullscreen mode

DNS provides the mapping between the two:

google.com
      │
      │ DNS
      ▼
142.250.151.100
Enter fullscreen mode Exit fullscreen mode

You can think of DNS somewhat like the contacts app on your phone.

You don't memorize:

+234 801 234 5678
Enter fullscreen mode Exit fullscreen mode

You save:

John
Enter fullscreen mode Exit fullscreen mode

and your phone knows which number belongs to John.

DNS does something similar:

google.com → 142.250.x.x
github.com → 140.82.x.x
Enter fullscreen mode Exit fullscreen mode

This means when you enter:

https://google.com
Enter fullscreen mode Exit fullscreen mode

your computer first needs to answer an important question:

What IP address should I connect to for google.com?

That's where a DNS resolver comes in.

What Is a DNS Resolver?

A DNS resolver is the component responsible for taking your DNS question and finding an answer.

For example, your browser wants to visit:

google.com
Enter fullscreen mode Exit fullscreen mode

It needs an IP address, so a simplified version of what happens looks like this:

Browser
   │
   │ "What is the IP for google.com?"
   ▼
DNS Resolver
   │
   │ finds the answer
   ▼
142.250.151.100
   │
   ▼
Browser connects to server
Enter fullscreen mode Exit fullscreen mode

Usually, you don't manually choose the resolver every time you browse the internet.

Your computer receives DNS configuration from your network, often through your router.

Your ISP may provide the DNS server, or you might use a public resolver such as Google's or Cloudflare's.

For example:

8.8.8.8 → Google Public DNS
1.1.1.1 → Cloudflare DNS
Enter fullscreen mode Exit fullscreen mode

So DNS is the overall naming system, while a DNS resolver is one of the components that helps you find answers within that system.

That's an important distinction:

DNS
│
└── The overall system for translating names into network information

DNS Resolver
│
└── A service that receives DNS questions and finds/returns answers
Enter fullscreen mode Exit fullscreen mode

Why Run My Own DNS Resolver?

This was the part that made the project interesting.

Instead of relying entirely on whatever DNS resolver my network gives me, I can run one on my own computer.

For example:

My Computer
     │
     │ google.com?
     ▼
My DNS Resolver
     │
     ▼
142.250.x.x
Enter fullscreen mode Exit fullscreen mode

But there's another benefit.

Because I control the resolver, I can teach it names that don't exist on the public internet.

For example, I can create:

myserver.home.arpa
Enter fullscreen mode Exit fullscreen mode

and decide:

myserver.home.arpa → 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Or:

website.home.arpa → 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Nobody needs to register these domains with a public domain registrar.

My resolver knows about them because I told it about them.

So our resolver will eventually be able to handle two types of questions:

                    My DNS Resolver
                         │
             ┌───────────┴───────────┐
             │                       │
             ▼                       ▼
       Public domain             My domain
        google.com         website.home.arpa
             │                       │
             ▼                       ▼
       Ask upstream DNS         Local record
             │                       │
             ▼                       ▼
       142.250.x.x              127.0.0.1
Enter fullscreen mode Exit fullscreen mode

That's what we're going to build.

What We're Building

We'll use Unbound, a DNS resolver, and run it locally on Fedora.

By the end, we'll have this:

                    ┌─────────────────────┐
                    │       Unbound       │
                    │     127.0.0.1:53    │
                    └──────────┬──────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
              ▼                ▼                ▼
       google.com       myserver.home.arpa   website.home.arpa
              │                │                │
              ▼                └───────┬────────┘
        Upstream DNS                   ▼
              │                    127.0.0.1
              ▼
       Google's IP
Enter fullscreen mode Exit fullscreen mode

Let's build it incrementally.

1. What DNS Am I Already Using?

Before installing anything, I wanted to see how DNS currently worked on my machine.

On Fedora, I ran:

resolvectl status
Enter fullscreen mode Exit fullscreen mode

My Wi-Fi interface showed:

Current DNS Server: 192.168.18.1
DNS Servers: 192.168.18.1
Enter fullscreen mode Exit fullscreen mode

192.168.18.1 is my router.

I also checked:

cat /etc/resolv.conf
Enter fullscreen mode Exit fullscreen mode

and found:

nameserver 127.0.0.53
Enter fullscreen mode Exit fullscreen mode

At first this might seem contradictory.

Why does one command say 192.168.18.1 while another says 127.0.0.53?

Because Fedora is using systemd-resolved.

The path is roughly:

Application
    │
    ▼
127.0.0.53
systemd-resolved
    │
    ▼
192.168.18.1
Router
    │
    ▼
Internet DNS
Enter fullscreen mode Exit fullscreen mode

127.0.0.53 is a local DNS stub provided by systemd-resolved.

The actual upstream DNS server in my case was being provided by my network.

We can see this when querying a domain:

dig google.com
Enter fullscreen mode Exit fullscreen mode

Part of my output looked like:

;; ANSWER SECTION:
google.com.    214    IN    A    142.251.209.110

;; SERVER: 127.0.0.53#53
Enter fullscreen mode Exit fullscreen mode

So normal DNS was already working.

Now I wanted to create another DNS resolver that I controlled myself.

2. Install Unbound

I chose Unbound as the resolver.

Install it on Fedora with:

sudo dnf install unbound
Enter fullscreen mode Exit fullscreen mode

Then verify the installation:

unbound -V
Enter fullscreen mode Exit fullscreen mode

I was running:

Version 1.26.0
Enter fullscreen mode Exit fullscreen mode

Start the service:

sudo systemctl start unbound
Enter fullscreen mode Exit fullscreen mode

Then check its status:

systemctl status unbound
Enter fullscreen mode Exit fullscreen mode

It should show something like:

Active: active (running)
Enter fullscreen mode Exit fullscreen mode

3. Is Unbound Actually Listening for DNS Requests?

DNS normally uses port 53, so we can inspect what's listening on that port:

sudo ss -tulpn | grep ':53'
Enter fullscreen mode Exit fullscreen mode

Among the output, I found:

127.0.0.1:53
Enter fullscreen mode Exit fullscreen mode

being used by Unbound.

This means I now effectively had two local DNS-related services:

127.0.0.53 → systemd-resolved
127.0.0.1  → Unbound
Enter fullscreen mode Exit fullscreen mode

That's an important distinction for the rest of this tutorial.

4. Query Unbound Directly

Normally I can run:

dig google.com
Enter fullscreen mode Exit fullscreen mode

But that uses my system-configured DNS.

dig also lets us explicitly choose which DNS server to query.

The syntax is:

dig @DNS_SERVER DOMAIN
Enter fullscreen mode Exit fullscreen mode

Therefore, to query Unbound:

dig @127.0.0.1 google.com
Enter fullscreen mode Exit fullscreen mode

Initially, mine returned:

status: SERVFAIL
Enter fullscreen mode Exit fullscreen mode

Unbound was running and receiving my request, but wasn't successfully resolving the domain.

So I configured upstream DNS servers.

5. Configure Unbound

Create a custom configuration file:

sudo nano /etc/unbound/conf.d/local.conf
Enter fullscreen mode Exit fullscreen mode

I started with:

server:
    interface: 127.0.0.1
    access-control: 127.0.0.0/8 allow

forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 8.8.8.8
Enter fullscreen mode Exit fullscreen mode

The important part here is:

forward-zone:
    name: "."
Enter fullscreen mode Exit fullscreen mode

The "." represents the DNS root, so this forwarding rule catches queries that aren't answered locally.

The two forward-addr entries specify upstream DNS resolvers.

Our setup is now:

dig
 │
 │ google.com?
 ▼
Unbound
127.0.0.1
 │
 │ I don't have a local record
 ▼
Upstream DNS
 │
 ▼
google.com IP
Enter fullscreen mode Exit fullscreen mode

6. Validate Before Restarting

Instead of blindly restarting Unbound and hoping the configuration works, we can validate it first:

sudo unbound-checkconf
Enter fullscreen mode Exit fullscreen mode

A valid configuration should give:

unbound-checkconf: no errors in /etc/unbound/unbound.conf
Enter fullscreen mode Exit fullscreen mode

Then restart:

sudo systemctl restart unbound
Enter fullscreen mode Exit fullscreen mode

Now try again:

dig @127.0.0.1 google.com
Enter fullscreen mode Exit fullscreen mode

This time I got:

status: NOERROR
Enter fullscreen mode Exit fullscreen mode

and:

;; ANSWER SECTION:
google.com.    150    IN    A    142.250.151.138
google.com.    150    IN    A    142.250.151.100
google.com.    150    IN    A    142.250.151.101
...
Enter fullscreen mode Exit fullscreen mode

More importantly:

;; SERVER: 127.0.0.1#53
Enter fullscreen mode Exit fullscreen mode

We now have a working local DNS resolver.

7. Creating Our Own Domains

Now for the fun part.

I wanted domains that don't exist on the public internet:

myserver.home.arpa
website.home.arpa
Enter fullscreen mode Exit fullscreen mode

I used home.arpa rather than something like .local.

home.arpa is reserved for naming resources within home networks, while .local has special meaning for multicast DNS (mDNS).

Edit the configuration again:

sudo nano /etc/unbound/conf.d/local.conf
Enter fullscreen mode Exit fullscreen mode

My complete configuration became:

server:
    interface: 127.0.0.1
    access-control: 127.0.0.0/8 allow

    local-zone: "home.arpa." static

    local-data: "myserver.home.arpa. IN A 127.0.0.1"
    local-data: "website.home.arpa. IN A 127.0.0.1"

forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 8.8.8.8
Enter fullscreen mode Exit fullscreen mode

Check it again:

sudo unbound-checkconf
Enter fullscreen mode Exit fullscreen mode

Then:

sudo systemctl restart unbound
Enter fullscreen mode Exit fullscreen mode

8. Test the Custom Domains

Now let's ask our resolver:

dig @127.0.0.1 myserver.home.arpa
Enter fullscreen mode Exit fullscreen mode

I got:

;; ANSWER SECTION:
myserver.home.arpa.    3600    IN    A    127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Try the second domain:

dig @127.0.0.1 website.home.arpa
Enter fullscreen mode Exit fullscreen mode

Result:

;; ANSWER SECTION:
website.home.arpa.    3600    IN    A    127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Both queries took essentially no time:

Query time: 0 msec
Enter fullscreen mode Exit fullscreen mode

That's because Unbound already knows the answers. They're defined directly in its configuration.

9. What Does the A Mean?

Consider:

website.home.arpa. IN A 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

This is a DNS A record.

An A record maps a hostname to an IPv4 address:

website.home.arpa
        │
        │ A record
        ▼
    127.0.0.1
Enter fullscreen mode Exit fullscreen mode

We could point another name at another machine on our network:

local-data: "server.home.arpa. IN A 192.168.18.50"
Enter fullscreen mode Exit fullscreen mode

Now our resolver would answer:

server.home.arpa → 192.168.18.50
Enter fullscreen mode Exit fullscreen mode

This is essentially the same fundamental concept used by public DNS.

The difference is that these particular records only exist inside our resolver.

10. Local Domains vs Internet Domains

We can now ask the same DNS server three questions:

dig @127.0.0.1 google.com

dig @127.0.0.1 myserver.home.arpa

dig @127.0.0.1 website.home.arpa
Enter fullscreen mode Exit fullscreen mode

Conceptually:

                        Unbound
                      127.0.0.1
                          │
           ┌──────────────┼──────────────┐
           │              │              │
           ▼              ▼              ▼

      google.com     myserver...    website...
           │              │              │
           ▼              ▼              ▼
      Not local        LOCAL          LOCAL
           │              │              │
           ▼              └──────┬───────┘
     Upstream DNS                ▼
           │                 127.0.0.1
           ▼
     Google IP
Enter fullscreen mode Exit fullscreen mode

That's the part that made DNS click for me.

A DNS resolver isn't necessarily some mysterious server sitting somewhere on the internet.

I can run one on my own laptop.

And I can tell it:

If someone asks for this name, give them this IP address.

11. Why Didn't I Change Fedora's DNS?

There was one more decision to make.

I could configure Fedora to use:

127.0.0.1
Enter fullscreen mode Exit fullscreen mode

as its normal DNS server.

Then applications would automatically use Unbound.

For this experiment, however, I decided not to change my system DNS.

My normal Fedora DNS remains:

Applications
     │
     ▼
systemd-resolved
127.0.0.53
     │
     ▼
Router
     │
     ▼
Internet
Enter fullscreen mode Exit fullscreen mode

And my experimental resolver remains:

dig @127.0.0.1
       │
       ▼
    Unbound
       │
       ├── custom domains
       │
       └── upstream DNS
Enter fullscreen mode Exit fullscreen mode

That means I can experiment with Unbound without accidentally breaking DNS for the rest of my system.

When I want to test it, I explicitly use:

dig @127.0.0.1 example.com
Enter fullscreen mode Exit fullscreen mode

12. Starting and Stopping the Resolver

Because I'm using this as a local experiment, I don't necessarily need Unbound running every time my computer starts.

Start it with:

sudo systemctl start unbound
Enter fullscreen mode Exit fullscreen mode

Check it:

systemctl status unbound
Enter fullscreen mode Exit fullscreen mode

And stop it:

sudo systemctl stop unbound
Enter fullscreen mode Exit fullscreen mode

If I eventually decide I want it permanently available, I can enable it:

sudo systemctl enable --now unbound
Enter fullscreen mode Exit fullscreen mode

What I Learned

This was a small project, but it cleared up several concepts I'd previously treated as separate things.

I now have a much better mental model of:

  • What DNS actually does
  • What a DNS resolver is
  • The difference between a DNS resolver and the overall DNS system
  • The difference between my system's DNS stub and its upstream DNS server
  • Why 127.0.0.53 appears in /etc/resolv.conf
  • How DNS uses port 53
  • How to query a specific DNS server with dig
  • How DNS forwarding works
  • What an A record does
  • How custom/private DNS names can exist without being registered publicly
  • How a local resolver can answer some queries itself and forward others upstream

The final setup is surprisingly small:

                    My Laptop
                       │
              ┌────────┴─────────┐
              │                  │
        Normal Fedora       My DNS Lab
              │                  │
       127.0.0.53           127.0.0.1
              │                  │
    systemd-resolved           Unbound
              │                  │
           Router         ┌──────┴──────┐
              │           │             │
         Internet     Local DNS      Internet
                      records        forwarding
Enter fullscreen mode Exit fullscreen mode

And all it took to create a domain of my own was essentially:

local-data: "website.home.arpa. IN A 127.0.0.1"
Enter fullscreen mode Exit fullscreen mode

Obviously, this is only the beginning of DNS.

But building a tiny working version locally made the bigger system much easier to understand.


Environment used:

  • Fedora Linux
  • Unbound 1.26
  • systemd-resolved
  • dig / BIND utilities

Top comments (0)