From 8f9abb6ed739774edfed231c47641d74900f3d4f Mon Sep 17 00:00:00 2001
From: MIZUTA Takeshi <mizuta.takeshi@fujitsu.com>
Date: Fri, 3 Jul 2020 19:51:13 +0900
Subject: [PATCH] MEDIUM: Support TCP keepalive parameters customization

It is now possible to customize TCP keepalive parameters.
These correspond to the socket options TCP_KEEPCNT, TCP_KEEPIDLE, TCP_KEEPINTVL
and are valid for the defaults, listen, frontend and backend sections.

This patch fixes GitHub issue #670.
---
 include/haproxy/proxy-t.h |   6 ++
 src/cfgparse-listen.c     |  98 +++++++++++++++++++++++
 src/proto_tcp.c           |  12 ++-
 src/proxy.c               | 159 ++++++++++++++++++++++++++++++++++++++
 src/session.c             |  12 ++-
 5 files changed, 285 insertions(+), 2 deletions(-)

diff --git a/include/haproxy/proxy-t.h b/include/haproxy/proxy-t.h
index 20cc3c9e1..c7e499930 100644
--- a/include/haproxy/proxy-t.h
+++ b/include/haproxy/proxy-t.h
@@ -306,6 +306,12 @@ struct proxy {
 	int  capture_len;			/* length of the string to be captured */
 	struct uri_auth *uri_auth;		/* if non-NULL, the (list of) per-URI authentications */
 	int max_ka_queue;			/* 1+maximum requests in queue accepted for reusing a K-A conn (0=none) */
+	int clitcpka_cnt;                       /* The maximum number of keepalive probes TCP should send before dropping the connection. (client side) */
+	int clitcpka_idle;                      /* The time (in seconds) the connection needs to remain idle before TCP starts sending keepalive probes. (client side) */
+	int clitcpka_intvl;                     /* The time (in seconds) between individual keepalive probes. (client side) */
+	int srvtcpka_cnt;                       /* The maximum number of keepalive probes TCP should send before dropping the connection. (server side) */
+	int srvtcpka_idle;                      /* The time (in seconds) the connection needs to remain idle before TCP starts sending keepalive probes. (server side) */
+	int srvtcpka_intvl;                     /* The time (in seconds) between individual keepalive probes. (server side) */
 	int monitor_uri_len;			/* length of the string above. 0 if unused */
 	char *monitor_uri;			/* a special URI to which we respond with HTTP/200 OK */
 	struct list mon_fail_cond;              /* list of conditions to fail monitoring requests (chained) */
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index 34cf346ab..124fb8fad 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -277,6 +277,10 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
 
 			curproxy->to_log = defproxy.to_log & ~LW_COOKIE & ~LW_REQHDR & ~ LW_RSPHDR;
 			curproxy->max_out_conns = defproxy.max_out_conns;
+
+			curproxy->clitcpka_cnt   = defproxy.clitcpka_cnt;
+			curproxy->clitcpka_idle  = defproxy.clitcpka_idle;
+			curproxy->clitcpka_intvl = defproxy.clitcpka_intvl;
 		}
 
 		if (curproxy->cap & PR_CAP_BE) {
@@ -337,6 +341,10 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
 			curproxy->conn_src.tproxy_addr = defproxy.conn_src.tproxy_addr;
 #endif
 			curproxy->load_server_state_from_file = defproxy.load_server_state_from_file;
+
+			curproxy->srvtcpka_cnt   = defproxy.srvtcpka_cnt;
+			curproxy->srvtcpka_idle  = defproxy.srvtcpka_idle;
+			curproxy->srvtcpka_intvl = defproxy.srvtcpka_intvl;
 		}
 
 		if (curproxy->cap & PR_CAP_FE) {
@@ -3018,6 +3026,96 @@ stats_error_parsing:
 		err_code |= ERR_ALERT | ERR_FATAL;
 		goto out;
 	}
