UDP/TCP Windows/Linux转换小工具 突破UDP限制

udptcp — UDP ↔ TCP 转换工具

UDP 与 TCP 双向透明转换,保留 UDP 数据报边界,支持域名解析自动重试。

背景

WireGuard 是一款现代 VPN 工具,协议简洁、性能优秀、安全性强,在正常网络环境下几乎是最优选择。然而它是纯 UDP 协议,而公司防火墙对 UDP 的管控非常激进:

  • 流量稍微超过 1 Mbit/s 就触发限速或丢包
  • 持续传输时 UDP 会话直接被阻断,导致 WireGuard 隧道断连
  • 相同条件下 TCP 流量完全不受影响

根本原因在于:公司防火墙可以对无连接的 UDP 流量做任意整形,而 TCP 有完整的连接状态,干扰成本更高、更容易被用户察觉,因此通常放行。

udptcp 正是为了解决这一问题而开发的:在 WireGuard 节点两侧各运行一个实例,将 UDP 报文透明包裹进 TCP 连接传输,绕过公司防火墙对 UDP 的干扰,使 WireGuard 在受限网络中恢复可用。

1
2
3
4
5
[WireGuard 客户端]
↓ UDP
[udptcp --u2t] ──── TCP ────▶ [udptcp --t2u]
↓ UDP
[WireGuard 服务端]

目录


原理

帧格式

TCP 是字节流,UDP 是数据报。为了在 TCP 上透明还原 UDP 报文边界,每条消息前加 2 字节大端长度头:

1
2
3
┌─────────────────────────────────────────────────┐
│ uint16_be length │ payload (0–65507 bytes) │
└─────────────────────────────────────────────────┘

接收方先读 2 字节,得到 length,再读 length 字节,还原为一个完整 UDP 报文。

–u2t 模式(UDP → TCP)

1
2
3
UDP client  →  [udptcp listen :udp-port]  →  TCP server :host:port
↑ 每个 UDP 源地址独立一条 TCP 连接
TCP 响应帧→解帧→sendto 回对应 UDP client

适用场景:将 UDP 应用连接到只暴露 TCP 接口的服务。

–t2u 模式(TCP → UDP)

1
2
3
TCP client  →  [udptcp listen :tcp-port]  →  UDP server :host:port
↑ 每条 TCP 连接独立一个 UDP socket
UDP 响应帧→加帧→发回 TCP client

适用场景:将 TCP 应用连接到只暴露 UDP 接口的服务(如 DNS、游戏服务器)。

DNS 重试

resolve_retry() 在 DNS 失败时以指数退避重试(1→2→4→8→16 秒),最多 5 次(TCP)或 3 次(UDP),适合目标域名尚未上线或网络暂时故障的场景。

会话管理(U2T)

同一 UDP 源地址(IP:port)对应同一条 TCP 连接,复用到 TCP 连接关闭。
TCP 连接建立在锁外完成,避免 DNS 重试期间阻塞其他 UDP 客户端。
采用双重检查锁(double-checked locking),防止并发建立重复连接。


编译

Linux

依赖:GCC + pthreads(任何主流发行版均已内置)

1
gcc udptcp_linux.c -o udptcp -lpthread

Windows(MinGW / MSYS2)

1
gcc udptcp_win.c -o udptcp_win.exe -lws2_32 -lshell32

注意:Windows 版不能定义 WIN32_LEAN_AND_MEAN,该宏会隐式定义 NOSERVICE,导致 <winsvc.h> 中的服务 API 被排除。


用法

两个平台命令行参数完全一致:

1
2
udptcp --u2t --listen <udp-port> --host <host> --port <port> [--verbose]
udptcp --t2u --listen <tcp-port> --host <host> --port <port> [--verbose]
参数 说明
--u2t UDP 监听 → TCP 转发
--t2u TCP 监听 → UDP 转发
--listen 本地监听端口
--host 远端主机(IP 或域名)
--port 远端端口
--verbose 每帧 hex+ascii 打印(调试用)
--help 显示帮助

示例

1
2
3
4
5
6
7
8
# UDP 客户端 → 本机 :7001 → TCP 服务器 127.0.0.1:7002
udptcp --u2t --listen 7001 --host 127.0.0.1 --port 7002

# TCP 客户端 → 本机 :6666 → UDP 服务器 fly.example.com:6666
udptcp --t2u --listen 6666 --host fly.example.com --port 6666

# 查看每帧内容
udptcp --t2u --listen 6666 --host 127.0.0.1 --port 5300 --verbose

服务部署

Linux systemd

编译后将二进制放到 /bin/(或任意路径),编写 unit 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/systemd/system/udptcp.service
[Unit]
Description=UDP<->TCP Converter
After=network-online.target
Wants=network-online.target

[Service]

Type=simple
ExecStart=/bin/udptcp --t2u --listen 6666 --host 127.0.0.1 --port 5300
Restart=on-failure
RestartSec=5

[Install]

WantedBy=multi-user.target

ExecStart 改为实际参数,然后:

1
2
3
4
5
6
7
8
cp udptcp.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable udptcp
systemctl start udptcp

# 查看状态
systemctl status udptcp
journalctl -u udptcp -f

卸载:

1
2
3
4
systemctl stop    udptcp
systemctl disable udptcp
rm /etc/systemd/system/udptcp.service
systemctl daemon-reload

Windows 服务

Windows 版内置服务管理,参数直接写入注册表 ImagePath,无需额外配置文件。

安装(自动请求 UAC 提权)

第一步:将 exe 放到永久目录

服务注册后,Windows SCM 从注册表 ImagePath 直接启动该路径的 exe。
exe 所在目录就是服务的运行目录,日志也写在这里——安装后不能移动或删除该目录。

推荐放到一个固定位置,例如:

1
C:\Services\udptcp\udptcp_win.exe

第二步:在该目录下执行安装命令

1
2
cd C:\Services\udptcp
udptcp_win.exe --install --t2u --listen 6666 --host fly.example.com --port 6666
  • 若当前非管理员,自动弹出 UAC 提权窗口,提权后继续安装,无需手动重跑
  • 安装成功后服务立即自动启动,无需额外 sc start
  • 启动参数嵌入注册表,无需额外配置文件:
    HKLM\SYSTEM\CurrentControlSet\Services\udptcp\ImagePath
    值示例:"C:\Services\udptcp\udptcp_win.exe" --t2u --listen 6666 --host fly.example.com --port 6666
  • 日志文件:C:\Services\udptcp\udptcp_win.log(与 exe 同目录)

重要C:\Services\udptcp\ 目录是服务运行目录,安装后不得移动、重命名或删除,
否则服务将无法启动。如需迁移路径,请先卸载再重新安装。

