translate DNS queries to avahi-resolve
at main 524 lines 12 kB view raw
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}