Commit message: smtpd's filter state machine can prematurely release resources
leading to a crash.  From gilles@
CVE: CVE-2020-35680
CWE: CWE-476
Vulnerability description: smtpd/lka_filter.c in OpenSMTPD before 6.8.0p1, in certain configurations, allows remote attackers to cause a denial of service (NULL pointer dereference and daemon crash) via a crafted pattern of client activity, because the filter state machine does not properly maintain the I/O channel between the SMTP engine and the filters layer.

Project: src
File: lka_filter.c

processor_errfd(struct io *io, int evt, void *arg)
{
	const char	*name = arg;
	char		*line = NULL;
	ssize_t		 len;

	switch (evt) {
	case IO_DATAIN:
		while ((line = io_getline(io, &len)) != NULL)
			log_warnx("%s: %s", name, line);
	}
}

Project: src
File: lka_filter.c

filter_builtins_global(struct filter_session *fs, struct filter *filter, uint64_t reqid)
{
	return filter_check_fcrdns(filter, fs->fcrdns) ||
	    filter_check_rdns(filter, fs->rdns) ||
	    filter_check_rdns_table(filter, K_DOMAIN, fs->rdns) ||
	    filter_check_rdns_regex(filter, fs->rdns) ||
	    filter_check_src_table(filter, K_NETADDR, ss_to_text(&fs->ss_src)) ||
	    filter_check_src_regex(filter, ss_to_text(&fs->ss_src)) ||
	    filter_check_helo_table(filter, K_DOMAIN, fs->helo) ||
	    filter_check_helo_regex(filter, fs->helo) ||
	    filter_check_auth(filter, fs->username) ||
	    filter_check_auth_table(filter, K_STRING, fs->username) ||
	    filter_check_auth_table(filter, K_CREDENTIALS, fs->username) ||
	    filter_check_auth_regex(filter, fs->username) ||
	    filter_check_mail_from_table(filter, K_MAILADDR, fs->mail_from) ||
	    filter_check_mail_from_regex(filter, fs->mail_from);
}

Project: src
File: lka_filter.c

filter_data_next(uint64_t token, uint64_t reqid, const char *line)
{
	struct filter_session  *fs;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	filter_data_internal(fs, token, reqid, line);
}

Project: src
File: lka_filter.c

filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param)
{
	struct filter_session  *fs;
	uint64_t		token = 0;
	char		       *nparam = NULL;
	
	fs = tree_xget(&sessions, reqid);

	switch (phase) {
	case FILTER_HELO:
	case FILTER_EHLO:
		free(fs->helo);
		fs->helo = xstrdup(param);
		break;
	case FILTER_MAIL_FROM:
		free(fs->mail_from);
		fs->mail_from = xstrdup(param + 1);
		*strchr(fs->mail_from, '>') = '\0';
		param = fs->mail_from;

		break;
	case FILTER_RCPT_TO:
		nparam = xstrdup(param + 1);
		*strchr(nparam, '>') = '\0';
		param = nparam;
		break;
	case FILTER_STARTTLS:
		/* TBD */
		break;
	default:
		break;
	}

	free(fs->lastparam);
	fs->lastparam = xstrdup(param);

	filter_protocol_internal(fs, &token, reqid, phase, param);
	if (nparam)
		free(nparam);
}

Project: src
File: lka_filter.c

lka_proc_forked(const char *name, uint32_t subsystems, int fd)
{
	struct processor_instance	*processor;

	if (!processors_inited) {
		dict_init(&processors);
		processors_inited = 1;
	}

	processor = xcalloc(1, sizeof *processor);
	processor->name = xstrdup(name);
	processor->io = io_new();
	processor->subsystems = subsystems;

	io_set_nonblocking(fd);

	io_set_fd(processor->io, fd);
	io_set_callback(processor->io, processor_io, processor->name);
	dict_xset(&processors, name, processor);
}

Project: src
File: lka_filter.c

processor_io(struct io *io, int evt, void *arg)
{
	struct processor_instance *processor;
	const char		*name = arg;
	char			*line = NULL;
	ssize_t			 len;

	switch (evt) {
	case IO_DATAIN:
		while ((line = io_getline(io, &len)) != NULL) {
			if (strncmp("register|", line, 9) == 0) {
				processor_register(name, line);
				continue;
			}
			
			processor = dict_xget(&processors, name);
			if (!processor->ready)
				fatalx("Non-register message before register|"
				    "ready: %s", line);
			else if (strncmp(line, "filter-result|", 14) == 0 ||
			    strncmp(line, "filter-dataline|", 16) == 0)
				lka_filter_process_response(name, line);
			else if (strncmp(line, "report|", 7) == 0)
				lka_report_proc(name, line);
			else
				fatalx("Invalid filter message type: %s", line);
		}
	}
}

Project: src
File: lka_filter.c

filter_result_rewrite(uint64_t reqid, const char *param)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_REWRITE);
	m_add_string(p_pony, param);
	m_close(p_pony);
}

Project: src
File: lka_filter.c

