commit 77ca1751959473007edd0e798061f62b4e79f0ce
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-31T17:11:44Z |
| subject | Added broadcast_reply=upstream mode |
commit 77ca1751959473007edd0e798061f62b4e79f0ce
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-31T17:11:44Z
Added broadcast_reply=upstream mode
---
README.md | 2 +-
docs/tinyproxy.conf.5.scd | 13 +++-
examples/minecraft-bedrock-lan.conf | 12 +++-
src/datagram_client.c | 60 ++++++++++++++-----
src/datagram_listener.c | 21 +------
src/datagram_raw.c | 114 +++++++++++-------------------------
src/datagram_raw.h | 8 +--
src/datagram_route.c | 13 ++--
src/file_conf.c | 41 ++++++++++++-
src/route.c | 11 ++++
src/route.h | 9 ++-
11 files changed, 175 insertions(+), 129 deletions(-)
diff --git a/README.md b/README.md
index 8432933..9ba0734 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Makes a remote Minecraft Bedrock server appear as a LAN server.
```sh
tinyproxy \
- -L "udp :19132 udp 123.123.123.123:19132 broadcast_reply"
+ -L "udp :19132 udp 123.123.123.123:19132 broadcast_reply=listen"
```
Run with config file:
diff --git a/docs/tinyproxy.conf.5.scd b/docs/tinyproxy.conf.5.scd
index fc40c92..46490b9 100644
--- a/docs/tinyproxy.conf.5.scd
+++ b/docs/tinyproxy.conf.5.scd
@@ -34,8 +34,17 @@ connect_timeout=SECONDS
Connection timeout for both listen and upstream.
Default: 5
-broadcast_reply
- UDP only. Forward broadcast packets to the upstream and relay replies from the listening endpoint.
+broadcast_reply=listen
+ UDP only. Send upstream replies back to the original client from the listening endpoint.
+
+broadcast_reply=upstream
+ UDP/IPv4 only. Forward broadcast requests to the configured upstream and
+ send upstream replies back to the original client using raw IPv4 packets
+ whose source address and port are the upstream endpoint.
+
+ This lets broadcast-based discovery advertise the upstream address directly,
+ so clients can connect to the upstream after discovery instead of sending
+ gameplay traffic through tinyproxy. Linux only; requires CAP_NET_RAW.
# ENDPOINTS
diff --git a/examples/minecraft-bedrock-lan.conf b/examples/minecraft-bedrock-lan.conf
index 1067f51..468e5b9 100644
--- a/examples/minecraft-bedrock-lan.conf
+++ b/examples/minecraft-bedrock-lan.conf
@@ -10,4 +10,14 @@
#
# Replace 123.123.123.123:19132 with your server address.
#
-udp :19132 udp 123.123.123.123:19132 broadcast_reply
+udp :19132 udp 123.123.123.123:19132 broadcast_reply=listen
+#
+# Advertise a Minecraft Bedrock server outside the local broadcast subnet.
+#
+# tinyproxy receives LAN discovery broadcasts on :19132, forwards them to
+# 10.0.1.123:19132, then sends the discovery reply back as if it came from
+# 10.0.1.123:19132.
+#
+# After discovery, the Minecraft client connects directly to 10.0.1.123:19132
+# for gameplay traffic, so tinyproxy only handles discovery.
+udp :19132 udp 10.0.1.123:19132 broadcast_reply=upstream
diff --git a/src/datagram_client.c b/src/datagram_client.c
index 09b65cd..3ec38ac 100644
--- a/src/datagram_client.c
+++ b/src/datagram_client.c
@@ -8,6 +8,7 @@
#include "env.h"
#include "route.h"
#include "datagram_client.h"
+#include "datagram_raw.h"
#include "proxy_proto_v2.h"
void free_datagram_client(struct datagram_client *c)
@@ -262,6 +263,44 @@ int send_datagram_payload_to_upstream(
return 0;
}
+static int send_datagram_payload_to_client(
+ struct datagram_client *c,
+ const unsigned char *data,
+ size_t data_len)
+{
+ struct datagram_route_ctx *ctx = c->ctx;
+ ssize_t sent;
+
+ if (ctx->route->opts.broadcast_reply == BROADCAST_REPLY_UPSTREAM) {
+ return datagram_raw_send_udp_ipv4(
+ ctx->raw_fd,
+ &ctx->route->upstream,
+ (const struct sockaddr *)&c->client_addr,
+ c->client_addr_len,
+ data,
+ data_len);
+ }
+
+ sent = sendto(
+ ctx->listen_fd,
+ (const char *)data,
+ data_len,
+ 0,
+ (const struct sockaddr *)&c->client_addr,
+ c->client_addr_len
+ );
+
+ if (sent < 0) {
+ return -EVUTIL_SOCKET_ERROR();
+ }
+
+ if ((size_t)sent != data_len) {
+ return -EIO;
+ }
+
+ return 0;
+}
+
static void upstream_read_cb(evutil_socket_t fd, short events, void *arg)
{
struct datagram_client *c = arg;
@@ -273,7 +312,10 @@ static void upstream_read_cb(evutil_socket_t fd, short events, void *arg)
compat_mutex_lock(&ctx->clients_mu);
for (;;) {
- ssize_t n = recv(fd, (char *)buf, sizeof(buf), 0);
+ ssize_t n;
+ int rc;
+
+ n = recv(fd, (char *)buf, sizeof(buf), 0);
if (n < 0) {
int err = EVUTIL_SOCKET_ERROR();
@@ -291,20 +333,10 @@ static void upstream_read_cb(evutil_socket_t fd, short events, void *arg)
goto out;
}
- 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();
-
+ rc = send_datagram_payload_to_client(c, buf, (size_t)n);
+ if (rc != 0) {
LOG_ERROR("udp send to client failed",
- "err", _LOGV(evutil_socket_error_to_string(err))
+ "err", _LOGV(strerror(-rc))
);
goto out;
}
diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index f862c44..1b0cc8b 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -97,23 +97,6 @@ int datagram_route_handle_packet(
return handle_datagram_builtin_packet(pkt);
}
- if (ctx->route->opts.transparent_replay) {
- rc = datagram_raw_replay_ipv4(
- ctx->raw_fd,
- ctx->route,
- (const struct sockaddr *)&pkt->peer_addr,
- pkt->peer_addr_len,
- pkt->data,
- pkt->data_len);
-
- if (rc != 0) {
- LOG_WARN("failed to replay datagram",
- "err", _LOGV(strerror(-rc)));
- }
-
- return rc;
- }
-
compat_mutex_lock(&ctx->clients_mu);
c = datagram_route_get_or_create_client(w, pkt);
@@ -265,7 +248,7 @@ static int bind_unix_datagram_listener(struct datagram_route_ctx *ctx)
return -err;
}
- if (r->opts.broadcast_reply) {
+ if (r->opts.broadcast_reply != BROADCAST_REPLY_OFF) {
LOG_ERROR("broadcast_reply is only valid for udp inet listeners",
"line", _LOGV(r->line_no));
return -EINVAL;
@@ -373,7 +356,7 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
return -err;
}
- if (r->opts.broadcast_reply) {
+ if (r->opts.broadcast_reply != BROADCAST_REPLY_OFF) {
int yes = 1;
if (setsockopt(ctx->listen_fd, SOL_SOCKET, SO_BROADCAST,
(const char *)&yes, sizeof(yes)) < 0) {
diff --git a/src/datagram_raw.c b/src/datagram_raw.c
index 53adb5a..f5ec6a0 100644
--- a/src/datagram_raw.c
+++ b/src/datagram_raw.c
@@ -39,60 +39,6 @@ static uint16_t checksum16(const void *data, size_t len)
#ifndef _WIN32
-struct udp4_pseudo_header {
- uint32_t src;
- uint32_t dst;
- uint8_t zero;
- uint8_t proto;
- uint16_t len;
-};
-
-static uint16_t udp4_checksum(
- const struct ip *ip,
- const struct udphdr *udp,
- const unsigned char *payload,
- size_t payload_len)
-{
- struct udp4_pseudo_header ph;
- size_t udp_len = sizeof(*udp) + payload_len;
- size_t total_len = sizeof(ph) + udp_len;
- unsigned char *buf;
- uint16_t sum;
-
- if (udp_len > UINT16_MAX || total_len > SIZE_MAX) {
- return 0;
- }
-
- buf = malloc(total_len);
- if (!buf) {
- return 0;
- }
-
- memset(&ph, 0, sizeof(ph));
- ph.src = ip->ip_src.s_addr;
- ph.dst = ip->ip_dst.s_addr;
- ph.zero = 0;
- ph.proto = IPPROTO_UDP;
- ph.len = htons((uint16_t)udp_len);
-
- memcpy(buf, &ph, sizeof(ph));
- memcpy(buf + sizeof(ph), udp, sizeof(*udp));
- memcpy(buf + sizeof(ph) + sizeof(*udp), payload, payload_len);
-
- sum = checksum16(buf, total_len);
- free(buf);
-
- /*
- * For IPv4 UDP, checksum 0 means "no checksum".
- * If the computed checksum is 0, transmit it as 0xffff.
- */
- if (sum == 0) {
- sum = 0xffff;
- }
-
- return sum;
-}
-
int datagram_raw_open_ipv4(evutil_socket_t *out_fd)
{
evutil_socket_t fd;
@@ -140,16 +86,16 @@ static int endpoint_to_sockaddr_in(
return 0;
}
-int datagram_raw_replay_ipv4(
+int datagram_raw_send_udp_ipv4(
evutil_socket_t raw_fd,
- const struct route *r,
- const struct sockaddr *peer_addr,
- socklen_t peer_addr_len,
+ const struct endpoint *src_ep,
+ const struct sockaddr *dst_addr,
+ socklen_t dst_addr_len,
const unsigned char *data,
size_t data_len)
{
- const struct sockaddr_in *src;
- struct sockaddr_in upstream_addr;
+ struct sockaddr_in src_addr;
+ const struct sockaddr_in *dst;
struct sockaddr_in send_dst;
unsigned char *packet;
struct ip *ip;
@@ -160,22 +106,24 @@ int datagram_raw_replay_ipv4(
ssize_t n;
int rc;
- (void)peer_addr_len;
+ if (raw_fd < 0 || !src_ep || !dst_addr || !data) {
+ return -EINVAL;
+ }
- if (raw_fd < 0 || !r || !peer_addr || !data) {
+ if (dst_addr_len < (socklen_t)sizeof(struct sockaddr_in)) {
return -EINVAL;
}
- if (peer_addr->sa_family != AF_INET) {
+ if (dst_addr->sa_family != AF_INET) {
return -EAFNOSUPPORT;
}
- rc = endpoint_to_sockaddr_in(&r->upstream, &upstream_addr);
+ rc = endpoint_to_sockaddr_in(src_ep, &src_addr);
if (rc != 0) {
return rc;
}
- src = (const struct sockaddr_in *)peer_addr;
+ dst = (const struct sockaddr_in *)dst_addr;
if (data_len > UINT16_MAX - ip_len - udp_len) {
return -EMSGSIZE;
@@ -199,24 +147,30 @@ int datagram_raw_replay_ipv4(
ip->ip_off = 0;
ip->ip_ttl = 64;
ip->ip_p = IPPROTO_UDP;
- ip->ip_src = src->sin_addr;
- ip->ip_dst = upstream_addr.sin_addr;
+ ip->ip_src = src_addr.sin_addr;
+ ip->ip_dst = dst->sin_addr;
ip->ip_sum = 0;
- ip->ip_sum = checksum16(ip, ip_len);
- udp->uh_sport = src->sin_port;
- udp->uh_dport = upstream_addr.sin_port;
+ udp->uh_sport = src_addr.sin_port;
+ udp->uh_dport = dst->sin_port;
udp->uh_ulen = htons((uint16_t)(udp_len + data_len));
udp->uh_sum = 0;
memcpy(packet + ip_len + udp_len, data, data_len);
- udp->uh_sum = udp4_checksum(ip, udp, data, data_len);
+ ip->ip_sum = checksum16(ip, ip_len);
+
+ /*
+ * For IPv4, UDP checksum 0 is legal and means "no checksum".
+ * Keep this simple for now; it avoids checksum/offload weirdness while
+ * testing spoofed discovery replies.
+ */
+ udp->uh_sum = 0;
memset(&send_dst, 0, sizeof(send_dst));
send_dst.sin_family = AF_INET;
- send_dst.sin_addr = upstream_addr.sin_addr;
- send_dst.sin_port = upstream_addr.sin_port;
+ send_dst.sin_addr = dst->sin_addr;
+ send_dst.sin_port = dst->sin_port;
n = sendto(
raw_fd,
@@ -247,18 +201,18 @@ int datagram_raw_open_ipv4(evutil_socket_t *out_fd)
return -ENOTSUP;
}
-int datagram_raw_replay_ipv4(
+int datagram_raw_send_udp_ipv4(
evutil_socket_t raw_fd,
- const struct route *r,
- const struct sockaddr *peer_addr,
- socklen_t peer_addr_len,
+ const struct endpoint *src_ep,
+ const struct sockaddr *dst_addr,
+ socklen_t dst_addr_len,
const unsigned char *data,
size_t data_len)
{
(void)raw_fd;
- (void)r;
- (void)peer_addr;
- (void)peer_addr_len;
+ (void)src_ap;
+ (void)dst_addr;
+ (void)dst_addr_len;
(void)data;
(void)data_len;
diff --git a/src/datagram_raw.h b/src/datagram_raw.h
index 1255d22..bc4e5f7 100644
--- a/src/datagram_raw.h
+++ b/src/datagram_raw.h
@@ -9,11 +9,11 @@
int datagram_raw_open_ipv4(evutil_socket_t *out_fd);
-int datagram_raw_replay_ipv4(
+int datagram_raw_send_udp_ipv4(
evutil_socket_t raw_fd,
- const struct route *r,
- const struct sockaddr *peer_addr,
- socklen_t peer_addr_len,
+ const struct endpoint *src_ep,
+ const struct sockaddr *dst_addr,
+ socklen_t dst_addr_len,
const unsigned char *data,
size_t data_len);
diff --git a/src/datagram_route.c b/src/datagram_route.c
index 1a4f7eb..8e73a3b 100644
--- a/src/datagram_route.c
+++ b/src/datagram_route.c
@@ -27,10 +27,6 @@ static int prepare_datagram_route(struct datagram_route_ctx *ctx)
}
}
- if (r->opts.transparent_replay) {
- return datagram_raw_open_ipv4(&ctx->raw_fd);
- }
-
return 0;
}
@@ -53,7 +49,14 @@ int start_datagram_route(
ctx->worker_pool = wpool;
ctx->route = r;
ctx->listen_fd = EVUTIL_INVALID_SOCKET;
- ctx->raw_fd = -1;
+ ctx->raw_fd = EVUTIL_INVALID_SOCKET;
+
+ if (r->opts.broadcast_reply == BROADCAST_REPLY_UPSTREAM) {
+ rc = datagram_raw_open_ipv4(&ctx->raw_fd);
+ if (rc != 0) {
+ return rc;
+ }
+ }
rc = compat_mutex_init(&ctx->clients_mu);
if (rc != 0) {
diff --git a/src/file_conf.c b/src/file_conf.c
index bd2bed9..8b6ae5a 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -150,6 +150,27 @@ static int parse_proto(const char *s, enum endpoint_proto *out)
return -1;
}
+static int parse_broadcast_reply_mode(
+ const char *value,
+ enum broadcast_reply_mode *out)
+{
+ if (!value || !out) {
+ return -EINVAL;
+ }
+
+ if (strcmp(value, "listen") == 0) {
+ *out = BROADCAST_REPLY_LISTEN;
+ return 0;
+ }
+
+ if (strcmp(value, "upstream") == 0) {
+ *out = BROADCAST_REPLY_UPSTREAM;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
static int parse_positive_int(const char *s, int *out)
{
char *end = NULL;
@@ -192,6 +213,16 @@ static int parse_route_options(char *s, struct route_options *opts)
} \
} while (0)
+#define PARSE_ENUM_OPT(name, parser, field) do { \
+ if (strcmp(key, name) == 0) { \
+ int rc = parser(value, &opts->field); \
+ if (rc != 0) { \
+ return rc; \
+ } \
+ goto next_option; \
+ } \
+ } while (0)
+
#define PARSE_BOOL_OPT(name, field) do { \
if (strcmp(tok, name) == 0) { \
opts->field = true; \
@@ -225,13 +256,20 @@ static int parse_route_options(char *s, struct route_options *opts)
PARSE_INT_OPT("idle_timeout", idle_timeout_sec);
PARSE_INT_OPT("connect_timeout", connect_timeout_sec);
+ PARSE_ENUM_OPT("broadcast_reply",
+ parse_broadcast_reply_mode,
+ broadcast_reply);
return -EINVAL;
}
PARSE_BOOL_OPT("proxy_v2", proxy_v2);
PARSE_BOOL_OPT("keep_alive", keep_alive);
- PARSE_BOOL_OPT("broadcast_reply", broadcast_reply);
+
+ if (strcmp(tok, "broadcast_reply") == 0) {
+ opts->broadcast_reply = BROADCAST_REPLY_LISTEN;
+ goto next_option;
+ }
return -EINVAL;
@@ -240,6 +278,7 @@ next_option:
}
#undef PARSE_BOOL_OPT
+#undef PARSE_ENUM_OPT
#undef PARSE_INT_OPT
return 0;
diff --git a/src/route.c b/src/route.c
index fa517c0..57944f1 100644
--- a/src/route.c
+++ b/src/route.c
@@ -51,6 +51,17 @@ void route_options_str(const struct route_options *opts, char *buf, size_t bufle
ADD_INT("connect_timeout", opts->connect_timeout_sec);
ADD_BOOL("keep_alive", opts->keep_alive);
+ switch (opts->broadcast_reply) {
+ case BROADCAST_REPLY_OFF:
+ break;
+ case BROADCAST_REPLY_LISTEN:
+ ADD_FMT("%s=%s", "broadcast_reply", "listen");
+ break;
+ case BROADCAST_REPLY_UPSTREAM:
+ ADD_FMT("%s=%s", "broadcast_reply", "upstream");
+ break;
+ }
+
#undef ADD_INT
#undef ADD_BOOL
#undef ADD_FMT
diff --git a/src/route.h b/src/route.h
index 24c3206..454796f 100644
--- a/src/route.h
+++ b/src/route.h
@@ -16,14 +16,19 @@
#define UDP_MAX_PACKET 65535
+enum broadcast_reply_mode {
+ BROADCAST_REPLY_OFF = 0,
+ BROADCAST_REPLY_LISTEN,
+ BROADCAST_REPLY_UPSTREAM,
+};
+
struct route_options {
bool proxy_v2;
bool keep_alive;
- bool broadcast_reply;
- bool transparent_replay;
int idle_timeout_sec;
int connect_timeout_sec;
+ enum broadcast_reply_mode broadcast_reply;
};
struct route {