commit a60a049b1c1c5f34b395531db968b7c6be978e4b
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-26T16:17:53Z |
| subject | Added udp_route with updated README |
commit a60a049b1c1c5f34b395531db968b7c6be978e4b
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-26T16:17:53Z
Added udp_route with updated README
---
Makefile | 1 +
README.md | 153 ++++++++++++-
TODO.md | 5 +
route.h | 25 ---
tcp_route.c | 33 ++-
tcp_route.h | 6 +-
tests/run_tests.py | 26 ++-
tests/support.py | 208 +++++++++++++++++-
tests/test_udp_basic.py | 39 ++++
tests/test_udp_proxy_v2.py | 63 ++++++
tinyproxy.c | 19 +-
udp_route.c | 531 +++++++++++++++++++++++++++++++++++++++++++++
udp_route.h | 16 ++
13 files changed, 1078 insertions(+), 47 deletions(-)
diff --git a/Makefile b/Makefile
index 183261d..fe9ef1f 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@ SRC := klog.c \
signal.c \
route.c \
tcp_route.c \
+ udp_route.c \
file_conf.c \
tinyproxy.c
diff --git a/README.md b/README.md
index 304a0ea..c12158d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,150 @@
-tinyproxy
+# tinyproxy
-This is a work in progress project.
+This is a work-in-progress tiny L4 proxy.
+
+The goal is not to become HAProxy, Envoy, nginx, or a general-purpose load balancer but to stay small and practical for simple inetd-style forwarding tasks.
## Rule of thumb
-1. Should work like an inet utils
-1. Be simple and effective
-1. Runs in foreground
-1. Uses libevent2 (for readability and future maintainance)
+
+1. Should feel like an inet utility.
+2. Be simple and effective.
+3. Run in the foreground.
+4. Use libevent2 for readability and future maintainability.
+5. Prefer explicit behavior over magic.
+6. Avoid protocol-specific features unless they are small and practical.
+
+## Configuration
+
+Configuration is line-based:
+
+```text
+LISTEN_HOST:LISTEN_PORT UPSTREAM_HOST:UPSTREAM_PORT PROTO [OPTIONS...]
+```
+
+Example TCP route:
+
+```text
+127.0.0.1:31232 127.0.0.1:41232 tcp
+```
+
+Example UDP route:
+
+```text
+127.0.0.1:31232 127.0.0.1:41232 udp
+```
+
+Example TCP route with Proxy Protocol v2:
+
+```text
+127.0.0.1:31232 127.0.0.1:41232 tcp proxy_v2
+```
+
+Example UDP route with Proxy Protocol v2:
+
+```text
+127.0.0.1:31232 127.0.0.1:41232 udp proxy_v2
+```
+
+Run with:
+
+```sh
+tinyproxy -c tinyproxy.conf
+```
+
+## Supported protocols
+
+### TCP
+
+TCP routes accept client connections and forward bytes to the configured upstream.
+
+Each accepted client connection creates one upstream connection.
+
+### UDP
+
+UDP routes listen for datagrams and forward them to the configured upstream.
+
+UDP is connectionless, but the proxy keeps a small per-client mapping internally so upstream replies can be sent back to the correct original client address.
+
+The current UDP implementation is intentionally simple:
+
+- IPv4 only for now.
+- One upstream UDP socket per observed client address.
+- Idle client mappings are expired after a timeout.
+- No attempt is made to provide delivery guarantees.
+- No attempt is made to reassemble fragmented UDP traffic.
+
+## Proxy Protocol v2
+
+The `proxy_v2` option makes tinyproxy send a Proxy Protocol v2 header to the upstream server.
+
+This is useful when the upstream server needs to know the original client address and port, but the proxy itself would otherwise hide that information.
+
+### TCP Proxy Protocol v2
+
+For TCP routes, tinyproxy writes one Proxy Protocol v2 header before forwarding the client stream.
+
+The upstream must understand Proxy Protocol v2. If it does not, the upstream will see binary header bytes before the application payload and will likely fail.
+
+### UDP Proxy Protocol v2
+
+For UDP routes, tinyproxy prepends a Proxy Protocol v2 header to each forwarded client datagram.
+
+The upstream receives:
+
+```text
+PROXY_V2_HEADER + ORIGINAL_UDP_PAYLOAD
+```
+
+For IPv4 UDP, the header uses:
+
+```text
+version/command = PROXY
+family/proto = INET/DGRAM
+address length = 12
+```
+
+This behavior is intended to match the useful subset of Proxy Protocol v2 used by load balancers that need to preserve client address information for UDP services.
+
+AWS documents Proxy Protocol v2 support on Network Load Balancer target groups, and its AWS networking blog notes that UDP is supported even though the walkthrough focuses on TCP:
+
+- [AWS: Preserving client IP address with Proxy protocol v2 and Network Load Balancer](https://aws.amazon.com/blogs/networking-and-content-delivery/preserving-client-ip-address-with-proxy-protocol-v2-and-network-load-balancer/)
+- [AWS: Target groups for your Network Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html)
+
+tinyproxy adds a Proxy Protocol v2 header so the upstream can recover:
+
+- source IP
+- source port
+- destination IP
+- destination port
+- transport protocol
+
+## Non-goals
+
+tinyproxy is not trying to provide:
+
+- TLS termination
+- HTTP routing
+- retries
+- load balancing
+- service discovery
+- health checks
+- dynamic configuration reloads
+- QUIC awareness
+- full AWS NLB emulation
+- full HAProxy compatibility
+
+Use HAProxy, Envoy, nginx, Cilium, or a real load balancer if you need those.
+
+## Development status
+
+This project is still experimental.
+
+Currently tested areas include:
+
+- TCP roundtrip forwarding
+- TCP stress forwarding
+- TCP Proxy Protocol v2 forwarding
+- UDP roundtrip forwarding
+- UDP Proxy Protocol v2 forwarding
+
+Expect rough edges.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..2a84ee1
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,5 @@
+Forwarding
+* IPv6 <-> IPv4
+
+options
+* udp timeout
diff --git a/route.h b/route.h
index 1c492b2..cbe0502 100644
--- a/route.h
+++ b/route.h
@@ -27,31 +27,6 @@ struct worker {
size_t id;
};
-struct accepted_client {
- evutil_socket_t fd;
- struct sockaddr_storage peer_addr;
- socklen_t peer_addr_len;
- const struct route *route;
-};
-
-typedef struct conn_s {
- struct worker *owner;
- const struct route *route;
-
- struct bufferevent *client;
- struct bufferevent *upstream;
-
- struct sockaddr_storage peer_addr;
- socklen_t peer_addr_len;
-} conn_t;
-
-struct listener_ctx {
- struct event_base *accept_base;
- struct worker *worker;
- const struct route *route;
- struct evconnlistener *listener;
-};
-
void route_options_str(const struct route *r, char *buf, size_t buflen);
#endif
diff --git a/tcp_route.c b/tcp_route.c
index 554adba..8f87227 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -16,6 +16,31 @@
#include "route.h"
#include "tcp_route.h"
+struct tcp_route_ctx {
+ struct event_base *accept_base;
+ struct worker *worker;
+ const struct route *route;
+ struct evconnlistener *listener;
+};
+
+typedef struct conn_s {
+ struct worker *owner;
+ const struct route *route;
+
+ struct bufferevent *client;
+ struct bufferevent *upstream;
+
+ struct sockaddr_storage peer_addr;
+ socklen_t peer_addr_len;
+} conn_t;
+
+struct accepted_client {
+ evutil_socket_t fd;
+ struct sockaddr_storage peer_addr;
+ socklen_t peer_addr_len;
+ const struct route *route;
+};
+
static void free_conn(conn_t *conn) {
if (conn == NULL) {
return;
@@ -198,7 +223,7 @@ static void accept_cb(
) {
(void)listener;
- struct listener_ctx *ctx = arg;
+ struct tcp_route_ctx *ctx = arg;
struct accepted_client ac = {
.fd = client_fd,
.route = ctx->route,
@@ -229,7 +254,7 @@ static void accept_error_cb(struct evconnlistener *listener, void *arg) {
int start_tcp_route(
struct worker *w,
const struct route *r,
- struct listener_ctx **out)
+ struct tcp_route_ctx **out)
{
struct sockaddr_in listen_addr;
memset(&listen_addr, 0, sizeof(listen_addr));
@@ -241,7 +266,7 @@ int start_tcp_route(
return -EINVAL;
}
- struct listener_ctx *ctx = calloc(1, sizeof(*ctx));
+ struct tcp_route_ctx *ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL) {
return -ENOMEM;
}
@@ -285,7 +310,7 @@ int start_tcp_route(
return 0;
}
-void free_tcp_route(struct listener_ctx *ctx)
+void free_tcp_route(struct tcp_route_ctx *ctx)
{
if (ctx == NULL) {
return;
diff --git a/tcp_route.h b/tcp_route.h
index 32b5a0b..6b3fb89 100644
--- a/tcp_route.h
+++ b/tcp_route.h
@@ -3,10 +3,12 @@
#include "route.h"
+struct tcp_route_ctx;
+
int start_tcp_route(
struct worker *w,
const struct route *r,
- struct listener_ctx **out);
+ struct tcp_route_ctx **out);
-void free_tcp_route(struct listener_ctx *ctx);
+void free_tcp_route(struct tcp_route_ctx *ctx);
#endif
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 4d5d170..8d3a523 100644
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -4,19 +4,31 @@ import asyncio
import os
import sys
-from .support import SkipTest, raise_fd_limit, run_default_tinyproxy
+from .support import (
+ SkipTest,
+ raise_fd_limit,
+ run_default_tcp_tinyproxy,
+ run_default_udp_tinyproxy,
+)
from . import test_tcp_basic
from . import test_tcp_stress
+from . import test_udp_basic
from . import test_haproxy_proxy_v2
+from . import test_udp_proxy_v2
-DEFAULT_PROXY_TEST_MODULES = [
+TCP_PROXY_TEST_MODULES = [
test_tcp_basic,
test_tcp_stress,
]
+UDP_PROXY_TEST_MODULES = [
+ test_udp_basic,
+]
+
STANDALONE_TEST_MODULES = [
test_haproxy_proxy_v2,
+ test_udp_proxy_v2,
]
@@ -49,8 +61,14 @@ async def main_async(proxy_bin: str) -> int:
passed = 0
skipped = 0
- async with run_default_tinyproxy(proxy_bin):
- for module in DEFAULT_PROXY_TEST_MODULES:
+ async with run_default_tcp_tinyproxy(proxy_bin):
+ for module in TCP_PROXY_TEST_MODULES:
+ p, s = await run_module_tests(module)
+ passed += p
+ skipped += s
+
+ async with run_default_udp_tinyproxy(proxy_bin):
+ for module in UDP_PROXY_TEST_MODULES:
p, s = await run_module_tests(module)
passed += p
skipped += s
diff --git a/tests/support.py b/tests/support.py
index d1d1c37..6e031df 100644
--- a/tests/support.py
+++ b/tests/support.py
@@ -5,6 +5,8 @@ import subprocess
import sys
import tempfile
import time
+import struct
+import ipaddress
from contextlib import asynccontextmanager
from dataclasses import dataclass
@@ -18,11 +20,153 @@ LISTEN_HOST = "127.0.0.1"
PROXY_PORT = 31232
BACKEND_PORT = 41232
+PROXY_V2_SIG = b"\r\n\r\n\x00\r\nQUIT\n"
+
class SkipTest(Exception):
pass
+class UDPClientProtocol(asyncio.DatagramProtocol):
+ def __init__(self, payload: bytes, future: asyncio.Future[bytes]) -> None:
+ self.payload = payload
+ self.future = future
+ self.transport: asyncio.DatagramTransport | None = None
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.transport = transport # type: ignore[assignment]
+ self.transport.sendto(self.payload)
+
+ def datagram_received(self, data: bytes, addr) -> None:
+ if not self.future.done():
+ self.future.set_result(data)
+
+ def error_received(self, exc: Exception) -> None:
+ if not self.future.done():
+ self.future.set_exception(exc)
+
+class UDPProxyV2EchoServerProtocol(asyncio.DatagramProtocol):
+ def __init__(self) -> None:
+ self.transport: asyncio.DatagramTransport | None = None
+ self.seen: asyncio.Future[None] | None = None
+ self.error: Exception | None = None
+ self.last_src: tuple[str, int] | None = None
+ self.last_dst: tuple[str, int] | None = None
+ self.last_raw: bytes | None = None
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.transport = transport # type: ignore[assignment]
+ loop = asyncio.get_running_loop()
+ self.seen = loop.create_future()
+
+ def datagram_received(self, data: bytes, addr) -> None:
+ self.last_raw = data
+
+ try:
+ src, dst, payload = parse_proxy_v2_udp4_packet(data)
+ except Exception as exc:
+ self.error = exc
+ if self.seen is not None and not self.seen.done():
+ self.seen.set_exception(exc)
+ return
+
+ self.last_src = src
+ self.last_dst = dst
+
+ if self.seen is not None and not self.seen.done():
+ self.seen.set_result(None)
+
+ if self.transport is not None:
+ self.transport.sendto(payload, addr)
+
+class UDPRoundtripClientProtocol(asyncio.DatagramProtocol):
+ def __init__(self) -> None:
+ self.transport: asyncio.DatagramTransport | None = None
+ self.pending: asyncio.Future[bytes] | None = None
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.transport = transport # type: ignore[assignment]
+
+ def datagram_received(self, data: bytes, addr) -> None:
+ if self.pending is not None and not self.pending.done():
+ self.pending.set_result(data)
+
+ def error_received(self, exc: Exception) -> None:
+ if self.pending is not None and not self.pending.done():
+ self.pending.set_exception(exc)
+
+ async def roundtrip(self, payload: bytes, timeout: float = 3.0) -> bytes:
+ if self.transport is None:
+ raise RuntimeError("UDP transport is not ready")
+
+ if self.pending is not None and not self.pending.done():
+ raise RuntimeError("UDP roundtrip already in progress")
+
+ loop = asyncio.get_running_loop()
+ self.pending = loop.create_future()
+
+ self.transport.sendto(payload)
+
+ try:
+ return await asyncio.wait_for(self.pending, timeout=timeout)
+ finally:
+ self.pending = None
+
+@asynccontextmanager
+async def udp_proxy_client():
+ loop = asyncio.get_running_loop()
+
+ transport, protocol = await loop.create_datagram_endpoint(
+ UDPRoundtripClientProtocol,
+ remote_addr=(LISTEN_HOST, PROXY_PORT),
+ )
+
+ try:
+ yield protocol
+ finally:
+ transport.close()
+
+def parse_proxy_v2_udp4_packet(data: bytes) -> tuple[tuple[str, int], tuple[str, int], bytes]:
+ if len(data) < 28:
+ raise ValueError(f"packet too short for PROXY v2 UDP4 header: {len(data)} bytes")
+
+ if data[:12] != PROXY_V2_SIG:
+ raise ValueError("bad PROXY v2 signature")
+
+ ver_cmd = data[12]
+ if ver_cmd != 0x21:
+ raise ValueError(f"bad PROXY v2 version/cmd: 0x{ver_cmd:02x}")
+
+ fam_proto = data[13]
+ if fam_proto != 0x12:
+ raise ValueError(f"bad PROXY v2 family/proto: 0x{fam_proto:02x}")
+
+ addr_len = struct.unpack("!H", data[14:16])[0]
+ if addr_len != 12:
+ raise ValueError(f"bad PROXY v2 UDP4 addr_len: {addr_len}")
+
+ src_ip = str(ipaddress.IPv4Address(data[16:20]))
+ dst_ip = str(ipaddress.IPv4Address(data[20:24]))
+ src_port, dst_port = struct.unpack("!HH", data[24:28])
+
+ payload = data[28:]
+
+ return (src_ip, src_port), (dst_ip, dst_port), payload
+
+async def udp_proxy_roundtrip(payload: bytes, timeout: float = 3.0) -> bytes:
+ loop = asyncio.get_running_loop()
+ future: asyncio.Future[bytes] = loop.create_future()
+
+ transport, _protocol = await loop.create_datagram_endpoint(
+ lambda: UDPClientProtocol(payload, future),
+ remote_addr=(LISTEN_HOST, PROXY_PORT),
+ )
+
+ try:
+ return await asyncio.wait_for(future, timeout=timeout)
+ finally:
+ transport.close()
+
def raise_fd_limit(wanted: int) -> None:
if resource is None:
return
@@ -206,6 +350,7 @@ async def run_tinyproxy_with_conf(
conf_text: str,
listen_host: str = LISTEN_HOST,
listen_port: int = PROXY_PORT,
+ proto: str = "tcp",
):
conf_path = None
proxy = None
@@ -220,7 +365,10 @@ async def run_tinyproxy_with_conf(
text=True,
)
- await wait_for_port(listen_host, listen_port)
+ if proto == "tcp":
+ await wait_for_port(listen_host, listen_port)
+ else:
+ await asyncio.sleep(0.1)
if proxy.poll() is not None:
stderr = proxy.stderr.read() if proxy.stderr else ""
@@ -247,7 +395,7 @@ async def run_tinyproxy_with_conf(
@asynccontextmanager
-async def run_default_tinyproxy(proxy_bin: str):
+async def run_default_tcp_tinyproxy(proxy_bin: str):
conf_text = (
f"{LISTEN_HOST}:{PROXY_PORT} "
f"{LISTEN_HOST}:{BACKEND_PORT} "
@@ -260,5 +408,61 @@ async def run_default_tinyproxy(proxy_bin: str):
conf_text=conf_text,
listen_host=LISTEN_HOST,
listen_port=PROXY_PORT,
+ proto="tcp",
+ ) as fixture:
+ yield fixture
+
+@asynccontextmanager
+async def run_default_udp_tinyproxy(proxy_bin: str):
+ conf_text = (
+ f"{LISTEN_HOST}:{PROXY_PORT} "
+ f"{LISTEN_HOST}:{BACKEND_PORT} "
+ f"udp\n"
+ )
+
+ async with run_udp_echo_backend(LISTEN_HOST, BACKEND_PORT):
+ async with run_tinyproxy_with_conf(
+ proxy_bin=proxy_bin,
+ conf_text=conf_text,
+ listen_host=LISTEN_HOST,
+ listen_port=PROXY_PORT,
+ proto="udp",
) as fixture:
yield fixture
+
+class UDPEchoServerProtocol(asyncio.DatagramProtocol):
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.transport = transport # type: ignore[assignment]
+
+ def datagram_received(self, data: bytes, addr) -> None:
+ self.transport.sendto(data, addr)
+
+
+@asynccontextmanager
+async def run_udp_echo_backend(host: str, port: int):
+ loop = asyncio.get_running_loop()
+
+ transport, _protocol = await loop.create_datagram_endpoint(
+ UDPEchoServerProtocol,
+ local_addr=(host, port),
+ )
+
+ try:
+ yield
+ finally:
+ transport.close()
+
+
+@asynccontextmanager
+async def run_udp_proxy_v2_echo_backend(host: str, port: int):
+ loop = asyncio.get_running_loop()
+
+ transport, protocol = await loop.create_datagram_endpoint(
+ UDPProxyV2EchoServerProtocol,
+ local_addr=(host, port),
+ )
+
+ try:
+ yield protocol
+ finally:
+ transport.close()
diff --git a/tests/test_udp_basic.py b/tests/test_udp_basic.py
new file mode 100644
index 0000000..0d1b801
--- /dev/null
+++ b/tests/test_udp_basic.py
@@ -0,0 +1,39 @@
+from .support import udp_proxy_roundtrip, udp_proxy_client
+
+
+async def test_udp_small_roundtrip() -> None:
+ payload = b"hello through udp proxy\n"
+ got = await udp_proxy_roundtrip(payload)
+
+ assert got == payload, f"udp small roundtrip mismatch: {got!r}"
+
+
+async def test_udp_1k_roundtrip() -> None:
+ payload = b"0123456789abcdef" * 64 # 1 KiB
+ got = await udp_proxy_roundtrip(payload, timeout=5.0)
+
+ assert got == payload, f"udp 1k roundtrip mismatch: got {len(got)} bytes"
+
+
+async def test_udp_4k_roundtrip() -> None:
+ payload = b"0123456789abcdef" * 256 # 4 KiB
+ got = await udp_proxy_roundtrip(payload, timeout=5.0)
+
+ assert got == payload, f"udp 4k roundtrip mismatch: got {len(got)} bytes"
+
+
+async def test_udp_many_sequential_datagrams(count: int = 1000) -> None:
+ async with udp_proxy_client() as client:
+ for i in range(count):
+ payload = f"udp-message-{i}\n".encode()
+ got = await client.roundtrip(payload)
+
+ assert got == payload, f"udp sequential datagram {i} failed"
+
+
+TESTS = [
+ ("test_udp_small_roundtrip", test_udp_small_roundtrip),
+ ("test_udp_1k_roundtrip", test_udp_1k_roundtrip),
+ ("test_udp_4k_roundtrip", test_udp_4k_roundtrip),
+ ("test_udp_many_sequential_datagrams", test_udp_many_sequential_datagrams),
+]
diff --git a/tests/test_udp_proxy_v2.py b/tests/test_udp_proxy_v2.py
new file mode 100644
index 0000000..7c26a67
--- /dev/null
+++ b/tests/test_udp_proxy_v2.py
@@ -0,0 +1,63 @@
+import asyncio
+import os
+
+from .support import (
+ BACKEND_PORT,
+ LISTEN_HOST,
+ PROXY_PORT,
+ SkipTest,
+ run_tinyproxy_with_conf,
+ run_udp_proxy_v2_echo_backend,
+ udp_proxy_roundtrip,
+)
+
+async def test_tinyproxy_sends_proxy_v2_for_udp() -> None:
+ proxy_bin = os.environ.get("TINYPROXY_BIN")
+ if not proxy_bin:
+ raise SkipTest("TINYPROXY_BIN is not set")
+
+ conf_text = (
+ f"{LISTEN_HOST}:{PROXY_PORT} "
+ f"{LISTEN_HOST}:{BACKEND_PORT} "
+ f"udp proxy_v2\n"
+ )
+
+ async with run_udp_proxy_v2_echo_backend(LISTEN_HOST, BACKEND_PORT) as backend:
+ async with run_tinyproxy_with_conf(
+ proxy_bin=proxy_bin,
+ conf_text=conf_text,
+ listen_host=LISTEN_HOST,
+ listen_port=PROXY_PORT,
+ proto="udp",
+ ):
+ payload = b"hello udp proxy v2\n"
+
+ echo_task = asyncio.create_task(udp_proxy_roundtrip(payload, timeout=3.0))
+
+ try:
+ got = await echo_task
+ except TimeoutError:
+ if backend.error is not None:
+ raw = backend.last_raw.hex(" ") if backend.last_raw else "<none>"
+ raise AssertionError(
+ f"backend failed to parse UDP PROXY v2: {backend.error}; raw={raw}"
+ ) from backend.error
+
+ raw = backend.last_raw.hex(" ") if backend.last_raw else "<none>"
+ raise AssertionError(
+ f"timed out waiting for UDP proxy v2 echo; backend raw={raw}"
+ )
+
+ assert got == payload, f"udp proxy v2 roundtrip mismatch: {got!r}"
+
+ assert backend.error is None, f"backend failed to parse PROXY v2: {backend.error}"
+ assert backend.last_src is not None, "backend did not record PROXY v2 source"
+ assert backend.last_dst is not None, "backend did not record PROXY v2 destination"
+
+ assert backend.last_src[0] == LISTEN_HOST
+ assert backend.last_dst[0] == LISTEN_HOST
+ assert backend.last_dst[1] == PROXY_PORT
+
+TESTS = [
+ ("test_tinyproxy_sends_proxy_v2_for_udp", test_tinyproxy_sends_proxy_v2_for_udp),
+]
diff --git a/tinyproxy.c b/tinyproxy.c
index bbdf30f..dfc8381 100644
--- a/tinyproxy.c
+++ b/tinyproxy.c
@@ -10,6 +10,7 @@
#include "signal.h"
#include "file_conf.h"
#include "tcp_route.h"
+#include "udp_route.h"
static void usage(FILE *out, const char *prog)
{
@@ -37,8 +38,10 @@ int main(int argc, char **argv)
struct route *routes = NULL;
size_t route_count = 0;
- struct listener_ctx **tcp_ctxs = NULL;
+ struct tcp_route_ctx **tcp_ctxs = NULL;
+ struct udp_route_ctx **udp_ctxs = NULL;
size_t tcp_ctx_count = 0;
+ size_t udp_ctx_count = 0;
struct event_base *base = NULL;
struct signal_events signals;
@@ -89,7 +92,13 @@ int main(int argc, char **argv)
tcp_ctxs = calloc(route_count, sizeof(*tcp_ctxs));
if (tcp_ctxs == NULL) {
- LOG_ERROR("calloc failed", "err", _LOGV(strerror(errno)));
+ LOG_ERROR("tcp calloc failed", "err", _LOGV(strerror(errno)));
+ goto out;
+ }
+
+ udp_ctxs = calloc(route_count, sizeof(*udp_ctxs));
+ if (udp_ctxs == NULL) {
+ LOG_ERROR("udp calloc failed", "err", _LOGV(strerror(errno)));
goto out;
}
@@ -132,8 +141,10 @@ int main(int argc, char **argv)
break;
case PROTO_UDP:
- LOG_WARN("skipping udp route: Not implemented yet");
- rc = 0;
+ rc = start_udp_route(&w, r, &udp_ctxs[udp_ctx_count]);
+ if (rc == 0) {
+ udp_ctx_count++;
+ }
break;
default:
diff --git a/udp_route.c b/udp_route.c
new file mode 100644
index 0000000..f44bf5a
--- /dev/null
+++ b/udp_route.c
@@ -0,0 +1,531 @@
+#include <event2/event.h>
+#include <event2/util.h>
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "klog.h"
+#include "file_conf.h"
+#include "compat_socket.h"
+#include "proxy_proto_v2.h"
+#include "route.h"
+#include "udp_route.h"
+
+#define UDP_MAX_PACKET 65535
+#define UDP_CLIENT_IDLE_TIMEOUT_SEC 60
+
+struct udp_route_ctx;
+
+struct udp_client {
+ struct udp_route_ctx *ctx;
+
+ evutil_socket_t fd;
+ struct event *ev;
+
+ struct sockaddr_storage client_addr;
+ socklen_t client_addr_len;
+
+ time_t last_seen;
+
+ struct udp_client *next;
+};
+
+struct udp_route_ctx {
+ struct event_base *base;
+ struct worker *worker;
+ const struct route *route;
+
+ evutil_socket_t listen_fd;
+ struct event *listen_ev;
+
+ struct sockaddr_storage local_addr;
+ socklen_t local_addr_len;
+
+ struct udp_client *clients;
+};
+
+static int sockaddr_equal(
+ const struct sockaddr_storage *a,
+ socklen_t a_len,
+ const struct sockaddr_storage *b,
+ socklen_t b_len
+) {
+ if (a_len != b_len) {
+ return 0;
+ }
+
+ if (a->ss_family != b->ss_family) {
+ return 0;
+ }
+
+ return memcmp(a, b, (size_t)a_len) == 0;
+}
+
+static void free_udp_client(struct udp_client *c)
+{
+ if (c == NULL) {
+ return;
+ }
+
+ if (c->ev != NULL) {
+ event_free(c->ev);
+ }
+
+ if (c->fd >= 0) {
+ evutil_closesocket(c->fd);
+ }
+
+ free(c);
+}
+
+static void cleanup_idle_udp_clients(struct udp_route_ctx *ctx)
+{
+ time_t now = time(NULL);
+ struct udp_client **pp = &ctx->clients;
+
+ while (*pp != NULL) {
+ struct udp_client *c = *pp;
+
+ if (now - c->last_seen <= UDP_CLIENT_IDLE_TIMEOUT_SEC) {
+ pp = &c->next;
+ continue;
+ }
+
+ *pp = c->next;
+ c->next = NULL;
+
+ LOG_INFO("udp client expired",
+ "client_family", _LOGV(c->client_addr.ss_family),
+ "client_len", _LOGV(c->client_addr_len)
+ );
+
+ free_udp_client(c);
+ }
+}
+
+static struct udp_client *find_udp_client(
+ struct udp_route_ctx *ctx,
+ const struct sockaddr_storage *addr,
+ socklen_t addr_len
+) {
+ for (struct udp_client *c = ctx->clients; c != NULL; c = c->next) {
+ if (sockaddr_equal(&c->client_addr, c->client_addr_len, addr, addr_len)) {
+ return c;
+ }
+ }
+
+ return NULL;
+}
+
+static void upstream_read_cb(evutil_socket_t fd, short events, void *arg)
+{
+ (void)events;
+
+ struct udp_client *c = arg;
+ struct udp_route_ctx *ctx = c->ctx;
+
+ unsigned char buf[UDP_MAX_PACKET];
+
+ for (;;) {
+ ssize_t n = recv(fd, (char *)buf, sizeof(buf), 0);
+ if (n < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ if (err == EAGAIN || err == EWOULDBLOCK) {
+ return;
+ }
+
+ LOG_ERROR("udp upstream recv failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ return;
+ }
+
+ if (n == 0) {
+ return;
+ }
+
+ ssize_t sent = sendto(
+ ctx->listen_fd,
+ (const char *)buf,
+ (size_t)n,
+ 0,
+ (const struct sockaddr *)&c->client_addr,
+ c->client_addr_len
+ );
+
+ if (sent < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp send to client failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ return;
+ }
+ }
+}
+
+static struct udp_client *create_udp_client(
+ struct udp_route_ctx *ctx,
+ const struct sockaddr_storage *client_addr,
+ socklen_t client_addr_len
+) {
+ const struct route *r = ctx->route;
+
+ struct udp_client *c = calloc(1, sizeof(*c));
+ if (c == NULL) {
+ return NULL;
+ }
+
+ c->ctx = ctx;
+ c->fd = -1;
+ c->client_addr = *client_addr;
+ c->client_addr_len = client_addr_len;
+ c->last_seen = time(NULL);
+
+ c->fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (c->fd < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp upstream socket failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ free_udp_client(c);
+ return NULL;
+ }
+
+ if (evutil_make_socket_nonblocking(c->fd) < 0) {
+ LOG_ERROR("evutil_make_socket_nonblocking failed");
+ free_udp_client(c);
+ return NULL;
+ }
+
+ 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) {
+ LOG_ERROR("invalid udp upstream address",
+ "upstream_host", _LOGV(r->upstream_host)
+ );
+ free_udp_client(c);
+ return NULL;
+ }
+
+ if (connect(
+ c->fd,
+ (const struct sockaddr *)&upstream_addr,
+ sizeof(upstream_addr)
+ ) < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp upstream connect failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ free_udp_client(c);
+ return NULL;
+ }
+
+ c->ev = event_new(ctx->base, c->fd, EV_READ | EV_PERSIST, upstream_read_cb, c);
+ if (c->ev == NULL) {
+ LOG_ERROR("event_new failed for udp upstream");
+ free_udp_client(c);
+ return NULL;
+ }
+
+ if (event_add(c->ev, NULL) < 0) {
+ LOG_ERROR("event_add failed for udp upstream");
+ free_udp_client(c);
+ return NULL;
+ }
+
+ c->next = ctx->clients;
+ ctx->clients = c;
+
+ LOG_INFO("udp client created",
+ "client_family", _LOGV(c->client_addr.ss_family),
+ "client_len", _LOGV(c->client_addr_len)
+ );
+
+ return c;
+}
+
+static int send_udp_payload_to_upstream(
+ struct udp_client *c,
+ const unsigned char *payload,
+ size_t payload_len
+) {
+ struct udp_route_ctx *ctx = c->ctx;
+ const struct route *r = ctx->route;
+
+ if (!r->send_proxy_v2) {
+ ssize_t sent = send(c->fd, (const char *)payload, payload_len, 0);
+ if (sent < 0) {
+ return -EVUTIL_SOCKET_ERROR();
+ }
+
+ return 0;
+ }
+
+ if (c->client_addr.ss_family != AF_INET ||
+ ctx->local_addr.ss_family != AF_INET) {
+ LOG_ERROR("PROXY v2 UDP currently only supports IPv4",
+ "client_family", _LOGV(c->client_addr.ss_family),
+ "local_family", _LOGV(ctx->local_addr.ss_family)
+ );
+ return -EAFNOSUPPORT;
+ }
+
+ unsigned char hdr[256];
+ size_t hdr_len = 0;
+
+ int rc = proxy_v2_build(
+ hdr,
+ sizeof(hdr),
+ (const struct sockaddr *)&c->client_addr,
+ c->client_addr_len,
+ (const struct sockaddr *)&ctx->local_addr,
+ ctx->local_addr_len,
+ SOCK_DGRAM,
+ &hdr_len
+ );
+ if (rc != 0) {
+ return rc;
+ }
+
+ if (hdr_len + payload_len > UDP_MAX_PACKET) {
+ return -EMSGSIZE;
+ }
+
+ unsigned char out[UDP_MAX_PACKET];
+
+ memcpy(out, hdr, hdr_len);
+ memcpy(out + hdr_len, payload, payload_len);
+
+ ssize_t sent = send(c->fd, (const char *)out, hdr_len + payload_len, 0);
+ if (sent < 0) {
+ return -EVUTIL_SOCKET_ERROR();
+ }
+
+ return 0;
+}
+
+static void listen_read_cb(evutil_socket_t fd, short events, void *arg)
+{
+ (void)events;
+
+ struct udp_route_ctx *ctx = arg;
+
+ cleanup_idle_udp_clients(ctx);
+
+ for (;;) {
+ unsigned char buf[UDP_MAX_PACKET];
+
+ struct sockaddr_storage client_addr;
+ socklen_t client_addr_len = sizeof(client_addr);
+
+ memset(&client_addr, 0, sizeof(client_addr));
+
+ ssize_t n = recvfrom(
+ fd,
+ (char *)buf,
+ sizeof(buf),
+ 0,
+ (struct sockaddr *)&client_addr,
+ &client_addr_len
+ );
+
+ if (n < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ if (err == EAGAIN || err == EWOULDBLOCK) {
+ return;
+ }
+
+ LOG_ERROR("udp recvfrom failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ return;
+ }
+
+ if (n == 0) {
+ 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);
+ if (c == NULL) {
+ return;
+ }
+ }
+
+ c->last_seen = time(NULL);
+
+ int rc = send_udp_payload_to_upstream(c, buf, (size_t)n);
+ if (rc < 0) {
+ LOG_ERROR("udp send to upstream failed",
+ "err", _LOGV(strerror(-rc))
+ );
+ return;
+ }
+ }
+}
+
+int start_udp_route(
+ struct worker *w,
+ const struct route *r,
+ struct udp_route_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) {
+ LOG_ERROR("invalid udp listen address",
+ "listen_host", _LOGV(r->listen_host)
+ );
+ return -EINVAL;
+ }
+
+ struct udp_route_ctx *ctx = calloc(1, sizeof(*ctx));
+ if (ctx == NULL) {
+ return -ENOMEM;
+ }
+
+ ctx->base = w->base;
+ ctx->worker = w;
+ ctx->route = r;
+ ctx->listen_fd = -1;
+
+ ctx->listen_fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (ctx->listen_fd < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp listen socket failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ free(ctx);
+ return -errno;
+ }
+
+ evutil_make_socket_closeonexec(ctx->listen_fd);
+
+ if (evutil_make_socket_nonblocking(ctx->listen_fd) < 0) {
+ LOG_ERROR("evutil_make_socket_nonblocking failed");
+ evutil_closesocket(ctx->listen_fd);
+ free(ctx);
+ return -EINVAL;
+ }
+
+ int one = 1;
+ setsockopt(
+ ctx->listen_fd,
+ SOL_SOCKET,
+ SO_REUSEADDR,
+ (const char *)&one,
+ sizeof(one)
+ );
+
+ if (bind(
+ ctx->listen_fd,
+ (const struct sockaddr *)&listen_addr,
+ sizeof(listen_addr)
+ ) < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp bind failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ evutil_closesocket(ctx->listen_fd);
+ free(ctx);
+ return -EADDRINUSE;
+ }
+
+ ctx->local_addr_len = sizeof(ctx->local_addr);
+ memset(&ctx->local_addr, 0, sizeof(ctx->local_addr));
+
+ if (getsockname(
+ ctx->listen_fd,
+ (struct sockaddr *)&ctx->local_addr,
+ &ctx->local_addr_len
+ ) < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("udp getsockname failed",
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ evutil_closesocket(ctx->listen_fd);
+ free(ctx);
+ return -EINVAL;
+ }
+
+ ctx->listen_ev = event_new(
+ ctx->base,
+ ctx->listen_fd,
+ EV_READ | EV_PERSIST,
+ listen_read_cb,
+ ctx
+ );
+ if (ctx->listen_ev == NULL) {
+ LOG_ERROR("event_new failed for udp listener");
+ evutil_closesocket(ctx->listen_fd);
+ free(ctx);
+ return -ENOMEM;
+ }
+
+ if (event_add(ctx->listen_ev, NULL) < 0) {
+ LOG_ERROR("event_add failed for udp listener");
+ event_free(ctx->listen_ev);
+ evutil_closesocket(ctx->listen_fd);
+ free(ctx);
+ return -EINVAL;
+ }
+
+ char opts[128];
+
+ route_options_str(r, opts, sizeof(opts));
+
+ LOG_INFO("udp route started",
+ "line", _LOGV(r->line_no),
+ "listen_host", _LOGV(r->listen_host),
+ "listen_port", _LOGV(r->listen_port),
+ "upstream_host", _LOGV(r->upstream_host),
+ "upstream_port", _LOGV(r->upstream_port),
+ "options", _LOGV(opts[0] ? opts : "")
+ );
+
+ *out = ctx;
+ return 0;
+}
+
+void free_udp_route(struct udp_route_ctx *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->listen_ev != NULL) {
+ event_free(ctx->listen_ev);
+ }
+
+ if (ctx->listen_fd >= 0) {
+ evutil_closesocket(ctx->listen_fd);
+ }
+
+ struct udp_client *c = ctx->clients;
+ while (c != NULL) {
+ struct udp_client *next = c->next;
+ free_udp_client(c);
+ c = next;
+ }
+
+ free(ctx);
+}
diff --git a/udp_route.h b/udp_route.h
new file mode 100644
index 0000000..2e11f67
--- /dev/null
+++ b/udp_route.h
@@ -0,0 +1,16 @@
+#ifndef UDP_ROUTE_H
+#define UDP_ROUTE_H
+
+#include "route.h"
+
+struct udp_route_ctx;
+
+int start_udp_route(
+ struct worker *w,
+ const struct route *r,
+ struct udp_route_ctx **out
+);
+
+void free_udp_route(struct udp_route_ctx *ctx);
+
+#endif