lka_report_smtp_link_identify(const char *direction, struct timeval *tv,
    uint64_t reqid, const char *method, const char *heloname)
{
	report_smtp_broadcast(reqid, direction, tv, "link-identify", "%s|%s\n",
	    method, heloname);
}

Project: src
File: lka_filter.c

lka_filter_proc_in_session(uint64_t reqid, const char *proc)
{
	struct filter_session	*fs;
	struct filter		*filter;
	size_t			 i;

	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return 0;

	filter = dict_get(&filters, fs->filter_name);
	if (filter == NULL || (filter->proc == NULL && filter->chain == NULL))
		return 0;

	if (filter->proc)
		return strcmp(filter->proc, proc) == 0 ? 1 : 0;

	for (i = 0; i < filter->chain_size; i++)
		if (filter->chain[i]->proc &&
		    strcmp(filter->chain[i]->proc, proc) == 0)
			return 1;

	return 0;
}

Project: src
File: lka_filter.c

lka_report_smtp_link_auth(const char *direction, struct timeval *tv, uint64_t reqid,
    const char *username, const char *result)
{
	struct filter_session *fs;

	if (strcmp(result, "pass") == 0) {
		fs = tree_xget(&sessions, reqid);
		fs->username = xstrdup(username);
	}
	report_smtp_broadcast(reqid, direction, tv, "link-auth", "%s|%s\n",
	    username, result);
}

Project: src
File: lka_filter.c

lka_filter_end(uint64_t reqid)
{
	struct filter_session	*fs;

	fs = tree_xpop(&sessions, reqid);
	free(fs->rdns);
	free(fs->helo);
	free(fs->mail_from);
	free(fs->username);
	free(fs->lastparam);
	free(fs->filter_name);
	free(fs);
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-end", reqid);
}

Project: src
File: lka_filter.c

filter_session_io(struct io *io, int evt, void *arg)
{
	struct filter_session *fs = arg;
	char *line = NULL;
	ssize_t len;

	log_trace(TRACE_IO, "filter session: %p: %s %s", fs, io_strevent(evt),
	    io_strio(io));

	switch (evt) {
	case IO_DATAIN:
	nextline:
		line = io_getline(fs->io, &len);
		/* No complete line received */
		if (line == NULL)
			return;

		filter_data(fs->id, line);

		goto nextline;
	}
}

Project: src
File: lka_filter.c

filter_builtins_mail_from(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}

Project: src
File: lka_filter.c

filter_check_auth_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->auth_table == NULL)
		return 0;
	
	if (key && table_match(filter->config->auth_table, kind, key) > 0)
		ret = 1;

	return filter->config->not_auth_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_check_src_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->src_table == NULL)
		return 0;

	if (table_match(filter->config->src_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_src_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_commit(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, size_t msgsz)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-commit", "%08x|%zd\n",
	    msgid, msgsz);
}

Project: src
File: lka_filter.c

lka_filter_register_hook(const char *name, const char *hook)
{
	struct dict		*subsystem;
	struct filter		*filter;
	const char	*filter_name;
	void		*iter;
	size_t	i;

	if (strncasecmp(hook, "smtp-in|", 8) == 0) {
		subsystem = &filter_smtp_in;
		hook += 8;
	}
	else
		fatalx("Invalid message direction: %s", hook);

	for (i = 0; i < nitems(filter_execs); i++)
		if (strcmp(hook, filter_execs[i].phase_name) == 0)
			break;
	if (i == nitems(filter_execs))
		fatalx("Unrecognized report name: %s", hook);

	iter = NULL;
	while (dict_iter(&filters, &iter, &filter_name, (void **)&filter))
		if (filter->proc && strcmp(name, filter->proc) == 0)
			filter->phases |= (1<<filter_execs[i].phase);
}

Project: src
File: lka_filter.c

filter_check_rdns(struct filter *filter, const char *hostname)
{
	int	ret = 0;
	struct netaddr	netaddr;

	if (!filter->config->rdns)
		return 0;

	/* this is a hack until smtp session properly deals with lack of rdns */
	ret = strcmp("<unknown>", hostname);
	if (ret == 0)
		return filter->config->not_rdns < 0 ? !ret : ret;

	/* if text_to_netaddress succeeds,
	 * we don't have an rDNS so the filter should match
	 */
	ret = !text_to_netaddr(&netaddr, hostname);
	return filter->config->not_rdns < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_check_rcpt_to_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->rcpt_to_table == NULL)
		return 0;

	if (table_match(filter->config->rcpt_to_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_rcpt_to_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

lka_report_smtp_timeout(const char *direction, struct timeval *tv, uint64_t reqid)
{
	report_smtp_broadcast(reqid, direction, tv, "timeout", "\n");
}

Project: src
File: lka_filter.c

filter_data_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *line)
{
	int	n;
	struct timeval	tv;

	gettimeofday(&tv, NULL);

	n = io_printf(lka_proc_get_io(filter->proc),
	    "filter|%s|%lld.%06ld|smtp-in|data-line|"
	    "%016"PRIx64"|%016"PRIx64"|%s\n",
	    PROTOCOL_VERSION,
	    tv.tv_sec, tv.tv_usec,
	    reqid, token, line);
	if (n == -1)
		fatalx("failed to write to processor");
}

Project: src
File: lka_filter.c

filter_builtins_connect(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}

Project: src
File: lka_filter.c

lka_proc_config(struct processor_instance *pi)
{
	io_printf(pi->io, "config|smtpd-version|%s\n", SMTPD_VERSION);
	io_printf(pi->io, "config|smtp-session-timeout|%d\n", SMTPD_SESSION_TIMEOUT);
	if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_IN)
		io_printf(pi->io, "config|subsystem|smtp-in\n");
	if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_OUT)
		io_printf(pi->io, "config|subsystem|smtp-out\n");
	io_printf(pi->io, "config|admd|%s\n",
	    env->sc_admd != NULL ? env->sc_admd : env->sc_hostname);
	io_printf(pi->io, "config|ready\n");
}

