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 name | Result |
|---|---|
allow.test | End the query hook chain and continue normal DNS resolution. |
block.test | Return REFUSED without querying an upstream server. |
drop.test | Drop the query without an upstream lookup or a DNS response. |
redirect.test | Return 192.0.2.42 for A or 2001:db8::42 for AAAA, with a TTL of 60 seconds. |
rewrite.test | Change A records in an unsigned response to 203.0.113.7 and set their TTL to 60 seconds. |
| Other names | Continue 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
#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);
}