OpenVPN的廣播問題以及tun和tap裝置的深層次挖掘

來源:互聯網
上載者:User

廣播到底通過還是不通過OpenVPN呢?tap處理二層,tun處理三層,雖然tun兩端ip是同一個子網,但是其二層卻不是,廣播是無法進行的,但是tap可以傳輸廣播;由於windows的虛擬網卡驅動的特殊性,為了讓windows也能進入vpn,OpenVPN和虛擬網卡驅動作了特殊且複雜的處理。本文詳述之(注意,本文不介紹OpenVPN的各種專業術語,比如路由模式和橋接模式之類,需要的話請參考OpenVPN的文檔或者FAQ)。
     怎麼理解tun裝置建立的是“點對點”鏈路,因為tun隧道是三層隧道,沒有二層鏈路,更不必說二層廣播鏈路了,我們知道資料連結層有兩種通訊方式,一種是點對點的方式,比如ppp協議,另一種是廣播的方式,比如乙太網路,tun裝置建立的隧道只有兩個端點,隧道中封裝的是IP資料報,雖然也需要arp協議來定位隧道對端tun裝置的mac,然而如果有n台機器同時串連進一個虛擬網路並且屬於同一個網段的話,其它機器是不會收到這個arp報文的,因為根本就沒有二層鏈路幫忙廣播轉寄這個arp報文

static inline unsigned int mroute_extract_addr_from_packet (struct mroute_addr *src,
                 struct mroute_addr *dest,
                 struct mroute_addr *esrc,
                 struct mroute_addr *edest,
                 const struct buffer *buf,
                 int tunnel_type)
{
...
  unsigned int ret = 0;
  verify_align_4 (buf);
  if (tunnel_type == DEV_TYPE_TUN) //如果是tun模式,那麼直接處理ipv4包頭,不再處理廣播的情況,但是可以處理ip多播
    ret = mroute_extract_addr_ipv4 (src, dest, buf);
  else if (tunnel_type == DEV_TYPE_TAP)  //只有在tap的情況下才解析二層地址。
    ret = mroute_extract_addr_ether (src, dest, esrc, edest, buf);
  return ret;
}
在XXX_addr_ether中會調用:
if (is_mac_mcast_addr (eth->dest))
    ret |= MROUTE_EXTRACT_BCAST;
在以後的代碼中通過這個MROUTE_EXTRACT_BCAST來判斷,進而進行廣播。需要注意的是,是在OpenVPN中而不是在tun/tap驅動中進行廣播的,可以把tap模式下的OpenVPN進程當成一個二層交換器,而SSL出入口和tap裝置出入口是交換器上的物理介面,廣播資料的一種行進方向是從SSL_read(雖然OpenVPN並不調用OpenSSL的libssl的介面)中將以太幀

讀出並解密,然後進入OpenVPN進行廣播,同時將資料交給自己一份,就是說往自己的tap裝置中寫入一份,廣播給別的機器的資料還是要經過隧道的方式進行的,就是說將資料經過SSL協議封裝然後通過socket發送。這個發送過程是通過multi_bcast來處理的,實際上multi_bcast並不是真正要發送資料,而是將待發送的資料連同其目的地資訊先放入到一個容器中,然後等到時機成熟時統一處理這個容器。所謂時機成熟就是在multi_process_outgoing_link被調用時:multi_process_outgoing_link-->multi_get_queue,正如multi_get_queue的注釋所說,這種容器不但存放廣播資料,還存放client-to-client資料以及多播資料,既然說到這裡了,那麼就說說client-to-client的一些話題。首先下面是一段錯誤的論述:
     client-to-client實際上目前是通過server來充當路由器的,所有的client-to-client的串連都要通過server進行中轉,中轉資料包到達server之後首先通過ethX到達應用程式層並且解除ssl封裝,然後server將此裸ip封裝後的資料包寫入tun0,通過路由之後,發現目的ip地址是到一個vpn的虛擬私人網段的一個ip地址,此時就又將資料從tun0發送出去從而又被openvpn接收,此時server查看自己是否設定了client-to-client,如果沒有設定的話,並且剛剛查到的虛擬私人的ip地址不是自己的話,那麼就說明這是一次client到client的通訊,丟棄該資料包,反之就將之寫入目的虛擬私人ip對應的真實ip的ssl串連,這個串連怎麼查詢得到呢?畢竟一個server可以擁有很多的client,其實有兩種方式,一種是通過為每一個用戶端配置一個tun虛擬網卡的方式,然後通過路由來實現區分,另一種方式就是在openvpn中解決,當資料從虛擬網卡發送時,實際上出去的是一個帶有標準ip頭的資料報,openvpn通過字元裝置讀取的就是這個資料報,它自己顯然可以通過讀取ip頭得到目的地址,然後得知對應的真實ssl串連是哪一個。對比兩種方式,第一種對效率影響很小,可以實現高速轉寄,但是管理複雜,第二種方式單點決策,管理簡單又安全,但是在應用程式層解析資料對效能影響很大,可以考慮並發。
     以上的論述錯就錯在OpenVPN是不可能如此複雜地實現client-to-client的,看了OpenVPN的原始碼之後,總的來說OpenVPN的實現很簡單,基本就是個轉寄站,看一下下面的流程:
while (true) {
...
    multi_process_io_udp (&multi);
...
}
static void multi_process_io_udp (struct multi_context *m)
{
...
    if (status & SOCKET_WRITE) //寫入socket
              multi_process_outgoing_link (m, mpp_flags);
      else if (status & TUN_WRITE) //寫入虛擬網卡字元裝置
              multi_process_outgoing_tun (m, mpp_flags);
      else if (status & SOCKET_READ) { //讀取socket
              read_incoming_link (&m->top);
              multi_release_io_lock (m);
              if (!IS_SIG (&m->top))
            multi_process_incoming_link (m, NULL, mpp_flags);
        }
      else if (status & TUN_READ) { //讀取虛擬網卡字元裝置
        read_incoming_tun (&m->top);
        multi_release_io_lock (m);
        if (!IS_SIG (&m->top))
            multi_process_incoming_tun (m, mpp_flags);
        }
}
其中multi_process_outgoing_link是寫socket的操作,當然在真正寫入之前要做SSL封裝,如果順著往該函數裡面看,就會發現寫往socket的資料來源自於一個隊列,就是multi_get_queue中處理的隊列,於是問題就是誰將資料放入了隊列,由於OpenVPN的邏輯就是上面的multi_process_io_udp,因此很顯然是multi_process_incoming_tun將資料放入了隊列,multi_process_incoming_tun最終調用了mroute_extract_addr_from_packet,這也就和本文的最開始的廣播問題聯絡了起來。總的來說OpenVPN在multi_process_io_udp中首先形成了下面兩個通道:
1.from tun/tap-->to socket
2.from socket-->to tun/tap
如果僅僅是這兩個通道的話,client-to-client通訊正如上面所說的那樣,可是OpenVPN中還提供了另外的通道,那就是:
3.from socket-->to socket
正如下面的調用路徑所示:
multi_process_incoming_link:
if (BLEN (&c->c2.buf) > 0){
    process_incoming_link (c);  //SSL解鎖裝,內部將c->c2.to_tun.len設定為需要寫入tun/tap的資料長度,也就是說預設是要寫入到虛擬網卡裝置的,但是在下面的邏輯中可能將c->c2.to_tun.len重新設定為0,什麼情況呢?那就是資料已經處理過了的情況,比如這是一個client-to-client的通訊,就沒有必要往虛擬網卡裝置寫入了,也證實了上面那段話的錯誤。既然有to_tun,那麼肯定有c->c2.to_link了,只是那是將從tun/tap讀出的資料寫往link也就是socket的。
    if (TUNNEL_TYPE (m->top.c1.tuntap) == DEV_TYPE_TUN) {
        mroute_flags = mroute_extract_addr_from_packet (...);
...
        else if (m->enable_c2c) { //如果c2c啟用的話
            if (mroute_flags & MROUTE_EXTRACT_MCAST) ...//組播
              else {
                      mi = multi_get_instance_by_virtual_addr (m, &dest, true); //server作為“路由器”找到目的client的socket
                  if (mi) {
                multi_unicast (m, &c->c2.to_tun, mi);  //單播發送,實際上就是放入了隊列,中轉源client到目的client的通訊
                register_activity (c, BLEN(&c->c2.to_tun));
                  c->c2.to_tun.len = 0; //凡是有這個語句的表示資料已經處理過了,不需要to-tun了,或者資料出錯
            }
            }
        }
...
如是說,在process_incoming_link就處理了client-to-client,根本就不需要再寫入tun/tap裝置,然後靠路由再寫入tun/tap。同時從上述調用路徑繼續跟蹤也可以看到基於tun的隧道是不支援廣播的,因為MROUTE_EXTRACT_BCAST標誌只在mroute_extract_addr_ether中被設定,而後者只有在tap模式中才會被調用,同時也只有在tap模式下調用mroute_extract_addr_ether的時候才會處理arp,並且只在一個packet filter先行編譯宏啟用時才被啟用,該宏不啟用的時候在tap裝置模式下arp通過正常的tap隧道被傳送,而arp正是一種鏈路層廣播。那麼問題又來了,如果是tun裝置模式的話,怎樣找到對端地址呢?這還要看linux kernel的tun驅動程式:
static void tun_net_init(struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    switch (tun->flags & TUN_TYPE_MASK) {
    case TUN_TUN_DEV:  //下面設定tun裝置的點對點模式
        dev->hard_header_len = 0;
        dev->addr_len = 0;
        dev->mtu = 1500;
        dev->type = ARPHRD_NONE; //沒有arp,就是一個點對點的串連,路由時直接從出口發出,不再arp
        dev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;
        dev->tx_queue_len = 10;
        break;
    case TUN_TAP_DEV:
        dev->set_multicast_list = tun_net_mclist;
        *(u16 *)dev->dev_addr = htons(0x00FF);
        get_random_bytes(dev->dev_addr + sizeof(u16), 4);
        ether_setup(dev);
        break;
    }
}
最終就回到了最初的問題,tun裝置沒有鏈路層,它是點對點的,client到server的定址是靠隧道進行,雖然是在一個ip網段,它們也不是靠arp來定址的,畢竟arp尋的是鏈路層地址,對於沒有鏈路層的隧道兩端來講,它還尋找什麼呢?可是windows的tap裝置的行為是不一樣的,windows的tap驅動並不會像linux的tun驅動那樣按照tun或者tap模式的不同分別設定網卡,於是就必須設定一個實際上不存在的ip地址代表對端,然後以此不存在的地址作為網關發送資料,事實上發往該網關的資料全部經由虛擬網卡發送,於是就是就走上了隧道上匝道,於是就有了net30的模式。
     windows的tap-win32驅動始終將帶有以太頭的幀發出(可以抓包,看代碼確認),因此tap-win32驅動並不真的支援點對點的ip串連,真正的點對點連接一般用於專用線路上,比如SLIP協議(很簡單的串列鏈路協議,類似HDLC),這種點對點鏈路其實也並不是沒有鏈路層,而是鏈路層特別簡單,當然了肯定沒有arp/廣播等機制支援多點定址了,windows機器一般用於個人電腦,而個人電腦一般使用乙太網路,根本沒有個人電腦使用點對點鏈路的,於是windows的虛擬網卡基本就是一個乙太網路的虛擬適配器,這從它的名稱tap-win32上也能看得出來。我們看一下tap-win32驅動的IO完成:
CompleteIRP:
if (p_PacketBuffer->m_SizeFlags & TP_TUN) { //如果是tun裝置模式的話就不將以太頭傳輸給使用者空間
      offset = ETHERNET_HEADER_SIZE;
      len = (int) (p_PacketBuffer->m_SizeFlags & TP_SIZE_MASK) - ETHERNET_HEADER_SIZE;
} else {
      offset = 0;
      len = (p_PacketBuffer->m_SizeFlags & TP_SIZE_MASK);
}
可以看出,windows上目前的虛擬網卡並沒有直接的點對點鏈路的概念(不知道今後有沒有人去開發),基本還是按照老一套機制來的,發送arp來進行乙太網路的定址,而對於非windows的系統上運行OpenVPN的tun裝置模式來講,arp是不需要的,也是永遠不會被發送的。對於windows來說,其arp是有人回應的,那麼是誰回應的呢,既然事情是由tap-win32的驅動引起的,那麼就不要在OpenVPN的代碼中尋找這個arp回應了,還是在tap-win32驅動本身尋找吧,再次重申,OpenVPN本來在tun裝置模式下不支援鏈路層,為了相容windows才定製出net30的拓撲來類比鏈路層的,tun模式雖然在ip層看來所有的ip處於一個網段,但是這同一個網段的ip之間的通訊靠的卻不是鏈路層(比如乙太網路的arp),而是各個用戶端和伺服器的點對點鏈路,如果是tap模式,那很顯然是有鏈路層的,並且就是乙太網路,arp會在client和server之間傳送。回到tap-win32驅動的問題,arp是怎麼發送又是怎麼接收的呢?
DriverEntry:
l_Properties->SendHandler = AdapterTransmit;
NDIS_STATUS
AdapterTransmit (IN NDIS_HANDLE p_AdapterContext,
         IN PNDIS_PACKET p_Packet,
         IN UINT p_Flags)
{
...           
    if (l_Adapter->m_tun) {
        ETH_HEADER *e;
        if (l_PacketLength < ETHERNET_HEADER_SIZE)
              goto no_queue;

        e = (ETH_HEADER *) l_PacketBuffer->m_Data;
        switch (ntohs (e->proto)) {
          case ETH_P_ARP:
...   //由於tap-win32必然要實現乙太網路卡的標準,實際上它就是一個乙太網路卡,從而arp也是必須要處理的,然而tun模式下的arp是沒有意義的,於是tap-win32不得不採用一種自問自答的方式來自圓其說。
                ProcessARP (l_Adapter,
                (PARP_PACKET) l_PacketBuffer->m_Data,
                l_Adapter->m_localIP,
                l_Adapter->m_remoteNetwork,
                l_Adapter->m_remoteNetmask,
                l_Adapter->m_TapToUser.dest); //此處自答自己發出的arp請求。
          default:
                goto no_queue;
          case ETH_P_IP:
...
          }
    if (IS_UP (l_Adapter)) //以下將資料包push到一個讀隊列,以便從使用者空間ReadFile讀取
              result = QueuePush (l_Adapter->m_Extension.m_PacketQueue, l_PacketBuffer);
...
}
BOOLEAN
ProcessARP (TapAdapterPointer p_Adapter,
        const PARP_PACKET src,
        const IPADDR adapter_ip,
        const IPADDR ip_network,
        const IPADDR ip_netmask,
        const MACADDR mac)
{
  if (src->m_Proto == htons (ETH_P_ARP)
      && MAC_EQUAL (src->m_MAC_Source, p_Adapter->m_MAC)
      ...//檢查確認這個arp是發送給自己的
      && src->m_ARP_IP_Destination != adapter_ip) {
      ARP_PACKET *arp = (ARP_PACKET *) MemAlloc (sizeof (ARP_PACKET), TRUE);
      if (arp) {
      // Initialize ARP reply fields
      arp->m_Proto = htons (ETH_P_ARP);
      arp->m_MAC_AddressType = htons (MAC_ADDR_TYPE);
      arp->m_PROTO_AddressType = htons (ETH_P_IP);
      arp->m_MAC_AddressSize = sizeof (MACADDR);
      arp->m_PROTO_AddressSize = sizeof (IPADDR);
      arp->m_ARP_Operation = htons (ARP_REPLY);
      // ARP addresses
      COPY_MAC (arp->m_MAC_Source, mac);  //mac實際上第3個位元組比p_Adapter->m_MAC要大1,這裡謊稱mac是從“遠端”過來的
      COPY_MAC (arp->m_MAC_Destination, p_Adapter->m_MAC); //“遠道而來”的arp-reply的目的地顯然是p_Adapter->m_MAC,也就是自己
      COPY_MAC (arp->m_ARP_MAC_Source, mac);
      COPY_MAC (arp->m_ARP_MAC_Destination, p_Adapter->m_MAC);
      arp->m_ARP_IP_Source = src->m_ARP_IP_Destination;
      arp->m_ARP_IP_Destination = adapter_ip;
      InjectPacket (p_Adapter, (UCHAR *) arp, sizeof (ARP_PACKET)); //類比接收arp-reply資料幀
      MemFree (arp, sizeof (ARP_PACKET));
    }
      return TRUE;
    }
  else
    return FALSE;
}
最終的InjectPacket調用NdisMEthIndicateReceive和NdisMEthIndicateReceiveComplete來使得虛擬網卡自己以為從物理鏈路收到了資料。最後總結的就是,tun模式下,linux系統或者unix系統從來不發送arp,windows發送的arp也不會到達OpenVPN進程,直接在tap-win32中類比,也正是基於此,有了諸如net30之類的網路拓撲,至於廣播的問題,嚴
格說來tun模式沒有廣播,即使windows的tap-win32驅動也沒有將arp廣播呈現到使用者空間的OpenVPN,最後,OpenVPN的體系是簡單的,實現是很對稱的,基本就是一個tun和link之間的轉寄站。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.