MEDIUM: tcp-act: add parameter rst-ttl to silent-drop

The silent-drop action was extended with an additional optional parameter,
[rst-ttl <ttl> ], causing HAProxy to send a TCP RST with the specified TTL
towards the client.

With this behaviour, the connection state on your own client-
facing middle-boxes (load balancers, firewalls) will be purged,
but the client will still assume the TCP connection is up because
the TCP RST packet expires before reaching the client.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 1eb87fc..2a951c3 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -6443,7 +6443,7 @@
     - set-var(<var-name>[,<cond> ...]) <expr>
     - set-var-fmt(<var-name>[,<cond> ...]) <fmt>
     - send-spoe-group <engine-name> <group-name>
-    - silent-drop
+    - silent-drop [ rst-ttl <ttl> ]
     - strict-mode { on | off }
     - tarpit [ { status | deny_status } <code>] ...
     - track-sc0 <key> [table <table>]
@@ -7398,24 +7398,27 @@
     http-request set-var(req.my_var) req.fhdr(user-agent),lower
     http-request set-var-fmt(txn.from) %[src]:%[src_port]
 
-http-request silent-drop [ { if | unless } <condition> ]
+http-request silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
-  This stops the evaluation of the rules and makes the client-facing connection
-  suddenly disappear using a system-dependent way that tries to prevent the
-  client from being notified. The effect it then that the client still sees an
-  established connection while there's none on HAProxy. The purpose is to
-  achieve a comparable effect to "tarpit" except that it doesn't use any local
-  resource at all on the machine running HAProxy. It can resist much higher
-  loads than "tarpit", and slow down stronger attackers. It is important to
-  understand the impact of using this mechanism. All stateful equipment placed
-  between the client and HAProxy (firewalls, proxies, load balancers) will also
-  keep the established connection for a long time and may suffer from this
-  action.
-  On modern Linux systems running with enough privileges, the TCP_REPAIR socket
-  option is used to block the emission of a TCP reset. On other systems, the
-  socket's TTL is reduced to 1 so that the TCP reset doesn't pass the first
-  router, though it's still delivered to local networks. Do not use it unless
-  you fully understand how it works.
+  This stops the evaluation of the rules and removes the client-facing
+  connection in a configurable way: When called without the rst-ttl argument,
+  we try to prevent sending any FIN or RST packet back to the client by
+  using TCP_REPAIR. If this fails (mainly because of missing privileges),
+  we fall back to sending a RST packet with a TTL of 1.
+
+  The effect is that the client still sees an established connection while
+  there is none on HAProxy, saving resources. However, stateful equipment
+  placed between the HAProxy and the client (firewalls, proxies,
+  load balancers) will also keep the established connection in their
+  session tables.
+
+  The optional rst-ttl changes this behaviour: TCP_REPAIR is not used,
+  and a RST packet with a configurable TTL is sent. When set to a
+  reasonable value, the RST packet travels through your own equipment,
+  deleting the connection in your middle-boxes, but does not arrive at
+  the client. Future packets from the client will then be dropped
+  already by your middle-boxes. These "local RST"s protect your resources,
+  but not the client's. Do not use it unless you fully understand how it works.
 
 http-request strict-mode { on | off } [ { if | unless } <condition> ]
 
@@ -7849,7 +7852,7 @@
   inline. Please refer to "http-request set-var" and "http-request set-var-fmt"
   for a complete description.
 
-http-response silent-drop [ { if | unless } <condition> ]
+http-response silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and makes the client-facing connection
   suddenly disappear using a system-dependent way that tries to prevent the
@@ -12650,7 +12653,7 @@
   scopes. Please refer to "http-request set-var" and "http-request set-var-fmt"
   for a complete description.
 
-tcp-request connection silent-drop [ { if | unless } <condition> ]
+tcp-request connection silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and makes the client-facing connection
   suddenly disappear using a system-dependent way that tries to prevent the
@@ -12972,7 +12975,7 @@
   inline. Please refer to "http-request set-var" and "http-request set-var-fmt"
   for a complete description.
 
-tcp-request content silent-drop [ { if | unless } <condition> ]
+tcp-request content silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and makes the client-facing connection
   suddenly disappear using a system-dependent way that tries to prevent the
@@ -13230,7 +13233,7 @@
   inline. Please refer to "http-request set-var" and "http-request set-var-fmt"
   for a complete description.
 
-tcp-request session silent-drop [ { if | unless } <condition> ]
+tcp-request session silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and makes the client-facing connection
   suddenly disappear using a system-dependent way that tries to prevent the
@@ -13400,7 +13403,7 @@
   inline. Please refer to "http-request set-var" and "http-request set-var-fmt"
   for a complete description.
 
-tcp-response content silent-drop [ { if | unless } <condition> ]
+tcp-response content silent-drop [ rst-ttl <ttl> ] [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and makes the client-facing connection
   suddenly disappear using a system-dependent way that tries to prevent the
diff --git a/src/tcp_act.c b/src/tcp_act.c
index f31c9c3..142e1ba 100644
--- a/src/tcp_act.c
+++ b/src/tcp_act.c
@@ -261,11 +261,25 @@
 	return ACT_RET_CONT;
 }
 
