Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dns/dnsmasq-to-unbound/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
work/
__pycache__/
*.pyc
8 changes: 8 additions & 0 deletions dns/dnsmasq-to-unbound/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PLUGIN_NAME= dnsmasq-to-unbound
PLUGIN_VERSION= 1.0
PLUGIN_REVISION= 0
PLUGIN_DEPENDS= dnsmasq
PLUGIN_COMMENT= Register dnsmasq DHCP leases and static hosts in Unbound DNS
PLUGIN_MAINTAINER= chall37@users.noreply.github.com

.include "../../Mk/plugins.mk"
140 changes: 140 additions & 0 deletions dns/dnsmasq-to-unbound/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Dnsmasq to Unbound DNS Registration

This OPNsense plugin automatically registers dnsmasq DHCP leases and static host entries in Unbound DNS, enabling local hostname resolution for DHCP clients.

> **Note:** This plugin is intended as a stopgap solution until native integration between Unbound and a supported DHCP service is implemented in OPNsense core.

## Background

OPNsense offers three DHCP server options, each with limitations for Unbound DNS integration:

| DHCP Server | Dynamic Lease DNS | Static Reservation DNS | Status |
|-------------|-------------------|------------------------|--------|
| ISC DHCP | Buggy | Yes | End-of-life, deprecated |
| Kea DHCP | **No** | Yes (requires Unbound restart) | Active, DNS integration deprioritized |
| dnsmasq | Built-in DNS only | Built-in DNS only | Active |

### ISC DHCP (Deprecated)