Project: src
File: lka_filter.c

lka_report_smtp_link_connect(const char *direction, struct timeval *tv, uint64_t reqid, const char *rdns,
    int fcrdns,
    const struct sockaddr_storage *ss_src,
    const struct sockaddr_storage *ss_dest)
{
	struct filter_session *fs;
	char	src[NI_MAXHOST + 5];
	char	dest[NI_MAXHOST + 5];
	uint16_t	src_port = 0;
	uint16_t	dest_port = 0;
	const char     *fcrdns_str;

	if (ss_src->ss_family == AF_INET)
		src_port = ntohs(((const struct sockaddr_in *)ss_src)->sin_port);
	else if (ss_src->ss_family == AF_INET6)
		src_port = ntohs(((const struct sockaddr_in6 *)ss_src)->sin6_port);

	if (ss_dest->ss_family == AF_INET)
		dest_port = ntohs(((const struct sockaddr_in *)ss_dest)->sin_port);
	else if (ss_dest->ss_family == AF_INET6)
		dest_port = ntohs(((const struct sockaddr_in6 *)ss_dest)->sin6_port);

	if (strcmp(ss_to_text(ss_src), "local") == 0) {
		(void)snprintf(src, sizeof src, "unix:%s", SMTPD_SOCKET);
		(void)snprintf(dest, sizeof dest, "unix:%s", SMTPD_SOCKET);
	} else {
		(void)snprintf(src, sizeof src, "%s:%d", ss_to_text(ss_src), src_port);
		(void)snprintf(dest, sizeof dest, "%s:%d", ss_to_text(ss_dest), dest_port);
	}

	switch (fcrdns) {
	case 1:
		fcrdns_str = "pass";
		break;
	case 0:
		fcrdns_str = "fail";
		break;
	default:
		fcrdns_str = "error";
		break;
	}

	fs = tree_xget(&sessions, reqid);
	fs->rdns = xstrdup(rdns);
	fs->fcrdns = fcrdns;
	fs->ss_src = *ss_src;
	fs->ss_dest = *ss_dest;

	report_smtp_broadcast(reqid, direction, tv, "link-connect",
	    "%s|%s|%s|%s\n", rdns, fcrdns_str, src, dest);
}

Project: src
File: lka_filter.c

lka_filter_ready(void)
{
	struct filter  	*filter;
	struct filter  	*subfilter;
	const char	*filter_name;
	struct filter_entry	*filter_entry;
	struct filter_chain	*filter_chain;
	void		*iter;
	size_t		i;
	size_t		j;

	/* all filters are ready, actually build the filter chains */
	iter = NULL;
	while (dict_iter(&filters, &iter, &filter_name, (void **)&filter)) {
		filter_chain = xcalloc(1, sizeof *filter_chain);
		for (i = 0; i < nitems(filter_execs); i++)
			TAILQ_INIT(&filter_chain->chain[i]);
		dict_set(&filter_chains, filter_name, filter_chain);

		if (filter->chain) {
			for (i = 0; i < filter->chain_size; i++) {
				subfilter = filter->chain[i];
				for (j = 0; j < nitems(filter_execs); ++j) {
					if (subfilter->phases & (1<<j)) {
						filter_entry = xcalloc(1, sizeof *filter_entry);
						filter_entry->id = generate_uid();
						filter_entry->name = subfilter->name;
						TAILQ_INSERT_TAIL(&filter_chain->chain[j],
						    filter_entry, entries);
					}
				}
			}
			continue;
		}

		for (i = 0; i < nitems(filter_execs); ++i) {
			if (filter->phases & (1<<i)) {
				filter_entry = xcalloc(1, sizeof *filter_entry);
				filter_entry->id = generate_uid();
				filter_entry->name = filter_name;
				TAILQ_INSERT_TAIL(&filter_chain->chain[i],
				    filter_entry, entries);
			}
		}
	}
}

Project: src
File: lka_filter.c