卸载

1
udptcp_win.exe --uninstall
  • 停止服务(等待最多 15 秒直至 STOPPED 状态)
  • 从 SCM 删除服务注册(注册表项同步清除)
  • 删除日志文件 udptcp_win.log
  • 卸载完成后可手动删除整个目录

手动控制

1
2
3
sc start udptcp
sc stop udptcp
sc query udptcp

注意事项

  • 服务以 SERVICE_AUTO_START 注册,随系统启动自动运行
  • 服务停止信号通过 g_stop_event(手动重置事件)传递到收发循环
  • UDP 模式:SO_RCVTIMEO = 1s,每秒检查一次停止信号
  • TCP 模式:select(timeout=1s)accept() 前检查停止信号

源码:udptcp_linux.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/*
* udptcp.c — UDP<->TCP converter with datagram boundary preservation
*
* Framing protocol over TCP: [uint16_be length][payload]
*
* --u2t --listen <udp-port> --host <h> --port <p>
* UDP clients --> TCP server at h:p
* Each unique UDP source addr gets its own TCP connection.
* TCP responses are framed back as UDP datagrams (backflow).
*
* --t2u --listen <tcp-port> --host <h> --port <p>
* TCP clients --> UDP server at h:p
* Each TCP connection gets its own UDP socket.
* UDP responses are framed back over TCP.
*
* --verbose hex+ascii dump every payload
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <signal.h>
#include <time.h>
/* atomic helpers via GCC builtins (compatible with GCC < 4.9) */
#define atomic_int volatile int
#define atomic_store(p, v) __sync_lock_test_and_set((p), (v))
#define atomic_load(p) __sync_fetch_and_add((p), 0)
#include <ctype.h>

#define MAX_PKT 65507 /* max UDP payload size */

static int g_verbose = 0;

/* ================================================================
* Logging
* ================================================================ */

static void ts_str(char *buf, size_t n) {
struct timespec tp;
clock_gettime(CLOCK_REALTIME, &tp);
struct tm tm;
localtime_r(&tp.tv_sec, &tm);
strftime(buf, n, "%Y-%m-%d %H:%M:%S", &tm);
size_t l = strlen(buf);
snprintf(buf + l, n - l, ".%03ld", tp.tv_nsec / 1000000);
}

#define LOG(fmt, ...) do { \
char _ts[40]; ts_str(_ts, sizeof(_ts)); \
printf("[%s] " fmt "\n", _ts, ##__VA_ARGS__); \
fflush(stdout); \
} while(0)


#define ERR(fmt, ...) do { \
char _ts[40]; ts_str(_ts, sizeof(_ts)); \
fprintf(stderr, "[%s] ERR " fmt "\n", _ts, ##__VA_ARGS__); \
} while(0)


static void hexdump(const char *label, const unsigned char *data, ssize_t len) {
char _ts[40]; ts_str(_ts, sizeof(_ts));
printf("[%s] %s (%zd bytes)\n", _ts, label, len);
for (ssize_t i = 0; i < len; i += 16) {
printf(" %04zx ", i);
for (int j = 0; j < 16; j++) {
if (i+j < len) printf("%02x ", data[i+j]); else printf(" ");
if (j == 7) printf(" ");
}
printf(" |");
for (int j = 0; j < 16 && i+j < len; j++)
putchar(isprint(data[i+j]) ? data[i+j] : '.');
printf("|\n");
}
fflush(stdout);
}

/* ================================================================
* I/O helpers
* ================================================================ */

static int write_all(int fd, const void *buf, size_t n) {
const char *p = buf;
while (n > 0) {
ssize_t w = write(fd, p, n);
if (w <= 0) return -1;
p += w; n -= w;
}
return 0;
}

static int read_exact(int fd, void *buf, size_t n) {
char *p = buf;
while (n > 0) {
ssize_t r = read(fd, p, n);
if (r <= 0) return -1;
p += r; n -= r;
}
return 0;
}

/* ================================================================
* Framing: [uint16_be len][payload]
* ================================================================ */

static int send_framed(int tcp_fd, const void *data, uint16_t len) {
uint8_t hdr[2] = { (uint8_t)(len >> 8), (uint8_t)(len & 0xff) };
if (write_all(tcp_fd, hdr, 2) < 0) return -1;
return write_all(tcp_fd, data, len);
}

/* returns payload length >0, or -1 on error/EOF */
static int recv_framed(int tcp_fd, void *buf, size_t bufsz) {
uint8_t hdr[2];
if (read_exact(tcp_fd, hdr, 2) < 0) return -1;
uint16_t len = ((uint16_t)hdr[0] << 8) | hdr[1];
if (len == 0) return -1;
if (len > bufsz) { ERR("frame too large: %u", len); return -1; }
if (read_exact(tcp_fd, buf, len) < 0) return -1;
return (int)len;
}

/* ================================================================
* Network helpers
* ================================================================ */


/* DNS resolve with exponential-backoff retry: 1s 2s 4s 8s 16s */
static struct addrinfo *resolve_retry(const char *host, const char *port,
int socktype, int retries)
{
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;
for (int i = 0; i <= retries; i++) {
if (i > 0) {
unsigned d = 1u << (i < 5 ? i - 1 : 4);
ERR("DNS retry %s:%s in %us (%d/%d)", host, port, d, i, retries);
sleep(d);
}
int rc = getaddrinfo(host, port, &hints, &res);
if (rc == 0) return res;
ERR("getaddrinfo %s:%s: %s", host, port, gai_strerror(rc));
}
return NULL;
}

static int tcp_connect_to(const char *host, const char *port) {
struct addrinfo *res = resolve_retry(host, port, SOCK_STREAM, 5);
if (!res) return -1;
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0 || connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
ERR("tcp_connect %s:%s: %s", host, port, strerror(errno));
if (fd >= 0) close(fd);
freeaddrinfo(res); return -1;
}
freeaddrinfo(res);
return fd;
}

static int udp_socket_to(const char *host, const char *port,
struct sockaddr_storage *dst, socklen_t *dstlen)
{
struct addrinfo *res = resolve_retry(host, port, SOCK_DGRAM, 3);
if (!res) return -1;
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { ERR("udp socket: %s", strerror(errno)); freeaddrinfo(res); return -1; }
memcpy(dst, res->ai_addr, res->ai_addrlen);
*dstlen = res->ai_addrlen;
freeaddrinfo(res);
return fd;
}

