This commit is contained in:
@@ -49,6 +49,11 @@ one-off files in `/tmp`:
|
||||
proves that slow manual `EHLO`, `NOOP`, and `RSET` input resets the timeout
|
||||
after each complete command. It restores the SMTP configuration and active
|
||||
service before exiting.
|
||||
- `smtp-stress-timeout-check.py` uses only two simultaneous connections to
|
||||
trigger the configurable load threshold. It proves that the first
|
||||
connection keeps the normal timeout while newly accepted inbound,
|
||||
trusted-device, and submission connections receive the shorter overload
|
||||
timeout, then restores the exact configuration and active service.
|
||||
- `sendmail-local-submission-check.py` runs as an unprivileged Unix account
|
||||
and verifies both `/usr/sbin/sendmail` and GNU `mail` through Bongo's
|
||||
restricted, rate-limited local SMTP submission path. It confirms that no
|
||||
|
||||
Executable
+281
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
# This program is free software, licensed under the terms of the GNU GPL.
|
||||
# See the Bongo COPYING file for full details.
|
||||
# Copyright (c) 2026 Bongo Project contributors
|
||||
|
||||
"""Verify load-dependent SMTP timeouts with two real connections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
ADMIN = os.environ.get("BONGO_TEST_ADMIN", "/usr/bin/bongo-admin")
|
||||
SYSTEMCTL = os.environ.get("BONGO_TEST_SYSTEMCTL", "/usr/bin/systemctl")
|
||||
INBOUND_HOST = os.environ.get("BONGO_TEST_INBOUND_HOST", "127.0.0.1")
|
||||
INBOUND_PORT = int(os.environ.get("BONGO_TEST_INBOUND_PORT", "25"))
|
||||
INTERNAL_HOST = os.environ.get(
|
||||
"BONGO_TEST_INTERNAL_HOST", "172.16.11.190")
|
||||
INTERNAL_PORT = int(os.environ.get("BONGO_TEST_INTERNAL_PORT", "26"))
|
||||
SUBMISSION_HOST = os.environ.get(
|
||||
"BONGO_TEST_SUBMISSION_HOST", "127.0.0.1")
|
||||
SUBMISSION_PORT = int(os.environ.get("BONGO_TEST_SUBMISSION_PORT", "587"))
|
||||
NORMAL_TIMEOUT = int(os.environ.get("BONGO_TEST_NORMAL_TIMEOUT", "5"))
|
||||
STRESS_TIMEOUT = int(os.environ.get("BONGO_TEST_STRESS_TIMEOUT", "2"))
|
||||
STARTUP_GRACE = float(os.environ.get("BONGO_TEST_STARTUP_GRACE", "2"))
|
||||
ALLOW_LIVE = os.environ.get("BONGO_ALLOW_LIVE_SMTP_TEST") == "1"
|
||||
|
||||
|
||||
class StressTimeoutError(RuntimeError):
|
||||
"""Raised when SMTP does not apply its load-dependent timeout."""
|
||||
|
||||
|
||||
def run(arguments: list[str], *, input_text: str | None = None) -> str:
|
||||
completed = subprocess.run(
|
||||
arguments,
|
||||
input=input_text,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode:
|
||||
raise StressTimeoutError(
|
||||
f"{' '.join(arguments)} failed: "
|
||||
f"{completed.stderr.strip() or completed.stdout.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def admin(*arguments: str, input_text: str | None = None) -> str:
|
||||
return run(
|
||||
["sudo", "-n", ADMIN, *arguments],
|
||||
input_text=input_text,
|
||||
)
|
||||
|
||||
|
||||
def read_configuration() -> dict:
|
||||
try:
|
||||
configuration = json.loads(admin("__config-read", "smtp"))
|
||||
except json.JSONDecodeError as error:
|
||||
raise StressTimeoutError(
|
||||
"bongo-admin returned invalid SMTP configuration") from error
|
||||
if not isinstance(configuration, dict):
|
||||
raise StressTimeoutError("SMTP configuration is not an object")
|
||||
return configuration
|
||||
|
||||
|
||||
def replace_configuration(configuration: dict) -> None:
|
||||
admin(
|
||||
"__config-replace",
|
||||
"smtp",
|
||||
input_text=json.dumps(configuration, separators=(",", ":")),
|
||||
)
|
||||
|
||||
|
||||
def read_greeting(connection: socket.socket, name: str) -> None:
|
||||
data = bytearray()
|
||||
while b"\n" not in data and len(data) <= 8192:
|
||||
chunk = connection.recv(4096)
|
||||
if not chunk:
|
||||
raise StressTimeoutError(
|
||||
f"{name} closed before the SMTP greeting")
|
||||
data.extend(chunk)
|
||||
if not bytes(data).startswith(b"220 "):
|
||||
raise StressTimeoutError(
|
||||
f"{name} returned an invalid SMTP greeting: {bytes(data)!r}")
|
||||
|
||||
|
||||
def open_connection(host: str, port: int, name: str) -> socket.socket:
|
||||
connection = socket.create_connection((host, port), timeout=10)
|
||||
connection.settimeout(10)
|
||||
try:
|
||||
read_greeting(connection, name)
|
||||
except BaseException:
|
||||
connection.close()
|
||||
raise
|
||||
return connection
|
||||
|
||||
|
||||
def wait_closed(
|
||||
connection: socket.socket,
|
||||
started: float,
|
||||
expected: int,
|
||||
name: str,
|
||||
) -> float:
|
||||
deadline = started + expected + 5
|
||||
received = bytearray()
|
||||
while time.monotonic() < deadline:
|
||||
connection.settimeout(max(0.1, deadline - time.monotonic()))
|
||||
try:
|
||||
chunk = connection.recv(4096)
|
||||
except socket.timeout:
|
||||
break
|
||||
if not chunk:
|
||||
elapsed = time.monotonic() - started
|
||||
if elapsed < max(0.5, expected * 0.65):
|
||||
raise StressTimeoutError(
|
||||
f"{name} closed too early after {elapsed:.2f}s")
|
||||
return elapsed
|
||||
received.extend(chunk)
|
||||
raise StressTimeoutError(
|
||||
f"{name} remained open beyond {expected + 5}s; "
|
||||
f"unexpected data={bytes(received)!r}")
|
||||
|
||||
|
||||
def assert_still_open(connection: socket.socket, name: str) -> None:
|
||||
readable, _, _ = select.select([connection], [], [], 0)
|
||||
if not readable:
|
||||
return
|
||||
data = connection.recv(4096)
|
||||
if not data:
|
||||
raise StressTimeoutError(
|
||||
f"{name} normal connection received the stress timeout")
|
||||
raise StressTimeoutError(
|
||||
f"{name} normal connection returned unexpected data: {data!r}")
|
||||
|
||||
|
||||
def wait_ready(host: str, port: int, name: str) -> None:
|
||||
deadline = time.monotonic() + 30
|
||||
while time.monotonic() < deadline:
|
||||
connection: socket.socket | None = None
|
||||
try:
|
||||
connection = open_connection(host, port, name)
|
||||
connection.sendall(b"QUIT\r\n")
|
||||
reply = connection.recv(4096)
|
||||
if not reply.startswith(b"221 "):
|
||||
raise OSError("SMTP did not accept readiness QUIT")
|
||||
return
|
||||
except (OSError, StressTimeoutError):
|
||||
time.sleep(0.1)
|
||||
finally:
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
raise StressTimeoutError(
|
||||
f"{name} did not become ready at {host}:{port}")
|
||||
|
||||
|
||||
def restart_bongo() -> None:
|
||||
run(["sudo", "-n", SYSTEMCTL, "restart", "bongo.service"])
|
||||
time.sleep(STARTUP_GRACE)
|
||||
for name, host, port in (
|
||||
("inbound", INBOUND_HOST, INBOUND_PORT),
|
||||
("internal", INTERNAL_HOST, INTERNAL_PORT),
|
||||
("submission", SUBMISSION_HOST, SUBMISSION_PORT),
|
||||
):
|
||||
wait_ready(host, port, name)
|
||||
if run(
|
||||
["sudo", "-n", SYSTEMCTL, "is-active", "bongo.service"]
|
||||
).strip() != "active":
|
||||
raise StressTimeoutError("bongo.service is not active")
|
||||
|
||||
|
||||
def check_endpoint(name: str, host: str, port: int) -> tuple[float, float]:
|
||||
normal: socket.socket | None = None
|
||||
stressed: socket.socket | None = None
|
||||
try:
|
||||
normal_started = time.monotonic()
|
||||
normal = open_connection(host, port, f"{name} normal")
|
||||
time.sleep(0.25)
|
||||
|
||||
stress_started = time.monotonic()
|
||||
stressed = open_connection(host, port, f"{name} stressed")
|
||||
stress_elapsed = wait_closed(
|
||||
stressed, stress_started, STRESS_TIMEOUT, f"{name} stressed")
|
||||
stressed.close()
|
||||
stressed = None
|
||||
|
||||
assert_still_open(normal, f"{name} normal")
|
||||
normal_elapsed = wait_closed(
|
||||
normal, normal_started, NORMAL_TIMEOUT, f"{name} normal")
|
||||
normal.close()
|
||||
normal = None
|
||||
|
||||
if stress_elapsed >= normal_elapsed:
|
||||
raise StressTimeoutError(
|
||||
f"{name} stress timeout was not shorter than normal")
|
||||
return normal_elapsed, stress_elapsed
|
||||
finally:
|
||||
if stressed is not None:
|
||||
stressed.close()
|
||||
if normal is not None:
|
||||
normal.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not ALLOW_LIVE:
|
||||
raise StressTimeoutError(
|
||||
"set BONGO_ALLOW_LIVE_SMTP_TEST=1 for the live stress test")
|
||||
if not 1 <= STRESS_TIMEOUT < NORMAL_TIMEOUT <= 15:
|
||||
raise StressTimeoutError(
|
||||
"require 1 <= stress timeout < normal timeout <= 15 seconds")
|
||||
|
||||
original = read_configuration()
|
||||
test_configuration = copy.deepcopy(original)
|
||||
test_configuration.update({
|
||||
"socket_timeout": NORMAL_TIMEOUT,
|
||||
"stress_socket_timeout": STRESS_TIMEOUT,
|
||||
"stress_load_percent": 100,
|
||||
"max_thread_load": 2,
|
||||
})
|
||||
results: list[str] = []
|
||||
test_error: BaseException | None = None
|
||||
restore_error: BaseException | None = None
|
||||
try:
|
||||
replace_configuration(test_configuration)
|
||||
restart_bongo()
|
||||
for name, host, port in (
|
||||
("inbound", INBOUND_HOST, INBOUND_PORT),
|
||||
("internal", INTERNAL_HOST, INTERNAL_PORT),
|
||||
("submission", SUBMISSION_HOST, SUBMISSION_PORT),
|
||||
):
|
||||
normal, stressed = check_endpoint(name, host, port)
|
||||
results.append(
|
||||
f"{name}=normal:{normal:.2f}s,stress:{stressed:.2f}s")
|
||||
except BaseException as error:
|
||||
test_error = error
|
||||
finally:
|
||||
try:
|
||||
replace_configuration(original)
|
||||
restart_bongo()
|
||||
if read_configuration() != original:
|
||||
raise StressTimeoutError(
|
||||
"SMTP configuration was not restored exactly")
|
||||
except BaseException as error:
|
||||
restore_error = error
|
||||
|
||||
if restore_error is not None:
|
||||
if test_error is not None:
|
||||
raise StressTimeoutError(
|
||||
f"test failed ({test_error}) and restoration also failed "
|
||||
f"({restore_error})")
|
||||
raise StressTimeoutError(
|
||||
f"failed to restore SMTP configuration: {restore_error}")
|
||||
if test_error is not None:
|
||||
raise test_error
|
||||
|
||||
print(
|
||||
"SMTP-33 PASS "
|
||||
f"maximum-connections=2 stress-threshold=100% "
|
||||
f"normal-timeout={NORMAL_TIMEOUT}s stress-timeout={STRESS_TIMEOUT}s "
|
||||
f"sessions={';'.join(results)} "
|
||||
"config-restored=yes service=active"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (
|
||||
OSError,
|
||||
StressTimeoutError,
|
||||
subprocess.SubprocessError,
|
||||
ValueError,
|
||||
) as error:
|
||||
print(f"SMTP-33 FAIL: {type(error).__name__}: {error}")
|
||||
raise SystemExit(1)
|
||||
@@ -132,6 +132,7 @@ inputs and outputs is absent from the ZIP.
|
||||
| SMTP-30 | curl, swaks, Xeams, GNU Mailutils, and paced Telnet delivery over authenticated submission and unauthenticated port 25/26 | [PASS](test-evidence/0.7-r1.md#smtp-30) | | |
|
||||
| SMTP-31 | Unprivileged sendmail-compatible local submission without the Bongo system credential | [PASS](test-evidence/0.7-r1.md#smtp-31) | | |
|
||||
| SMTP-32 | Telnet activity resets the configured per-operation timeout; abandoned port 25/26 sessions close | [PASS](test-evidence/0.7-r1.md#smtp-32) | | |
|
||||
| SMTP-33 | Load threshold gives newly accepted inbound, trusted-device, and submission sessions the short stress timeout | | | |
|
||||
| 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 | | | |
|
||||
|
||||
@@ -41,6 +41,14 @@ worker indefinitely. The default is 300 seconds, matching Postfix's normal
|
||||
`smtpd_timeout`; values from 1 through 3600 seconds are accepted. Bongo rejects
|
||||
zero rather than treating it as an unlimited connection.
|
||||
|
||||
When the projected number of active SMTP connection threads reaches
|
||||
`stress_load_percent` of `max_thread_load`, a newly accepted connection uses
|
||||
`stress_socket_timeout` instead. The defaults are 80 percent and 10 seconds,
|
||||
following Postfix's normal-versus-overload timeout model. Existing sessions
|
||||
retain the timeout selected when they were accepted, while new sessions
|
||||
return to the normal value automatically as load falls. The shorter policy
|
||||
also covers the PROXY header and TLS negotiation performed after accept.
|
||||
|
||||
## Trusted downstream proxy metadata
|
||||
|
||||
Bongo can preserve the original SMTP client's connection address when it
|
||||
|
||||
@@ -251,6 +251,7 @@ Connection *ConnAddressPoolConnect(AddressPool *pool, unsigned long timeOut);
|
||||
|
||||
BOOL ConnStartup(unsigned long TimeOut);
|
||||
BOOL ConnSetDefaultTimeout(unsigned long TimeOut);
|
||||
BOOL ConnSetTimeout(Connection *Conn, unsigned long TimeOut);
|
||||
void ConnShutdown(void);
|
||||
|
||||
void ConnSSLContextFree(bongo_ssl_context *Context);
|
||||
|
||||
@@ -5,6 +5,7 @@ add_executable(bongosmtp
|
||||
internal-relay.c
|
||||
capabilities.c
|
||||
auth-results.c
|
||||
load-policy.c
|
||||
protocol.c
|
||||
proxy.c
|
||||
)
|
||||
@@ -76,6 +77,15 @@ if(BUILD_TESTING)
|
||||
)
|
||||
add_test(NAME smtp-protocol COMMAND smtp-protocol-test)
|
||||
|
||||
add_executable(smtp-load-policy-test
|
||||
tests/load-policy-test.c
|
||||
load-policy.c
|
||||
)
|
||||
target_include_directories(smtp-load-policy-test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
add_test(NAME smtp-load-policy COMMAND smtp-load-policy-test)
|
||||
|
||||
add_executable(smtp-internal-relay-test
|
||||
tests/internal-relay-test.c
|
||||
internal-relay.c
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/****************************************************************************
|
||||
* <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.
|
||||
*
|
||||
* To contact Novell about this file by physical or electronic mail,
|
||||
* you may find current contact information at www.novell.com.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include "load-policy.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
unsigned long
|
||||
SMTPConnectionTimeoutForLoad(
|
||||
unsigned int activeConnections,
|
||||
unsigned int maximumConnections,
|
||||
unsigned int stressLoadPercent,
|
||||
unsigned long normalTimeout,
|
||||
unsigned long stressTimeout)
|
||||
{
|
||||
uint64_t projected;
|
||||
uint64_t threshold;
|
||||
|
||||
if (maximumConnections == 0U || stressLoadPercent == 0U ||
|
||||
stressLoadPercent > 100U) {
|
||||
return normalTimeout;
|
||||
}
|
||||
|
||||
projected = (uint64_t)activeConnections + 1U;
|
||||
threshold = ((uint64_t)maximumConnections * stressLoadPercent + 99U) /
|
||||
100U;
|
||||
return projected >= threshold ? stressTimeout : normalTimeout;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/****************************************************************************
|
||||
* <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.
|
||||
*
|
||||
* To contact Novell about this file by physical or electronic mail,
|
||||
* you may find current contact information at www.novell.com.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef BONGO_SMTP_LOAD_POLICY_H
|
||||
#define BONGO_SMTP_LOAD_POLICY_H
|
||||
|
||||
unsigned long SMTPConnectionTimeoutForLoad(
|
||||
unsigned int activeConnections,
|
||||
unsigned int maximumConnections,
|
||||
unsigned int stressLoadPercent,
|
||||
unsigned long normalTimeout,
|
||||
unsigned long stressTimeout);
|
||||
|
||||
#endif
|
||||
+43
-1
@@ -54,6 +54,7 @@
|
||||
#include "auth-results.h"
|
||||
#include "external_accounts.h"
|
||||
#include "capabilities.h"
|
||||
#include "load-policy.h"
|
||||
#include "protocol.h"
|
||||
|
||||
struct {
|
||||
@@ -99,6 +100,8 @@ struct {
|
||||
int max_null_sender;
|
||||
int max_mx_servers;
|
||||
int socket_timeout;
|
||||
int stress_socket_timeout;
|
||||
int stress_load_percent;
|
||||
BOOL spf_verify;
|
||||
BOOL dkim_verify;
|
||||
BOOL dmarc_verify;
|
||||
@@ -107,7 +110,11 @@ struct {
|
||||
char *srs_domain;
|
||||
char *srs_secret_file;
|
||||
GArray *lmtp_transports;
|
||||
} SMTP;
|
||||
} SMTP = {
|
||||
.socket_timeout = 300,
|
||||
.stress_socket_timeout = 10,
|
||||
.stress_load_percent = 80
|
||||
};
|
||||
|
||||
static BongoConfigItem LMTPTransportList = {
|
||||
BONGO_JSON_STRING, NULL, &SMTP.lmtp_transports
|
||||
@@ -166,6 +173,8 @@ static BongoConfigItem SMTPConfig[] = {
|
||||
BONGO_CONFIG_INT("o:max_flood_count/i", &SMTP.max_flood_count), // UNUSED ?
|
||||
BONGO_CONFIG_INT("o:max_null_sender/i", &SMTP.max_null_sender),
|
||||
BONGO_CONFIG_INT("o:socket_timeout/i", &SMTP.socket_timeout),
|
||||
BONGO_CONFIG_INT("o:stress_socket_timeout/i", &SMTP.stress_socket_timeout),
|
||||
BONGO_CONFIG_INT("o:stress_load_percent/i", &SMTP.stress_load_percent),
|
||||
BONGO_CONFIG_INT("o:max_mx_servers/i", &SMTP.max_mx_servers),
|
||||
{ BONGO_JSON_BOOL, "o:relay_local_mail/b", &SMTP.relay_local },
|
||||
{ BONGO_JSON_BOOL, "o:require_auth/b", &SMTP.require_auth },
|
||||
@@ -4948,6 +4957,21 @@ SMTPListenerReady(Connection *listener)
|
||||
return (descriptor.revents & POLLIN) ? 1 : 0;
|
||||
}
|
||||
|
||||
static void
|
||||
SMTPApplyConnectionTimeout(Connection *connection)
|
||||
{
|
||||
unsigned long timeout = SMTPConnectionTimeoutForLoad(
|
||||
(unsigned int)XplSafeRead(SMTPConnThreads),
|
||||
(unsigned int)SMTP.max_thread_load,
|
||||
(unsigned int)SMTP.stress_load_percent,
|
||||
(unsigned long)SMTP.socket_timeout,
|
||||
(unsigned long)SMTP.stress_socket_timeout);
|
||||
|
||||
if (!ConnSetTimeout(connection, timeout)) {
|
||||
Log(LOG_ERROR, "Could not apply SMTP connection timeout");
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
ServerSocketInit (void)
|
||||
{
|
||||
@@ -5063,6 +5087,7 @@ SMTPSubmissionServer(void *ignored)
|
||||
if (!SMTP_IS_EXITING() && errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
SMTPApplyConnectionTimeout(conn);
|
||||
if (!SMTPApplyProxyProtocol(conn, "SMTP submission")) {
|
||||
ConnClose(conn);
|
||||
ConnFree(conn);
|
||||
@@ -5150,6 +5175,7 @@ SMTPInternalServer(void *ignored)
|
||||
if (!SMTP_IS_EXITING() && errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
SMTPApplyConnectionTimeout(conn);
|
||||
if (!SMTPApplyProxyProtocol(conn, "internal SMTP")) {
|
||||
ConnClose(conn);
|
||||
ConnFree(conn);
|
||||
@@ -5246,6 +5272,7 @@ SMTPServer (void *ignored)
|
||||
break;
|
||||
}
|
||||
if (ConnAccept(SMTPServerConnection, &conn) != -1) {
|
||||
SMTPApplyConnectionTimeout(conn);
|
||||
if (!SMTPApplyProxyProtocol(conn, "SMTP")) {
|
||||
ConnClose(conn);
|
||||
ConnFree(conn);
|
||||
@@ -5427,6 +5454,7 @@ SMTPSSLServer (void *ignored)
|
||||
break;
|
||||
}
|
||||
if (ConnAccept(SMTPServerConnectionSSL, &conn) != -1) {
|
||||
SMTPApplyConnectionTimeout(conn);
|
||||
if (!SMTPApplyProxyProtocol(conn, "SMTPS")) {
|
||||
ConnClose(conn);
|
||||
ConnFree(conn);
|
||||
@@ -5634,6 +5662,20 @@ ValidateListenerConfiguration(void)
|
||||
Log(LOG_ERROR, "SMTP socket timeout must be between 1 and 3600 seconds");
|
||||
return FALSE;
|
||||
}
|
||||
if (SMTP.stress_socket_timeout < 1 ||
|
||||
SMTP.stress_socket_timeout > SMTP.socket_timeout) {
|
||||
Log(LOG_ERROR,
|
||||
"SMTP stress socket timeout must be between 1 and the normal timeout");
|
||||
return FALSE;
|
||||
}
|
||||
if (SMTP.stress_load_percent < 1 || SMTP.stress_load_percent > 100) {
|
||||
Log(LOG_ERROR, "SMTP stress load percentage must be between 1 and 100");
|
||||
return FALSE;
|
||||
}
|
||||
if (SMTP.max_thread_load < 1) {
|
||||
Log(LOG_ERROR, "SMTP maximum thread load must be greater than zero");
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/****************************************************************************
|
||||
* <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.
|
||||
*
|
||||
* To contact Novell about this file by physical or electronic mail,
|
||||
* you may find current contact information at www.novell.com.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include "load-policy.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
assert(SMTPConnectionTimeoutForLoad(0, 10000, 80, 300, 10) == 300);
|
||||
assert(SMTPConnectionTimeoutForLoad(7998, 10000, 80, 300, 10) == 300);
|
||||
assert(SMTPConnectionTimeoutForLoad(7999, 10000, 80, 300, 10) == 10);
|
||||
assert(SMTPConnectionTimeoutForLoad(9999, 10000, 80, 300, 10) == 10);
|
||||
|
||||
assert(SMTPConnectionTimeoutForLoad(0, 2, 100, 5, 2) == 5);
|
||||
assert(SMTPConnectionTimeoutForLoad(1, 2, 100, 5, 2) == 2);
|
||||
|
||||
assert(SMTPConnectionTimeoutForLoad(100, 0, 80, 300, 10) == 300);
|
||||
assert(SMTPConnectionTimeoutForLoad(100, 100, 0, 300, 10) == 300);
|
||||
assert(SMTPConnectionTimeoutForLoad(100, 100, 101, 300, 10) == 300);
|
||||
return 0;
|
||||
}
|
||||
@@ -47,6 +47,8 @@
|
||||
"max_flood_count": 1000,
|
||||
"max_null_sender": 10,
|
||||
"socket_timeout": 300,
|
||||
"stress_socket_timeout": 10,
|
||||
"stress_load_percent": 80,
|
||||
"max_mx_servers": 20,
|
||||
"require_auth": false,
|
||||
"spf_verify": true,
|
||||
|
||||
@@ -131,6 +131,34 @@ class ConfigurationModelTest(unittest.TestCase):
|
||||
for issue in errors(validate_all(
|
||||
configs, defaults, check_files=False))))
|
||||
|
||||
def test_smtp_stress_timeout_policy_is_bounded(self):
|
||||
defaults = templates()
|
||||
for key, value in (
|
||||
("stress_socket_timeout", 0),
|
||||
("stress_socket_timeout", 3601),
|
||||
("stress_load_percent", 0),
|
||||
("stress_load_percent", 101),
|
||||
("max_thread_load", 0),
|
||||
):
|
||||
with self.subTest(key=key, value=value):
|
||||
configs = templates()
|
||||
configs["smtp"][key] = value
|
||||
problems = errors(validate_all(
|
||||
configs, defaults, check_files=False))
|
||||
self.assertTrue(any(
|
||||
issue.document == "smtp" and issue.path == key
|
||||
for issue in problems))
|
||||
|
||||
configs = templates()
|
||||
configs["smtp"]["socket_timeout"] = 5
|
||||
configs["smtp"]["stress_socket_timeout"] = 6
|
||||
problems = errors(validate_all(
|
||||
configs, defaults, check_files=False))
|
||||
self.assertTrue(any(
|
||||
issue.document == "smtp" and
|
||||
issue.path == "stress_socket_timeout"
|
||||
for issue in problems))
|
||||
|
||||
def test_safe_reverse_proxy_scenario(self):
|
||||
defaults = templates()
|
||||
scenario = Scenario(
|
||||
|
||||
@@ -150,6 +150,19 @@ ConnSetDefaultTimeout(unsigned long TimeOut)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL
|
||||
ConnSetTimeout(Connection *Conn, unsigned long TimeOut)
|
||||
{
|
||||
if (Conn == NULL || TimeOut == 0 ||
|
||||
TimeOut > (unsigned long)(INT_MAX / 1000)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Conn->receive.timeOut = TimeOut;
|
||||
Conn->send.timeOut = TimeOut;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
ConnSSLContextFree(bongo_ssl_context *context)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,11 @@ main(void)
|
||||
assert(after->send.timeOut == 3);
|
||||
assert(before->receive.timeOut == 600);
|
||||
assert(before->send.timeOut == 600);
|
||||
assert(!ConnSetTimeout(NULL, 2));
|
||||
assert(!ConnSetTimeout(before, 0));
|
||||
assert(ConnSetTimeout(before, 2));
|
||||
assert(before->receive.timeOut == 2);
|
||||
assert(before->send.timeOut == 2);
|
||||
|
||||
ConnShutdown();
|
||||
return 0;
|
||||
|
||||
@@ -578,7 +578,17 @@ def _validate_smtp(config: Mapping[str, Any], issues: list[Issue], check_files:
|
||||
_issue(issues, "smtp", "proxy_protocol_networks",
|
||||
"enabled PROXY protocol requires trusted proxy networks")
|
||||
|
||||
_positive(issues, "smtp", config, "max_thread_load", 100000)
|
||||
_positive(issues, "smtp", config, "socket_timeout", 3600)
|
||||
_positive(issues, "smtp", config, "stress_socket_timeout", 3600)
|
||||
_positive(issues, "smtp", config, "stress_load_percent", 100)
|
||||
normal_timeout = config.get("socket_timeout")
|
||||
stress_timeout = config.get("stress_socket_timeout")
|
||||
if (isinstance(normal_timeout, int) and
|
||||
isinstance(stress_timeout, int) and
|
||||
stress_timeout > normal_timeout):
|
||||
_issue(issues, "smtp", "stress_socket_timeout",
|
||||
"stress timeout must not exceed the normal socket timeout")
|
||||
|
||||
for key in ("xclient_trusted_destinations", "xforward_trusted_destinations",
|
||||
"proxy_header_trusted_destinations"):
|
||||
|
||||
Reference in New Issue
Block a user