目录


功能

跨网段透明端口转发。典型场景:

场景一:代理跨网段共享
shadowsocks 的 HTTP/SOCKS 代理运行在内网 A,网段 B 的机器可以 ping 通 A 但无法访问代理端口(代理服务仅对本网段开放),在网段 B 部署 forward.c 监听本地端口并转发至 A 的代理,即可让网段 B 的所有机器通过同一入口使用代理。

场景二:RTSP 摄像头测试接入
摄像头 IP 固定、部署在隔离网段,调试时不想改摄像头 IP 也不想重新接线。将一台临时机同时接入隔离网和开发网,在临时机上运行 forward.c 将开发网端口转发至摄像头的 RTSP 端口(TCP 554),播放器直连临时机即可拉流,测试完毕关闭 forward.c 不留配置残留。

  • 单 C 文件,无外部依赖,gcc forward.c -o forward -lpthread 即可,兼容老系统
  • --tcp:TCP 监听 → TCP 转发,每条连接独立线程,双向管道,支持半关闭
  • --udp:UDP 监听 → UDP 转发,按源地址维护 session,回文过期保护
  • 每次新连接/session 重新 DNS 解析,目标 IP 变更自动生效
  • g_conn_total / g_conn_active 原子连接计数,日志实时可观测

编译与用法

1
gcc forward.c -o forward -lpthread
1
2
./forward --tcp --listen <port> --host <host> --port <port> [--verbose]
./forward --udp --listen <port> --host <host> --port <port> [--verbose]
参数 说明
--tcp TCP 监听 → TCP 转发
--udp UDP 监听 → UDP 转发(session per source address)
--listen 本地监听端口
--host 目标主机(IP 或域名,每次新连接重新解析)
--port 目标端口
--verbose hex+ascii 打印每包内容

TCP 模式设计

每条 TCP 连接 spawn 一个 handle 线程(detached):

1
2
3
4
5
handle()
→ DNS 解析 + connect 到目标(每次新连接重新解析)
→ spawn c2r 线程(detached):client → remote
→ 自身运行 r2c:remote → client
→ r2c 结束后 close(client) + close(remote)

pipe_data 内置写重试循环,任意一端 EOF 时 shutdown(dst, SHUT_WR) 正确传递半关闭语义。


UDP 模式设计

按 UDP 源地址维护 session,每 session 一个连接到目标的 UDP socket:

1
2
3
4
5
6
7
8
recvfrom(listen_fd) 收到包
→ udp_get_sess():查找或新建 session
新 session:DNS 解析 + connect UDP socket + spawn udp_backflow 线程
→ send(sess->fwd_fd, ...) 转发到目标

udp_backflow 线程:
→ recvfrom(fwd_fd) 等待目标回文
→ sendto(listen_fd, src_addr) 回发给原始客户端

回文过期(UDP Response Expiry)防护

UDP 是无连接协议,当目标还没准备好时,内核会将 ICMP 错误转成 recvfrom 的 errno。原始实现 if (n < 0) break 会导致 backflow 线程永久退出,之后目标就绪后的回文无人接收。

udp_backflow 对以下 errno 全部 continue 而非 break,确保线程存活等待目标就绪:

errno 触发原因 处理
ECONNREFUSED 目标端口暂未就绪(ICMP Port Unreachable) continue
EHOSTUNREACH 目标主机暂不可达(ICMP Host Unreachable) continue
ENETUNREACH 网络暂不可达(ICMP Net Unreachable) continue
EAGAIN / EWOULDBLOCK SO_RCVTIMEO=1s 超时,定期检查 alive 标志 continue
EINTR 信号中断 continue
其他 真正的致命错误 break

双重检查锁(Double-Checked Locking)

udp_get_sess 先无锁查找,找到直接返回;找不到才加锁新建,建好后再次在锁内确认无竞态插入:

1
2
3
无锁查找 → 找到 → 返回
→ 找不到 → 加锁 → 再次查找 → 找到(竞态)→ 解锁返回
→ 找不到 → 新建 session → 解锁返回

源码:forward.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
#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>
#include <stdatomic.h>
#include <ctype.h>

static const char *g_host;
static const char *g_port;
static atomic_long g_conn_total;
static atomic_long g_conn_active;
static int g_verbose = 0;

static void ts(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 len = strlen(buf);
snprintf(buf + len, n - len, ".%03ld", tp.tv_nsec / 1000000);
}

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


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


