penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 25d79ad7184c2e51d840a50427590906803ea037

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-29T22:29:54Z
subjectUpdate fuzzer, removed default config
commit 25d79ad7184c2e51d840a50427590906803ea037
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-29T22:29:54Z

    Update fuzzer, removed default config
---
 Dockerfile                          |   4 +-
 examples/tinyproxy.conf             |  37 +++++++
 fuzz/Makefile                       |  20 ++--
 fuzz/README.md                      |   8 ++
 fuzz/fuzz_file_conf.c               | 112 +++------------------
 fuzz/fuzz_proxy_v2_build.c          |   2 +-
 fuzz/seeds/file_conf/tinyproxy.conf |   3 +
 src/file_conf.c                     | 190 ++++++++++++++++++++++++++++++------
 src/file_conf.h                     |  10 +-
 src/route.c                         |   1 +
 src/tinyproxy.c                     |  15 +--
 tinyproxy.conf                      |  38 --------
 12 files changed, 249 insertions(+), 191 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index a63b63a..deeaf3f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,12 +18,10 @@ RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
 COPY tests ./tests
 RUN make test STATIC=1
 
-RUN cp /build/tinyproxy.conf /etc/ && cp /build/bin/tinyproxy /usr/bin/
+RUN cp /build/bin/tinyproxy /usr/bin/
 
 FROM scratch
 
 COPY --from=build /build/bin/tinyproxy /usr/bin/tinyproxy
-COPY --from=build /build/tinyproxy.conf /etc/tinyproxy.conf
 
 ENTRYPOINT ["/usr/bin/tinyproxy"]
-CMD ["-c", "/etc/tinyproxy.conf"]
diff --git a/examples/tinyproxy.conf b/examples/tinyproxy.conf
new file mode 100644
index 0000000..f556b7d
--- /dev/null
+++ b/examples/tinyproxy.conf
@@ -0,0 +1,37 @@
+# See man 5 tinyproxy
+# TCP stream forwarding.
+listen tcp :80     tcp 10.0.1.1:80
+listen tcp :443    tcp 10.0.1.1:443    proxy_v2,keep_alive
+
+# TCP # listener to UNIX stream backend.
+listen tcp  :12990                unix /tmp/test.sock
+
+# UDP datagram forwarding.
+listen udp :12345  udp 10.0.1.1:12345  idle_timeout=10,connect_timeout=17
+listen udp :19132  udp 10.0.1.1:19132
+
+# UDP # listener to UNIX datagram backend.
+listen udp :13000  unix-dgram /tmp/testd.sock
+
+# Builtin TCP test backends.
+listen tcp :12995  builtin client_addr
+listen tcp :12996  builtin hang         idle_timeout=3,connect_timeout=1
+listen tcp :12997  builtin discard
+listen tcp :12998  builtin close
+listen tcp :8080   builtin http-ok
+
+# Builtin UDP test backends.
+listen udp :12995  builtin client_addr
+listen udp :12996  builtin hang
+listen udp :12997  builtin discard
+listen udp :12998  builtin close
+listen udp :8080   builtin http-ok
+
+# Static file TCP backends.
+listen tcp :14000  file examples/http-response.txt
+
+listen unix /tmp/tinyproxy-chain-1.sock file examples/http-response.txt
+listen tcp  :13002                      unix /tmp/tinyproxy-chain-1.sock
+listen tcp  :13003                      tcp  127.0.0.1:13002
+listen unix /tmp/tinyproxy-chain-4.sock tcp  127.0.0.1:13003
+listen tcp  :14001                      unix /tmp/tinyproxy-chain-4.sock
diff --git a/fuzz/Makefile b/fuzz/Makefile
index c6e0acb..fbe7418 100644
--- a/fuzz/Makefile
+++ b/fuzz/Makefile
@@ -31,25 +31,17 @@ all: $(BINS)
 $(OUT):
 	mkdir -p $(OUT)
 
-$(OUT)/fuzz_proxy_v2_build: \
-	fuzz_proxy_v2_build.c \
-	$(SRC_DIR)/proxy_proto_v2.c \
-	| $(OUT)
+$(OUT)/fuzz_proxy_v2_build: fuzz_proxy_v2_build.c $(PROJECT_SRCS)
 	$(CC) $(CFLAGS) $(FUZZ_CFLAGS) $(CPPFLAGS) \
