怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

140 篇文章 1 订阅
83 篇文章 0 订阅

文中部分内容,因为没有找到特别权威的资料,因此掺杂着不少个人的理解,如有错误,欢迎指出。

背景

由于个人的一些特殊需要,想要对自己mbp的流量进行内部分发,简单点描述就是部分直连、部分走公司VPN、部分走socks5代理。

调研了一下市面上的一些解决方案:

  1. PAC。配置较为简单方便,但是对于很多应用是无法走PAC的,尤其是终端应用
  2. mellow。不知道为什么,规则一旦配多了就会出现部分规则不生效的问题(没找到相应issue,可能是我哪里配置的有问题)
  3. surge。配置很方便,但是有三个问题,第一个问题是贵,第二个问题是dns解析显示的是surge内部的dns地址(这个理由倒是不打紧),第三个问题也是最重要的问题是,对其他VPN同时使用的情况支持不那么友好
  4. proxifier。不知道为什么,我的电脑用起来有时会卡卡的,有时还会出现网络混乱的情况

于是就想要自己实现一个网络流量分发的工具。

虚拟网卡TUN/TAP

关于网络流量分发这个问题,在网上看到了很多解法,我个人比较感兴趣的是使用TUN/TAP虚拟网卡来实现。由于能力有限,还处于学习过程中,因此本期就只实现了ping请求的转发。

简单描述一下,TUN/TAP是通过软件模拟的网络设备,提供与网络设备完全相同的功能。其中TAP模拟了以太网设备,即操作第二层数据包。TUN则模拟了网络层设备,即第三层数据包。

那么接下来我们就来看看我们如何来操作虚拟网卡,因为IP包简单,于是个人就选择了使用TUN设备。本文全部基于mac操作系统,以TUN设备为例,并使用python语言来编码。

对于macos而言,操作tun设备还是比较简单的,不过首先需要先安装TUN/TAP,

brew cask install tuntap

安装完后,就可以看到我们的/dev目录下多了如下一些文件: tap0 --- tap15、tun0 --- tun15

在linux中,是有所不同的,但是网上关于linux的TUN/TAP的资料还是比较丰富的,因此此处就不详解了。

接下来,就是如何操作tun设备,对于mac而言,整个操作十分简单,就和直接操作文件没什么两样,代码如下:

import os
import subprocess
from scapy.layers.inet import *

tun_fd = os.open('/dev/tun11', os.O_RDWR)
subprocess.check_call('ifconfig tun11 192.168.7.1 192.168.7.2 up', shell=True)

# 添加路由规则,用于测试
subprocess.check_call('route -n add -net 180.101.49.0 -netmask 255.255.255.0 192.168.7.2', shell=True)

while True:
    packet = os.read(tun_fd, 2048)
    ip = IP(packet)       # 此处使用了scapy包来处理网络协议
    ip.show()

运行程序(需要使用sudo来运行)。我们可以先使用ifconfig来看看我们的网络设备状态,可以看到我们的网络设备中多了一个tun11的设备,但是这个设备有两个IP,我们可能会比较好奇,这个放到下文解释,

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

同样的,我们再来看一下我们的路由规则,可以发现多了两条tun设备路由规则,其中192.168.7.2 -> 192.168.7.1是在创建虚拟网卡的时候就自动创建的,另一条则是我们在代码里手动添加的。

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

接下来,我们来ping一下180.101.49.10这个地址,看看返回结果,

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

从这些信息中我们可以看到这是一个ICMP包,比较有意思的一点,我们可以发现,该包的来源IP对应了TUN设备的源地址。

使用TUN设备模拟ping包的响应

接下来,我们尝试着去响应这个ICMP包,我们只需对调一下数据包中的src_ip和dst_ip,然后将ICMP的type设为echo-reply,当然了最终还是需要重新计算一下校验和。代码如下:

import os
import subprocess
from scapy.layers.inet import *

tun_fd = os.open('/dev/tun11', os.O_RDWR)
subprocess.check_call('ifconfig tun11 192.168.7.1 192.168.7.2 up', shell=True)