filter_check_src_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->src_regex == NULL)
		return 0;

	if (table_match(filter->config->src_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_src_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_check_rdns_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->rdns_table == NULL)
		return 0;
	
	if (table_match(filter->config->rdns_table, kind, key) > 0)
		ret = 1;

	return filter->config->not_rdns_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_builtins_commit(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}

Project: src
File: lka_filter.c

lka_report_init(void)
{
	struct reporters	*tailq;
	size_t			 i;

	dict_init(&report_smtp_in);
	dict_init(&report_smtp_out);

	for (i = 0; i < nitems(smtp_events); ++i) {
		tailq = xcalloc(1, sizeof (struct reporters));
		TAILQ_INIT(tailq);
		dict_xset(&report_smtp_in, smtp_events[i].event, tailq);

		tailq = xcalloc(1, sizeof (struct reporters));
		TAILQ_INIT(tailq);
		dict_xset(&report_smtp_out, smtp_events[i].event, tailq);
	}
}

Project: src
File: lka_filter.c

filter_check_mail_from_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->mail_from_regex == NULL)
		return 0;

	if (table_match(filter->config->mail_from_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_mail_from_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_result_proceed(uint64_t reqid)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_PROCEED);
	m_close(p_pony);
}

Project: src
File: lka_filter.c

lka_filter_data_end(uint64_t reqid)
{
	struct filter_session	*fs;

	fs = tree_xget(&sessions, reqid);
	if (fs->io) {
		io_free(fs->io);
		fs->io = NULL;
	}
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-end", reqid);
}

Project: src
File: lka_filter.c

filter_builtins_helo(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_rcpt(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-rcpt", "%08x|%s|%s\n",
	    msgid, result, address);
}

Project: src
File: lka_filter.c

lka_filter_begin(uint64_t reqid, const char *filter_name)
{
	struct filter_session	*fs;

	if (!filters_inited) {
		tree_init(&sessions);
		filters_inited = 1;
	}

	fs = xcalloc(1, sizeof (struct filter_session));
	fs->id = reqid;
	fs->filter_name = xstrdup(filter_name);
	tree_xset(&sessions, fs->id, fs);

	log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-begin", reqid);
}

Project: src
File: lka_filter.c

filter_result_reject(uint64_t reqid, const char *message)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_REJECT);
	m_add_string(p_pony, message);
	m_close(p_pony);
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_mail(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-mail", "%08x|%s|%s\n",
	    msgid, result, address);
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_begin(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-begin", "%08x\n",
	    msgid);
}

Project: src
File: lka_filter.c

filter_protocol_next(uint64_t token, uint64_t reqid, enum filter_phase phase)
{
	struct filter_session  *fs;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	filter_protocol_internal(fs, &token, reqid, phase, fs->lastparam);
}

Project: src
File: lka_filter.c

filter_check_helo_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->helo_table == NULL)
		return 0;

	if (table_match(filter->config->helo_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_helo_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_builtins_rcpt_to(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid) ||
	    filter_check_rcpt_to_table(filter, K_MAILADDR, param) ||
	    filter_check_rcpt_to_regex(filter, param);
}

Project: src
File: lka_filter.c

filter_builtins_data(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_reset(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-reset", "%08x\n",
	    msgid);
}

Project: src
File: lka_filter.c

lka_filter_process_response(const char *name, const char *line)
{
	uint64_t reqid;
	uint64_t token;
	char buffer[LINE_MAX];
	char *ep = NULL;
	char *kind = NULL;
	char *qid = NULL;
	/*char *phase = NULL;*/
	char *response = NULL;
	char *parameter = NULL;
	struct filter_session *fs;

	(void)strlcpy(buffer, line, sizeof buffer);
	if ((ep = strchr(buffer, '|')) == NULL)
		fatalx("Missing token: %s", line);
	ep[0] = '\0';

	kind = buffer;

	qid = ep+1;
	if ((ep = strchr(qid, '|')) == NULL)
		fatalx("Missing reqid: %s", line);
	ep[0] = '\0';

	reqid = strtoull(qid, &ep, 16);
	if (qid[0] == '\0' || *ep != '\0')
		fatalx("Invalid reqid: %s", line);
	if (errno == ERANGE && reqid == ULLONG_MAX)
		fatal("Invalid reqid: %s", line);

	qid = ep+1;
	if ((ep = strchr(qid, '|')) == NULL)
		fatal("Missing directive: %s", line);
	ep[0] = '\0';

	token = strtoull(qid, &ep, 16);
	if (qid[0] == '\0' || *ep != '\0')
		fatalx("Invalid token: %s", line);
	if (errno == ERANGE && token == ULLONG_MAX)
		fatal("Invalid token: %s", line);

	response = ep+1;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	if (strcmp(kind, "filter-dataline") == 0) {
		if (fs->phase != FILTER_DATA_LINE)
			fatalx("filter-dataline out of dataline phase");
		filter_data_next(token, reqid, response);
		return;
	}
	if (fs->phase == FILTER_DATA_LINE)
		fatalx("filter-result in dataline phase");

	if ((ep = strchr(response, '|'))) {
		parameter = ep + 1;
		ep[0] = '\0';
	}

	if (strcmp(response, "proceed") == 0) {
		if (parameter != NULL)
			fatalx("Unexpected parameter after proceed: %s", line);
		filter_protocol_next(token, reqid, 0);
		return;
	} else if (strcmp(response, "junk") == 0) {
		if (parameter != NULL)
			fatalx("Unexpected parameter after junk: %s", line);
		if (fs->phase == FILTER_COMMIT)
			fatalx("filter-reponse junk after DATA");
		filter_result_junk(reqid);
		return;
	} else {
		if (parameter == NULL)
			fatalx("Missing parameter: %s", line);

		if (strcmp(response, "rewrite") == 0)
			filter_result_rewrite(reqid, parameter);
		else if (strcmp(response, "reject") == 0)
			filter_result_reject(reqid, parameter);
		else if (strcmp(response, "disconnect") == 0)
			filter_result_disconnect(reqid, parameter);
		else
			fatalx("Invalid directive: %s", line);
	}
}

Project: src
File: lka_filter.c

filter_result_junk(uint64_t reqid)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_JUNK);
	m_close(p_pony);
}