-/* Executes the "silent-drop" action. May be called from {tcp,http}{request,response} */
+/* Executes the "silent-drop" action. May be called from {tcp,http}{request,response}.
+ * If rule->arg.act.p[0] is 0, TCP_REPAIR is tried first, with a fallback to
+ * sending a RST with TTL 1 towards the client. If it is [1-255], we will skip
+ * TCP_REPAIR and prepare the socket to send a RST with the requested TTL when
+ * the connection is killed by channel_abort().
+ */
 static enum act_return tcp_exec_action_silent_drop(struct act_rule *rule, struct proxy *px,
                                                    struct session *sess, struct stream *strm, int flags)
 {
 	struct connection *conn = objt_conn(sess->origin);
+	unsigned int ttl __maybe_unused = (uintptr_t)rule->arg.act.p[0];
+	char tcp_repair_enabled __maybe_unused;
+
+	if (ttl == 0) {
+		tcp_repair_enabled = 1;
+		ttl = 1;
+	} else {
+		tcp_repair_enabled = 0;
+	}
 
 	if (!conn)
 		goto out;
@@ -298,22 +312,27 @@
 	HA_ATOMIC_OR(&fdtab[conn->handle.fd].state, FD_LINGER_RISK);
 
 #ifdef TCP_REPAIR
-	if (setsockopt(conn->handle.fd, IPPROTO_TCP, TCP_REPAIR, &one, sizeof(one)) == 0) {
+	/* try to put socket in repair mode if sending a RST was not requested by
+	 * config. this often fails due to missing permissions (CAP_NET_ADMIN capability)
+	 */
+	if (tcp_repair_enabled && (setsockopt(conn->handle.fd, IPPROTO_TCP, TCP_REPAIR, &one, sizeof(one)) == 0)) {
 		/* socket will be quiet now */
 		goto out;
 	}
 #endif
-	/* either TCP_REPAIR is not defined or it failed (eg: permissions).
-	 * Let's fall back on the TTL trick, though it only works for routed
-	 * network and has no effect on local net.
+
+	/* Either TCP_REPAIR is not defined, it failed (eg: permissions), or was
+	 * not executed because a RST with a specific TTL was requested to be sent.
+	 * Set the TTL of the client connection before the connection is killed
+	 * by channel_abort and a RST packet will be emitted by the TCP/IP stack.
 	 */
 #ifdef IP_TTL
 	if (conn->src && conn->src->ss_family == AF_INET)
-		setsockopt(conn->handle.fd, IPPROTO_IP, IP_TTL, &one, sizeof(one));
+		setsockopt(conn->handle.fd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
 #endif
 #ifdef IPV6_UNICAST_HOPS
 	if (conn->src && conn->src->ss_family == AF_INET6)
-		setsockopt(conn->handle.fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &one, sizeof(one));
+		setsockopt(conn->handle.fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl, sizeof(ttl));
 #endif
  out:
 	/* kill the stream if any */
@@ -480,15 +499,41 @@
 #endif
 }
 
-
-/* Parse a "silent-drop" action. It takes no argument. It returns ACT_RET_PRS_OK on
- * success, ACT_RET_PRS_ERR on error.
+/* Parse a "silent-drop" action. It may take 2 optional arguments to define a
+ * "rst-ttl" parameter. It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR
+ * on error.
  */
-static enum act_parse_ret tcp_parse_silent_drop(const char **args, int *orig_arg, struct proxy *px,
+static enum act_parse_ret tcp_parse_silent_drop(const char **args, int *cur_arg, struct proxy *px,
                                                 struct act_rule *rule, char **err)
 {
+	unsigned int rst_ttl  = 0;
+	char *endp;
+
 	rule->action     = ACT_CUSTOM;
 	rule->action_ptr = tcp_exec_action_silent_drop;
+
+	if (strcmp(args[*cur_arg], "rst-ttl") == 0) {
+		if (!*args[*cur_arg + 1]) {
+			memprintf(err, "missing rst-ttl value\n");
+			return ACT_RET_PRS_ERR;
+		}
+
+		rst_ttl = (unsigned int)strtoul(args[*cur_arg + 1], &endp, 0);
+
+		if (endp && *endp != '\0') {
+			memprintf(err, "invalid character starting at '%s' (value 1-255 expected)\n",
+					endp);
+			return ACT_RET_PRS_ERR;
+		}
+		if ((rst_ttl == 0) || (rst_ttl > 255) ) {
+			memprintf(err, "valid rst-ttl values are [1-255]\n");
+			return ACT_RET_PRS_ERR;
+		}
+
+		*cur_arg += 2;
+	}
+
+	rule->arg.act.p[0] = (void *)(uintptr_t)rst_ttl;
 	return ACT_RET_PRS_OK;
 }