+	else if (!strcmp(args[0], "tcpka-cnt")) {
+		if (warnifnotcap(curproxy, PR_CAP_BE | PR_CAP_FE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (*(args[1]) == 0) {
+			ha_alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		if (curproxy->cap & PR_CAP_FE)
+			curproxy->clitcpka_cnt = atol(args[1]);
+		if (curproxy->cap & PR_CAP_BE)
+			curproxy->srvtcpka_cnt = atol(args[1]);
+		if (alertif_too_many_args(1, file, linenum, args, &err_code))
+			goto out;
+	}
+	else if (!strcmp(args[0], "tcpka-idle")) {
+		unsigned int tcpka_idle;
+		const char *res;
+		if (warnifnotcap(curproxy, PR_CAP_BE | PR_CAP_FE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (*(args[1]) == 0) {
+			ha_alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		res = parse_time_err(args[1], &tcpka_idle, TIME_UNIT_S);
+		if (res == PARSE_TIME_OVER) {
+			ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to <%s>, maximum value is 2147483647 s (~68 years).\n",
+				 file, linenum, args[1], args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		else if (res == PARSE_TIME_UNDER) {
+			ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to <%s>, minimum non-null value is 1 s.\n",
+				 file, linenum, args[1], args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		else if (res) {
+			ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to <%s>.\n",
+				 file, linenum, *res, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		if (curproxy->cap & PR_CAP_FE)
+			curproxy->clitcpka_idle = tcpka_idle;
+		if (curproxy->cap & PR_CAP_BE)
+			curproxy->srvtcpka_idle = tcpka_idle;
+		if (alertif_too_many_args(1, file, linenum, args, &err_code))
+			goto out;
+	}
+	else if (!strcmp(args[0], "tcpka-intvl")) {
+		unsigned int tcpka_intvl;
+		const char *res;
+		if (warnifnotcap(curproxy, PR_CAP_BE | PR_CAP_FE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (*(args[1]) == 0) {
+			ha_alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		res = parse_time_err(args[1], &tcpka_intvl, TIME_UNIT_S);
+		if (res == PARSE_TIME_OVER) {
+			ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to <%s>, maximum value is 2147483647 s (~68 years).\n",
+				 file, linenum, args[1], args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		else if (res == PARSE_TIME_UNDER) {
+			ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to <%s>, minimum non-null value is 1 s.\n",
+				 file, linenum, args[1], args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		else if (res) {
+			ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to <%s>.\n",
+				 file, linenum, *res, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		if (curproxy->cap & PR_CAP_FE)
+			curproxy->clitcpka_intvl = tcpka_intvl;
+		if (curproxy->cap & PR_CAP_BE)
+			curproxy->srvtcpka_intvl = tcpka_intvl;
+		if (alertif_too_many_args(1, file, linenum, args, &err_code))
+			goto out;
+	}
 	else if (!strcmp(args[0], "cliexp") || !strcmp(args[0], "reqrep")) {  /* replace request header from a regex */
 		ha_alert("parsing [%s:%d] : The '%s' directive is not supported anymore since HAProxy 2.1. "
 			 "Use 'http-request replace-path', 'http-request replace-uri' or 'http-request replace-header' instead.\n",
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index d1e89e407..be867402c 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -379,9 +379,19 @@ int tcp_connect_server(struct connection *conn, int flags)
 		return SF_ERR_INTERNAL;
 	}
 
-	if (be->options & PR_O_TCP_SRV_KA)
+	if (be->options & PR_O_TCP_SRV_KA) {
 		setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one));
 
+		if (be->srvtcpka_cnt)
+			setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &be->srvtcpka_cnt, sizeof(be->srvtcpka_cnt));
+
+		if (be->srvtcpka_idle)
+			setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &be->srvtcpka_idle, sizeof(be->srvtcpka_idle));
+
+		if (be->srvtcpka_intvl)
+			setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &be->srvtcpka_intvl, sizeof(be->srvtcpka_intvl));
+	}
+
 	/* allow specific binding :
 	 * - server-specific at first
 	 * - proxy-specific next
diff --git a/src/proxy.c b/src/proxy.c
index a47509542..862e9041d 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -570,6 +570,159 @@ proxy_parse_retry_on(char **args, int section, struct proxy *curpx,
 	return 0;
 }
 
+/* This function parses "{srv|cli}tcpka-cnt" statements */
+static int proxy_parse_tcpka_cnt(char **args, int section, struct proxy *proxy,
+                                    struct proxy *defpx, const char *file, int line,
+                                    char **err)
+{
+	int retval;
+	char *res;
+	unsigned int tcpka_cnt;
+
+	retval = 0;
+
+	if (*args[1] == 0) {
+		memprintf(err, "'%s' expects an integer value", args[0]);
+		return -1;
+	}
+
+	tcpka_cnt = strtol(args[1], &res, 0);
+	if (*res) {
+		memprintf(err, "'%s' : unexpected character '%c' in integer value '%s'", args[0], *res, args[1]);
+		return -1;
+	}
+
+	if (!strcmp(args[0], "clitcpka-cnt")) {
+		if (!(proxy->cap & PR_CAP_FE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no frontend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->clitcpka_cnt = tcpka_cnt;
+	} else if (!strcmp(args[0], "srvtcpka-cnt")) {
+		if (!(proxy->cap & PR_CAP_BE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no backend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->srvtcpka_cnt = tcpka_cnt;
+	} else {
+		/* unreachable */
+		memprintf(err, "'%s': unknown keyword", args[0]);
+		return -1;
+	}
+
+	return retval;
+}
+
+/* This function parses "{srv|cli}tcpka-idle" statements */
+static int proxy_parse_tcpka_idle(char **args, int section, struct proxy *proxy,
+                                  struct proxy *defpx, const char *file, int line,
+                                  char **err)
+{
+	int retval;
+	const char *res;
+	unsigned int tcpka_idle;
+
+	retval = 0;
+
+	if (*args[1] == 0) {
+		memprintf(err, "'%s' expects an integer value", args[0]);
+		return -1;
+	}
+	res = parse_time_err(args[1], &tcpka_idle, TIME_UNIT_S);
+	if (res == PARSE_TIME_OVER) {
+		memprintf(err, "timer overflow in argument '%s' to '%s' (maximum value is 2147483647 ms or ~24.8 days)",
+			  args[1], args[0]);
+		return -1;
+	}
+	else if (res == PARSE_TIME_UNDER) {
+		memprintf(err, "timer underflow in argument '%s' to '%s' (minimum non-null value is 1 ms)",
+			  args[1], args[0]);
+		return -1;
+	}
+	else if (res) {
+		memprintf(err, "unexpected character '%c' in argument to <%s>.\n", *res, args[0]);
+		return -1;
+	}
+
+	if (!strcmp(args[0], "clitcpka-idle")) {
+		if (!(proxy->cap & PR_CAP_FE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no frontend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->clitcpka_idle = tcpka_idle;
+	} else if (!strcmp(args[0], "srvtcpka-idle")) {
+		if (!(proxy->cap & PR_CAP_BE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no backend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->srvtcpka_idle = tcpka_idle;
+	} else {
+		/* unreachable */
+		memprintf(err, "'%s': unknown keyword", args[0]);
+		return -1;
+	}
+
+	return retval;
+}
+
+/* This function parses "{srv|cli}tcpka-intvl" statements */
+static int proxy_parse_tcpka_intvl(char **args, int section, struct proxy *proxy,
+		                   struct proxy *defpx, const char *file, int line,
+                                   char **err)
+{
+	int retval;
+	const char *res;
+	unsigned int tcpka_intvl;
+
+	retval = 0;
+
+	if (*args[1] == 0) {
+		memprintf(err, "'%s' expects an integer value", args[0]);
+		return -1;
+	}
+	res = parse_time_err(args[1], &tcpka_intvl, TIME_UNIT_S);
+	if (res == PARSE_TIME_OVER) {
+		memprintf(err, "timer overflow in argument '%s' to '%s' (maximum value is 2147483647 ms or ~24.8 days)",
+			  args[1], args[0]);
+		return -1;
+	}
+	else if (res == PARSE_TIME_UNDER) {
+		memprintf(err, "timer underflow in argument '%s' to '%s' (minimum non-null value is 1 ms)",
+			  args[1], args[0]);
+		return -1;
+	}
+	else if (res) {
+		memprintf(err, "unexpected character '%c' in argument to <%s>.\n", *res, args[0]);
+		return -1;
+	}
+
+	if (!strcmp(args[0], "clitcpka-intvl")) {
+		if (!(proxy->cap & PR_CAP_FE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no frontend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->clitcpka_intvl = tcpka_intvl;
+	} else if (!strcmp(args[0], "srvtcpka-intvl")) {
+		if (!(proxy->cap & PR_CAP_BE)) {
+			memprintf(err, "%s will be ignored because %s '%s' has no backend capability",
+			          args[0], proxy_type_str(proxy), proxy->id);
+			retval = 1;
+		}
+		proxy->srvtcpka_intvl = tcpka_intvl;
+	} else {
+		/* unreachable */
+		memprintf(err, "'%s': unknown keyword", args[0]);
+		return -1;
+	}
+
+	return retval;
+}
+
 /* This function inserts proxy <px> into the tree of known proxies. The proxy's
  * name is used as the storing key so it must already have been initialized.
  */
@@ -1675,6 +1828,12 @@ static struct cfg_kw_list cfg_kws = {ILH, {
 	{ CFG_LISTEN, "max-keep-alive-queue", proxy_parse_max_ka_queue },
 	{ CFG_LISTEN, "declare", proxy_parse_declare },
 	{ CFG_LISTEN, "retry-on", proxy_parse_retry_on },
+	{ CFG_LISTEN, "clitcpka-cnt", proxy_parse_tcpka_cnt },
+	{ CFG_LISTEN, "clitcpka-idle", proxy_parse_tcpka_idle },
+	{ CFG_LISTEN, "clitcpka-intvl", proxy_parse_tcpka_intvl },
+	{ CFG_LISTEN, "srvtcpka-cnt", proxy_parse_tcpka_cnt },
+	{ CFG_LISTEN, "srvtcpka-idle", proxy_parse_tcpka_idle },
+	{ CFG_LISTEN, "srvtcpka-intvl", proxy_parse_tcpka_intvl },
 	{ 0, NULL, NULL },
 }};
 
diff --git a/src/session.c b/src/session.c
index 02b021201..51380d638 100644
--- a/src/session.c
+++ b/src/session.c
@@ -224,9 +224,19 @@ int session_accept_fd(struct listener *l, int cfd, struct sockaddr_storage *addr
 	if (l->addr.ss_family == AF_INET || l->addr.ss_family == AF_INET6) {
 		setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, (char *) &one, sizeof(one));
 
-		if (p->options & PR_O_TCP_CLI_KA)
+		if (p->options & PR_O_TCP_CLI_KA) {
 			setsockopt(cfd, SOL_SOCKET, SO_KEEPALIVE, (char *) &one, sizeof(one));
 
+			if (p->clitcpka_cnt)
+				setsockopt(cfd, IPPROTO_TCP, TCP_KEEPCNT, &p->clitcpka_cnt, sizeof(p->clitcpka_cnt));
+
+			if (p->clitcpka_idle)
+				setsockopt(cfd, IPPROTO_TCP, TCP_KEEPIDLE, &p->clitcpka_idle, sizeof(p->clitcpka_idle));
+
+			if (p->clitcpka_intvl)
+				setsockopt(cfd, IPPROTO_TCP, TCP_KEEPINTVL, &p->clitcpka_intvl, sizeof(p->clitcpka_intvl));
+		}
+
 		if (p->options & PR_O_TCP_NOLING)
 			fdtab[cfd].linger_risk = 1;
 
-- 
2.25.4