static void addr_str(struct sockaddr_storage *ss, char *buf, size_t n) {
char ip[INET6_ADDRSTRLEN];
int port;
if (ss->ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)ss;
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip));
port = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)ss;
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof(ip));
port = ntohs(s->sin6_port);
}
snprintf(buf, n, "%s:%d", ip, port);
}

/* ================================================================
* U2T mode: UDP listen → TCP forward
* ================================================================ */


struct u2t_session {
struct sockaddr_storage udp_src;
socklen_t udp_srclen;
char key[64]; /* "ip:port" string */
int tcp_fd;
int udp_fd; /* shared listen socket (sendto back) */
atomic_int alive;
struct u2t_session *next;
};

struct u2t_map {
struct u2t_session *head;
pthread_mutex_t lock;
int udp_fd;
const char *fwd_host;
const char *fwd_port;
};

struct u2t_backflow_arg {
struct u2t_session *sess;
struct u2t_map *map;
};

/* TCP → UDP backflow thread (one per UDP client session) */
static void *u2t_backflow(void *arg) {
struct u2t_backflow_arg *a = arg;
struct u2t_session *s = a->sess;
struct u2t_map *m = a->map;
free(a);

uint8_t buf[MAX_PKT];
int n;
while ((n = recv_framed(s->tcp_fd, buf, sizeof(buf))) > 0) {
if (g_verbose) hexdump("TCP->UDP(back)", buf, n);
sendto(s->udp_fd, buf, n, 0,
(struct sockaddr *)&s->udp_src, s->udp_srclen);
}

LOG(" [%s] session closed", s->key);
atomic_store(&s->alive, 0);

pthread_mutex_lock(&m->lock);
struct u2t_session **pp = &m->head;
while (*pp && *pp != s) pp = &(*pp)->next;
if (*pp) *pp = s->next;
pthread_mutex_unlock(&m->lock);

close(s->tcp_fd);
free(s);
return NULL;
}

static struct u2t_session *u2t_get_session(struct u2t_map *map,
struct sockaddr_storage *src, socklen_t srclen)
{

char key[64];
addr_str(src, key, sizeof(key));

/* fast path: existing alive session */
pthread_mutex_lock(&map->lock);
struct u2t_session *s;
for (s = map->head; s; s = s->next)
if (s->udp_srclen == srclen &&
memcmp(&s->udp_src, src, srclen) == 0 &&
atomic_load(&s->alive)) {
pthread_mutex_unlock(&map->lock);
return s;
}
pthread_mutex_unlock(&map->lock);

/* TCP connect (with DNS retry) outside the lock so other sessions aren't blocked */
int tcp_fd = tcp_connect_to(map->fwd_host, map->fwd_port);
if (tcp_fd < 0) return NULL;

s = calloc(1, sizeof(*s));
memcpy(&s->udp_src, src, srclen);
s->udp_srclen = srclen;
strncpy(s->key, key, sizeof(s->key) - 1);
s->tcp_fd = tcp_fd;
s->udp_fd = map->udp_fd;
atomic_store(&s->alive, 1);

/* re-acquire lock; if another thread raced in and made the same session, use theirs */
pthread_mutex_lock(&map->lock);
struct u2t_session *existing;
for (existing = map->head; existing; existing = existing->next)
if (existing->udp_srclen == srclen &&
memcmp(&existing->udp_src, src, srclen) == 0 &&
atomic_load(&existing->alive)) {
pthread_mutex_unlock(&map->lock);
close(tcp_fd);
free(s);
return existing;
}
s->next = map->head;
map->head = s;
pthread_mutex_unlock(&map->lock);

LOG(" new session [%s] -> TCP %s:%s", key, map->fwd_host, map->fwd_port);

struct u2t_backflow_arg *ba = malloc(sizeof(*ba));
ba->sess = s; ba->map = map;
pthread_t t;
pthread_create(&t, NULL, u2t_backflow, ba);
pthread_detach(t);
return s;
}

static void run_u2t(const char *lport, const char *fwd_host, const char *fwd_port) {
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo("0.0.0.0", lport, &hints, &res) != 0) {
ERR("getaddrinfo udp %s: %s", lport, strerror(errno)); return;
}
int udp_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (udp_fd < 0) { perror("udp socket"); return; }
int opt = 1;
setsockopt(udp_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(udp_fd, res->ai_addr, res->ai_addrlen) < 0) { perror("udp bind"); return; }
freeaddrinfo(res);

LOG("[U2T] UDP 0.0.0.0:%s -> TCP %s:%s", lport, fwd_host, fwd_port);

struct u2t_map map;
map.head = NULL;
map.udp_fd = udp_fd;
map.fwd_host = fwd_host;
map.fwd_port = fwd_port;
pthread_mutex_init(&map.lock, NULL);

uint8_t buf[MAX_PKT];
for (;;) {
struct sockaddr_storage src;
socklen_t srclen = sizeof(src);
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&src, &srclen);
if (n < 0) { ERR("recvfrom: %s", strerror(errno)); continue; }

struct u2t_session *sess = u2t_get_session(&map, &src, srclen);
if (!sess) continue;

if (g_verbose) hexdump("UDP->TCP", buf, n);
if (send_framed(sess->tcp_fd, buf, (uint16_t)n) < 0) {
ERR("send_framed [%s]: %s", sess->key, strerror(errno));
atomic_store(&sess->alive, 0);
}
}
}

/* ================================================================
* T2U mode: TCP listen → UDP forward
* ================================================================ */


struct t2u_conn {
int tcp_fd;
int udp_fd;
struct sockaddr_storage udp_dst;
socklen_t udp_dstlen;
char peer[64];
atomic_int alive;
};

/* UDP → TCP backflow (one per TCP connection) */
static void *t2u_udp_reader(void *arg) {
struct t2u_conn *c = arg;
uint8_t buf[MAX_PKT];
ssize_t n;
while (atomic_load(&c->alive)) {
n = recvfrom(c->udp_fd, buf, sizeof(buf), 0, NULL, NULL);
if (n < 0) break;
if (g_verbose) hexdump("UDP->TCP(back)", buf, n);
if (send_framed(c->tcp_fd, buf, (uint16_t)n) < 0) break;
}
return NULL;
}

static void *t2u_handle(void *arg) {
struct t2u_conn *c = arg;

pthread_t t;
pthread_create(&t, NULL, t2u_udp_reader, c);
pthread_detach(t);

uint8_t buf[MAX_PKT];
int n;
while ((n = recv_framed(c->tcp_fd, buf, sizeof(buf))) > 0) {
if (g_verbose) hexdump("TCP->UDP", buf, n);
sendto(c->udp_fd, buf, n, 0,
(struct sockaddr *)&c->udp_dst, c->udp_dstlen);
}

atomic_store(&c->alive, 0);
shutdown(c->tcp_fd, SHUT_RDWR);
LOG(" [%s] closed", c->peer);
close(c->tcp_fd);
close(c->udp_fd);
free(c);
return NULL;
}

