编写第一个TUN调试Demo:C语言示例

TUN调试 / 28人浏览

凌晨三点十七分,我的Telegram群聊炸了。

“老哥,你的节点又被封了?”“这次是TCP阻断还是TLS指纹?”“别慌,我换了个端口,但延迟飙到400ms……”

屏幕的光映在我脸上,我盯着路由器后台那串冰冷的日志——“Connection reset by peer”。这不是第一次了。在币圈混,你总得有点看家本领:有人玩合约,有人玩土狗,而我,玩的是网络隧道。当你的USDT从交易所提币到冷钱包,当你的节点跟海外服务器交换心跳包,你比谁都清楚:这年头,连流量都在被“审查”

我关掉群聊,打开了一个空白的.c文件。今晚,我要写一个TUN设备调试Demo。不是为了造轮子,是为了在下次节点被封时,我能亲手撕开那条虚拟的“网线”,看看数据包到底死在了哪一跳。


h2: 为什么是TUN?为什么是现在?

你可能用过Clash、Sing-box,或者更硬核的WireGuard。它们的核心底层,都绕不开一个东西——TUN/TAP虚拟网卡

想象一下:你的手机和服务器之间,本来隔着一堵墙。普通代理是“敲门”进去,而TUN设备是直接在墙上凿了个洞,然后铺了一条专属管道。所有流量,不管你是TCP、UDP还是ICMP,只要进了这个“洞”,就全被你的程序接管。

在币圈,这太重要了。交易所的API请求、链上节点的广播、甚至你那个跑在VPS上的交易机器人,都需要一个稳定、可控、能自己调试的网络通道。而TUN设备,就是你亲手打造这条通道的第一块砖

但问题是,网上关于TUN的资料,要么是晦涩的内核文档,要么是封装好的高级语言库。今天,我用C语言,从零开始,写一个能跑起来的调试Demo。不依赖任何第三方库,只用系统调用。


h3: 第一步:掀开TUN设备的“盖头”——open()ioctl()

在Linux上,TUN设备是一个文件。是的,你没听错,一切皆文件。我们通过open("/dev/net/tun", O_RDWR)打开这个“设备文件”,然后用ioctl()告诉内核:“我要创建一个TUN设备,名字叫tun0,类型是IFF_TUN(点对点三层设备)”。

这里有个关键点:IFF_NO_PI。这个标志位决定我们读到的数据是否包含额外的包头信息。对于调试Demo,我们加上它,直接拿到纯IP数据包,省去解析的麻烦。

