penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 56cef8cfc80e2cc23df4b1c2d8ce30baf19f082a

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-31T15:22:51Z
subjectDraft datagram_raw
commit 56cef8cfc80e2cc23df4b1c2d8ce30baf19f082a
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-31T15:22:51Z

    Draft datagram_raw
---
 src/datagram_listener.c |  22 +++-
 src/datagram_raw.c      | 268 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/datagram_raw.h      |  20 ++++
 src/datagram_route.c    |  11 ++
 src/datagram_route.h    |   7 ++
 src/route.h             |   1 +
 src/worker.c            |   5 -
 7 files changed, 328 insertions(+), 6 deletions(-)

diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index 5816acc..f862c44 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -7,6 +7,7 @@
 #include "datagram_listener.h"
 #include "datagram_client.h"
 #include "datagram_builtin.h"
+#include "datagram_raw.h"
 
 #ifdef TINYPROXY_DEBUG
 #include <stdlib.h>
@@ -96,6 +97,23 @@ int datagram_route_handle_packet(
 		return handle_datagram_builtin_packet(pkt);
 	}
 
+	if (ctx->route->opts.transparent_replay) {
+		rc = datagram_raw_replay_ipv4(
+				ctx->raw_fd,
+				ctx->route,
+				(const struct sockaddr *)&pkt->peer_addr,
+				pkt->peer_addr_len,
+				pkt->data,
+				pkt->data_len);
+
+		if (rc != 0) {
+			LOG_WARN("failed to replay datagram",
+					"err", _LOGV(strerror(-rc)));
+		}
+
+		return rc;
+	}
+
 	compat_mutex_lock(&ctx->clients_mu);
 
 	c = datagram_route_get_or_create_client(w, pkt);
@@ -113,7 +131,9 @@ out:
 
 	if (rc != 0) {
 		LOG_WARN("failed to send datagram payload upstream",
-			"err", _LOGV(strerror(-rc)));
+				"listen", _LOGV_ENDPOINT(&ctx->route->listen),
+				"upstream", _LOGV_ENDPOINT(&ctx->route->upstream),
+				"err", _LOGV(strerror(-rc)));
 	}
 
 	return rc;
