commit 645116d4e692f0696f0918e705c80a8e0e2e35da from: Kirill A. Korinsky date: Fri Jan 31 23:41:18 2025 UTC Implementat SPF as it described in RFC 7208 commit - 7da80e474cc43ffc4c26454ae45a58a2cdd8640a commit + 645116d4e692f0696f0918e705c80a8e0e2e35da blob - 10c1cc66485be1a20b5b3ee2cfe6678aa2521e37 blob + 1c32695a9a8eaba8f30225133c48ddfdf0ca268e --- main.c +++ main.c @@ -1,5 +1,8 @@ /* + * Copyright (c) 2024 Kirill A. Korinsky * Copyright (c) 2022 Martijn van Duren + * Copyright (c) 2019 Martijn van Duren + * Copyright (c) 2017 Gilles Chehade * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -31,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -111,6 +115,54 @@ enum iprev_state { IPREV_FAIL }; +/* + * Base on RFC7208 + */ +enum spf_state { + SPF_NONE, + SPF_NEUTRAL, + SPF_PASS, + SPF_FAIL, + SPF_SOFTFAIL, + SPF_TEMPERROR, + SPF_PERMERROR +}; + +/* + * RFC 5321 doesn't limit record size, enforce some resanable limit + */ +#define SPF_RECORD_MAX 4096 + +struct spf_query { + struct spf_record *spf; + struct event_asr *eva; + int type; + enum spf_state q; + int include; + int exists; + char *domain; + char *txt; + int pos; +}; + +struct spf_record { + struct osmtpd_ctx *ctx; + enum spf_state state; + const char *state_reason; + char *sender_local; + char *sender_domain; + int nqueries; + int running; + int done; +/* RFC 7208 Section 4.6.4 limits to 10 DNS lookup, + * and one is reserved for the first query. + * To prevent of infinity loop I count each CNAME + * as dedicated lookup, same as A and AAAA. + * So, I use 41 as the limit. */ +#define SPF_DNS_LOOKUP_LIMIT 41 + struct spf_query queries[SPF_DNS_LOOKUP_LIMIT]; +}; + struct header { struct message *msg; uint8_t readdone; @@ -132,17 +184,27 @@ struct message { struct header *header; size_t nheaders; int readdone; + int nqueries; }; struct session { struct osmtpd_ctx *ctx; enum iprev_state iprev; + struct spf_record *spf_helo; + struct spf_record *spf_mailfrom; + struct sockaddr_storage src; + char *identity; + char *rdns; }; void usage(void); void auth_conf(const char *, const char *); void auth_connect(struct osmtpd_ctx *, const char *, enum osmtpd_status, struct sockaddr_storage *, struct sockaddr_storage *); +void spf_identity(struct osmtpd_ctx *, const char *); +void spf_mailfrom(struct osmtpd_ctx *, const char *); void auth_dataline(struct osmtpd_ctx *, const char *); +void *spf_record_new(struct osmtpd_ctx *, const char *); +void spf_record_free(struct spf_record *); void *auth_session_new(struct osmtpd_ctx *); void auth_session_free(struct osmtpd_ctx *, void *); void *auth_message_new(struct osmtpd_ctx *); @@ -173,7 +235,24 @@ void dkim_body_parse(struct message *, const char *); void dkim_body_verify(struct dkim_signature *); void dkim_rr_resolve(struct asr_result *, void *); const char *iprev_state2str(enum iprev_state); +char *spf_evaluate_domain(struct spf_record *, const char *); +void spf_lookup_record(struct spf_record *, const char *, int, + enum spf_state, int, int); +void spf_done(struct spf_record *, enum spf_state, const char *); +void spf_resolve(struct asr_result *, void *); +void spf_resolve_txt(struct dns_rr *, struct spf_query *); +void spf_resolve_mx(struct dns_rr *, struct spf_query *); +void spf_resolve_a(struct dns_rr *, struct spf_query *); +void spf_resolve_aaaa(struct dns_rr *, struct spf_query *); +void spf_resolve_cname(struct dns_rr *, struct spf_query *); +char* spf_parse_txt(const char *, size_t); +int spf_check_cidr(struct spf_record *, struct in_addr *, int ); +int spf_check_cidr6(struct spf_record *, struct in6_addr *, int ); +int spf_execute_txt(struct spf_query *); +const char *spf_state2str(enum spf_state); +int spf_ar_cat(const char *, struct spf_record *, char **, size_t *, ssize_t *); void auth_message_verify(struct message *); +void auth_ar_create(struct osmtpd_ctx *); ssize_t auth_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...) __attribute__((__format__ (printf, 4, 5))); int auth_ar_print(struct osmtpd_ctx *, const char *); @@ -196,10 +275,13 @@ main(int argc, char *argv[]) if ((ectx = EVP_ENCODE_CTX_new()) == NULL) osmtpd_err(1, "EVP_ENCODE_CTX_new"); - osmtpd_need(OSMTPD_NEED_FCRDNS); + osmtpd_need(OSMTPD_NEED_SRC|OSMTPD_NEED_FCRDNS|OSMTPD_NEED_IDENTITY|OSMTPD_NEED_GREETING); osmtpd_register_conf(auth_conf); osmtpd_register_filter_dataline(auth_dataline); osmtpd_register_report_connect(1, auth_connect); + osmtpd_register_filter_helo(spf_identity); + osmtpd_register_filter_ehlo(spf_identity); + osmtpd_register_filter_mailfrom(spf_mailfrom); osmtpd_local_session(auth_session_new, auth_session_free); osmtpd_local_message(auth_message_new, auth_message_free); osmtpd_run(); @@ -236,6 +318,56 @@ auth_connect(struct osmtpd_ctx *ctx, const char *rdns, ses->iprev = IPREV_PASS; else ses->iprev = IPREV_FAIL; + + memcpy(&ses->src, src, sizeof(struct sockaddr_storage)); + + if (rdns != NULL) { + if ((ses->rdns = strdup(rdns)) == NULL) + osmtpd_err(1, "%s: malloc", __func__); + } +} + +void +spf_identity(struct osmtpd_ctx *ctx, const char *identity) +{ + char from[HOST_NAME_MAX + 12]; + + struct session *ses = ctx->local_session; + + if (identity == NULL) { + osmtpd_filter_proceed(ctx); + return; + } + + if ((ses->identity = strdup(identity)) == NULL) + osmtpd_err(1, "%s: strdup", __func__); + + if (strlen(identity) == 0) { + osmtpd_filter_proceed(ctx); + return; + } + + snprintf(from, sizeof(from), "postmaster@%s", identity); + + if ((ses->spf_helo = spf_record_new(ctx, from)) == NULL) + osmtpd_filter_proceed(ctx); +} + +void +spf_mailfrom(struct osmtpd_ctx *ctx, const char *from) +{ + struct session *ses = ctx->local_session; + + if (from == NULL || !strlen(from)) { + osmtpd_filter_proceed(ctx); + return; + } + + if (ses->spf_mailfrom) + spf_record_free(ses->spf_mailfrom); + + if ((ses->spf_mailfrom = spf_record_new(ctx, from)) == NULL) + osmtpd_filter_proceed(ctx); } void @@ -275,7 +407,82 @@ auth_dataline(struct osmtpd_ctx *ctx, const char *line return; } else { dkim_body_parse(msg, line); + } +} + +void * +spf_record_new(struct osmtpd_ctx *ctx, const char *from) +{ + int i; + const char *at; + struct spf_record *spf; + + if ((spf = malloc(sizeof(*spf))) == NULL) + osmtpd_err(1, "%s: malloc", __func__); + + spf->ctx = ctx; + spf->state = SPF_NONE; + spf->state_reason = NULL; + spf->nqueries = 0; + spf->running = 0; + spf->done = 0; + + for (i = 0; i < SPF_DNS_LOOKUP_LIMIT; i++) { + spf->queries[i].domain = NULL; + spf->queries[i].txt = NULL; + spf->queries[i].eva = NULL; + } + + from = osmtpd_ltok_skip_cfws(from, 1); + + if ((at = osmtpd_ltok_skip_local_part(from, 0)) == NULL) + goto fail; + + if ((spf->sender_local = strndup(from, at - from)) == NULL) + osmtpd_err(1, "%s: malloc", __func__); + + if (*at != '@') + goto fail_local; + at++; + + if ((from = osmtpd_ltok_skip_domain(at, 0)) == NULL) + goto fail_local; + + + if ((spf->sender_domain = strndup(at, from - at)) == NULL) + osmtpd_err(1, "%s: malloc", __func__); + + spf_lookup_record( + spf, spf->sender_domain, T_TXT, SPF_PASS, 0, 0); + + return spf; + +fail_local: + free(spf->sender_local); + +fail: + free(spf); + return NULL; +} + +void +spf_record_free(struct spf_record *spf) +{ + int i; + + for (i = 0; i < SPF_DNS_LOOKUP_LIMIT; i++) { + if (spf->queries[i].domain) + free(spf->queries[i].domain); + if (spf->queries[i].txt) + free(spf->queries[i].txt); + if (spf->queries[i].eva) + event_asr_abort(spf->queries[i].eva); } + + free(spf->sender_local); + free(spf->sender_domain); + + free(spf); } void * @@ -289,6 +496,12 @@ auth_session_new(struct osmtpd_ctx *ctx) ses->ctx = ctx; ses->iprev = IPREV_NONE; + ses->spf_helo = NULL; + ses->spf_mailfrom = NULL; + + ses->identity = NULL; + ses->rdns = NULL; + return ses; } @@ -296,6 +509,15 @@ void auth_session_free(struct osmtpd_ctx *ctx, void *data) { struct session *ses = data; + + if (ses->spf_helo) + spf_record_free(ses->spf_helo); + if (ses->spf_mailfrom) + spf_record_free(ses->spf_mailfrom); + if (ses->identity) + free(ses->identity); + if (ses->rdns) + free(ses->rdns); free(ses); } @@ -320,6 +542,7 @@ auth_message_new(struct osmtpd_ctx *ctx) msg->header = NULL; msg->nheaders = 0; msg->readdone = 0; + msg->nqueries = 0; return msg; } @@ -559,12 +782,15 @@ dkim_lookup_record(struct dkim_signature *sig, const c if (sig->query != NULL) { event_asr_abort(sig->query); sig->query = NULL; + sig->header->msg->nqueries--; } if ((query = res_query_async(domain, C_IN, T_TXT, NULL)) == NULL) osmtpd_err(1, "res_query_async"); if ((sig->query = event_asr_run(query, dkim_rr_resolve, sig)) == NULL) osmtpd_err(1, "res_query_async"); + + sig->header->msg->nqueries++; } void @@ -1109,6 +1335,7 @@ dkim_signature_state(struct dkim_signature *sig, enum if (sig->query != NULL) { event_asr_abort(sig->query); sig->query = NULL; + sig->header->msg->nqueries--; } switch (sig->state) { case DKIM_UNKNOWN: @@ -1173,6 +1400,7 @@ dkim_rr_resolve(struct asr_result *ar, void *arg) char buf[HOST_NAME_MAX + 1]; sig->query = NULL; + sig->header->msg->nqueries--; if (sig->state != DKIM_UNKNOWN) goto verify; @@ -1624,20 +1852,786 @@ iprev_state2str(enum iprev_state state) } } -void -auth_message_verify(struct message *msg) +char * +spf_evaluate_domain(struct spf_record *spf, const char *domain) { - struct session *ses = msg->ctx->local_session; - struct dkim_signature *sig; - size_t i; - ssize_t n, aroff = 0; - int found = 0; - char *line = NULL; - size_t linelen = 0; + struct session *ses = spf->ctx->local_session; - if (!msg->readdone) + char spec[HOST_NAME_MAX + 1]; + char macro[HOST_NAME_MAX + 1], smacro[sizeof(macro)]; + char delimiters[sizeof(".-+,/_=")]; + char *endptr, *tmp; + const u_char *addr; + size_t i, mlen; + long digits; + int reverse; + + if (domain == NULL || domain[0] == '\0') { + spf_done(spf, SPF_PERMERROR, "Empty domain"); + return NULL; + } + + for (i = 0; + domain[0] != ' ' && domain[0] != '\0' && i < sizeof(spec); + domain++) { + + if (domain[0] < 0x21 || domain[0] > 0x7e) { + spf_done( + spf, SPF_PERMERROR, "Invalid character in domain-spec"); + return NULL; + } + + if (domain[0] != '%') { + spec[i++] = domain[0]; + continue; + } + domain++; + + switch (domain[0]) { + case '%': + spec[i++] = '%'; + break; + case '_': + spec[i++] = ' '; + break; + case '-': + if (i + 3 >= sizeof(spec)) { + spf_done( + spf, SPF_PERMERROR, "domain-spec too large"); + return NULL; + } + + spec[i++] = '%'; + spec[i++] = '2'; + spec[i++] = '0'; + break; + case '{': + domain++; + digits = -1; + reverse = 0; + delimiters[0] = '\0'; + + switch (domain[0]) { + case 'S': + case 's': + mlen = (size_t) snprintf(macro, sizeof(macro), + "%s@%s", spf->sender_local, + spf->sender_domain); + break; + case 'L': + case 'l': + mlen = strlcpy(macro, + spf->sender_local, sizeof(macro)); + break; + case 'O': + case 'o': + mlen = strlcpy(macro, + spf->sender_domain, + sizeof(macro)); + break; + case 'D': + case 'd': + if (spf->nqueries < 1) { + spf_done(spf, SPF_PERMERROR, + "no domain for d macro"); + return NULL; + } + mlen = strlcpy(macro, + spf->queries[spf->nqueries - 1].domain, + sizeof(macro)); + break; + case 'I': + case 'i': + if (ses->src.ss_family == AF_INET) { + addr = (u_char *)(&((struct sockaddr_in *) + &(ses->src))->sin_addr); + mlen = snprintf(macro, sizeof(macro), + "%u.%u.%u.%u", + (addr[0] & 0xff), (addr[1] & 0xff), + (addr[2] & 0xff), (addr[3] & 0xff)); + } else if (ses->src.ss_family == AF_INET6) { + addr = (u_char *)(&((struct sockaddr_in6 *) + &(ses->src))->sin6_addr); + mlen = snprintf(macro, sizeof(macro), + "%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx." + "%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx." + "%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx." + "%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx.%hhx", + (u_char) ((addr[0] >> 4) & 0x0f), (u_char) (addr[0] & 0x0f), + (u_char) ((addr[1] >> 4) & 0x0f), (u_char) (addr[1] & 0x0f), + (u_char) ((addr[2] >> 4) & 0x0f), (u_char) (addr[2] & 0x0f), + (u_char) ((addr[3] >> 4) & 0x0f), (u_char) (addr[3] & 0x0f), + (u_char) ((addr[4] >> 4) & 0x0f), (u_char) (addr[4] & 0x0f), + (u_char) ((addr[5] >> 4) & 0x0f), (u_char) (addr[5] & 0x0f), + (u_char) ((addr[6] >> 4) & 0x0f), (u_char) (addr[6] & 0x0f), + (u_char) ((addr[7] >> 4) & 0x0f), (u_char) (addr[7] & 0x0f), + (u_char) ((addr[8] >> 4) & 0x0f), (u_char) (addr[8] & 0x0f), + (u_char) ((addr[9] >> 4) & 0x0f), (u_char) (addr[9] & 0x0f), + (u_char) ((addr[10] >> 4) & 0x0f), (u_char) (addr[10] & 0x0f), + (u_char) ((addr[11] >> 4) & 0x0f), (u_char) (addr[11] & 0x0f), + (u_char) ((addr[12] >> 4) & 0x0f), (u_char) (addr[12] & 0x0f), + (u_char) ((addr[13] >> 4) & 0x0f), (u_char) (addr[13] & 0x0f), + (u_char) ((addr[14] >> 4) & 0x0f), (u_char) (addr[14] & 0x0f), + (u_char) ((addr[15] >> 4) & 0x0f), (u_char) (addr[15] & 0x0f)); + } else { + spf_done(spf, SPF_PERMERROR, + "unsupported type of address"); + return NULL; + } + break; + case 'P': + case 'p': + mlen = strlcpy(macro, ses->rdns, sizeof(macro)); + break; + case 'V': + case 'v': + if (ses->src.ss_family == AF_INET) + mlen = strlcpy(macro, "in-addr", + sizeof(macro)); + else if (ses->src.ss_family == AF_INET6) + mlen = strlcpy(macro, "ip6", + sizeof(macro)); + else { + spf_done(spf, SPF_PERMERROR, + "unsupported type of address"); + return NULL; + } + break; + case 'H': + case 'h': + mlen = strlcpy(macro, ses->identity, + sizeof(macro)); + break; + default: + spf_done(spf, SPF_PERMERROR, + "Unexpected macro in domain-spec"); + return NULL; + } + + if (mlen >= sizeof(macro)) { + spf_done(spf, SPF_PERMERROR, + "Macro expansions too large"); + return NULL; + } + + domain++; + if (isdigit(domain[0])) { + digits = strtol(domain, &endptr, 10); + if (digits < 1) { + spf_done(spf, SPF_PERMERROR, + "digits in macro can't be 0"); + return NULL; + } + domain = endptr; + } + + if (domain[0] == 'r') { + domain++; + reverse = 1; + } + + for (; strchr(".-+,/_=", domain[0]) != NULL; domain++) { + if (strchr(delimiters, domain[0]) == NULL) { + delimiters[strlen(delimiters) + 1] = '\0'; + delimiters[strlen(delimiters)] = domain[0]; + } + } + + if (delimiters[0] == '\0') { + delimiters[0] = '.'; + delimiters[1] = '\0'; + } + + if (domain[0] != '}') { + spf_done(spf, SPF_PERMERROR, + "Mallformed macro, expected end"); + return NULL; + } + + if (reverse) { + smacro[0] = '\0'; + tmp = macro + strlen(macro) - 1; + /* + * DIGIT rightmost elements after reversal is DIGIT + * lefmost elements before reversal + */ + while (1) { + while (tmp > macro && + strchr(delimiters, tmp[0]) == NULL) + tmp--; + if (tmp == macro) + break; + if (digits == 0) + break; + if (digits > 0) + digits--; + + tmp[0] = '\0'; + if (smacro[0] != '\0') + strlcat(smacro, ".", sizeof(smacro)); + strlcat(smacro, tmp + 1, sizeof(smacro)); + tmp--; + } + if (digits != 0) { + if (smacro[0] != '\0') + strlcat(smacro, ".", sizeof(smacro)); + strlcat(smacro, macro, sizeof(smacro)); + } + } else { + if (digits != -1) { + tmp = macro; + endptr = macro + strlen(macro); + while (digits > 0) { + while (tmp < endptr && + strchr(delimiters, tmp[0]) == NULL) + tmp++; + if (tmp == endptr) + break; + if (digits == 1) { + tmp[0] = '\0'; + break; + } + digits--; + tmp++; + } + } + strlcpy(smacro, macro, sizeof(smacro)); + } + + spec[i] = '\0'; + i = strlcat(spec, smacro, sizeof(spec)); + if (i >= sizeof(spec)) { + spf_done( + spf, SPF_PERMERROR, "domain-spec too large"); + return NULL; + } + break; + + default: + spf_done(spf, SPF_PERMERROR, + "Mallformed macro, unexpected character after %"); + return NULL; + } + } + + if ((tmp = strndup(spec, i)) == NULL) + osmtpd_err(1, "%s: strndup", __func__); + + return tmp; +} + +void +spf_lookup_record(struct spf_record *spf, const char *domain, int type, + enum spf_state qualifier, int include, int exists) +{ + struct asr_query *aq; + struct spf_query *query; + + if (spf->done) + return; + + if (spf->nqueries >= SPF_DNS_LOOKUP_LIMIT) { + spf_done(spf, SPF_PERMERROR, "To many DNS queries"); + return; + } + + query = &spf->queries[spf->nqueries]; + query->spf = spf; + query->type = type; + query->q = qualifier; + query->include = include; + query->exists = exists; + query->txt = NULL; + query->eva = NULL; + + if ((query->domain = spf_evaluate_domain(spf, domain)) == NULL) + return; + + if (domain == NULL || !strlen(domain)) { + spf_done(spf, SPF_PERMERROR, "Empty domain"); + return; + } + + if ((aq = res_query_async(query->domain, C_IN, type, NULL)) == NULL) + osmtpd_err(1, "res_query_async"); + + if ((query->eva = event_asr_run(aq, spf_resolve, query)) == NULL) + osmtpd_err(1, "event_asr_run"); + + spf->running++; + spf->nqueries++; +} + +void +spf_resolve(struct asr_result *ar, void *arg) +{ + int i; + + struct spf_query *query = arg; + struct spf_record *spf = query->spf; + struct unpack pack; + struct dns_header h; + struct dns_query q; + struct dns_rr rr; + char buf[HOST_NAME_MAX + 1]; + + query->eva = NULL; + query->spf->running--; + + if (ar->ar_h_errno == TRY_AGAIN + || ar->ar_h_errno == NO_RECOVERY) { + spf_done(query->spf, SPF_TEMPERROR, hstrerror(ar->ar_h_errno)); + goto end; + } + + if (ar->ar_h_errno == HOST_NOT_FOUND) { + if (query->include && !query->exists) + spf_done(query->spf, + SPF_PERMERROR, hstrerror(ar->ar_h_errno)); + goto consume; + } + + unpack_init(&pack, ar->ar_data, ar->ar_datalen); + if (unpack_header(&pack, &h) != 0 || + unpack_query(&pack, &q) != 0) { + osmtpd_warn(query->spf->ctx, + "Mallformed SPF DNS response for domain %s: %s", + print_dname(q.q_dname, buf, sizeof(buf)), + pack.err); + spf_done(query->spf, SPF_TEMPERROR, pack.err); + goto end; + } + + for (; h.ancount; h.ancount--) { + if (unpack_rr(&pack, &rr) != 0) { + osmtpd_warn(query->spf->ctx, + "Mallformed SPF DNS record for domain %s: %s", + print_dname(q.q_dname, buf, sizeof(buf)), + pack.err); + continue; + } + + switch (rr.rr_type) + { + case T_TXT: + spf_resolve_txt(&rr, query); + break; + + case T_MX: + spf_resolve_mx(&rr, query); + break; + + case T_A: + spf_resolve_a(&rr, query); + break; + + case T_AAAA: + spf_resolve_aaaa(&rr, query); + break; + + case T_CNAME: + spf_resolve_cname(&rr, query); + break; + + default: + osmtpd_warn(spf->ctx, + "Unexpected SPF DNS record: %d for domain %s", + rr.rr_type, query->domain); + spf_done(query->spf, SPF_TEMPERROR, "Unexpected record"); + break; + } + + if (spf->done) + goto end; + } + + consume: + if (spf->running > 0) + goto end; + + for (i = spf->nqueries - 1; i >= 0; i--) { + if (spf->queries[i].txt != NULL) { + if (spf_execute_txt(&spf->queries[i]) != 0) + break; + } + } + + end: + free(ar->ar_data); + if (!spf->done && spf->running == 0) + spf_done(spf, SPF_NONE, NULL); +} + +void +spf_resolve_txt(struct dns_rr *rr, struct spf_query *query) +{ + char *txt; + txt = spf_parse_txt(rr->rr.other.rdata, rr->rr.other.rdlen); + if (txt == NULL) { + osmtpd_warn(NULL, "spf_parse_txt"); + return; + } + + if (strncasecmp("v=spf1 ", txt, 7)) { + free(txt); + return; + } + + if (query->txt != NULL) { + free(txt); + spf_done(query->spf, SPF_PERMERROR, "Duplicated SPF record"); + return; + } + + query->txt = txt; + query->pos = 0; + spf_execute_txt(query); +} + +void +spf_resolve_mx(struct dns_rr *rr, struct spf_query *query) +{ + char buf[HOST_NAME_MAX + 1]; + + char *domain = print_dname(rr->rr.mx.exchange, buf, sizeof(buf)); + + spf_lookup_record(query->spf, domain, T_A, + query->q, query->include, 0); + spf_lookup_record(query->spf, domain, T_AAAA, + query->q, query->include, 0); +} + +void +spf_resolve_a(struct dns_rr *rr, struct spf_query *query) +{ + if (query->exists || + spf_check_cidr(query->spf, &rr->rr.in_a.addr, 32) == 0) { + spf_done(query->spf, query->q, NULL); + } +} + +void +spf_resolve_aaaa(struct dns_rr *rr, struct spf_query *query) +{ + if (spf_check_cidr6(query->spf, &rr->rr.in_aaaa.addr6, 128) == 0) { + spf_done(query->spf, query->q, NULL); + } +} + +void +spf_resolve_cname(struct dns_rr *rr, struct spf_query *query) +{ + char buf[HOST_NAME_MAX + 1]; + + char *domain = print_dname(rr->rr.cname.cname, buf, sizeof(buf)); + + spf_lookup_record(query->spf, domain, query->type, + query->q, query->include, 0); +} + +char * +spf_parse_txt(const char *rdata, size_t rdatalen) +{ + size_t len, dstsz = SPF_RECORD_MAX - 1; + ssize_t r = 0; + char *dst, *odst; + + if (rdatalen >= dstsz) { + errno = EOVERFLOW; + return NULL; + } + + odst = dst = malloc(dstsz); + if (dst == NULL) + osmtpd_err(1, "%s: malloc", __func__); + + while (rdatalen) { + len = *(const unsigned char *)rdata; + if (len >= rdatalen) { + errno = EINVAL; + return NULL; + } + + rdata++; + rdatalen--; + + if (len == 0) + continue; + + if (len >= dstsz) { + errno = EOVERFLOW; + return NULL; + } + memmove(dst, rdata, len); + dst += len; + dstsz -= len; + + rdata += len; + rdatalen -= len; + r += len; + } + + odst[r] = '\0'; + + return odst; +} + +int +spf_check_cidr(struct spf_record *spf, struct in_addr *net, int bits) +{ + struct in_addr *addr; + struct session *ses = spf->ctx->local_session; + + if (ses->src.ss_family != AF_INET) + return -1; + + if (bits == 0) + return 0; + + addr = &(((struct sockaddr_in *)(&ses->src))->sin_addr); + + return ((addr->s_addr ^ net->s_addr) & htonl(0xFFFFFFFFu << (32 - bits))); +} + +int +spf_check_cidr6(struct spf_record *spf, struct in6_addr *net, int bits) +{ + int rc; + uint32_t *a, *n, whole, incomplete; + struct in6_addr *addr; + struct session *ses = spf->ctx->local_session; + + if (ses->src.ss_family != AF_INET6) + return -1; + + if (bits == 0) + return 0; + + addr = &(((struct sockaddr_in6 *)(&ses->src))->sin6_addr); + + a = addr->__u6_addr.__u6_addr32; + n = net->__u6_addr.__u6_addr32; + + whole = bits >> 5; + incomplete = bits & 0x1f; + if (whole) { + rc = memcmp(a, n, whole << 2); + if (rc) + return rc; + } + if (incomplete) + return (a[whole] ^ n[whole]) & htonl((0xffffffffu) << (32 - incomplete)); + + return 0; +} + +int +spf_execute_txt(struct spf_query *query) +{ + struct in_addr ina; + struct in6_addr in6a; + char *ap = NULL; + char *in = query->txt + query->pos; + char *end; + int bits; + + enum spf_state q = query->q; + + while ((ap = strsep(&in, " ")) != NULL) { + if (strcasecmp(ap, "v=spf1") == 0) + continue; + + end = ap + strlen(ap)-1; + if (*end == '.') + *end = '\0'; + + if (*ap == '+') { + q = SPF_PASS; + ap++; + } else if (*ap == '-') { + q = SPF_FAIL; + ap++; + } else if (*ap == '~') { + q = SPF_SOFTFAIL; + ap++; + } else if (*ap == '?') { + q = SPF_NEUTRAL; + ap++; + } + + if (q != SPF_PASS && query->include) + continue; + + if (strncasecmp("all", ap, 3) == 0) { + spf_done(query->spf, q, NULL); + return 0; + } + if (strncasecmp("ip4:", ap, 4) == 0) { + if ((bits = inet_net_pton(AF_INET, ap + 4, &ina, sizeof(ina))) == -1) + continue; + + if (spf_check_cidr(query->spf, &ina, bits) == 0) { + spf_done(query->spf, q, NULL); + return 0; + } + continue; + } + if (strncasecmp("ip6:", ap, 4) == 0) { + if ((bits = inet_net_pton(AF_INET6, ap + 4, &ina, sizeof(ina))) == -1) + continue; + + if (spf_check_cidr6(query->spf, &in6a, bits) == 0) { + spf_done(query->spf, q, NULL); + return 0; + } + continue; + } + if (strcasecmp("a", ap) == 0) { + spf_lookup_record(query->spf, query->domain, T_A, + q, query->include, 0); + spf_lookup_record(query->spf, query->domain, T_AAAA, + q, query->include, 0); + break; + } + if (strncasecmp("a:", ap, 2) == 0) { + spf_lookup_record(query->spf, ap + 2, T_A, + q, query->include, 0); + spf_lookup_record(query->spf, ap + 2, T_AAAA, + q, query->include, 0); + break; + } + if (strncasecmp("exists:", ap, 7) == 0) { + spf_lookup_record(query->spf, ap + 7, T_A, + q, query->include, 1); + break; + } + if (strncasecmp("include:", ap, 8) == 0) { + spf_lookup_record(query->spf, ap + 8, T_TXT, q, 1, 0); + break; + } + if (strncasecmp("redirect=", ap, 9) == 0) { + if (in != NULL) + continue; + spf_lookup_record(query->spf, ap + 9, T_TXT, + q, query->include, 0); + return 0; + } + if (strcasecmp("mx", ap) == 0) { + spf_lookup_record(query->spf, query->domain, T_MX, + q, query->include, 0); + break; + } + if (strncasecmp("mx:", ap, 3) == 0) { + spf_lookup_record(query->spf, ap + 3, T_MX, + q, query->include, 0); + break; + } + } + + if (in == NULL) + return 0; + + query->pos = in - query->txt; + + return query->pos; +} + +void +spf_done(struct spf_record *spf, enum spf_state state, const char *reason) +{ + int i; + + if (spf->done) return; + for (i = 0; i < spf->nqueries; i++) { + if (spf->queries[i].eva) { + event_asr_abort(spf->queries[i].eva); + spf->queries[i].eva = NULL; + } + } + + spf->nqueries = 0; + spf->running = 0; + spf->state = state; + spf->state_reason = reason; + spf->done = 1; + + osmtpd_filter_proceed(spf->ctx); +} + +const char * +spf_state2str(enum spf_state state) +{ + switch (state) + { + case SPF_NONE: + return "none"; + case SPF_NEUTRAL: + return "neutral"; + case SPF_PASS: + return "pass"; + case SPF_FAIL: + return "fail"; + case SPF_SOFTFAIL: + return "softfail"; + case SPF_TEMPERROR: + return "temperror"; + case SPF_PERMERROR: + return "permerror"; + } +} + +int +spf_ar_cat(const char *type, struct spf_record *spf, char **line, size_t *linelen, ssize_t *aroff) +{ + if (spf == NULL) { + if ((*aroff = + auth_ar_cat(line, linelen, *aroff, + "; spf=none %s=none", type) + ) == -1) { + return -1; + } + return 0; + } + + if ((*aroff = + auth_ar_cat(line, linelen, *aroff, + "; spf=%s", spf_state2str(spf->state)) + ) == -1) { + return -1; + } + + if ((*aroff = + auth_ar_cat(line, linelen, *aroff, + " %s=%s@%s", + type, + spf->sender_local, + spf->sender_domain) + ) == -1) { + return -1; + } + + if (spf->state_reason != NULL) { + if ((*aroff = + auth_ar_cat(line, linelen, *aroff, + " reason=\"%s\"", spf->state_reason) + ) == -1) { + return -1; + } + } + + return 0; +} + +void +auth_message_verify(struct message *msg) +{ + size_t i; + + if (!msg->readdone || msg->nqueries > 0) + return; + for (i = 0; i < msg->nheaders; i++) { if (msg->header[i].sig == NULL) continue; @@ -1648,6 +2642,21 @@ auth_message_verify(struct message *msg) dkim_signature_state(msg->header[i].sig, DKIM_PASS, NULL); } + auth_ar_create(msg->ctx); +} + +void +auth_ar_create(struct osmtpd_ctx *ctx) +{ + struct dkim_signature *sig; + size_t i; + ssize_t n, aroff = 0; + int found = 0; + char *line = NULL; + size_t linelen = 0; + struct session *ses = ctx->local_session; + struct message *msg = ctx->local_message; + if ((aroff = auth_ar_cat(&line, &linelen, aroff, "Authentication-Results: %s", authservid)) == -1) osmtpd_err(1, "%s: malloc", __func__); @@ -1699,14 +2708,26 @@ auth_message_verify(struct message *msg) "; iprev=%s", iprev_state2str(ses->iprev))) == -1) osmtpd_err(1, "%s: malloc", __func__); - if (aroff == -1) + if (spf_ar_cat("smtp.helo", ses->spf_helo, + &line, &linelen, &aroff) != 0) osmtpd_err(1, "%s: malloc", __func__); - if (auth_ar_print(msg->ctx, line) != 0) { - osmtpd_warnx(msg->ctx, "Mallformed AR header"); - goto fail; + if (ses->spf_mailfrom != NULL) { + if (spf_ar_cat("smtp.mailfrom", ses->spf_mailfrom, + &line, &linelen, &aroff) != 0) + osmtpd_err(1, "%s: malloc", __func__); + } else { + if (spf_ar_cat("smtp.mailfrom", ses->spf_helo, + &line, &linelen, &aroff) != 0) + osmtpd_err(1, "%s: malloc", __func__); } + if (aroff == -1) + osmtpd_err(1, "%s: malloc", __func__); + + if (auth_ar_print(msg->ctx, line) != 0) + osmtpd_warn(msg->ctx, "Invalid AR line: %s", line); + rewind(msg->origf); while ((n = getline(&line, &linelen, msg->origf)) != -1) { line[n - 1] = '\0'; @@ -1714,7 +2735,6 @@ auth_message_verify(struct message *msg) } if (ferror(msg->origf)) osmtpd_err(1, "%s: ferror", __func__); - fail: free(line); return; } @@ -1765,6 +2785,10 @@ auth_ar_print(struct osmtpd_ctx *ctx, const char *star sizeof("iprev=") - 1) == 0) { ncheckpoint = osmtpd_ltok_skip_keyword( ncheckpoint + sizeof("iprev=") - 1, 0); + } else if (strncmp(ncheckpoint, "spf=", + sizeof("spf=") - 1) == 0) { + ncheckpoint = osmtpd_ltok_skip_keyword( + ncheckpoint + sizeof("spf=") - 1, 0); /* reasonspec */ } else if (strncmp(ncheckpoint, "reason=", sizeof("reason=") - 1) == 0) {