在我的
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)) |
简而言之,就是首先打开对应的设备文件,然后通过 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)) |
当然要运行上面的代码,
你首先要到这里下载并安装 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) |