关于 TUN/TAP 设备

时间:2022-11-27 08:19:13

长期以来对tun和tap这对兄弟分不太清,今天下定决心研究了一下代码,总算是搞明白了。

首先它们都是从/dev/net/tun里ioctl出来的虚拟设备,一个是通过IFF_TUN,另一个是 IFF_TAP。最好的例子莫过于vpnc里面的代码了。

C:
  1. int tun_open ( char  *dev,  enum if_mode_enum mode )
  2. {
  3.          struct ifreq ifr;
  4.          int fd, err;
  5.  
  6.          if  ( (fd  = open ( "/dev/net/tun", O_RDWR ) ) < 0 )  {
  7.                 error ( 0, errno,
  8.                          "can't open /dev/net/tun, check that it is either device char 10 200 or (with DevFS) a symlink to ../misc/net/tun (not misc/net/tun)" );
  9.                  return  -1;
  10.          }
  11.  
  12.         memset ( &ifr,  0sizeof (ifr ) );
  13.         ifr. ifr_flags  =  ( (mode  == IF_MODE_TUN ) ? IFF_TUN  : IFF_TAP ) | IFF_NO_PI;
  14.          if  ( *dev )
  15.                 strncpy (ifr. ifr_name, dev, IFNAMSIZ );
  16.  
  17.          if  ( (err  = ioctl (fd, TUNSETIFF,  ( void  * ) &ifr ) ) < 0 )  {
  18.                 close (fd );
  19.                  return err;
  20.          }
  21.         strcpy (dev, ifr. ifr_name );
  22.          return fd;
  23. }

用的ioctl的命令都是同一个TUNSETIFF。

虽然是出自一个娘,但它们仍然有大的不同。tun是点对点的设备,而tap是一个普通的以太网卡设备。也就是说,tun设备其实完全不需要有物理地址的!它收到和发出的包不需要arp,也不需要有数据链路层的头!而tap设备则是有完整的物理地址和完整的以太网帧。

用一个实际的例子来验证一下:

tap0    Link encap:Ethernet  HWaddr 0E:78:39:78:E7:A7
          inet addr:192.168.1.109  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::c78:39ff:fe78:e7a7/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:21 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)


tun0    Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet addr:X.X.X.X  P-t-P:X.X.X.X  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1412  Metric:1
          RX packets:6 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:690 (690.0 b)  TX bytes:402 (402.0 b)


% ethtool -i tun0
driver: tun
version: 1.6
firmware-version: N/A
bus-info: tun
% ethtool -i tap0
driver: tun
version: 1.6
firmware-version: N/A
bus-info: tap

 
 

继续回来看代码。还是vpnc的代码 tunip.c,看它发送的时候做了什么处理:

C:
  1. static  int tun_send_ip ( struct sa_block  *s )
  2. {
  3.          int sent, len;
  4.         uint8_t  *start;
  5.  
  6.         start  = s ->ipsec. rx. buf;
  7.         len    = s ->ipsec. rx. buflen;
  8.  
  9.          if  (opt_if_mode  == IF_MODE_TAP )  {
  10. #ifndef __sun__
  11.                  /*
  12.                  * Add ethernet header before s->ipsec.rx.buf where
  13.                  * at least ETH_HLEN bytes should be available.
  14.                  */
  15.                  struct ether_header  *eth_hdr  =  ( struct ether_header  * )  (s ->ipsec. rx. buf  - ETH_HLEN );
  16.  
  17.                 memcpy (eth_hdr ->ether_dhost, s ->tun_hwaddr, ETH_ALEN );
  18.                 memcpy (eth_hdr ->ether_shost, s ->tun_hwaddr, ETH_ALEN );
  19.  
  20.                  /* Use a different MAC as source */
  21.                 eth_hdr ->ether_shost [ 0 ]  ^= 0x80;  /* toggle some visible bit */
  22.                 eth_hdr ->ether_type  = htons (ETHERTYPE_IP );
  23.  
  24.                 start  =  (uint8_t  * ) eth_hdr;
  25.                 len  += ETH_HLEN;
  26. #endif
  27.          }
  28.  
  29.         sent  = tun_write (s ->tun_fd, start, len );
  30.          if  (sent  != len )
  31.                 syslog (LOG_ERR,  "truncated in: %d -> %d\n", len, sent );
  32.         hex_dump ( "Tx pkt", start, len,  NULL );
  33.          return  1;
  34. }

从上面的代码我们很容易看出:

1. 所谓发送就是对/dev/net/tun进行写操作。对称的,所谓接收就是读操作。
2. 如果是tap设备,发送时还要多加一个以太网的头。

我们再看内核中对应的代码是怎么处理的,在drivers/net/tun.c 中的 tun_get_user():