Project: src
File: lka_filter.c

filter_check_auth_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->auth_regex == NULL)
		return 0;

	if (key && table_match(filter->config->auth_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_auth_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_data_internal(struct filter_session *fs, uint64_t token, uint64_t reqid, const char *line)
{
	struct filter_chain	*filter_chain;
	struct filter_entry	*filter_entry;
	struct filter		*filter;

	if (!token)
		fs->phase = FILTER_DATA_LINE;
	if (fs->phase != FILTER_DATA_LINE)
		fatalx("misbehaving filter");

	/* based on token, identify the filter_entry we should apply  */
	filter_chain = dict_get(&filter_chains, fs->filter_name);
	filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]);
	if (token) {
		TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries)
		    if (filter_entry->id == token)
			    break;
		if (filter_entry == NULL)
			fatalx("misbehaving filter");
		filter_entry = TAILQ_NEXT(filter_entry, entries);
	}

	/* no filter_entry, we either had none or reached end of chain */
	if (filter_entry == NULL) {
		io_printf(fs->io, "%s\n", line);
		return;
	}

	/* pass data to the filter */
	filter = dict_get(&filters, filter_entry->name);
	filter_data_query(filter, filter_entry->id, reqid, line);
}

Project: src
File: lka_filter.c

lka_report_smtp_link_greeting(const char *direction, uint64_t reqid,
    struct timeval *tv, const char *domain)
{
	report_smtp_broadcast(reqid, direction, tv, "link-greeting", "%s\n",
	    domain);
}

Project: src
File: lka_filter.c

lka_filter_data_begin(uint64_t reqid)
{
	struct filter_session  *fs;
	int	sp[2];
	int	fd = -1;

	fs = tree_xget(&sessions, reqid);

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1)
		goto end;
	io_set_nonblocking(sp[0]);
	io_set_nonblocking(sp[1]);
	fd = sp[0];
	fs->io = io_new();
	io_set_fd(fs->io, sp[1]);
	io_set_callback(fs->io, filter_session_io, fs);

end:
	m_create(p_pony, IMSG_FILTER_SMTP_DATA_BEGIN, 0, 0, fd);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, fd != -1 ? 1 : 0);
	m_close(p_pony);
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-begin fd=%d", reqid, fd);
}

Project: src
File: lka_filter.c

lka_report_proc(const char *name, const char *line)
{
	char buffer[LINE_MAX];
	struct timeval tv;
	char *ep, *sp, *direction;
	uint64_t reqid;

	if (strlcpy(buffer, line + 7, sizeof(buffer)) >= sizeof(buffer))
		fatalx("Invalid report: line too long: %s", line);

	errno = 0;
	tv.tv_sec = strtoll(buffer, &ep, 10);
	if (ep[0] != '.' || errno != 0)
		fatalx("Invalid report: invalid time: %s", line);
	sp = ep + 1;
	tv.tv_usec = strtol(sp, &ep, 10);
	if (ep[0] != '|' || errno != 0)
		fatalx("Invalid report: invalid time: %s", line);
	if (ep - sp != 6)
		fatalx("Invalid report: invalid time: %s", line);

	direction = ep + 1;
	if (strncmp(direction, "smtp-in|", 8) == 0) {
		direction[7] = '\0';
		direction += 7;
#if 0
	} else if (strncmp(direction, "smtp-out|", 9) == 0) {
		direction[8] = '\0';
		direction += 8;
#endif
	} else
		fatalx("Invalid report: invalid direction: %s", line);

	reqid = strtoull(sp, &ep, 16);
	if (ep[0] != '|' || errno != 0)
		fatalx("Invalid report: invalid reqid: %s", line);
	sp = ep + 1;

	lka_report_filter_report(reqid, name, 0, direction, &tv, sp);
}

Project: src
File: lka_filter.c

lka_filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param)
{
	filter_protocol(reqid, phase, param);
}

Project: src
File: lka_filter.c