static void run_t2u(const char *lport, const char *fwd_host, const char *fwd_port) {
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo("0.0.0.0", lport, &hints, &res) != 0) {
ERR("getaddrinfo tcp %s: %s", lport, strerror(errno)); return;
}
int srv = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (srv < 0) { perror("socket"); return; }
int opt = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(srv, res->ai_addr, res->ai_addrlen) < 0) { perror("bind"); return; }
freeaddrinfo(res);
if (listen(srv, 128) < 0) { perror("listen"); return; }

LOG("[T2U] TCP 0.0.0.0:%s -> UDP %s:%s", lport, fwd_host, fwd_port);

for (;;) {
struct sockaddr_storage peer;
socklen_t plen = sizeof(peer);
int tcp_fd = accept(srv, (struct sockaddr *)&peer, &plen);
if (tcp_fd < 0) { ERR("accept: %s", strerror(errno)); continue; }

struct t2u_conn *c = calloc(1, sizeof(*c));
c->tcp_fd = tcp_fd;
atomic_store(&c->alive, 1);
addr_str(&peer, c->peer, sizeof(c->peer));

c->udp_fd = udp_socket_to(fwd_host, fwd_port, &c->udp_dst, &c->udp_dstlen);
if (c->udp_fd < 0) { close(tcp_fd); free(c); continue; }

/* connect UDP so recv only gets responses from target */
if (connect(c->udp_fd, (struct sockaddr *)&c->udp_dst, c->udp_dstlen) < 0) {
ERR("udp connect: %s", strerror(errno));
close(c->udp_fd); close(tcp_fd); free(c); continue;
}

LOG("[T2U] TCP [%s] -> UDP %s:%s", c->peer, fwd_host, fwd_port);

pthread_t t;
pthread_create(&t, NULL, t2u_handle, c);
pthread_detach(t);
}
}

/* ================================================================
* main
* ================================================================ */

static void usage(const char *p) {
printf(
"Usage:\n"
" %s --u2t --listen <udp-port> --host <h> --port <p> [--verbose]\n"
" %s --t2u --listen <tcp-port> --host <h> --port <p> [--verbose]\n"
"\n"
"Options:\n"
" --u2t UDP listen -> TCP forward (each UDP src gets its own TCP conn)\n"
" --t2u TCP listen -> UDP forward (each TCP conn gets its own UDP sock)\n"
" --listen local port to bind\n"
" --host remote host (IP or domain name)\n"
" --port remote port\n"
" --verbose hex+ascii dump of every payload\n"
" --help show this help\n"
"\n"
"Framing: [uint16_be length][payload] — preserves UDP datagram boundaries\n"
"\n"
"Examples:\n"
" %s --u2t --listen 7001 --host 127.0.0.1 --port 7002\n"
" %s --t2u --listen 6666 --host 127.0.0.1 --port 5300\n",
p, p, p, p);
exit(0);
}

static void usage_err(const char *p) {
fprintf(stderr, "error: invalid arguments. Run '%s --help' for usage.\n", p);
exit(1);
}

int main(int argc, char *argv[]) {
int mode_u2t = 0, mode_t2u = 0;
const char *listen_port = NULL, *fwd_host = NULL, *fwd_port = NULL;

for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--u2t")) mode_u2t = 1;
else if (!strcmp(argv[i], "--t2u")) mode_t2u = 1;
else if (!strcmp(argv[i], "--verbose")) g_verbose = 1;
else if (!strcmp(argv[i], "--listen") && i+1 < argc) listen_port = argv[++i];
else if (!strcmp(argv[i], "--host") && i+1 < argc) fwd_host = argv[++i];
else if (!strcmp(argv[i], "--port") && i+1 < argc) fwd_port = argv[++i];
else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) usage(argv[0]);
else usage_err(argv[0]);
}

signal(SIGPIPE, SIG_IGN);

if ((mode_u2t + mode_t2u) != 1) { fputs("specify exactly one of --u2t / --t2u\n", stderr); usage_err(argv[0]); }
if (!listen_port || !fwd_host || !fwd_port) { fputs("--listen, --host, --port required\n", stderr); usage_err(argv[0]); }

if (mode_u2t) run_u2t(listen_port, fwd_host, fwd_port);
else run_t2u(listen_port, fwd_host, fwd_port);
return 0;
}

源码:udptcp_win.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
/*
* udptcp_win.c — UDP<->TCP converter for Windows
*
* Compile: gcc udptcp_win.c -o udptcp_win.exe -lws2_32 -lshell32
*
* Framing over TCP: [uint16_be length][payload]
*
* Run directly:
* udptcp_win.exe --u2t --listen <udp-port> --host <h> --port <p> [--verbose]
* udptcp_win.exe --t2u --listen <tcp-port> --host <h> --port <p> [--verbose]
*
* Windows service:
* udptcp_win.exe --install --u2t --listen 6666 --host example.com --port 6666
* udptcp_win.exe --uninstall
* sc start udptcp
* sc stop udptcp
*
* Startup parameters are embedded in the service ImagePath in the registry:
* HKLM\SYSTEM\CurrentControlSet\Services\udptcp\ImagePath
* Log file (service mode): same directory as the exe, udptcp_win.log
*/

#define _WIN32_WINNT 0x0600 /* Vista+ for inet_ntop / getaddrinfo */
/* Note: do NOT define WIN32_LEAN_AND_MEAN — it implies NOSERVICE */
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <winsvc.h>
#include <shellapi.h>
#ifdef _MSC_VER
# pragma comment(lib, "ws2_32.lib")
# pragma comment(lib, "shell32.lib")
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>

#define SERVICE_NAME "udptcp"
#define MAX_PKT 65507

static int g_verbose = 0;
static HANDLE g_stop_event = NULL; /* manual-reset event; set by svc_ctrl to signal stop */

static int should_stop(void) {
return g_stop_event &&
WaitForSingleObject(g_stop_event, 0) == WAIT_OBJECT_0;
}

static void interruptible_sleep(DWORD ms) {
if (g_stop_event) WaitForSingleObject(g_stop_event, ms);
else Sleep(ms);
}

