diff options
author | Eric Faurot <eric@cvs.openbsd.org> | 2012-04-14 09:24:19 +0000 |
---|---|---|
committer | Eric Faurot <eric@cvs.openbsd.org> | 2012-04-14 09:24:19 +0000 |
commit | 852a7ed231a6296007177e8f5df9ec3978d4dab8 (patch) | |
tree | da2dada44712de6b50d559bd5cf9b3869eba3746 /lib/libc/asr/res_send_async.c | |
parent | b24bb61092bfc78fc1d60744dd936d4f10e3de57 (diff) |
Import asr, an experimental async resolver implementation.
The idea is to eventually replace the existing resolver with
something better. Time to start working on it in tree.
ok deraadt@
Diffstat (limited to 'lib/libc/asr/res_send_async.c')
-rw-r--r-- | lib/libc/asr/res_send_async.c | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/lib/libc/asr/res_send_async.c b/lib/libc/asr/res_send_async.c new file mode 100644 index 00000000000..3516c685ad2 --- /dev/null +++ b/lib/libc/asr/res_send_async.c @@ -0,0 +1,809 @@ +/* $OpenBSD: res_send_async.c,v 1.1 2012/04/14 09:24:18 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/uio.h> + +#include <netinet/in.h> +#include <arpa/nameser.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <resolv.h> /* for res_random */ +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "asr.h" +#include "asr_private.h" + +#define OP_QUERY (0) + +static int res_send_async_run(struct async *, struct async_res *); +static int sockaddr_connect(const struct sockaddr *, int); +static int udp_send(struct async *); +static int udp_recv(struct async *); +static int tcp_write(struct async *); +static int tcp_read(struct async *); +static int validate_packet(struct async *); +static int setup_query(struct async *, const char *, const char *, int, int); +static int ensure_ibuf(struct async *, size_t); + +#ifdef DEBUG +char *print_addr(const struct sockaddr *, char *, size_t); +#endif + +#define AS_NS_SA(p) ((p)->as_ctx->ac_ns[(p)->as_ns_idx - 1]) + + +struct async * +res_send_async(const unsigned char *buf, int buflen, unsigned char *ans, + int anslen, struct asr *asr) +{ + struct asr_ctx *ac; + struct async *as; + struct packed p; + struct header h; + struct query q; + +#ifdef DEBUG + if (asr_debug) { + asr_printf("asr: res_send_async()\n"); + asr_dump_packet(stderr, buf, buflen, 0); + } +#endif + ac = asr_use_resolver(asr); + if ((as = async_new(ac, ASR_SEND)) == NULL) { + asr_ctx_unref(ac); + return (NULL); /* errno set */ + } + as->as_run = res_send_async_run; + + if (ans) { + as->as.dns.flags |= ASYNC_EXTIBUF; + as->as.dns.ibuf = ans; + as->as.dns.ibufsize = anslen; + as->as.dns.ibuflen = 0; + } else { + as->as.dns.ibuf = NULL; + as->as.dns.ibufsize = 0; + as->as.dns.ibuflen = 0; + } + + as->as.dns.flags |= ASYNC_EXTOBUF; + as->as.dns.obuf = (unsigned char*)buf; + as->as.dns.obuflen = buflen; + as->as.dns.obufsize = buflen; + + packed_init(&p, (char*)buf, buflen); + unpack_header(&p, &h); + unpack_query(&p, &q); + if (p.err) { + errno = EINVAL; + goto err; + } + as->as.dns.reqid = h.id; + as->as.dns.type = q.q_type; + as->as.dns.class = q.q_class; + as->as.dns.dname = strdup(q.q_dname); + if (as->as.dns.dname == NULL) + goto err; /* errno set */ + + asr_ctx_unref(ac); + return (as); + err: + if (as) + async_free(as); + asr_ctx_unref(ac); + return (NULL); +} + +/* + * Unlike res_query(), this version will actually return the packet + * if it has received a valid one (errno == 0) even if h_errno is + * not NETDB_SUCCESS. So the packet *must* be freed if necessary + * (ans == NULL). + */ +struct async * +res_query_async(const char *name, int class, int type, unsigned char *ans, + int anslen, struct asr *asr) +{ + struct asr_ctx *ac; + struct async *as; +#ifdef DEBUG + asr_printf("asr: res_query_async(\"%s\", %i, %i)\n", name, class, type); +#endif + ac = asr_use_resolver(asr); + as = res_query_async_ctx(name, class, type, ans, anslen, ac); + asr_ctx_unref(ac); + + return (as); +} + +struct async * +res_query_async_ctx(const char *name, int class, int type, unsigned char *ans, + int anslen, struct asr_ctx *a_ctx) +{ + struct async *as; + +#ifdef DEBUG + asr_printf("asr: res_query_async_ctx(\"%s\", %i, %i)\n", name, class, + type); +#endif + if ((as = async_new(a_ctx, ASR_SEND)) == NULL) + return (NULL); /* errno set */ + as->as_run = res_send_async_run; + + if (ans) { + as->as.dns.flags |= ASYNC_EXTIBUF; + as->as.dns.ibuf = ans; + as->as.dns.ibufsize = anslen; + } else { + as->as.dns.ibuf = NULL; + as->as.dns.ibufsize = 0; + } + as->as.dns.ibuflen = 0; + + /* This adds a "." to name if it doesn't already has one. + * That's how res_query() behaves (trough res_mkquery"). + */ + if (setup_query(as, name, NULL, class, type) == -1) + goto err; /* errno set */ + + return (as); + + err: + if (as) + async_free(as); + + return (NULL); +} + +static int +res_send_async_run(struct async *as, struct async_res *ar) +{ + next: + switch(as->as_state) { + + case ASR_STATE_INIT: + + if (as->as_ctx->ac_nscount == 0) { + ar->ar_errno = ECONNREFUSED; + async_set_state(as, ASR_STATE_HALT); + break; + } + + async_set_state(as, ASR_STATE_NEXT_NS); + break; + + case ASR_STATE_NEXT_NS: + + if (asr_iter_ns(as) == -1) { + ar->ar_errno = ETIMEDOUT; + async_set_state(as, ASR_STATE_HALT); + break; + } + + if (as->as_ctx->ac_options & RES_USEVC || + as->as.dns.obuflen > PACKETSZ) + async_set_state(as, ASR_STATE_TCP_WRITE); + else + async_set_state(as, ASR_STATE_UDP_SEND); + break; + + case ASR_STATE_UDP_SEND: + + if (udp_send(as) == -1) { + async_set_state(as, ASR_STATE_NEXT_NS); + break; + } + async_set_state(as, ASR_STATE_UDP_RECV); + ar->ar_cond = ASYNC_READ; + ar->ar_fd = as->as_fd; + ar->ar_timeout = as->as_timeout; + return (ASYNC_COND); + break; + + case ASR_STATE_UDP_RECV: + + if (udp_recv(as) == -1) { + if (errno == ENOMEM) { + ar->ar_errno = errno; + async_set_state(as, ASR_STATE_HALT); + break; + } + if (errno != EOVERFLOW) { + /* Fail or timeout */ + async_set_state(as, ASR_STATE_NEXT_NS); + break; + } + if (as->as_ctx->ac_options & RES_IGNTC) + async_set_state(as, ASR_STATE_PACKET); + else + async_set_state(as, ASR_STATE_TCP_WRITE); + } else + async_set_state(as, ASR_STATE_PACKET); + break; + + case ASR_STATE_TCP_WRITE: + + switch (tcp_write(as)) { + case -1: /* fail or timeout */ + async_set_state(as, ASR_STATE_NEXT_NS); + break; + case 0: + async_set_state(as, ASR_STATE_TCP_READ); + ar->ar_cond = ASYNC_READ; + ar->ar_fd = as->as_fd; + ar->ar_timeout = as->as_timeout; + return (ASYNC_COND); + case 1: + ar->ar_cond = ASYNC_WRITE; + ar->ar_fd = as->as_fd; + ar->ar_timeout = as->as_timeout; + return (ASYNC_COND); + } + break; + + case ASR_STATE_TCP_READ: + + switch (tcp_read(as)) { + case -1: /* Fail or timeout */ + if (errno == ENOMEM) { + ar->ar_errno = errno; + async_set_state(as, ASR_STATE_HALT); + } else + async_set_state(as, ASR_STATE_NEXT_NS); + break; + case 0: + async_set_state(as, ASR_STATE_PACKET); + break; + case 1: + ar->ar_cond = ASYNC_READ; + ar->ar_fd = as->as_fd; + ar->ar_timeout = as->as_timeout; + return (ASYNC_COND); + } + break; + + case ASR_STATE_PACKET: + + memmove(&ar->ar_sa.sa, AS_NS_SA(as), AS_NS_SA(as)->sa_len); + ar->ar_datalen = as->as.dns.ibuflen; + ar->ar_data = as->as.dns.ibuf; + as->as.dns.ibuf = NULL; + ar->ar_errno = 0; + ar->ar_rcode = as->as.dns.rcode; + async_set_state(as, ASR_STATE_HALT); + break; + + case ASR_STATE_HALT: + + if (ar->ar_errno) { + ar->ar_h_errno = TRY_AGAIN; + ar->ar_count = 0; + ar->ar_datalen = -1; + ar->ar_data = NULL; + } else if (as->as.dns.ancount) { + ar->ar_h_errno = NETDB_SUCCESS; + ar->ar_count = as->as.dns.ancount; + } else { + ar->ar_count = 0; + switch(as->as.dns.rcode) { + case NXDOMAIN: + ar->ar_h_errno = HOST_NOT_FOUND; + break; + case SERVFAIL: + ar->ar_h_errno = TRY_AGAIN; + break; + case NOERROR: + ar->ar_h_errno = NO_DATA; + break; + default: + ar->ar_h_errno = NO_RECOVERY; + } + } + return (ASYNC_DONE); + + default: + + ar->ar_errno = EOPNOTSUPP; + ar->ar_h_errno = NETDB_INTERNAL; + async_set_state(as, ASR_STATE_HALT); + break; + } + goto next; +} + +static int +sockaddr_connect(const struct sockaddr *sa, int socktype) +{ + int errno_save, flags, sock; + + if ((sock = socket(sa->sa_family, socktype, 0)) == -1) + goto fail; + + if ((flags = fcntl(sock, F_GETFL, 0)) == -1) + goto fail; + + flags |= O_NONBLOCK; + + if ((flags = fcntl(sock, F_SETFL, flags)) == -1) + goto fail; + + if (connect(sock, sa, sa->sa_len) == -1) { + if (errno == EINPROGRESS) + return (sock); + goto fail; + } + + return (sock); + + fail: + + if (sock != -1) { + errno_save = errno; + close(sock); + errno = errno_save; + } + + return (-1); +} + +/* + * Prepare the DNS packet for the query type "type", class "class" and domain + * name created by the concatenation on "name" and "dom". + * Return 0 on success, set errno and return -1 on error. + */ +static int +setup_query(struct async *as, const char *name, const char *dom, + int class, int type) +{ + struct packed p; + struct header h; + char fqdn[MAXDNAME]; + char dname[MAXDNAME]; + + if (as->as.dns.flags & ASYNC_EXTOBUF) { + errno = EINVAL; +#ifdef DEBUG + asr_printf("attempting to write in user packet"); +#endif + return (-1); + } + + if (asr_make_fqdn(name, dom, fqdn, sizeof(fqdn)) > sizeof(fqdn)) { + errno = EINVAL; +#ifdef DEBUG + asr_printf("asr_make_fqdn: name too long\n"); +#endif + return (-1); + } + + if (dname_from_fqdn(fqdn, dname, sizeof(dname)) == -1) { + errno = EINVAL; +#ifdef DEBUG + asr_printf("dname_from_fqdn: invalid\n"); +#endif + return (-1); + } + + if (as->as.dns.obuf == NULL) { + as->as.dns.obufsize = PACKETSZ; + as->as.dns.obuf = malloc(as->as.dns.obufsize); + if (as->as.dns.obuf == NULL) + return (-1); /* errno set */ + } + as->as.dns.obuflen = 0; + + memset(&h, 0, sizeof h); + h.id = res_randomid(); + if (as->as_ctx->ac_options & RES_RECURSE) + h.flags |= RD_MASK; + h.qdcount = 1; + + packed_init(&p, as->as.dns.obuf, as->as.dns.obufsize); + pack_header(&p, &h); + pack_query(&p, type, class, dname); + if (p.err) { +#ifdef DEBUG + asr_printf("error packing query"); +#endif + errno = EINVAL; + return (-1); + } + + /* Remember the parameters. */ + as->as.dns.reqid = h.id; + as->as.dns.type = type; + as->as.dns.class = class; + if (as->as.dns.dname) + free(as->as.dns.dname); + as->as.dns.dname = strdup(dname); + if (as->as.dns.dname == NULL) { +#ifdef DEBUG + asr_printf("strdup"); +#endif + return (-1); /* errno set */ + } + as->as.dns.obuflen = p.offset; + +#ifdef DEBUG + if (asr_debug) { + asr_printf("------- asr_setup_query(): packet -------\n"); + asr_dump_packet(stderr, as->as.dns.obuf, as->as.dns.obuflen, 0); + asr_printf("-----------------------------------------\n"); + } +#endif + + return (0); +} + +/* + * Create a connect UDP socket and send the output packet. + * + * Return 0 on success, or -1 on error (errno set). + */ +static int +udp_send(struct async *as) +{ + ssize_t n; + int save_errno; +#ifdef DEBUG + char buf[256]; + + if (asr_debug) + asr_printf("asr: [%p] connecting to %s UDP\n", as, + print_addr(AS_NS_SA(as), buf, sizeof buf)); +#endif + as->as_fd = sockaddr_connect(AS_NS_SA(as), SOCK_DGRAM); + if (as->as_fd == -1) + return (-1); /* errno set */ + + as->as_timeout = as->as_ctx->ac_nstimeout; + + n = send(as->as_fd, as->as.dns.obuf, as->as.dns.obuflen, 0); + if (n == -1) { + save_errno = errno; + close(as->as_fd); + errno = save_errno; + as->as_fd = -1; + return (-1); + } + + return (0); +} + +/* + * Try to receive a valid packet from the current UDP socket. + * + * Return 0 if a full packet could be read, or -1 on error (errno set). + */ +static int +udp_recv(struct async *as) +{ + ssize_t n; + int save_errno; + + /* Allocate input buf if needed */ + if (as->as.dns.ibuf == NULL) { + if (ensure_ibuf(as, PACKETSZ) == -1) { + save_errno = errno; + close(as->as_fd); + errno = save_errno; + as->as_fd = -1; + return (-1); + } + } + + n = recv(as->as_fd, as->as.dns.ibuf, as->as.dns.ibufsize, 0); + save_errno = errno; + close(as->as_fd); + errno = save_errno; + as->as_fd = -1; + if (n == -1) + return (-1); + + as->as.dns.ibuflen = n; + +#ifdef DEBUG + if (asr_debug) { + asr_printf("------- asr_udp_recv() packet -------\n"); + asr_dump_packet(stderr, as->as.dns.ibuf, as->as.dns.ibuflen, 0); + asr_printf("-------------------------------------\n"); + } +#endif + + if (validate_packet(as) == -1) + return (-1); /* errno set */ + + return (0); +} + +/* + * Write the output packet to the TCP socket. + * + * Return 0 when all bytes have been sent, 1 there is no buffer space on the + * socket or it is not connected yet, or -1 on error (errno set). + */ +static int +tcp_write(struct async *as) +{ + struct iovec iov[2]; + uint16_t len; + ssize_t n; + int i, se; + socklen_t sl; +#ifdef DEBUG + char buf[256]; +#endif + + /* First try to connect if not already */ + if (as->as_fd == -1) { +#ifdef DEBUG + if (asr_debug) + asr_printf("asr: [%p] connecting to %s TCP\n", as, + print_addr(AS_NS_SA(as), buf, sizeof buf)); +#endif + as->as_fd = sockaddr_connect(AS_NS_SA(as), SOCK_STREAM); + if (as->as_fd == -1) + return (-1); /* errno set */ + as->as_timeout = as->as_ctx->ac_nstimeout; + return (1); + } + + i = 0; + + /* Check if the connection succeeded. */ + if (as->as.dns.datalen == 0) { + sl = sizeof(se); + if (getsockopt(as->as_fd, SOL_SOCKET, SO_ERROR, &se, &sl) == -1) + goto close; /* errno set */ + if (se) { + errno = se; + goto close; + } + + as->as.dns.bufpos = 0; + + /* Send the packet length first */ + len = htons(as->as.dns.obuflen); + iov[i].iov_base = &len; + iov[i].iov_len = sizeof(len); + i++; + } + + iov[i].iov_base = as->as.dns.obuf + as->as.dns.bufpos; + iov[i].iov_len = as->as.dns.obuflen - as->as.dns.bufpos; + i++; + + n = writev(as->as_fd, iov, i); + if (n == -1) + goto close; /* errno set */ + + /* + * We want at least the packet length to be written out the first time. + * Technically we could recover but that makes little sense to support + * that. + */ + if (as->as.dns.datalen == 0 && n < 2) { + errno = EIO; + goto close; + } + + if (as->as.dns.datalen == 0) { + as->as.dns.datalen = len; + n -= 2; + } + + as->as.dns.bufpos += n; + if (as->as.dns.bufpos == as->as.dns.obuflen) { + /* All sent. Prepare for TCP read */ + as->as.dns.datalen = 0; + return (0); + } + + /* More data to write */ + as->as_timeout = as->as_ctx->ac_nstimeout; + return (1); + +close: + close(as->as_fd); + as->as_fd = -1; + return (-1); +} + +/* + * Try to read a valid packet from the current TCP socket. + * + * Return 0 if a full packet could be read, 1 if more data is needed and the + * socket must be read again, or -1 on error (errno set). + */ +static int +tcp_read(struct async *as) +{ + uint16_t len; + ssize_t n; + int save_errno; + + /* We must read the packet len first */ + if (as->as.dns.datalen == 0) { + n = read(as->as_fd, &len, sizeof(len)); + if (n == -1) + goto close; /* errno set */ + /* + * If the server has sent us only the first byte, we fail. + * Technically, we could recover but it might not be worth + * supporting that. + */ + if (n < 2) { + errno = EIO; + goto close; + } + + as->as.dns.datalen = ntohs(len); + as->as.dns.bufpos = 0; + as->as.dns.ibuflen = 0; + + if (ensure_ibuf(as, as->as.dns.datalen) == -1) + goto close; /* errno set */ + } + + n = read(as->as_fd, as->as.dns.ibuf + as->as.dns.ibuflen, + as->as.dns.datalen - as->as.dns.ibuflen); + if (n == -1) + goto close; /* errno set */ + if (n == 0) { + errno = ECONNRESET; + goto close; + } + as->as.dns.ibuflen += n; + + /* See if we got all the advertised bytes. */ + if (as->as.dns.ibuflen != as->as.dns.datalen) + return (1); + +#ifdef DEBUG + if (asr_debug) { + asr_printf("------- asr_tcp_read() packet -------\n"); + asr_dump_packet(stderr, as->as.dns.ibuf, as->as.dns.ibuflen, 0); + asr_printf("-------------------------------------\n"); + } +#endif + if (validate_packet(as) == -1) + goto close; /* errno set */ + + errno = 0; +close: + save_errno = errno; + close(as->as_fd); + errno = save_errno; + as->as_fd = -1; + return (errno ? -1 : 0); +} + +/* + * Make sure the input buffer is at least "n" bytes long. + * If not (or not allocated) allocated enough space, unless the + * buffer is external (owned by the caller), in which case it fails. + */ +static int +ensure_ibuf(struct async *as, size_t n) +{ + char *t; + + if (as->as.dns.flags & ASYNC_EXTIBUF) { + if (n <= as->as.dns.ibufsize) + return (0); + errno = EINVAL; + return (-1); + } + + if (as->as.dns.ibuf == NULL) { + as->as.dns.ibuf = malloc(n); + if (as->as.dns.ibuf == NULL) + return (-1); /* errno set */ + as->as.dns.ibufsize = n; + return (0); + } + + if (as->as.dns.ibufsize >= n) + return (0); + + t = realloc(as->as.dns.ibuf, n); + if (t == NULL) + return (-1); /* errno set */ + as->as.dns.ibuf = t; + as->as.dns.ibufsize = n; + + return (0); +} + +/* + * Check if the received packet is valid. + * Return 0 on success, or set errno and return -1. + */ +static int +validate_packet(struct async *as) +{ + struct packed p; + struct header h; + struct query q; + struct rr rr; + int r; + + packed_init(&p, as->as.dns.ibuf, as->as.dns.ibuflen); + + unpack_header(&p, &h); + if (p.err) + goto inval; + + if (h.id != as->as.dns.reqid) { +#ifdef DEBUG + asr_printf("incorrect reqid\n"); +#endif + goto inval; + } + if (h.qdcount != 1) + goto inval; + /* Should be zero, we could allow this */ + if ((h.flags & Z_MASK) != 0) + goto inval; + /* Actually, it depends on the request but we only use OP_QUERY */ + if (OPCODE(h.flags) != OP_QUERY) + goto inval; + /* Must be a response */ + if ((h.flags & QR_MASK) == 0) + goto inval; + + as->as.dns.rcode = RCODE(h.flags); + as->as.dns.ancount = h.ancount; + + unpack_query(&p, &q); + if (p.err) + goto inval; + + if (q.q_type != as->as.dns.type || + q.q_class != as->as.dns.class || + strcasecmp(q.q_dname, as->as.dns.dname)) { +#ifdef DEBUG + asr_printf("incorrect type/class/dname '%s' != '%s'\n", + q.q_dname, as->as.dns.dname); +#endif + goto inval; + } + + /* Check for truncation */ + if (h.flags & TC_MASK) { + errno = EOVERFLOW; + return (-1); + } + + /* Validate the rest of the packet */ + for(r = h.ancount + h.nscount + h.arcount; r; r--) + unpack_rr(&p, &rr); + + if (p.err || (p.offset != as->as.dns.ibuflen)) + goto inval; + + return (0); + + inval: + errno = EINVAL; + return (-1); +} |