Skip to main content

DNS policy module (v2)

This Linux C ABI v2 example shows how to control DNS queries and replace DNS responses. It uses libresolv to parse DNS messages and a module-owned buffer to store rewritten responses.

Example behavior​

Query nameResult
allow.testEnd the query hook chain and continue normal DNS resolution.
block.testReturn REFUSED without querying an upstream server.
drop.testDrop the query without an upstream lookup or a DNS response.
redirect.testReturn 192.0.2.42 for A or 2001:db8::42 for AAAA, with a TTL of 60 seconds.
rewrite.testChange A records in an unsigned response to 203.0.113.7 and set their TTL to 60 seconds.
Other namesContinue to the next module and normal processing.

For allow.test and rewrite.test, configure a test DNS server to provide answers for those names. The example does not create their upstream records. For rewrite.test, a response with no matching A records or with signature records is returned unchanged.

ALLOW ends only the current hook chain. It does not bypass domain rules or transport checks, and response hooks can still change the result. See the DNS action rules.

Download and build​

Download policy.c and mudfish_dns_module.h into the same directory. The header reference includes the complete header for reading in your browser.

On Linux, install a C compiler and development headers for the standard C library and libresolv. From the download directory, run:

cc -std=c11 -shared -fPIC -Wall -Wextra -Werror -I. \
policy.c -lresolv -o policy.so

Follow Load a module to install policy.so and restart the service. Start DNS in the app before testing queries.

For a local test using the default DNS listener, these queries demonstrate blocking and address synthesis (dig must be installed):

dig @127.0.0.1 block.test A
dig @127.0.0.1 redirect.test A
dig @127.0.0.1 redirect.test AAAA

The first query should return REFUSED; the next two should return the addresses shown in the table. If you changed the listener address or port, adjust the commands accordingly. A query for drop.test times out by design.

How it works​

The query and response callbacks return 0 on success, with the decision in result->action. They do not return action constants directly.

Initialization allocates a response buffer in the module context. The response hook copies the borrowed response into that buffer before editing it, and destroy() frees the context. The replacement buffer stays valid after the callback returns, as required by the buffer ownership rules.

The response hook leaves signed messages unchanged. It does not validate or re-sign DNSSEC data. Synthetic and replaced responses are not stored in the host cache.

Complete source​

policy.c
#include "mudfish_dns_module.h"

#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

struct policy {
uint8_t response[65535];
};

static int
question(const struct mudfish_dns_event *event, ns_rr *rr)
{
ns_msg message;

if (event->query.len > 65535 ||
ns_initparse(event->query.data, (int)event->query.len, &message) < 0)
return (-1);
return (ns_parserr(&message, ns_s_qd, 0, rr));
}

static int32_t
dns_query(void *context, const struct mudfish_dns_event *event,
struct mudfish_dns_result *result)
{
ns_rr rr;

(void)context;
if (question(event, &rr) < 0)
return (-1);
if (strcasecmp(ns_rr_name(rr), "allow.test") == 0)
result->action = MUDFISH_DNS_ALLOW;
else if (strcasecmp(ns_rr_name(rr), "block.test") == 0)
result->action = MUDFISH_DNS_BLOCK;
else if (strcasecmp(ns_rr_name(rr), "drop.test") == 0)
result->action = MUDFISH_DNS_DROP;
else if (strcasecmp(ns_rr_name(rr), "redirect.test") == 0) {
result->action = MUDFISH_DNS_ANSWER;
result->ttl = 60;
if (ns_rr_type(rr) == ns_t_aaaa) {
result->address_family = MUDFISH_DNS_IPV6;
inet_pton(AF_INET6, "2001:db8::42", result->address);
} else {
result->address_family = MUDFISH_DNS_IPV4;
inet_pton(AF_INET, "192.0.2.42", result->address);
}
}
return (0);
}

static int32_t
dns_response(void *context, const struct mudfish_dns_event *event,
struct mudfish_dns_result *result)
{
struct policy *policy = context;
ns_msg message;
ns_rr rr;
uint8_t *rdata;
int section, i, changed = 0;

if (question(event, &rr) < 0)
return (-1);
if (strcasecmp(ns_rr_name(rr), "rewrite.test") != 0)
return (0);
if (event->response.len > sizeof(policy->response))
return (-1);
memcpy(policy->response, event->response.data, event->response.len);
if (ns_initparse(policy->response, (int)event->response.len, &message) < 0)
return (-1);
/* This example only edits unsigned answers. A policy that edits signed
* data must also rebuild/remove the affected signatures.
*/
for (section = ns_s_an; section <= ns_s_ar; section++) {
for (i = 0; i < ns_msg_count(message, section); i++) {
if (ns_parserr(&message, (ns_sect)section, i, &rr) < 0)
return (-1);
if (ns_rr_type(rr) == ns_t_rrsig || ns_rr_type(rr) == ns_t_sig ||
ns_rr_type(rr) == ns_t_tsig)
return (0);
}
}
for (i = 0; i < ns_msg_count(message, ns_s_an); i++) {
if (ns_parserr(&message, ns_s_an, i, &rr) < 0)
return (-1);
if (ns_rr_class(rr) != ns_c_in || ns_rr_type(rr) != ns_t_a ||
ns_rr_rdlen(rr) != 4)
continue;
/* rdata belongs to our copy. TTL precedes RDLENGTH (2 bytes). */
rdata = policy->response + (ns_rr_rdata(rr) - policy->response);
inet_pton(AF_INET, "203.0.113.7", rdata);
ns_put32(60, rdata - 6);
changed = 1;
}
if (changed) {
result->action = MUDFISH_DNS_REPLACE;
result->response.data = policy->response;
result->response.len = event->response.len;
}
return (0);
}

static void
destroy(void *context)
{

free(context);
}

int32_t
mudfish_dns_module_init_v2(uint32_t abi_version, size_t module_size,
struct mudfish_dns_module_v2 *module)
{

if (abi_version != MUDFISH_DNS_MODULE_ABI_V2 || module_size != sizeof(*module))
return (-1);
module->context = calloc(1, sizeof(struct policy));
if (module->context == NULL)
return (-1);
module->on_dns_query = dns_query;
module->on_dns_response = dns_response;
module->destroy = destroy;
return (0);
}