diff --git a/src/datagram_raw.c b/src/datagram_raw.c
new file mode 100644
index 0000000..53adb5a
--- /dev/null
+++ b/src/datagram_raw.c
@@ -0,0 +1,268 @@
+#include "datagram_raw.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef _WIN32
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#endif
+
+#include "endpoint.h"
+
+static uint16_t checksum16(const void *data, size_t len)
+{
+	const uint8_t *p = data;
+	uint32_t sum = 0;
+
+	while (len >= 2) {
+		sum += ((uint16_t)p[0] << 8) | p[1];
+		p += 2;
+		len -= 2;
+	}
+
+	if (len != 0) {
+		sum += ((uint16_t)p[0] << 8);
+	}
+
+	while ((sum >> 16) != 0) {
+		sum = (sum & 0xffffu) + (sum >> 16);
+	}
+
+	return (uint16_t)~sum;
+}
+
+#ifndef _WIN32
+
+struct udp4_pseudo_header {
+	uint32_t src;
+	uint32_t dst;
+	uint8_t zero;
+	uint8_t proto;
+	uint16_t len;
+};
+
+static uint16_t udp4_checksum(
+	const struct ip *ip,
+	const struct udphdr *udp,
+	const unsigned char *payload,
+	size_t payload_len)
+{
+	struct udp4_pseudo_header ph;
+	size_t udp_len = sizeof(*udp) + payload_len;
+	size_t total_len = sizeof(ph) + udp_len;
+	unsigned char *buf;
+	uint16_t sum;
+
+	if (udp_len > UINT16_MAX || total_len > SIZE_MAX) {
+		return 0;
+	}
+
+	buf = malloc(total_len);
+	if (!buf) {
+		return 0;
+	}
+
+	memset(&ph, 0, sizeof(ph));
+	ph.src = ip->ip_src.s_addr;
+	ph.dst = ip->ip_dst.s_addr;
+	ph.zero = 0;
+	ph.proto = IPPROTO_UDP;
+	ph.len = htons((uint16_t)udp_len);
+
+	memcpy(buf, &ph, sizeof(ph));
+	memcpy(buf + sizeof(ph), udp, sizeof(*udp));
+	memcpy(buf + sizeof(ph) + sizeof(*udp), payload, payload_len);
+
+	sum = checksum16(buf, total_len);
+	free(buf);
+
+	/*
+	 * For IPv4 UDP, checksum 0 means "no checksum".
+	 * If the computed checksum is 0, transmit it as 0xffff.
+	 */
+	if (sum == 0) {
+		sum = 0xffff;
+	}
+
+	return sum;
+}
+
+int datagram_raw_open_ipv4(evutil_socket_t *out_fd)
+{
+	evutil_socket_t fd;
+	int one = 1;
+
+	if (!out_fd) {
+		return -EINVAL;
+	}
+
+	fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+	if (fd < 0) {
+		return -errno;
+	}
+
+	if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0) {
+		int err = errno;
+		evutil_closesocket(fd);
+		return -err;
+	}
+
+	*out_fd = fd;
+	return 0;
+}
+
+static int endpoint_to_sockaddr_in(
+	const struct endpoint *ep,
+	struct sockaddr_in *out)
+{
+	if (!ep || !out) {
+		return -EINVAL;
+	}
+
+	if (ep->kind != ENDPOINT_INET) {
+		return -EINVAL;
+	}
+
+	memset(out, 0, sizeof(*out));
+	out->sin_family = AF_INET;
+	out->sin_port = htons(ep->port);
+
+	if (inet_pton(AF_INET, ep->host, &out->sin_addr) != 1) {
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int datagram_raw_replay_ipv4(
+	evutil_socket_t raw_fd,
+	const struct route *r,
+	const struct sockaddr *peer_addr,
+	socklen_t peer_addr_len,
+	const unsigned char *data,
+	size_t data_len)
+{
+	const struct sockaddr_in *src;
+	struct sockaddr_in upstream_addr;
+	struct sockaddr_in send_dst;
+	unsigned char *packet;
+	struct ip *ip;
+	struct udphdr *udp;
+	size_t ip_len = sizeof(*ip);
+	size_t udp_len = sizeof(*udp);
+	size_t packet_len;
+	ssize_t n;
+	int rc;
+
+	(void)peer_addr_len;
+
+	if (raw_fd < 0 || !r || !peer_addr || !data) {
+		return -EINVAL;
+	}
+
+	if (peer_addr->sa_family != AF_INET) {
+		return -EAFNOSUPPORT;
+	}
+
+	rc = endpoint_to_sockaddr_in(&r->upstream, &upstream_addr);
+	if (rc != 0) {
+		return rc;
+	}
+
+	src = (const struct sockaddr_in *)peer_addr;
+
+	if (data_len > UINT16_MAX - ip_len - udp_len) {
+		return -EMSGSIZE;
+	}
+
+	packet_len = ip_len + udp_len + data_len;
+
+	packet = calloc(1, packet_len);
+	if (!packet) {
+		return -ENOMEM;
+	}
+
+	ip = (struct ip *)packet;
+	udp = (struct udphdr *)(packet + ip_len);
+
+	ip->ip_v = 4;
+	ip->ip_hl = 5;
+	ip->ip_tos = 0;
+	ip->ip_len = htons((uint16_t)packet_len);
+	ip->ip_id = 0;
+	ip->ip_off = 0;
+	ip->ip_ttl = 64;
+	ip->ip_p = IPPROTO_UDP;
+	ip->ip_src = src->sin_addr;
+	ip->ip_dst = upstream_addr.sin_addr;
+	ip->ip_sum = 0;
+	ip->ip_sum = checksum16(ip, ip_len);
+
+	udp->uh_sport = src->sin_port;
+	udp->uh_dport = upstream_addr.sin_port;
+	udp->uh_ulen = htons((uint16_t)(udp_len + data_len));
+	udp->uh_sum = 0;
+
+	memcpy(packet + ip_len + udp_len, data, data_len);
+
+	udp->uh_sum = udp4_checksum(ip, udp, data, data_len);
+
+	memset(&send_dst, 0, sizeof(send_dst));
+	send_dst.sin_family = AF_INET;
+	send_dst.sin_addr = upstream_addr.sin_addr;
+	send_dst.sin_port = upstream_addr.sin_port;
+
+	n = sendto(
+		raw_fd,
+		(const char *)packet,
+		packet_len,
+		0,
+		(const struct sockaddr *)&send_dst,
+		sizeof(send_dst));
+
+	free(packet);
+
+	if (n < 0) {
+		return -errno;
+	}
+
+	if ((size_t)n != packet_len) {
+		return -EIO;
+	}
+
+	return 0;
+}
+
+#else
+
+int datagram_raw_open_ipv4(evutil_socket_t *out_fd)
+{
+	(void)out_fd;
+	return -ENOTSUP;
+}
+
+int datagram_raw_replay_ipv4(
+	evutil_socket_t raw_fd,
+	const struct route *r,
+	const struct sockaddr *peer_addr,
+	socklen_t peer_addr_len,
+	const unsigned char *data,
+	size_t data_len)
+{
+	(void)raw_fd;
+	(void)r;
+	(void)peer_addr;
+	(void)peer_addr_len;
+	(void)data;
+	(void)data_len;
+
+	return -ENOTSUP;
+}
+
+#endif
diff --git a/src/datagram_raw.h b/src/datagram_raw.h
new file mode 100644
index 0000000..1255d22
--- /dev/null
+++ b/src/datagram_raw.h
@@ -0,0 +1,20 @@
+#ifndef DATAGRAM_RAW_H
+#define DATAGRAM_RAW_H
+
+#include <stddef.h>
+
+#include <event2/util.h>
+
+#include "route.h"
+
+int datagram_raw_open_ipv4(evutil_socket_t *out_fd);
+
+int datagram_raw_replay_ipv4(
+	evutil_socket_t raw_fd,
+	const struct route *r,
+	const struct sockaddr *peer_addr,
+	socklen_t peer_addr_len,
+	const unsigned char *data,
+	size_t data_len);
+
+#endif
diff --git a/src/datagram_route.c b/src/datagram_route.c
index 7af85b2..1a4f7eb 100644
--- a/src/datagram_route.c
+++ b/src/datagram_route.c
@@ -9,6 +9,7 @@
 #include "datagram_route.h"
 #include "datagram_listener.h"
 #include "datagram_client.h"
+#include "datagram_raw.h"
 
 static int prepare_datagram_route(struct datagram_route_ctx *ctx)
 {
@@ -26,6 +27,10 @@ static int prepare_datagram_route(struct datagram_route_ctx *ctx)
 		}
 	}
 
+	if (r->opts.transparent_replay) {
+		return datagram_raw_open_ipv4(&ctx->raw_fd);
+	}
+
 	return 0;
 }
 
