penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit a85d9c71f0552d99ddf63550d8d39c8986097121

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-27T13:44:16Z
subjecttcp upstream support unix socket
commit a85d9c71f0552d99ddf63550d8d39c8986097121
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-27T13:44:16Z

    tcp upstream support unix socket
---
 file_conf.c    |  39 +++++++++++++++---
 klog.c         |  27 +++++++++++++
 klog.h         |  11 +++++
 route.h        |  21 +++++++---
 tcp_route.c    | 126 +++++++++++++++++++++++++++++++++++++++------------------
 tinyproxy.c    |   7 ++--
 tinyproxy.conf |  11 ++---
 udp_route.c    |  18 ++++-----
 8 files changed, 190 insertions(+), 70 deletions(-)

diff --git a/file_conf.c b/file_conf.c
index 08dc0cd..3a867e9 100644
--- a/file_conf.c
+++ b/file_conf.c
@@ -193,6 +193,37 @@ static inline void route_options_set_defaults(struct route_options *opts)
 	opts->connect_timeout_sec = ROUTE_DEFAULT_CONNECT_TIMEOUT_SEC;
 }
 
+static int parse_endpoint(const char *s, struct endpoint *ep)
+{
+	const char *path;
+
+	memset(ep, 0, sizeof(*ep));
+
+	if (strncmp(s, "unix:", 5) == 0) {
+		path = s + 5;
+
+		if (path[0] == '\0') {
+			return -1;
+		}
+
+		if (strlen(path) >= sizeof(ep->path)) {
+			return -1;
+		}
+
+		ep->kind = ENDPOINT_UNIX;
+		strcpy(ep->path, path);
+		return 0;
+	}
+
+	ep->kind = ENDPOINT_TCP;
+
+	if (split_host_port(s, ep->host, sizeof(ep->host), &ep->port) != 0) {
+		return -1;
+	}
+
+	return 0;
+}
+
 static int parse_route_line(char *line, struct route *route)
 {
 	char *fields[4] = {0};
@@ -217,15 +248,11 @@ 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) {
+	if (parse_endpoint(fields[0], &route->listen) != 0) {
 		return -1;
 	}
 
-	if (split_host_port(fields[1],
-	                    route->upstream_host, sizeof(route->upstream_host),
-	                    &route->upstream_port) != 0) {
+	if (parse_endpoint(fields[1], &route->upstream) != 0) {
 		return -1;
 	}
 
diff --git a/klog.c b/klog.c
index 28b6b55..e44bd2e 100644
--- a/klog.c
+++ b/klog.c
@@ -11,6 +11,8 @@
 #include <time.h>
 #endif
 
+#include "route.h"
+
 static int localtime_compat(const time_t *t, struct tm *out)
 {
 #ifdef _WIN32
@@ -55,6 +57,28 @@ static void print_quoted_value(const char *s)
 	fputc('"', stderr);
 }
 
+static void log_print_endpoint(FILE *out, const struct endpoint *ep)
+{
+	if (ep == NULL) {
+		fputs("<nil>", out);
+		return;
+	}
+
+	switch (ep->kind) {
+	case ENDPOINT_TCP:
+		fprintf(out, "%s:%u", ep->host, ep->port);
+		return;
+
+	case ENDPOINT_UNIX:
+		fprintf(out, "unix:%s", ep->path);
+		return;
+
+	default:
+		fputs("<unknown>", out);
+		return;
+	}
+}
+
 static void print_log_value(struct log_value value)
 {
 	switch (value.type) {
@@ -79,6 +103,9 @@ static void print_log_value(struct log_value value)
 	case LOG_VALUE_ULLONG:
 		fprintf(stderr, "%llu", value.v.ull);
 		break;
+	case LOG_VALUE_ENDPOINT:
+		log_print_endpoint(stderr, value.v.endpoint);
+		break;
 	default:
 		fputs("<invalid>", stderr);
 		break;
diff --git a/klog.h b/klog.h
index 92cdf93..5a74183 100644
--- a/klog.h
+++ b/klog.h
@@ -3,6 +3,8 @@
 
 #include <stdint.h>
 
+struct endpoint;
+
 enum log_value_type {
 	LOG_VALUE_STR = 1,
 	LOG_VALUE_INT,
@@ -11,6 +13,7 @@ enum log_value_type {
 	LOG_VALUE_ULONG,
 	LOG_VALUE_LLONG,
 	LOG_VALUE_ULLONG,
+	LOG_VALUE_ENDPOINT,
 };
 
 struct log_value {
@@ -23,6 +26,7 @@ struct log_value {
 		unsigned long ul;
 		long long ll;
 		unsigned long long ull;
+		const struct endpoint *endpoint;
 	} v;
 };
 
@@ -63,6 +67,11 @@ static inline struct log_value log_value_ullong(unsigned long long v)
 	return (struct log_value){ LOG_VALUE_ULLONG, { .ull = v } };
 }
 
+static inline struct log_value log_value_endpoint(const struct endpoint *v)
+{
+	return (struct log_value){ LOG_VALUE_ENDPOINT, { .endpoint = v } };
+}
+
 #define _LOGV(v) \
 	_Generic((v), \
 		char *: log_value_str, \
@@ -79,6 +88,8 @@ static inline struct log_value log_value_ullong(unsigned long long v)
 		unsigned long long: log_value_ullong \
 	)(v)
 
+#define _LOGV_ENDPOINT(v) log_value_endpoint(v)
+
 /*
  * Structured fields must be passed as:
  *
diff --git a/route.h b/route.h
index 500dee9..c755bb6 100644
--- a/route.h
+++ b/route.h
@@ -24,12 +24,23 @@ struct route_options {
 	int connect_timeout_sec;
 };
 
-struct route {
-	char listen_host[ROUTE_HOST_MAX];
-	uint16_t listen_port;
+enum endpoint_kind {
+	ENDPOINT_TCP,
+	ENDPOINT_UNIX,
+};
+
+struct endpoint {
+	enum endpoint_kind kind;
+
+	char host[256];
+	uint16_t port;
 
-	char upstream_host[ROUTE_HOST_MAX];
-	uint16_t upstream_port;
+	char path[108]; /* sockaddr_un sun_path limit on Linux */
+};
+
+struct route {
+	struct endpoint listen;
+	struct endpoint upstream;
 
 	enum proto proto;
 	struct route_options opts;
diff --git a/tcp_route.c b/tcp_route.c
index bf89054..aea2bab 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -9,6 +9,10 @@
 #include <stdlib.h>
 #include <string.h>
 
+#ifndef _WIN32
+#include <sys/un.h>
+#endif
+
 #include "klog.h"
 #include "file_conf.h"
 #include "compat_socket.h"
@@ -164,6 +168,70 @@ static void event_cb(struct bufferevent *bev, short events, void *arg) {
 	(void)bev;
 }
 
+static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
+{
+	if (ep == NULL) {
+		return -EINVAL;
+	}
+
+	switch (ep->kind) {
+	case ENDPOINT_TCP: {
+		struct sockaddr_in addr;
+
+		memset(&addr, 0, sizeof(addr));
+		addr.sin_family = AF_INET;
+		addr.sin_port = htons(ep->port);
+
+		if (inet_pton(AF_INET, ep->host, &addr.sin_addr) != 1) {
+			return -EINVAL;
+		}
+
+		if (bufferevent_socket_connect(
+			    bev,
+			    (struct sockaddr *)&addr,
+			    sizeof(addr)) < 0) {
+			return -errno;
+		}
+
+		return 0;
+	}
+
+	case ENDPOINT_UNIX:
+#ifdef _WIN32
+		return -ENOTSUP;
+#else
+	{
+		struct sockaddr_un addr;
+
+		memset(&addr, 0, sizeof(addr));
+		addr.sun_family = AF_UNIX;
+
+		if (ep->path[0] == '\0') {
+			return -EINVAL;
+		}
+
+		if (strlen(ep->path) >= sizeof(addr.sun_path)) {
+			return -ENAMETOOLONG;
+		}
+
+		strcpy(addr.sun_path, ep->path);
+
+		if (bufferevent_socket_connect(
+			    bev,
+			    (struct sockaddr *)&addr,
+			    sizeof(addr)) < 0) {
+			return -errno;
+		}
+
+		return 0;
+	}
+#endif
+
+	default:
+		return -ENOTSUP;
+	}
+}
+
 static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac) {
 	conn_t *conn = calloc(1, sizeof(*conn));
 	if (conn == NULL) {
@@ -197,32 +265,8 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
 	bufferevent_setwatermark(conn->client, EV_READ, 0, BEV_READ_HIGH_WATER);
 	bufferevent_setwatermark(conn->upstream, EV_READ, 0, BEV_READ_HIGH_WATER);
 
-	bufferevent_setcb(
-		conn->client,
-		pipe_read_cb,
-		pipe_write_cb,
-		event_cb,
-		conn
-	);
-
-	bufferevent_setcb(
-		conn->upstream,
-		pipe_read_cb,
-		pipe_write_cb,
-		event_cb,
-		conn
-	);
-
-	struct sockaddr_in upstream_addr;
-	memset(&upstream_addr, 0, sizeof(upstream_addr));
-	upstream_addr.sin_family = AF_INET;
-	upstream_addr.sin_port = htons(r->upstream_port);
-
-	if (inet_pton(AF_INET, r->upstream_host, &upstream_addr.sin_addr) != 1) {
-		LOG_ERROR("invalid upstream address");
-		free_conn(conn);
-		return;
-	}
+	bufferevent_setcb(conn->client, pipe_read_cb, pipe_write_cb, event_cb, conn);
+	bufferevent_setcb(conn->upstream, pipe_read_cb, pipe_write_cb, event_cb, conn);
 
 	/*
 	 * Do not read from the client yet.
@@ -233,23 +277,25 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
 	bufferevent_disable(conn->client, EV_READ);
 
 	set_connect_timeout(conn, r);
-	if (bufferevent_socket_connect(
-			conn->upstream,
-			(struct sockaddr *)&upstream_addr,
-			sizeof(upstream_addr)
-		) < 0) {
-		LOG_ERROR("upstream connect failed");
+
+	rc = connect_upstream(conn->upstream, &r->upstream);
+	if (rc < 0) {
+		LOG_ERROR("upstream connect failed",
+			"upstream", _LOGV_ENDPOINT(&r->upstream),
+			"err", _LOGV(strerror(-rc))
+		);
 		free_conn(conn);
 		return;
 	}
 
-	if (r->opts.keep_alive) {
+	if (r->opts.keep_alive && r->upstream.kind == ENDPOINT_TCP) {
 		evutil_socket_t upstream_fd = bufferevent_getfd(conn->upstream);
 
 		if (upstream_fd >= 0) {
 			int rc = set_socket_keepalive(upstream_fd, r);
 			if (rc < 0) {
 				LOG_WARN("failed to enable upstream TCP keepalive",
+					"upstream", _LOGV_ENDPOINT(&r->upstream),
 					"err", _LOGV(strerror(-rc))
 				);
 			}
@@ -351,10 +397,12 @@ int start_tcp_route(
 	struct sockaddr_in listen_addr;
 	memset(&listen_addr, 0, sizeof(listen_addr));
 	listen_addr.sin_family = AF_INET;
-	listen_addr.sin_port = htons(r->listen_port);
+	listen_addr.sin_port = htons(r->listen.port);
 
-	if (inet_pton(AF_INET, r->listen_host, &listen_addr.sin_addr) != 1) {
-		LOG_ERROR("invalid listen address", "listen_host", _LOGV(r->listen_host));
+	if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
+		LOG_ERROR("invalid listen address",
+			"listen", _LOGV_ENDPOINT(&r->listen)
+		);
 		return -EINVAL;
 	}
 
@@ -391,10 +439,8 @@ int start_tcp_route(
 
 	LOG_INFO("route started",
 		"line", _LOGV(r->line_no),
-		"listen_host", _LOGV(r->listen_host),
-		"listen_port", _LOGV(r->listen_port),
-		"upstream_host", _LOGV(r->upstream_host),
-		"upstream_port", _LOGV(r->upstream_port),
+		"listen", _LOGV_ENDPOINT(&r->listen),
+		"upstream", _LOGV_ENDPOINT(&r->upstream),
 		"options", _LOGV(opts[0] ? opts : "")
 	);
 
diff --git a/tinyproxy.c b/tinyproxy.c
index dfc8381..1d2db66 100644
--- a/tinyproxy.c
+++ b/tinyproxy.c
@@ -156,10 +156,9 @@ int main(int argc, char **argv)
 		if (rc != 0) {
 			LOG_ERROR("failed to start route",
 				"line", _LOGV(r->line_no),
-				"listen_host", _LOGV(r->listen_host),
-				"listen_port", _LOGV(r->listen_port),
-				"upstream_host", _LOGV(r->upstream_host),
-				"upstream_port", _LOGV(r->upstream_port));
+				"listen", _LOGV_ENDPOINT(&r->listen),
+				"upstream", _LOGV_ENDPOINT(&r->upstream)
+			);
 			goto out;
 		}
 	}
diff --git a/tinyproxy.conf b/tinyproxy.conf
index f7a3620..ff974fe 100644
--- a/tinyproxy.conf
+++ b/tinyproxy.conf
@@ -1,5 +1,6 @@
-# listen      upstream        proto  option
-:80           10.0.1.1:80     tcp
-: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
+# listen      upstream            proto  option
+:80           10.0.1.1:80         tcp
+: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
+:12990        unix:/tmp/test.sock tcp
diff --git a/udp_route.c b/udp_route.c
index c3e6af6..f6d777b 100644
--- a/udp_route.c
+++ b/udp_route.c
@@ -206,11 +206,11 @@ static struct udp_client *create_udp_client(
 	memset(&upstream_addr, 0, sizeof(upstream_addr));
 
 	upstream_addr.sin_family = AF_INET;
-	upstream_addr.sin_port = htons(r->upstream_port);
+	upstream_addr.sin_port = htons(r->upstream.port);
 
-	if (inet_pton(AF_INET, r->upstream_host, &upstream_addr.sin_addr) != 1) {
+	if (inet_pton(AF_INET, r->upstream.host, &upstream_addr.sin_addr) != 1) {
 		LOG_ERROR("invalid udp upstream address",
-			"upstream_host", _LOGV(r->upstream_host)
+			"upstream", _LOGV_ENDPOINT(&r->upstream)
 		);
 		free_udp_client(c);
 		return NULL;
@@ -385,11 +385,11 @@ int start_udp_route(
 	memset(&listen_addr, 0, sizeof(listen_addr));
 
 	listen_addr.sin_family = AF_INET;
-	listen_addr.sin_port = htons(r->listen_port);
+	listen_addr.sin_port = htons(r->listen.port);
 
-	if (inet_pton(AF_INET, r->listen_host, &listen_addr.sin_addr) != 1) {
+	if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
 		LOG_ERROR("invalid udp listen address",
-			"listen_host", _LOGV(r->listen_host)
+			"listen", _LOGV_ENDPOINT(&r->listen)
 		);
 		return -EINVAL;
 	}
@@ -494,10 +494,8 @@ int start_udp_route(
 
 	LOG_INFO("udp route started",
 		"line", _LOGV(r->line_no),
-		"listen_host", _LOGV(r->listen_host),
-		"listen_port", _LOGV(r->listen_port),
-		"upstream_host", _LOGV(r->upstream_host),
-		"upstream_port", _LOGV(r->upstream_port),
+		"listen", _LOGV_ENDPOINT(&r->listen),
+		"upstream", _LOGV_ENDPOINT(&r->upstream),
 		"options", _LOGV(opts[0] ? opts : "")
 	);