penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit a90133fa349698587dcf7eb71bb66eb9e9219cc4

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-01T04:06:18Z
subjectIPv6 support
commit a90133fa349698587dcf7eb71bb66eb9e9219cc4
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-01T04:06:18Z

    IPv6 support
---
 examples/ipv6.conf      |  7 ++++
 src/datagram_client.c   | 28 ++++++++------
 src/datagram_listener.c | 99 ++++++++++++++++++++++++++++++++++++++-----------
 src/endpoint.c          | 47 ++++++++++++++++++++++-
 src/endpoint.h          |  6 ++-
 src/file_conf.c         | 82 +++++++++++++++++++++++++++++++---------
 src/stream_conn.c       | 31 ++++++++--------
 src/stream_listener.c   | 16 ++++----
 8 files changed, 241 insertions(+), 75 deletions(-)

diff --git a/examples/ipv6.conf b/examples/ipv6.conf
new file mode 100644
index 0000000..4bdf4ea
--- /dev/null
+++ b/examples/ipv6.conf
@@ -0,0 +1,7 @@
+# v6 to v4
+tcp :48899 builtin http_ok
+tcp [::1]:48892 tcp 127.0.0.1:48899
+tcp [::1]:48891 tcp [::1]:48892
+
+udp [::1]:9292 builtin http_ok
+udp [::1]:9293 udp [::1]:9292
diff --git a/src/datagram_client.c b/src/datagram_client.c
index 3ec38ac..de7a523 100644
--- a/src/datagram_client.c
+++ b/src/datagram_client.c
@@ -100,18 +100,19 @@ static int connect_datagram_upstream(struct datagram_client *c, const struct end
 
 	switch (upstream->kind) {
 
-	case ENDPOINT_INET: {
-		struct sockaddr_in upstream_addr;
-
-		memset(&upstream_addr, 0, sizeof(upstream_addr));
-		upstream_addr.sin_family = AF_INET;
-		upstream_addr.sin_port = htons(upstream->port);
+	case ENDPOINT_INET:
+	case ENDPOINT_INET6:
+	{
+		struct sockaddr_storage upstream_addr;
+		socklen_t upstream_addr_len;
+		int rc;
 
-		if (inet_pton(AF_INET, upstream->host, &upstream_addr.sin_addr) != 1) {
-			return -EINVAL;
+		rc = endpoint_to_sockaddr(upstream, &upstream_addr, &upstream_addr_len);
+		if (rc != 0) {
+			return rc;
 		}
 
-		c->fd = socket(AF_INET, SOCK_DGRAM, 0);
+		c->fd = socket(upstream_addr.ss_family, SOCK_DGRAM, 0);
 		if (c->fd < 0) {
 			return -EVUTIL_SOCKET_ERROR();
 		}
@@ -119,9 +120,14 @@ static int connect_datagram_upstream(struct datagram_client *c, const struct end
 		if (connect(
 				c->fd,
 				(const struct sockaddr *)&upstream_addr,
-				sizeof(upstream_addr)
+				upstream_addr_len
 			) < 0) {
-			return -EVUTIL_SOCKET_ERROR();
+			int err = EVUTIL_SOCKET_ERROR();
+
+			evutil_closesocket(c->fd);
+			c->fd = -1;
+
+			return -err;
 		}
 
 		return 0;
diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index 1b0cc8b..6aaf141 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -331,22 +331,37 @@ static int bind_unix_datagram_listener(struct datagram_route_ctx *ctx)
 static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
 {
 	const struct route *r = ctx->route;
-	struct sockaddr_in listen_addr;
+	struct sockaddr_storage listen_addr;
+	socklen_t listen_addr_len;
+	int family;
 	int one = 1;
+	int rc;
 
-	memset(&listen_addr, 0, sizeof(listen_addr));
+	rc = endpoint_to_sockaddr(&r->listen, &listen_addr, &listen_addr_len);
+	if (rc != 0) {
+		LOG_ERROR("invalid udp listen address",
+			"listen", _LOGV_ENDPOINT(&r->listen)
+		);
+		return rc;
+	}
 
-	listen_addr.sin_family = AF_INET;
-	listen_addr.sin_port = htons(r->listen.port);
+	switch (r->listen.kind) {
+	case ENDPOINT_INET:
+		family = AF_INET;
+		break;
 
-	if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
-		LOG_ERROR("invalid udp listen address",
+	case ENDPOINT_INET6:
+		family = AF_INET6;
+		break;
+
+	default:
+		LOG_ERROR("invalid udp listen endpoint kind",
 			"listen", _LOGV_ENDPOINT(&r->listen)
 		);
 		return -EINVAL;
 	}
 
-	ctx->listen_fd = socket(AF_INET, SOCK_DGRAM, 0);
+	ctx->listen_fd = socket(family, SOCK_DGRAM, 0);
 	if (ctx->listen_fd < 0) {
 		int err = EVUTIL_SOCKET_ERROR();
 
@@ -357,13 +372,46 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
 	}
 
 	if (r->opts.broadcast_reply != BROADCAST_REPLY_OFF) {
+		if (r->listen.kind != ENDPOINT_INET) {
+			LOG_ERROR("broadcast_reply is only supported for IPv4 UDP",
+				"line", _LOGV(r->line_no),
+				"listen", _LOGV_ENDPOINT(&r->listen)
+			);
+			return -EINVAL;
+		}
+
+		{
+			int yes = 1;
+
+			if (setsockopt(ctx->listen_fd, SOL_SOCKET, SO_BROADCAST,
+						(const char *)&yes, sizeof(yes)) < 0) {
+				int err = EVUTIL_SOCKET_ERROR();
+
+				LOG_ERROR("failed to enable broadcast",
+					"line", _LOGV(r->line_no),
+					"err", _LOGV(evutil_socket_error_to_string(err))
+				);
+				return -err;
+			}
+		}
+	}
+
+	if (family == AF_INET6) {
 		int yes = 1;
-		if (setsockopt(ctx->listen_fd, SOL_SOCKET, SO_BROADCAST,
+
+		/*
+		 * Keep behavior explicit:
+		 *   udp :1234      => IPv4 only
+		 *   udp [::]:1234  => IPv6 only
+		 */
+		if (setsockopt(ctx->listen_fd, IPPROTO_IPV6, IPV6_V6ONLY,
 					(const char *)&yes, sizeof(yes)) < 0) {
 			int err = EVUTIL_SOCKET_ERROR();
-			LOG_ERROR("failed to enable broadcast",
-					"line", _LOGV(r->line_no),
-					"err", _LOGV(evutil_socket_error_to_string(err)));
+
+			LOG_ERROR("failed to set IPV6_V6ONLY",
+				"line", _LOGV(r->line_no),
+				"err", _LOGV(evutil_socket_error_to_string(err))
+			);
 			return -err;
 		}
 	}
@@ -375,25 +423,33 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
 		return -EINVAL;
 	}
 
-	setsockopt(
-		ctx->listen_fd,
-		SOL_SOCKET,
-		SO_REUSEADDR,
-		(const char *)&one,
-		sizeof(one)
-	);
+	if (setsockopt(
+			ctx->listen_fd,
+			SOL_SOCKET,
+			SO_REUSEADDR,
+			(const char *)&one,
+			sizeof(one)
+		) < 0) {
+		int err = EVUTIL_SOCKET_ERROR();
+
+		LOG_ERROR("failed to set SO_REUSEADDR",
+			"line", _LOGV(r->line_no),
+			"err", _LOGV(evutil_socket_error_to_string(err))
+		);
+		return -err;
+	}
 
 	if (bind(
 			ctx->listen_fd,
 			(const struct sockaddr *)&listen_addr,
-			sizeof(listen_addr)
+			listen_addr_len
 		) < 0) {
 		int err = EVUTIL_SOCKET_ERROR();
 
 		LOG_ERROR("udp bind failed",
 			"err", _LOGV(evutil_socket_error_to_string(err))
 		);
-		return -EADDRINUSE;
+		return -err;
 	}
 
 	ctx->local_addr_len = sizeof(ctx->local_addr);
@@ -409,7 +465,7 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
 		LOG_ERROR("udp getsockname failed",
 			"err", _LOGV(evutil_socket_error_to_string(err))
 		);
-		return -EINVAL;
+		return -err;
 	}
 
 	ctx->listen_ev = event_new(
@@ -436,6 +492,7 @@ int bind_datagram_listener(struct datagram_route_ctx *ctx)
 {
 	switch (ctx->route->listen.kind) {
 	case ENDPOINT_INET:
+	case ENDPOINT_INET6:
 		return bind_udp_datagram_listener(ctx);
 
 	case ENDPOINT_UNIX_DGRAM:
diff --git a/src/endpoint.c b/src/endpoint.c
index 53ca6fc..863522a 100644
--- a/src/endpoint.c
+++ b/src/endpoint.c
@@ -1,6 +1,7 @@
+#include <errno.h>
 #include <stdbool.h>
 #include <stdio.h>
-#include <errno.h>
+#include <string.h>
 
 #include "endpoint.h"
 
@@ -20,6 +21,10 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
 		snprintf(buf, buf_len, "%s:%u", ep->host, ep->port);
 		return 0;
 
+	case ENDPOINT_INET6:
+		snprintf(buf, buf_len, "[%s]:%u", ep->host, ep->port);
+		return 0;
+
 	case ENDPOINT_UNIX:
 		snprintf(buf, buf_len, "unix:%s", ep->path);
 		return 0;
@@ -42,6 +47,46 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
 	}
 }
 
+int endpoint_to_sockaddr(const struct endpoint *ep,
+								struct sockaddr_storage *ss,
+								socklen_t *ss_len)
+{
+	memset(ss, 0, sizeof(*ss));
+
+	switch (ep->kind) {
+	case ENDPOINT_INET: {
+		struct sockaddr_in *addr4 = (struct sockaddr_in *)ss;
+
+		addr4->sin_family = AF_INET;
+		addr4->sin_port = htons(ep->port);
+
+		if (inet_pton(AF_INET, ep->host, &addr4->sin_addr) != 1) {
+			return -EINVAL;
+		}
+
+		*ss_len = sizeof(*addr4);
+		return 0;
+	}
+
+	case ENDPOINT_INET6: {
+		struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)ss;
+
+		addr6->sin6_family = AF_INET6;
+		addr6->sin6_port = htons(ep->port);
+
+		if (inet_pton(AF_INET6, ep->host, &addr6->sin6_addr) != 1) {
+			return -EINVAL;
+		}
+
+		*ss_len = sizeof(*addr6);
+		return 0;
+	}
+
+	default:
+		return -EINVAL;
+	}
+}
+
 bool endpoint_is_stream(const struct endpoint *ep)
 {
 	if (ep == NULL) {
diff --git a/src/endpoint.h b/src/endpoint.h
index 867e92a..3624430 100644
--- a/src/endpoint.h
+++ b/src/endpoint.h
@@ -3,6 +3,7 @@
 
 #include <stdint.h>
 
+#include "compat.h"
 #include "x_builtins.h"
 
 enum endpoint_proto {
@@ -16,6 +17,7 @@ enum endpoint_proto {
 
 enum endpoint_kind {
 	ENDPOINT_INET,
+	ENDPOINT_INET6,
 	ENDPOINT_UNIX,
 	ENDPOINT_UNIX_DGRAM,
 	ENDPOINT_BUILTIN,
@@ -40,5 +42,7 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len);
 bool endpoint_is_stream(const struct endpoint *ep);
 bool endpoint_is_datagram(const struct endpoint *ep);
 bool endpoint_is_listenable(const struct endpoint *ep);
-
+int endpoint_to_sockaddr(const struct endpoint *ep,
+								struct sockaddr_storage *ss,
+								socklen_t *ss_len);
 #endif
diff --git a/src/file_conf.c b/src/file_conf.c
index 8b6ae5a..65fe613 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -86,30 +86,74 @@ static int find_conflicting_route(
 
 static int split_host_port(const char *input,
 						   char *host, size_t host_len,
-						   uint16_t *port)
+						   uint16_t *port,
+						   bool *is_ipv6)
 {
-	const char *colon = strrchr(input, ':');
-	if (!colon) {
+	const char *port_s;
+	size_t hlen;
+
+	if (!input || !host || host_len == 0 || !port || !is_ipv6) {
 		return -1;
 	}
 
-	size_t hlen = (size_t)(colon - input);
-	const char *port_s = colon + 1;
+	host[0] = '\0';
+	*is_ipv6 = false;
 
-	if (hlen >= host_len || *port_s == '\0') {
-		return -1;
-	}
+	if (input[0] == '[') {
+		const char *close = strchr(input, ']');
 
-	memcpy(host, input, hlen);
-	host[hlen] = '\0';
+		if (!close) {
+			return -1;
+		}
 
-	if (parse_port(port_s, port) != 0) {
-		return -1;
+		if (close[1] != ':') {
+			return -1;
+		}
+
+		hlen = (size_t)(close - (input + 1));
+		port_s = close + 2;
+
+		if (hlen == 0 || hlen >= host_len || *port_s == '\0') {
+			return -1;
+		}
+
+		memcpy(host, input + 1, hlen);
+		host[hlen] = '\0';
+		*is_ipv6 = true;
+	} else {
+		const char *first_colon = strchr(input, ':');
+		const char *last_colon = strrchr(input, ':');
+
+		if (!last_colon) {
+			return -1;
+		}
+
+		/*
+		 * Reject bare IPv6 with port.
+		 * Use [::1]:80 instead.
+		 */
+		if (first_colon != last_colon) {
+			return -1;
+		}
+
+		hlen = (size_t)(last_colon - input);
+		port_s = last_colon + 1;
+
+		if (hlen >= host_len || *port_s == '\0') {
+			return -1;
+		}
+
+		memcpy(host, input, hlen);
+		host[hlen] = '\0';
+
+		/* Allow ":80" as IPv4 wildcard shorthand. */
+		if (host[0] == '\0') {
+			snprintf(host, host_len, "0.0.0.0");
+		}
 	}
 
-	// Allow ":80" as shorthand for all interfaces.
-	if (host[0] == '\0') {
-		snprintf(host, host_len, "0.0.0.0");
+	if (parse_port(port_s, port) != 0) {
+		return -1;
 	}
 
 	return 0;
@@ -301,14 +345,16 @@ static int parse_endpoint(enum endpoint_proto proto, const char *s, struct endpo
 
 	switch (proto) {
 	case PROTO_TCP:
-	case PROTO_UDP:
-		ep->kind = ENDPOINT_INET;
+	case PROTO_UDP: {
+		bool is_ipv6 = false;
 
-		if (split_host_port(s, ep->host, sizeof(ep->host), &ep->port) != 0) {
+		if (split_host_port(s, ep->host, sizeof(ep->host), &ep->port, &is_ipv6) != 0) {
 			return -1;
 		}
 
+		ep->kind = is_ipv6 ? ENDPOINT_INET6 : ENDPOINT_INET;
 		return 0;
+	}
 
 	case PROTO_UNIX_STREAM:
 		path = s;
diff --git a/src/stream_conn.c b/src/stream_conn.c
index 464c247..e9bb34b 100644
--- a/src/stream_conn.c
+++ b/src/stream_conn.c
@@ -207,26 +207,27 @@ void stream_upstream_event_cb(struct bufferevent *bev, short events, void *arg)
 
 static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
 {
-	if (ep == NULL) {
+	if (bev == NULL || ep == NULL) {
 		return -EINVAL;
 	}
 
 	switch (ep->kind) {
-	case ENDPOINT_INET: {
-		struct sockaddr_in addr;
-
-		memset(&addr, 0, sizeof(addr));
-		addr.sin_family = AF_INET;
-		addr.sin_port = htons(ep->port);
+	case ENDPOINT_INET:
+	case ENDPOINT_INET6:
+	{
+		struct sockaddr_storage addr;
+		socklen_t addr_len;
+		int rc;
 
-		if (inet_pton(AF_INET, ep->host, &addr.sin_addr) != 1) {
-			return -EINVAL;
+		rc = endpoint_to_sockaddr(ep, &addr, &addr_len);
+		if (rc != 0) {
+			return rc;
 		}
 
 		if (bufferevent_socket_connect(
-			    bev,
-			    (struct sockaddr *)&addr,
-			    sizeof(addr)) < 0) {
+				bev,
+				(struct sockaddr *)&addr,
+				addr_len) < 0) {
 			return -errno;
 		}
 
@@ -251,9 +252,9 @@ static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
 		strcpy(addr.sun_path, ep->path);
 
 		if (bufferevent_socket_connect(
-			    bev,
-			    (struct sockaddr *)&addr,
-			    sizeof(addr)) < 0) {
+				bev,
+				(struct sockaddr *)&addr,
+				sizeof(addr)) < 0) {
 			return -errno;
 		}
 
diff --git a/src/stream_listener.c b/src/stream_listener.c
index d7e0732..8b79117 100644
--- a/src/stream_listener.c
+++ b/src/stream_listener.c
@@ -67,16 +67,15 @@ void accept_error_cb(struct evconnlistener *listener, void *arg)
 static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
 {
 	const struct route *r = ctx->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);
+	struct sockaddr_storage listen_addr;
+	socklen_t listen_addr_len;
+	int rc;
 
-	if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
+	rc = endpoint_to_sockaddr(&r->listen, &listen_addr, &listen_addr_len);
+	if (rc != 0) {
 		LOG_ERROR("invalid listen address",
 			"listen", _LOGV_ENDPOINT(&r->listen));
-		return -EINVAL;
+		return rc;
 	}
 
 	ctx->listener = evconnlistener_new_bind(
@@ -86,7 +85,7 @@ static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
 		LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
 		-1,
 		(struct sockaddr *)&listen_addr,
-		sizeof(listen_addr)
+		listen_addr_len
 	);
 
 	if (ctx->listener == NULL) {
@@ -217,6 +216,7 @@ int bind_stream_listener(struct stream_route_ctx *ctx)
 {
 	switch (ctx->route->listen.kind) {
 	case ENDPOINT_INET:
+	case ENDPOINT_INET6:
 		return bind_tcp_stream_listener(ctx);
 
 	case ENDPOINT_UNIX: