commit 8f5e169ae99f4df067449e0f1cf24012417140c0
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-28T02:19:52Z |
| subject | Added file:// backend and man pages |
commit 8f5e169ae99f4df067449e0f1cf24012417140c0
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-28T02:19:52Z
Added file:// backend and man pages
---
.gitignore | 6 +++
Makefile | 1 +
docs/tinyproxy.1.scd | 34 +++++++++++++
docs/tinyproxy.conf.5.scd | 91 +++++++++++++++++++++++++++++++++
examples/http-response.txt | 6 +++
mk/man.mk | 29 +++++++++++
src/file_conf.c | 36 +++++++++++++
src/route.c | 4 ++
src/route.h | 1 +
src/tcp_route.c | 124 +++++++++++++++++++++++++++++++++++++++++++++
src/tinyproxy.c | 16 ++++++
tinyproxy.conf | 3 ++
12 files changed, 351 insertions(+)
diff --git a/.gitignore b/.gitignore
index 18168ab..654fdc0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
bin/
+dist/
build/
corpus/
libevent2/
@@ -7,6 +8,11 @@ __pycache__
*.swp
.DS_Store
+# Generated man pages from scdoc
+docs/*.1
+docs/*.5
+docs/*.8
+
# Prerequisites
*.d
diff --git a/Makefile b/Makefile
index ca1e372..ef9f582 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ PROJECT_ROOT := $(CURDIR)
include mk/common.mk
include mk/libevent.mk
+include mk/man.mk
BIN := $(BIN_DIR)/tinyproxy$(EXEEXT)
diff --git a/docs/tinyproxy.1.scd b/docs/tinyproxy.1.scd
new file mode 100644
index 0000000..6071c05
--- /dev/null
+++ b/docs/tinyproxy.1.scd
@@ -0,0 +1,34 @@
+tinyproxy(1)
+
+# NAME
+
+tinyproxy - lightweight TCP/UDP proxy
+
+# SYNOPSIS
+
+*tinyproxy* [options]
+
+# DESCRIPTION
+
+tinyproxy forwards TCP and UDP traffic.
+
+Supports:
+- PROXY protocol v2
+- UNIX sockets
+- UDP forwarding
+
+# OPTIONS
+
+*-c* <config>
+ Load config file.
+
+*-v*
+ Show version.
+
+# EXAMPLES
+
+ tinyproxyc -c /etc/tinyproxy.conf
+
+# AUTHORS
+
+Penguin ts
diff --git a/docs/tinyproxy.conf.5.scd b/docs/tinyproxy.conf.5.scd
new file mode 100644
index 0000000..93bef01
--- /dev/null
+++ b/docs/tinyproxy.conf.5.scd
@@ -0,0 +1,91 @@
+tinyproxy.conf(5)
+
+# NAME
+
+tinyproxy.conf - tinyproxy configuration file
+
+# DESCRIPTION
+
+tinyproxy.conf defines TCP and UDP forwarding routes.
+
+# GLOBAL OPTIONS
+
+TINYPROXY_RUNTIME_DIR="/tmp/tinyproxy"
+
+# ROUTES
+
+listen upstream protocol [options...]
+
+# OPTIONS
+
+proxy_v2
+ Enable proxy v2 for upstream
+ Default: false
+
+keep_alive
+ Enable keep alive for tcp
+ Default: false
+
+idle_timeout=SECONDS
+ Idle timeout for both listen and upstream
+ Default: 60
+
+connect_timeout=SECONDS
+ Connection timeout for both listen and upstream
+ Default: 5
+
+# ENDPOINTS
+
+unix:
+ unix:/tmp/backend.sock
+
+unix datagram:
+ unix-dgram:/tmp/socket.sock
+
+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.
+
+file:///path/to/file
+ Return the contents of the file in raw bytes.
+
+# EXAMPLES
+
+Forward HTTPS:
+
+ Direct tcp forward with default settings
+ 0.0.0.0:443 10.0.1.10:443 tcp
+
+ Upstream accepts proxy protocol v2
+ 0.0.0.0:443 10.0.1.10:443 tcp proxy_v2
+
+UDP route:
+
+ 0.0.0.0:53 1.1.1.1:53 udp
+
+Serve a fixed HTTP response:
+
+ $ printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nContent-Length: 14\\r\\nConnection: close\\r\\n\\r\\nhello, world!\\n' > response.http
+
+ 0.0.0.0:8080 file://response.http tcp
+
+# FILES
+
+/etc/tinyproxy.conf
+
+# SEE ALSO
+
+tinyproxy(1)
diff --git a/examples/http-response.txt b/examples/http-response.txt
new file mode 100644
index 0000000..d5da047
--- /dev/null
+++ b/examples/http-response.txt
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-Type: text/plain
+Content-Length: 14
+Connection: close
+
+hello, world!
diff --git a/mk/man.mk b/mk/man.mk
new file mode 100644
index 0000000..1d626fd
--- /dev/null
+++ b/mk/man.mk
@@ -0,0 +1,29 @@
+SCDOC ?= scdoc
+
+MAN_SCD := docs/tinyproxy.1.scd \
+ docs/tinyproxy.conf.5.scd
+
+MAN := $(MAN_SCD:.scd=)
+MAN_DIST := $(PROJECT_ROOT)/dist
+
+.PHONY: man clean-man install-man uninstall-man
+
+man: $(MAN)
+
+docs/%: docs/%.scd
+ $(SCDOC) < $< > $@
+
+install-man: man
+ mkdir -p $(MAN_DIST)/usr/share/man/man1
+ mkdir -p $(MAN_DIST)/usr/share/man/man5
+ install -Dm644 docs/tinyproxy.1 \
+ $(MAN_DIST)/usr/share/man/man1/tinyproxy.1
+ install -Dm644 docs/tinyproxy.conf.5 \
+ $(MAN_DIST)/usr/share/man/man5/tinyproxy.conf.5
+
+uninstall-man:
+ rm -f $(DESTDIR)$(PREFIX)/share/man/man1/tinyproxy.1
+ rm -f $(DESTDIR)$(PREFIX)/share/man/man5/tinyproxy.conf.5
+
+clean-man:
+ rm -f $(MAN)
diff --git a/src/file_conf.c b/src/file_conf.c
index 4ca1c21..68126ea 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -193,6 +193,23 @@ static inline void route_options_set_defaults(struct route_options *opts)
opts->connect_timeout_sec = ROUTE_DEFAULT_CONNECT_TIMEOUT_SEC;
}
+static const char *file_uri_path(const char *s)
+{
+ const char *path = s + 7; /* file:// */
+
+#ifdef _WIN32
+ /* Accept file:///C:/path as C:/path */
+ if (path[0] == '/' &&
+ ((path[1] >= 'A' && path[1] <= 'Z') ||
+ (path[1] >= 'a' && path[1] <= 'z')) &&
+ path[2] == ':') {
+ path++;
+ }
+#endif
+
+ return path;
+}
+
static int parse_endpoint(const char *s, struct endpoint *ep)
{
const char *path;
@@ -228,6 +245,25 @@ static int parse_endpoint(const char *s, struct endpoint *ep)
ep->kind = ENDPOINT_BUILTIN;
ep->builtin = builtin;
+ return 0;
+ } else if (strncmp(s, "file://", 7) == 0) {
+ const char *path = file_uri_path(s);
+
+ if (path[0] == '\0') {
+ LOG_ERROR("missing file:// path");
+ return -1;
+ }
+
+ if (strlen(path) >= sizeof(ep->path)) {
+ LOG_ERROR("file:// path too long",
+ "max", _LOGV(sizeof(ep->path) - 1)
+ );
+ return -1;
+ }
+
+ ep->kind = ENDPOINT_FILE;
+ strcpy(ep->path, path);
+
return 0;
} else if (strncmp(s, "unix-dgram:", 11) == 0) {
path = s + 11;
diff --git a/src/route.c b/src/route.c
index 06f339c..2efcdc6 100644
--- a/src/route.c
+++ b/src/route.c
@@ -30,6 +30,10 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
snprintf(buf, buf_len, "builtin://%s", x_builtin_name(ep->builtin));
return 0;
+ case ENDPOINT_FILE:
+ snprintf(buf, buf_len, "file://%s", ep->path);
+ return 0;
+
default:
snprintf(buf, buf_len, "<unknown>");
return 0;
diff --git a/src/route.h b/src/route.h
index e672512..7e5c5e3 100644
--- a/src/route.h
+++ b/src/route.h
@@ -31,6 +31,7 @@ enum endpoint_kind {
ENDPOINT_UNIX,
ENDPOINT_UNIX_DGRAM,
ENDPOINT_BUILTIN,
+ ENDPOINT_FILE,
};
struct endpoint {
diff --git a/src/tcp_route.c b/src/tcp_route.c
index a65ef99..ce46022 100644
--- a/src/tcp_route.c
+++ b/src/tcp_route.c
@@ -20,6 +20,8 @@
#define BEV_READ_HIGH_WATER (256 * 1024)
#define BEV_WRITE_RESUME_WATER (128 * 1024)
+#define FILE_CHUNK_SIZE 4096
+
struct tcp_route_ctx {
struct event_base *accept_base;
struct worker *worker;
@@ -38,6 +40,11 @@ typedef struct conn_s {
socklen_t peer_addr_len;
} conn_t;
+struct file_conn {
+ conn_t *conn;
+ FILE *fp;
+};
+
struct accepted_client {
evutil_socket_t fd;
struct sockaddr_storage peer_addr;
@@ -304,6 +311,118 @@ static void builtin_close_after_write_cb(struct bufferevent *bev, void *arg)
}
}
+static void file_done(struct file_conn *fc)
+{
+ if (fc == NULL) {
+ return;
+ }
+
+ if (fc->fp != NULL) {
+ fclose(fc->fp);
+ fc->fp = NULL;
+ }
+
+ free_conn(fc->conn);
+ free(fc);
+}
+
+static void file_event_cb(struct bufferevent *bev, short events, void *arg)
+{
+ struct file_conn *fc = arg;
+
+ (void)bev;
+ (void)events;
+
+ file_done(fc);
+}
+
+static void file_write_cb(struct bufferevent *bev, void *arg)
+{
+ struct file_conn *fc = arg;
+ struct evbuffer *out = bufferevent_get_output(bev);
+ char buf[FILE_CHUNK_SIZE];
+
+ while (evbuffer_get_length(out) < BEV_READ_HIGH_WATER) {
+ size_t n = fread(buf, 1, sizeof(buf), fc->fp);
+
+ if (n > 0) {
+ if (bufferevent_write(bev, buf, n) < 0) {
+ file_done(fc);
+ return;
+ }
+
+ continue;
+ }
+
+ if (ferror(fc->fp)) {
+ file_done(fc);
+ return;
+ }
+
+ /*
+ * EOF. If nothing is queued anymore, close now.
+ * Otherwise keep the write callback installed so we close
+ * after libevent drains the remaining output.
+ */
+ if (evbuffer_get_length(out) == 0) {
+ file_done(fc);
+ }
+
+ return;
+ }
+}
+
+static int start_tcp_file(conn_t *conn)
+{
+ const struct route *r;
+ struct file_conn *fc;
+ FILE *fp;
+
+ if (conn == NULL || conn->route == NULL) {
+ return -EINVAL;
+ }
+
+ r = conn->route;
+
+ fp = fopen(r->upstream.path, "rb");
+ if (fp == NULL) {
+ int err = errno;
+
+ LOG_ERROR("failed to open file upstream",
+ "path", _LOGV(r->upstream.path),
+ "err", _LOGV(strerror(err))
+ );
+
+ free_conn(conn);
+ return -err;
+ }
+
+ if (conn->upstream != NULL) {
+ bufferevent_free(conn->upstream);
+ conn->upstream = NULL;
+ }
+
+ fc = calloc(1, sizeof(*fc));
+ if (fc == NULL) {
+ fclose(fp);
+ free_conn(conn);
+ return -ENOMEM;
+ }
+
+ fc->conn = conn;
+ fc->fp = fp;
+
+ bufferevent_setwatermark(conn->client, EV_WRITE, 0, BEV_READ_HIGH_WATER);
+ bufferevent_setcb(conn->client, NULL, file_write_cb, file_event_cb, fc);
+ bufferevent_enable(conn->client, EV_WRITE);
+
+ set_client_idle_timeout(conn, r);
+
+ file_write_cb(conn->client, fc);
+
+ return 0;
+}
+
static int start_tcp_builtin(conn_t *conn)
{
struct x_builtin_request req;
@@ -390,6 +509,11 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
return;
}
+ if (ac->route->upstream.kind == ENDPOINT_FILE) {
+ start_tcp_file(conn);
+ return;
+ }
+
conn->upstream = bufferevent_socket_new(w->base, -1, BEV_OPT_CLOSE_ON_FREE);
if (conn->upstream == NULL) {
free_conn(conn);
diff --git a/src/tinyproxy.c b/src/tinyproxy.c
index 2c3d6f2..3e3a00f 100644
--- a/src/tinyproxy.c
+++ b/src/tinyproxy.c
@@ -24,8 +24,24 @@ static void usage(FILE *out, const char *prog)
prog);
}
+static void prevent_socket_write_from_killing_process(void)
+{
+#ifndef _WIN32
+ /*
+ * On Unix, writing to a closed socket may raise SIGPIPE.
+ * The default SIGPIPE action is process termination.
+ *
+ * Proxies must treat closed peers as ordinary I/O errors,
+ * not as a reason to kill the whole daemon.
+ */
+ signal(SIGPIPE, SIG_IGN);
+#endif
+}
+
int main(int argc, char **argv)
{
+ prevent_socket_write_from_killing_process();
+
const char *conf_path = "tinyproxy.conf";
int opt;
int exit_code = 1;
diff --git a/tinyproxy.conf b/tinyproxy.conf
index f042f9d..3112617 100644
--- a/tinyproxy.conf
+++ b/tinyproxy.conf
@@ -30,3 +30,6 @@
:12996 builtin://hang udp
:12997 builtin://discard udp
:12998 builtin://close udp
+
+:14000 file://examples/http-response.txt tcp
+:14001 file:///does/not/exist tcp