penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 45da16e9451237ce75d2734ac3497046b41484b5

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-25T03:13:41Z
subjectBasic proxy protocol v2. NOT tested yet
commit 45da16e9451237ce75d2734ac3497046b41484b5
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-25T03:13:41Z

    Basic proxy protocol v2. NOT tested yet
---
 Makefile         |   3 +-
 file_conf.c      |   2 -
 proxy_proto_v2.c | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 proxy_proto_v2.h |  35 +++++++++++++
 tcp_route.c      |  62 ++++++++++++++++++----
 5 files changed, 246 insertions(+), 13 deletions(-)

diff --git a/Makefile b/Makefile
index 6fd8d6e..94761c3 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,8 @@ STRIP ?= strip
 PROJECT_ROOT := $(CURDIR)
 BIN_DIR := $(CURDIR)/bin
 
-SRC := tcp_route.c \
+SRC := proxy_proto_v2.c \
+	   tcp_route.c \
        file_conf.c \
        tinyproxy.c
 
diff --git a/file_conf.c b/file_conf.c
index 4780bb1..2ef5ab1 100644
--- a/file_conf.c
+++ b/file_conf.c
@@ -1,5 +1,3 @@
-// file_conf.c
-
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
diff --git a/proxy_proto_v2.c b/proxy_proto_v2.c
new file mode 100644
index 0000000..8be5bdc
--- /dev/null
+++ b/proxy_proto_v2.c
@@ -0,0 +1,157 @@
+#include <event2/bufferevent.h>
+
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+#include "proxy_proto_v2.h"
+
+static void put_u16(unsigned char *p, uint16_t v)
+{
+	p[0] = (unsigned char)(v >> 8);
+	p[1] = (unsigned char)(v & 0xff);
+}
+
+int proxy_v2_build(
+	unsigned char *buf,
+	size_t buf_len,
+	const struct sockaddr *src,
+	socklen_t src_len,
+	const struct sockaddr *dst,
+	socklen_t dst_len,
+	int sock_type,
+	size_t *out_len
+) {
+	unsigned char fam_proto;
+	uint16_t addr_len;
+	unsigned char *p;
+
+	if (buf == NULL || src == NULL || dst == NULL || out_len == NULL) {
+		return -EINVAL;
+	}
+
+	if (src->sa_family != dst->sa_family) {
+		return -EAFNOSUPPORT;
+	}
+
+	if (sock_type == SOCK_STREAM) {
+		fam_proto = PP2_TRANS_STREAM;
+	} else if (sock_type == SOCK_DGRAM) {
+		fam_proto = PP2_TRANS_DGRAM;
+	} else {
+		return -EPROTONOSUPPORT;
+	}
+
+	switch (src->sa_family) {
+	case AF_INET:
+		if (src_len < sizeof(struct sockaddr_in) ||
+		    dst_len < sizeof(struct sockaddr_in)) {
+			return -EINVAL;
+		}
+
+		addr_len = 12;
+
+		if (buf_len < 16 + addr_len) {
+			return -ENOSPC;
+		}
+
+		memcpy(buf, PROXY_V2_SIG, 12);
+		buf[12] = PP2_VERSION_CMD_PROXY;
+		buf[13] = PP2_FAM_INET | fam_proto;
+		put_u16(buf + 14, addr_len);
+
+		p = buf + 16;
+
+		const struct sockaddr_in *s4 = (const struct sockaddr_in *)src;
+		const struct sockaddr_in *d4 = (const struct sockaddr_in *)dst;
+
+		memcpy(p, &s4->sin_addr, 4);
+		p += 4;
+
+		memcpy(p, &d4->sin_addr, 4);
+		p += 4;
+
+		memcpy(p, &s4->sin_port, 2);
+		p += 2;
+
+		memcpy(p, &d4->sin_port, 2);
+		p += 2;
+
+		*out_len = 16 + addr_len;
+		return 0;
+
+	case AF_INET6:
+		if (src_len < sizeof(struct sockaddr_in6) ||
+		    dst_len < sizeof(struct sockaddr_in6)) {
+			return -EINVAL;
+		}
+
+		addr_len = 36;
+
+		if (buf_len < 16 + addr_len) {
+			return -ENOSPC;
+		}
+
+		memcpy(buf, PROXY_V2_SIG, 12);
+		buf[12] = PP2_VERSION_CMD_PROXY;
+		buf[13] = PP2_FAM_INET6 | fam_proto;
+		put_u16(buf + 14, addr_len);
+
+		p = buf + 16;
+
+		const struct sockaddr_in6 *s6 = (const struct sockaddr_in6 *)src;
+		const struct sockaddr_in6 *d6 = (const struct sockaddr_in6 *)dst;
+
+		memcpy(p, &s6->sin6_addr, 16);
+		p += 16;
+
+		memcpy(p, &d6->sin6_addr, 16);
+		p += 16;
+
+		memcpy(p, &s6->sin6_port, 2);
+		p += 2;
+
+		memcpy(p, &d6->sin6_port, 2);
+		p += 2;
+
+		*out_len = 16 + addr_len;
+		return 0;
+
+	default:
+		return -EAFNOSUPPORT;
+	}
+}
+
+int proxy_v2_write_bufferevent(
+	struct bufferevent *bev,
+	const struct sockaddr *src,
+	socklen_t src_len,
+	const struct sockaddr *dst,
+	socklen_t dst_len,
+	int sock_type
+) {
+	unsigned char hdr[256];
+	size_t hdr_len = 0;
+
+	int rc = proxy_v2_build(
+		hdr,
+		sizeof(hdr),
+		src,
+		src_len,
+		dst,
+		dst_len,
+		sock_type,
+		&hdr_len
+	);
+	if (rc != 0) {
+		return rc;
+	}
+
+	if (bufferevent_write(bev, hdr, hdr_len) < 0) {
+		return -EIO;
+	}
+
+	return 0;
+}
diff --git a/proxy_proto_v2.h b/proxy_proto_v2.h
new file mode 100644
index 0000000..0a3ed47
--- /dev/null
+++ b/proxy_proto_v2.h
@@ -0,0 +1,35 @@
+#ifndef PROXY_PROTO_V2_H
+#define PROXY_PROTO_V2_H
+
+#define PROXY_V2_SIG "\r\n\r\n\0\r\nQUIT\n"
+
+#define PP2_VERSION_CMD_PROXY 0x21
+
+#define PP2_FAM_INET          0x10
+#define PP2_FAM_INET6         0x20
+#define PP2_FAM_UNIX          0x30
+
+#define PP2_TRANS_STREAM      0x01
+#define PP2_TRANS_DGRAM       0x02
+
+int proxy_v2_build(
+	unsigned char *buf,
+	size_t buf_len,
+	const struct sockaddr *src,
+	socklen_t src_len,
+	const struct sockaddr *dst,
+	socklen_t dst_len,
+	int sock_type,
+	size_t *out_len
+);
+
+int proxy_v2_write_bufferevent(
+	struct bufferevent *bev,
+	const struct sockaddr *src,
+	socklen_t src_len,
+	const struct sockaddr *dst,
+	socklen_t dst_len,
+	int sock_type
+);
+
+#endif
diff --git a/tcp_route.c b/tcp_route.c
index 0421b67..c09abaf 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -11,8 +11,11 @@
 #include <string.h>
 
 #include "file_conf.h"