/* ================================================================
* Logging (stdout/stderr; service mode redirects these to a file)
* ================================================================ */
static void ts_str(char *buf, size_t n) {
SYSTEMTIME st; GetLocalTime(&st);
snprintf(buf, n, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
}

#define LOG(fmt, ...) do { \
char _ts[40]; ts_str(_ts, sizeof(_ts)); \
printf("[%s] " fmt "\n", _ts, ##__VA_ARGS__); \
fflush(stdout); \
} while(0)

#define ERR(fmt, ...) do { \
char _ts[40]; ts_str(_ts, sizeof(_ts)); \
fprintf(stderr, "[%s] ERR " fmt "\n", _ts, ##__VA_ARGS__); \
fflush(stderr); \
} while(0)

static const char *wsa_err(void) {
static char buf[32];
snprintf(buf, sizeof(buf), "WSA#%d", WSAGetLastError());
return buf;
}

static void hexdump(const char *label, const unsigned char *data, int len) {
char _ts[40]; ts_str(_ts, sizeof(_ts));
printf("[%s] %s (%d bytes)\n", _ts, label, len);
for (int i = 0; i < len; i += 16) {
printf(" %04x ", i);
for (int j = 0; j < 16; j++) {
if (i+j < len) printf("%02x ", data[i+j]); else printf(" ");
if (j == 7) printf(" ");
}
printf(" |");
for (int j = 0; j < 16 && i+j < len; j++)
putchar(isprint((unsigned char)data[i+j]) ? data[i+j] : '.');
printf("|\n");
}
fflush(stdout);
}

/* ================================================================
* I/O helpers
* ================================================================ */
static int write_all(SOCKET fd, const void *buf, int n) {
const char *p = (const char *)buf;
while (n > 0) {
int w = send(fd, p, n, 0);
if (w == SOCKET_ERROR) return -1;
p += w; n -= w;
}
return 0;
}

static int read_exact(SOCKET fd, void *buf, int n) {
char *p = (char *)buf;
while (n > 0) {
int r = recv(fd, p, n, 0);
if (r <= 0) return -1;
p += r; n -= r;
}
return 0;
}

/* ================================================================
* Framing: [uint16_be len][payload]
* ================================================================ */
static int send_framed(SOCKET tcp_fd, const void *data, uint16_t len) {
uint8_t hdr[2] = { (uint8_t)(len >> 8), (uint8_t)(len & 0xff) };
if (write_all(tcp_fd, hdr, 2) < 0) return -1;
return write_all(tcp_fd, data, (int)len);
}

static int recv_framed(SOCKET tcp_fd, void *buf, int bufsz) {
uint8_t hdr[2];
if (read_exact(tcp_fd, hdr, 2) < 0) return -1;
uint16_t len = ((uint16_t)hdr[0] << 8) | hdr[1];
if (len == 0 || (int)len > bufsz) { ERR("bad frame len %u", len); return -1; }
if (read_exact(tcp_fd, buf, (int)len) < 0) return -1;
return (int)len;
}

/* ================================================================
* Network helpers
* ================================================================ */
static void addr_str(struct sockaddr_storage *ss, char *buf, size_t n) {
char ip[INET6_ADDRSTRLEN] = {0};
int port = 0;
if (ss->ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)ss;
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip));
port = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)ss;
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof(ip));
port = ntohs(s->sin6_port);
}
snprintf(buf, n, "%s:%d", ip, port);
}

/* DNS resolve with exponential-backoff retry: 1 2 4 8 16 s */
static struct addrinfo *resolve_retry(const char *host, const char *port,
int socktype, int retries) {
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;
for (int i = 0; i <= retries; i++) {
if (i > 0) {
DWORD d = (DWORD)(1u << (i < 5 ? i - 1 : 4));
ERR("DNS retry %s:%s in %lus (%d/%d)", host, port, (unsigned long)d, i, retries);
interruptible_sleep(d * 1000);
if (should_stop()) return NULL;
}
int rc = getaddrinfo(host, port, &hints, &res);
if (rc == 0) return res;
ERR("getaddrinfo %s:%s: %s", host, port, gai_strerror(rc));
}
return NULL;
}

static SOCKET tcp_connect_to(const char *host, const char *port) {
struct addrinfo *res = resolve_retry(host, port, SOCK_STREAM, 5);
if (!res) return INVALID_SOCKET;
SOCKET fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd == INVALID_SOCKET ||
connect(fd, res->ai_addr, (int)res->ai_addrlen) == SOCKET_ERROR) {
ERR("tcp_connect %s:%s: %s", host, port, wsa_err());
if (fd != INVALID_SOCKET) closesocket(fd);
freeaddrinfo(res); return INVALID_SOCKET;
}
freeaddrinfo(res);
return fd;
}

static SOCKET udp_socket_to(const char *host, const char *port,
struct sockaddr_storage *dst, int *dstlen) {
struct addrinfo *res = resolve_retry(host, port, SOCK_DGRAM, 3);
if (!res) return INVALID_SOCKET;
SOCKET fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd == INVALID_SOCKET) {
ERR("udp socket: %s", wsa_err()); freeaddrinfo(res); return INVALID_SOCKET;
}
memcpy(dst, res->ai_addr, res->ai_addrlen);
*dstlen = (int)res->ai_addrlen;
freeaddrinfo(res);
return fd;
}

/* ================================================================
* U2T: UDP listen -> TCP forward
* ================================================================ */

struct u2t_session {
struct sockaddr_storage udp_src;
int udp_srclen;
char key[64];
SOCKET tcp_fd;
SOCKET udp_fd; /* shared listen socket for sendto back */
volatile LONG alive;
struct u2t_session *next;
};

struct u2t_map {
struct u2t_session *head;
CRITICAL_SECTION lock;
SOCKET udp_fd;
const char *fwd_host;
const char *fwd_port;
};

struct u2t_backflow_arg {
struct u2t_session *sess;
struct u2t_map *map;
};

static DWORD WINAPI u2t_backflow(LPVOID arg) {
struct u2t_backflow_arg *a = (struct u2t_backflow_arg *)arg;
struct u2t_session *s = a->sess;
struct u2t_map *m = a->map;
free(a);

uint8_t buf[MAX_PKT];
int n;
while ((n = recv_framed(s->tcp_fd, buf, sizeof(buf))) > 0) {
if (g_verbose) hexdump("TCP->UDP(back)", buf, n);
sendto(s->udp_fd, (const char *)buf, n, 0,
(struct sockaddr *)&s->udp_src, s->udp_srclen);
}

LOG(" [%s] session closed", s->key);
InterlockedExchange(&s->alive, 0);

EnterCriticalSection(&m->lock);
struct u2t_session **pp = &m->head;
while (*pp && *pp != s) pp = &(*pp)->next;
if (*pp) *pp = s->next;
LeaveCriticalSection(&m->lock);

closesocket(s->tcp_fd);
free(s);
return 0;
}

