commit 44819870202ffe8d6d391e9a002313d547ef7fbc
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-27T23:14:54Z |
| subject | Added builtin upstream endpoints |
commit 44819870202ffe8d6d391e9a002313d547ef7fbc
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-27T23:14:54Z
Added builtin upstream endpoints
---
.gitignore | 1 +
Makefile | 2 +
fuzz/Makefile | 1 +
src/file_conf.c | 16 ++++++
src/klog.c | 24 ++-------
src/route.c | 35 +++++++++++++
src/route.h | 6 +++
src/tcp_route.c | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
src/udp_route.c | 129 +++++++++++++++++++++++++++++++++++++++++++++++
src/x_builtins.c | 97 +++++++++++++++++++++++++++++++++++
src/x_builtins.h | 40 +++++++++++++++
tinyproxy.conf | 27 +++++++++-
12 files changed, 504 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
index e01bc0e..18168ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ libevent2/
__pycache__
*.log
*.swp
+.DS_Store
# Prerequisites
*.d
diff --git a/Makefile b/Makefile
index 5361c96..ca1e372 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,8 @@ BIN := $(BIN_DIR)/tinyproxy$(EXEEXT)
SRC := $(wildcard $(SRC_DIR)/*.c)
+CFLAGS += -ffile-prefix-map=$(SRC_DIR)/=
+
all: $(BIN)
$(BIN): $(SRC) $(LIBEVENT_DEPS)
diff --git a/fuzz/Makefile b/fuzz/Makefile
index cf43612..0c55461 100644
--- a/fuzz/Makefile
+++ b/fuzz/Makefile
@@ -62,6 +62,7 @@ $(OUT)/fuzz_tcp_route: \
$(SRC_DIR)/tcp_route.c \
$(SRC_DIR)/proxy_proto_v2.c \
$(SRC_DIR)/route.c \
+ $(SRC_DIR)/x_builtins.c \
$(SRC_DIR)/klog.c \
fuzz_tcp_route.c \
-o $@ \
diff --git a/src/file_conf.c b/src/file_conf.c
index 62a5e20..4ca1c21 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -212,6 +212,22 @@ static int parse_endpoint(const char *s, struct endpoint *ep)
ep->kind = ENDPOINT_UNIX;
strcpy(ep->path, path);
+ return 0;
+ } else if (strncmp(s, "builtin://", 10) == 0) {
+ const char *name = s + 10;
+ enum x_builtin_upstream builtin;
+
+ if (name[0] == '\0') {
+ return -1;
+ }
+
+ if (x_builtin_parse(name, &builtin) < 0) {
+ return -1;
+ }
+
+ ep->kind = ENDPOINT_BUILTIN;
+ ep->builtin = builtin;
+
return 0;
} else if (strncmp(s, "unix-dgram:", 11) == 0) {
path = s + 11;
diff --git a/src/klog.c b/src/klog.c
index 2c358ba..36b07b8 100644
--- a/src/klog.c
+++ b/src/klog.c
@@ -59,28 +59,10 @@ static void print_quoted_value(const char *s)
static void log_print_endpoint(FILE *out, const struct endpoint *ep)
{
- if (ep == NULL) {
- fputs("<nil>", out);
- return;
- }
-
- switch (ep->kind) {
- case ENDPOINT_INET:
- fprintf(out, "%s:%u", ep->host, ep->port);
- return;
-
- case ENDPOINT_UNIX:
- fprintf(out, "unix:%s", ep->path);
- return;
+ char buf[256];
- case ENDPOINT_UNIX_DGRAM:
- fprintf(out, "unix-dgram:%s", ep->path);
- return;
-
- default:
- fputs("<unknown>", out);
- return;
- }
+ endpoint_to_string(ep, buf, sizeof(buf));
+ fputs(buf, out);
}
static void print_log_value(struct log_value value)
diff --git a/src/route.c b/src/route.c
index 83a069c..06f339c 100644
--- a/src/route.c
+++ b/src/route.c
@@ -1,6 +1,41 @@
#include <stdio.h>
+#include <errno.h>
#include "route.h"
+int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
+{
+ if (buf == NULL || buf_len == 0) {
+ return -EINVAL;
+ }
+
+ if (ep == NULL) {
+ snprintf(buf, buf_len, "<nil>");
+ return 0;
+ }
+
+ switch (ep->kind) {
+ case ENDPOINT_INET:
+ 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;
+
+ case ENDPOINT_UNIX_DGRAM:
+ snprintf(buf, buf_len, "unix-dgram:%s", ep->path);
+ return 0;
+
+ case ENDPOINT_BUILTIN:
+ snprintf(buf, buf_len, "builtin://%s", x_builtin_name(ep->builtin));
+ return 0;
+
+ default:
+ snprintf(buf, buf_len, "<unknown>");
+ return 0;
+ }
+}
+
void route_options_str(const struct route_options *opts, char *buf, size_t buflen)
{
size_t n = 0;
diff --git a/src/route.h b/src/route.h
index 586f0b2..e672512 100644
--- a/src/route.h
+++ b/src/route.h
@@ -5,6 +5,8 @@
#include <stddef.h>
#include <stdint.h>
+#include "x_builtins.h"
+
struct event_base;
#define ROUTE_HOST_MAX 256
@@ -28,6 +30,7 @@ enum endpoint_kind {
ENDPOINT_INET,
ENDPOINT_UNIX,
ENDPOINT_UNIX_DGRAM,
+ ENDPOINT_BUILTIN,
};
struct endpoint {
@@ -37,6 +40,8 @@ struct endpoint {
uint16_t port;
char path[108]; /* sockaddr_un sun_path limit on Linux */
+
+ enum x_builtin_upstream builtin;
};
struct route {
@@ -54,6 +59,7 @@ struct worker {
size_t id;
};
+int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len);
void route_options_str(const struct route_options *opts, char *buf, size_t buflen);
#endif
diff --git a/src/tcp_route.c b/src/tcp_route.c
index 827c684..a65ef99 100644
--- a/src/tcp_route.c
+++ b/src/tcp_route.c
@@ -15,6 +15,7 @@
#include "proxy_proto_v2.h"
#include "route.h"
#include "tcp_route.h"
+#include "x_builtins.h"
#define BEV_READ_HIGH_WATER (256 * 1024)
#define BEV_WRITE_RESUME_WATER (128 * 1024)
@@ -124,6 +125,16 @@ static void set_idle_timeouts(conn_t *conn, const struct route *r)
bufferevent_set_timeouts(conn->upstream, &idle_timeout, &idle_timeout);
}
+static void set_client_idle_timeout(conn_t *conn, const struct route *r)
+{
+ struct timeval idle_timeout = {
+ .tv_sec = r->opts.idle_timeout_sec,
+ .tv_usec = 0,
+ };
+
+ bufferevent_set_timeouts(conn->client, &idle_timeout, &idle_timeout);
+}
+
static int set_socket_keepalive(evutil_socket_t fd, const struct route *r)
{
int v = r->opts.keep_alive ? 1 : 0;
@@ -155,7 +166,9 @@ static void event_cb(struct bufferevent *bev, short events, void *arg) {
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
- LOG_ERROR("connection error", "err", _LOGV(evutil_socket_error_to_string(err)));
+ LOG_ERROR("connection error",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
}
free_conn(conn);
@@ -228,6 +241,128 @@ static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
}
}
+static const char *tcp_client_addr_string(conn_t *conn, char *buf, size_t buf_len)
+{
+ struct sockaddr_storage ss;
+ socklen_t len = sizeof(ss);
+ evutil_socket_t fd;
+ int port = 0;
+
+ if (conn == NULL || conn->client == NULL || buf == NULL || buf_len == 0) {
+ return NULL;
+ }
+
+ fd = bufferevent_getfd(conn->client);
+ if (fd < 0) {
+ return NULL;
+ }
+
+ if (getpeername(fd, (struct sockaddr *)&ss, &len) < 0) {
+ return NULL;
+ }
+
+ if (ss.ss_family == AF_INET) {
+ struct sockaddr_in *sin = (struct sockaddr_in *)&ss;
+
+ if (inet_ntop(AF_INET, &sin->sin_addr, buf, buf_len) == NULL) {
+ return NULL;
+ }
+
+ port = ntohs(sin->sin_port);
+ } else if (ss.ss_family == AF_INET6) {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&ss;
+
+ if (inet_ntop(AF_INET6, &sin6->sin6_addr, buf, buf_len) == NULL) {
+ return NULL;
+ }
+
+ port = ntohs(sin6->sin6_port);
+ } else {
+ snprintf(buf, buf_len, "unknown");
+ return buf;
+ }
+
+ snprintf(buf + strlen(buf), buf_len - strlen(buf), ":%d", port);
+ return buf;
+}
+
+static void builtin_client_read_cb(struct bufferevent *bev, void *arg)
+{
+ struct evbuffer *input = bufferevent_get_input(bev);
+
+ (void)arg;
+
+ evbuffer_drain(input, evbuffer_get_length(input));
+}
+
+static void builtin_close_after_write_cb(struct bufferevent *bev, void *arg)
+{
+ conn_t *conn = arg;
+
+ if (evbuffer_get_length(bufferevent_get_output(bev)) == 0) {
+ free_conn(conn);
+ }
+}
+
+static int start_tcp_builtin(conn_t *conn)
+{
+ struct x_builtin_request req;
+ struct x_builtin_response res;
+ const struct route *r;
+ char client_addr[128];
+ int rc;
+
+ if (conn == NULL || conn->route == NULL) {
+ return -EINVAL;
+ }
+
+ r = conn->route;
+
+ memset(&req, 0, sizeof(req));
+
+ req.builtin = r->upstream.builtin;
+ req.client_addr = tcp_client_addr_string(conn, client_addr, sizeof(client_addr));
+
+ rc = x_builtin_handle(&req, &res);
+ if (rc < 0) {
+ free_conn(conn);
+ return rc;
+ }
+
+ if (conn->upstream != NULL) {
+ bufferevent_free(conn->upstream);
+ conn->upstream = NULL;
+ }
+
+ set_client_idle_timeout(conn, r);
+
+ switch (res.action) {
+ case X_BUILTIN_ACTION_CLOSE:
+ if (res.data_len > 0) {
+ bufferevent_write(conn->client, res.data, res.data_len);
+ bufferevent_setcb(conn->client, NULL, builtin_close_after_write_cb, event_cb, conn);
+ bufferevent_enable(conn->client, EV_WRITE);
+ } else {
+ free_conn(conn);
+ }
+ return 0;
+
+ case X_BUILTIN_ACTION_DISCARD:
+ bufferevent_setcb(conn->client, builtin_client_read_cb, NULL, event_cb, conn);
+ bufferevent_enable(conn->client, EV_READ);
+ return 0;
+
+ case X_BUILTIN_ACTION_HANG:
+ bufferevent_setcb(conn->client, NULL, NULL, event_cb, conn);
+ bufferevent_enable(conn->client, EV_READ);
+ return 0;
+
+ default:
+ free_conn(conn);
+ return -EINVAL;
+ }
+}
+
static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac) {
conn_t *conn = calloc(1, sizeof(*conn));
if (conn == NULL) {
@@ -244,9 +379,19 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
conn->peer_addr_len = ac->peer_addr_len;
conn->client = bufferevent_socket_new(w->base, ac->fd, BEV_OPT_CLOSE_ON_FREE);
- conn->upstream = bufferevent_socket_new(w->base, -1, BEV_OPT_CLOSE_ON_FREE);
+ if (conn->client == NULL) {
+ free(conn);
+ evutil_closesocket(ac->fd);
+ return;
+ }
- if (conn->client == NULL || conn->upstream == NULL) {
+ if (ac->route->upstream.kind == ENDPOINT_BUILTIN) {
+ start_tcp_builtin(conn);
+ return;
+ }
+
+ conn->upstream = bufferevent_socket_new(w->base, -1, BEV_OPT_CLOSE_ON_FREE);
+ if (conn->upstream == NULL) {
free_conn(conn);
return;
}
diff --git a/src/udp_route.c b/src/udp_route.c
index 285f987..ed67cb4 100644
--- a/src/udp_route.c
+++ b/src/udp_route.c
@@ -16,6 +16,7 @@
#include "proxy_proto_v2.h"
#include "route.h"
#include "udp_route.h"
+#include "x_builtins.h"
#define UDP_MAX_PACKET 65535
@@ -176,6 +177,111 @@ static void upstream_read_cb(evutil_socket_t fd, short events, void *arg)
}
}
+static const char *udp_client_addr_string(
+ const struct udp_client *c,
+ char *buf,
+ size_t buf_len
+) {
+ void *addr;
+ uint16_t port;
+
+ if (c == NULL || buf == NULL || buf_len == 0) {
+ return NULL;
+ }
+
+ switch (c->client_addr.ss_family) {
+ case AF_INET: {
+ const struct sockaddr_in *sin =
+ (const struct sockaddr_in *)&c->client_addr;
+
+ addr = (void *)&sin->sin_addr;
+ port = ntohs(sin->sin_port);
+
+ if (inet_ntop(AF_INET, addr, buf, buf_len) == NULL) {
+ return NULL;
+ }
+
+ break;
+ }
+
+ case AF_INET6: {
+ const struct sockaddr_in6 *sin6 =
+ (const struct sockaddr_in6 *)&c->client_addr;
+
+ addr = (void *)&sin6->sin6_addr;
+ port = ntohs(sin6->sin6_port);
+
+ if (inet_ntop(AF_INET6, addr, buf, buf_len) == NULL) {
+ return NULL;
+ }
+
+ break;
+ }
+
+ default:
+ if (snprintf(buf, buf_len, "unknown") < 0) {
+ return NULL;
+ }
+ return buf;
+ }
+
+ if (snprintf(buf + strlen(buf), buf_len - strlen(buf), ":%u", port) < 0) {
+ return NULL;
+ }
+
+ return buf;
+}
+
+static int start_udp_builtin(struct udp_client *c)
+{
+ struct x_builtin_request req;
+ struct x_builtin_response res;
+ char client_addr[128];
+ int rc;
+
+ if (c == NULL || c->ctx == NULL || c->ctx->route == NULL) {
+ return -EINVAL;
+ }
+
+ memset(&req, 0, sizeof(req));
+
+ req.builtin = c->ctx->route->upstream.builtin;
+ req.client_addr = udp_client_addr_string(c, client_addr, sizeof(client_addr));
+
+ rc = x_builtin_handle(&req, &res);
+ if (rc < 0) {
+ return rc;
+ }
+
+ switch (res.action) {
+ case X_BUILTIN_ACTION_CLOSE:
+ if (res.data_len == 0) {
+ return 0;
+ }
+
+ if (sendto(
+ c->ctx->listen_fd,
+ res.data,
+ res.data_len,
+ 0,
+ (const struct sockaddr *)&c->client_addr,
+ c->client_addr_len) < 0) {
+ return -EVUTIL_SOCKET_ERROR();
+ }
+
+ return 0;
+
+ case X_BUILTIN_ACTION_DISCARD:
+ return 0;
+
+ case X_BUILTIN_ACTION_HANG:
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
static int connect_udp_upstream(struct udp_client *c, const struct endpoint *upstream)
{
if (upstream == NULL) {
@@ -183,6 +289,7 @@ static int connect_udp_upstream(struct udp_client *c, const struct endpoint *ups
}
switch (upstream->kind) {
+
case ENDPOINT_INET: {
struct sockaddr_in upstream_addr;
@@ -446,6 +553,27 @@ static void listen_read_cb(evutil_socket_t fd, short events, void *arg)
continue;
}
+ if (ctx->route->upstream.kind == ENDPOINT_BUILTIN) {
+ struct udp_client tmp;
+
+ memset(&tmp, 0, sizeof(tmp));
+ tmp.ctx = ctx;
+ tmp.fd = -1;
+ tmp.client_addr = client_addr;
+ tmp.client_addr_len = client_addr_len;
+ tmp.last_seen = time(NULL);
+
+ int rc = start_udp_builtin(&tmp);
+ if (rc < 0) {
+ LOG_ERROR("udp builtin failed",
+ "err", _LOGV(evutil_socket_error_to_string(-rc))
+ );
+ return;
+ }
+
+ continue;
+ }
+
struct udp_client *c = find_udp_client(ctx, &client_addr, client_addr_len);
if (c == NULL) {
c = create_udp_client(ctx, &client_addr, client_addr_len);
@@ -479,6 +607,7 @@ int start_udp_route(
}
if (r->upstream.kind != ENDPOINT_INET &&
+ r->upstream.kind != ENDPOINT_BUILTIN &&
r->upstream.kind != ENDPOINT_UNIX_DGRAM) {
LOG_ERROR("unsupported udp upstream endpoint",
"upstream", _LOGV_ENDPOINT(&r->upstream)
diff --git a/src/x_builtins.c b/src/x_builtins.c
new file mode 100644
index 0000000..bbde45f
--- /dev/null
+++ b/src/x_builtins.c
@@ -0,0 +1,97 @@
+#include "x_builtins.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+int x_builtin_parse(const char *s, enum x_builtin_upstream *out)
+{
+ if (s == NULL || out == NULL) {
+ return -EINVAL;
+ }
+
+ if (strcmp(s, "client_addr") == 0 || strcmp(s, "X_CLIENT_ADDR") == 0) {
+ *out = X_BUILTIN_CLIENT_ADDR;
+ return 0;
+ }
+
+ if (strcmp(s, "discard") == 0 || strcmp(s, "X_DISCARD") == 0) {
+ *out = X_BUILTIN_DISCARD;
+ return 0;
+ }
+
+ if (strcmp(s, "hang") == 0 || strcmp(s, "X_HANG") == 0) {
+ *out = X_BUILTIN_HANG;
+ return 0;
+ }
+
+ if (strcmp(s, "close") == 0 || strcmp(s, "X_CLOSE") == 0) {
+ *out = X_BUILTIN_CLOSE;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+const char *x_builtin_name(enum x_builtin_upstream builtin)
+{
+ switch (builtin) {
+ case X_BUILTIN_CLIENT_ADDR:
+ return "client_addr";
+ case X_BUILTIN_DISCARD:
+ return "discard";
+ case X_BUILTIN_HANG:
+ return "hang";
+ case X_BUILTIN_CLOSE:
+ return "close";
+ case X_BUILTIN_NONE:
+ default:
+ return "none";
+ }
+}
+
+int x_builtin_handle(
+ const struct x_builtin_request *req,
+ struct x_builtin_response *res
+) {
+ int n;
+
+ if (req == NULL || res == NULL) {
+ return -EINVAL;
+ }
+
+ memset(res, 0, sizeof(*res));
+
+ switch (req->builtin) {
+ case X_BUILTIN_CLIENT_ADDR:
+ if (req->client_addr == NULL) {
+ return -EINVAL;
+ }
+
+ n = snprintf(res->data, sizeof(res->data), "%s\n", req->client_addr);
+ if (n < 0 || (size_t)n >= sizeof(res->data)) {
+ return -ENOSPC;
+ }
+
+ res->data_len = (size_t)n;
+ res->action = X_BUILTIN_ACTION_CLOSE;
+ return 0;
+
+ case X_BUILTIN_DISCARD:
+ res->action = X_BUILTIN_ACTION_DISCARD;
+ return 0;
+
+ case X_BUILTIN_CLOSE:
+ res->data_len = 0;
+ res->action = X_BUILTIN_ACTION_CLOSE;
+ return 0;
+
+ case X_BUILTIN_HANG:
+ res->action = X_BUILTIN_ACTION_HANG;
+ return 0;
+
+ case X_BUILTIN_NONE:
+ default:
+ return -EINVAL;
+ }
+}
diff --git a/src/x_builtins.h b/src/x_builtins.h
new file mode 100644
index 0000000..aca0151
--- /dev/null
+++ b/src/x_builtins.h
@@ -0,0 +1,40 @@
+#ifndef X_BUILTINS_H
+#define X_BUILTINS_H
+
+#include <stddef.h>
+#include <sys/types.h>
+
+enum x_builtin_upstream {
+ X_BUILTIN_NONE = 0,
+ X_BUILTIN_CLIENT_ADDR,
+ X_BUILTIN_DISCARD,
+ X_BUILTIN_HANG,
+ X_BUILTIN_CLOSE,
+};
+
+enum x_builtin_action {
+ X_BUILTIN_ACTION_CLOSE,
+ X_BUILTIN_ACTION_DISCARD,
+ X_BUILTIN_ACTION_HANG,
+};
+
+struct x_builtin_request {
+ enum x_builtin_upstream builtin;
+ const char *client_addr;
+};
+
+struct x_builtin_response {
+ enum x_builtin_action action;
+ char data[256];
+ size_t data_len;
+};
+
+int x_builtin_parse(const char *s, enum x_builtin_upstream *out);
+const char *x_builtin_name(enum x_builtin_upstream builtin);
+
+int x_builtin_handle(
+ const struct x_builtin_request *req,
+ struct x_builtin_response *res
+);
+
+#endif
diff --git a/tinyproxy.conf b/tinyproxy.conf
index 67fa8f9..f042f9d 100644
--- a/tinyproxy.conf
+++ b/tinyproxy.conf
@@ -1,7 +1,32 @@
+# builtin://client_addr
+# Returns the observed client address as plain text, then closes.
+# Example response: 127.0.0.1:54321
+#
+# builtin://discard
+# TCP: reads and discards client data until the client closes or timeout expires.
+# UDP: silently drops each datagram.
+#
+# builtin://hang
+# Accepts the connection/datagram and intentionally produces no response.
+# TCP: the connection remains open until the client closes or the route idle timeout expires.
+# UDP: this is effectively a silent drop.
+#
+# builtin://close
+# Accepts the connection and immediately closes it without reading or writing.
+# For UDP, this is equivalent to discard/drop because UDP has no connection to close.
+
# listen upstream proto option
:80 10.0.1.1:80 tcp
:443 10.0.1.1:443 tcp proxy_v2,keep_alive
:12990 unix:/tmp/test.sock tcp
:12345 10.0.1.1:12345 udp idle_timeout=10,connect_timeout=17
:19132 10.0.1.1:19132 udp proxy_v2
-:12998 unix-dgram:/tmp/testd.sock udp
+:13000 unix-dgram:/tmp/testd.sock udp
+:12995 builtin://client_addr tcp
+:12996 builtin://hang tcp idle_timeout=3,connect_timeout=1
+:12997 builtin://discard tcp
+:12998 builtin://close tcp
+:12995 builtin://client_addr udp
+:12996 builtin://hang udp
+:12997 builtin://discard udp
+:12998 builtin://close udp