penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit ffda9e00ba5cdb289068ddcd79b9740875d917ab

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-26T19:44:34Z
subjectAdded keep_alive, connect_timeou, idle_timeout options
commit ffda9e00ba5cdb289068ddcd79b9740875d917ab
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-26T19:44:34Z

    Added keep_alive, connect_timeou, idle_timeout options
---
 file_conf.c      | 146 +++++++++++++++++++++++++++++++++++++++++++++++--------
 proxy_proto_v2.h |   2 +
 route.c          |  44 ++++++++++++++---
 route.h          |  25 ++++++++--
 tcp_route.c      |   4 +-
 tinyproxy.conf   |   4 +-
 udp_route.c      |   7 ++-
 7 files changed, 192 insertions(+), 40 deletions(-)

diff --git a/file_conf.c b/file_conf.c
index 50a52b8..08dc0cd 100644
--- a/file_conf.c
+++ b/file_conf.c
@@ -4,6 +4,7 @@
 #include <stdbool.h>
 #include <ctype.h>
 #include <errno.h>
+#include <limits.h>
 
 #include "klog.h"
 #include "file_conf.h"
@@ -90,19 +91,123 @@ static int parse_proto(const char *s, enum proto *out)
 	return -1;
 }
 
+static int parse_positive_int(const char *s, int *out)
+{
+	char *end = NULL;
+	long v;
+
+	if (s == NULL || *s == '\0') {
+		return -EINVAL;
+	}
+
+	errno = 0;
+	v = strtol(s, &end, 10);
+	if (errno != 0 || end == s || *end != '\0') {
+		return -EINVAL;
+	}
+
+	if (v <= 0 || v > INT_MAX) {
+		return -ERANGE;
+	}
+
+	*out = (int)v;
+	return 0;
+}
+
+static int parse_route_options(char *s, struct route_options *opts)
+{
+	char *saveptr = NULL;
+	char *tok;
+
+	if (s == NULL || *s == '\0') {
+		return 0;
+	}
+
+#define PARSE_INT_OPT(name, field) do { \
+		if (strcmp(key, name) == 0) { \
+			int rc = parse_positive_int(value, &opts->field); \
+			if (rc != 0) { \
+				return rc; \
+			} \
+			goto next_option; \
+		} \
+	} while (0)
+
+#define PARSE_BOOL_OPT(name, field) do { \
+		if (strcmp(tok, name) == 0) { \
+			opts->field = true; \
+			goto next_option; \
+		} \
+	} while (0)
+
+	for (tok = strtok_r(s, ",", &saveptr);
+	     tok != NULL;
+	     tok = strtok_r(NULL, ",", &saveptr)) {
+		char *eq;
+
+		tok = trim(tok);
+		if (*tok == '\0') {
+			return -EINVAL;
+		}
+
+		eq = strchr(tok, '=');
+
+		if (eq != NULL) {
+			const char *key;
+			const char *value;
+
+			*eq = '\0';
+			key = trim(tok);
+			value = trim(eq + 1);
+
+			if (*key == '\0' || *value == '\0') {
+				return -EINVAL;
+			}
+
+			PARSE_INT_OPT("idle_timeout", idle_timeout_sec);
+			PARSE_INT_OPT("connect_timeout", connect_timeout_sec);
+
+			return -EINVAL;
+		}
+
+		PARSE_BOOL_OPT("proxy_v2", proxy_v2);
+		PARSE_BOOL_OPT("keep_alive", keep_alive);
+
+		return -EINVAL;
+
+next_option:
+		continue;
+	}
+
+#undef PARSE_BOOL_OPT
+#undef PARSE_INT_OPT
+
+	return 0;
+}
+
+static inline void route_options_set_defaults(struct route_options *opts)
+{
+	opts->proxy_v2 = false;
+	opts->keep_alive = false;
+	opts->idle_timeout_sec = ROUTE_DEFAULT_IDLE_TIMEOUT_SEC;
+	opts->connect_timeout_sec = ROUTE_DEFAULT_CONNECT_TIMEOUT_SEC;
+}
+
 static int parse_route_line(char *line, struct route *route)
 {
 	char *fields[4] = {0};
 	size_t count = 0;
+	char *saveptr = NULL;
+	char *tok;
 
-	char *tok = strtok(line, " \t");
-	while (tok && count < 4) {
-		fields[count++] = tok;
-		tok = strtok(NULL, " \t");
-	}
+	tok = strtok_r(line, " \t\r\n", &saveptr);
+	while (tok != NULL) {
+		if (count >= 4) {
+			return -1; // too many fields
+		}
 
-	if (tok != NULL) {
-		return -1; // too many fields
+		fields[count++] = tok;
+		tok = strtok_r(NULL, " \t\r\n", &saveptr);
 	}
 
 	if (count < 3) {
@@ -110,16 +215,17 @@ static int parse_route_line(char *line, struct route *route)
 	}
 
 	memset(route, 0, sizeof(*route));
+	route_options_set_defaults(&route->opts);
 
 	if (split_host_port(fields[0],
-						route->listen_host, sizeof(route->listen_host),
-						&route->listen_port) != 0) {
+	                    route->listen_host, sizeof(route->listen_host),
+	                    &route->listen_port) != 0) {
 		return -1;
 	}
 
 	if (split_host_port(fields[1],
-						route->upstream_host, sizeof(route->upstream_host),
-						&route->upstream_port) != 0) {
+	                    route->upstream_host, sizeof(route->upstream_host),
+	                    &route->upstream_port) != 0) {
 		return -1;
 	}
 
