1
0
mirror of https://github.com/openbsd/src.git synced 2024-12-22 07:27:59 -08:00
openbsd-src/usr.sbin/relayd/relay_udp.c

551 lines
13 KiB
C

/* $OpenBSD: relay_udp.c,v 1.51 2024/05/18 06:34:46 jsg Exp $ */
/*
* Copyright (c) 2007 - 2013 Reyk Floeter <reyk@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/queue.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/tree.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <event.h>
#include <imsg.h>
#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_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 /* authoritative 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)));
}