commit b1e45e55a003dc011ad90abe3661a8a8406a72ac
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-25T01:50:47Z |
| subject | Basic tcp route |
commit b1e45e55a003dc011ad90abe3661a8a8406a72ac
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-25T01:50:47Z
Basic tcp route
---
.gitignore | 5 ++
Makefile | 73 +++++++++++++++
README.md | 9 ++
file_conf.c | 225 ++++++++++++++++++++++++++++++++++++++++++++++
file_conf.h | 12 +++
route.h | 34 +++++++
tcp_route.c | 220 +++++++++++++++++++++++++++++++++++++++++++++
tcp_route.h | 12 +++
tests/test_proxy.py | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++
tinyproxy.c | 146 ++++++++++++++++++++++++++++++
tinyproxy.conf | 5 ++
11 files changed, 991 insertions(+)
diff --git a/.gitignore b/.gitignore
index 845cda6..280e3f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
+bin/
+build/
+libevent2/
+*.log
+
# Prerequisites
*.d
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6fd8d6e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,73 @@
+export
+
+CC ?= cc
+AR := /usr/bin/ar
+RANLIB := /usr/bin/ranlib
+STRIP ?= strip
+
+PROJECT_ROOT := $(CURDIR)
+BIN_DIR := $(CURDIR)/bin
+
+SRC := tcp_route.c \
+ file_conf.c \
+ tinyproxy.c
+
+BIN := $(BIN_DIR)/tinyproxy
+
+BUILD_DIR := $(PROJECT_ROOT)/build
+
+LIBEVENT_SRC := $(PROJECT_ROOT)/libevent2
+LIBEVENT_PREFIX := $(BUILD_DIR)/libevent-install
+LIBEVENT_CORE_A := $(LIBEVENT_PREFIX)/lib/libevent_core.a
+
+CFLAGS ?= -Os -Wall -Wextra -ffunction-sections -fdata-sections
+CPPFLAGS += -I$(LIBEVENT_PREFIX)/include
+LDFLAGS += -L$(LIBEVENT_PREFIX)/lib
+
+ifeq ($(shell uname),Darwin)
+LDFLAGS += -Wl,-dead_strip
+else
+LDFLAGS += -Wl,--gc-sections
+endif
+
+LDLIBS += -levent_core
+
+all: $(BIN)
+
+$(BIN): $(SRC) $(LIBEVENT_CORE_A)
+ mkdir -p $(BIN_DIR)
+ $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(SRC) $(LDFLAGS) $(LDLIBS)
+
+strip: $(BIN)
+ $(STRIP) $(BIN)
+
+$(LIBEVENT_CORE_A):
+ cd $(LIBEVENT_SRC) && \
+ AR=$(AR) RANLIB=$(RANLIB) ./configure \
+ --prefix="$(LIBEVENT_PREFIX)" \
+ --disable-openssl \
+ --disable-samples \
+ --disable-libevent-regress \
+ --disable-debug-mode \
+ --disable-malloc-replacement \
+ --disable-thread-support \
+ --disable-dependency-tracking \
+ --enable-static \
+ --disable-shared
+ AR=$(AR) RANLIB=$(RANLIB) $(MAKE) -C $(LIBEVENT_SRC)
+ AR=$(AR) RANLIB=$(RANLIB) $(MAKE) -C $(LIBEVENT_SRC) install
+
+test: $(BIN)
+ python3 tests/test_proxy.py $(BIN)
+
+clean:
+ rm -rf $(BIN_DIR)
+
+clean-libevent:
+ -$(MAKE) -C $(LIBEVENT_SRC) distclean
+ rm -rf $(LIBEVENT_PREFIX)
+
+distclean: clean clean-libevent
+ rm -rf $(BUILD_DIR)
+
+.PHONY: all clean clean-libevent distclean test strip
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bbdfa93
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+tinyproxy
+
+This is a work in progress project.
+
+## Rules of thumb
+1. Work as a simple inet utils
+1. Be simple and effective
+1. Runs in foreground
+1. Uses libevent2 (for readability and future maintainance)
diff --git a/file_conf.c b/file_conf.c
new file mode 100644
index 0000000..4780bb1
--- /dev/null
+++ b/file_conf.c
@@ -0,0 +1,225 @@
+// file_conf.c
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include "file_conf.h"
+
+#define MAX_LINE_LEN 512
+
+static char *trim(char *s)
+{
+ while (isspace((unsigned char)*s)) {
+ s++;
+ }
+
+ if (*s == '\0') {
+ return s;
+ }
+
+ char *end = s + strlen(s) - 1;
+ while (end > s && isspace((unsigned char)*end)) {
+ *end = '\0';
+ end--;
+ }
+
+ return s;
+}
+
+static int parse_port(const char *s, uint16_t *out)
+{
+ char *end = NULL;
+ errno = 0;
+
+ unsigned long v = strtoul(s, &end, 10);
+
+ if (errno != 0 || *s == '\0' || *end != '\0' || v == 0 || v > 65535) {
+ return -1;
+ }
+
+ *out = (uint16_t)v;
+ return 0;
+}
+
+static int split_host_port(const char *input,
+ char *host, size_t host_len,
+ uint16_t *port)
+{
+ const char *colon = strrchr(input, ':');
+ if (!colon) {
+ return -1;
+ }
+
+ size_t hlen = (size_t)(colon - input);
+ const char *port_s = colon + 1;
+
+ if (hlen >= host_len || *port_s == '\0') {
+ return -1;
+ }
+
+ memcpy(host, input, hlen);
+ host[hlen] = '\0';
+
+ if (parse_port(port_s, port) != 0) {
+ return -1;
+ }
+
+ // Allow ":80" as shorthand for all interfaces.
+ if (host[0] == '\0') {
+ snprintf(host, host_len, "0.0.0.0");
+ }
+
+ return 0;
+}
+
+static int parse_proto(const char *s, enum proto *out)
+{
+ if (strcmp(s, "tcp") == 0) {
+ *out = PROTO_TCP;
+ return 0;
+ }
+
+ if (strcmp(s, "udp") == 0) {
+ *out = PROTO_UDP;
+ return 0;
+ }
+
+ return -1;
+}
+
+static int parse_route_line(char *line, struct route *route)
+{
+ char *fields[4] = {0};
+ size_t count = 0;
+
+ char *tok = strtok(line, " \t");
+ while (tok && count < 4) {
+ fields[count++] = tok;
+ tok = strtok(NULL, " \t");
+ }
+
+ if (tok != NULL) {
+ return -1; // too many fields
+ }
+
+ if (count < 3) {
+ return -1;
+ }
+
+ memset(route, 0, sizeof(*route));
+
+ if (split_host_port(fields[0],
+ route->listen_host, sizeof(route->listen_host),
+ &route->listen_port) != 0) {
+ return -1;
+ }
+
+ if (split_host_port(fields[1],
+ route->upstream_host, sizeof(route->upstream_host),
+ &route->upstream_port) != 0) {
+ return -1;
+ }
+
+ if (parse_proto(fields[2], &route->proto) != 0) {
+ return -1;
+ }
+
+ route->send_proxy_v2 = false;
+
+ if (count == 4) {
+ if (strcmp(fields[3], "proxyv2") != 0) {
+ return -1;
+ }
+
+ route->send_proxy_v2 = true;
+ }
+
+ return 0;
+}
+
+int load_routes_from_file(const char *path, struct route **routes_out, size_t *count_out)
+{
+ FILE *fp = fopen(path, "r");
+ if (!fp) {
+ return -errno;
+ }
+
+ struct route *routes = NULL;
+ size_t count = 0;
+ size_t cap = 0;
+
+ char buf[MAX_LINE_LEN];
+ unsigned int line_no = 0;
+
+ while (fgets(buf, sizeof(buf), fp)) {
+ line_no++;
+
+ // Reject overlong lines.
+ if (!strchr(buf, '\n') && !feof(fp)) {
+ fclose(fp);
+ free(routes);
+ return -E2BIG;
+ }
+
+ char *line = trim(buf);
+
+ if (*line == '\0' || *line == '#') {
+ continue;
+ }
+
+ // Strip inline comments.
+ char *hash = strchr(line, '#');
+ if (hash) {
+ *hash = '\0';
+ line = trim(line);
+ if (*line == '\0') {
+ continue;
+ }
+ }
+
+ if (count == cap) {
+ size_t new_cap = cap ? cap * 2 : 8;
+ struct route *new_routes = realloc(routes, new_cap * sizeof(*routes));
+ if (!new_routes) {
+ fclose(fp);
+ free(routes);
+ return -ENOMEM;
+ }
+
+ routes = new_routes;
+ cap = new_cap;
+ }
+
+ if (parse_route_line(line, &routes[count]) != 0) {
+ fprintf(stderr, "%s:%u: invalid route config\n", path, line_no);
+ fclose(fp);
+ free(routes);
+ return -EINVAL;
+ }
+
+ count++;
+ }
+
+ if (ferror(fp)) {
+ int err = errno;
+ fclose(fp);
+ free(routes);
+ return -err;
+ }
+
+ fclose(fp);
+
+ *routes_out = routes;
+ *count_out = count;
+
+ return 0;
+}
+
+void free_routes(struct route *routes)
+{
+ free(routes);
+}
diff --git a/file_conf.h b/file_conf.h
new file mode 100644
index 0000000..2a76145
--- /dev/null
+++ b/file_conf.h
@@ -0,0 +1,12 @@
+#ifndef FILE_CONF_H
+#define FILE_CONF_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "route.h"
+
+int load_routes_from_file(const char *path, struct route **routes_out, size_t *count_out);
+void free_routes(struct route *routes);
+
+#endif
diff --git a/route.h b/route.h
new file mode 100644
index 0000000..90ecea0
--- /dev/null
+++ b/route.h
@@ -0,0 +1,34 @@
+#ifndef ROUTE_H
+#define ROUTE_H
+
+#include <event2/bufferevent.h>
+#include <stdbool.h>
+
+enum proto {
+ PROTO_TCP,
+ PROTO_UDP,
+};
+
+struct route {
+ char listen_host[64];
+ uint16_t listen_port;
+
+ char upstream_host[64];
+ uint16_t upstream_port;
+
+ enum proto proto;
+ bool send_proxy_v2;
+};
+
+typedef struct conn_s {
+ struct bufferevent *client;
+ struct bufferevent *upstream;
+} conn_t;
+
+struct listener_ctx {
+ struct event_base *base;
+ const struct route *route;
+ struct evconnlistener *listener;
+};
+
+#endif
diff --git a/tcp_route.c b/tcp_route.c
new file mode 100644
index 0000000..0421b67
--- /dev/null
+++ b/tcp_route.c
@@ -0,0 +1,220 @@
+#include <event2/bufferevent.h>
+#include <event2/buffer.h>
+#include <event2/event.h>
+#include <event2/listener.h>
+#include <event2/util.h>
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "file_conf.h"
+#include "tcp_route.h"
+
+static void free_conn(conn_t *conn) {
+ if (conn == NULL) {
+ return;
+ }
+
+ if (conn->client != NULL) {
+ bufferevent_free(conn->client);
+ }
+
+ if (conn->upstream != NULL) {
+ bufferevent_free(conn->upstream);
+ }
+
+ free(conn);
+}
+
+static void pipe_read_cb(struct bufferevent *src, void *arg) {
+ conn_t *conn = arg;
+
+ struct bufferevent *dst;
+
+ if (src == conn->client) {
+ dst = conn->upstream;
+ } else if (src == conn->upstream) {
+ dst = conn->client;
+ } else {
+ return;
+ }
+
+ struct evbuffer *input = bufferevent_get_input(src);
+ struct evbuffer *output = bufferevent_get_output(dst);
+
+ evbuffer_add_buffer(output, input);
+}
+
+static void event_cb(struct bufferevent *bev, short events, void *arg) {
+ conn_t *conn = arg;
+
+ if (events & BEV_EVENT_CONNECTED) {
+ bufferevent_enable(conn->client, EV_READ | EV_WRITE);
+ bufferevent_enable(conn->upstream, EV_READ | EV_WRITE);
+ return;
+ }
+
+ if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR | BEV_EVENT_TIMEOUT)) {
+ if (events & BEV_EVENT_ERROR) {
+ int err = EVUTIL_SOCKET_ERROR();
+ fprintf(stderr, "connection error: %s\n",
+ evutil_socket_error_to_string(err));
+ }
+
+ free_conn(conn);
+ }
+
+ (void)bev;
+}
+
+static void accept_cb(
+ struct evconnlistener *listener,
+ evutil_socket_t client_fd,
+ struct sockaddr *addr,
+ int socklen,
+ void *arg
+) {
+ (void)listener;
+ (void)addr;
+ (void)socklen;
+
+ struct listener_ctx *ctx = arg;
+ const struct route *r = ctx->route;
+ struct event_base *base = ctx->base;
+
+ conn_t *conn = calloc(1, sizeof(*conn));
+ if (conn == NULL) {
+ evutil_closesocket(client_fd);
+ return;
+ }
+
+ conn->client = bufferevent_socket_new(base, client_fd, BEV_OPT_CLOSE_ON_FREE);
+ conn->upstream = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
+
+ if (conn->client == NULL || conn->upstream == NULL) {
+ free_conn(conn);
+ return;
+ }
+
+ bufferevent_setcb(
+ conn->client,
+ pipe_read_cb,
+ NULL,
+ event_cb,
+ conn
+ );
+
+ bufferevent_setcb(
+ conn->upstream,
+ pipe_read_cb,
+ NULL,
+ event_cb,
+ conn
+ );
+
+ struct sockaddr_in upstream_addr;
+ memset(&upstream_addr, 0, sizeof(upstream_addr));
+ upstream_addr.sin_family = AF_INET;
+ upstream_addr.sin_port = htons(r->upstream_port);
+
+ if (inet_pton(AF_INET, r->upstream_host, &upstream_addr.sin_addr) != 1) {
+ fprintf(stderr, "invalid upstream address\n");
+ free_conn(conn);
+ return;
+ }
+
+ if (bufferevent_socket_connect(
+ conn->upstream,
+ (struct sockaddr *)&upstream_addr,
+ sizeof(upstream_addr)
+ ) < 0) {
+ fprintf(stderr, "upstream connect failed\n");
+ free_conn(conn);
+ 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.
+ */
+}
+
+static void accept_error_cb(struct evconnlistener *listener, void *arg) {
+ struct event_base *base = arg;
+ int err = EVUTIL_SOCKET_ERROR();
+
+ fprintf(stderr, "accept error: %s\n",
+ evutil_socket_error_to_string(err));
+
+ evconnlistener_free(listener);
+ event_base_loopexit(base, NULL);
+}
+
+int start_tcp_route(
+ struct event_base *base,
+ const struct route *r,
+ struct listener_ctx **out)
+{
+ 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);
+
+ if (inet_pton(AF_INET, r->listen_host, &listen_addr.sin_addr) != 1) {
+ fprintf(stderr, "invalid listen address: %s\n", r->listen_host);
+ return -EINVAL;
+ }
+
+ struct listener_ctx *ctx = calloc(1, sizeof(*ctx));
+ if (ctx == NULL) {
+ return -ENOMEM;
+ }
+
+ ctx->base = base;
+ ctx->route = r;
+
+ ctx->listener = evconnlistener_new_bind(
+ base,
+ accept_cb,
+ ctx,
+ LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
+ 128,
+ (struct sockaddr *)&listen_addr,
+ sizeof(listen_addr)
+ );
+
+ if (ctx->listener == NULL) {
+ fprintf(stderr, "evconnlistener_new_bind failed\n");
+ free(ctx);
+ return -EADDRINUSE;
+ }
+
+ evconnlistener_set_error_cb(ctx->listener, accept_error_cb);
+
+ fprintf(stderr, "listening on %s:%u, forwarding to %s:%u\n",
+ r->listen_host, r->listen_port,
+ r->upstream_host, r->upstream_port);
+
+ *out = ctx;
+ return 0;
+}
+
+void free_tcp_route(struct listener_ctx *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->listener != NULL) {
+ evconnlistener_free(ctx->listener);
+ }
+
+ free(ctx);
+}
diff --git a/tcp_route.h b/tcp_route.h
new file mode 100644
index 0000000..99e2cc5
--- /dev/null
+++ b/tcp_route.h
@@ -0,0 +1,12 @@
+#ifndef TCP_ROUTE_H
+#define TCP_ROUTE_H
+
+#include "route.h"
+
+int start_tcp_route(
+ struct event_base *base,
+ const struct route *r,
+ struct listener_ctx **out);
+
+void free_tcp_route(struct listener_ctx *ctx);
+#endif
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
new file mode 100755
index 0000000..5a9e5cd
--- /dev/null
+++ b/tests/test_proxy.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+
+import asyncio
+import os
+import resource
+import signal
+import subprocess
+import sys
+import time
+import tempfile
+
+LISTEN_HOST = "127.0.0.1"
+PROXY_PORT = 31232
+BACKEND_PORT = 41232
+
+def raise_fd_limit(wanted: int) -> None:
+ soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+
+ if soft >= wanted:
+ return
+
+ new_soft = min(wanted, hard)
+
+ try:
+ resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard))
+ print(f"raised RLIMIT_NOFILE soft limit: {soft} -> {new_soft}")
+ except OSError as exc:
+ print(f"warning: failed to raise fd limit: {exc}", file=sys.stderr)
+ print(f"current RLIMIT_NOFILE soft={soft} hard={hard}", file=sys.stderr)
+
+
+async def wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:
+ deadline = time.monotonic() + timeout
+
+ while time.monotonic() < deadline:
+ try:
+ reader, writer = await asyncio.open_connection(host, port)
+ writer.close()
+ await writer.wait_closed()
+ return
+ except OSError:
+ await asyncio.sleep(0.05)
+
+ raise RuntimeError(f"timed out waiting for {host}:{port}")
+
+
+async def echo_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
+ try:
+ while True:
+ data = await reader.read(65536)
+ if not data:
+ return
+
+ writer.write(data)
+ await writer.drain()
+
+ except (ConnectionResetError, BrokenPipeError):
+ return
+
+ finally:
+ writer.close()
+
+ try:
+ await writer.wait_closed()
+ except OSError:
+ pass
+
+
+async def recv_exact(reader: asyncio.StreamReader, size: int) -> bytes:
+ chunks = []
+ remaining = size
+
+ while remaining > 0:
+ chunk = await reader.read(min(65536, remaining))
+ if not chunk:
+ break
+
+ chunks.append(chunk)
+ remaining -= len(chunk)
+
+ return b"".join(chunks)
+
+
+async def proxy_roundtrip(payload: bytes, timeout: float = 10.0) -> bytes:
+ reader, writer = await asyncio.open_connection(LISTEN_HOST, PROXY_PORT)
+
+ try:
+ writer.write(payload)
+ await writer.drain()
+
+ return await asyncio.wait_for(
+ recv_exact(reader, len(payload)),
+ timeout=timeout,
+ )
+
+ finally:
+ writer.close()
+
+ try:
+ await writer.wait_closed()
+ except OSError:
+ pass
+
+
+async def test_small_roundtrip() -> None:
+ payload = b"hello through proxy\n"
+ got = await proxy_roundtrip(payload)
+ assert got == payload, f"small roundtrip mismatch: {got!r}"
+
+
+async def test_large_roundtrip() -> None:
+ payload = b"0123456789abcdef" * 131072 # 2 MiB
+ got = await proxy_roundtrip(payload, timeout=30.0)
+ assert got == payload, f"large roundtrip mismatch: got {len(got)} bytes"
+
+
+async def test_many_sequential_connections(count: int) -> None:
+ for i in range(count):
+ payload = f"message-{i}\n".encode()
+ got = await proxy_roundtrip(payload)
+ assert got == payload, f"sequential connection {i} failed"
+
+
+async def run_one_concurrent(i: int, payload_size: int, sem: asyncio.Semaphore) -> None:
+ async with sem:
+ prefix = f"worker-{i}-".encode()
+ payload = (prefix * ((payload_size // len(prefix)) + 1))[:payload_size]
+
+ got = await proxy_roundtrip(payload, timeout=30.0)
+
+ if got != payload:
+ raise AssertionError(
+ f"worker {i} mismatch: expected {len(payload)} bytes, got {len(got)}"
+ )
+
+
+async def test_concurrent_connections(
+ total: int,
+ concurrency: int,
+ payload_size: int,
+) -> None:
+ sem = asyncio.Semaphore(concurrency)
+
+ tasks = [
+ asyncio.create_task(run_one_concurrent(i, payload_size, sem))
+ for i in range(total)
+ ]
+
+ done = 0
+
+ for task in asyncio.as_completed(tasks):
+ await task
+ done += 1
+
+ if done % 1000 == 0:
+ print(f" completed {done}/{total}")
+
+
+async def main_async(proxy_bin: str) -> int:
+ raise_fd_limit(65535)
+
+ backend_server = await asyncio.start_server(
+ echo_handler,
+ LISTEN_HOST,
+ BACKEND_PORT,
+ backlog=4096,
+ )
+
+ with tempfile.NamedTemporaryFile("w", suffix=".conf") as f:
+ f.write(f"{LISTEN_HOST}:{PROXY_PORT} {LISTEN_HOST}:{BACKEND_PORT} tcp\n")
+ f.flush()
+
+ proxy = subprocess.Popen(
+ [proxy_bin, "-c", f.name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+
+ await asyncio.sleep(0.2)
+
+ if proxy.poll() is not None:
+ stderr = proxy.stderr.read() if proxy.stderr else ""
+ raise RuntimeError(
+ f"proxy exited early with code {proxy.returncode}\n{stderr}"
+ )
+
+ try:
+ await wait_for_port(LISTEN_HOST, BACKEND_PORT)
+ await wait_for_port(LISTEN_HOST, PROXY_PORT)
+
+ print("running test_small_roundtrip...")
+ await test_small_roundtrip()
+ print("ok test_small_roundtrip")
+
+ print("running test_large_roundtrip...")
+ await test_large_roundtrip()
+ print("ok test_large_roundtrip")
+
+ print("running test_many_sequential_connections 1000...")
+ await test_many_sequential_connections(1000)
+ print("ok test_many_sequential_connections")
+
+ total = int(os.environ.get("TOTAL", "10000"))
+ concurrency = int(os.environ.get("CONCURRENCY", "10000"))
+ payload_size = int(os.environ.get("PAYLOAD_SIZE", "1024"))
+
+ print(
+ f"running test_concurrent_connections "
+ f"total={total} concurrency={concurrency} payload_size={payload_size}..."
+ )
+
+ await test_concurrent_connections(
+ total=total,
+ concurrency=concurrency,
+ payload_size=payload_size,
+ )
+
+ print("ok test_concurrent_connections")
+ print("all tests passed")
+ return 0
+
+ finally:
+ proxy.terminate()
+
+ try:
+ proxy.wait(timeout=2.0)
+ except subprocess.TimeoutExpired:
+ proxy.kill()
+ proxy.wait(timeout=2.0)
+
+ backend_server.close()
+ await backend_server.wait_closed()
+
+ stderr = proxy.stderr.read() if proxy.stderr else ""
+ if stderr:
+ print("\nproxy stderr:")
+ print(stderr)
+
+
+def main() -> int:
+ if len(sys.argv) != 2:
+ print(f"usage: {sys.argv[0]} tinyproxy", file=sys.stderr)
+ return 2
+
+ return asyncio.run(main_async(sys.argv[1]))
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tinyproxy.c b/tinyproxy.c
new file mode 100644
index 0000000..c4b9416
--- /dev/null
+++ b/tinyproxy.c
@@ -0,0 +1,146 @@
+#include <event2/event.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "file_conf.h"
+#include "tcp_route.h"
+
+static void usage(FILE *out, const char *prog)
+{
+ fprintf(out,
+ "usage: %s [-c config-file]\n"
+ "\n"
+ "options:\n"
+ " -c FILE path to config file (default: tinyproxy.conf)\n"
+ " -v show the version\n"
+ " -h show this help\n",
+ prog);
+}
+
+int main(int argc, char **argv)
+{
+ const char *conf_path = "tinyproxy.conf";
+ int opt;
+
+ while ((opt = getopt(argc, argv, "c:h:v")) != -1) {
+ switch (opt) {
+ case 'c':
+ if (optarg == NULL || optarg[0] == '\0') {
+ fprintf(stderr, "missing config path after -c\n");
+ return 2;
+ }
+ conf_path = optarg;
+ break;
+
+ case 'h':
+ usage(stdout, argv[0]);
+ return 0;
+
+ case 'v':
+ fprintf(stdout, "tinyproxy v0.0.1\n");
+ return 0;
+
+ default:
+ usage(stderr, argv[0]);
+ return 2;
+ }
+ }
+
+ if (optind != argc) {
+ fprintf(stderr, "unexpected argument: %s\n", argv[optind]);
+ usage(stderr, argv[0]);
+ return 2;
+ }
+
+ struct route *routes = NULL;
+ size_t route_count = 0;
+
+ int rc = load_routes_from_file(conf_path, &routes, &route_count);
+ if (rc != 0) {
+ fprintf(stderr, "failed to load config %s: %s\n",
+ conf_path, strerror(-rc));
+ return 1;
+ }
+
+ if (route_count == 0) {
+ fprintf(stderr, "config %s has no routes\n", conf_path);
+ free_routes(routes);
+ return 1;
+ }
+
+ struct listener_ctx **tcp_ctxs = calloc(route_count, sizeof(*tcp_ctxs));
+ if (tcp_ctxs == NULL) {
+ free_routes(routes);
+ return 1;
+ }
+
+ size_t tcp_ctx_count = 0;
+
+ struct event_base *base = event_base_new();
+ if (base == NULL) {
+ fprintf(stderr, "event_base_new failed\n");
+ return 1;
+ }
+
+ for (size_t i = 0; i < route_count; i++) {
+ const struct route *r = &routes[i];
+
+ switch (r->proto) {
+ case PROTO_TCP:
+ rc = start_tcp_route(base, r, &tcp_ctxs[tcp_ctx_count]);
+ if (rc == 0) {
+ tcp_ctx_count++;
+ }
+ break;
+
+ case PROTO_UDP:
+ // rc = start_udp_route(r);
+ fprintf(stderr, "skipping udp route: Not implemented yet\n");
+ break;
+
+ default:
+ fprintf(stderr, "route %zu has unknown protocol\n", i);
+ rc = -EINVAL;
+ break;
+ }
+
+ if (rc != 0) {
+ fprintf(stderr,
+ "failed to start route %zu: %s:%u -> %s:%u: %s\n",
+ i,
+ r->listen_host, r->listen_port,
+ r->upstream_host, r->upstream_port,
+ strerror(-rc));
+
+ for (size_t j = 0; j < tcp_ctx_count; j++) {
+ free_tcp_route(tcp_ctxs[j]);
+ }
+
+ free(tcp_ctxs);
+ event_base_free(base);
+ free_routes(routes);
+ return 1;
+ }
+ }
+
+ rc = event_base_dispatch(base);
+
+ for (size_t i = 0; i < tcp_ctx_count; i++) {
+ free_tcp_route(tcp_ctxs[i]);
+ }
+
+ free(tcp_ctxs);
+ event_base_free(base);
+ free_routes(routes);
+
+ if (rc != 0) {
+ fprintf(stderr, "event loop failed: %s\n", strerror(-rc));
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tinyproxy.conf b/tinyproxy.conf
new file mode 100644
index 0000000..df3f5e0
--- /dev/null
+++ b/tinyproxy.conf
@@ -0,0 +1,5 @@
+# listen upstream proto option
+:80 10.0.1.1:80 tcp
+:443 10.0.1.1:443 tcp proxyv2
+:12345 10.0.1.1:12345 udp
+:19132 10.0.1.1:19132 udp proxyv2