通过 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)

相关推荐
jimy12 小时前
安卓里运行Linux
linux·运维·服务器
爱凤的小光3 小时前
Linux清理磁盘技巧---个人笔记
linux·运维
耗同学一米八4 小时前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
知星小度S5 小时前
系统核心解析:深入文件系统底层机制——Ext系列探秘:从磁盘结构到挂载链接的全链路解析
linux
2401_890443025 小时前
Linux 基础IO
linux·c语言
智慧地球(AI·Earth)6 小时前
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南
linux·ssh·ai编程
老王熬夜敲代码7 小时前
解决IP不够用的问题
linux·网络·笔记
zly35007 小时前
linux查看正在运行的nginx的当前工作目录(webroot)
linux·运维·nginx
QT 小鲜肉7 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
问道飞鱼8 小时前
【Linux知识】Linux 虚拟机磁盘扩缩容操作指南(按文件系统分类)
linux·运维·服务器·磁盘扩缩容