static struct u2t_session *u2t_get_session(struct u2t_map *map,
struct sockaddr_storage *src, int srclen) {

char key[64];
addr_str(src, key, sizeof(key));

EnterCriticalSection(&map->lock);
struct u2t_session *s;
for (s = map->head; s; s = s->next)
if (s->udp_srclen == srclen &&
memcmp(&s->udp_src, src, srclen) == 0 &&
InterlockedCompareExchange(&s->alive, 0, 0) != 0) {
LeaveCriticalSection(&map->lock);
return s;
}
LeaveCriticalSection(&map->lock);

/* TCP connect outside the lock so other sessions aren't blocked */
SOCKET tcp_fd = tcp_connect_to(map->fwd_host, map->fwd_port);
if (tcp_fd == INVALID_SOCKET) return NULL;

s = (struct u2t_session *)calloc(1, sizeof(*s));
memcpy(&s->udp_src, src, srclen);
s->udp_srclen = srclen;
strncpy(s->key, key, sizeof(s->key) - 1);
s->tcp_fd = tcp_fd;
s->udp_fd = map->udp_fd;
InterlockedExchange(&s->alive, 1);

EnterCriticalSection(&map->lock);
struct u2t_session *existing;
for (existing = map->head; existing; existing = existing->next)
if (existing->udp_srclen == srclen &&
memcmp(&existing->udp_src, src, srclen) == 0 &&
InterlockedCompareExchange(&existing->alive, 0, 0) != 0) {
LeaveCriticalSection(&map->lock);
closesocket(tcp_fd); free(s);
return existing;
}
s->next = map->head;
map->head = s;
LeaveCriticalSection(&map->lock);

LOG(" new session [%s] -> TCP %s:%s", key, map->fwd_host, map->fwd_port);

struct u2t_backflow_arg *ba = (struct u2t_backflow_arg *)malloc(sizeof(*ba));
ba->sess = s; ba->map = map;
HANDLE h = CreateThread(NULL, 0, u2t_backflow, ba, 0, NULL);
if (h) CloseHandle(h);
return s;
}

static void run_u2t(const char *lport, const char *fwd_host, const char *fwd_port) {
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, lport, &hints, &res) != 0) {
ERR("getaddrinfo udp :%s: %s", lport, wsa_err()); return;
}
SOCKET udp_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (udp_fd == INVALID_SOCKET) { ERR("udp socket: %s", wsa_err()); return; }
int opt = 1;
setsockopt(udp_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt));
if (bind(udp_fd, res->ai_addr, (int)res->ai_addrlen) == SOCKET_ERROR) {
ERR("udp bind: %s", wsa_err()); closesocket(udp_fd); return;
}
freeaddrinfo(res);

LOG("[U2T] UDP 0.0.0.0:%s -> TCP %s:%s", lport, fwd_host, fwd_port);

/* 1-second recv timeout so the loop can check should_stop() */
DWORD rcvtmo = 1000;
setsockopt(udp_fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&rcvtmo, sizeof(rcvtmo));

struct u2t_map map;
map.head = NULL;
map.udp_fd = udp_fd;
map.fwd_host = fwd_host;
map.fwd_port = fwd_port;
InitializeCriticalSection(&map.lock);

uint8_t buf[MAX_PKT];
for (;;) {
if (should_stop()) break;
struct sockaddr_storage src;
int srclen = sizeof(src);
int n = recvfrom(udp_fd, (char *)buf, sizeof(buf), 0,
(struct sockaddr *)&src, &srclen);
if (n == SOCKET_ERROR) {
if (WSAGetLastError() == WSAETIMEDOUT) continue;
ERR("recvfrom: %s", wsa_err());
if (should_stop()) break;
continue;
}

struct u2t_session *sess = u2t_get_session(&map, &src, srclen);
if (!sess) continue;

if (g_verbose) hexdump("UDP->TCP", buf, n);
if (send_framed(sess->tcp_fd, buf, (uint16_t)n) < 0) {
ERR("send_framed [%s]: %s", sess->key, wsa_err());
InterlockedExchange(&sess->alive, 0);
}
}
closesocket(udp_fd);
}

/* ================================================================
* T2U: TCP listen -> UDP forward
* ================================================================ */

struct t2u_conn {
SOCKET tcp_fd;
SOCKET udp_fd;
struct sockaddr_storage udp_dst;
int udp_dstlen;
char peer[64];
volatile LONG alive;
};

static DWORD WINAPI t2u_udp_reader(LPVOID arg) {
struct t2u_conn *c = (struct t2u_conn *)arg;
uint8_t buf[MAX_PKT];
while (InterlockedCompareExchange(&c->alive, 0, 0) != 0) {
struct sockaddr_storage from;
int fromlen = sizeof(from);
int n = recvfrom(c->udp_fd, (char *)buf, sizeof(buf), 0,
(struct sockaddr *)&from, &fromlen);
if (n == SOCKET_ERROR) break;
if (g_verbose) hexdump("UDP->TCP(back)", buf, n);
if (send_framed(c->tcp_fd, buf, (uint16_t)n) < 0) break;
}
return 0;
}

static DWORD WINAPI t2u_handle(LPVOID arg) {
struct t2u_conn *c = (struct t2u_conn *)arg;

HANDLE h = CreateThread(NULL, 0, t2u_udp_reader, c, 0, NULL);
if (h) CloseHandle(h);

uint8_t buf[MAX_PKT];
int n;
while ((n = recv_framed(c->tcp_fd, buf, sizeof(buf))) > 0) {
if (g_verbose) hexdump("TCP->UDP", buf, n);
sendto(c->udp_fd, (const char *)buf, n, 0,
(struct sockaddr *)&c->udp_dst, c->udp_dstlen);
}

InterlockedExchange(&c->alive, 0);
shutdown(c->tcp_fd, SD_BOTH);
LOG(" [%s] closed", c->peer);
closesocket(c->tcp_fd);
closesocket(c->udp_fd);
free(c);
return 0;
}

static void run_t2u(const char *lport, const char *fwd_host, const char *fwd_port) {
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, lport, &hints, &res) != 0) {
ERR("getaddrinfo tcp :%s: %s", lport, wsa_err()); return;
}
SOCKET srv = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (srv == INVALID_SOCKET) { ERR("socket: %s", wsa_err()); return; }
int opt = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt));
if (bind(srv, res->ai_addr, (int)res->ai_addrlen) == SOCKET_ERROR) {
ERR("bind: %s", wsa_err()); closesocket(srv); return;
}
freeaddrinfo(res);
if (listen(srv, 128) == SOCKET_ERROR) { ERR("listen: %s", wsa_err()); return; }

