penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 923d73f9fa4678ef5aa7404e2fc2c98f788e00f2

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-30T06:24:54Z
subjectWiring pthread
commit 923d73f9fa4678ef5aa7404e2fc2c98f788e00f2
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-30T06:24:54Z

    Wiring pthread
---
 mk/common.mk            |   2 +
 src/compat_thread.c     | 151 ++++++++++++++++++++
 src/compat_thread.h     |  41 ++++++
 src/datagram_client.h   |  12 ++
 src/datagram_listener.c | 163 +++++++++++++---------
 src/datagram_route.c    |  14 +-
 src/datagram_route.h    |  10 +-
 src/proxy_proto_v2.c    |   4 -
 src/route_runtime.c     |  10 +-
 src/route_runtime.h     |   7 +-
 src/stream_conn.c       |  13 +-
 src/stream_conn.h       |   4 +-
 src/stream_listener.c   |  12 +-
 src/stream_route.c      |  11 +-
 src/stream_route.h      |  17 ++-
 src/tinyproxy.c         |  37 ++++-
 src/worker.c            | 364 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/worker.h            |  41 +++++-
 src/worker_pool.c       | 121 ++++++++++++++++
 src/worker_pool.h       |  22 +++
 20 files changed, 952 insertions(+), 104 deletions(-)

diff --git a/mk/common.mk b/mk/common.mk
index 2231f27..7d1f3f9 100644
--- a/mk/common.mk
+++ b/mk/common.mk
@@ -26,6 +26,7 @@ UNAME_S := $(shell uname)
 
 ifeq ($(UNAME_S),Darwin)
 STATIC ?= 0
+CFLAGS  += -pthread
 LDFLAGS += -Wl,-dead_strip
 TEST_FLAGS := CONCURRENCY=1000 TOTAL=1000 FD_LIMIT=2560
 else ifeq ($(OS),Windows_NT)
@@ -34,6 +35,7 @@ LDFLAGS += -Wl,--gc-sections
 TEST_FLAGS := CONCURRENCY=100 TOTAL=100 FD_LIMIT=512
 else
 STATIC ?= 1
+CFLAGS  += -pthread
 LDFLAGS += -Wl,--gc-sections
 TEST_FLAGS :=
 endif
diff --git a/src/compat_thread.c b/src/compat_thread.c
new file mode 100644
index 0000000..f5e7e25
--- /dev/null
+++ b/src/compat_thread.c
@@ -0,0 +1,151 @@
+#include "compat_thread.h"
+
+#ifdef _WIN32
+
+#include <errno.h>
+#include <stdlib.h>
+
+struct compat_thread_start {
+	compat_thread_fn fn;
+	void *arg;
+};
+
+static DWORD WINAPI compat_thread_trampoline(LPVOID raw)
+{
+	struct compat_thread_start *start = raw;
+	compat_thread_fn fn = start->fn;
+	void *arg = start->arg;
+
+	free(start);
+
+	fn(arg);
+
+	return 0;
+}
+
+int compat_thread_create(
+	compat_thread_t *thread,
+	compat_thread_fn fn,
+	void *arg
+) {
+	struct compat_thread_start *start;
+
+	if (!thread || !fn) {
+		return EINVAL;
+	}
+
+	start = malloc(sizeof(*start));
+	if (!start) {
+		return ENOMEM;
+	}
+
+	start->fn = fn;
+	start->arg = arg;
+
+	*thread = CreateThread(
+		NULL,
+		0,
+		compat_thread_trampoline,
+		start,
+		0,
+		NULL
+	);
+
+	if (!*thread) {
+		free(start);
+		return (int)GetLastError();
+	}
+
+	return 0;
+}
+
+int compat_thread_join(compat_thread_t thread)
+{
+	DWORD rc;
+
+	if (!thread) {
+		return EINVAL;
+	}
+
+	rc = WaitForSingleObject(thread, INFINITE);
+	if (rc != WAIT_OBJECT_0) {
+		return (int)GetLastError();
+	}
+
+	CloseHandle(thread);
+
+	return 0;
+}
+
+int compat_mutex_init(compat_mutex_t *mutex)
+{
+	if (!mutex) {
+		return EINVAL;
+	}
+
+	InitializeCriticalSection(mutex);
+
+	return 0;
+}
+
+void compat_mutex_lock(compat_mutex_t *mutex)
+{
+	EnterCriticalSection(mutex);
+}
+
+void compat_mutex_unlock(compat_mutex_t *mutex)
+{
+	LeaveCriticalSection(mutex);
+}
+
+void compat_mutex_destroy(compat_mutex_t *mutex)
+{
+	DeleteCriticalSection(mutex);
+}
+
+#else
+
+#include <errno.h>
+
+int compat_thread_create(
+	compat_thread_t *thread,
+	compat_thread_fn fn,
+	void *arg
+) {
+	if (!thread || !fn) {
+		return EINVAL;
+	}
+
+	return pthread_create(thread, NULL, fn, arg);
+}
+
+int compat_thread_join(compat_thread_t thread)
+{
+	return pthread_join(thread, NULL);
+}
+
+int compat_mutex_init(compat_mutex_t *mutex)
+{
+	if (!mutex) {
+		return EINVAL;
+	}
+
+	return pthread_mutex_init(mutex, NULL);
+}
+
+void compat_mutex_lock(compat_mutex_t *mutex)
+{
+	pthread_mutex_lock(mutex);
+}
+
+void compat_mutex_unlock(compat_mutex_t *mutex)
+{
+	pthread_mutex_unlock(mutex);
+}
+
+void compat_mutex_destroy(compat_mutex_t *mutex)
+{
+	pthread_mutex_destroy(mutex);
+}
+
+#endif
diff --git a/src/compat_thread.h b/src/compat_thread.h
new file mode 100644
index 0000000..4326322
--- /dev/null
+++ b/src/compat_thread.h
@@ -0,0 +1,41 @@
+#ifndef TINYPROXY_COMPAT_THREAD_H
+#define TINYPROXY_COMPAT_THREAD_H
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <pthread.h>
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void *(*compat_thread_fn)(void *arg);
+
+#ifdef _WIN32
+typedef HANDLE compat_thread_t;
+typedef CRITICAL_SECTION compat_mutex_t;
+#else
+typedef pthread_t compat_thread_t;
+typedef pthread_mutex_t compat_mutex_t;
+#endif
+
+int compat_thread_create(
+	compat_thread_t *thread,
+	compat_thread_fn fn,
+	void *arg
+);
+
+int compat_thread_join(compat_thread_t thread);
+
+int compat_mutex_init(compat_mutex_t *mutex);
+void compat_mutex_lock(compat_mutex_t *mutex);
+void compat_mutex_unlock(compat_mutex_t *mutex);
+void compat_mutex_destroy(compat_mutex_t *mutex);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/datagram_client.h b/src/datagram_client.h
index 2320ec3..a3f238d 100644
--- a/src/datagram_client.h
+++ b/src/datagram_client.h
@@ -19,6 +19,18 @@ struct datagram_client {
 	struct datagram_client *next;
 };
 
+struct datagram_packet {
+	const struct route *route;
+
+	evutil_socket_t listen_fd;
+
+	struct sockaddr_storage peer_addr;
+	socklen_t peer_addr_len;
+
+	unsigned char data[65535];
+	size_t data_len;
+};
+
 void cleanup_idle_datagram_clients(struct datagram_route_ctx *ctx);
 void free_datagram_client(struct datagram_client *c);
 
diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index 4fe0260..40d9603 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -8,86 +8,121 @@
 #include "datagram_client.h"
 #include "datagram_builtin.h"
 
-static void listen_read_cb(evutil_socket_t fd, short events, void *arg)
+static int handle_datagram_builtin_packet(
+	struct datagram_route_ctx *ctx,
+	const struct datagram_packet *pkt)
 {
-	(void)events;
-
-	struct datagram_route_ctx *ctx = arg;
+	struct datagram_client tmp;
+	memset(&tmp, 0, sizeof(tmp));
+
+	tmp.ctx = ctx;
+	tmp.fd = EVUTIL_INVALID_SOCKET;
+	tmp.client_addr = pkt->peer_addr;
+	tmp.client_addr_len = pkt->peer_addr_len;
+	tmp.last_seen = time(NULL);
+
+	int rc = start_datagram_builtin(&tmp);
+	if (rc < 0) {
+		LOG_ERROR("udp builtin failed", "err", _LOGV(strerror(-rc)));
+	}
+	return rc;
+}
 
-	cleanup_idle_datagram_clients(ctx);
+static struct datagram_client* datagram_route_get_or_create_client(
+	struct datagram_route_ctx *ctx,
+	const struct datagram_packet *pkt)
+{
+	struct datagram_client *c = find_datagram_client(ctx, &pkt->peer_addr, pkt->peer_addr_len);
+	if (c == NULL) {
+		c = create_datagram_client(ctx, &pkt->peer_addr, pkt->peer_addr_len);
+		if (c == NULL) {
+			return c;
+		}
+	}
+	c->last_seen = time(NULL);
+	return c;
+}
 