-		$(SRC_DIR)/proxy_proto_v2.c \
-		fuzz_proxy_v2_build.c \
+		-I$(SRC_DIR) \
+		$^ \
 		-o $@ \
 		$(LDFLAGS) $(FUZZ_LDFLAGS) $(LDLIBS)
 
-$(OUT)/fuzz_file_conf: \
-	fuzz_file_conf.c \
-	$(SRC_DIR)/file_conf.c \
-	$(SRC_DIR)/klog.c \
-	| $(OUT)
+$(OUT)/fuzz_file_conf: fuzz_file_conf.c $(PROJECT_SRCS)
 	$(CC) $(CFLAGS) $(FUZZ_CFLAGS) $(CPPFLAGS) \
-		$(SRC_DIR)/file_conf.c \
-		$(SRC_DIR)/klog.c \
-		fuzz_file_conf.c \
+		-I$(SRC_DIR) \
+		$^ \
 		-o $@ \
 		$(LDFLAGS) $(FUZZ_LDFLAGS) $(LDLIBS)
 
diff --git a/fuzz/README.md b/fuzz/README.md
new file mode 100755
index 0000000..7fc1adf
--- /dev/null
+++ b/fuzz/README.md
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+macos
+
+Fuzz for 8 hours
+```
+LIBEVENT_SRC=libevent2/ MAX_TOTAL_TIME=28800 MAX_LEN=65536 CC="$(brew --prefix llvm)/bin/clang" caffeinate make -C fuzz run-file_conf
+```
diff --git a/fuzz/fuzz_file_conf.c b/fuzz/fuzz_file_conf.c
index 24351b8..93cb1d4 100644
--- a/fuzz/fuzz_file_conf.c
+++ b/fuzz/fuzz_file_conf.c
@@ -5,119 +5,33 @@
 #include <unistd.h>
 #include <errno.h>
 
+#include "route.h"
 #include "file_conf.h"
 
-static void check_endpoint(const struct endpoint *ep)
-{
-	switch (ep->kind) {
-	case ENDPOINT_INET:
-		if (ep->port == 0) {
-			abort();
-		}
-		if (ep->host[0] == '\0') {
-			abort();
-		}
-		break;
-
-	case ENDPOINT_UNIX:
-	case ENDPOINT_UNIX_DGRAM:
-		if (ep->path[0] == '\0') {
-			abort();
-		}
-		break;
-
-	default:
-		abort();
-	}
-}
-
-static void check_route(const struct route *r)
-{
-	check_endpoint(&r->listen);
-	check_endpoint(&r->upstream);
-
-	switch (r->proto) {
-	case PROTO_TCP:
-	case PROTO_UDP:
-		break;
-	default:
-		abort();
-	}
-
-	if (r->line_no == 0) {
-		abort();
-	}
-
-	if (r->opts.idle_timeout_sec <= 0) {
-		abort();
-	}
-
-	if (r->opts.connect_timeout_sec <= 0) {
-		abort();
-	}
-}
-
 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
-	/*
-	 * Keep the fuzzer fast. file_conf.c has MAX_LINE_LEN=512, so this is
-	 * already enough to exercise normal, multiline, and overlong cases.
-	 */
-	if (size > 8192) {
-		return 0;
-	}
-
-	char path[] = "/tmp/tinyproxy-file-conf-fuzz-XXXXXX";
-	int fd = mkstemp(path);
-	if (fd < 0) {
-		return 0;
-	}
-
-	size_t off = 0;
-	while (off < size) {
-		ssize_t n = write(fd, data + off, size - off);
-		if (n < 0) {
-			if (errno == EINTR) {
-				continue;
-			}
-
-			close(fd);
-			unlink(path);
-			return 0;
-		}
-
-		if (n == 0) {
-			break;
-		}
-
-		off += (size_t)n;
-	}
-
-	close(fd);
-
 	struct route *routes = NULL;
 	size_t count = 0;
 
-	int rc = load_routes_from_file(path, &routes, &count);
-
-	unlink(path);
+	int rc = load_routes_from_text(
+		"fuzzer",
+		(const char *)data,
+		size,
+		&routes,
+		&count
+	);
 
 	if (rc == 0) {
 		if (count > 0 && routes == NULL) {
 			abort();
 		}
 
-		for (size_t i = 0; i < count; i++) {
-			check_route(&routes[i]);
-		}
+		free(routes);
+		return 0;
+	}
 