ISC DHCP had Unbound integration via `unbound_watcher.py`, but it suffers from reliability issues where the [watcher daemon silently crashes](https://github.com/opnsense/core/issues/8075) when encountering malformed hostnames, stopping all subsequent DNS registration until Unbound is restarted. ISC DHCP reached end-of-life in 2022 and is being phased out of OPNsense.

### Kea DHCP

Kea is ISC's strategic replacement but currently only supports static reservation DNS registration in Unbound - dynamic leases are [not registered](https://github.com/opnsense/core/issues/7475). Static reservations also require an Unbound restart to take effect. This limitation is acknowledged but deprioritized by the OPNsense team due to architectural complexity concerns.

### dnsmasq

dnsmasq includes its own DNS server with automatic lease registration, but many users prefer Unbound for its DNSSEC validation, DNS-over-TLS support, and advanced caching. When using Unbound as the primary resolver, dnsmasq's internal DNS registrations are not directly accessible.

**Query forwarding** from Unbound to dnsmasq is possible but problematic:

- Forwarding is either brittle or incurs a performance penalty: Unbound either needs explicit knowledge of every domain served by dnsmasq (requiring configuration to stay in sync), or all queries must be routed through dnsmasq first, adding latency to every DNS lookup and negating Unbound's direct recursive resolution capabilities.
- Static reservations [don't inherit the system domain](https://github.com/opnsense/core/issues/8612) - each must have the domain manually specified or queries fail.
- Domain overrides [may not apply consistently](https://github.com/opnsense/core/issues/9277) to static mappings vs dynamic leases.
- Requires additional configuration for `private-domain` (rebind protection exemption) and `domain-insecure` (DNSSEC exemption) for each local domain.

### This Plugin

This plugin bridges the gap by directly registering dnsmasq DHCP data into Unbound via `unbound-control`, providing the simplicity of dnsmasq DHCP with the features of Unbound DNS. It avoids the reliability issues of the ISC DHCP watcher by using a more robust file-watching mechanism and graceful error handling.

## Features

- Watches dnsmasq lease file and static hosts for changes
- Registers A and PTR records in Unbound DNS
- Supports multiple domains via dnsmasq's IP-range-to-domain mapping
- Deduplicates records (static entries take precedence over leases)
- Automatic cleanup of stale records
- System status notifications in OPNsense web UI
- Periodic reconciliation to handle Unbound restarts

## Requirements

- OPNsense with Unbound DNS resolver enabled (remote control is enabled by default)
- dnsmasq plugin installed and configured with DHCP

## Installation

Install via the OPNsense plugin system or manually:

```
pkg install os-dnsmasq-to-unbound
```

## Configuration

Navigate to **Services > Dnsmasq to Unbound** in the OPNsense web UI.

### Settings

| Option | Description |
|--------|-------------|
| Enable | Enable/disable the DNS registration service |
| Watch Leases | Register DNS entries for DHCP leases |
| Watch Static | Register DNS entries for static host mappings |
| Domain Filter | Limit registration to specific domains (comma-separated) |

### Domain Configuration

The plugin reads domain configuration from dnsmasq's configuration:

- **Global domain**: `domain=lan` in dnsmasq.conf
- **Range-specific domains**: `domain=guest,192.168.20.1,192.168.20.254`

If no domain is configured in dnsmasq, DHCP leases cannot be registered (static hosts with explicit domains will still work).

## How It Works

1. The daemon watches `/var/db/dnsmasq.leases` and `/var/etc/dnsmasq-hosts` for changes
2. When changes are detected, it parses the files and compares with current Unbound state
3. New records are added, changed records are updated, and stale records are removed
4. Records are marked with a TXT record (`managed-by=dnsmasq-to-unbound`) for identification
5. Every 5 minutes, a full reconciliation runs to catch any missed changes

## Troubleshooting

### Service Status

Check service status via CLI:
```
configctl dnsmasqtounbound status
```

View registered records:
```
configctl dnsmasqtounbound listrecords
```

### System Logs

Check system logs for errors:
```
grep dnsmasq_watcher /var/log/system/latest.log
```

### Common Issues

**"Unbound remote control not enabled"**
- This should not normally occur as OPNsense enables remote control by default
- Check that Unbound is running and restart if necessary

**"No domain configured in dnsmasq.conf"**
- Add `domain=lan` (or your domain) to dnsmasq configuration

**Records not appearing**
- Verify the service is running
- Check that Unbound is running and controllable
- Ensure domains match the domain filter (if configured)

### Status Notifications

The plugin reports status via OPNsense's system status indicator:

| Status | Meaning |
|--------|---------|
| OK (green) | Service running normally |
| Warning (yellow) | Some records skipped (check logs) |
| Error (red) | Service failed (check logs for details) |

## License

BSD 2-Clause License. See source files for full license text.
8 changes: 8 additions & 0 deletions dns/dnsmasq-to-unbound/pkg-descr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enables Unbound DNS to automatically register hostnames from
dnsmasq DHCP leases and static reservations.

Watches /var/db/dnsmasq.leases for changes and updates Unbound
DNS records via unbound-control, allowing DHCP clients to be
resolved by hostname without running dnsmasq's DNS server.

WWW: https://github.com/opnsense/plugins
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* Copyright (C) 2025 C. Hall (chall37@users.noreply.github.com)
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/

/**
* Check if the dnsmasq to unbound service is enabled
* @return bool
*/
function dnsmasqtounbound_enabled()
{
$model = new \OPNsense\DnsmasqToUnbound\DnsmasqToUnbound();
return (string)$model->enabled == '1';
}

/**
* Register dnsmasq to unbound service for the dashboard widget
* @return array
*/
function dnsmasqtounbound_services()
{
$services = [];

if (!dnsmasqtounbound_enabled()) {
return $services;
}

$services[] = [
'description' => gettext('Dnsmasq to Unbound Watcher'),
'configd' => [
'restart' => ['dnsmasqtounbound restart'],
'start' => ['dnsmasqtounbound start'],
'stop' => ['dnsmasqtounbound stop'],
],
'pidfile' => '/var/run/dnsmasq_watcher.pid',
'name' => 'dnsmasq_watcher',
];

return $services;
}

/**
* Register configuration sections for HA sync
* @return array
*/
function dnsmasqtounbound_xmlrpc_sync()
{
$result = [];
$result['id'] = 'dnsmasqtounbound';
$result['section'] = 'OPNsense.DnsmasqToUnbound';
$result['description'] = gettext('Dnsmasq to Unbound');
$result['services'] = ['dnsmasq_watcher'];
return [$result];
}

/**
* Register syslog facility
* @return array
*/
function dnsmasqtounbound_syslog()
{
$syslogconf = [];
$syslogconf['dnsmasq_watcher'] = ['facility' => ['dnsmasq_watcher']];
return $syslogconf;
}
83 changes: 83 additions & 0 deletions dns/dnsmasq-to-unbound/src/etc/rc.d/dnsmasq_watcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/sh

# Copyright (c) 2025 C. Hall (chall37@users.noreply.github.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

#
# PROVIDE: dnsmasq_watcher
# REQUIRE: DAEMON unbound
# KEYWORD: shutdown
#

. /etc/rc.subr

name=dnsmasq_watcher
rcvar=dnsmasq_watcher_enable
command=/usr/local/opnsense/scripts/unbound/dnsmasq_watcher.py
command_interpreter=/usr/local/bin/python3
pidfile="/var/run/${name}.pid"

load_rc_config $name

: ${dnsmasq_watcher_enable:=NO}

start_postcmd=dnsmasq_watcher_poststart
stop_cmd=dnsmasq_watcher_stop

dnsmasq_watcher_poststart()
{
# Give the daemon time to initialize
for i in 1 2 3 4 5; do
sleep 1
if [ -s ${pidfile} ]; then
break
fi
done
}

dnsmasq_watcher_stop()
{
if [ -z "$rc_pid" ]; then
[ -n "$rc_fast" ] && return 0
_run_rc_notrunning
return 1
fi
echo -n "Stopping ${name}."
kill -15 ${rc_pid}
# Wait max 2 seconds for graceful exit
for i in $(seq 1 20); do
if [ -z "`/bin/ps -p ${rc_pid} -o pid=`" ]; then
break
fi
sleep 0.1
done
# Force kill if still running
if [ -n "`/bin/ps -p ${rc_pid} -o pid=`" ]; then
kill -9 ${rc_pid} >/dev/null 2>&1
fi
rm -f ${pidfile}
echo "done."
}

run_rc_command $1
Loading