LOG("[T2U] TCP 0.0.0.0:%s -> UDP %s:%s", lport, fwd_host, fwd_port);

for (;;) {
if (should_stop()) break;
/* 1-second select so we can check should_stop() without blocking forever */
fd_set fds;
FD_ZERO(&fds);
FD_SET(srv, &fds);
struct timeval tv = {1, 0};
if (select(0, &fds, NULL, NULL, &tv) <= 0) continue;

struct sockaddr_storage peer;
int plen = sizeof(peer);
SOCKET tcp_fd = accept(srv, (struct sockaddr *)&peer, &plen);
if (tcp_fd == INVALID_SOCKET) { ERR("accept: %s", wsa_err()); continue; }

struct t2u_conn *c = (struct t2u_conn *)calloc(1, sizeof(*c));
c->tcp_fd = tcp_fd;
InterlockedExchange(&c->alive, 1);
addr_str(&peer, c->peer, sizeof(c->peer));

c->udp_fd = udp_socket_to(fwd_host, fwd_port, &c->udp_dst, &c->udp_dstlen);
if (c->udp_fd == INVALID_SOCKET) { closesocket(tcp_fd); free(c); continue; }

if (connect(c->udp_fd, (struct sockaddr *)&c->udp_dst, c->udp_dstlen) == SOCKET_ERROR) {
ERR("udp connect: %s", wsa_err());
closesocket(c->udp_fd); closesocket(tcp_fd); free(c); continue;
}

LOG("[T2U] TCP [%s] -> UDP %s:%s", c->peer, fwd_host, fwd_port);

HANDLE h = CreateThread(NULL, 0, t2u_handle, c, 0, NULL);
if (h) CloseHandle(h);
}
closesocket(srv);
}

/* ================================================================
* UAC / admin helpers (pattern from wholeton_login.c)
* ================================================================ */

/* CheckTokenMembership: works even when UAC is disabled */
static int is_admin(void) {
BOOL result = FALSE;
PSID admins = NULL;
SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY;
if (AllocateAndInitializeSid(&nt, 2,
SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0, &admins)) {
CheckTokenMembership(NULL, admins, &result);
FreeSid(admins);
}
return result;
}

/* extract args portion from GetCommandLineW(), skip exe token */
static const wchar_t *cmdline_args_only(void) {
const wchar_t *p = GetCommandLineW();
if (!p) return L"";
while (*p == L' ') p++;
if (*p == L'"') { p++; while (*p && *p != L'"') p++; if (*p) p++; }
else { while (*p && *p != L' ') p++; }
while (*p == L' ') p++;
return p;
}

/* re-launch elevated via UAC; uses wide-char GetCommandLineW so Unicode paths are safe */
static void relaunch_as_admin(void) {
wchar_t exe[MAX_PATH];
GetModuleFileNameW(NULL, exe, MAX_PATH);

SHELLEXECUTEINFOW sei;
memset(&sei, 0, sizeof(sei));
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.lpVerb = L"runas";
sei.lpFile = exe;
sei.lpParameters = cmdline_args_only();
sei.nShow = SW_SHOWNORMAL;

if (ShellExecuteExW(&sei)) {
WaitForSingleObject(sei.hProcess, INFINITE);
CloseHandle(sei.hProcess);
} else {
DWORD e = GetLastError();
if (e == ERROR_CANCELLED) fprintf(stderr, "UAC cancelled.\n");
else fprintf(stderr, "ShellExecuteEx failed: %lu\n", e);
}
}

/* ================================================================
* Windows Service
* ================================================================ */

static int g_mode_u2t = 0;
static const char *g_listen_port = NULL;
static const char *g_fwd_host = NULL;
static const char *g_fwd_port = NULL;

static SERVICE_STATUS g_svc_status;
static SERVICE_STATUS_HANDLE g_svc_handle;

static void svc_report(DWORD state) {
g_svc_status.dwCurrentState = state;
SetServiceStatus(g_svc_handle, &g_svc_status);
}

static VOID WINAPI svc_ctrl(DWORD ctrl) {
if (ctrl == SERVICE_CONTROL_STOP || ctrl == SERVICE_CONTROL_SHUTDOWN) {
svc_report(SERVICE_STOP_PENDING);
if (g_stop_event) SetEvent(g_stop_event); /* wake run_u2t / run_t2u loops */
}
}

static VOID WINAPI svc_main(DWORD argc, LPSTR *argv) {
(void)argc; (void)argv;

g_svc_handle = RegisterServiceCtrlHandlerA(SERVICE_NAME, svc_ctrl);
if (!g_svc_handle) return;

g_stop_event = CreateEvent(NULL, TRUE, FALSE, NULL); /* manual-reset */

memset(&g_svc_status, 0, sizeof(g_svc_status));
g_svc_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_svc_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
svc_report(SERVICE_RUNNING);

/* append log next to the exe (no freopen — keeps stdout available for debugging) */
char logpath[MAX_PATH];
GetModuleFileNameA(NULL, logpath, MAX_PATH);
char *dot = strrchr(logpath, '.');
if (dot) strcpy(dot, ".log"); else strcat(logpath, ".log");
FILE *lf = fopen(logpath, "a");
/* redirect both handles to the log file */
if (lf) { freopen(logpath, "a", stdout); freopen(logpath, "a", stderr); fclose(lf); }

WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);

if (g_mode_u2t) run_u2t(g_listen_port, g_fwd_host, g_fwd_port);
else run_t2u(g_listen_port, g_fwd_host, g_fwd_port);

WSACleanup();
CloseHandle(g_stop_event);
g_stop_event = NULL;
svc_report(SERVICE_STOPPED);
}

/* Build service ImagePath: "exe" + all argv[] except --install */
static void svc_install(int argc, char *argv[]) {
char exe[MAX_PATH];
GetModuleFileNameA(NULL, exe, MAX_PATH);

char cmdline[4096];
snprintf(cmdline, sizeof(cmdline), "\"%s\"", exe);
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--install")) continue;
size_t rem = sizeof(cmdline) - strlen(cmdline) - 1;
strncat(cmdline, " ", rem);
rem--;
if (strchr(argv[i], ' ')) {
strncat(cmdline, "\"", rem); rem--;
strncat(cmdline, argv[i], rem); rem -= strlen(argv[i]);
strncat(cmdline, "\"", rem);
} else {
strncat(cmdline, argv[i], rem);
}
}

SC_HANDLE scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (!scm) {
fprintf(stderr, "OpenSCManager failed: %lu (run as Administrator)\n", GetLastError());
return;
}

