Skip to content

Commit 533cf74

Browse files
committed
Added HEARTBEAT protocol version (v5) for keeping long-running connections alive
Added CF_PROTOCOL_HEARTBEAT (protocol version 5) which allows either side of a connection to send periodic heartbeat transactions to keep it alive during long-running operations. ReceiveTransaction() silently consumes heartbeats so callers never see them, with a time-based cap (150s) to prevent denial of service. Ticket: ENT-13699 Changelog: Title Signed-off-by: Lars Erik Wik <lars.erik.wik@northern.tech>
1 parent e56ed82 commit 533cf74

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

libcfnet/net.c

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@
3232
#include <misc_lib.h>
3333
#include <cf3.defs.h>
3434
#include <protocol.h>
35+
#include <protocol_version.h>
3536

37+
/* Maximum seconds to spend consuming heartbeats before aborting.
38+
* Slightly above the 120s wait gate timeout to allow for the
39+
* legitimate use case. A count-based limit would be wrong here —
40+
* a malicious peer could send 1 heartbeat every 29.9s (just under
41+
* SO_RCVTIMEO) to hold the connection for hours. */
42+
#define MAX_HEARTBEAT_DURATION 150
3643

3744
/* TODO remove libpromises dependency. */
3845
extern char BINDINTERFACE[CF_MAXVARSIZE]; /* cf3globals.c, cf3.extern.h */
@@ -131,11 +138,31 @@ int SendTransaction(ConnectionInfo *conn_info,
131138
}
132139
}
133140

141+
/**
142+
* Send a heartbeat transaction to keep the connection alive during
143+
* long-running server-side operations.
144+
*
145+
* No-op if the protocol version does not support heartbeats.
146+
* Heartbeats are silently consumed by ReceiveTransaction() on the
147+
* receiving end - callers never see them.
148+
*
149+
* @return 0 on success (or no-op), -1 on error
150+
*/
151+
int SendHeartbeat(ConnectionInfo *conn_info)
152+
{
153+
assert(conn_info != NULL);
154+
if (!ProtocolSupportsHeartbeat(conn_info->protocol))
155+
{
156+
return 0;
157+
}
158+
return SendTransaction(conn_info, "HEARTBEAT", sizeof("HEARTBEAT"), CF_MORE);
159+
}
160+
134161
/*************************************************************************/
135162

136163
/**
137-
* Receive a transaction packet of at most CF_BUFSIZE-1 bytes, and
138-
* NULL-terminate it.
164+
* Receive a single transaction packet of at most CF_BUFSIZE-1 bytes,
165+
* and NULL-terminate it.
139166
*
140167
* @param #buffer must be of size at least CF_BUFSIZE.
141168
*
@@ -146,8 +173,10 @@ int SendTransaction(ConnectionInfo *conn_info,
146173
* @TODO shutdown() the connection in all cases were this function returns -1,
147174
* in order to protect against future garbage reads.
148175
*/
149-
int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more)
176+
static int ReceiveTransactionInner(ConnectionInfo *conn_info, char *buffer, int *more)
150177
{
178+
assert(conn_info != NULL);
179+
151180
char proto[CF_INBAND_OFFSET + 1] = { 0 };
152181
int ret;
153182

@@ -283,6 +312,52 @@ int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more)
283312
return ret;
284313
}
285314

315+
/**
316+
* Receive a transaction packet, silently consuming any heartbeat
317+
* transactions (ENT-13699). Callers never see heartbeats.
318+
*
319+
* @see ReceiveTransactionInner() for parameter and return value details.
320+
*/
321+
int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more)
322+
{
323+
assert(conn_info != NULL);
324+
325+
time_t heartbeat_start = 0;
326+
327+
while (true)
328+
{
329+
int ret = ReceiveTransactionInner(conn_info, buffer, more);
330+
if (ret == -1)
331+
{
332+
return -1;
333+
}
334+
335+
/* Silently consume heartbeat transactions so callers
336+
* never need to handle them. */
337+
if (ProtocolSupportsHeartbeat(conn_info->protocol)
338+
&& ret == (int) sizeof("HEARTBEAT")
339+
&& memcmp(buffer, "HEARTBEAT", sizeof("HEARTBEAT")) == 0)
340+
{
341+
if (heartbeat_start == 0)
342+
{
343+
heartbeat_start = time(NULL);
344+
}
345+
if (time(NULL) - heartbeat_start > MAX_HEARTBEAT_DURATION)
346+
{
347+
Log(LOG_LEVEL_WARNING,
348+
"ReceiveTransaction: heartbeats exceeded %ds, aborting",
349+
MAX_HEARTBEAT_DURATION);
350+
conn_info->status = CONNECTIONINFO_STATUS_BROKEN;
351+
return -1;
352+
}
353+
Log(LOG_LEVEL_DEBUG, "ReceiveTransaction: heartbeat received");
354+
continue;
355+
}
356+
357+
return ret;
358+
}
359+
}
360+
286361
/* BWlimit global variables
287362
288363
Throttling happens for all network interfaces, all traffic being sent for

libcfnet/net.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extern uint32_t bwlimit_kbytes;
3636

3737
int SendTransaction(ConnectionInfo *conn_info, const char *buffer, int len, char status);
3838
int ReceiveTransaction(ConnectionInfo *conn_info, char *buffer, int *more);
39+
int SendHeartbeat(ConnectionInfo *conn_info);
3940

4041
int SetReceiveTimeout(int fd, unsigned long ms);
4142

libcfnet/protocol_version.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ ProtocolVersion ParseProtocolVersionPolicy(const char *const s)
3737
{
3838
return CF_PROTOCOL_FILESTREAM;
3939
}
40+
else if (StringEqual(s, "5") || StringEqual(s, "heartbeat"))
41+
{
42+
return CF_PROTOCOL_HEARTBEAT;
43+
}
4044
else if (StringEqual(s, "latest"))
4145
{
4246
return CF_PROTOCOL_LATEST;

libcfnet/protocol_version.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ typedef enum
4040
CF_PROTOCOL_TLS = 2,
4141
CF_PROTOCOL_COOKIE = 3,
4242
CF_PROTOCOL_FILESTREAM = 4,
43+
CF_PROTOCOL_HEARTBEAT = 5,
4344
} ProtocolVersion;
4445

4546
/* We use CF_PROTOCOL_LATEST as the default for new connections. */
46-
#define CF_PROTOCOL_LATEST CF_PROTOCOL_FILESTREAM
47+
#define CF_PROTOCOL_LATEST CF_PROTOCOL_HEARTBEAT
4748

4849
static inline const char *ProtocolVersionString(const ProtocolVersion p)
4950
{
@@ -57,6 +58,8 @@ static inline const char *ProtocolVersionString(const ProtocolVersion p)
5758
return "classic";
5859
case CF_PROTOCOL_FILESTREAM:
5960
return "filestream";
61+
case CF_PROTOCOL_HEARTBEAT:
62+
return "heartbeat";
6063
default:
6164
return "undefined";
6265
}
@@ -92,6 +95,11 @@ static inline bool ProtocolSupportsFileStream(const ProtocolVersion p)
9295
return (p >= CF_PROTOCOL_FILESTREAM);
9396
}
9497

98+
static inline bool ProtocolSupportsHeartbeat(const ProtocolVersion p)
99+
{
100+
return (p >= CF_PROTOCOL_HEARTBEAT);
101+
}
102+
95103
static inline bool ProtocolTerminateCSV(const ProtocolVersion p)
96104
{
97105
return (p < CF_PROTOCOL_COOKIE);

0 commit comments

Comments
 (0)