/* $OpenBSD: relay_udp.c,v 1.49 2018/08/06 17:31:31 benno Exp $ */ /* * Copyright (c) 2007 - 2013 Reyk Floeter * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "relayd.h" extern volatile sig_atomic_t relay_sessions; extern objid_t relay_conid; static struct relayd *env = NULL; struct shuffle relay_shuffle; int relay_udp_socket(struct sockaddr_storage *, in_port_t, struct protocol *); void relay_udp_request(struct rsession *); void relay_udp_timeout(int, short, void *); void relay_dns_log(struct rsession *, u_int8_t *, size_t); void *relay_dns_validate(struct rsession *, struct relay *, struct sockaddr_storage *, u_int8_t *, size_t); int relay_dns_request(struct rsession *); void relay_udp_response(int, short, void *); void relay_dns_result(struct rsession *, u_int8_t *, size_t); int relay_dns_cmp(struct rsession *, struct rsession *); void relay_udp_privinit(struct relay *rlay) { if (rlay->rl_conf.flags & F_TLS) fatalx("tls over udp is not supported"); rlay->rl_conf.flags |= F_UDP; } void relay_udp_init(struct relayd *x_env, struct relay *rlay) { struct protocol *proto = rlay->rl_proto; if (env == NULL) env = x_env; switch (proto->type) { case RELAY_PROTO_DNS: proto->validate = relay_dns_validate; proto->request = relay_dns_request; proto->cmp = relay_dns_cmp; shuffle_init(&relay_shuffle); break; default: fatalx("unsupported udp protocol"); break; } } int relay_udp_bind(struct sockaddr_storage *ss, in_port_t port, struct protocol *proto) { int s; if ((s = relay_udp_socket(ss, port, proto)) == -1) return (-1); if (bind(s, (struct sockaddr *)ss, ss->ss_len) == -1) goto bad; return (s); bad: close(s); return (-1); } int relay_udp_socket(struct sockaddr_storage *ss, in_port_t port, struct protocol *proto) { int s = -1, val; if (relay_socket_af(ss, port) == -1) goto bad; if ((s = socket(ss->ss_family, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP)) == -1) goto bad; /* * Socket options */ if (proto->tcpflags & TCPFLAG_BUFSIZ) { val = proto->tcpbufsiz; if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) == -1) goto bad; val = proto->tcpbufsiz; if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) == -1) goto bad; } /* * IP options */ if (proto->tcpflags & TCPFLAG_IPTTL) { val = (int)proto->tcpipttl; switch (ss->ss_family) { case AF_INET: if (setsockopt(s, IPPROTO_IP, IP_TTL, &val, sizeof(val)) == -1) goto bad; break; case AF_INET6: if (setsockopt(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val)) == -1) goto bad; break; } } if (proto->tcpflags & TCPFLAG_IPMINTTL) { val = (int)proto->tcpipminttl; switch (ss->ss_family) { case AF_INET: if (setsockopt(s, IPPROTO_IP, IP_MINTTL, &val, sizeof(val)) == -1) goto bad; break; case AF_INET6: if (setsockopt(s, IPPROTO_IPV6, IPV6_MINHOPCOUNT, &val, sizeof(val)) == -1) goto bad; break; } } return (s); bad: if (s != -1) close(s); return (-1); } void relay_udp_response(int fd, short sig, void *arg) { struct rsession *con = arg; struct relay *rlay = con->se_relay; struct protocol *proto = rlay->rl_proto; void *priv = NULL; struct sockaddr_storage ss; u_int8_t buf[IBUF_READ_SIZE]; ssize_t len; socklen_t slen; if (sig == EV_TIMEOUT) { relay_udp_timeout(fd, sig, arg); return; } if (rlay->rl_conf.flags & F_DISABLE) return; slen = sizeof(ss); if ((len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&ss, &slen)) < 1) return; /* Parse and validate the packet header */ if (proto->validate != NULL && (priv = (*proto->validate)(con, rlay, &ss, buf, len)) == NULL) return; relay_close(con, "unknown response", 1); free(priv); } void relay_udp_server(int fd, short sig, void *arg) { struct privsep *ps = env->sc_ps; struct relay *rlay = arg; struct protocol *proto = rlay->rl_proto; struct rsession *con = NULL; struct ctl_natlook *cnl = NULL; socklen_t slen; struct timeval tv; struct sockaddr_storage ss; u_int8_t buf[IBUF_READ_SIZE]; void *priv = NULL; ssize_t len; event_add(&rlay->rl_ev, NULL); if (rlay->rl_conf.flags & F_DISABLE) return; slen = sizeof(ss); if ((len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&ss, &slen)) < 1) return; if (proto->validate != NULL && (priv = (*proto->validate)(NULL, rlay, &ss, buf, len)) == NULL) return; if ((con = calloc(1, sizeof(*con))) == NULL) { free(priv); return; } /* * Replace the DNS request Id with a random Id. */ con->se_priv = priv; con->se_in.s = -1; con->se_out.s = -1; con->se_in.dst = &con->se_out; con->se_out.dst = &con->se_in; con->se_in.con = con; con->se_out.con = con; con->se_relay = rlay; con->se_id = ++relay_conid; con->se_in.dir = RELAY_DIR_REQUEST; con->se_out.dir = RELAY_DIR_RESPONSE; con->se_retry = rlay->rl_conf.dstretry; con->se_out.port = rlay->rl_conf.dstport; switch (ss.ss_family) { case AF_INET: con->se_in.port = ((struct sockaddr_in *)&ss)->sin_port; break; case AF_INET6: con->se_in.port = ((struct sockaddr_in6 *)&ss)->sin6_port; break; } bcopy(&ss, &con->se_in.ss, sizeof(con->se_in.ss)); getmonotime(&con->se_tv_start); bcopy(&con->se_tv_start, &con->se_tv_last, sizeof(con->se_tv_last)); relay_sessions++; SPLAY_INSERT(session_tree, &rlay->rl_sessions, con); relay_session_publish(con); /* Increment the per-relay session counter */ rlay->rl_stats[ps->ps_instance].last++; /* Pre-allocate output buffer */ con->se_out.output = evbuffer_new(); if (con->se_out.output == NULL) { relay_close(con, "failed to allocate output buffer", 1); return; } /* Pre-allocate log buffer */ con->se_haslog = 0; con->se_log = evbuffer_new(); if (con->se_log == NULL) { relay_close(con, "failed to allocate log buffer", 1); return; } if (rlay->rl_conf.flags & F_NATLOOK) { if ((cnl = calloc(1, sizeof(*cnl))) == NULL) { relay_close(con, "failed to allocate natlookup", 1); return; } } /* Save the received data */ if (evbuffer_add(con->se_out.output, buf, len) == -1) { relay_close(con, "failed to store buffer", 1); free(cnl); return; } if (cnl != NULL) { con->se_cnl = cnl; bzero(cnl, sizeof(*cnl)); cnl->in = -1; cnl->id = con->se_id; cnl->proc = ps->ps_instance; cnl->proto = IPPROTO_UDP; bcopy(&con->se_in.ss, &cnl->src, sizeof(cnl->src)); bcopy(&rlay->rl_conf.ss, &cnl->dst, sizeof(cnl->dst)); proc_compose(env->sc_ps, PROC_PFE, IMSG_NATLOOK, cnl, sizeof(*cnl)); /* Schedule timeout */ evtimer_set(&con->se_ev, relay_natlook, con); bcopy(&rlay->rl_conf.timeout, &tv, sizeof(tv)); evtimer_add(&con->se_ev, &tv); return; } relay_session(con); } void relay_udp_timeout(int fd, short sig, void *arg) { struct rsession *con = arg; if (sig != EV_TIMEOUT) fatalx("invalid timeout event"); relay_close(con, "udp timeout", 1); } /* * Domain Name System support */ struct relay_dns_priv { u_int16_t dp_inkey; u_int16_t dp_outkey; }; struct relay_dnshdr { u_int16_t dns_id; u_int8_t dns_flags0; #define DNS_F0_QR 0x80 /* response flag */ #define DNS_F0_OPCODE 0x78 /* message type */ #define DNS_F0_AA 0x04 /* authorative answer */ #define DNS_F0_TC 0x02 /* truncated message */ #define DNS_F0_RD 0x01 /* recursion desired */ u_int8_t dns_flags1; #define DNS_F1_RA 0x80 /* recursion available */ #define DNS_F1_RES 0x40 /* reserved */ #define DNS_F1_AD 0x20 /* authentic data */ #define DNS_F1_CD 0x10 /* checking disabled */ #define DNS_F1_RCODE 0x0f /* response code */ u_int16_t dns_qdcount; u_int16_t dns_ancount; u_int16_t dns_nscount; u_int16_t dns_arcount; } __packed; void relay_dns_log(struct rsession *con, u_int8_t *buf, size_t len) { struct relay_dnshdr *hdr = (struct relay_dnshdr *)buf; /* Validate the header length */ if (len < sizeof(*hdr)) { log_debug("%s: session %d: short dns packet", __func__, con->se_id); return; } log_debug("%s: session %d: %s id 0x%x " "flags 0x%x:0x%x qd %u an %u ns %u ar %u", __func__, con->se_id, hdr->dns_flags0 & DNS_F0_QR ? "response" : "request", ntohs(hdr->dns_id), hdr->dns_flags0, hdr->dns_flags1, ntohs(hdr->dns_qdcount), ntohs(hdr->dns_ancount), ntohs(hdr->dns_nscount), ntohs(hdr->dns_arcount)); } void * relay_dns_validate(struct rsession *con, struct relay *rlay, struct sockaddr_storage *ss, u_int8_t *buf, size_t len) { struct relay_dnshdr *hdr = (struct relay_dnshdr *)buf; struct rsession lookup; u_int16_t key; struct relay_dns_priv *priv, lpriv; /* Validate the header length */ if (len < sizeof(*hdr)) return (NULL); key = ntohs(hdr->dns_id); /* * Check if the header has the response flag set, otherwise * return 0 to tell the UDP server to create a new session. */ if ((hdr->dns_flags0 & DNS_F0_QR) == 0) { priv = malloc(sizeof(struct relay_dns_priv)); if (priv == NULL) return (NULL); priv->dp_inkey = shuffle_generate16(&relay_shuffle); priv->dp_outkey = key; return ((void *)priv); } /* * Lookup if this response is for a known session and if the * remote host matches the original destination of the request. */ if (con == NULL) { lpriv.dp_inkey = key; lookup.se_priv = &lpriv; if ((con = SPLAY_FIND(session_tree, &rlay->rl_sessions, &lookup)) != NULL && con->se_priv != NULL && relay_cmp_af(ss, &con->se_out.ss) == 0) relay_dns_result(con, buf, len); } else { priv = con->se_priv; if (priv == NULL || key != priv->dp_inkey) { relay_close(con, "invalid response", 1); return (NULL); } relay_dns_result(con, buf, len); } /* * This is not a new session, ignore it in the UDP server. */ return (NULL); } int relay_dns_request(struct rsession *con) { struct relay *rlay = con->se_relay; struct relay_dns_priv *priv = con->se_priv; u_int8_t *buf = EVBUFFER_DATA(con->se_out.output); size_t len = EVBUFFER_LENGTH(con->se_out.output); struct relay_dnshdr *hdr; socklen_t slen; if (buf == NULL || priv == NULL || len < 1) return (-1); if (log_getverbose() > 1) relay_dns_log(con, buf, len); getmonotime(&con->se_tv_start); if (!TAILQ_EMPTY(&rlay->rl_tables)) { if (relay_from_table(con) != 0) return (-1); } else if (con->se_out.ss.ss_family == AF_UNSPEC) { bcopy(&rlay->rl_conf.dstss, &con->se_out.ss, sizeof(con->se_out.ss)); con->se_out.port = rlay->rl_conf.dstport; } if ((con->se_out.s = relay_udp_socket(&con->se_out.ss, con->se_out.port, rlay->rl_proto)) == -1) return (-1); slen = con->se_out.ss.ss_len; hdr = (struct relay_dnshdr *)buf; hdr->dns_id = htons(priv->dp_inkey); retry: if (sendto(con->se_out.s, buf, len, 0, (struct sockaddr *)&con->se_out.ss, slen) == -1) { if (con->se_retry) { con->se_retry--; log_debug("%s: session %d: " "forward failed: %s, %s", __func__, con->se_id, strerror(errno), con->se_retry ? "next retry" : "last retry"); goto retry; } log_debug("%s: session %d: forward failed: %s", __func__, con->se_id, strerror(errno)); return (-1); } event_again(&con->se_ev, con->se_out.s, EV_TIMEOUT|EV_READ, relay_udp_response, &con->se_tv_start, &rlay->rl_conf.timeout, con); return (0); } void relay_dns_result(struct rsession *con, u_int8_t *buf, size_t len) { struct relay *rlay = con->se_relay; struct relay_dns_priv *priv = con->se_priv; struct relay_dnshdr *hdr; socklen_t slen; if (priv == NULL) fatalx("%s: response to invalid session", __func__); if (log_getverbose() > 1) relay_dns_log(con, buf, len); /* * Replace the random DNS request Id with the original Id */ hdr = (struct relay_dnshdr *)buf; hdr->dns_id = htons(priv->dp_outkey); slen = con->se_out.ss.ss_len; if (sendto(rlay->rl_s, buf, len, 0, (struct sockaddr *)&con->se_in.ss, slen) == -1) { relay_close(con, "response failed", 1); return; } relay_close(con, "session closed", 0); } int relay_dns_cmp(struct rsession *a, struct rsession *b) { struct relay_dns_priv *ap = a->se_priv; struct relay_dns_priv *bp = b->se_priv; if (ap == NULL || bp == NULL) fatalx("%s: invalid session", __func__); return (memcmp(&ap->dp_inkey, &bp->dp_inkey, sizeof(u_int16_t))); }