TUN/TAP 设备是一种让用户态程序向内核协议栈注入数据的设备,一个工作在三层,一个工作在二层。理论知识可以看:
本文只讲怎么用,直接上代码:
#include <fcntl.h>
#include <sys/socket.h>
#include <linux/ip.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <iostream>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <thread>
using namespace std;
void task(char* dev, int net_addr)
{
int tun_fd;
struct ifreq ifr;
tun_fd = open("/dev/net/tun", O_RDWR);
if(0 > tun_fd)
{
cout<<"open tun file error!" << endl;
return;
}
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strncpy(ifr.ifr_ifrn.ifrn_name, dev, IFNAMSIZ);
if(0 > ioctl(tun_fd, TUNSETIFF, &ifr))
{
string err_str = strerror(errno);
cout << "Failed to set TUN device name: " << err_str << endl;
close(tun_fd);
return;
}
// Bring up the interface
int sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if(0 > ioctl(sock, SIOCGIFFLAGS, &ifr))
{
string err_str = strerror(errno);
cout << "Failed to bring up socket: " << err_str << endl;
close(tun_fd);
return;
}
ifr.ifr_flags |= IFF_UP | IFF_RUNNING | IFF_PROMISC;
if(0 > ioctl(sock, SIOCSIFFLAGS, &ifr))
{
string err_str = strerror(errno);
cout << "Failed to set socket flags: " << err_str << endl;
close(tun_fd);
return;
}
ifr.ifr_addr.sa_family = AF_INET;
((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr.s_addr = net_addr;
if(0 > ioctl(sock, SIOCSIFADDR, &ifr))
{
string err_str = strerror(errno);
cout << "Failed to set socket address: " << err_str << endl;
close(tun_fd);
return;
}
ifr.ifr_netmask.sa_family = AF_INET;
((struct sockaddr_in *)&ifr.ifr_netmask)->sin_addr.s_addr = inet_addr("255.255.255.0");
if(0 > ioctl(sock, SIOCSIFNETMASK, &ifr))
{
string err_str = strerror(errno);
cout << "Failed to set socket netmask: " << err_str << endl;
close(tun_fd);
return;
}
if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0)
{
string err_str = strerror(errno);
cout << "Failed to get socket index: " << err_str << endl;
close (tun_fd);
return;
}
struct sockaddr_ll addr;
memset (&addr, 0, sizeof (addr));
addr.sll_family = AF_PACKET;
addr.sll_ifindex = ifr.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ALL);
printf("addr.sll_ifindex %d" ,addr.sll_ifindex);
if (-1 == bind(sock, (struct sockaddr *)&addr, sizeof(addr)))
{
string err_str = strerror(errno);
printf("binding socket %d with if %s error: %s",
sock, dev, strerror(errno));
close (tun_fd);
return;
}
int N_bytes;
unsigned char msg[8000];
in_addr ip_addr;
ip_addr.s_addr = net_addr;
int j = 500;
while(j-- > 0)
{
N_bytes = read(tun_fd, msg, 8000);
if (N_bytes > 0)
{
printf("%s recv msg len: %3d, msg: ", inet_ntoa(ip_addr), N_bytes);
for (int i = 0; i < 20 && i < N_bytes; i++) printf("%2x ", msg[i]);
cout << endl;
}
}
while(true)
{
N_bytes = recv (sock, msg, 8000, 0);
if (N_bytes > 0)
{
printf("%s sock recv msg len: %3d, msg: ", inet_ntoa(ip_addr), N_bytes);
for (int i = 0; i < 20 && i < N_bytes; i++) printf("%2x ", msg[i]);
cout << endl;
}
}
}
int main(){
char dev[IFNAMSIZ] = "tun0";
char dev2[IFNAMSIZ] = "tun1";
thread t(task, dev, inet_addr("172.23.1.25"));
thread t2(task, dev2, inet_addr("172.23.1.24"));
t.join();
t2.join();
}
1、上面代码建立了两个tun设备(tun0和tun1),每个设备有自己的地址。
2、task中演示了两种访问tun设备的方式 – 文件方式和socket方式(为了让socket能收发所有的包,用了SOCK_RAW,这里可以根据自己的需要建立其他协议类型的socket)。
3、假设有个目标地址是 10.0.0.10,执行下面的命令,程序就可以打印所接收到的包了。
ping 10.0.0.10 -I tun0
curl可以如下使用:
sudo curl --interface tun_srsue -O target_url
4、如果使用tun方式,收到的包都是ip包;如果使用tap方式,收到的是以太网帧。
5、跟netlink其实有点像,很多场景下可以互换。不过从使用方便程度看,tun占很大优势,因为不用编写内核驱动。跟netlink的性能没有对比测试,有空了测测。
说到使用方便性,在go语言中体现更明显。目前为止还没有哪个netlink库能很方便的使用,但是tun库是有的,代码如下:
package main
import (
"log"
"github.com/songgao/water"
)
func main() {
ifce, err := water.New(water.Config{
DeviceType: water.TUN,
})
if err != nil {
log.Fatal(err)
}
log.Printf("Interface Name: %s\n", ifce.Name())
packet := make([]byte, 2000)
for {
n, err := ifce.Read(packet)
if err != nil {
log.Fatal(err)
}
log.Printf("Packet Received: % x\n", packet[:n])
}
}