+#include "proxy_proto_v2.h"
 #include "tcp_route.h"
 
+#define PROXY_V2_SIG "\r\n\r\n\0\r\nQUIT\n"
+
 static void free_conn(conn_t *conn) {
 	if (conn == NULL) {
 		return;
@@ -78,8 +81,6 @@ static void accept_cb(
 	void *arg
 ) {
 	(void)listener;
-	(void)addr;
-	(void)socklen;
 
 	struct listener_ctx *ctx = arg;
 	const struct route *r = ctx->route;
@@ -126,6 +127,14 @@ static void accept_cb(
 		return;
 	}
 
+	/*
+	 * Do not read from the client yet.
+	 *
+	 * Otherwise client bytes may be copied into the upstream output buffer
+	 * before the PROXY v2 header is queued.
+	 */
+	bufferevent_disable(conn->client, EV_READ);
+
 	if (bufferevent_socket_connect(
 			conn->upstream,
 			(struct sockaddr *)&upstream_addr,
@@ -136,14 +145,47 @@ static void accept_cb(
 		return;
 	}
 
-	/*
-	 * Important:
-	 * The upstream connection is async. libevent will emit BEV_EVENT_CONNECTED
-	 * later. This simple proxy enables client reading immediately.
-	 *
-	 * For PROXY v2, we need to write the PROXY header to
-	 * conn->upstream output before forwarding client bytes.
-	 */
+	if (r->send_proxy_v2) {
+		struct sockaddr_in local_addr;
+		socklen_t local_len = sizeof(local_addr);
+
+		memset(&local_addr, 0, sizeof(local_addr));
+
+		if (getsockname(client_fd, (struct sockaddr *)&local_addr, &local_len) < 0) {
+			perror("getsockname");
+			free_conn(conn);
+			return;
+		}
+
+		if (addr == NULL || socklen < (int)sizeof(struct sockaddr_in)) {
+			fprintf(stderr, "invalid client address\n");
+			free_conn(conn);
+			return;
+		}
+
+		if (((struct sockaddr *)addr)->sa_family != AF_INET ||
+		    local_addr.sin_family != AF_INET) {
+			fprintf(stderr, "PROXY v2 currently only supports IPv4 TCP\n");
+			free_conn(conn);
+			return;
+		}
+
+		if (proxy_v2_write_bufferevent(
+			conn->upstream,
+			addr,
+			(socklen_t)socklen,
+			(struct sockaddr *)&local_addr,
+			local_len,
+			SOCK_STREAM
+		) < 0) {
+			fprintf(stderr, "failed to write PROXY v2 header\n");
+			free_conn(conn);
+			return;
+		}
+	}
+
+	bufferevent_enable(conn->client, EV_READ | EV_WRITE);
+	bufferevent_enable(conn->upstream, EV_READ | EV_WRITE);
 }
 
 static void accept_error_cb(struct evconnlistener *listener, void *arg) {