c int tun_alloc(char *dev_name) { struct ifreq ifr; int fd = open("/dev/net/tun", O_RDWR); memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; strncpy(ifr.ifr_name, dev_name, IFNAMSIZ-1); if (ioctl(fd, TUNSETIFF, &ifr) < 0) { perror("ioctl(TUNSETIFF)"); close(fd); return -1; } // 内核会回写实际分配的名字,如果传入的是空字符串 strcpy(dev_name, ifr.ifr_name); return fd; }

注意:如果你传入的设备名是tun%d,内核会自动分配一个空闲的编号。在调试时,我习惯固定名字,比如tun666,这样方便用tcpdump抓包。


h3: 第二步:给TUN设备“通电”——配置IP与路由

光有设备文件还不够,它得像一张真正的网卡一样,有IP地址,有路由规则。否则,内核不知道把哪些流量扔进这个“洞”里。

这里要用到ioctl()的另一个变种:SIOCSIFADDR(设置IP)、SIOCSIFNETMASK(设置掩码)、SIOCSIFFLAGS(激活设备)。更优雅的方式是直接用system()调用ip命令,但为了纯C,我们手写一个函数。

c void configtun(char *dev) { int sock = socket(AFINET, SOCKDGRAM, 0); struct ifreq ifr; struct sockaddrin addr = (struct sockaddr_in)&ifr.ifr_addr;

strncpy(ifr.ifr_name, dev, IFNAMSIZ-1);  // 设置IP: 10.0.0.1 addr->sin_family = AF_INET; inet_pton(AF_INET, "10.0.0.1", &addr->sin_addr); ioctl(sock, SIOCSIFADDR, &ifr);  // 设置掩码: 255.255.255.0 addr->sin_family = AF_INET; inet_pton(AF_INET, "255.255.255.0", &addr->sin_addr); ioctl(sock, SIOCSIFNETMASK, &ifr);  // 激活设备 ioctl(sock, SIOCGIFFLAGS, &ifr); ifr.ifr_flags |= IFF_UP | IFF_RUNNING; ioctl(sock, SIOCSIFFLAGS, &ifr);  close(sock); 

}

关键一步:添加路由。你必须让系统知道,哪些目标IP的流量要走tun0。最简单的调试方式,是添加一条默认路由:ip route add default dev tun0。但在调试时,我建议只引流特定IP,比如只引流你的币安API服务器IP,避免把自己SSH断掉。


h3: 第三步:主循环——读取数据包,打印“灵魂”

现在,我们的TUN设备已经“通电”了。接下来是最激动人心的部分:tun_fdread()数据包。每读到一个包,我们就解析它的IP头,打印出源IP、目的IP、协议类型、长度。这就是你的“网络显微镜”。

c int main() { char tunname[IFNAMSIZ] = "tun666"; int tunfd = tunalloc(tunname); if (tun_fd < 0) return 1;

config_tun(tun_name); printf("TUN设备 %s 已就绪,开始抓取数据包...\n", tun_name); printf("提示:用 ping -I tun666 8.8.8.8 测试,或 curl --interface tun666 https://api.binance.com\n\n");  char buffer[1500]; // MTU 一般1500 while (1) {     ssize_t n = read(tun_fd, buffer, sizeof(buffer));     if (n < 0) { perror("read"); break; }      // 解析IP头 (IPv4)     struct iphdr *iph = (struct iphdr*)buffer;     if (iph->version == 4) {         char src[INET_ADDRSTRLEN], dst[INET_ADDRSTRLEN];         inet_ntop(AF_INET, &iph->saddr, src, sizeof(src));         inet_ntop(AF_INET, &iph->daddr, dst, sizeof(dst));          printf("[%ld bytes] %s -> %s | ", n, src, dst);         switch (iph->protocol) {             case 1: printf("ICMP"); break;             case 6: printf("TCP"); break;             case 17: printf("UDP"); break;             default: printf("Proto(%d)", iph->protocol);         }         printf(" | ID: %u\n", ntohs(iph->id));     } } close(tun_fd); return 0; 

}

注意:编译时需要链接-lresolv吗?不需要。但需要包含头文件<linux/if.h>, <linux/if_tun.h>, <netinet/ip.h>。编译命令:gcc -o tun_demo tun_demo.c


h3: 场景实战:当你的币安API请求“消失”了

假设你的交易机器人突然连不上币安了。你用curl --interface tun666 https://api.binance.com/api/v3/ping测试,然后看我们的Demo输出。

你会看到类似: [64 bytes] 10.0.0.1 -> 34.224.112.34 | TCP | ID: 45231 [54 bytes] 10.0.0.1 -> 34.224.112.34 | TCP | ID: 45232

如果什么输出都没有,说明流量根本没进TUN设备。检查路由:ip route show table all | grep tun666。大概率是你忘了加ip route add 34.224.112.34 dev tun666

如果只有握手包,没有后续数据,说明TUN设备只收到了SYN,但回包丢了。这是因为你的程序只read,从不write。真正的TUN设备是双向的,你需要把收到的包处理后,再write(fd, buffer, n)回去。但在调试阶段,我们只是“看”,不“动”。


h3: 进阶技巧:用select()实现超时与多路复用

真实场景中,你不可能只读一个TUN设备。你可能还要同时监听一个UDP socket(用于WireGuard协议),或者一个TCP socket(用于代理转发)。这时候,select()就派上用场了。

c fdset rfds; struct timeval tv; int maxfd = (tunfd > udpfd) ? tunfd : udp_fd;

while (1) { FDZERO(&rfds); FDSET(tunfd, &rfds); FDSET(udpfd, &rfds); tv.tvsec = 1; // 1秒超时 tv.tv_usec = 0;

int ret = select(maxfd + 1, &rfds, NULL, NULL, &tv); if (ret < 0) { perror("select"); break; } else if (ret == 0) {     printf("."); // 心跳超时,可以发个心跳包     fflush(stdout);     continue; }  if (FD_ISSET(tun_fd, &rfds)) {     // 处理TUN数据包 } if (FD_ISSET(udp_fd, &rfds)) {     // 处理UDP数据包(比如来自对端节点的加密流量) } 

}

这个模式,就是所有VPN、代理软件的核心骨架。你可以在TUN侧读取明文流量,加密后从UDP socket发出去;收到UDP数据后解密,再write回TUN设备。


h3: 调试陷阱:为什么我read到的全是乱码?

这是新手最常问的问题。如果你没加IFF_NO_PI标志,那么每次read返回的数据前面会多一个4字节的struct tun_pi头,包含协议族和标志位。如果你没解析它,直接当IP头用,自然是乱的。

解决方案:要么加IFF_NO_PI(推荐),要么在读取后跳过前4字节: c struct tun_pi *pi = (struct tun_pi*)buffer; char *ip_start = buffer + sizeof(struct tun_pi);

另一个坑:MTU。TUN设备的默认MTU是1500,但如果你的物理网卡MTU是1400(比如PPPoE拨号),那么超过1400字节的包会被静默丢弃。用ip link set dev tun666 mtu 1400调整。


h3: 让Demo更有币圈味道:监控你的“矿池心跳”

假设你有一个挖矿程序,连接着矿池的stratum+tcp://pool.com:3333。你想验证挖矿流量是否走了TUN隧道,并且观察数据包频率。

修改你的主循环,加一个计数器: c static int packet_count = 0; static time_t last_print = 0; ... // 在read之后 packet_count++; time_t now = time(NULL); if (now - last_print >= 5) { printf("过去5秒收到 %d 个数据包,流量 %ld 字节\n", packet_count, total_bytes); packet_count = 0; total_bytes = 0; last_print = now; }

然后运行: bash

sudo ./tun_demo

终端2: 让挖矿软件走TUN

sudo ip route add pool.com dev tun666 ./miner --url stratum+tcp://pool.com:3333 --user your_wallet.worker

你会看到Demo打印出规律性的TCP数据包——这就是你的矿机在跟矿池“握手”和提交算力。如果数据包突然停了,说明你的网络被掐了,或者矿池IP变了。


h3: 最后一块拼图:为什么用C而不是Go/Rust?

你可能问,现在写网络工具都用Go,性能好又安全。但C语言的好处是极致透明。没有GC停顿,没有运行时开销,你直接操作内核的缓冲区。对于调试底层网络问题,C是最锋利的手术刀

比如,你想看TCP包的seqack号,用C解析TCP头就是几次指针偏移的事。而在Go里,你还需要引入gopacket库,编译体积直接翻倍。

但更重要的是:当你用C亲手实现了从open()read()再到write()的完整链路,你对“网络”的理解会发生质变。下次你的节点被封,你不会再盲目换端口,而是能冷静地分析:是TLS握手被重置了?还是HTTP/2的SETTINGS帧被识别了? 这种掌控感,是任何高级语言都给不了你的。


窗外,天已经蒙蒙亮。我的TUN Demo还在跑着,屏幕上滚动着各种IP数据包。群里的兄弟问我:“搞定了吗?”我打了一行字:

“搞定了。我看到了我的流量长什么样。”

然后我关掉Demo,打开tcpdump -i tun666 -nn,开始抓包分析那个被墙的节点。这一次,我不再是那个只会重启路由器的韭菜了。

版权声明:

作者: 最新鸿蒙OS VPN免费节点分享

链接: https://harmonyosvpn.com/tun-debug/first-tun-debug-demo-c.htm

来源: harmonyosvpn.com

文章版权归作者所有,未经允许请勿转载。

最新文章

归档

标签