背景
比如我需要对某些网络流量数据包转发或者修改,我们可以通过TUN创建虚拟网络设置操作。我这边就简单实现通过TUN网络接口去访问百度网站。
效果
思路
下面是大致的流程,从TUN网络接口拿到tcp数据包,通过应用解析出SYN数据,创建SOCKET连接,封装SYN+ACK的TCP包写入TUN网络接口中,就会收到TUN网络接口ACK完成TCP三次握手。之后TUN网络接口发送PUSH数据包,将PUSH数据包中的数据解析出来写入SOCKET中并且回应ACK,当读到SOCKET数据将数据封装PUSH数据包写回TUN网络接口,完成通信。最后当收到TUN网络接口传来的FIN,封装ACK包写入SOCKET中,再封装FIN数据包写入SOCKET中,TUN网络接口就会发送ACK表示收到关闭信息,关闭SOCKET连接。
TUN 网络接口主要由 IP 地址和子网掩码组成。当配置好 TUN 接口后,符合该 IP 和子网掩码的流量将通过此接口传输。如果希望将百度的流量通过这个 TUN 接口,需要先解析出百度的所有 IP 地址,然后设置 TUN 接口的 IP 和子网掩码。
bash
# 查看所有解析的ip
root@36cd6fe9250c:~# nslookup www.baidu.com
Server: 192.168.65.7
Address: 192.168.65.7#53
Non-authoritative answer:
www.baidu.com canonical name = www.a.shifen.com.
Name: www.a.shifen.com
Address: 183.2.172.185
Name: www.a.shifen.com
Address: 183.2.172.42
root@36cd6fe9250c:~#
可以看到解析出来183.2.172.42
和183.2.172.185
,TUN网络接口配置ip:183.2.172.1,子网掩码:255.255.255.0,那两个IP就会走这个TUN网络接口。
SOCKET还得选择其他的网络接口,不设置的话,用上面的IP创建SOCKET还会走TUN网络接口,这就会导致死循环。
Java的SOCKET还不能选择网络接口,还得配合DNS来解决上面问题,还是C方便些。
技术
- 创建TUN(创建网络接口,用来接收数据)
- 创建SOCKET连接
- 使用EPOLL(管理多个SOCKET连接)
- 线程安全问题(上互斥锁解决)
遇到的问题
-
如何向 EPOLL 添加新的 SOCKET 监听,以确保能够及时响应?需要注意的是,EPOLL 的超时设置为 3 秒,这是否会导致在添加新 SOCKET 后需要等待 3 秒才能进行监听?(创建一个管道让EPOLL监听,有新的SOCKET连接就写入数据到管道,就可以唤醒了EPOLL)
-
怎么知道TCP校验值正常,如果TCP校验值错误,TUN网络接口那边不认可该数据包,等于没接收到。(通过Java那边开源包重新计算比较)
-
RAW SOCKET 是可以发IP+TCP协议包都不用自己转换了,当发送SYN,服务的那边回SYN+ACK,内核就发送RST,认为未知连接(不能解决,还是老实的自己解析协议包)。
-
控制台打印日志乱码,需要将.c文件的编码GB2312改成UTF-8-BOM。
环境
- Visual Studio 2022 (Windows11)
- Ubuntu 16.04 (Docker)
Ubuntu 16.04
Docker部署
运行容器
-d
选项让容器以后台模式运行。-p 22:22
映射到容器的 SSH 的22 端口-p 9222:9222
映射到容器的 谷歌浏览器远程调试 的9222 端口--cap-add=NET_ADMIN
和--device=/dev/net/tun
赋予容器管理网络设备的能力。
cmd
docker run -d --name ubuntu -p 22:22 -p 9222:9222 --cap-add=NET_ADMIN --device=/dev/net/tun ubuntu:16.04 /sbin/init
bash
**进入容器**
```cmd
docker exec -it ubuntu bash
Ubuntu容器里执行(允许root用户SSH连接)
bash
# 更新源
apt update
# 安装SSH服务和netstat,vim
apt install -y openssh-server net-tools vim curl
# 添加SSH目录来存放临时文件
mkdir -p /var/run/sshd
# 启动服务
/usr/sbin/sshd
# 查看22端口是否启动
netstat -plnt
# root用户设置密码
passwd root
# 允许root登录,修改sshd_config下的PermitRootLogin yes
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# 需要重启服务:kill掉任务,再启动sshd
kill -9 `ps -ef | grep '/usr/sbin/sshd' | grep -v grep | awk '{print $2}'`
/usr/sbin/sshd
推荐使用Xshell连接SSH,主要免费。
Ubuntu配置Visual Studio连接环境
bash
apt install -y openssh-server build-essential gdb rsync make zip
Visual Studio 2022
安装 Linux 开发环境
创建一个linux的控制台项目
添加Linux连接
工具->选项->跨平台->连接管理器
简单编写C代码,将main.cpp变成main.c(主要不会C++)
代码
c
#include<stdio.h>
int main()
{
printf("%s 向你问好!\n", "TunDemo");
return 0;
}
运行控制台显示
引用 #include <pthread.h>
,需要添加链接器,下面查询加载的pthread库
bash
# 查看加载的pthread
ldconfig -p | grep pthread
属性->链接器->输入->附加依赖项,我的目录是在Ubuntu的/lib/x86_64-linux-gnu/libpthread.so.0
部分代码
创建TUN
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <errno.h>
int tun_open(const char* dev) {
struct ifreq ifr;
int fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
perror("Opening /dev/net/tun");
return -1;
}
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // TUN device without packet info
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
if (ioctl(fd, TUNSETIFF, (void*)&ifr) < 0) {
perror("TUNSETIFF");
close(fd);
return -1;
}
// 激活接口
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
close(fd);
return -1;
}
if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) < 0) {
perror("Getting interface flags failed");
close(sockfd);
close(fd);
return -1;
}
ifr.ifr_flags |= IFF_UP; // 设置接口为 UP
if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0) {
perror("Setting interface flags failed");
close(sockfd);
close(fd);
return -1;
}
close(sockfd);
return fd;
}
int set_ip_and_mask(const char* dev, const char* ip, const char* mask) {
int sockfd;
struct ifreq ifr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 设置 IP 地址
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
struct sockaddr_in* addr = (struct sockaddr_in*)&ifr.ifr_addr;
addr->sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr->sin_addr);
if (ioctl(sockfd, SIOCSIFADDR, &ifr) < 0) {
perror("Setting IP address failed");
close(sockfd);
return -1;
}
// 设置子网掩码
inet_pton(AF_INET, mask, &addr->sin_addr);
if (ioctl(sockfd, SIOCSIFNETMASK, &ifr) < 0) {
perror("Setting subnet mask failed");
close(sockfd);
return -1;
}
close(sockfd);
return 0;
}
int main() {
char buffer[2048];
const char* dev = "tun0";
const char* ip = "183.2.172.1"; // 设置所需的 IP 地址
//const char* ip = "192.168.1.1"; // 设置所需的 IP 地址
const char* mask = "255.255.255.0"; // 设置所需的子网掩码
int fd = tun_open(dev);
if (fd < 0) return;
// 设置 IP 地址和子网掩码
if (set_ip_and_mask(dev, ip, mask) < 0) {
close(fd);
return;
}
printf("创建网络接口成功,名称:%s,ip:%s,子网掩码:%s\n", dev, ip, mask);
while (1) {
ssize_t nread = read(fd, buffer, sizeof(buffer));
if (nread < 0) {
printf("读取网络接口[%s]失败:%s\n", dev, strerror(errno));
break;
}
if (nread < sizeof(struct iphdr)) {
printf("数据包长度不足,无法满足IP数据包的要求,过滤\n");
continue;
}
printf("读取到数据的长度:%d\n", nread);
}
close(fd);
return 0;
}
本来想粘贴EPOLL那里的代码,代码太长感兴趣的可以看看完整代码里面的。
测试
curl命令简单测试
通过curl访问请求100次取平均值
bash
( for i in {1..100}; do curl -o /dev/null -s -w "%{time_total}\n" https://www.baidu.com; done ) | awk '{ total += $1; count++ } END { print "Average Time:", total/count, "seconds" }'
序号 | 正常 | tun |
---|---|---|
1 | 0.08277 | 0.08584 |
2 | 0.08238 | 0.08279 |
3 | 0.08385 | 0.08398 |
4 | 0.08283 | 0.08584 |
5 | 0.08396 | 0.08347 |
平均 | 0.08324 | 0.08484 |
简单测试看平均相差1.6毫秒感觉不错。
谷歌浏览器远程调试
在Ubuntu下执行
bash
# 安装Chromium 浏览器
apt install chromium-browser
# 安装字体(远程调试页面字体没有识别出来)
apt install -y fonts-noto fonts-liberation
# 安装dbus
apt install dbus
# 启动dbus
service dbus start
# 启动远程调试
chromium-browser --headless --disable-gpu --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --no-sandbox --disable-dev-shm-usage --user-data-dir=/tmp/chrome-user-data
打开谷歌浏览器调试
谷歌浏览器输入地址 chrome://inspect/#devices
点击Configure
添加远程的9222
添加打开百度网站
点击inspect
访问,可以使用F5强制刷新,不走缓存,看加载时间(毫秒),加载时间是从请求发出到页面的所有内容(包括图像、样式、脚本等)完全加载的时间。我多刷新几次发现他复用连接,本来还想统计时间。
完整代码
下面代码只是实现了IPV4的TCP协议,感兴趣可以看一下。