penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

commit 44819870202ffe8d6d391e9a002313d547ef7fbc

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-27T23:14:54Z
subjectAdded 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