SC_HANDLE svc = CreateServiceA(
scm,
SERVICE_NAME, /* service name (internal) */
"UDP<->TCP Converter", /* display name */
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, /* start on boot */
SERVICE_ERROR_NORMAL,
cmdline, /* ImagePath with embedded args */
NULL, NULL, NULL, NULL, NULL);

if (!svc) {
DWORD e = GetLastError();
if (e == ERROR_SERVICE_EXISTS)
fprintf(stderr, "Service '%s' already exists — run --uninstall first.\n", SERVICE_NAME);
else
fprintf(stderr, "CreateService failed: %lu\n", e);
} else {
SERVICE_DESCRIPTIONA desc = { (LPSTR)"UDP<->TCP framing converter with DNS retry" };
ChangeServiceConfig2A(svc, SERVICE_CONFIG_DESCRIPTION, &desc);

printf("Service '%s' installed.\n", SERVICE_NAME);
printf("ImagePath : %s\n", cmdline);
printf("Log file : (exe dir)\\udptcp_win.log\n");

/* start the service immediately after install */
if (StartServiceA(svc, 0, NULL))
printf("Service started.\n");
else
fprintf(stderr, "StartService failed: %lu\n", GetLastError());

CloseServiceHandle(svc);
}
CloseServiceHandle(scm);
}

static void svc_uninstall(void) {
SC_HANDLE scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
if (!scm) {
fprintf(stderr, "OpenSCManager failed: %lu (run as Administrator)\n", GetLastError());
return;
}
SC_HANDLE svc = OpenServiceA(scm, SERVICE_NAME,
SERVICE_STOP | DELETE | SERVICE_QUERY_STATUS);
if (!svc) {
fprintf(stderr, "Service '%s' not found: %lu\n", SERVICE_NAME, GetLastError());
CloseServiceHandle(scm); return;
}

/* stop the service and wait for it to reach STOPPED */
SERVICE_STATUS st;
if (ControlService(svc, SERVICE_CONTROL_STOP, &st)) {
for (int i = 0; i < 30; i++) {
Sleep(500);
if (!QueryServiceStatus(svc, &st)) break;
if (st.dwCurrentState == SERVICE_STOPPED) break;
}
}

if (DeleteService(svc))
printf("Service '%s' deleted.\n", SERVICE_NAME);
else
fprintf(stderr, "DeleteService failed: %lu\n", GetLastError());

CloseServiceHandle(svc);
CloseServiceHandle(scm);

/* delete the log file */
char logpath[MAX_PATH];
GetModuleFileNameA(NULL, logpath, MAX_PATH);
char *dot = strrchr(logpath, '.');
if (dot) strcpy(dot, ".log"); else strcat(logpath, ".log");
if (DeleteFileA(logpath))
printf("Log file deleted: %s\n", logpath);

printf("Uninstall complete.\n");
}

/* ================================================================
* main
* ================================================================ */
static void usage(const char *p) {
printf(
"Usage:\n"
" %s --u2t --listen <udp-port> --host <h> --port <p> [--verbose]\n"
" %s --t2u --listen <tcp-port> --host <h> --port <p> [--verbose]\n"
" %s --install --u2t --listen <udp-port> --host <h> --port <p>\n"
" %s --uninstall\n"
"\n"
"Options:\n"
" --u2t UDP listen -> TCP forward\n"
" --t2u TCP listen -> UDP forward\n"
" --listen local port to bind\n"
" --host remote host — IP or domain name (DNS retry on failure)\n"
" --port remote port\n"
" --verbose hex+ascii dump of every payload\n"
" --install install as Windows service (requires Administrator)\n"
" --uninstall remove Windows service (requires Administrator)\n"
"\n"
"Service notes:\n"
" Parameters are stored in the registry ImagePath — no separate config needed.\n"
" Log: <exe directory>\\udptcp_win.log\n"
" sc start %s / sc stop %s\n"
"\n"
"Framing: [uint16_be length][payload] preserves UDP datagram boundaries\n",
p, p, p, p, SERVICE_NAME, SERVICE_NAME);
exit(0);
}

int main(int argc, char *argv[]) {
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);

/* handle --install / --uninstall before WSAStartup */
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--install") || !strcmp(argv[i], "--uninstall")) {
if (!is_admin()) {
printf("Administrator required — requesting UAC elevation...\n");
relaunch_as_admin();
return 0;
}
}
if (!strcmp(argv[i], "--install")) {
svc_install(argc, argv);
printf("\nPress any key to exit...\n"); getchar();
return 0;
}
if (!strcmp(argv[i], "--uninstall")) {
svc_uninstall();
printf("\nPress any key to exit...\n"); getchar();
return 0;
}
if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) usage(argv[0]);
}

int mode_u2t = 0, mode_t2u = 0;
const char *listen_port = NULL, *fwd_host = NULL, *fwd_port = NULL;

for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--u2t")) mode_u2t = 1;
else if (!strcmp(argv[i], "--t2u")) mode_t2u = 1;
else if (!strcmp(argv[i], "--verbose")) g_verbose = 1;
else if (!strcmp(argv[i], "--listen") && i+1 < argc) listen_port = argv[++i];
else if (!strcmp(argv[i], "--host") && i+1 < argc) fwd_host = argv[++i];
else if (!strcmp(argv[i], "--port") && i+1 < argc) fwd_port = argv[++i];
else { fprintf(stderr, "unknown arg: %s\n", argv[i]); return 1; }
}

if ((mode_u2t + mode_t2u) != 1) {
fputs("specify exactly one of --u2t / --t2u\n", stderr); return 1;
}
if (!listen_port || !fwd_host || !fwd_port) {
fputs("--listen, --host, --port required\n", stderr); return 1;
}

/* save for svc_main (called by SCM dispatcher in a separate thread) */
g_mode_u2t = mode_u2t;
g_listen_port = listen_port;
g_fwd_host = fwd_host;
g_fwd_port = fwd_port;

/*
* Try to connect to SCM as a service.
* If started from a console, ERROR_FAILED_SERVICE_CONTROLLER_CONNECT is
* returned immediately and we fall through to run as a normal process.
*/
SERVICE_TABLE_ENTRYA svc_table[] = {
{ (LPSTR)SERVICE_NAME, svc_main },
{ NULL, NULL }
};
if (!StartServiceCtrlDispatcherA(svc_table)) {
if (GetLastError() != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
fprintf(stderr, "StartServiceCtrlDispatcher: %lu\n", GetLastError());
return 1;
}
/* console mode */
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "WSAStartup failed\n"); return 1;
}
if (mode_u2t) run_u2t(listen_port, fwd_host, fwd_port);
else run_t2u(listen_port, fwd_host, fwd_port);
WSACleanup();
}
return 0;
}