-		free_routes(routes);
-	} else {
-		/*
-		 * On failure, load_routes_from_file() should not transfer ownership.
-		 * Keep this loose because callers should not rely on output values
-		 * after error unless the API explicitly promises that.
-		 */
+	if (routes != NULL) {
+		abort();
 	}
 
 	return 0;
diff --git a/fuzz/fuzz_proxy_v2_build.c b/fuzz/fuzz_proxy_v2_build.c
index 2861e51..dc73f59 100644
--- a/fuzz/fuzz_proxy_v2_build.c
+++ b/fuzz/fuzz_proxy_v2_build.c
@@ -4,7 +4,7 @@
 #include <sys/socket.h>
 #include <netinet/in.h>
 
-#include "../src/proxy_proto_v2.h"
+#include "proxy_proto_v2.h"
 
 static uint16_t read_u16(const uint8_t *p)
 {
diff --git a/fuzz/seeds/file_conf/tinyproxy.conf b/fuzz/seeds/file_conf/tinyproxy.conf
new file mode 100644
index 0000000..3c762a9
--- /dev/null
+++ b/fuzz/seeds/file_conf/tinyproxy.conf
@@ -0,0 +1,3 @@
+# Fuzz seed valid conf
+listen tcp :80     tcp 10.0.1.1:80
+listen tcp :443    tcp 10.0.1.1:443    proxy_v2,keep_alive
diff --git a/src/file_conf.c b/src/file_conf.c
index 2b3f8e1..1d193ba 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -7,8 +7,8 @@
 #include <limits.h>
 
 #include "klog.h"
-#include "file_conf.h"
 #include "endpoint.h"
+#include "file_conf.h"
 
 #define MAX_LINE_LEN 512
 #define MAX_ROUTE_FIELDS 32
@@ -47,6 +47,43 @@ static int parse_port(const char *s, uint16_t *out)
 	return 0;
 }
 
+static bool same_listen_endpoint(
+	const struct endpoint *a,
+	const struct endpoint *b
+) {
+	if (a->proto != b->proto) {
+		return false;
+	}
+
+	switch (a->proto) {
+	case PROTO_TCP:
+	case PROTO_UDP:
+		return strcmp(a->host, b->host) == 0 &&
+			a->port == b->port;
+
+	case PROTO_UNIX_STREAM:
+	case PROTO_UNIX_DGRAM:
+		return strcmp(a->path, b->path) == 0;
+
+	default:
+		return false;
+	}
+}
+
+static int find_conflicting_route(
+	const struct route *routes,
+	size_t count,
+	const struct route *new_route
+) {
+	for (size_t i = 0; i < count; i++) {
+		if (same_listen_endpoint(&routes[i].listen, &new_route->listen)) {
+			return (int)i;
+		}
+	}
+
+	return -1;
+}
+
 static int split_host_port(const char *input,
 						   char *host, size_t host_len,
 						   uint16_t *port)
@@ -394,37 +431,65 @@ int parse_route_line(char *line, struct route *route)
 	return 0;
 }
 
