Pages

Tuesday, 27 October 2015

用 Python 操作虚拟网卡

在我的 XTunnel 项目中,已经用 Python 作过这种相对底层的工作了(这说明 Python 果然还是非常强大的,上下层通吃啊),不过那边目前还是只实现了 Linux 的版本。后来我又陆陆续续地把 Windows 以及 Mac 下的操作方法给搞通了,今天就来总结一下。
在 Linux 内核中,特别是在现在的发行版中,应该都已经有了 TUN/TAP 虚拟网卡的驱动程序,看一下有没有 /dev/net/tun 这个文件就可以知道了。如果没有,就执行一下 sudo modprobe tun 这个命令吧。如果还是没有,那就 Google 之吧。下面上代码:
import fcntl
import os
import struct
import subprocess
# Some constants used to ioctl the device file. I got them by a simple C
# program.
TUNSETIFF = 0x400454ca
TUNSETOWNER = TUNSETIFF + 2
IFF_TUN = 0x0001
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000
# Open TUN device file.
tun = open('/dev/net/tun', 'r+b')
# Tall it we want a TUN device named tun0.
ifr = struct.pack('16sH', 'tun0', IFF_TUN | IFF_NO_PI)
fcntl.ioctl(tun, TUNSETIFF, ifr)
# Optionally, we want it be accessed by the normal user.
fcntl.ioctl(tun, TUNSETOWNER, 1000)
# Bring it up and assign addresses.
subprocess.check_call('ifconfig tun0 192.168.7.1 pointopoint 192.168.7.2 up',
shell=True)
while True:
# Read an IP packet been sent to this TUN device.
packet = list(os.read(tun.fileno(), 2048))
# Modify it to an ICMP Echo Reply packet.
#
# Note that I have not checked content of the packet, but treat all packets
# been sent to our TUN device as an ICMP Echo Request.
# Swap source and destination address.
packet[12:16], packet[16:20] = packet[16:20], packet[12:16]
# Under Linux, the code below is not necessary to make the TUN device to
# work. I don't know why yet, but if you run tcpdump, you can see the
# difference.
if True:
# Change ICMP type code to Echo Reply (0).
packet[20] = chr(0)
# Clear original ICMP Checksum field.
packet[22:24] = chr(0), chr(0)
# Calculate new checksum.
checksum = 0
# for every 16-bit of the ICMP payload:
for i in range(20, len(packet), 2):
half_word = (ord(packet[i]) << 8) + ord(packet[i+1])
checksum += half_word
# Get one's complement of the checksum.
checksum = ~(checksum + 4) & 0xffff
# Put the new checksum back into the packet.
packet[22] = chr(checksum >> 8)
packet[23] = chr(checksum & ((1 << 8) -1))
# Write the reply packet into TUN device.
os.write(tun.fileno(), ''.join(packet))
view rawtun-ping-linux.pyhosted with ❤ by GitHub
简而言之,就是首先打开对应的设备文件,然后通过 ioctl 系统调用告诉它我们想要的网卡类型和名称,同时还可以告诉它我们想以普通用户的身份来对它进行操作。之后通过 ifconfig 命令将新建的网卡拉起来,就可以开始读写了。
当你以 root 身份运行这个脚本的时候,可以 ping 一下 192.168.7.2 这个地址试试,看看是不是能 ping 得通。
下面的代码则在 Mac 环境中实现了同样的功能(不过还没有设置用户身份的功能):
import os
import subprocess
# Open file corresponding to the TUN device.
tun = open('/dev/tun0', 'r+b')
# Bring it up and assign addresses.
subprocess.check_call('ifconfig tun0 192.168.7.1 192.168.7.2 up', shell=True)
while True:
# Read an IP packet been sent to this TUN device.
packet = list(os.read(tun.fileno(), 2048))
# Modify it to an ICMP Echo Reply packet.
#
# Note that I have not checked content of the packet, but treat all packets
# been sent to our TUN device as an ICMP Echo Request.
# Swap source and destination address.
packet[12:16], packet[16:20] = packet[16:20], packet[12:16]
# Change ICMP type code to Echo Reply (0).
packet[20] = chr(0)
# Clear original ICMP Checksum field.
packet[22:24] = chr(0), chr(0)
# Calculate new checksum.
checksum = 0
# for every 16-bit of the ICMP payload:
for i in range(20, len(packet), 2):
half_word = (ord(packet[i]) << 8) + ord(packet[i+1])
checksum += half_word
# Get one's complement of the checksum.
checksum = ~(checksum + 4) & 0xffff
# Put the new checksum back into the packet.
packet[22] = chr(checksum >> 8)
packet[23] = chr(checksum & ((1 << 8) - 1))
# Write the reply packet into TUN device.
os.write(tun.fileno(), ''.join(packet))
view rawtun-ping-mac.pyhosted with ❤ by GitHub
当然要运行上面的代码,你首先要到这里下载并安装 Mac 下的 TUN/TAP 设备的驱动程序才行。安装之后,在系统的 /dev 目录中就会分别有 16 个 /dev/tunX 以及 /dev/tapX ( X 表示网卡序号)字符设备文件,分别对应于 16 个同名的 TUN/TAP 虚拟网卡。当然,在运行这个脚本之前,你是看不到这些网卡的。不不,用 ifconfig -a 也不行。
因为与 Linux 下的驱动的实现方法不同,这里是用的文件名来标识网卡类型与名称,所以就不需要 Linux 版本中的第一个 ioctl 调用了。
也不记得一开始是在哪边看到的一个讲 TUN/TAP 编程的文章,说要实现对 ping 报文的处理,只要简单地将读到的 IP 报文中的源地址与目的地址对换一下,再写回去就可以了。在 Linux 系统中也确实是如此,因此我也就没有深究。直到后来才发现,同样的招数在 Mac 下居然没用。于是赶紧上网翻 ICMP 的报文格式,改报文中的 type 码,并重新计算 checksum ,这才搞定。这时也才发现,用 Python 来操作原始的字节流还是没有 C 这种底层语言直观啊。
可是 Linux 下为什么不需要这么麻烦呢?于是回去抓了抓包,这才发现,相对于 Mac 下的一问( ping 命令)一答( Python 脚本),在 Linux 下居然是两问两答,一问是 ping 命令,一问是我们的那个 Python 脚本。这也不奇怪,我连 ICMP 中的 type 码都没改,发过来的是 request ,那再发出去的当然还是 request 。至于应答,大概就是 Linux 的 TUN/TAP 驱动搞的鬼了。
最后,当然也有 Windows 的实现版本啦,不过代码被我丢到公司的 Windows 工作用机上了,所以,就请您且听下回分解了。

