penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit f203fc805793cdb94851d95fe76fd2a3db1b86de

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-10T09:39:58Z
subjectAdded bind_wait option
commit f203fc805793cdb94851d95fe76fd2a3db1b86de
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-10T09:39:58Z

    Added bind_wait option
---
 docs/tinyproxy.conf.5.scd | 11 +++++++
 src/bind.c                | 42 +++++++++++++++++++++++++
 src/bind.h                | 14 +++++++++
 src/compat_cpu.c          | 18 +++++++++++
 src/compat_cpu.h          |  1 +
 src/datagram_listener.c   | 38 +++++++++++------------
 src/file_conf.c           |  1 +
 src/route.c               |  3 ++
 src/route.h               |  1 +
 src/stream_listener.c     | 78 ++++++++++++++++++++++++++++++++++++++++++-----
 10 files changed, 179 insertions(+), 28 deletions(-)

diff --git a/docs/tinyproxy.conf.5.scd b/docs/tinyproxy.conf.5.scd
index 0d26e21..1c62f47 100644
--- a/docs/tinyproxy.conf.5.scd
+++ b/docs/tinyproxy.conf.5.scd
@@ -53,6 +53,17 @@ connect_timeout=SECONDS
 	Connection timeout for both listen and upstream.
 	Default: 5
 
+bind_wait=SECONDS
+	Wait up to SECONDS for the listen address to become available when
+	bind(2) fails with EADDRINUSE.
+
+	This is useful during rolling restarts where a replacement process may
+	start before the previous process has released the listen port. If the
+	address becomes available before the timeout expires, tinyproxy binds it
+	and continues startup. Otherwise startup fails.
+
+	The default is 0, which fails immediately.
+
 broadcast_reply=listen
 	UDP only. Send upstream replies back to the original client from the listening endpoint.
 
diff --git a/src/bind.c b/src/bind.c
new file mode 100644
index 0000000..8ee3630
--- /dev/null
+++ b/src/bind.c
@@ -0,0 +1,42 @@
+#include "klog.h"
+#include "compat_cpu.h"
+
+#include "bind.h"
+
+#define BIND_WAIT_INTERVAL_MS 100
+
+int bind_with_wait(
+	evutil_socket_t fd,
+	const struct sockaddr *addr,
+	socklen_t addr_len,
+	const struct route *r)
+{
+	int waited_ms = 0;
+	int wait_ms = 1000 * r->opts.bind_wait_sec;
+
+	for (;;) {
+		if (bind(fd, addr, addr_len) == 0) {
+			if(0 < waited_ms) {
+				LOG_INFO("listener bound after wait",
+					"listen", _LOGV_ENDPOINT(&r->listen));
+			}
+			return 0;
+		}
+
+		int err = EVUTIL_SOCKET_ERROR();
+
+		if (err != EADDRINUSE || wait_ms <= 0 || waited_ms >= wait_ms) {
+			return err ? -err : -EIO;
+		}
+
+		if (waited_ms == 0) {
+			LOG_WARN("listener address in use, waiting to bind",
+				"listen", _LOGV_ENDPOINT(&r->listen),
+				"timeout", _LOGV(r->opts.bind_wait_sec));
+		}
+
+		sleep_ms(BIND_WAIT_INTERVAL_MS);
+		waited_ms += BIND_WAIT_INTERVAL_MS;
+	}
+}
+
diff --git a/src/bind.h b/src/bind.h
new file mode 100644
index 0000000..0ce52c4
--- /dev/null
+++ b/src/bind.h
@@ -0,0 +1,14 @@
+#ifndef BIND_H
+#define BIND_H
+
+#include <event2/util.h>
+
+#include "route.h"
+
+int bind_with_wait(
+	evutil_socket_t fd,
+	const struct sockaddr *addr,
+	socklen_t addr_len,
+	const struct route *r);
+
+#endif
diff --git a/src/compat_cpu.c b/src/compat_cpu.c
index a7f4287..d0153d0 100644
--- a/src/compat_cpu.c
+++ b/src/compat_cpu.c
@@ -6,6 +6,9 @@
 #include <windows.h>
 #else
 #include <unistd.h>