filter_check_rcpt_to_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->rcpt_to_regex == NULL)
		return 0;

	if (table_match(filter->config->rcpt_to_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_rcpt_to_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

processor_register(const char *name, const char *line)
{
	struct processor_instance *processor;

	processor = dict_xget(&processors, name);

	if (strcmp(line, "register|ready") == 0) {
		processor->ready = 1;
		return;
	}

	if (strncmp(line, "register|report|", 16) == 0) {
		lka_report_register_hook(name, line+16);
		return;
	}

	if (strncmp(line, "register|filter|", 16) == 0) {
		lka_filter_register_hook(name, line+16);
		return;
	}

	fatalx("Invalid register line received: %s", line);
}

Project: src
File: lka_filter.c

report_smtp_broadcast(uint64_t reqid, const char *direction, struct timeval *tv, const char *event,
    const char *format, ...)
{
	va_list		ap;
	struct dict	*d;
	struct reporters	*tailq;
	struct reporter_proc	*rp;

	if (strcmp("smtp-in", direction) == 0)
		d = &report_smtp_in;

	else if (strcmp("smtp-out", direction) == 0)
		d = &report_smtp_out;

	else
		fatalx("unexpected direction: %s", direction);

	tailq = dict_xget(d, event);
	TAILQ_FOREACH(rp, tailq, entries) {
		if (!lka_filter_proc_in_session(reqid, rp->name))
			continue;

		va_start(ap, format);
		if (io_printf(lka_proc_get_io(rp->name),
		    "report|%s|%lld.%06ld|%s|%s|%016"PRIx64"%s",
		    PROTOCOL_VERSION, tv->tv_sec, tv->tv_usec, direction,
		    event, reqid, format[0] != '\n' ? "|" : "") == -1 ||
		    io_vprintf(lka_proc_get_io(rp->name), format, ap) == -1)
			fatalx("failed to write to processor");
		va_end(ap);
	}
}

Project: src
File: lka_filter.c

lka_proc_ready(void)
{
	void	*iter;
	struct processor_instance	*pi;

	iter = NULL;
	while (dict_iter(&processors, &iter, NULL, (void **)&pi))
		if (!pi->ready)
			return 0;
	return 1;
}

Project: src
File: lka_filter.c

filter_builtins_notimpl(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return 0;
}

Project: src
File: lka_filter.c

filter_check_fcrdns(struct filter *filter, int fcrdns)
{
	int	ret = 0;

	if (!filter->config->fcrdns)
		return 0;

	ret = fcrdns == 1;
	return filter->config->not_fcrdns < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_check_mail_from_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->mail_from_table == NULL)
		return 0;

	if (table_match(filter->config->mail_from_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_mail_from_table < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_protocol_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *phase, const char *param)
{
	int	n;
	struct filter_session	*fs;
	struct timeval	tv;

	gettimeofday(&tv, NULL);
	
	fs = tree_xget(&sessions, reqid);
	if (strcmp(phase, "connect") == 0)
		n = io_printf(lka_proc_get_io(filter->proc),
		    "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s|%s\n",
		    PROTOCOL_VERSION,
		    tv.tv_sec, tv.tv_usec,
		    phase, reqid, token, fs->rdns, param);
	else
		n = io_printf(lka_proc_get_io(filter->proc),
		    "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s\n",
		    PROTOCOL_VERSION,
		    tv.tv_sec, tv.tv_usec,
		    phase, reqid, token, param);
	if (n == -1)
		fatalx("failed to write to processor");
}

Project: src
File: lka_filter.c

lka_report_smtp_link_disconnect(const char *direction, struct timeval *tv, uint64_t reqid)
{
	report_smtp_broadcast(reqid, direction, tv, "link-disconnect", "\n");
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_data(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-data", "%08x|%s\n",
	    msgid, result);
}

Project: src
File: lka_filter.c

filter_result_disconnect(uint64_t reqid, const char *message)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_DISCONNECT);
	m_add_string(p_pony, message);
	m_close(p_pony);
}

Project: src
File: lka_filter.c

filter_check_helo_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->helo_regex == NULL)
		return 0;

	if (table_match(filter->config->helo_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_helo_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

lka_report_register_hook(const char *name, const char *hook)
{
	struct dict	*subsystem;
	struct reporter_proc	*rp;
	struct reporters	*tailq;
	void *iter;
	size_t	i;

	if (strncmp(hook, "smtp-in|", 8) == 0) {
		subsystem = &report_smtp_in;
		hook += 8;
	}
	else if (strncmp(hook, "smtp-out|", 9) == 0) {
		subsystem = &report_smtp_out;
		hook += 9;
	}
	else
		fatalx("Invalid message direction: %s", hook);

	if (strcmp(hook, "*") == 0) {
		iter = NULL;
		while (dict_iter(subsystem, &iter, NULL, (void **)&tailq)) {
			rp = xcalloc(1, sizeof *rp);
			rp->name = xstrdup(name);
			TAILQ_INSERT_TAIL(tailq, rp, entries);
		}
		return;
	}

	for (i = 0; i < nitems(smtp_events); i++)
		if (strcmp(hook, smtp_events[i].event) == 0)
			break;
	if (i == nitems(smtp_events))
		fatalx("Unrecognized report name: %s", hook);

	tailq = dict_get(subsystem, hook);
	rp = xcalloc(1, sizeof *rp);
	rp->name = xstrdup(name);
	TAILQ_INSERT_TAIL(tailq, rp, entries);
}

Project: src
File: lka_filter.c

filter_protocol_internal(struct filter_session *fs, uint64_t *token, uint64_t reqid, enum filter_phase phase, const char *param)
{
	struct filter_chain	*filter_chain;
	struct filter_entry	*filter_entry;
	struct filter		*filter;
	struct timeval		 tv;
	const char		*phase_name = filter_execs[phase].phase_name;
	int			 resume = 1;

	if (!*token) {
		fs->phase = phase;
		resume = 0;
	}

	/* XXX - this sanity check requires a protocol change, stub for now */
	phase = fs->phase;
	if (fs->phase != phase)
		fatalx("misbehaving filter");

	/* based on token, identify the filter_entry we should apply  */
	filter_chain = dict_get(&filter_chains, fs->filter_name);
	filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]);
	if (*token) {
		TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries)
		    if (filter_entry->id == *token)
			    break;
		if (filter_entry == NULL)
			fatalx("misbehaving filter");
		filter_entry = TAILQ_NEXT(filter_entry, entries);
	}

	/* no filter_entry, we either had none or reached end of chain */
	if (filter_entry == NULL) {
		log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, resume=%s, "
		    "action=proceed",
		    fs->id, phase_name, resume ? "y" : "n");
		filter_result_proceed(reqid);
		return;
	}

	/* process param with current filter_entry */
	*token = filter_entry->id;
	filter = dict_get(&filters, filter_entry->name);
	if (filter->proc) {
		log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
		    "resume=%s, action=deferred, filter=%s",
		    fs->id, phase_name, resume ? "y" : "n",
		    filter->name);
		filter_protocol_query(filter, filter_entry->id, reqid,
		    filter_execs[fs->phase].phase_name, param);
		return;	/* deferred response */
	}

	if (filter_execs[fs->phase].func(fs, filter, reqid, param)) {
		if (filter->config->rewrite) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=rewrite, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->rewrite);
			    filter_result_rewrite(reqid, filter->config->rewrite);
			return;
		}
		else if (filter->config->disconnect) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=disconnect, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->disconnect);
			filter_result_disconnect(reqid, filter->config->disconnect);
			return;
		}
		else if (filter->config->junk) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=junk, filter=%s, query=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param);
			filter_result_junk(reqid);
			return;
		} else if (filter->config->report) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=report, filter=%s, query=%s response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param, filter->config->report);

			gettimeofday(&tv, NULL);
			lka_report_filter_report(fs->id, filter->name, 1,
			    "smtp-in", &tv, filter->config->report);
		} else if (filter->config->bypass) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=bypass, filter=%s, query=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param);
			filter_result_proceed(reqid);
			return;
		} else {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=reject, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->reject);
			filter_result_reject(reqid, filter->config->reject);
			return;
		}
	}

	log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
	    "resume=%s, action=proceed, filter=%s, query=%s",
	    fs->id, phase_name, resume ? "y" : "n",
	    filter->name,
	    param);

	/* filter_entry resulted in proceed, try next filter */
	filter_protocol_internal(fs, token, reqid, phase, param);
	return;
}

