通过 C 代码在 Ubuntu 中创建 TUN,访问百度网站

背景

比如我需要对某些网络流量数据包转发或者修改,我们可以通过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连接。

sequenceDiagram participant A as TUN participant B as 客户端 participant C as SOCKET %% TCP 三次握手 note over A,B: TCP 三次握手 A->>B: SYN B->>C: 创建连接 C-->>B: 连接成功 B-->>A: SYN + ACK A->>B: ACK %% 建立连接后通信 note over A,B: 建立连接后通信 A->>B: PSH B->>C: 解析写入 B-->>A: ACK C-->>B: 封装写回 B-->>A: PSH A->>B: ACK %% TCP 四次挥手 note over A,B: TCP 四次挥手 A->>B: FIN B-->>A: ACK B->>A: FIN C-->>B: 关闭连接 A-->>B: ACK note right of A: 连接关闭

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.42183.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协议,感兴趣可以看一下。

tun_demo: linux下使用tun完成百度访问 (gitee.com)

相关推荐
二进制杯莫停2 分钟前
掌控网络流量的利器:tcconfig
linux
watl018 分钟前
【Android】unzip aar删除冲突classes再zip
android·linux·运维
赵大仁1 小时前
在 CentOS 7 上安装 Node.js 20 并升级 GCC、make 和 glibc
linux·运维·服务器·ide·ubuntu·centos·计算机基础
vvw&1 小时前
Docker Build 命令详解:在 Ubuntu 上构建 Docker 镜像教程
linux·运维·服务器·ubuntu·docker·容器·开源
冷曦_sole1 小时前
linux-21 目录管理(一)mkdir命令,创建空目录
linux·运维·服务器
最后一个bug1 小时前
STM32MP1linux根文件系统目录作用
linux·c语言·arm开发·单片机·嵌入式硬件
dessler2 小时前
Docker-Dockerfile讲解(二)
linux·运维·docker
卫生纸不够用2 小时前
子Shell及Shell嵌套模式
linux·bash
world=hello2 小时前
关于科研中使用linux服务器的集锦
linux·服务器
soragui3 小时前
【ChatGPT】OpenAI 如何使用流模式进行回答
linux·运维·游戏