+
+#include <time.h>
+#include <errno.h>
 #endif
 
 int compat_cpu_count(void)
@@ -31,3 +34,18 @@ int compat_cpu_count(void)
 	return (int)n;
 #endif
 }
+
+void sleep_ms(unsigned int ms)
+{
+#ifdef _WIN32
+	Sleep(ms);
+#else
+	struct timespec ts;
+
+	ts.tv_sec = ms / 1000;
+	ts.tv_nsec = (long)(ms % 1000) * 1000000L;
+
+	while (nanosleep(&ts, &ts) < 0 && errno == EINTR) {
+	}
+#endif
+}
diff --git a/src/compat_cpu.h b/src/compat_cpu.h
index 4a3a77b..99d6421 100644
--- a/src/compat_cpu.h
+++ b/src/compat_cpu.h
@@ -2,5 +2,6 @@
 #define COMPAT_CPU_H
 
 int compat_cpu_count(void);
+void sleep_ms(unsigned int);
 
 #endif
diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index 6aaf141..8df02d0 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -4,6 +4,8 @@
 
 #include "klog.h"
 #include "route.h"
+#include "bind.h"
+#include "compat_cpu.h"
 #include "datagram_listener.h"
 #include "datagram_client.h"
 #include "datagram_builtin.h"
@@ -12,29 +14,22 @@
 #ifdef TINYPROXY_DEBUG
 #include <stdlib.h>
 
-#ifndef _WIN32
-#include <unistd.h>
-#endif
 static void tinyproxy_debug_race_sleep(const char *name)
 {
 	const char *want = getenv("TINYPROXY_RACE_SLEEP");
-	const char *delay_s = getenv("TINYPROXY_RACE_SLEEP_US");
-	long delay_us;
+	const char *delay_s = getenv("TINYPROXY_RACE_SLEEP_MS");
+	long delay_ms;
 
 	if (!want || strcmp(want, name) != 0) {
 		return;
 	}
 
-	delay_us = delay_s ? strtol(delay_s, NULL, 10) : 1000;
-	if (delay_us <= 0) {
-		delay_us = 1000;
+	delay_ms = delay_s ? strtol(delay_s, NULL, 10) : 1;
+	if (delay_ms <= 0) {
+		delay_ms = 1;
 	}
 
-#ifdef _WIN32
-	Sleep((DWORD)((delay_us + 999) / 1000));
-#else
-	usleep((useconds_t)delay_us);
-#endif
+	sleep_ms(delay_ms);
 }
 #else
 static void tinyproxy_debug_race_sleep(const char *name)
@@ -439,17 +434,20 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
 		return -err;
 	}
 