Project: src
File: lka_filter.c

filter_check_auth(struct filter *filter, const char *username)
{
	int ret = 0;

	if (!filter->config->auth)
		return 0;

	ret = username ? 1 : 0;

	return filter->config->not_auth < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

filter_check_rdns_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->rdns_regex == NULL)
		return 0;

	if (table_match(filter->config->rdns_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_rdns_regex < 0 ? !ret : ret;
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_rollback(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-rollback", "%08x\n",
	    msgid);
}

Project: src
File: lka_filter.c

lka_proc_get_io(const char *name)
{
	struct processor_instance *processor;

	processor = dict_xget(&processors, name);

	return processor->io;
}

Project: src
File: lka_filter.c

lka_report_filter_report(uint64_t reqid, const char *name, int builtin,
    const char *direction, struct timeval *tv, const char *message)
{
	report_smtp_broadcast(reqid, direction, tv, "filter-report",
	    "%s|%s|%s\n", builtin ? "builtin" : "proc",
	    name, message);
}

Project: src
File: lka_filter.c

lka_filter_init(void)
{
	void		*iter;
	const char	*name;
	struct filter  	*filter;
	struct filter_config	*filter_config;
	size_t		i;
	char		 buffer[LINE_MAX];	/* for traces */

	dict_init(&filters);
	dict_init(&filter_chains);

	/* first pass, allocate and init individual filters */
	iter = NULL;
	while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) {
		switch (filter_config->filter_type) {
		case FILTER_TYPE_BUILTIN:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->phases |= (1<<filter_config->phase);
			filter->config = filter_config;
			dict_set(&filters, name, filter);
			log_trace(TRACE_FILTERS, "filters init type=builtin, name=%s, hooks=%08x",
			    name, filter->phases);
			break;

		case FILTER_TYPE_PROC:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->proc = filter_config->proc;
			filter->config = filter_config;
			dict_set(&filters, name, filter);
			log_trace(TRACE_FILTERS, "filters init type=proc, name=%s, proc=%s",
			    name, filter_config->proc);
			break;

		case FILTER_TYPE_CHAIN:
			break;
		}
	}

	/* second pass, allocate and init filter chains but don't build yet */
	iter = NULL;
	while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) {
		switch (filter_config->filter_type) {
		case FILTER_TYPE_CHAIN:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->chain = xcalloc(filter_config->chain_size, sizeof(void **));
			filter->chain_size = filter_config->chain_size;
			filter->config = filter_config;

			buffer[0] = '\0';
			for (i = 0; i < filter->chain_size; ++i) {
				filter->chain[i] = dict_xget(&filters, filter_config->chain[i]);
				if (i)
					(void)strlcat(buffer, ", ", sizeof buffer);
				(void)strlcat(buffer, filter->chain[i]->name, sizeof buffer);
			}
			log_trace(TRACE_FILTERS, "filters init type=chain, name=%s { %s }", name, buffer);

			dict_set(&filters, name, filter);
			break;

		case FILTER_TYPE_BUILTIN:
		case FILTER_TYPE_PROC:
			break;
		}
	}
}