-	for (;;) {
-		unsigned char buf[UDP_MAX_PACKET];
+static int datagram_route_handle_packet(
+	struct datagram_route_ctx *ctx,
+	const struct datagram_packet *pkt)
+{
+	struct datagram_client *c;
+	int rc;
 
-		struct sockaddr_storage client_addr;
-		socklen_t client_addr_len = sizeof(client_addr);
+	if (pkt->route->upstream.kind == ENDPOINT_BUILTIN) {
+		return handle_datagram_builtin_packet(ctx, pkt);
+	}
 
-		memset(&client_addr, 0, sizeof(client_addr));
+	c = datagram_route_get_or_create_client(ctx, pkt);
+	if (!c) {
+		return -ENOMEM;
+	}
 
-		ssize_t n = recvfrom(
-			fd,
-			(char *)buf,
-			sizeof(buf),
-			0,
-			(struct sockaddr *)&client_addr,
-			&client_addr_len
-		);
+	rc = send_datagram_payload_to_upstream(c, pkt->data, pkt->data_len);
+	if (rc != 0) {
+		LOG_WARN("failed to send datagram payload upstream", "err", _LOGV(strerror(-rc)));
+		return rc;
+	}
 
-		if (n < 0) {
-			int err = EVUTIL_SOCKET_ERROR();
+	return 0;
+}
 
-			if (socket_err_is_retriable(err)) {
-				return;
-			}
+static int dispatch_datagram_packet(
+	struct datagram_route_ctx *ctx,
+	const struct datagram_packet *pkt
+) {
+	/*
+	 * Datagram routes are pinned to ctx->base for now.
+	 * Later this function can choose a worker/shard.
+	 */
+	return datagram_route_handle_packet(ctx, pkt);
+}
 
-			LOG_ERROR("udp listen recv failed",
-				"err", _LOGV(evutil_socket_error_to_string(err))
-			);
-			return;
-		}
+static void listen_read_cb(evutil_socket_t fd, short events, void *arg)
+{
+	struct datagram_route_ctx *ctx = arg;
+	struct datagram_packet pkt;
+	ssize_t n;
 
-		if (n == 0) {
-			continue;
-		}
+	(void)events;
 
-		if (ctx->route->upstream.kind == ENDPOINT_BUILTIN) {
-			struct datagram_client tmp;
-
-			memset(&tmp, 0, sizeof(tmp));
-			tmp.ctx = ctx;
-			tmp.fd = -1;
-			tmp.client_addr = client_addr;
-			tmp.client_addr_len = client_addr_len;
-			tmp.last_seen = time(NULL);
-
-			int rc = start_datagram_builtin(&tmp);
-			if (rc < 0) {
-				LOG_ERROR("udp builtin failed",
-						"err", _LOGV(evutil_socket_error_to_string(-rc))
-						);
-				return;
-			}
-
-			continue;
-		}
+	cleanup_idle_datagram_clients(ctx);
 
-		struct datagram_client *c = find_datagram_client(ctx, &client_addr, client_addr_len);
-		if (c == NULL) {
-			c = create_datagram_client(ctx, &client_addr, client_addr_len);
-			if (c == NULL) {
-				return;
-			}
-		}
+	memset(&pkt, 0, sizeof(pkt));
+	pkt.route = ctx->route;
+	pkt.listen_fd = fd;
+	pkt.peer_addr_len = sizeof(pkt.peer_addr);
+
+	n = recvfrom(
+		fd,
+		(char *)pkt.data,
+		sizeof(pkt.data),
+		0,
+		(struct sockaddr *)&pkt.peer_addr,
+		&pkt.peer_addr_len
+	);
 
-		c->last_seen = time(NULL);
+	if (n < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
 
-		int rc = send_datagram_payload_to_upstream(c, buf, (size_t)n);
-		if (rc < 0) {
-			LOG_ERROR("udp send to upstream failed",
-				"err", _LOGV(evutil_socket_error_to_string(-rc))
-			);
+		if (socket_err_is_retriable(err)) {
 			return;
 		}
+
+		LOG_ERROR("udp listen recv failed",
+				"err", _LOGV(evutil_socket_error_to_string(err))
+				);
+		return;
+	}
+
+	pkt.data_len = (size_t)n;
+
+	int rc = dispatch_datagram_packet(ctx, &pkt);
+	if (rc != 0) {
+		LOG_WARN("failed to dispatch datagram packet",
+				"err", _LOGV(rc)
+				);
 	}
 }
 
diff --git a/src/datagram_route.c b/src/datagram_route.c
index 7ba2311..fcf2377 100644
--- a/src/datagram_route.c
+++ b/src/datagram_route.c
@@ -5,7 +5,6 @@
 
 #include "klog.h"
 #include "env.h"
-#include "worker.h"
 #include "route.h"
 #include "datagram_route.h"
 #include "datagram_listener.h"
@@ -31,10 +30,11 @@ static int prepare_datagram_route(struct datagram_route_ctx *ctx)
 }
 
 int start_datagram_route(
-	struct worker *w,
-	const struct route *r,
-	struct datagram_route_ctx *ctx
-) {
+		struct event_base *accept_base,
+		struct worker_pool *wpool,
+		const struct route *r,
+		struct datagram_route_ctx *ctx)
+{
 	int rc;
 	char opts[128];
 
@@ -44,8 +44,8 @@ int start_datagram_route(
 
 	memset(ctx, 0, sizeof(*ctx));
 
-	ctx->base = w->base;
-	ctx->worker = w;
+	ctx->base = accept_base;
+	ctx->worker_pool = wpool;
 	ctx->route = r;
 	ctx->listen_fd = -1;
 
diff --git a/src/datagram_route.h b/src/datagram_route.h
index 9b56c4d..0dec487 100644
--- a/src/datagram_route.h
+++ b/src/datagram_route.h
@@ -4,6 +4,7 @@
 #include <event2/util.h>
 
 #include "compat.h"
+#include "worker_pool.h"
 
 struct event;
 struct event_base;
@@ -13,7 +14,7 @@ struct datagram_client;
 
 struct datagram_route_ctx {
 	struct event_base *base;
-	struct worker *worker;
+	struct worker_pool *worker_pool;
 	const struct route *route;
 
 	evutil_socket_t listen_fd;
@@ -25,8 +26,11 @@ struct datagram_route_ctx {
 	struct datagram_client *clients;
 };
 
-int start_datagram_route(struct worker *w, const struct route *r,
-	struct datagram_route_ctx *ctx);
+int start_datagram_route(
+		struct event_base *accept_base,
+		struct worker_pool *wpool,
+		const struct route *r,
+		struct datagram_route_ctx *ctx);
 
 void stop_datagram_route(struct datagram_route_ctx *ctx);
 
diff --git a/src/proxy_proto_v2.c b/src/proxy_proto_v2.c
index e584239..0046db7 100644
--- a/src/proxy_proto_v2.c
+++ b/src/proxy_proto_v2.c
@@ -29,10 +29,6 @@ int proxy_v2_build(
 		return -EINVAL;
 	}
 
-	if (src_len < 0 || dst_len < 0) {
-		return -EINVAL;
-	}
-
 	if (src->sa_family != dst->sa_family) {
 		return -EAFNOSUPPORT;
 	}
diff --git a/src/route_runtime.c b/src/route_runtime.c
index 87b0244..39dfc67 100644
--- a/src/route_runtime.c
+++ b/src/route_runtime.c
@@ -50,7 +50,11 @@ int validate_route(const struct route *r)
 	return -EINVAL;
 }
 
-int start_route(struct worker *w, const struct route *r, struct route_ctx *ctx)
+int start_route(
+		struct event_base *accept_base,
+		struct worker_pool *wp,
+		const struct route *r,
+		struct route_ctx *ctx)
 {
 	int rc;
 
@@ -61,7 +65,7 @@ int start_route(struct worker *w, const struct route *r, struct route_ctx *ctx)
 	if (endpoint_is_stream(&r->listen)) {
 		ctx->kind = ROUTE_CTX_STREAM;
 
-		rc = start_stream_route(w, r, &ctx->u.stream);
+		rc = start_stream_route(accept_base, wp, r, &ctx->u.stream);
 		if (rc != 0) {
 			ctx->kind = ROUTE_CTX_NONE;
 			memset(&ctx->u.stream, 0, sizeof(ctx->u.stream));
@@ -73,7 +77,7 @@ int start_route(struct worker *w, const struct route *r, struct route_ctx *ctx)
 	if (endpoint_is_datagram(&r->listen)) {
 		ctx->kind = ROUTE_CTX_DATAGRAM;
 
-		rc = start_datagram_route(w, r, &ctx->u.datagram);
+		rc = start_datagram_route(accept_base, wp, r, &ctx->u.datagram);
 		if (rc != 0) {
 			ctx->kind = ROUTE_CTX_NONE;
 			memset(&ctx->u.datagram, 0, sizeof(ctx->u.datagram));
diff --git a/src/route_runtime.h b/src/route_runtime.h
index a192f36..22ad642 100644
--- a/src/route_runtime.h
+++ b/src/route_runtime.h
@@ -23,7 +23,12 @@ struct route_ctx {
 
 int validate_route(const struct route *r);
 
-int start_route(struct worker *w, const struct route *r, struct route_ctx *ctx);
+int start_route(
+		struct event_base *accept_base,
+		struct worker_pool *wp,
+		const struct route *r,
+		struct route_ctx *ctx);
+
 void stop_route(struct route_ctx *ctx);
 
 #endif
diff --git a/src/stream_conn.c b/src/stream_conn.c
index 78d9328..2cd3c46 100644
--- a/src/stream_conn.c
+++ b/src/stream_conn.c
@@ -265,7 +265,7 @@ static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
 	}
 }
 
-static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac) {
+void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac) {
 	conn_t *conn = calloc(1, sizeof(*conn));
 	if (conn == NULL) {
 		evutil_closesocket(ac->fd);
@@ -407,8 +407,15 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
 	bufferevent_enable(conn->upstream, EV_READ | EV_WRITE);
 }
 
-void dispatch_client_fd(struct worker *w, struct accepted_client *ac) {
-	worker_adopt_client_fd(w, ac);
+int dispatch_client_fd(struct worker *w, struct accepted_client *ac)
+{
+	return worker_enqueue_client_fd(
+		w,
+		ac->route,
+		ac->fd,
+		(const struct sockaddr *)&ac->peer_addr,
+		ac->peer_addr_len
+	);
 }
 
 #ifdef FUZZ
diff --git a/src/stream_conn.h b/src/stream_conn.h
index bbac010..f4ef594 100644
--- a/src/stream_conn.h
+++ b/src/stream_conn.h
@@ -3,7 +3,7 @@
 
 #include "stream_route.h"
 
-void dispatch_client_fd(struct worker *w, struct accepted_client *ac);
+int dispatch_client_fd(struct worker *w, struct accepted_client *ac);
 void free_conn(conn_t *conn);
 
 void set_client_idle_timeout(conn_t *conn, const struct route *r);
@@ -11,6 +11,8 @@ void set_client_idle_timeout(conn_t *conn, const struct route *r);
 void stream_client_event_cb(struct bufferevent *bev, short events, void *arg);
 void stream_upstream_event_cb(struct bufferevent *bev, short events, void *arg);
 
+void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac);
+
 #ifdef FUZZ
 int stream_route_adopt_client_for_fuzz(
 	struct event_base *base,
diff --git a/src/stream_listener.c b/src/stream_listener.c
index 3813fa4..1aa4255 100644
--- a/src/stream_listener.c
+++ b/src/stream_listener.c
@@ -5,6 +5,7 @@
 
 #include "klog.h"
 #include "route.h"
+#include "worker_pool.h"
 #include "stream_listener.h"
 #include "stream_conn.h"
 
@@ -32,7 +33,16 @@ static void accept_cb(
 		return;
 	}
 
-	dispatch_client_fd(ctx->worker, &ac);
+	struct worker *w = worker_pool_next(ctx->worker_pool);
+	if (!w) {
+		LOG_ERROR("no worker available", "socklen", _LOGV(socklen));
+		evutil_closesocket(client_fd);
+		return;
+	}
+
+	if (dispatch_client_fd(w, &ac) != 0) {
+		evutil_closesocket(client_fd);
+	}
 }
 
 void accept_error_cb(struct evconnlistener *listener, void *arg)
diff --git a/src/stream_route.c b/src/stream_route.c
index f80ac48..fb553ad 100644
--- a/src/stream_route.c
+++ b/src/stream_route.c
@@ -9,16 +9,19 @@
 #include "route.h"
 #include "stream_listener.h"
 
-int start_stream_route(struct worker *w, const struct route *r,
-	struct stream_route_ctx *ctx)
+int start_stream_route(
+		struct event_base *accept_base,
+		struct worker_pool *wpool,
+		const struct route *r,
+		struct stream_route_ctx *ctx)
 {
 	int rc;
 	char opts[128];
 
 	memset(ctx, 0, sizeof(*ctx));
 
-	ctx->accept_base = w->base;
-	ctx->worker = w;
+	ctx->accept_base = accept_base;
+	ctx->worker_pool = wpool;
 	ctx->route = r;
 
 	rc = bind_stream_listener(ctx);
diff --git a/src/stream_route.h b/src/stream_route.h
index 45b38dc..f9d809e 100644
--- a/src/stream_route.h
+++ b/src/stream_route.h
@@ -14,7 +14,7 @@ struct evconnlistener;
 
 struct stream_route_ctx {
 	struct event_base *accept_base;
-	struct worker *worker;
+	struct worker_pool *worker_pool;
 	const struct route *route;
 	struct evconnlistener *listener;
 };
@@ -34,14 +34,23 @@ typedef struct conn_s {
 } conn_t;
 
 struct accepted_client {
+	const struct route *route;
 	evutil_socket_t fd;
+
 	struct sockaddr_storage peer_addr;
 	socklen_t peer_addr_len;
-	const struct route *route;
 };
 
-int start_stream_route(struct worker *w, const struct route *r,
-	struct stream_route_ctx *ctx);
+struct accepted_client_node {
+	struct accepted_client client;
+	struct accepted_client_node *next;
+};
+
+int start_stream_route(
+		struct event_base *accept_base,
+		struct worker_pool *wpool,
+		const struct route *r,
+		struct stream_route_ctx *ctx);
 
 void stop_stream_route(struct stream_route_ctx *ctx);
 
diff --git a/src/tinyproxy.c b/src/tinyproxy.c
index b11e00f..7066cca 100644
--- a/src/tinyproxy.c
+++ b/src/tinyproxy.c
@@ -10,7 +10,7 @@
 #include "klog.h"
 #include "signals.h"
 #include "file_conf.h"
-#include "worker.h"
+#include "worker_pool.h"
 #include "route.h"
 #include "route_runtime.h"
 
@@ -53,6 +53,8 @@ int main(int argc, char **argv)
 	const char **inline_routes = NULL;
 	size_t inline_route_count = 0;
 	size_t inline_route_cap = 0;
+	bool worker_pool_ready = false;
+	bool worker_pool_started = false;
 
 #ifdef _WIN32
 	int wsa_started = 0;
@@ -112,7 +114,7 @@ int main(int argc, char **argv)
 				return 0;
 
 			case 'v':
-				fprintf(stdout, "tinyproxy v0.1.4\n");
+				fprintf(stdout, "tinyproxy v0.2.0\n");
 				free(inline_routes);
 				return 0;
 
@@ -216,10 +218,22 @@ int main(int argc, char **argv)
 	}
 	signals_started = 1;
 
-	struct worker w = {
-		.base = base,
-		.id = 0,
-	};
+	struct worker_pool wpool;
+	size_t worker_count = 1;
+
+	rc = worker_pool_init(&wpool, worker_count);
+	if (rc != 0) {
+		LOG_ERROR("failed to init worker pool", "err", _LOGV(rc));
+		goto out;
+	}
+	worker_pool_ready = true;
+
+	rc = worker_pool_start(&wpool);
+	if (rc != 0) {
+		LOG_ERROR("failed to start worker pool", "err", _LOGV(rc));
+		goto out;
+	}
+	worker_pool_started = true;
 
 	for (size_t i = 0; i < route_count; i++) {
 		const struct route *r = &routes[i];
@@ -234,7 +248,7 @@ int main(int argc, char **argv)
 			goto out;
 		}
 
-		rc = start_route(&w, r, &route_ctxs[route_ctx_count]);
+		rc = start_route(base, &wpool, r, &route_ctxs[route_ctx_count]);
 		if (rc == 0) {
 			route_ctx_count++;
 		}
@@ -262,6 +276,15 @@ out:
 		stop_route(&route_ctxs[i]);
 	}
 
+	if (worker_pool_started) {
+		worker_pool_stop(&wpool);
+		worker_pool_join(&wpool);
+	}
+
+	if (worker_pool_ready) {
+		worker_pool_free(&wpool);
+	}
+
 	if (signals_started) {
 		free_signal_handlers(&signals);
 	}
diff --git a/src/worker.c b/src/worker.c
new file mode 100644
index 0000000..ce9c753
--- /dev/null
+++ b/src/worker.c
@@ -0,0 +1,364 @@
+#include <event2/event.h>
+#include <event2/util.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "klog.h"
+#include "stream_conn.h"
+#include "worker.h"
+
+static void worker_notify_cb(evutil_socket_t fd, short events, void *arg);
+
+static int notify_worker(struct worker *w)
+{
+	char byte = 1;
+	int rc;
+
+	(void)w;
+
+#ifdef _WIN32
+	rc = send(w->notify_send_fd, &byte, 1, 0);
+	if (rc == SOCKET_ERROR) {
+		return WSAGetLastError();
+	}
+#else
+	rc = (int)send(w->notify_send_fd, &byte, 1, 0);
+	if (rc < 0) {
+		return errno;
+	}
+#endif
+
+	return 0;
+}
+
+static void drain_notify_fd(evutil_socket_t fd)
+{
+	char buf[128];
+
+	for (;;) {
+#ifdef _WIN32
+		int n = recv(fd, buf, sizeof(buf), 0);
+		if (n == SOCKET_ERROR) {
+			int err = WSAGetLastError();
+			if (err == WSAEWOULDBLOCK) {
+				return;
+			}
+			return;
+		}
+#else
+		ssize_t n = recv(fd, buf, sizeof(buf), 0);
+		if (n < 0) {
+			if (errno == EAGAIN || errno == EWOULDBLOCK) {
+				return;
+			}
+			return;
+		}
+#endif
+
+		if (n == 0) {
+			return;
+		}
+
+		if ((size_t)n < sizeof(buf)) {
+			return;
+		}
+	}
+}
+
+static struct accepted_client_node *worker_take_pending(struct worker *w)
+{
+	struct accepted_client_node *head;
+
+	compat_mutex_lock(&w->pending_mu);
+
+	head = w->pending_head;
+	w->pending_head = NULL;
+	w->pending_tail = NULL;
+
+	compat_mutex_unlock(&w->pending_mu);
+
+	return head;
+}
+
+static void worker_process_pending(struct worker *w)
+{
+	struct accepted_client_node *node = worker_take_pending(w);
+
+	while (node) {
+		struct accepted_client_node *next = node->next;
+
+		worker_adopt_client_fd(w, &node->client);
+
+		free(node);
+		node = next;
+	}
+}
+
+static void *worker_main(void *arg)
+{
+	struct worker *w = arg;
+
+	event_base_dispatch(w->base);
+
+	return NULL;
+}
+
+int worker_init(struct worker *w, unsigned int id)
+{
+	evutil_socket_t fds[2] = {
+		EVUTIL_INVALID_SOCKET,
+		EVUTIL_INVALID_SOCKET,
+	};
+
+	if (!w) {
+		return EINVAL;
+	}
+
+	memset(w, 0, sizeof(*w));
+
+	w->id = id;
+	w->notify_recv_fd = EVUTIL_INVALID_SOCKET;
+	w->notify_send_fd = EVUTIL_INVALID_SOCKET;
+
+	w->base = event_base_new();
+	if (!w->base) {
+		return ENOMEM;
+	}
+
+	if (compat_mutex_init(&w->pending_mu) != 0) {
+		event_base_free(w->base);
+		w->base = NULL;
+		return errno ? errno : EINVAL;
+	}
+
+	if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
+		compat_mutex_destroy(&w->pending_mu);
+		event_base_free(w->base);
+		w->base = NULL;
+		return EVUTIL_SOCKET_ERROR();
+	}
+
+	w->notify_recv_fd = fds[0];
+	w->notify_send_fd = fds[1];
+
+	evutil_make_socket_nonblocking(w->notify_recv_fd);
+	evutil_make_socket_nonblocking(w->notify_send_fd);
+	evutil_make_socket_closeonexec(w->notify_recv_fd);
+	evutil_make_socket_closeonexec(w->notify_send_fd);
+
+	w->notify_event = event_new(
+		w->base,
+		w->notify_recv_fd,
+		EV_READ | EV_PERSIST,
+		worker_notify_cb,
+		w
+	);
+	if (!w->notify_event) {
+		evutil_closesocket(w->notify_recv_fd);
+		evutil_closesocket(w->notify_send_fd);
+		w->notify_recv_fd = EVUTIL_INVALID_SOCKET;
+		w->notify_send_fd = EVUTIL_INVALID_SOCKET;
+
+		compat_mutex_destroy(&w->pending_mu);
+		event_base_free(w->base);
+		w->base = NULL;
+
+		return ENOMEM;
+	}
+
+	if (event_add(w->notify_event, NULL) < 0) {
+		event_free(w->notify_event);
+		w->notify_event = NULL;
+
+		evutil_closesocket(w->notify_recv_fd);
+		evutil_closesocket(w->notify_send_fd);
+		w->notify_recv_fd = EVUTIL_INVALID_SOCKET;
+		w->notify_send_fd = EVUTIL_INVALID_SOCKET;
+
+		compat_mutex_destroy(&w->pending_mu);
+		event_base_free(w->base);
+		w->base = NULL;
+
+		return EINVAL;
+	}
+
+	return 0;
+}
+
+int worker_start(struct worker *w)
+{
+	int rc;
+
+	if (!w || !w->base) {
+		return EINVAL;
+	}
+
+	if (w->started) {
+		return 0;
+	}
+
+	rc = compat_thread_create(&w->thread, worker_main, w);
+	if (rc != 0) {
+		return rc;
+	}
+
+	w->started = true;
+
+	return 0;
+}
+
+void worker_stop(struct worker *w)
+{
+	if (!w) {
+		return;
+	}
+
+	compat_mutex_lock(&w->pending_mu);
+	w->stopping = true;
+	compat_mutex_unlock(&w->pending_mu);
+
+	if (w->started) {
+		(void)notify_worker(w);
+	}
+}
+
+void worker_join(struct worker *w)
+{
+	if (!w || !w->started) {
+		return;
+	}
+
+	(void)compat_thread_join(w->thread);
+	w->started = false;
+}
+
+void worker_free(struct worker *w)
+{
+	struct accepted_client_node *node;
+
+	if (!w) {
+		return;
+	}
+
+	worker_stop(w);
+	worker_join(w);
+
+	node = worker_take_pending(w);
+	while (node) {
+		struct accepted_client_node *next = node->next;
+
+		if (node->client.fd != EVUTIL_INVALID_SOCKET) {
+			evutil_closesocket(node->client.fd);
+		}
+
+		free(node);
+		node = next;
+	}
+
+	if (w->notify_event) {
+		event_free(w->notify_event);
+		w->notify_event = NULL;
+	}
+
+	if (w->notify_recv_fd != EVUTIL_INVALID_SOCKET) {
+		evutil_closesocket(w->notify_recv_fd);
+		w->notify_recv_fd = EVUTIL_INVALID_SOCKET;
+	}
+
+	if (w->notify_send_fd != EVUTIL_INVALID_SOCKET) {
+		evutil_closesocket(w->notify_send_fd);
+		w->notify_send_fd = EVUTIL_INVALID_SOCKET;
+	}
+
+	compat_mutex_destroy(&w->pending_mu);
+
+	if (w->base) {
+		event_base_free(w->base);
+		w->base = NULL;
+	}
+}
+
+int worker_enqueue_client_fd(
+	struct worker *w,
+	const struct route *route,
+	evutil_socket_t fd,
+	const struct sockaddr *addr,
+	socklen_t addr_len
+) {
+	struct accepted_client_node *node;
+	int rc;
+
+	if (!w || !route || fd == EVUTIL_INVALID_SOCKET) {
+		return EINVAL;
+	}
+
+	if (addr_len > sizeof(node->client.peer_addr)) {
+		return EINVAL;
+	}
+
+	node = calloc(1, sizeof(*node));
+	if (!node) {
+		return ENOMEM;
+	}
+
+	node->client.route = route;
+	node->client.fd = fd;
+
+	if (addr && addr_len > 0) {
+		memcpy(&node->client.peer_addr, addr, addr_len);
+		node->client.peer_addr_len = addr_len;
+	}
+
+	compat_mutex_lock(&w->pending_mu);
+
+	if (w->stopping) {
+		compat_mutex_unlock(&w->pending_mu);
+		free(node);
+		return ECANCELED;
+	}
+
+	if (w->pending_tail) {
+		w->pending_tail->next = node;
+	} else {
+		w->pending_head = node;
+	}
+
+	w->pending_tail = node;
+
+	compat_mutex_unlock(&w->pending_mu);
+
+	rc = notify_worker(w);
+	if (rc != 0) {
+		/*
+		 * The fd is already queued and owned by the worker.
+		 * Do not close it here.
+		 */
+		LOG_WARN("failed to notify worker",
+			"worker", _LOGV(w->id),
+			"err", _LOGV(rc)
+		);
+	}
+
+	return 0;
+}
+
+static void worker_notify_cb(evutil_socket_t fd, short events, void *arg)
+{
+	struct worker *w = arg;
+
+	(void)events;
+
+	drain_notify_fd(fd);
+
+	worker_process_pending(w);
+
+	compat_mutex_lock(&w->pending_mu);
+	bool stopping = w->stopping;
+	compat_mutex_unlock(&w->pending_mu);
+
+	if (stopping) {
+		event_base_loopbreak(w->base);
+	}
+}
diff --git a/src/worker.h b/src/worker.h
index 53c28e6..021b90d 100644
--- a/src/worker.h
+++ b/src/worker.h
@@ -1,11 +1,48 @@
 #ifndef WORKER_H
 #define WORKER_H
 
-struct event_base;
+#include <stdbool.h>
+#include <stddef.h>
+
+#include <event2/event.h>
+#include <event2/util.h>
+
+#include "compat_thread.h"
+#include "compat.h"
+#include "route.h"
+
+struct accept_client_node;
 
 struct worker {
+	unsigned int id;
+
 	struct event_base *base;
-	size_t id;
+
+	compat_thread_t thread;
+	bool started;
+	bool stopping;
+
+	evutil_socket_t notify_recv_fd;
+	evutil_socket_t notify_send_fd;
+	struct event *notify_event;
+
+	struct accepted_client_node *pending_head;
+	struct accepted_client_node *pending_tail;
+	compat_mutex_t pending_mu;
 };
 
+int worker_init(struct worker *w, unsigned int id);
+int worker_start(struct worker *w);
+void worker_stop(struct worker *w);
+void worker_join(struct worker *w);
+void worker_free(struct worker *w);
+
+int worker_enqueue_client_fd(
+	struct worker *w,
+	const struct route *route,
+	evutil_socket_t fd,
+	const struct sockaddr *addr,
+	socklen_t addr_len
+);
+
 #endif
diff --git a/src/worker_pool.c b/src/worker_pool.c
new file mode 100644
index 0000000..410a3b3
--- /dev/null
+++ b/src/worker_pool.c
@@ -0,0 +1,121 @@
+#include "worker_pool.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+int worker_pool_init(struct worker_pool *pool, size_t count)
+{
+	size_t i;
+
+	if (!pool || count == 0) {
+		return EINVAL;
+	}
+
+	memset(pool, 0, sizeof(*pool));
+
+	pool->workers = calloc(count, sizeof(pool->workers[0]));
+	if (!pool->workers) {
+		return ENOMEM;
+	}
+
+	pool->count = count;
+
+	for (i = 0; i < count; i++) {
+		int rc = worker_init(&pool->workers[i], (unsigned int)i);
+		if (rc != 0) {
+			while (i > 0) {
+				i--;
+				worker_free(&pool->workers[i]);
+			}
+
+			free(pool->workers);
+			pool->workers = NULL;
+			pool->count = 0;
+
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+int worker_pool_start(struct worker_pool *pool)
+{
+	size_t i;
+
+	if (!pool || !pool->workers) {
+		return EINVAL;
+	}
+
+	for (i = 0; i < pool->count; i++) {
+		int rc = worker_start(&pool->workers[i]);
+		if (rc != 0) {
+			while (i > 0) {
+				i--;
+				worker_stop(&pool->workers[i]);
+				worker_join(&pool->workers[i]);
+			}
+
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+void worker_pool_stop(struct worker_pool *pool)
+{
+	size_t i;
+
+	if (!pool || !pool->workers) {
+		return;
+	}
+
+	for (i = 0; i < pool->count; i++) {
+		worker_stop(&pool->workers[i]);
+	}
+}
+
+void worker_pool_join(struct worker_pool *pool)
+{
+	size_t i;
+
+	if (!pool || !pool->workers) {
+		return;
+	}
+
+	for (i = 0; i < pool->count; i++) {
+		worker_join(&pool->workers[i]);
+	}
+}
+
+void worker_pool_free(struct worker_pool *pool)
+{
+	size_t i;
+
+	if (!pool || !pool->workers) {
+		return;
+	}
+
+	for (i = 0; i < pool->count; i++) {
+		worker_free(&pool->workers[i]);
+	}
+
+	free(pool->workers);
+	memset(pool, 0, sizeof(*pool));
+}
+
+struct worker *worker_pool_next(struct worker_pool *pool)
+{
+	struct worker *w;
+
+	if (!pool || !pool->workers || pool->count == 0) {
+		return NULL;
+	}
+
+	w = &pool->workers[pool->next % pool->count];
+	pool->next++;
+
+	return w;
+}
\ No newline at end of file
diff --git a/src/worker_pool.h b/src/worker_pool.h
new file mode 100644
index 0000000..7f8bd05
--- /dev/null
+++ b/src/worker_pool.h
@@ -0,0 +1,22 @@
+#ifndef WORKER_POOL_H
+#define WORKER_POOL_H
+
+#include <stddef.h>
+
+#include "worker.h"
+
+struct worker_pool {
+	struct worker *workers;
+	size_t count;
+	size_t next;
+};
+
+int worker_pool_init(struct worker_pool *pool, size_t count);
+int worker_pool_start(struct worker_pool *pool);
+void worker_pool_stop(struct worker_pool *pool);
+void worker_pool_join(struct worker_pool *pool);
+void worker_pool_free(struct worker_pool *pool);
+
+struct worker *worker_pool_next(struct worker_pool *pool);
+
+#endif