static void hexdump(const char *label, const unsigned char *data, ssize_t len) {
char _ts[40]; ts(_ts, sizeof(_ts));
printf("[%s] %s (%zd bytes)\n", _ts, label, len);
for (ssize_t i = 0; i < len; i += 16) {
printf(" %06zx ", 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);
}

struct pipe_args {
int src;
int dst;
char label[64];
long *bytes_out;
};

static void *pipe_data(void *arg) {
struct pipe_args *p = arg;
unsigned char buf[65536];
ssize_t n;
long bytes = 0;

LOG(" pipe start [%s]", p->label);

while ((n = read(p->src, buf, sizeof(buf))) > 0) {
if (g_verbose) hexdump(p->label, buf, n);
ssize_t sent = 0;
while (sent < n) {
ssize_t w = write(p->dst, buf + sent, n - sent);
if (w <= 0) goto done;
sent += w;
}
bytes += n;
}
done:
*p->bytes_out = bytes;
LOG(" pipe end [%s] %ld bytes", p->label, bytes);
shutdown(p->dst, SHUT_WR);
free(p);
return NULL;
}

static void *handle(void *arg) {
int client = *(int *)arg;
free(arg);

long id = atomic_fetch_add(&g_conn_total, 1) + 1;
long active = atomic_fetch_add(&g_conn_active, 1) + 1;

struct sockaddr_storage peer;
socklen_t plen = sizeof(peer);
char paddr[INET6_ADDRSTRLEN] = "?";
int pport = 0;
if (getpeername(client, (struct sockaddr *)&peer, &plen) == 0) {
if (peer.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&peer;
inet_ntop(AF_INET, &s->sin_addr, paddr, sizeof(paddr));
pport = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&peer;
inet_ntop(AF_INET6, &s->sin6_addr, paddr, sizeof(paddr));
pport = ntohs(s->sin6_port);
}
}

LOG("[#%ld] connect %s:%d (active=%ld)", id, paddr, pport, active);

struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

int rc = getaddrinfo(g_host, g_port, &hints, &res);
if (rc != 0) {
ERR("[#%ld] getaddrinfo %s:%s: %s", id, g_host, g_port, gai_strerror(rc));
close(client);
atomic_fetch_sub(&g_conn_active, 1);
return NULL;
}

int remote = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (remote < 0) {
ERR("[#%ld] socket: %s", id, strerror(errno));
freeaddrinfo(res); close(client);
atomic_fetch_sub(&g_conn_active, 1);
return NULL;
}
if (connect(remote, res->ai_addr, res->ai_addrlen) < 0) {
ERR("[#%ld] connect %s:%s: %s", id, g_host, g_port, strerror(errno));
freeaddrinfo(res); close(remote); close(client);
atomic_fetch_sub(&g_conn_active, 1);
return NULL;
}
freeaddrinfo(res);
LOG("[#%ld] tunnel %s:%d <-> %s:%s", id, paddr, pport, g_host, g_port);

long bytes_c2r = 0, bytes_r2c = 0;

struct pipe_args *c2r = malloc(sizeof(*c2r));
struct pipe_args *r2c = malloc(sizeof(*r2c));
c2r->src = client; c2r->dst = remote;
r2c->src = remote; r2c->dst = client;
snprintf(c2r->label, sizeof(c2r->label), "#%ld cli->remote", id);
snprintf(r2c->label, sizeof(r2c->label), "#%ld remote->cli", id);
c2r->bytes_out = &bytes_c2r;
r2c->bytes_out = &bytes_r2c;

pthread_t t;
pthread_create(&t, NULL, pipe_data, c2r);
pthread_detach(t);
pipe_data(r2c);

close(remote);
close(client);

long still_active = atomic_fetch_sub(&g_conn_active, 1) - 1;
LOG("[#%ld] closed %s:%d sent=%ld recv=%ld (active=%ld)",
id, paddr, pport, bytes_c2r, bytes_r2c, still_active);

return NULL;
}

/* ================================================================
* TCP: listen/accept loop
* ================================================================ */

static void run_tcp(const char *lport) {
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("[*] listening TCP 0.0.0.0:%s -> %s:%s%s",
lport, g_host, g_port, g_verbose ? " [verbose]" : "");

for (;;) {
int *client = malloc(sizeof(int));
*client = accept(srv, NULL, NULL);
if (*client < 0) {
free(client);
ERR("accept: %s", strerror(errno));
continue;
}
pthread_t t;
pthread_create(&t, NULL, handle, client);
pthread_detach(t);
}
}

/* ================================================================
* UDP: session per source address, transparent forwarding
* ================================================================ */

struct udp_sess {
struct sockaddr_storage src;
socklen_t srclen;
int fwd_fd; /* connected UDP socket to target */
int listen_fd; /* shared listen socket */
atomic_int alive;
struct udp_sess *next;
};

static struct udp_sess *g_udp_head = NULL;
static pthread_mutex_t g_udp_lock = PTHREAD_MUTEX_INITIALIZER;

static void *udp_backflow(void *arg) {
struct udp_sess *s = arg;

struct timeval tv = {1, 0};
setsockopt(s->fwd_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

unsigned char buf[65536];
while (atomic_load(&s->alive)) {
ssize_t n = recvfrom(s->fwd_fd, buf, sizeof(buf), 0, NULL, NULL);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
if (errno == ECONNREFUSED || errno == EHOSTUNREACH || errno == ENETUNREACH) continue;
break;
}
if (g_verbose) hexdump("UDP<-fwd", buf, n);
sendto(s->listen_fd, buf, n, 0,
(struct sockaddr *)&s->src, s->srclen);
}

atomic_store(&s->alive, 0);

pthread_mutex_lock(&g_udp_lock);
struct udp_sess **pp = &g_udp_head;
while (*pp && *pp != s) pp = &(*pp)->next;
if (*pp) *pp = s->next;
pthread_mutex_unlock(&g_udp_lock);

long still_active = atomic_fetch_sub(&g_conn_active, 1) - 1;
LOG(" UDP session closed (active=%ld)", still_active);

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

static struct udp_sess *udp_get_sess(int listen_fd,
struct sockaddr_storage *src, socklen_t srclen)
{

pthread_mutex_lock(&g_udp_lock);
struct udp_sess *s;
for (s = g_udp_head; s; s = s->next)
if (s->srclen == srclen && memcmp(&s->src, src, srclen) == 0
&& atomic_load(&s->alive)) {
pthread_mutex_unlock(&g_udp_lock);
return s;
}
pthread_mutex_unlock(&g_udp_lock);

/* DNS re-resolve on every new session so IP changes take effect */
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
int rc = getaddrinfo(g_host, g_port, &hints, &res);
if (rc != 0) {
ERR("getaddrinfo %s:%s: %s", g_host, g_port, gai_strerror(rc));
return NULL;
}
int fwd_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fwd_fd < 0 || connect(fwd_fd, res->ai_addr, res->ai_addrlen) < 0) {
ERR("udp connect %s:%s: %s", g_host, g_port, strerror(errno));
if (fwd_fd >= 0) close(fwd_fd);
freeaddrinfo(res); return NULL;
}
freeaddrinfo(res);

s = calloc(1, sizeof(*s));
memcpy(&s->src, src, srclen);
s->srclen = srclen;
s->fwd_fd = fwd_fd;
s->listen_fd = listen_fd;
atomic_store(&s->alive, 1);

/* double-checked insert */
pthread_mutex_lock(&g_udp_lock);
struct udp_sess *existing;
for (existing = g_udp_head; existing; existing = existing->next)
if (existing->srclen == srclen && memcmp(&existing->src, src, srclen) == 0
&& atomic_load(&existing->alive)) {
pthread_mutex_unlock(&g_udp_lock);
close(fwd_fd); free(s);
return existing;
}
s->next = g_udp_head;
g_udp_head = s;
pthread_mutex_unlock(&g_udp_lock);

char ip[INET6_ADDRSTRLEN]; int port = 0;
if (src->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in *)src)->sin_addr, ip, sizeof(ip));
port = ntohs(((struct sockaddr_in *)src)->sin_port);
} else {
inet_ntop(AF_INET6, &((struct sockaddr_in6 *)src)->sin6_addr, ip, sizeof(ip));
port = ntohs(((struct sockaddr_in6 *)src)->sin6_port);
}
long id = atomic_fetch_add(&g_conn_total, 1) + 1;
atomic_fetch_add(&g_conn_active, 1);
LOG("[#%ld] UDP session %s:%d -> %s:%s", id, ip, port, g_host, g_port);