Project: src
File: lka_filter.c

lka_report_smtp_tx_envelope(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, uint64_t evpid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-envelope",
	    "%08x|%016"PRIx64"\n", msgid, evpid);
}

Project: src
File: lka_filter.c

filter_data(uint64_t reqid, const char *line)
{
	struct filter_session  *fs;

	fs = tree_xget(&sessions, reqid);

	filter_data_internal(fs, 0, reqid, line);
}

Project: src
File: lka_filter.c

lka_report_smtp_protocol_server(const char *direction, struct timeval *tv, uint64_t reqid, const char *response)
{
	report_smtp_broadcast(reqid, direction, tv, "protocol-server", "%s\n",
	    response);
}

Project: src
File: lka_filter.c

lka_proc_errfd(const char *name, int fd)
{
	struct processor_instance	*processor;

	processor = dict_xget(&processors, name);

	io_set_nonblocking(fd);

	processor->errfd = io_new();
	io_set_fd(processor->errfd, fd);
	io_set_callback(processor->errfd, processor_errfd, processor->name);

	lka_proc_config(processor);
}

Project: src
File: lka_filter.c

lka_report_smtp_filter_response(const char *direction, struct timeval *tv, uint64_t reqid,
    int phase, int response, const char *param)
{
	const char *phase_name;
	const char *response_name;

	switch (phase) {
	case FILTER_CONNECT:
		phase_name = "connected";
		break;
	case FILTER_HELO:
		phase_name = "helo";
		break;
	case FILTER_EHLO:
		phase_name = "ehlo";
		break;
	case FILTER_STARTTLS:
		phase_name = "tls";
		break;
	case FILTER_AUTH:
		phase_name = "auth";
		break;
	case FILTER_MAIL_FROM:
		phase_name = "mail-from";
		break;
	case FILTER_RCPT_TO:
		phase_name = "rcpt-to";
		break;
	case FILTER_DATA:
		phase_name = "data";
		break;
	case FILTER_DATA_LINE:
		phase_name = "data-line";
		break;
	case FILTER_RSET:
		phase_name = "rset";
		break;
	case FILTER_QUIT:
		phase_name = "quit";
		break;
	case FILTER_NOOP:
		phase_name = "noop";
		break;
	case FILTER_HELP:
		phase_name = "help";
		break;
	case FILTER_WIZ:
		phase_name = "wiz";
		break;
	case FILTER_COMMIT:
		phase_name = "commit";
		break;
	default:
		phase_name = "";
	}

	switch (response) {
	case FILTER_PROCEED:
		response_name = "proceed";
		break;
	case FILTER_JUNK:
		response_name = "junk";
		break;
	case FILTER_REWRITE:
		response_name = "rewrite";
		break;
	case FILTER_REJECT:
		response_name = "reject";
		break;
	case FILTER_DISCONNECT:
		response_name = "disconnect";
		break;
	default:
		response_name = "";
	}

	report_smtp_broadcast(reqid, direction, tv, "filter-response",
	    "%s|%s%s%s\n", phase_name, response_name, param ? "|" : "",
	    param ? param : "");
}

Project: src
File: lka_filter.c

lka_report_smtp_protocol_client(const char *direction, struct timeval *tv, uint64_t reqid, const char *command)
{
	report_smtp_broadcast(reqid, direction, tv, "protocol-client", "%s\n",
	    command);
}

Project: src
File: lka_filter.c

lka_report_smtp_link_tls(const char *direction, struct timeval *tv, uint64_t reqid, const char *ciphers)
{
	report_smtp_broadcast(reqid, direction, tv, "link-tls", "%s\n",
	    ciphers);
}