C:
  1. switch  (tun ->flags  & TUN_TYPE_MASK )  {
  2.          case TUN_TUN_DEV :
  3.                  if  (tun ->flags  & TUN_NO_PI )  {
  4.                  //...
  5.                  }
  6.  
  7.                 skb_reset_mac_header (skb );
  8.                 skb ->protocol  = pi. proto;
  9.                 skb ->dev  = tun ->dev;
  10.                  break;
  11.          case TUN_TAP_DEV :
  12.                 skb ->protocol  = eth_type_trans (skb, tun ->dev );
  13.                  break;

内核直接忽略了 tun 设备的以太网帧。现在,整个流程我们就已经很清楚了。

可是,上面只是用vpnc的例子。我们知道,实际中像kvm虚拟机才是tap的使用大户,我们很有必要看一下kvm是怎么使用tap设备的。为了方便起见,我们不看 qemu-kvm,因为它的代码过于复杂,我们看一个简单的kvm tools的实现。

这部分的主要代码在 virtio/net.c里面,virtio_net__tap_init()是在启动虚拟机时初始化tap设备的,然后启动两个线程分别监控tap设备的收发,代码是virtio_net_rx_thread()和virtio_net_tx_thread(),它们负责把进来的IO操作转换成对/dev/net/tun的读写。可是,IO操作是怎么进来的呢?这是关键。

顺着代码里的“针”一个个找下去,我们不难发现,IO操作是由kvm模拟出来的。首先它会把CPU指令中对应的IO操作进行转化,这部分在内核中,arch/x86/kvm/emulate.c::x86_emulate_insn():

C:
  1. do_io_in :
  2.                 c ->dst. bytes  = min (c ->dst. bytes, 4u );
  3.                  if  ( !emulator_io_permited (ctxt, ops, c ->src. val, c ->dst. bytes ) )  {
  4.                         emulate_gp (ctxt,  0 );
  5.                          goto done;
  6.                  }
  7.                  if  ( !pio_in_emulated (ctxt, ops, c ->dst. bytes, c ->src. val,
  8.                                       &c ->dst. val ) )
  9.                          goto done;  /* IO is needed */
  10.                  break;

pio_in_emulated() 调用的 emulator_pio_in_emulated() 会进一步触发KVM_EXIT_IO:

C:
  1. static  int emulator_pio_in_emulated ( int size,  unsigned  short port,  void  *val,
  2.                               unsigned  int count,  struct kvm_vcpu  *vcpu )
  3. {
  4.          if  (vcpu ->arch. pio. count )
  5.                  goto data_avail;
  6.  
  7.         trace_kvm_pio ( 0, port, size,  1 );
  8.  
  9.         vcpu ->arch. pio. port  = port;
  10.         vcpu ->arch. pio. in  =  1;
  11.         vcpu ->arch. pio. count   = count;
  12.         vcpu ->arch. pio. size  = size;
  13.  
  14.          if  ( !kernel_pio (vcpu, vcpu ->arch. pio_data ) )  {
  15.         data_avail :
  16.                 memcpy (val, vcpu ->arch. pio_data, size  * count );
  17.                 vcpu ->arch. pio. count  =  0;
  18.                  return  1;
  19.          }
  20.  
  21.         vcpu ->run ->exit_reason  = KVM_EXIT_IO;
  22.         vcpu ->run ->io. direction  = KVM_EXIT_IO_IN;
  23.         vcpu ->run ->io. size  = size;
  24.         vcpu ->run ->io. data_offset  = KVM_PIO_PAGE_OFFSET  * PAGE_SIZE;
  25.         vcpu ->run ->io. count  = count;
  26.         vcpu ->run ->io. port  = port;
  27.  
  28.          return  0;
  29. }

内核部分结束,转到用户空间,用户空间的 vcpu 会捕捉到这个事件,在 kvm-cpu.c::kvm_cpu__start() 中:

C:
  1. case KVM_EXIT_IO :  {
  2.                         bool ret;
  3.  
  4.                         ret  = kvm__emulate_io (cpu ->kvm,
  5.                                         cpu ->kvm_run ->io. port,
  6.                                          (u8  * )cpu ->kvm_run  +
  7.                                         cpu ->kvm_run ->io. data_offset,
  8.                                         cpu ->kvm_run ->io. direction,
  9.                                         cpu ->kvm_run ->io. size,
  10.                                         cpu ->kvm_run ->io. count );
  11.  
  12.                          if  ( !ret )
  13.                                  goto panic_kvm;
  14.                          break;
  15.                  }

kvm__emulate_io() 就会调用在 virtio/net.c 注册的 virtio_net_pci_io_in(),数据就这样流向了 tap 网卡了。


转自:http://wangcong.org/blog/