-int load_routes_from_file(const char *path, struct route **routes_out, size_t *count_out)
-{
-	FILE *fp = fopen(path, "r");
-	if (!fp) {
-		return -errno;
-	}
-
+int load_routes_from_text(
+	const char *name,
+	const char *data,
+	size_t data_len,
+	struct route **routes_out,
+	size_t *count_out
+) {
 	struct route *routes = NULL;
 	size_t count = 0;
 	size_t cap = 0;
 
-	char buf[MAX_LINE_LEN];
+	size_t pos = 0;
 	unsigned int line_no = 0;
 
-	while (fgets(buf, sizeof(buf), fp)) {
+	*routes_out = NULL;
+	*count_out = 0;
+
+	while (pos < data_len) {
 		line_no++;
 
-		// Reject overlong lines.
-		if (!strchr(buf, '\n') && !feof(fp)) {
-			fclose(fp);
+		const char *line_start = data + pos;
+		const void *nlp = memchr(line_start, '\n', data_len - pos);
+
+		size_t raw_len;
+		if (nlp) {
+			raw_len = (const char *)nlp - line_start;
+			pos += raw_len + 1;
+		} else {
+			raw_len = data_len - pos;
+			pos = data_len;
+		}
+
+		/* Handle CRLF input nicely. */
+		if (raw_len > 0 && line_start[raw_len - 1] == '\r') {
+			raw_len--;
+		}
+
+		/*
+		 * Need room for NUL.
+		 *
+		 * This keeps the same basic safety property as the fgets()
+		 * version: no parser call ever sees an overlarge line.
+		 */
+		if (raw_len >= MAX_LINE_LEN) {
 			free(routes);
 			return -E2BIG;
 		}
 
+		char buf[MAX_LINE_LEN];
+		memcpy(buf, line_start, raw_len);
+		buf[raw_len] = '\0';
+
 		char *line = trim(buf);
 
 		if (*line == '\0' || *line == '#') {
 			continue;
 		}
 
-		// Strip inline comments.
+		/* Strip inline comments. */
 		char *hash = strchr(line, '#');
 		if (hash) {
 			*hash = '\0';
@@ -436,9 +501,14 @@ int load_routes_from_file(const char *path, struct route **routes_out, size_t *c
 
 		if (count == cap) {
 			size_t new_cap = cap ? cap * 2 : 8;
+
+			if (new_cap > SIZE_MAX / sizeof(*routes)) {
+				free(routes);
+				return -EOVERFLOW;
+			}
+
 			struct route *new_routes = realloc(routes, new_cap * sizeof(*routes));
 			if (!new_routes) {
-				fclose(fp);
 				free(routes);
 				return -ENOMEM;
 			}
@@ -453,34 +523,96 @@ int load_routes_from_file(const char *path, struct route **routes_out, size_t *c
 		struct route *r = &routes[count];
 		if (parse_route_line(line, r) != 0) {
 			LOG_ERROR("invalid route config",
-					"path", _LOGV(path),
-					"line", _LOGV(line_no),
-					"text", _LOGV(original));
-			fclose(fp);
+				"name", _LOGV(name ? name : "<memory>"),
+				"line", _LOGV(line_no),
+				"text", _LOGV(original)
+			);
+
 			free(routes);
 			return -EINVAL;
 		}
 
 		r->line_no = line_no;
+
+		int conflict_idx = find_conflicting_route(routes, count, r);
+		if (conflict_idx >= 0) {
+			const struct route *old = &routes[conflict_idx];
+
+			LOG_ERROR("conflicting route config",
+				"name", _LOGV(name ? name : "<memory>"),
+				"line", _LOGV(line_no),
+				"conflicts_with_line", _LOGV(old->line_no),
+				"listen", _LOGV_ENDPOINT(&r->listen),
+				"text", _LOGV(original)
+			);
+
+			free(routes);
+			return -EADDRINUSE;
+		}
+
 		count++;
 	}
 
-	if (ferror(fp)) {
+	*routes_out = routes;
+	*count_out = count;
+
+	return 0;
+}
+
+int load_routes_from_file(const char *path, struct route **routes_out, size_t *count_out)
+{
+	FILE *fp = fopen(path, "rb");
+	if (!fp) {
+		return -errno;
+	}
+
+	if (fseek(fp, 0, SEEK_END) != 0) {
 		int err = errno;
 		fclose(fp);
-		free(routes);
 		return -err;
 	}
 
-	fclose(fp);
+	long file_size = ftell(fp);
+	if (file_size < 0) {
+		int err = errno;
+		fclose(fp);
+		return -err;
+	}
 
-	*routes_out = routes;
-	*count_out = count;
+	if (fseek(fp, 0, SEEK_SET) != 0) {
+		int err = errno;
+		fclose(fp);
+		return -err;
+	}
 
-	return 0;
-}
+	char *data = NULL;
 
-void free_routes(struct route *routes)
-{
-	free(routes);
+	if (file_size > 0) {
+		data = malloc((size_t)file_size);
+		if (!data) {
+			fclose(fp);
+			return -ENOMEM;
+		}
+
+		size_t nread = fread(data, 1, (size_t)file_size, fp);
+		if (nread != (size_t)file_size) {
+			int err = ferror(fp) ? errno : EIO;
+			free(data);
+			fclose(fp);
+			return -err;
+		}
+	}
+
+	fclose(fp);
+
+	int rc = load_routes_from_text(
+		path,
+		data ? data : "",
+		(size_t)file_size,
+		routes_out,
+		count_out
+	);
+
+	free(data);
+	return rc;
 }
diff --git a/src/file_conf.h b/src/file_conf.h
index 5b7baad..fd0bffe 100644
--- a/src/file_conf.h
+++ b/src/file_conf.h
@@ -7,7 +7,15 @@
 #include "route.h"
 
 int parse_route_line(char *line, struct route *route);
+
+int load_routes_from_text(
+	const char *name,
+	const char *data,
+	size_t data_len,
+	struct route **routes_out,
+	size_t *count_out
+);
+
 int load_routes_from_file(const char *path, struct route **routes_out, size_t *count_out);
-void free_routes(struct route *routes);
 
 #endif
diff --git a/src/route.c b/src/route.c
index aa368b9..fa517c0 100644
--- a/src/route.c
+++ b/src/route.c
@@ -1,4 +1,5 @@
 #include <stdio.h>
+#include <stdlib.h>
 #include <errno.h>
 
 #include "worker.h"
diff --git a/src/tinyproxy.c b/src/tinyproxy.c
index 1486884..f1731c9 100644
--- a/src/tinyproxy.c
+++ b/src/tinyproxy.c
@@ -46,13 +46,10 @@ int main(int argc, char **argv)
 {
 	prevent_socket_write_from_killing_process();
 
-	const char *conf_path = "tinyproxy.conf";
 	int opt;
 	int exit_code = 1;
 	int rc;
 
-	int conf_path_set = 0;
-
 	const char **inline_routes = NULL;
 	size_t inline_route_count = 0;
 	size_t inline_route_cap = 0;
@@ -61,6 +58,7 @@ int main(int argc, char **argv)
 	int wsa_started = 0;
 #endif
 
+	const char *conf_path = NULL;
 	struct route *routes = NULL;
 	size_t route_count = 0;
 
@@ -80,7 +78,6 @@ int main(int argc, char **argv)
 					return 2;
 				}
 				conf_path = optarg;
-				conf_path_set = 1;
 				break;
 
 			case 'L':
@@ -126,7 +123,13 @@ int main(int argc, char **argv)
 		}
 	}
 
-	if (conf_path_set && inline_route_count > 0) {
+	if(conf_path == NULL && inline_route_count == 0) {
+		usage(stdout, argv[0]);
+		free(inline_routes);
+		return 0;
+	}
+
+	if (conf_path && inline_route_count > 0) {
 		LOG_ERROR("-c and -L cannot be used together");
 		usage(stderr, argv[0]);
 		free(inline_routes);
@@ -269,7 +272,7 @@ out:
 		event_base_free(base);
 	}
 
-	free_routes(routes);
+	free(routes);
 	free(inline_routes);
 
 #ifdef _WIN32
diff --git a/tinyproxy.conf b/tinyproxy.conf
deleted file mode 100644
index 4aea92a..0000000
--- a/tinyproxy.conf
+++ /dev/null
@@ -1,38 +0,0 @@
-# See man 5 tinyproxy
-# TCP stream forwarding.
-# listen tcp :80     tcp 10.0.1.1:80
-# listen tcp :443    tcp 10.0.1.1:443    proxy_v2,keep_alive
-
-# TCP # listener to UNIX stream backend.
-# listen tcp  :12990                unix /tmp/test.sock
-
-# UDP datagram forwarding.
-# listen udp :12345  udp 10.0.1.1:12345  idle_timeout=10,connect_timeout=17
-# listen udp :19132  udp 10.0.1.1:19132
-
-# UDP # listener to UNIX datagram backend.
-# listen udp :13000  unix-dgram /tmp/testd.sock
-
-# Builtin TCP test backends.
-# listen tcp :12995  builtin client_addr
-# listen tcp :12996  builtin hang         idle_timeout=3,connect_timeout=1
-# listen tcp :12997  builtin discard
-# listen tcp :12998  builtin close
-# listen tcp :8080   builtin http-ok
-
-# Builtin UDP test backends.
-# listen udp :12995  builtin client_addr
-# listen udp :12996  builtin hang
-# listen udp :12997  builtin discard
-# listen udp :12998  builtin close
-# listen udp :8080   builtin http-ok
-
-# Static file TCP backends.
-# listen tcp :14000  file examples/http-response.txt
-# listen tcp :14001  file /does/not/exist
-
-# listen unix /tmp/tinyproxy-chain-1.sock file examples/http-response.txt
-# listen tcp  :13002                      unix /tmp/tinyproxy-chain-1.sock
-# listen tcp  :13003                      tcp  127.0.0.1:13002
-# listen unix /tmp/tinyproxy-chain-4.sock tcp  127.0.0.1:13003
-# listen tcp  :14001                      unix /tmp/tinyproxy-chain-4.sock