pthread_t t;
pthread_create(&t, NULL, udp_backflow, s);
pthread_detach(t);
return s;
}

static void run_udp(const char *lport) {
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 fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { perror("udp socket"); return; }
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) { perror("udp bind"); return; }
freeaddrinfo(res);

LOG("[*] listening UDP 0.0.0.0:%s -> %s:%s%s",
lport, g_host, g_port, g_verbose ? " [verbose]" : "");

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

struct udp_sess *sess = udp_get_sess(fd, &src, srclen);
if (!sess) continue;

if (g_verbose) hexdump("UDP->fwd", buf, n);
send(sess->fwd_fd, buf, n, 0);
}
}

static void usage(const char *prog) {
fprintf(stderr,
"Usage:\n"
" %s --tcp --listen <port> --host <host> --port <port> [--verbose]\n"
" %s --udp --listen <port> --host <host> --port <port> [--verbose]\n"
" --tcp TCP listen -> TCP forward\n"
" --udp UDP listen -> UDP forward (session per source address)\n"
" --verbose hex+ascii dump of every payload\n",
prog, prog);
exit(1);
}

int main(int argc, char *argv[]) {
const char *listen_addr = NULL;
int mode_tcp = 0, mode_udp = 0;
g_host = NULL;
g_port = NULL;

for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--tcp")) mode_tcp = 1;
else if (!strcmp(argv[i], "--udp")) mode_udp = 1;
else if (!strcmp(argv[i], "--listen") && i+1 < argc) listen_addr = argv[++i];
else if (!strcmp(argv[i], "--host") && i+1 < argc) g_host = argv[++i];
else if (!strcmp(argv[i], "--port") && i+1 < argc) g_port = argv[++i];
else if (!strcmp(argv[i], "--verbose")) g_verbose = 1;
else usage(argv[0]);
}
if ((mode_tcp + mode_udp) != 1 || !listen_addr || !g_host || !g_port)
usage(argv[0]);

signal(SIGPIPE, SIG_IGN);

if (mode_tcp) run_tcp(listen_addr);
else run_udp(listen_addr);
return 0;
}