summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilly Tarreau <w@1wt.eu>2020-10-07 19:55:15 +0200
committerWilly Tarreau <w@1wt.eu>2020-10-07 19:55:15 +0200
commitdb9f0524509801b83ec428a8ae2ca535ea3ae264 (patch)
tree34e50d543a131279733d8965b90cf9aea29d1421
parent456e906b9ec9b81c6453380c8e2893a3450c1647 (diff)
downloadhaproxy-db9f0524509801b83ec428a8ae2ca535ea3ae264.tar.gz
MEDIUM: udp: implement udp_suspend() and udp_resume()20201007-proxy-state-7
In Linux kernel's net/ipv4/udp.c there's a udp_disconnect() function which is called when connecting to AF_UNSPEC, and which unhashes a "connection". This property, which is also documented in connect(2) both in Linux and Open Group's man pages for datagrams, is interesting because it allows to reverse a connect() which is in fact a filter on the source. As such we can suspend a receiver by making it connect to itself, which will cause it not to receive any traffic anymore, letting a new one receive it all, then resume it by breaking this connection. This was tested to work well on Linux, other operating systems should also be tested. Before this, sending a SIGTTOU to a process having a UDP syslog forwarder would cause this error: [WARNING] 280/194249 (3268) : Paused frontend GLOBAL. [WARNING] 280/194249 (3268) : Some proxies refused to pause, performing soft stop now. [WARNING] 280/194249 (3268) : Proxy GLOBAL stopped (cumulated conns: FE: 0, BE: 0). [WARNING] 280/194249 (3268) : Proxy sylog-loadb stopped (cumulated conns: FE: 0, BE: 0). With this change, it now proceeds just like with TCP listeners: [WARNING] 280/195503 (3885) : Paused frontend GLOBAL. [WARNING] 280/195503 (3885) : Paused frontend sylog-loadb. And SIGTTIN also works: [WARNING] 280/195507 (3885) : Resumed frontend GLOBAL. [WARNING] 280/195507 (3885) : Resumed frontend sylog-loadb. On Linux this also works with TCP listeners (which can then be resumed using listen()) and established TCP sockets (which we currently kill using setsockopt(so_linger)), both not being portable on other OSes. UNIX sockets and ABNS sockets do not support it however (connect always fails). This needs to be further explored to see if other OSes might benefit from this to perform portable and reliable resets particularly on the backend side.
-rw-r--r--src/proto_udp.c47
1 files changed, 45 insertions, 2 deletions
diff --git a/src/proto_udp.c b/src/proto_udp.c
index 33a4dca46..1dc731778 100644
--- a/src/proto_udp.c
+++ b/src/proto_udp.c
@@ -42,6 +42,7 @@
static int udp_bind_listener(struct listener *listener, char *errmsg, int errlen);
static int udp_suspend_receiver(struct receiver *rx);
+static int udp_resume_receiver(struct receiver *rx);
static void udp_enable_listener(struct listener *listener);
static void udp_disable_listener(struct listener *listener);
static void udp4_add_listener(struct listener *listener, int port);
@@ -62,6 +63,7 @@ static struct protocol proto_udp4 = {
.rx_enable = sock_enable,
.rx_disable = sock_disable,
.rx_suspend = udp_suspend_receiver,
+ .rx_resume = udp_resume_receiver,
.receivers = LIST_HEAD_INIT(proto_udp4.receivers),
.nb_receivers = 0,
};
@@ -83,6 +85,7 @@ static struct protocol proto_udp6 = {
.rx_enable = sock_enable,
.rx_disable = sock_disable,
.rx_suspend = udp_suspend_receiver,
+ .rx_resume = udp_resume_receiver,
.receivers = LIST_HEAD_INIT(proto_udp6.receivers),
.nb_receivers = 0,
};
@@ -181,11 +184,51 @@ static void udp_disable_listener(struct listener *l)
/* Suspend a receiver. Returns < 0 in case of failure, 0 if the receiver
* was totally stopped, or > 0 if correctly suspended.
+ * The principle is a bit ugly but works well, at least on Linux: in order to
+ * suspend the receiver, we want it to stop receiving traffic, which means that
+ * the socket must be unhashed from the kernel's socket table. The simple way
+ * to do this is to connect to any address that is reachable and will not be
+ * used by regular traffic, and a great one is reconnecting to self.
*/
static int udp_suspend_receiver(struct receiver *rx)
{
- /* we don't support suspend on UDP */
- return -1;
+ struct sockaddr_storage ss;
+ socklen_t len = sizeof(ss);
+
+ if (rx->fd < 0)
+ return 0;
+
+ if (getsockname(rx->fd, (struct sockaddr *)&ss, &len) < 0)
+ return -1;
+
+ if (connect(rx->fd, (struct sockaddr *)&ss, len) < 0)
+ return -1;
+
+ /* not necessary but may make debugging clearer */
+ fd_stop_recv(rx->fd);
+ return 1;
+}
+
+/* Resume a receiver. Returns < 0 in case of failure, 0 if the receiver
+ * was totally stopped, or > 0 if correctly suspended.
+ * The principle is to reverse the change above, we'll break the connection by
+ * connecting to AF_UNSPEC. The association breaks and the socket starts to
+ * receive from everywhere again.
+ */
+static int udp_resume_receiver(struct receiver *rx)
+{
+ struct sockaddr sa;
+ socklen_t len = sizeof(sa);
+
+ if (rx->fd < 0)
+ return 0;
+
+ sa.sa_family = AF_UNSPEC;
+ if (connect(rx->fd, &sa, len) < 0)
+ return -1;
+
+ fd_want_recv(rx->fd);
+ return 1;
}
/*