-	if (bind(
-			ctx->listen_fd,
-			(const struct sockaddr *)&listen_addr,
-			listen_addr_len
-		) < 0) {
-		int err = EVUTIL_SOCKET_ERROR();
+	rc = bind_with_wait(
+		ctx->listen_fd,
+		(const struct sockaddr *)&listen_addr,
+		listen_addr_len,
+		ctx->route
+	);
+
+	if (rc < 0) {
+		int err = -rc;
 
 		LOG_ERROR("udp bind failed",
 			"err", _LOGV(evutil_socket_error_to_string(err))
 		);
-		return -err;
+		return rc;
 	}
 
 	ctx->local_addr_len = sizeof(ctx->local_addr);
diff --git a/src/file_conf.c b/src/file_conf.c
index 0826d2c..9b01654 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -300,6 +300,7 @@ static int parse_route_options(char *s, struct route_options *opts)
 
 			PARSE_INT_OPT("idle_timeout", idle_timeout_sec);
 			PARSE_INT_OPT("connect_timeout", connect_timeout_sec);
+			PARSE_INT_OPT("bind_wait", bind_wait_sec);
 			PARSE_ENUM_OPT("broadcast_reply",
 				parse_broadcast_reply_mode,
 				broadcast_reply);
diff --git a/src/route.c b/src/route.c
index e7fc502..f16826f 100644
--- a/src/route.c
+++ b/src/route.c
@@ -55,6 +55,9 @@ void route_options_str(const struct route_options *opts, char *buf, size_t bufle
 	if(opts->connect_timeout_sec != ROUTE_DEFAULT_CONNECT_TIMEOUT_SEC) {
 		ADD_INT("connect_timeout", opts->connect_timeout_sec);
 	}
+	if(opts->bind_wait_sec != 0) {
+		ADD_INT("bind_wait", opts->bind_wait_sec);
+	}
 	if(opts->keep_alive) {
 		ADD_BOOL("keep_alive", opts->keep_alive);
 	}
diff --git a/src/route.h b/src/route.h
index 3017e73..b16f630 100644
--- a/src/route.h
+++ b/src/route.h
@@ -32,6 +32,7 @@ struct route_options {
 
 	int idle_timeout_sec;
 	int connect_timeout_sec;
+	int bind_wait_sec;
 	enum broadcast_reply_mode broadcast_reply;
 };
 
diff --git a/src/stream_listener.c b/src/stream_listener.c
index 8b79117..eeee459 100644
--- a/src/stream_listener.c
+++ b/src/stream_listener.c
@@ -5,6 +5,7 @@
 
 #include "klog.h"
 #include "route.h"
+#include "bind.h"
 #include "worker_pool.h"
 #include "stream_listener.h"
 #include "stream_conn.h"
@@ -69,6 +70,8 @@ static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
 	const struct route *r = ctx->route;
 	struct sockaddr_storage listen_addr;
 	socklen_t listen_addr_len;
+	evutil_socket_t fd;
+	struct evconnlistener *listener;
 	int rc;
 
 	rc = endpoint_to_sockaddr(&r->listen, &listen_addr, &listen_addr_len);
@@ -78,22 +81,81 @@ static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
 		return rc;
 	}
 
-	ctx->listener = evconnlistener_new_bind(
+	fd = socket(listen_addr.ss_family, SOCK_STREAM, 0);
+	if (fd < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
+
+		LOG_ERROR("failed to create tcp listener socket",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		return err ? -err : -EIO;
+	}
+
+	if (evutil_make_listen_socket_reuseable(fd) < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
+
+		LOG_ERROR("failed to make tcp listener reusable",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		evutil_closesocket(fd);
+		return err ? -err : -EIO;
+	}
+
+	if (evutil_make_socket_nonblocking(fd) < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
+
+		LOG_ERROR("failed to make tcp listener nonblocking",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		evutil_closesocket(fd);
+		return err ? -err : -EIO;
+	}
+
+	rc = bind_with_wait(
+		fd,
+		(struct sockaddr *)&listen_addr,
+		listen_addr_len,
+		r
+	);
+	if (rc != 0) {
+		int err = -rc;
+
+		LOG_ERROR("failed to bind tcp listener socket",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		evutil_closesocket(fd);
+		return rc;
+	}
+
+	if (listen(fd, -1) < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
+
+		LOG_ERROR("failed to listen on tcp listener socket",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		evutil_closesocket(fd);
+		return err ? -err : -EIO;
+	}
+
+	listener = evconnlistener_new(
 		ctx->accept_base,
 		accept_cb,
 		ctx,
-		LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
+		LEV_OPT_CLOSE_ON_FREE,
 		-1,
-		(struct sockaddr *)&listen_addr,
-		listen_addr_len
+		fd
 	);
+	if (listener == NULL) {
+		int err = EVUTIL_SOCKET_ERROR();
 
-	if (ctx->listener == NULL) {
-		LOG_ERROR("evconnlistener_new_bind failed",
-			"listen", _LOGV_ENDPOINT(&r->listen));
-		return -EADDRINUSE;
+		LOG_ERROR("failed to create tcp listener",
+			"listen", _LOGV_ENDPOINT(&r->listen),
+			"err", _LOGV(evutil_socket_error_to_string(err)));
+		evutil_closesocket(fd);
+		return err ? -err : -EIO;
 	}
 
+	ctx->listener = listener;
 	return 0;
 }