@@ -127,14 +233,8 @@ static int parse_route_line(char *line, struct route *route)
 		return -1;
 	}
 
-	route->send_proxy_v2 = false;
-
-	if (count == 4) {
-		if (strcmp(fields[3], "proxy_v2") != 0) {
-			return -1;
-		}
-
-		route->send_proxy_v2 = true;
+	if (count == 4 && parse_route_options(fields[3], &route->opts) != 0) {
+		return -1;
 	}
 
 	return 0;
@@ -193,9 +293,15 @@ int load_routes_from_file(const char *path, struct route **routes_out, size_t *c
 			cap = new_cap;
 		}
 
+		char original[MAX_LINE_LEN];
+		snprintf(original, sizeof(original), "%s", line);
+
 		struct route *r = &routes[count];
 		if (parse_route_line(line, r) != 0) {
-			LOG_ERROR("invalid route config", "path", _LOGV(path), "line", _LOGV(line_no));
+			LOG_ERROR("invalid route config",
+					"path", _LOGV(path),
+					"line", _LOGV(line_no),
+					"text", _LOGV(original));
 			fclose(fp);
 			free(routes);
 			return -EINVAL;
diff --git a/proxy_proto_v2.h b/proxy_proto_v2.h
index 03e3435..9a2e579 100644
--- a/proxy_proto_v2.h
+++ b/proxy_proto_v2.h
@@ -12,6 +12,8 @@
 #define PP2_TRANS_STREAM	  0x01
 #define PP2_TRANS_DGRAM	   0x02
 
+struct bufferevent;
+
 int proxy_v2_build(
 	unsigned char *buf,
 	size_t buf_len,
diff --git a/route.c b/route.c
index eb39569..83a069c 100644
--- a/route.c
+++ b/route.c
@@ -1,23 +1,53 @@
 #include <stdio.h>
 #include "route.h"
 
-void route_options_str(const struct route *r, char *buf, size_t buflen)
+void route_options_str(const struct route_options *opts, char *buf, size_t buflen)
 {
 	size_t n = 0;
 
+	if (buflen == 0) {
+		return;
+	}
+
 	buf[0] = '\0';
 
-#define ADD_OPT(name) do { \
-		n += snprintf(buf + n, buflen - n, "%s%s", n == 0 ? "" : ", ", name); \
+#define ADD_FMT(fmt, ...) do { \
+		int written; \
 		if (n >= buflen) { \
 			buf[buflen - 1] = '\0'; \
 			return; \
 		} \
+		written = snprintf( \
+			buf + n, \
+			buflen - n, \
+			"%s" fmt, \
+			n == 0 ? "" : ", ", \
+			__VA_ARGS__ \
+		); \
+		if (written < 0) { \
+			buf[buflen - 1] = '\0'; \
+			return; \
+		} \
+		if ((size_t)written >= buflen - n) { \
+			n = buflen - 1; \
+			buf[n] = '\0'; \
+			return; \
+		} \
+		n += (size_t)written; \
 	} while (0)
 
-	if (r->send_proxy_v2) {
-		ADD_OPT("proxy_v2");
-	}
+#define ADD_BOOL(name, enabled) \
+	ADD_FMT("%s=%s", name, (enabled) ? "true" : "false")
+
+#define ADD_INT(name, value) \
+	ADD_FMT("%s=%d", name, value)
+
+	ADD_BOOL("proxy_v2", opts->proxy_v2);
+	ADD_INT("idle_timeout", opts->idle_timeout_sec);
+	ADD_INT("connect_timeout", opts->connect_timeout_sec);
+	ADD_BOOL("keep_alive", opts->keep_alive);
 
-#undef ADD_OPT
+#undef ADD_INT
+#undef ADD_BOOL
+#undef ADD_FMT
 }
diff --git a/route.h b/route.h
index cbe0502..500dee9 100644
--- a/route.h
+++ b/route.h
@@ -1,23 +1,38 @@
 #ifndef ROUTE_H
 #define ROUTE_H
 
-#include <event2/bufferevent.h>
 #include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct event_base;
+
+#define ROUTE_HOST_MAX 256
+#define ROUTE_DEFAULT_IDLE_TIMEOUT_SEC 60
+#define ROUTE_DEFAULT_CONNECT_TIMEOUT_SEC 5
 
 enum proto {
 	PROTO_TCP,
 	PROTO_UDP,
 };
 
+struct route_options {
+	bool proxy_v2;
+	bool keep_alive;
+
+	int idle_timeout_sec;
+	int connect_timeout_sec;
+};
+
 struct route {
-	char listen_host[64];
+	char listen_host[ROUTE_HOST_MAX];
 	uint16_t listen_port;
 
-	char upstream_host[64];
+	char upstream_host[ROUTE_HOST_MAX];
 	uint16_t upstream_port;
 
 	enum proto proto;
-	bool send_proxy_v2;
+	struct route_options opts;
 
 	unsigned int line_no;
 };
@@ -27,6 +42,6 @@ struct worker {
 	size_t id;
 };
 
-void route_options_str(const struct route *r, char *buf, size_t buflen);
+void route_options_str(const struct route_options *opts, char *buf, size_t buflen);
 
 #endif
diff --git a/tcp_route.c b/tcp_route.c
index 8f87227..c84184d 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -165,7 +165,7 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
 		return;
 	}
 
-	if (r->send_proxy_v2) {
+	if (r->opts.proxy_v2) {
 		struct sockaddr_in local_addr;
 		socklen_t local_len = sizeof(local_addr);
 
@@ -295,7 +295,7 @@ int start_tcp_route(
 
 	char opts[128];
 
-	route_options_str(r, opts, sizeof(opts));
+	route_options_str(&r->opts, opts, sizeof(opts));
 
 	LOG_INFO("route started",
 		"line", _LOGV(r->line_no),
diff --git a/tinyproxy.conf b/tinyproxy.conf
index c7e7307..f7a3620 100644
--- a/tinyproxy.conf
+++ b/tinyproxy.conf
@@ -1,5 +1,5 @@
 # listen      upstream        proto  option
 :80           10.0.1.1:80     tcp
-:443          10.0.1.1:443    tcp    proxy_v2
-:12345        10.0.1.1:12345  udp
+:443          10.0.1.1:443    tcp    proxy_v2,keep_alive
+:12345        10.0.1.1:12345  udp    idle_timeout=10,connect_timeout=17
 :19132        10.0.1.1:19132  udp    proxy_v2
diff --git a/udp_route.c b/udp_route.c
index 4a4fc6e..c3e6af6 100644
--- a/udp_route.c
+++ b/udp_route.c
@@ -15,7 +15,6 @@
 #include "udp_route.h"
 
 #define UDP_MAX_PACKET 65535
-#define UDP_CLIENT_IDLE_TIMEOUT_SEC 60
 
 struct udp_route_ctx;
 
@@ -89,7 +88,7 @@ static void cleanup_idle_udp_clients(struct udp_route_ctx *ctx)
 	while (*pp != NULL) {
 		struct udp_client *c = *pp;
 
-		if (now - c->last_seen <= UDP_CLIENT_IDLE_TIMEOUT_SEC) {
+		if (now - c->last_seen <= ctx->route->opts.idle_timeout_sec) {
 			pp = &c->next;
 			continue;
 		}
@@ -263,7 +262,7 @@ static int send_udp_payload_to_upstream(
 	struct udp_route_ctx *ctx = c->ctx;
 	const struct route *r = ctx->route;
 
-	if (!r->send_proxy_v2) {
+	if (!r->opts.proxy_v2) {
 		ssize_t sent = send(c->fd, (const char *)payload, payload_len, 0);
 		if (sent < 0) {
 			return -EVUTIL_SOCKET_ERROR();
@@ -491,7 +490,7 @@ int start_udp_route(
 
 	char opts[128];
 
-	route_options_str(r, opts, sizeof(opts));
+	route_options_str(&r->opts, opts, sizeof(opts));
 
 	LOG_INFO("udp route started",
 		"line", _LOGV(r->line_no),