@@ -48,6 +53,7 @@ int start_datagram_route(
 	ctx->worker_pool = wpool;
 	ctx->route = r;
 	ctx->listen_fd = EVUTIL_INVALID_SOCKET;
+	ctx->raw_fd = -1;
 
 	rc = compat_mutex_init(&ctx->clients_mu);
 	if (rc != 0) {
@@ -164,6 +170,11 @@ void free_datagram_route(struct datagram_route_ctx *ctx)
 
 	compat_mutex_unlock(&ctx->clients_mu);
 
+	if (socket_is_valid(ctx->raw_fd)) {
+		evutil_closesocket(ctx->raw_fd);
+		ctx->raw_fd = -1;
+	}
+
 	if (ctx->listen_ev) {
 		event_free(ctx->listen_ev);
 		ctx->listen_ev = NULL;
diff --git a/src/datagram_route.h b/src/datagram_route.h
index c5f27fc..30641bf 100644
--- a/src/datagram_route.h
+++ b/src/datagram_route.h
@@ -27,6 +27,8 @@ struct datagram_route_ctx {
 	socklen_t local_addr_len;
 
 	struct datagram_client *clients;
+
+	evutil_socket_t raw_fd;
 };
 
 int start_datagram_route(
@@ -38,4 +40,9 @@ int start_datagram_route(
 void stop_datagram_route_listener(struct datagram_route_ctx *ctx);
 void free_datagram_route(struct datagram_route_ctx *ctx);
 
+static inline int socket_is_valid(evutil_socket_t fd)
+{
+    return fd != (evutil_socket_t)EVUTIL_INVALID_SOCKET;
+}
+
 #endif
diff --git a/src/route.h b/src/route.h
index d95138e..24c3206 100644
--- a/src/route.h
+++ b/src/route.h
@@ -20,6 +20,7 @@ struct route_options {
 	bool proxy_v2;
 	bool keep_alive;
 	bool broadcast_reply;
+	bool transparent_replay;
 
 	int idle_timeout_sec;
 	int connect_timeout_sec;
diff --git a/src/worker.c b/src/worker.c
index c13abec..fcea64d 100644
--- a/src/worker.c
+++ b/src/worker.c
@@ -278,11 +278,6 @@ void worker_join(struct worker *w)
 	w->started = false;
 }
 
-static int socket_is_valid(evutil_socket_t fd)
-{
-    return fd != (evutil_socket_t)EVUTIL_INVALID_SOCKET;
-}
-
 static void worker_process_pending(struct worker *w)
 {
 	struct worker_msg *msg = worker_take_pending(w);