Update 2011-04-26: Windows 下的实现代码如下:
import _winreg as reg
import win32file
adapter_key = r'SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}'
def get_device_guid():
with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters:
try:
for i in xrange(10000):
key_name = reg.EnumKey(adapters, i)
with reg.OpenKey(adapters, key_name) as adapter:
try:
component_id = reg.QueryValueEx(adapter, 'ComponentId')[0]
if component_id == 'tap0801':
return reg.QueryValueEx(adapter, 'NetCfgInstanceId')[0]
except WindowsError, err:
pass
except WindowsError, err:
pass
def CTL_CODE(device_type, function, method, access):
return (device_type << 16) | (access << 14) | (function << 2) | method;
def TAP_CONTROL_CODE(request, method):
return CTL_CODE(34, request, method, 0)
TAP_IOCTL_CONFIG_POINT_TO_POINT = TAP_CONTROL_CODE(5, 0)
TAP_IOCTL_SET_MEDIA_STATUS = TAP_CONTROL_CODE(6, 0)
TAP_IOCTL_CONFIG_TUN = TAP_CONTROL_CODE(10, 0)
if __name__ == '__main__':
guid = get_device_guid()
handle = win32file.CreateFile(r'\\.\Global\%s.tap' % guid,
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE,
None, win32file.OPEN_EXISTING,
win32file.FILE_ATTRIBUTE_SYSTEM, # | win32file.FILE_FLAG_OVERLAPPED,
None)
print(handle.handle)
if False:
win32file.DeviceIoControl(handle, TAP_IOCTL_CONFIG_POINT_TO_POINT,
'\xc0\xa8\x11\x01\xc0\xa8\x11\x10', None);
else:
win32file.DeviceIoControl(handle, TAP_IOCTL_SET_MEDIA_STATUS, '\x01\x00\x00\x00', None)
win32file.DeviceIoControl(handle, TAP_IOCTL_CONFIG_TUN,
'\x0a\x03\x00\x01\x0a\x03\x00\x00\xff\xff\xff\x00', None)
while True:
l, p = win32file.ReadFile(handle, 2000)
q = p[:12] + p[16:20] + p[12:16] + p[20:]
win32file.WriteFile(handle, q)
print(p, q)
win32file.CloseHandle(handle)
view rawtun-ping-win.pyhosted with ❤ by GitHub


from http://glacjay.info/blog/2010-09-18/用-python-操作虚拟网卡.html
相关帖子:http://briteming.blogspot.com/2015/09/udpip-udpipvpn.html