# 添加路由规则,用于测试
subprocess.check_call('route -n add -net 180.101.49.0 -netmask 255.255.255.0 192.168.7.2', shell=True)

while True:
    packet = os.read(tun_fd, 2048)
    ip = IP(packet)       # 此处使用了scapy包来处理网络协议

    reply_icmp = ICMP(ip.payload)
    reply_icmp.setfieldval('type', 0)     # 0代表echo-reply
    reply_icmp.setfieldval('chksum', None)     # 设为None,scapy包会自动计算

    reply_ip = IP(packet)
    reply_ip.setfieldval('src', ip.dst)
    reply_ip.setfieldval('dst', ip.src)
    reply_ip.setfieldval('len', None)
    reply_ip.setfieldval('chksum', None)
    reply_ip.setfieldval('payload', reply_icmp)

    os.write(tun_fd, bytes(reply_ip))    # 此处就直接往tun_fd直接回写数据即可

运行结果如下:

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

在这里,我们可以看到如果想返回数据就直接往tun_fd中回写数据即可,在这里我们其实就可以猜出TUN网卡的两个IP的作用,当应用程序流量流经TUN设备的时候,该应用发送的包的来源IP就对应了TUN设备的源IP,而TUN设备的目的IP就好比是这张虚拟网卡的IP。

使用TUN设备进行ping请求的转发

到上面为止,我们介绍了TUN设备的基本操作,以及基于TUN设备模拟ping请求的响应。但是接下来我们进一步增加难度,我们现在需要对ping请求进行转发,在这里就涉及到一个问题,就是请求无限循环的问题,因为配置了180.101.49.11会路由到TUN设备,而我们进行请求转发就一定需要将这个请求发出去,但是发出去的时候又会重新路由到TUN设备,就会形成死循环。这个问题怎么解呢?

在linux中,我们可以通过iptable的方式来解决,但是mac下相应的资料却不多。经过一番研究,我发现我们发送请求可以指定网卡,这样就可以不用经过路由了。(这答案真是太暴露智商了,不过这个问题的确让我困惑了很久)

那么接下来的操作就比较简单了,不过需要注意的是,我们不能将ping包原封不动地转发出去,因为这样的话ping包就会收到两份响应,会出现DUP的标记。其中一份来自外部真实响应,第二份来自我们代理返回的响应。

import select
import uuid
from scapy.all import *
from scapy.layers.inet import *

DEFAULT_NET = 'en0'
BUFFER_SIZE = 2048

# 主程序
tun_fd = os.open('/dev/tun11', os.O_RDWR)
subprocess.check_call('ifconfig tun11 192.168.7.1 192.168.7.2 up', shell=True)
subprocess.check_call('route -n add -net 180.101.49.0 -netmask 255.255.255.0 192.168.7.2', shell=True)

icmp_info_map = {}
icmp_socket_list = []

while True:
    read_fds = [tun_fd]
    read_fds.extend(icmp_socket_list)
    r_fds = select(read_fds, [], [], 10)[0]    # 由于kqueue不会写,此处就使用select,简单一点
    if r_fds:
        for fd in r_fds:
            if fd == tun_fd:      # 如果是来自虚拟网卡的数据
                data = os.read(tun_fd, BUFFER_SIZE)
                ip = IP(data)
                src_ip = ip.src
                dst_ip = ip.dst
                if ip.payload.name == 'ICMP':
                    info = {
                        'src_ip': src_ip,
                        'dst_ip': dst_ip,
                        'data': ip
                    }
                    mark_key = str(uuid.uuid1())
                    icmp_info_map[mark_key] = info  # 这里对收到的ping与发送的ping做了一个映射
                    send_to_remote_ip = IP(dst=dst_ip) / ICMP() / mark_key
                    icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
                    icmp_socket.settimeout(5)
                    icmp_socket.bind(('192.168.199.111', 0))    # 我电脑的en0网卡的地址
                    icmp_socket.connect((dst_ip, 1))
                    icmp_socket_list.append(icmp_socket)
                    sendp(bytes(ICMP(send_to_remote_ip.payload)), socket=icmp_socket)
            elif fd in icmp_socket_list:     # 如果是来自外部响应数据
                data, addr = fd.recvfrom(BUFFER_SIZE)
                icmp_socket_list.remove(fd)
                fd.close()
                recv_ip = IP(data)
                recv_icmp = ICMP(recv_ip.payload)
                mark_key = str(bytes(recv_icmp.payload), 'utf-8')
                if mark_key not in icmp_info_map:
                    continue
                info = icmp_info_map[mark_key]
                icmp_info_map.pop(mark_key)
                send_icmp = ICMP(info['data'].payload)
                send_icmp.setfieldval('type', recv_icmp.type)
                send_icmp.setfieldval('chksum', None)
                send_ip = IP(dst=info['src_ip'], src=info['dst_ip']) / send_icmp
                os.write(tun_fd, bytes(send_ip))

