diff --git a/libcfnet/net.c b/libcfnet/net.c index 9de7663f64..ad6fb93b6e 100644 --- a/libcfnet/net.c +++ b/libcfnet/net.c @@ -32,7 +32,14 @@ #include #include #include +#include +/* Maximum seconds to spend consuming heartbeats before aborting. + * Slightly above the 120s wait gate timeout to allow for the + * legitimate use case. A count-based limit would be wrong here — + * a malicious peer could send 1 heartbeat every 29.9s (just under + * SO_RCVTIMEO) to hold the connection for hours. */ +#define MAX_HEARTBEAT_DURATION 150 /* TODO remove libpromises dependency. */ extern char BINDINTERFACE[CF_MAXVARSIZE]; /* cf3globals.c, cf3.extern.h */ @@ -131,11 +138,31 @@ int SendTransaction(ConnectionInfo *conn_info, } } +/** + * Send a heartbeat transaction to keep the connection alive during + * long-running server-side operations. + * + * No-op if the protocol version does not support heartbeats. + * Heartbeats are silently consumed by ReceiveTransaction() on the + * receiving end - callers never see them. + * + * @return 0 on success (or no-op), -1 on error + */ +int SendHeartbeat(ConnectionInfo *conn_info) +{ + assert(conn_info != NULL); + if (!ProtocolSupportsHeartbeat(conn_info->protocol)) + { + return 0; + } + return SendTransaction(conn_info, "HEARTBEAT", sizeof("HEARTBEAT"), CF_MORE); +} + /*************************************************************************/ /** - * Receive a transaction packet of at most CF_BUFSIZE-1 bytes, and - * NULL-terminate it. + * Receive a single transaction packet of at most CF_BUFSIZE-1 bytes, + * and NULL-terminate it. * * @param #buffer must be of size at least CF_BUFSIZE. * @@ -146,8 +173,10 @@ int SendTransaction(ConnectionInfo *conn_info, * @TODO shutdown() the connection in all cases were this function returns -1, * in order to protect against future garbage reads. */ -int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more) +static int ReceiveTransactionInner(ConnectionInfo *conn_info, char *buffer, int *more) { + assert(conn_info != NULL); + char proto[CF_INBAND_OFFSET + 1] = { 0 }; int ret; @@ -283,6 +312,52 @@ int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more) return ret; } +/** + * Receive a transaction packet, silently consuming any heartbeat + * transactions (ENT-13699). Callers never see heartbeats. + * + * @see ReceiveTransactionInner() for parameter and return value details. + */ +int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more) +{ + assert(conn_info != NULL); + + time_t heartbeat_start = 0; + + while (true) + { + int ret = ReceiveTransactionInner(conn_info, buffer, more); + if (ret == -1) + { + return -1; + } + + /* Silently consume heartbeat transactions so callers + * never need to handle them. */ + if (ProtocolSupportsHeartbeat(conn_info->protocol) + && ret == (int) sizeof("HEARTBEAT") + && memcmp(buffer, "HEARTBEAT", sizeof("HEARTBEAT")) == 0) + { + if (heartbeat_start == 0) + { + heartbeat_start = time(NULL); + } + if (time(NULL) - heartbeat_start > MAX_HEARTBEAT_DURATION) + { + Log(LOG_LEVEL_WARNING, + "ReceiveTransaction: heartbeats exceeded %ds, aborting", + MAX_HEARTBEAT_DURATION); + conn_info->status = CONNECTIONINFO_STATUS_BROKEN; + return -1; + } + Log(LOG_LEVEL_DEBUG, "ReceiveTransaction: heartbeat received"); + continue; + } + + return ret; + } +} + /* BWlimit global variables Throttling happens for all network interfaces, all traffic being sent for diff --git a/libcfnet/net.h b/libcfnet/net.h index 59eaf22764..334bb29896 100644 --- a/libcfnet/net.h +++ b/libcfnet/net.h @@ -36,6 +36,7 @@ extern uint32_t bwlimit_kbytes; int SendTransaction(ConnectionInfo *conn_info, const char *buffer, int len, char status); int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more); +int SendHeartbeat(ConnectionInfo *conn_info); int SetReceiveTimeout(int fd, unsigned long ms); diff --git a/libcfnet/protocol_version.c b/libcfnet/protocol_version.c index e99ef2c89d..c043e04463 100644 --- a/libcfnet/protocol_version.c +++ b/libcfnet/protocol_version.c @@ -37,6 +37,10 @@ ProtocolVersion ParseProtocolVersionPolicy(const char *const s) { return CF_PROTOCOL_FILESTREAM; } + else if (StringEqual(s, "5") || StringEqual(s, "heartbeat")) + { + return CF_PROTOCOL_HEARTBEAT; + } else if (StringEqual(s, "latest")) { return CF_PROTOCOL_LATEST; diff --git a/libcfnet/protocol_version.h b/libcfnet/protocol_version.h index 61e9cf3855..a63cc6777e 100644 --- a/libcfnet/protocol_version.h +++ b/libcfnet/protocol_version.h @@ -40,10 +40,11 @@ typedef enum CF_PROTOCOL_TLS = 2, CF_PROTOCOL_COOKIE = 3, CF_PROTOCOL_FILESTREAM = 4, + CF_PROTOCOL_HEARTBEAT = 5, } ProtocolVersion; /* We use CF_PROTOCOL_LATEST as the default for new connections. */ -#define CF_PROTOCOL_LATEST CF_PROTOCOL_FILESTREAM +#define CF_PROTOCOL_LATEST CF_PROTOCOL_HEARTBEAT static inline const char *ProtocolVersionString(const ProtocolVersion p) { @@ -57,6 +58,8 @@ static inline const char *ProtocolVersionString(const ProtocolVersion p) return "classic"; case CF_PROTOCOL_FILESTREAM: return "filestream"; + case CF_PROTOCOL_HEARTBEAT: + return "heartbeat"; default: return "undefined"; } @@ -92,6 +95,11 @@ static inline bool ProtocolSupportsFileStream(const ProtocolVersion p) return (p >= CF_PROTOCOL_FILESTREAM); } +static inline bool ProtocolSupportsHeartbeat(const ProtocolVersion p) +{ + return (p >= CF_PROTOCOL_HEARTBEAT); +} + static inline bool ProtocolTerminateCSV(const ProtocolVersion p) { return (p < CF_PROTOCOL_COOKIE);