Enforce SMTP MTA-STS destination policies
Debian Trixie package bundle / packages (push) Successful in 14m54s
Debian Trixie package bundle / packages (push) Successful in 14m54s
This commit is contained in:
+2
-1
@@ -11,7 +11,8 @@ The SMTP server supports classic SMTP and ESMTP with `SIZE`, `PIPELINING`,
|
||||
SASL authentication, and `REQUIRETLS` on TLS-capable listeners. `BINARYMIME`
|
||||
is deliberately not advertised or accepted. Authenticated submission and
|
||||
implicit TLS use listener-specific policy; Internet delivery uses
|
||||
opportunistic STARTTLS unless a destination policy requires TLS.
|
||||
opportunistic STARTTLS unless REQUIRETLS, DNSSEC-authenticated DANE, or an
|
||||
RFC 8461 MTA-STS destination policy requires authenticated TLS.
|
||||
|
||||
LMTP transports are available for local downstream services such as Mailman.
|
||||
Trusted SMTP/LMTP peers can receive original-client metadata through
|
||||
|
||||
@@ -127,6 +127,7 @@ inputs and outputs is absent from the ZIP.
|
||||
| SMTP-06 | Local authenticated delivery from test1 to test2 | | | |
|
||||
| SMTP-07 | Internet delivery prefers STARTTLS and follows opportunistic fallback | | | |
|
||||
| SMTP-08 | Required TLS and `REQUIRETLS` defer rather than downgrade | | | |
|
||||
| SMTP-27 | DANE takes precedence; MTA-STS enforce/testing/cache and MX wildcard policy | | | |
|
||||
| SMTP-09 | Relayhost hostname, port, TLS, authentication, failure, and recovery | | | |
|
||||
| SMTP-10 | Open-relay tests reject unauthenticated/untrusted third-party relay | | | |
|
||||
| SMTP-11 | `SIZE` exact limit, over-limit, absent size, and disk-space responses | | | |
|
||||
|
||||
+11
@@ -28,6 +28,17 @@ mandatory. Bogus or indeterminate DNSSEC results defer delivery. Insecure DNS
|
||||
or securely proven TLSA absence leaves the existing opportunistic STARTTLS
|
||||
policy in place.
|
||||
|
||||
`outbound_mta_sts_enabled` also defaults to `true`. When DANE is inactive,
|
||||
Bongo discovers the RFC 8461 policy at `_mta-sts.<domain>`, downloads it only
|
||||
over certificate-validated HTTPS, and keeps the validated policy in
|
||||
`outbound_mta_sts_cache_directory`. An `enforce` policy permits only listed
|
||||
MX hosts and requires authenticated TLS 1.2 or newer; policy, certificate,
|
||||
STARTTLS, and handshake failures defer delivery without a plaintext retry. A
|
||||
`testing` policy records the same failures without changing delivery. A valid
|
||||
cached policy remains authoritative through its `max_age` when DNS discovery
|
||||
or HTTPS refresh temporarily fails. DANE always takes precedence over
|
||||
MTA-STS.
|
||||
|
||||
The resolver named in `resolv.conf` must therefore be a trusted validating
|
||||
resolver reached over a trusted local path; Bongo must not trust an AD bit
|
||||
received from an untrusted network resolver. GnuTLS/libdane also needs its
|
||||
|
||||
@@ -105,6 +105,7 @@ typedef struct {
|
||||
int XplDnsInit(void);
|
||||
void XplDnsResultFree(XplDns_Result *result);
|
||||
XplDns_Result * XplDnsLookupIP(const char *domain);
|
||||
XplDns_Result * XplDnsLookupTXT(const char *domain);
|
||||
int XplDnsMxRecordsAreNull(const XplDns_RecordList *records);
|
||||
XplDns_MxLookup *XplDnsNewMxLookup(const char *domain);
|
||||
XplDns_IpList * XplDnsNextMxLookupIpList(XplDns_MxLookup *mx);
|
||||
|
||||
@@ -23,6 +23,7 @@ d @XPL_DEFAULT_STATE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_DBF_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_WORK_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_CACHE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_CACHE_DIR@/mta-sts 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_SCMS_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_SPOOL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
d @XPL_DEFAULT_SYSTEM_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
|
||||
|
||||
@@ -22,6 +22,7 @@ target_link_libraries(bongosmtp
|
||||
add_executable(bongosmtpc
|
||||
smtpc.c
|
||||
dane.c
|
||||
mta-sts.c
|
||||
capabilities.c
|
||||
protocol.c
|
||||
proxy.c
|
||||
@@ -93,4 +94,17 @@ if(BUILD_TESTING)
|
||||
GnuTLS::GnuTLS
|
||||
)
|
||||
add_test(NAME smtp-dane COMMAND smtp-dane-test)
|
||||
|
||||
add_executable(smtp-mta-sts-test
|
||||
tests/mta-sts-test.c
|
||||
mta-sts.c
|
||||
)
|
||||
target_include_directories(smtp-mta-sts-test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(smtp-mta-sts-test PRIVATE
|
||||
bongoxpl
|
||||
CURL::libcurl
|
||||
)
|
||||
add_test(NAME smtp-mta-sts COMMAND smtp-mta-sts-test)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of version 2 of the GNU General Public License
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact Novell, Inc.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
|
||||
|
||||
#include <config.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <xpldns.h>
|
||||
|
||||
#include "mta-sts.h"
|
||||
|
||||
#define POLICY_LIMIT 65536U
|
||||
#define POLICY_REFRESH 86400
|
||||
|
||||
typedef struct {
|
||||
char data[POLICY_LIMIT + 1U];
|
||||
size_t length;
|
||||
} PolicyDownload;
|
||||
|
||||
static char *
|
||||
Trim(char *value)
|
||||
{
|
||||
char *end;
|
||||
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
end = value + strlen(value);
|
||||
while (end > value && (end[-1] == ' ' || end[-1] == '\t' ||
|
||||
end[-1] == '\r'))
|
||||
*--end = '\0';
|
||||
return value;
|
||||
}
|
||||
|
||||
static int
|
||||
ValidIdentifier(const char *value)
|
||||
{
|
||||
size_t index;
|
||||
size_t length = strlen(value);
|
||||
|
||||
if (length == 0U || length > 32U) return 0;
|
||||
for (index = 0U; index < length; index++)
|
||||
if (!isalnum((unsigned char)value[index])) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
SMTPMtaStsParseRecord(const char *record, char id[33])
|
||||
{
|
||||
char copy[512];
|
||||
char *field;
|
||||
char *save = NULL;
|
||||
int foundId = 0;
|
||||
|
||||
if (record == NULL || id == NULL || strlen(record) >= sizeof(copy))
|
||||
return 0;
|
||||
strcpy(copy, record);
|
||||
field = strtok_r(copy, ";", &save);
|
||||
if (field == NULL || strcmp(Trim(field), "v=STSv1") != 0) return 0;
|
||||
while ((field = strtok_r(NULL, ";", &save)) != NULL) {
|
||||
char *name = Trim(field);
|
||||
char *equals = strchr(name, '=');
|
||||
char *value;
|
||||
|
||||
if (equals == NULL) return 0;
|
||||
*equals = '\0';
|
||||
name = Trim(name);
|
||||
value = Trim(equals + 1);
|
||||
if (strcmp(name, "id") == 0 && !foundId) {
|
||||
if (!ValidIdentifier(value)) return 0;
|
||||
strcpy(id, value);
|
||||
foundId = 1;
|
||||
}
|
||||
}
|
||||
return foundId;
|
||||
}
|
||||
|
||||
static int
|
||||
ValidDomainPattern(const char *pattern)
|
||||
{
|
||||
const char *value = pattern;
|
||||
const char *label;
|
||||
size_t length;
|
||||
size_t index;
|
||||
|
||||
if (strncmp(value, "*.", 2U) == 0) value += 2;
|
||||
length = strlen(value);
|
||||
if (length == 0U || length > 253U || value[0] == '.' ||
|
||||
value[length - 1U] == '.')
|
||||
return 0;
|
||||
for (index = 0U; index < length; index++) {
|
||||
unsigned char byte = (unsigned char)value[index];
|
||||
if (!(isalnum(byte) || byte == '-' || byte == '.')) return 0;
|
||||
}
|
||||
for (label = value; *label != '\0';) {
|
||||
const char *dot = strchr(label, '.');
|
||||
size_t labelLength = dot == NULL ? strlen(label) : (size_t)(dot - label);
|
||||
|
||||
if (labelLength == 0U || labelLength > 63U || label[0] == '-' ||
|
||||
label[labelLength - 1U] == '-')
|
||||
return 0;
|
||||
label = dot == NULL ? label + labelLength : dot + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
SMTPMtaStsParsePolicy(const char *text, SMTPMtaStsPolicy *policy)
|
||||
{
|
||||
char *copy;
|
||||
char *line;
|
||||
char *save = NULL;
|
||||
int haveVersion = 0;
|
||||
int haveMode = 0;
|
||||
int haveMaxAge = 0;
|
||||
unsigned int fieldNumber = 0U;
|
||||
|
||||
if (text == NULL || policy == NULL || strlen(text) > POLICY_LIMIT)
|
||||
return 0;
|
||||
memset(policy, 0, sizeof(*policy));
|
||||
copy = strdup(text);
|
||||
if (copy == NULL) return 0;
|
||||
for (line = strtok_r(copy, "\n", &save); line != NULL;
|
||||
line = strtok_r(NULL, "\n", &save)) {
|
||||
char *name = Trim(line);
|
||||
char *colon = strchr(name, ':');
|
||||
char *value;
|
||||
|
||||
if (colon == NULL) goto invalid;
|
||||
*colon = '\0';
|
||||
value = Trim(colon + 1);
|
||||
name = Trim(name);
|
||||
fieldNumber++;
|
||||
if (strcmp(name, "version") == 0) {
|
||||
if (!haveVersion) {
|
||||
if (fieldNumber != 1U || strcmp(value, "STSv1") != 0)
|
||||
goto invalid;
|
||||
haveVersion = 1;
|
||||
}
|
||||
} else if (strcmp(name, "mode") == 0) {
|
||||
if (!haveMode) {
|
||||
if (strcmp(value, "enforce") == 0)
|
||||
policy->mode = SMTP_MTA_STS_ENFORCE;
|
||||
else if (strcmp(value, "testing") == 0)
|
||||
policy->mode = SMTP_MTA_STS_TESTING;
|
||||
else if (strcmp(value, "none") == 0)
|
||||
policy->mode = SMTP_MTA_STS_INACTIVE;
|
||||
else
|
||||
goto invalid;
|
||||
haveMode = 1;
|
||||
}
|
||||
} else if (strcmp(name, "max_age") == 0) {
|
||||
if (!haveMaxAge) {
|
||||
char *end = NULL;
|
||||
const char *digit;
|
||||
unsigned long age;
|
||||
|
||||
for (digit = value; *digit != '\0'; digit++)
|
||||
if (!isdigit((unsigned char)*digit)) goto invalid;
|
||||
errno = 0;
|
||||
age = strtoul(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' ||
|
||||
age > 31557600UL)
|
||||
goto invalid;
|
||||
policy->maxAge = age;
|
||||
haveMaxAge = 1;
|
||||
}
|
||||
} else if (strcmp(name, "mx") == 0) {
|
||||
size_t length;
|
||||
|
||||
if (policy->mxCount >= SMTP_MTA_STS_MAX_MX ||
|
||||
!ValidDomainPattern(value))
|
||||
goto invalid;
|
||||
length = strlen(value);
|
||||
memcpy(policy->mx[policy->mxCount], value, length + 1U);
|
||||
policy->mxCount++;
|
||||
}
|
||||
}
|
||||
free(copy);
|
||||
return haveVersion && haveMode && haveMaxAge &&
|
||||
(policy->mode == SMTP_MTA_STS_INACTIVE || policy->mxCount > 0U);
|
||||
|
||||
invalid:
|
||||
free(copy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
SMTPMtaStsMxMatches(const SMTPMtaStsPolicy *policy, const char *hostname)
|
||||
{
|
||||
unsigned int index;
|
||||
size_t hostLength;
|
||||
|
||||
if (policy == NULL || hostname == NULL) return 0;
|
||||
hostLength = strlen(hostname);
|
||||
while (hostLength > 0U && hostname[hostLength - 1U] == '.') hostLength--;
|
||||
for (index = 0U; index < policy->mxCount; index++) {
|
||||
const char *pattern = policy->mx[index];
|
||||
size_t patternLength = strlen(pattern);
|
||||
|
||||
if (pattern[0] != '*' || pattern[1] != '.') {
|
||||
if (patternLength == hostLength &&
|
||||
strncasecmp(pattern, hostname, hostLength) == 0)
|
||||
return 1;
|
||||
continue;
|
||||
}
|
||||
pattern += 1; /* Keep the dot: wildcard must match at least one label. */
|
||||
patternLength--;
|
||||
if (hostLength > patternLength &&
|
||||
memchr(hostname, '.', hostLength - patternLength) == NULL &&
|
||||
strncasecmp(hostname + hostLength - patternLength, pattern,
|
||||
patternLength) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t
|
||||
DownloadWrite(char *data, size_t size, size_t count, void *opaque)
|
||||
{
|
||||
PolicyDownload *download = opaque;
|
||||
size_t length;
|
||||
|
||||
if (size != 0U && count > SIZE_MAX / size) return 0U;
|
||||
length = size * count;
|
||||
if (length > POLICY_LIMIT - download->length) return 0U;
|
||||
memcpy(download->data + download->length, data, length);
|
||||
download->length += length;
|
||||
download->data[download->length] = '\0';
|
||||
return length;
|
||||
}
|
||||
|
||||
static int
|
||||
FetchPolicy(const char *domain, SMTPMtaStsPolicy *policy)
|
||||
{
|
||||
CURL *curl;
|
||||
CURLcode result;
|
||||
long response = 0L;
|
||||
char url[320];
|
||||
char *contentType = NULL;
|
||||
PolicyDownload download;
|
||||
int written;
|
||||
|
||||
written = snprintf(url, sizeof(url),
|
||||
"https://mta-sts.%s/.well-known/mta-sts.txt", domain);
|
||||
if (written < 0 || (size_t)written >= sizeof(url))
|
||||
return 0;
|
||||
memset(&download, 0, sizeof(download));
|
||||
curl = curl_easy_init();
|
||||
if (curl == NULL) return 0;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Bongo MTA-STS/0.7");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, DownloadWrite);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &download);
|
||||
result = curl_easy_perform(curl);
|
||||
if (result == CURLE_OK) {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
|
||||
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &contentType);
|
||||
}
|
||||
if (result != CURLE_OK || response != 200L || contentType == NULL ||
|
||||
strncasecmp(contentType, "text/plain", 10U) != 0 ||
|
||||
(contentType[10] != '\0' && contentType[10] != ';' &&
|
||||
!isspace((unsigned char)contentType[10])) ||
|
||||
memchr(download.data, '\0', download.length) != NULL ||
|
||||
!SMTPMtaStsParsePolicy(download.data, policy)) {
|
||||
curl_easy_cleanup(curl);
|
||||
return 0;
|
||||
}
|
||||
curl_easy_cleanup(curl);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
CachePath(char *path, size_t pathSize, const char *directory,
|
||||
const char *domain)
|
||||
{
|
||||
size_t index;
|
||||
int written;
|
||||
|
||||
for (index = 0U; domain[index] != '\0'; index++) {
|
||||
unsigned char byte = (unsigned char)domain[index];
|
||||
if (!(isalnum(byte) || byte == '-' || byte == '.')) return 0;
|
||||
}
|
||||
written = snprintf(path, pathSize, "%s/%s.policy", directory, domain);
|
||||
return written >= 0 && (size_t)written < pathSize;
|
||||
}
|
||||
|
||||
static int
|
||||
LoadCache(const char *directory, const char *domain,
|
||||
SMTPMtaStsPolicy *policy, int requireCurrent)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char header[128];
|
||||
char *body = NULL;
|
||||
char id[33];
|
||||
long long fetched;
|
||||
long size;
|
||||
FILE *file;
|
||||
time_t now = time(NULL);
|
||||
int result = 0;
|
||||
|
||||
if (!CachePath(path, sizeof(path), directory, domain)) return 0;
|
||||
file = fopen(path, "r");
|
||||
if (file == NULL) return 0;
|
||||
if (fscanf(file, "id:%32s\nfetched:%lld\n", id, &fetched) != 2 ||
|
||||
fseek(file, 0L, SEEK_END) != 0 || (size = ftell(file)) < 0L ||
|
||||
size > (long)POLICY_LIMIT + 80L || fseek(file, 0L, SEEK_SET) != 0)
|
||||
goto done;
|
||||
body = malloc((size_t)size + 1U);
|
||||
if (body == NULL || fgets(header, sizeof(header), file) == NULL ||
|
||||
fgets(header, sizeof(header), file) == NULL)
|
||||
goto done;
|
||||
size = (long)fread(body, 1U, POLICY_LIMIT, file);
|
||||
body[size] = '\0';
|
||||
if (!SMTPMtaStsParsePolicy(body, policy)) goto done;
|
||||
strcpy(policy->id, id);
|
||||
snprintf(policy->domain, sizeof(policy->domain), "%s", domain);
|
||||
policy->fetched = (time_t)fetched;
|
||||
if (requireCurrent &&
|
||||
(now < policy->fetched ||
|
||||
(unsigned long)(now - policy->fetched) > policy->maxAge))
|
||||
goto done;
|
||||
result = 1;
|
||||
|
||||
done:
|
||||
free(body);
|
||||
fclose(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int
|
||||
SaveCache(const char *directory, const SMTPMtaStsPolicy *policy,
|
||||
const char *body)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char temporary[PATH_MAX];
|
||||
int descriptor;
|
||||
FILE *file;
|
||||
int result = 0;
|
||||
int written;
|
||||
|
||||
if ((mkdir(directory, 0750) != 0 && errno != EEXIST) ||
|
||||
!CachePath(path, sizeof(path), directory, policy->domain))
|
||||
return 0;
|
||||
written = snprintf(temporary, sizeof(temporary), "%s/.policy.XXXXXX",
|
||||
directory);
|
||||
if (written < 0 || (size_t)written >= sizeof(temporary) ||
|
||||
(descriptor = mkstemp(temporary)) < 0)
|
||||
return 0;
|
||||
file = fdopen(descriptor, "w");
|
||||
if (file == NULL) {
|
||||
close(descriptor);
|
||||
unlink(temporary);
|
||||
return 0;
|
||||
}
|
||||
if (fprintf(file, "id:%s\nfetched:%lld\n%s", policy->id,
|
||||
(long long)policy->fetched, body) >= 0 &&
|
||||
fflush(file) == 0 && fsync(descriptor) == 0) result = 1;
|
||||
if (fclose(file) != 0) result = 0;
|
||||
if (result && rename(temporary, path) == 0) return 1;
|
||||
unlink(temporary);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
AppendPolicyLine(char *body, size_t bodySize, size_t *used,
|
||||
const char *format, const char *value)
|
||||
{
|
||||
int written;
|
||||
|
||||
if (*used >= bodySize) return 0;
|
||||
written = snprintf(body + *used, bodySize - *used, format, value);
|
||||
if (written < 0 || (size_t)written >= bodySize - *used) return 0;
|
||||
*used += (size_t)written;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
DiscoverRecord(const char *domain, char id[33])
|
||||
{
|
||||
char name[300];
|
||||
XplDns_Result *result;
|
||||
XplDns_RecordList *record;
|
||||
unsigned int count = 0U;
|
||||
|
||||
if (snprintf(name, sizeof(name), "_mta-sts.%s", domain) >=
|
||||
(int)sizeof(name))
|
||||
return 0;
|
||||
result = XplDnsLookupTXT(name);
|
||||
if (result == NULL || result->status != XPLDNS_SUCCESS) {
|
||||
XplDnsResultFree(result);
|
||||
return 0;
|
||||
}
|
||||
for (record = result->list; record != NULL; record = record->next) {
|
||||
char candidate[33];
|
||||
|
||||
if (record->type == XPLDNS_RR_TXT &&
|
||||
SMTPMtaStsParseRecord(record->record.TXT.txt, candidate)) {
|
||||
if (++count == 1U) strcpy(id, candidate);
|
||||
}
|
||||
}
|
||||
XplDnsResultFree(result);
|
||||
return count == 1U;
|
||||
}
|
||||
|
||||
SMTPMtaStsMode
|
||||
SMTPMtaStsLookup(const char *domain, const char *mxHostname,
|
||||
const char *cacheDirectory, SMTPMtaStsPolicy *policy)
|
||||
{
|
||||
SMTPMtaStsPolicy cached;
|
||||
SMTPMtaStsPolicy fetched;
|
||||
char id[33];
|
||||
char body[POLICY_LIMIT + 1U];
|
||||
int haveCache;
|
||||
int haveRecord;
|
||||
time_t now = time(NULL);
|
||||
|
||||
if (domain == NULL || mxHostname == NULL || cacheDirectory == NULL ||
|
||||
policy == NULL)
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
memset(policy, 0, sizeof(*policy));
|
||||
if (!ValidDomainPattern(domain) || strchr(domain, '*') != NULL ||
|
||||
!ValidDomainPattern(mxHostname) || strchr(mxHostname, '*') != NULL)
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
haveCache = LoadCache(cacheDirectory, domain, &cached, 1);
|
||||
haveRecord = DiscoverRecord(domain, id);
|
||||
if (haveRecord && haveCache && strcmp(id, cached.id) == 0 &&
|
||||
now >= cached.fetched && now - cached.fetched < POLICY_REFRESH) {
|
||||
*policy = cached;
|
||||
} else if (haveRecord) {
|
||||
if (FetchPolicy(domain, &fetched)) {
|
||||
/* Serialize the validated representation for the durable cache. */
|
||||
size_t used;
|
||||
unsigned int index;
|
||||
int written;
|
||||
|
||||
strcpy(fetched.id, id);
|
||||
snprintf(fetched.domain, sizeof(fetched.domain), "%s", domain);
|
||||
fetched.fetched = now;
|
||||
used = 0U;
|
||||
if (!AppendPolicyLine(body, sizeof(body), &used,
|
||||
"version: STSv1\nmode: %s\n",
|
||||
fetched.mode == SMTP_MTA_STS_ENFORCE
|
||||
? "enforce"
|
||||
: fetched.mode == SMTP_MTA_STS_TESTING
|
||||
? "testing"
|
||||
: "none"))
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
for (index = 0U; index < fetched.mxCount; index++)
|
||||
if (!AppendPolicyLine(body, sizeof(body), &used, "mx: %s\n",
|
||||
fetched.mx[index]))
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
if (used >= sizeof(body)) return SMTP_MTA_STS_INACTIVE;
|
||||
written = snprintf(body + used, sizeof(body) - used,
|
||||
"max_age: %lu\n", fetched.maxAge);
|
||||
if (written < 0 || (size_t)written >= sizeof(body) - used)
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
(void)SaveCache(cacheDirectory, &fetched, body);
|
||||
*policy = fetched;
|
||||
} else if (haveCache) {
|
||||
*policy = cached;
|
||||
} else {
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
}
|
||||
} else if (haveCache) {
|
||||
*policy = cached;
|
||||
} else {
|
||||
return SMTP_MTA_STS_INACTIVE;
|
||||
}
|
||||
|
||||
if (policy->mode != SMTP_MTA_STS_INACTIVE &&
|
||||
!SMTPMtaStsMxMatches(policy, mxHostname))
|
||||
return policy->mode;
|
||||
return policy->mode;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of version 2 of the GNU General Public License
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact Novell, Inc.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
|
||||
|
||||
#ifndef _SMTP_MTA_STS_H
|
||||
#define _SMTP_MTA_STS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#define SMTP_MTA_STS_MAX_MX 32U
|
||||
|
||||
typedef enum {
|
||||
SMTP_MTA_STS_INACTIVE = 0,
|
||||
SMTP_MTA_STS_TESTING,
|
||||
SMTP_MTA_STS_ENFORCE
|
||||
} SMTPMtaStsMode;
|
||||
|
||||
typedef struct {
|
||||
SMTPMtaStsMode mode;
|
||||
char id[33];
|
||||
char domain[256];
|
||||
unsigned long maxAge;
|
||||
time_t fetched;
|
||||
unsigned int mxCount;
|
||||
char mx[SMTP_MTA_STS_MAX_MX][256];
|
||||
} SMTPMtaStsPolicy;
|
||||
|
||||
int SMTPMtaStsParseRecord(const char *record, char id[33]);
|
||||
int SMTPMtaStsParsePolicy(const char *text, SMTPMtaStsPolicy *policy);
|
||||
int SMTPMtaStsMxMatches(const SMTPMtaStsPolicy *policy, const char *hostname);
|
||||
SMTPMtaStsMode SMTPMtaStsLookup(const char *domain, const char *mxHostname,
|
||||
const char *cacheDirectory,
|
||||
SMTPMtaStsPolicy *policy);
|
||||
|
||||
#endif
|
||||
@@ -54,6 +54,8 @@ static struct {
|
||||
BOOL outbound_tls_verify;
|
||||
BOOL outbound_tls_required;
|
||||
BOOL outbound_dane_enabled;
|
||||
BOOL outbound_mta_sts_enabled;
|
||||
char *outbound_mta_sts_cache_directory;
|
||||
GArray *lmtp_transports;
|
||||
GArray *xclient_trusted_destinations;
|
||||
GArray *xforward_trusted_destinations;
|
||||
@@ -84,6 +86,8 @@ static BongoConfigItem MailAuthConfig[] = {
|
||||
{ BONGO_JSON_BOOL, "o:outbound_tls_verify/b", &MailAuth.outbound_tls_verify },
|
||||
{ BONGO_JSON_BOOL, "o:outbound_tls_required/b", &MailAuth.outbound_tls_required },
|
||||
{ BONGO_JSON_BOOL, "o:outbound_dane_enabled/b", &MailAuth.outbound_dane_enabled },
|
||||
{ BONGO_JSON_BOOL, "o:outbound_mta_sts_enabled/b", &MailAuth.outbound_mta_sts_enabled },
|
||||
{ BONGO_JSON_STRING, "o:outbound_mta_sts_cache_directory/s", &MailAuth.outbound_mta_sts_cache_directory },
|
||||
{ BONGO_JSON_ARRAY, "o:lmtp_transports/a", &LMTPTransportList },
|
||||
{ BONGO_JSON_ARRAY, "o:xclient_trusted_destinations/a", &XClientTrustedHostList },
|
||||
{ BONGO_JSON_ARRAY, "o:xforward_trusted_destinations/a", &XForwardTrustedHostList },
|
||||
@@ -952,6 +956,9 @@ DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) {
|
||||
const char *envelopeSender = NULL;
|
||||
unsigned int ReplyCode = 0U;
|
||||
SMTPDanePolicy danePolicy = SMTP_DANE_INACTIVE;
|
||||
SMTPMtaStsPolicy mtaStsPolicy;
|
||||
SMTPMtaStsMode mtaStsMode = SMTP_MTA_STS_INACTIVE;
|
||||
BOOL mtaStsMxValid = TRUE;
|
||||
|
||||
requiresTLS = (Queue->flags & MSG_FLAG_REQUIRETLS) != 0;
|
||||
if (MailAuth.outbound_dane_enabled && !Remote->isLMTP) {
|
||||
@@ -966,6 +973,31 @@ DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) {
|
||||
goto finalization;
|
||||
}
|
||||
}
|
||||
memset(&mtaStsPolicy, 0, sizeof(mtaStsPolicy));
|
||||
if (danePolicy == SMTP_DANE_INACTIVE &&
|
||||
MailAuth.outbound_mta_sts_enabled && !Remote->isLMTP) {
|
||||
mtaStsMode = SMTPMtaStsLookup(
|
||||
Remote->originalNextHopDomain, Remote->transportHost,
|
||||
MailAuth.outbound_mta_sts_cache_directory != NULL &&
|
||||
MailAuth.outbound_mta_sts_cache_directory[0] != '\0'
|
||||
? MailAuth.outbound_mta_sts_cache_directory
|
||||
: XPL_DEFAULT_CACHE_DIR "/mta-sts",
|
||||
&mtaStsPolicy);
|
||||
mtaStsMxValid = mtaStsMode == SMTP_MTA_STS_INACTIVE ||
|
||||
SMTPMtaStsMxMatches(&mtaStsPolicy,
|
||||
Remote->transportHost);
|
||||
if (!mtaStsMxValid && mtaStsMode == SMTP_MTA_STS_ENFORCE) {
|
||||
Log(LOG_ERROR,
|
||||
"MTA-STS policy for %.900s rejects MX %.900s",
|
||||
Remote->originalNextHopDomain, Remote->transportHost);
|
||||
Recip->Result = DELIVER_TRY_LATER;
|
||||
goto finalization;
|
||||
}
|
||||
if (!mtaStsMxValid && mtaStsMode == SMTP_MTA_STS_TESTING)
|
||||
Log(LOG_WARN,
|
||||
"MTA-STS testing policy for %.900s does not match MX %.900s",
|
||||
Remote->originalNextHopDomain, Remote->transportHost);
|
||||
}
|
||||
|
||||
beginSession:
|
||||
/* We are connected to the remote SMTP server, but have not started the
|
||||
@@ -1015,6 +1047,7 @@ beginConversation:
|
||||
|
||||
/* REQUIRETLS overrides the legacy per-message no-TLS flag. */
|
||||
if ((requiresTLS || danePolicy >= SMTP_DANE_TLS_REQUIRED ||
|
||||
mtaStsMode == SMTP_MTA_STS_ENFORCE ||
|
||||
!(Queue->flags & MSG_FLAG_SMTPC_NOSSL)) &&
|
||||
(Extensions & EXT_TLS) && !Remote->conn->ssl.enable &&
|
||||
!plaintextFallback) {
|
||||
@@ -1029,8 +1062,14 @@ beginConversation:
|
||||
if ((Ret = ConnEncryptPeer(Remote->conn, SMTPAgent.SSL_Context,
|
||||
Remote->transportHost,
|
||||
(requiresTLS ||
|
||||
mtaStsMode == SMTP_MTA_STS_ENFORCE ||
|
||||
MailAuth.outbound_tls_verify))) < 0) {
|
||||
if (mtaStsMode == SMTP_MTA_STS_TESTING)
|
||||
Log(LOG_WARN,
|
||||
"MTA-STS testing policy detected a TLS negotiation or identity failure at %.900s",
|
||||
Remote->transportHost);
|
||||
if (!requiresTLS && danePolicy == SMTP_DANE_INACTIVE &&
|
||||
mtaStsMode != SMTP_MTA_STS_ENFORCE &&
|
||||
!MailAuth.outbound_tls_required &&
|
||||
!MailAuth.outbound_tls_verify &&
|
||||
ReconnectRemotePlaintext(Remote)) {
|
||||
@@ -1056,11 +1095,18 @@ beginConversation:
|
||||
Recip->Result = DELIVER_TRY_LATER;
|
||||
goto finalization;
|
||||
}
|
||||
if (mtaStsMode == SMTP_MTA_STS_TESTING &&
|
||||
!ConnTLSVerifyPeer(Remote->conn->ssl.context,
|
||||
Remote->transportHost))
|
||||
Log(LOG_WARN,
|
||||
"MTA-STS testing policy detected an invalid TLS identity for %.900s",
|
||||
Remote->transportHost);
|
||||
goto beginConversation;
|
||||
}
|
||||
}
|
||||
if ((MailAuth.outbound_tls_required || MailAuth.outbound_tls_verify ||
|
||||
danePolicy >= SMTP_DANE_TLS_REQUIRED ||
|
||||
mtaStsMode == SMTP_MTA_STS_ENFORCE ||
|
||||
requiresTLS) &&
|
||||
!Remote->conn->ssl.enable) {
|
||||
Log(LOG_ERROR, "Remote server %s does not offer required TLS",
|
||||
@@ -1068,6 +1114,11 @@ beginConversation:
|
||||
Recip->Result = DELIVER_TRY_LATER;
|
||||
goto finalization;
|
||||
}
|
||||
if (mtaStsMode == SMTP_MTA_STS_TESTING &&
|
||||
!Remote->conn->ssl.enable && !(Extensions & EXT_TLS))
|
||||
Log(LOG_WARN,
|
||||
"MTA-STS testing policy detected missing STARTTLS at %.900s",
|
||||
Remote->transportHost);
|
||||
if (requiresTLS && !(Extensions & EXT_REQUIRETLS)) {
|
||||
Log(LOG_ERROR,
|
||||
"Remote server %s does not support required REQUIRETLS transport",
|
||||
@@ -1758,6 +1809,7 @@ XplServiceMain(int argc, char *argv[])
|
||||
MailAuth.outbound_tls_verify = FALSE;
|
||||
MailAuth.outbound_tls_required = FALSE;
|
||||
MailAuth.outbound_dane_enabled = TRUE;
|
||||
MailAuth.outbound_mta_sts_enabled = TRUE;
|
||||
|
||||
/* Initialize the Bongo libraries */
|
||||
startupOpts = BA_STARTUP_CONNIO | BA_STARTUP_NMAP;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "smtp.h"
|
||||
#include "dane.h"
|
||||
#include "mta-sts.h"
|
||||
|
||||
#define AGENT_NAME "smtpd_c"
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of version 2 of the GNU General Public License
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact Novell, Inc.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "mta-sts.h"
|
||||
|
||||
static void
|
||||
TestRecord(void)
|
||||
{
|
||||
char id[33];
|
||||
|
||||
assert(SMTPMtaStsParseRecord("v=STSv1; id=20260721", id));
|
||||
assert(strcmp(id, "20260721") == 0);
|
||||
assert(SMTPMtaStsParseRecord(" v=STSv1 ; id = abc123 ; ext=x ", id));
|
||||
assert(strcmp(id, "abc123") == 0);
|
||||
assert(!SMTPMtaStsParseRecord("id=one; v=STSv1", id));
|
||||
assert(!SMTPMtaStsParseRecord("v=STSv2; id=one", id));
|
||||
assert(!SMTPMtaStsParseRecord("v=STSv1; id=bad-id", id));
|
||||
assert(!SMTPMtaStsParseRecord("v=STSv1", id));
|
||||
}
|
||||
|
||||
static void
|
||||
TestPolicy(void)
|
||||
{
|
||||
SMTPMtaStsPolicy policy;
|
||||
|
||||
assert(SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmx: mail.example.test\n"
|
||||
"mx: *.mx.example.test\nmax_age: 86400\n", &policy));
|
||||
assert(policy.mode == SMTP_MTA_STS_ENFORCE);
|
||||
assert(policy.maxAge == 86400UL);
|
||||
assert(policy.mxCount == 2U);
|
||||
assert(SMTPMtaStsMxMatches(&policy, "mail.example.test"));
|
||||
assert(SMTPMtaStsMxMatches(&policy, "a.mx.example.test."));
|
||||
assert(!SMTPMtaStsMxMatches(&policy, "mx.example.test"));
|
||||
assert(!SMTPMtaStsMxMatches(&policy, "a.b.mx.example.test"));
|
||||
assert(!SMTPMtaStsMxMatches(&policy, "evil-example.test"));
|
||||
|
||||
assert(SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: none\nmax_age: 0\n", &policy));
|
||||
assert(policy.mode == SMTP_MTA_STS_INACTIVE);
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"mode: enforce\nversion: STSv1\nmx: mx.example\nmax_age: 1\n",
|
||||
&policy));
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmax_age: 1\n", &policy));
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmx: bad..example\nmax_age: 1\n",
|
||||
&policy));
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmx: -bad.example\nmax_age: 1\n",
|
||||
&policy));
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmx: mx.example\n"
|
||||
"max_age: 31557601\n", &policy));
|
||||
assert(!SMTPMtaStsParsePolicy(
|
||||
"version: STSv1\nmode: enforce\nmx: mx.example\nmax_age: +1\n",
|
||||
&policy));
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
TestRecord();
|
||||
TestPolicy();
|
||||
return 0;
|
||||
}
|
||||
@@ -26,6 +26,8 @@
|
||||
"outbound_tls_verify": false,
|
||||
"outbound_tls_required": false,
|
||||
"outbound_dane_enabled": true,
|
||||
"outbound_mta_sts_enabled": true,
|
||||
"outbound_mta_sts_cache_directory": "/var/lib/bongo/cache/mta-sts",
|
||||
"smtp_utf8_enabled": true,
|
||||
"requiretls_enabled": true,
|
||||
"allow_auth": true,
|
||||
|
||||
@@ -165,6 +165,18 @@ class ConfigurationModelTest(unittest.TestCase):
|
||||
messages = [str(issue) for issue in validate_all(configs, defaults, False)]
|
||||
self.assertTrue(any("proxy_protocol_networks" in message for message in messages))
|
||||
|
||||
def test_mta_sts_cache_requires_absolute_path(self):
|
||||
defaults = templates()
|
||||
configs = apply_scenario(defaults, Scenario(
|
||||
hostname="mail.example.test", bind_address="192.0.2.10",
|
||||
domains=["example.test"], admin_password="0123456789ab",
|
||||
web_mode="direct", web_public_url="http://mail.example.test:8080",
|
||||
))
|
||||
configs["smtp"]["outbound_mta_sts_cache_directory"] = "relative/cache"
|
||||
messages = [str(issue) for issue in validate_all(configs, defaults, False)]
|
||||
self.assertTrue(any("outbound_mta_sts_cache_directory" in message
|
||||
for message in messages))
|
||||
|
||||
def test_direct_https_binds_443_and_redirects_80(self):
|
||||
defaults = templates()
|
||||
configs = apply_scenario(defaults, Scenario(
|
||||
|
||||
@@ -593,6 +593,11 @@ def _validate_smtp(config: Mapping[str, Any], issues: list[Issue], check_files:
|
||||
_issue(issues, "smtp", "srs_secret_file", "expected an absolute path")
|
||||
elif check_files and not os.path.isfile(secret):
|
||||
_issue(issues, "smtp", "srs_secret_file", "secret file does not exist", "warning")
|
||||
if config.get("outbound_mta_sts_enabled"):
|
||||
cache_directory = config.get("outbound_mta_sts_cache_directory")
|
||||
if not isinstance(cache_directory, str) or not os.path.isabs(cache_directory):
|
||||
_issue(issues, "smtp", "outbound_mta_sts_cache_directory",
|
||||
"expected an absolute path")
|
||||
|
||||
|
||||
def _validate_mail_listener(name: str, config: Mapping[str, Any], issues: list[Issue]) -> None:
|
||||
|
||||
@@ -34,4 +34,8 @@ if(BUILD_TESTING)
|
||||
add_executable(xpl-dns-mx-test tests/dns-mx-test.c)
|
||||
target_link_libraries(xpl-dns-mx-test PRIVATE bongoxpl)
|
||||
add_test(NAME xpl-dns-mx COMMAND xpl-dns-mx-test)
|
||||
|
||||
add_executable(xpl-dns-txt-test tests/dns-txt-test.c)
|
||||
target_link_libraries(xpl-dns-txt-test PRIVATE bongoxpl)
|
||||
add_test(NAME xpl-dns-txt COMMAND xpl-dns-txt-test)
|
||||
endif()
|
||||
|
||||
+57
-3
@@ -420,6 +420,9 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
int x;
|
||||
int res;
|
||||
|
||||
if (answer_buffer == NULL || result == NULL ||
|
||||
answer_len < (int)sizeof(XplDnsResponseHeader)) return;
|
||||
|
||||
// try to parse the answer
|
||||
header = (XplDnsResponseHeader *)answer_buffer;
|
||||
result->dnssec_authenticated = ((HEADER *)answer_buffer)->ad != 0;
|
||||
@@ -436,6 +439,7 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
if (size < 0) return;
|
||||
|
||||
// Skip QCODE / QCLASS
|
||||
if ((size_t)(response_end - response) < (size_t)size + 4U) return;
|
||||
response += size + 4;
|
||||
|
||||
// does the buffer extent look right?
|
||||
@@ -448,25 +452,39 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
char answer_domain[XPLDNS_NAMELEN + 1];
|
||||
|
||||
item = MemMalloc(sizeof(XplDns_RecordList));
|
||||
if (item == NULL) return;
|
||||
item->next = NULL;
|
||||
|
||||
answer_domain[0] = '\0';
|
||||
res = _XplDns_ParseName(answer_buffer, answer_len,
|
||||
response, answer_domain, XPLDNS_NAMELEN);
|
||||
if (res < 0) return; // couldn't parse name...
|
||||
if (res < 0) {
|
||||
MemFree(item);
|
||||
return; // couldn't parse name...
|
||||
}
|
||||
|
||||
response += res;
|
||||
if (response >= response_end) return;
|
||||
if ((size_t)(response_end - response) < sizeof(XplDnsAnswer)) {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
|
||||
dns_answer = (XplDnsAnswer *)response;
|
||||
|
||||
response += sizeof(XplDnsAnswer);
|
||||
if (response >= response_end) return;
|
||||
if ((size_t)(response_end - response) < ntohs(dns_answer->size)) {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO : check ntohs(ans->class) is right
|
||||
|
||||
switch(ntohs(dns_answer->rtype)) {
|
||||
case XPLDNS_RR_A: {
|
||||
if (ntohs(dns_answer->size) != sizeof(XplDnsAnswerARecord)) {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
item->type = XPLDNS_RR_A;
|
||||
XplDnsAnswerARecord *rec = (XplDnsAnswerARecord *)response;
|
||||
_XplDns_CopyName(item->record.A.name, answer_domain);
|
||||
@@ -474,6 +492,30 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
_XplDnsResult_AppendRecord(result, item);
|
||||
}
|
||||
break;
|
||||
case XPLDNS_RR_TXT: {
|
||||
unsigned int offset = 0U;
|
||||
unsigned int output = 0U;
|
||||
unsigned int record_size = ntohs(dns_answer->size);
|
||||
|
||||
item->type = XPLDNS_RR_TXT;
|
||||
item->record.TXT.txt[0] = '\0';
|
||||
while (offset < record_size) {
|
||||
unsigned int chunk = response[offset++];
|
||||
|
||||
if (chunk > record_size - offset ||
|
||||
chunk > XPLDNS_NAMELEN - output) {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
memcpy(item->record.TXT.txt + output,
|
||||
response + offset, chunk);
|
||||
output += chunk;
|
||||
offset += chunk;
|
||||
}
|
||||
item->record.TXT.txt[output] = '\0';
|
||||
_XplDnsResult_AppendRecord(result, item);
|
||||
}
|
||||
break;
|
||||
case XPLDNS_RR_CNAME: {
|
||||
item->type = XPLDNS_RR_CNAME;
|
||||
|
||||
@@ -489,6 +531,11 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
XplDnsAnswerMxRecord *rec = (XplDnsAnswerMxRecord *)response;
|
||||
int r;
|
||||
|
||||
if (ntohs(dns_answer->size) < sizeof(XplDnsAnswerMxRecord)) {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
|
||||
_XplDns_CopyName(item->record.MX.name, answer_domain);
|
||||
item->type = XPLDNS_RR_MX;
|
||||
item->record.MX.preference = ntohs(rec->pref);
|
||||
@@ -502,6 +549,7 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
if (r >= 0) {
|
||||
_XplDnsResult_AppendRecord(result, item);
|
||||
} else {
|
||||
MemFree(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -520,3 +568,9 @@ _XplDns_ParseQuery(const unsigned char *answer_buffer, int answer_len, XplDns_Re
|
||||
result->status = XPLDNS_SUCCESS;
|
||||
return;
|
||||
}
|
||||
|
||||
XplDns_Result *
|
||||
XplDnsLookupTXT(const char *domain)
|
||||
{
|
||||
return _XplDns_Query(domain, XPLDNS_RR_TXT);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of version 2 of the GNU General Public License
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact Novell, Inc.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <xpl.h>
|
||||
|
||||
#include "../dns.h"
|
||||
|
||||
static const unsigned char TxtResponse[] = {
|
||||
0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
||||
0x04, 't', 'e', 's', 't', 0x00,
|
||||
0x00, 0x10, 0x00, 0x01,
|
||||
0xc0, 0x0c, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x3c, 0x00, 0x15,
|
||||
0x08, 'v', '=', 'S', 'T', 'S', 'v', '1', ';',
|
||||
0x0b, ' ', 'i', 'd', '=', '2', '0', '2', '6', '0', '7', '2'
|
||||
};
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
XplDns_Result result;
|
||||
XplDns_Result truncated;
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
result.status = XPLDNS_FAIL;
|
||||
_XplDns_ParseQuery(TxtResponse, sizeof(TxtResponse), &result);
|
||||
assert(result.status == XPLDNS_SUCCESS);
|
||||
assert(result.list != NULL);
|
||||
assert(result.list->type == XPLDNS_RR_TXT);
|
||||
assert(strcmp(result.list->record.TXT.txt,
|
||||
"v=STSv1; id=2026072") == 0);
|
||||
assert(result.list->next == NULL);
|
||||
MemFree(result.list);
|
||||
|
||||
memset(&truncated, 0, sizeof(truncated));
|
||||
truncated.status = XPLDNS_FAIL;
|
||||
_XplDns_ParseQuery(TxtResponse, sizeof(TxtResponse) - 1U, &truncated);
|
||||
assert(truncated.status == XPLDNS_FAIL);
|
||||
assert(truncated.list == NULL);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user