这段代码比较简陋,仍然存在在一些问题,例如socket超时释放、mark_key是否合理等问题。但是基本上可以粗略地表达一个朴素地ping包转发的思路,运行结果如下:

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

当然了,当我们不使用TUN设备的时候,结果也是一样的,如下,

怎么使用TUN虚拟网卡实现ping请求转发?带你一文解决

 

到这里,我们一个简单的利用TUN设备转发ping包的功能就算完成了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用golang实现tun虚拟网卡读写的示例代码: ```go package main import ( "fmt" "net" "os" "golang.org/x/sys/unix" ) func main() { // 打开tun虚拟网卡设备 fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0) if err != nil { fmt.Printf("Error opening tun device: %v\n", err) return } // 设置tun虚拟网卡的名称和类型 ifr := &unix.Ifreq{} copy(ifr.IfrnName[:], []byte("mytun")) ifr.IfruFlags = unix.IFF_TUN | unix.IFF_NO_PI _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TUNSETIFF, uintptr(unsafe.Pointer(ifr))) if errno != 0 { fmt.Printf("Error setting tun device: %v\n", errno) return } // 获取tun虚拟网卡的IP地址和掩码 addr := &unix.Ifreq{} copy(addr.IfrnName[:], []byte("mytun")) _, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.SIOCGIFADDR, uintptr(unsafe.Pointer(addr))) if errno != 0 { fmt.Printf("Error getting tun device address: %v\n", errno) return } ip := net.IPv4(addr.IfruAddr[0], addr.IfruAddr[1], addr.IfruAddr[2], addr.IfruAddr[3]) mask := &unix.Ifreq{} copy(mask.IfrnName[:], []byte("mytun")) _, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.SIOCGIFNETMASK, uintptr(unsafe.Pointer(mask))) if errno != 0 { fmt.Printf("Error getting tun device netmask: %v\n", errno) return } netmask := net.IPv4(mask.IfruAddr[0], mask.IfruAddr[1], mask.IfruAddr[2], mask.IfruAddr[3]) fmt.Printf("Tun device IP is %s, netmask is %s\n", ip.String(), netmask.String()) // 读取tun虚拟网卡的数据包 buf := make([]byte, 1500) n, err := unix.Read(fd, buf) if err != nil { fmt.Printf("Error reading from tun device: %v\n", err) return } fmt.Printf("Received %d bytes from tun device: %v\n", n, buf[:n]) // 向tun虚拟网卡发送数据包 n, err = unix.Write(fd, []byte("hello world")) if err != nil { fmt.Printf("Error writing to tun device: %v\n", err) return } fmt.Printf("Sent %d bytes to tun device\n", n) } ``` 在以上示例代码中,我们首先使用`unix.Open()`函数打开tun虚拟网卡设备,然后使用`unix.Syscall()`函数调用`ioctl()`系统调用来设置tun虚拟网卡的名称和类型,以及获取tun虚拟网卡的IP地址和掩码。然后,我们使用`unix.Read()`函数从tun虚拟网卡读取数据包,并使用`unix.Write()`函数向tun虚拟网卡发送数据包。 需要注意的是,以上示例代码仅用于演示如何使用golang实现tun虚拟网卡读写,并不能直接运行。完整的实现应该考虑更多的细节和错误处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值