[BACK]Return to relay_udp.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / relayd

File: [local] / src / usr.sbin / relayd / relay_udp.c (download)

Revision 1.50, Wed Dec 28 21:30:18 2022 UTC (17 months ago) by jmc
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3
Changes since 1.49: +2 -2 lines

spelling fixes; from paul tagliamonte
any parts of his diff not taken are noted on tech

/*	$OpenBSD: relay_udp.c,v 1.50 2022/12/28 21:30:18 jmc 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_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		/* 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)));
}