translate DNS queries to avahi-resolve
1/*
2 * Copyright (c) 2026 joshua stein <jcs@jcs.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17#include <sys/types.h>
18#include <sys/socket.h>
19#include <sys/wait.h>
20
21#include <arpa/inet.h>
22#include <netinet/in.h>
23
24#include <err.h>
25#include <errno.h>
26#include <netdb.h>
27#include <signal.h>
28#include <stdarg.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32#include <syslog.h>
33#include <unistd.h>
34
35#define DNS_HEADER_SIZE 12
36#define DNS_MAX_NAME 255
37#define DNS_MAX_LABEL 63
38#define DNS_TYPE_A 1
39#define DNS_TYPE_NS 2
40#define DNS_TYPE_AAAA 28
41#define DNS_CLASS_IN 1
42#define DNS_RCODE_NOERROR 0
43#define DNS_RCODE_NXDOMAIN 3
44
45#define DNS_FLAG_QR 0x8000 /* response */
46#define DNS_FLAG_AA 0x0400 /* authoritative */
47#define DNS_FLAG_RD 0x0100 /* recursion desired */
48#define DNS_FLAG_RA 0x0080 /* recursion available */
49
50int debug, verbose;
51
52__dead void usage(void);
53void logmsg(const char *, ...);
54int dns_parse_name(uint8_t *, size_t, size_t, char *, size_t, size_t *);
55const char *dns_type_str(uint16_t);
56size_t dns_build_response(uint8_t *, size_t, uint8_t *, size_t, uint16_t,
57 void *, size_t);
58size_t dns_build_ns_response(uint8_t *, size_t, uint8_t *);
59size_t dns_build_nxdomain(uint8_t *, size_t, uint8_t *, size_t);
60int avahi_resolve(const char *, int, void *);
61
62__dead void
63usage(void)
64{
65 extern char *__progname;
66
67 fprintf(stderr, "usage: %s [-dv] [-b address] -p port\n", __progname);
68 exit(1);
69}
70
71void
72logmsg(const char *fmt, ...)
73{
74 va_list ap;
75
76 va_start(ap, fmt);
77 if (debug)
78 vprintf(fmt, ap);
79 else
80 vsyslog(LOG_INFO, fmt, ap);
81 va_end(ap);
82}
83
84int
85main(int argc, char *argv[])
86{
87 struct addrinfo hints, *res, *res0;
88 struct sockaddr_storage ss;
89 struct in_addr addr4;
90 struct in6_addr addr6;
91 socklen_t sslen;
92 ssize_t n;
93 size_t namelen, qnamelen, resplen;
94 char name[DNS_MAX_NAME + 1];
95 const char *bindaddr = NULL;
96 const char *port = NULL;
97 uint8_t qbuf[512], rbuf[512];
98 uint16_t qtype, qclass;
99 int ch, error, s;
100
101 while ((ch = getopt(argc, argv, "b:dp:v")) != -1) {
102 switch (ch) {
103 case 'b':
104 bindaddr = optarg;
105 break;
106 case 'd':
107 debug = 1;
108 break;
109 case 'p':
110 port = optarg;
111 break;
112 case 'v':
113 verbose++;
114 break;
115 default:
116 usage();
117 }
118 }
119 argc -= optind;
120 argv += optind;
121
122 if (port == NULL)
123 usage();
124
125 if (!debug) {
126 if (daemon(0, 0) == -1)
127 err(1, "daemon");
128 openlog("avahi-proxy", LOG_PID | LOG_NDELAY, LOG_DAEMON);
129 }
130
131 signal(SIGPIPE, SIG_IGN);
132
133 memset(&hints, 0, sizeof(hints));
134 hints.ai_family = AF_UNSPEC;
135 hints.ai_socktype = SOCK_DGRAM;
136 hints.ai_flags = AI_PASSIVE;
137
138 error = getaddrinfo(bindaddr, port, &hints, &res0);
139 if (error != 0)
140 errx(1, "getaddrinfo: %s", gai_strerror(error));
141
142 s = -1;
143 for (res = res0; res != NULL; res = res->ai_next) {
144 s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
145 if (s == -1)
146 continue;
147 if (bind(s, res->ai_addr, res->ai_addrlen) == -1) {
148 close(s);
149 s = -1;
150 continue;
151 }
152 break;
153 }
154 freeaddrinfo(res0);
155
156 if (s == -1)
157 err(1, "socket/bind");
158
159 for (;;) {
160 sslen = sizeof(ss);
161 n = recvfrom(s, qbuf, sizeof(qbuf), 0, (struct sockaddr *)&ss,
162 &sslen);
163 if (n == -1) {
164 if (errno == EINTR)
165 continue;
166 warn("recvfrom");
167 continue;
168 }
169
170 if (n < DNS_HEADER_SIZE)
171 continue;
172
173 /* parse question */
174 if (dns_parse_name(qbuf, n, DNS_HEADER_SIZE, name, sizeof(name),
175 &qnamelen) == -1) {
176 warnx("failed parsing DNS query");
177 continue;
178 }
179
180 if (DNS_HEADER_SIZE + qnamelen + 4 > (size_t)n) {
181 warnx("bogus qnamelen %zu + 4 > %zu", qnamelen, n);
182 continue;
183 }
184
185 qtype = (qbuf[DNS_HEADER_SIZE + qnamelen] << 8) |
186 qbuf[DNS_HEADER_SIZE + qnamelen + 1];
187 qclass = (qbuf[DNS_HEADER_SIZE + qnamelen + 2] << 8) |
188 qbuf[DNS_HEADER_SIZE + qnamelen + 3];
189
190 if (qclass != DNS_CLASS_IN) {
191 if (verbose)
192 logmsg("ignoring query for non-IN (%d) name "
193 "%s\n", qclass, name);
194 continue;
195 }
196
197 /* respond to ". NS" probe from unwind */
198 if (strcmp(name, ".") == 0 && qtype == DNS_TYPE_NS) {
199 resplen = dns_build_ns_response(rbuf, sizeof(rbuf),
200 qbuf);
201 if (verbose)
202 logmsg("%s %s -> localhost.\n",
203 dns_type_str(qtype), name);
204 } else if ((namelen = strlen(name)) >= 6 &&
205 strcasecmp(name + namelen - 6, ".local") == 0 &&
206 qtype == DNS_TYPE_A &&
207 avahi_resolve(name, AF_INET, &addr4) == 0) {
208 resplen = dns_build_response(rbuf, sizeof(rbuf),
209 qbuf, n, DNS_TYPE_A, &addr4, 4);
210 } else if (namelen >= 6 &&
211 strcasecmp(name + namelen - 6, ".local") == 0 &&
212 qtype == DNS_TYPE_AAAA &&
213 avahi_resolve(name, AF_INET6, &addr6) == 0) {
214 resplen = dns_build_response(rbuf, sizeof(rbuf),
215 qbuf, n, DNS_TYPE_AAAA, &addr6, 16);
216 } else {
217 resplen = dns_build_nxdomain(rbuf, sizeof(rbuf),
218 qbuf, n);
219 if (verbose)
220 logmsg("%s %s NXDOMAIN\n",
221 dns_type_str(qtype), name);
222 }
223
224 if (resplen > 0)
225 sendto(s, rbuf, resplen, 0, (struct sockaddr *)&ss,
226 sslen);
227 }
228
229 return 0;
230}
231
232int
233dns_parse_name(uint8_t *pkt, size_t pktlen, size_t offset, char *dst,
234 size_t dstlen, size_t *lenp)
235{
236 size_t i, label_len, total = 0, dst_off = 0;
237
238 for (;;) {
239 if (offset >= pktlen)
240 return -1;
241
242 label_len = pkt[offset++];
243 total++;
244
245 if (label_len == 0) {
246 /* root label */
247 if (dst_off == 0) {
248 if (dstlen < 2)
249 return -1;
250 dst[dst_off++] = '.';
251 }
252 dst[dst_off] = '\0';
253 *lenp = total;
254 return 0;
255 }
256
257 if (label_len > DNS_MAX_LABEL)
258 return -1;
259 if (offset + label_len > pktlen)
260 return -1;
261 if (dst_off + label_len + 1 >= dstlen)
262 return -1;
263
264 if (dst_off > 0)
265 dst[dst_off++] = '.';
266
267 for (i = 0; i < label_len; i++)
268 dst[dst_off++] = pkt[offset++];
269 total += label_len;
270 }
271}
272
273const char *
274dns_type_str(uint16_t type)
275{
276 switch (type) {
277 case DNS_TYPE_A:
278 return "A";
279 case DNS_TYPE_NS:
280 return "NS";
281 case DNS_TYPE_AAAA:
282 return "AAAA";
283 default:
284 return "?";
285 }
286}
287
288size_t
289dns_build_response(uint8_t *rbuf, size_t rlen, uint8_t *qbuf, size_t qlen,
290 uint16_t type, void *addr, size_t addrlen)
291{
292 size_t off, qsec_len;
293 uint16_t flags;
294
295 /* question section length: from byte 12 to end of qtype/qclass */
296 for (qsec_len = 0; DNS_HEADER_SIZE + qsec_len < qlen; qsec_len++) {
297 if (qbuf[DNS_HEADER_SIZE + qsec_len] == 0) {
298 qsec_len += 5; /* null + qtype(2) + qclass(2) */
299 break;
300 }
301 }
302
303 if (DNS_HEADER_SIZE + qsec_len + 12 + addrlen > rlen) {
304 warnx("%s: bogus size (%d + %zu + 12 + %zu) > %zu",
305 __func__, DNS_HEADER_SIZE, qsec_len, addrlen, rlen);
306 return 0;
307 }
308
309 /* copy header and question */
310 memcpy(rbuf, qbuf, DNS_HEADER_SIZE + qsec_len);
311
312 /* set response flags */
313 flags = DNS_FLAG_QR | DNS_FLAG_AA | DNS_FLAG_RA;
314 if (qbuf[2] & (DNS_FLAG_RD >> 8))
315 flags |= DNS_FLAG_RD;
316 rbuf[2] = flags >> 8;
317 rbuf[3] = flags & 0xff;
318
319 /* qdcount = 1 */
320 rbuf[4] = 0; rbuf[5] = 1;
321 /* ancount = 1 */
322 rbuf[6] = 0; rbuf[7] = 1;
323 /* nscount = 0 */
324 rbuf[8] = 0; rbuf[9] = 0;
325 /* arcount = 0 */
326 rbuf[10] = 0; rbuf[11] = 0;
327
328 off = DNS_HEADER_SIZE + qsec_len;
329
330 /* answer: pointer to qname */
331 rbuf[off++] = 0xc0;
332 rbuf[off++] = 0x0c;
333 /* type */
334 rbuf[off++] = type >> 8;
335 rbuf[off++] = type & 0xff;
336 /* class IN */
337 rbuf[off++] = 0;
338 rbuf[off++] = DNS_CLASS_IN;
339 /* TTL = 60 */
340 rbuf[off++] = 0;
341 rbuf[off++] = 0;
342 rbuf[off++] = 0;
343 rbuf[off++] = 60;
344 /* rdlength */
345 rbuf[off++] = addrlen >> 8;
346 rbuf[off++] = addrlen & 0xff;
347 /* rdata */
348 memcpy(rbuf + off, addr, addrlen);
349 off += addrlen;
350
351 return off;
352}
353
354size_t
355dns_build_ns_response(uint8_t *rbuf, size_t rlen, uint8_t *qbuf)
356{
357 /* "localhost." in wire format */
358 static uint8_t localhost[] = {
359 9, 'l','o','c','a','l','h','o','s','t', '\0'
360 };
361 size_t off, qsec_len;
362 uint16_t flags;
363
364 /* question: null label + qtype(2) + qclass(2) = 5 bytes for "." */
365 qsec_len = 5;
366
367 if (DNS_HEADER_SIZE + qsec_len + 12 + sizeof(localhost) > rlen) {
368 warnx("%s: bogus size (%d + %zu + 12 + %zu) > %zu",
369 __func__, DNS_HEADER_SIZE, qsec_len, sizeof(localhost),
370 rlen);
371 return 0;
372 }
373
374 /* copy header and question */
375 memcpy(rbuf, qbuf, DNS_HEADER_SIZE + qsec_len);
376
377 /* set response flags */
378 flags = DNS_FLAG_QR | DNS_FLAG_AA | DNS_FLAG_RA;
379 if (qbuf[2] & (DNS_FLAG_RD >> 8))
380 flags |= DNS_FLAG_RD;
381 rbuf[2] = flags >> 8;
382 rbuf[3] = flags & 0xff;
383
384 /* qdcount = 1 */
385 rbuf[4] = 0; rbuf[5] = 1;
386 /* ancount = 1 */
387 rbuf[6] = 0; rbuf[7] = 1;
388 /* nscount = 0 */
389 rbuf[8] = 0; rbuf[9] = 0;
390 /* arcount = 0 */
391 rbuf[10] = 0; rbuf[11] = 0;
392
393 off = DNS_HEADER_SIZE + qsec_len;
394
395 /* answer: pointer to qname */
396 rbuf[off++] = 0xc0;
397 rbuf[off++] = 0x0c;
398 /* type NS */
399 rbuf[off++] = 0;
400 rbuf[off++] = DNS_TYPE_NS;
401 /* class IN */
402 rbuf[off++] = 0;
403 rbuf[off++] = DNS_CLASS_IN;
404 /* TTL = 86400 */
405 rbuf[off++] = 0;
406 rbuf[off++] = 0x01;
407 rbuf[off++] = 0x51;
408 rbuf[off++] = 0x80;
409 /* rdlength */
410 rbuf[off++] = 0;
411 rbuf[off++] = sizeof(localhost);
412 /* rdata: localhost. */
413 memcpy(rbuf + off, localhost, sizeof(localhost));
414 off += sizeof(localhost);
415
416 return off;
417}
418
419size_t
420dns_build_nxdomain(uint8_t *rbuf, size_t rlen, uint8_t *qbuf, size_t qlen)
421{
422 size_t qsec_len;
423 uint16_t flags;
424
425 /* find question section length */
426 for (qsec_len = 0; DNS_HEADER_SIZE + qsec_len < qlen; qsec_len++) {
427 if (qbuf[DNS_HEADER_SIZE + qsec_len] == 0) {
428 qsec_len += 5; /* null + qtype(2) + qclass(2) */
429 break;
430 }
431 }
432
433 if (DNS_HEADER_SIZE + qsec_len > rlen) {
434 warnx("%s: bogus size (%d + %zu) > %zu",
435 __func__, DNS_HEADER_SIZE, qsec_len, rlen);
436 return 0;
437 }
438
439 /* copy header and question */
440 memcpy(rbuf, qbuf, DNS_HEADER_SIZE + qsec_len);
441
442 /* set response flags with NXDOMAIN rcode */
443 flags = DNS_FLAG_QR | DNS_FLAG_AA | DNS_FLAG_RA;
444 if (qbuf[2] & (DNS_FLAG_RD >> 8))
445 flags |= DNS_FLAG_RD;
446 rbuf[2] = flags >> 8;
447 rbuf[3] = (flags & 0xff) | DNS_RCODE_NXDOMAIN;
448
449 /* qdcount = 1 */
450 rbuf[4] = 0; rbuf[5] = 1;
451 /* ancount = 0 */
452 rbuf[6] = 0; rbuf[7] = 0;
453 /* nscount = 0 */
454 rbuf[8] = 0; rbuf[9] = 0;
455 /* arcount = 0 */
456 rbuf[10] = 0; rbuf[11] = 0;
457
458 return DNS_HEADER_SIZE + qsec_len;
459}
460
461int
462avahi_resolve(const char *name, int af, void *addr)
463{
464 char buf[128], *tab, abuf[INET6_ADDRSTRLEN];
465 ssize_t n;
466 pid_t pid;
467 int pipefd[2], status;
468
469 if (pipe(pipefd) == -1)
470 return -1;
471
472 pid = fork();
473 if (pid == -1) {
474 close(pipefd[0]);
475 close(pipefd[1]);
476 return -1;
477 }
478
479 if (pid == 0) {
480 close(pipefd[0]);
481 if (dup2(pipefd[1], STDOUT_FILENO) == -1)
482 _exit(1);
483 close(pipefd[1]);
484 execlp("avahi-resolve", "avahi-resolve",
485 af == AF_INET6 ? "-6" : "-4",
486 "-n", name,
487 (char *)NULL);
488 _exit(1);
489 }
490
491 close(pipefd[1]);
492
493 n = read(pipefd[0], buf, sizeof(buf) - 1);
494 close(pipefd[0]);
495
496 if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status) ||
497 WEXITSTATUS(status) != 0)
498 return -1;
499
500 if (n <= 0)
501 return -1;
502
503 buf[n] = '\0';
504
505 /* strip trailing newline */
506 while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r'))
507 buf[--n] = '\0';
508
509 /* avahi-resolve output: "hostname.local\t192.168.1.1\n" */
510 tab = strchr(buf, '\t');
511 if (tab == NULL)
512 return -1;
513
514 if (inet_pton(af, tab + 1, addr) != 1)
515 return -1;
516
517 if (verbose) {
518 inet_ntop(af, addr, abuf, sizeof(abuf));
519 logmsg("%s %s -> %s\n", af == AF_INET6 ? "AAAA" : "A",
520 name, abuf);
521 }
522
523 return 0;
524}