Ещё один виртуальный интерфейс

В предыдущей заметке был показан набросок кода модуля ядра Linux для создания дополнительного виртуального сетевого интерфейса. Это был упрощённый фрагмент из реального проекта, отработавшего несколько лет без сбоев и рекламаций, так что он вполне может служить шаблоном для дальнейшего улучшения, исправления и развития.

Но такой подход к реализации, во-первых, не единственный, а, во-вторых, в некоторых ситуациях он может быть и неприемлемым (например, во встраиваемой системе с ядром младше 2.6.36, где ещё нет вызова netdev_rx_handler_register()). Ниже будет рассмотрен альтернативный вариант с той же функциональностью, но реализующий её на совсем другом слое сетевого стека TCP/IP.


Достаточно много, чтобы не повторяться, написано о том, что уровни (слои) сетевого стека TCP/IP не соответствуют однозначно 7-ми уровням модели взаимодействия открытых систем OSI/ISO (или, если честнее, модель OSI, близкая сердцу академических кругов, оказалась неадекватной реально развивающейся сети TCP/IP). Создание виртуального интерфейса, в предыдущей обсуждавшейся реализации, выполнялось на уровне интерфейсов (L2, Level 2 — очень примерно соответствующий канальному уровню OSI). Нынешняя реализация использует возможности сетевого уровня (L3).

Представляется целесообразным рассмотреть некоторый минимум относительно средств сетевого уровня, в объёме даже чуть шире, чем необходимо для текущей задачи, для возможностей последующего её расширения. На сетевом уровне стека сетевых протоколов (TCP/IP, но не только — здесь же поддерживаются и все другие семейства протоколов, но на сегодня они представляются мало актуальными) обеспечивается обработка таких протоколов как: IP/IPv4/IPv6, IPX, IGMP, RIP, OSPF, ARP, или осуществляется добавление оригинальных пользовательских протоколов. Для установки обработчиков сетевого уровня предоставляется API сетевого уровня (<linux/netdevice.h>):

struct packet_type { 
   __be16  type; /* This is really htons(ether_type). */ 
   struct net_device *dev; /* NULL is wildcarded here    */ 
   int (*func) (struct sk_buff*, struct net_device*, struct packet_type*, struct net_device*); 
...
   struct list_head list; 
}; 
extern void dev_add_pack( struct packet_type *pt ); 
extern void dev_remove_pack( struct packet_type *pt ); 


Фактически, в протокольных модулях ядра мы должны добавить фильтр, через который проходят буфера сокетов из входящего потока интерфейса (исходящий поток реализуется проще, как было показано в предыдущей реализации). Функция dev_add_pack() добавляет ещё один новый обработчик для пакетов заданного типа, реализуемый функцией func(). Функция добавляет, но не замещает существующий обработчик (в том числе и обработчик по умолчанию сетевой системы Linux). На обработку в функцию отбираются (попадают) те буфера сокетов, которые удовлетворяют критерием, заложенным в структуре struct packet_type (по типу протокола type и сетевому интерфейсу dev).

Примечание: По той же схеме (установка функции-фильтра) происходит добавление новых протоколов и на более высоком, транспортном уровне сетевого стека (на котором обрабатываются, например, протоколы UDP, TCP, SCTP). Все более высокие уровни (более-менее аналогичные уровням модели OSI) в ядре не представлены, и обслуживаются в пространстве пользователя техникой программирования BSD сокетов. Но все эти, относящиеся к более высоким уровням, детали более не будут рассматриваться в тексте.

Если мы хотели бы добавить новый протокол (проприетарный), то должны были бы переопределить его тип:

#define PROTO_ID 0x1234 
static struct packet_type test_proto = { 
   __constant_htons( PROT_ID ), 
   ...
}


Проблему при этом составило бы то, что стандартный IP-стек не знает такого протокола, и всю его обработку нам придётся взять на себя. Но в наши цели входит только переопределить обработку некоторых пакетов, то для этого используем константу ETH_P_ALL, указывающую, что через фильтр должны проходить все протоколы (а если поле dev равно NULL — то и все сетевые интерфейсы).

Для сравнения и конкретизации, большое число идентификаторов протоколов (Ethernet Protocol ID's) находим в <linux/if_ether.h>, вот некоторые из них, для примера:

#define ETH_P_LOOP   0x0060  /* Ethernet Loopback packet  */
#define ETH_P_IP     0x0800  /* Internet Protocol packet  */
#define ETH_P_ARP    0x0806  /* Address Resolution packet */
#define ETH_P_PAE    0x888E  /* Port Access Entity (IEEE 802.1X) */
#define ETH_P_ALL    0x0003  /* Every packet (be careful!!!) */
...


В данном случае поле type — это не абстрактное числовое значение в программном коде, это значение в бинарном виде будет вписано в заголовок Ethernet кадра, физически отправляемого в среду распространения:

struct ethhdr { 
   unsigned char h_dest[ETH_ALEN];   /* destination eth addr */ 
   unsigned char h_source[ETH_ALEN]; /* source ether addr    */ 
   __be16       h_proto;             /* packet type ID field */ 
} __attribute__((packed)); 


(Это же описание нам понадобится в коде при заполнении структуры struct packet_type в модуле).

Сама функция фильтра (поле func), которую нам ещё предстоит написать, может быть, в простейшем варианте, нечто подобное:

int test_pack_rcv( struct sk_buff *skb, struct net_device *dev, 
                   struct packet_type *pt, struct net_device *odev ) { 
   LOG( "packet received with length: %u\n", skb->len );
   kfree_skb( skb ); 
   return skb->len; 
};


Функция показана здесь, главным образом, из-за обязательного вызова kfree_skb(). Он, в отличие от, казалось бы, близкого по смыслу dev_kfree_skb() в передающем канале, не уничтожает сокетный буфер, а только декрементирует его счётчик использования (поле users). При установке каждого дополнительного фильтра протокола вызовом dev_add_pack() это поле сокетных буферов будет инкрементировано. Вы можете установить несколько фильтров сетевого уровня (в одном и том же, или нескольких загружаемых модулях) и они будут срабатывать все в порядке обратному их установке, но каждый из них должен выполнить kfree_skb(). В противном случае вы будете иметь медленную, но неуклонную утечку памяти в сетевом стеке, так что её результат, как крах системы, обнаружится только через несколько часов непрерывной работы.

Это достаточно интересное и не очевидное место, настолько, что есть смысл отвлечься и посмотреть исходный код реализации kfree_skb() (файл net/core/skbuff.c):

void kfree_skb(struct sk_buff *skb) {
   if (unlikely(!skb))
      return;
   if (likely(atomic_read(&skb->users) == 1))
      smp_rmb();
   else if (likely(!atomic_dec_and_test(&skb->users)))
      return;
   trace_kfree_skb(skb, __builtin_return_address(0));
   __kfree_skb(skb);
}


Вызов kfree_skb() будет реально освобождать буфер сокета только в случае skb->users == 1, при всех остальных значениях он будет только декрементировать skb->users (счётчик использования).

Теперь у нас есть достаточно деталей для того, чтобы организовать работу виртуального интерфейса, но используя, на этот раз, сетевой уровень IP-стека.


Поступим как и ранее: создадим два варианта модуля — упрощённый вариант virtl.ko, сетевой интерфейс (virt0) которого замещает родительский сетевой интерфейс, и полный вариант virt.ko, который анализирует сетевые фреймы протоколов (ARP и IP4), и затрагивает только тот трафик, который его интерфейсу относится. Разница состоит в том, что на время загрузки упрощённого модуля работа родительского интерфейса временно прекращается (до выгрузки модуля virtl.ko), а при загрузке полного варианта оба интерфейса могут работать параллельно и независимо. Код полного модуля заметно более громоздкий, а для понимания принципов он ничего не добавляет. Далее детально рассмотрен упрощённый вариант, показывающий принципы, и только позже мы минимально коснёмся полного варианта (код его и протокол испытаний приведены в архиве примеров):

здесь достаточно длинный код
#include <linux/module.h> 
#include <linux/version.h> 
#include <linux/netdevice.h> 
#include <linux/etherdevice.h> 
#include <linux/inetdevice.h> 
#include <linux/moduleparam.h> 
#include <net/arp.h> 
#include <linux/ip.h> 

#define ERR(...) printk( KERN_ERR "! "__VA_ARGS__ ) 
#define LOG(...) printk( KERN_INFO "! "__VA_ARGS__ ) 
#define DBG(...) if( debug != 0 ) printk( KERN_INFO "! "__VA_ARGS__ ) 

static char* link = "eth0"; 
module_param( link, charp, 0 ); 

static char* ifname = "virt"; 
module_param( ifname, charp, 0 ); 

static int debug = 0; 
module_param( debug, int, 0 ); 

static struct net_device *child = NULL; 
static struct net_device_stats stats;  // статическая таблица статистики интерфейса 
static u32 child_ip; 

struct priv { 
   struct net_device *parent; 
}; 

static char* strIP( u32 addr ) {      // диагностика IP в точечной нотации 
   static char saddr[ MAX_ADDR_LEN ]; 
   sprintf( saddr, "%d.%d.%d.%d", 
            ( addr ) & 0xFF, ( addr >> 8 ) & 0xFF, 
            ( addr >> 16 ) & 0xFF, ( addr >> 24 ) & 0xFF 
          ); 
   return saddr; 
} 

static int open( struct net_device *dev ) { 
   struct in_device *in_dev = dev->ip_ptr; 
   struct in_ifaddr *ifa = in_dev->ifa_list;      /* IP ifaddr chain */ 
   LOG( "%s: device opened", dev->name ); 
   child_ip = ifa->ifa_address; 
   netif_start_queue( dev ); 
   if( debug != 0 ) { 
      char sdebg[ 40 ] = ""; 
      sprintf( sdebg, "%s:", strIP( ifa->ifa_address ) ); 
      strcat( sdebg, strIP( ifa->ifa_mask ) ); 
      DBG( "%s: %s", dev->name, sdebg ); 
   } 
   return 0; 
} 

static int stop( struct net_device *dev ) { 
   LOG( "%s: device closed", dev->name ); 
   netif_stop_queue( dev ); 
   return 0; 
} 

static struct net_device_stats *get_stats( struct net_device *dev ) { 
   return &stats; 
} 

// передача фрейма 
static netdev_tx_t start_xmit( struct sk_buff *skb, struct net_device *dev ) { 
   struct priv *priv = netdev_priv( dev ); 
   stats.tx_packets++; 
   stats.tx_bytes += skb->len; 
   skb->dev = priv->parent;   // передача в родительский (физический) интерфейс 
   skb->priority = 1; 
   dev_queue_xmit( skb ); 
   DBG( "tx: injecting frame from %s to %s with length: %u", 
        dev->name, skb->dev->name, skb->len ); 
   return 0; 
   return NETDEV_TX_OK; 
} 

static struct net_device_ops net_device_ops = { 
   .ndo_open = open, 
   .ndo_stop = stop, 
   .ndo_get_stats = get_stats, 
   .ndo_start_xmit = start_xmit, 
}; 

// приём фрейма 
int pack_parent( struct sk_buff *skb, struct net_device *dev, 
                 struct packet_type *pt, struct net_device *odev ) { 
   skb->dev = child;          // передача фрейма в виртуальный интерфейс 
   stats.rx_packets++; 
   stats.rx_bytes += skb->len; 
   DBG( "tx: injecting frame from %s to %s with length: %u", 
        dev->name, skb->dev->name, skb->len ); 
   kfree_skb( skb ); 
   return skb->len; 
}; 
 
static struct packet_type proto_parent = { 
   __constant_htons( ETH_P_ALL ), // перехватывать все пакеты: ETH_P_ARP & ETH_P_IP 
   NULL, 
   pack_parent, 
   (void*)1, 
   NULL 
}; 

int __init init( void ) { 
   void setup( struct net_device *dev ) { // вложенная функция (расширение GCC) 
      int j; 
      ether_setup( dev ); 
      memset( netdev_priv( dev ), 0, sizeof( struct priv ) ); 
      dev->netdev_ops = &net_device_ops; 
      for( j = 0; j < ETH_ALEN; ++j )     // заполнить MAC фиктивным адресом 
         dev->dev_addr[ j ] = (char)j; 
   } 
   int err = 0; 
   struct priv *priv; 
   char ifstr[ 40 ]; 
   sprintf( ifstr, "%s%s", ifname, "%d" ); 
#if (LINUX_VERSION_CODE < KERNEL_VERSION(3, 17, 0)) 
   child = alloc_netdev( sizeof( struct priv ), ifstr, setup ); 
#else 
   child = alloc_netdev( sizeof( struct priv ), ifstr, NET_NAME_UNKNOWN, setup ); 
#endif 
   if( child == NULL ) { 
      ERR( "%s: allocate error", THIS_MODULE->name ); return -ENOMEM; 
   } 
   priv = netdev_priv( child ); 
   priv->parent = dev_get_by_name( &init_net, link ); // родительский интерфейс  
   if( !priv->parent ) { 
      ERR( "%s: no such net: %s", THIS_MODULE->name, link ); 
      err = -ENODEV; goto err; 
   } 
   if( priv->parent->type != ARPHRD_ETHER && priv->parent->type != ARPHRD_LOOPBACK ) { 
      ERR( "%s: illegal net type", THIS_MODULE->name ); 
      err = -EINVAL; goto err; 
   } 
   memcpy( child->dev_addr, priv->parent->dev_addr, ETH_ALEN ); 
   memcpy( child->broadcast, priv->parent->broadcast, ETH_ALEN ); 
   if( ( err = dev_alloc_name( child, child->name ) ) ) { 
      ERR( "%s: allocate name, error %i", THIS_MODULE->name, err ); 
      err = -EIO; goto err; 
   } 
   register_netdev( child );         // зарегистрировать новый интерфейс 
   proto_parent.dev = priv->parent; 
   dev_add_pack( &proto_parent );    // установить обработчик фреймов для родителя 
   LOG( "module %s loaded", THIS_MODULE->name ); 
   LOG( "%s: create link %s", THIS_MODULE->name, child->name ); 
   return 0; 
err: 
   free_netdev( child ); 
   return err; 
} 

void __exit virt_exit( void ) { 
   struct priv *priv= netdev_priv( child ); 
   dev_remove_pack( &proto_parent ); // удалить обработчик фреймов 
   unregister_netdev( child ); 
   dev_put( priv->parent ); 
   free_netdev( child ); 
   LOG( "module %s unloaded", THIS_MODULE->name ); 
   LOG( "=============================================" ); 
} 

module_init( init ); 
module_exit( virt_exit ); 

MODULE_AUTHOR( "Oleg Tsiliuric" ); 
MODULE_LICENSE( "GPL v2" ); 
MODULE_VERSION( "3.7" ); 



Всё достаточно прозрачно:

  • После регистрации нового сетевого интерфейса (virt0) он выполняет вызов dev_add_pack(), устанавливающий фильтр принимаемых пакетов для родительского интерфейса;
  • Предварительно устанавливается в структуре packet_type поле dev на указатель родительского интерфейса: только с этого интерфейса входящий трафик будет перехватываться определённой в структуре функцией pack_parent();
  • Эта функция фиксирует статистику интерфейса и, самое главное, подменяет в сокетном буфере указатель родительского интерфейса на виртуальный.
  • Обратная подмена (виртуального на физический) происходит в функции отправки фрейма start_xmit().


Вот как это работает:

  • На тестируемом компьютере загружаем модуль и конфигурируем его на отдельную новую подсеть:
    $ ip address 
    ...
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 
        link/ether 08:00:27:52:b9:e0 brd ff:ff:ff:ff:ff:ff 
        inet 192.168.1.21/24 brd 192.168.1.255 scope global eth0 
        inet6 fe80::a00:27ff:fe52:b9e0/64 scope link 
           valid_lft forever preferred_lft forever 
    3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 
        link/ether 08:00:27:0f:13:6d brd ff:ff:ff:ff:ff:ff 
        inet 192.168.56.102/24 brd 192.168.56.255 scope global eth1 
        inet6 fe80::a00:27ff:fe0f:136d/64 scope link 
           valid_lft forever preferred_lft forever 
    $ sudo insmod virt.ko link=eth1 debug=1 
    $ sudo ifconfig virt0 192.168.50.19 
    $ sudo ifconfig virt0 
    virt0     Link encap:Ethernet  HWaddr 08:00:27:0f:13:6d 
              inet addr:192.168.50.19  Bcast:192.168.50.255  Mask:255.255.255.0 
              inet6 addr: fe80::a00:27ff:fe0f:136d/64 Scope:Link 
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1 
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0 
              TX packets:46 errors:0 dropped:0 overruns:0 carrier:0 
              collisions:0 txqueuelen:1000 
              RX bytes:0 (0.0 B)  TX bytes:8373 (8.1 KiB) 
    
    

    (Здесь показана статистика с нулевым числом принятых байт на интерфейсе).
  • На компьютере, с которого проводим тестирование, создаём алиасный IP для новой подсети (192.168.50.0/24) и можем осуществлять трафик на созданный интерфейс:
    $ sudo ifconfig vboxnet0:1 192.168.50.1
    $ ping 192.168.50.19 
    PING 192.168.50.19 (192.168.50.19) 56(84) bytes of data. 
    64 bytes from 192.168.50.19: icmp_req=1 ttl=64 time=0.627 ms 
    64 bytes from 192.168.50.19: icmp_req=2 ttl=64 time=0.305 ms 
    64 bytes from 192.168.50.19: icmp_req=3 ttl=64 time=0.326 ms 
    ^C 
    --- 192.168.50.19 ping statistics --- 
    3 packets transmitted, 3 received, 0% packet loss, time 2000ms 
    rtt min/avg/max/mdev = 0.305/0.419/0.627/0.148 ms 
    
    
  • На этом же (тестирующем) компьютере (ответной стороне) очень информационно наблюдать трафик (в отдельном терминале), фиксируемый tcpdump:
    $ sudo tcpdump -i vboxnet0 
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 
    listening on vboxnet0, link-type EN10MB (Ethernet), capture size 65535 bytes 
    ... 
    18:41:01.740607 ARP, Request who-has 192.168.50.19 tell 192.168.50.1, length 28 
    18:41:01.741104 ARP, Reply 192.168.50.19 is-at 08:00:27:0f:13:6d (oui Unknown), length 28 
    18:41:01.741116 IP 192.168.50.1 > 192.168.50.19: ICMP echo request, id 8402, seq 1, length 64 
    18:41:01.741211 IP 192.168.50.19 > 192.168.50.1: ICMP echo reply, id 8402, seq 1, length 64 
    18:41:02.741164 IP 192.168.50.1 > 192.168.50.19: ICMP echo request, id 8402, seq 2, length 64 
    18:41:02.741451 IP 192.168.50.19 > 192.168.50.1: ICMP echo reply, id 8402, seq 2, length 64 
    18:41:03.741163 IP 192.168.50.1 > 192.168.50.19: ICMP echo request, id 8402, seq 3, length 64 
    18:41:03.741471 IP 192.168.50.19 > 192.168.50.1: ICMP echo reply, id 8402, seq 3, length 64 
    18:41:06.747701 ARP, Request who-has 192.168.50.1 tell 192.168.50.19, length 28 
    18:41:06.747715 ARP, Reply 192.168.50.1 is-at 0a:00:27:00:00:00 (oui Unknown), length 28 
    
    


Теперь коротко, в два слова, о том, как сделать полновесный виртуальный интерфейс, работающий только со своим трафиком, и не нарушающий работу родительского интерфейса (то, что делает полная версия модуля в архиве). Для этого необходимо:

  • Объявить два раздельных обработчика протоколов (для протоколов разрешения имён ARP и собственно для протокола IP):
    // обработчик фреймов ETH_P_ARP 
    int arp_pack_rcv( struct sk_buff *skb, struct net_device *dev, 
                      struct packet_type *pt, struct net_device *odev ) { 
       ...
       return skb->len; 
    }; 
    
    static struct packet_type arp_proto = { 
       __constant_htons( ETH_P_ARP ), 
       NULL, 
       arp_pack_rcv,  // фильтр пртокола ETH_P_ARP 
       (void*)1, 
       NULL 
    }; 
    
    // обработчик фреймов ETH_P_IP 
    int ip4_pack_rcv( struct sk_buff *skb, struct net_device *dev, 
                      struct packet_type *pt, struct net_device *odev ) { 
       ...
       return skb->len; 
    }; 
    
    static struct packet_type ip4_proto = { 
       __constant_htons( ETH_P_IP ), 
       NULL, 
       ip4_pack_rcv,    // фильтр пртокола ETH_P_IP 
       (void*)1, 
       NULL 
    }; 
    
    
  • Оба их последовательно зарегистрировать в функции инициализации модуля:
       arp_proto.dev = ip4_proto.dev = priv->parent; // перехват только с родительского интерфейса 
       dev_add_pack( &arp_proto ); 
       dev_add_pack( &ip4_proto ); 
    
    

  • Каждый из установленных фильтров должен осуществлять подмену интерфейса только для тех фреймов, IP получателя которых совпадает с IP интерфейса…
  • Два раздельных обработчика удобны тем, что заголовки фреймов ARP и IP имеют совершенно разный формат, и выделять IP назначения в них приходится по-разному (весь полный код показан в архиве примера).


Используя такой полновесный модуль, можно открыть к хосту, например, две параллельные сессии SSH на разные интерфейсы (использующие разные IP), которые будут в параллель реально использовать единый общий физический интерфейс:

$ ssh olej@192.168.50.17 
olej@192.168.50.17's password: 
Last login: Mon Jul 16 15:52:16 2012 from 192.168.1.9 
...
$ ssh olej@192.168.56.101 
olej@192.168.56.101's password: 
Last login: Mon Jul 16 17:29:57 2012 from 192.168.50.1 
...
$ who 
olej     tty1         2012-07-16 09:29 (:0) 
olej     pts/0        2012-07-16 09:33 (:0.0) 
...
olej     pts/6        2012-07-16 17:29 (192.168.50.1) 
olej     pts/7        2012-07-16 17:31 (192.168.56.1) 

Последняя показанная команда (who) выполняется уже в сессии SSH, то есть на том самом удалённом хосте, к которому и фиксируется два независимых подключения из двух различных подсетей (последние две строки вывода), которые на самом деле представляют один хост, но с точки зрения различных его сетевых интерфейсов.


При подготовке и отладке примеров модулей, для уточнения деталей, активно использовалась вот эта (достаточно свежая) книга: Rami Rosen: «Linux Kernel Networking: Implementation and Theory», Apress, 650 pages, 2014, ISBN-13: 978-1-4302-6196-4.
51he4s8RCWL._SX403_BO1,204,203,200_.jpg

Автор любезно предоставил её для свободного скачивания ещё до выхода книги в продажу (2013-12-22). Скачать её можно на этой странице.

Все, кого интересуют вопросы, подобные обсуждавшимся в этой статье, смогут найти в этом издании много идей для дальнейшего развития техники собственного использования сетевых интерфейсов.

Архив кодов, упоминаемых в тексте, для экспериментов и дальнейшего развития, можно взять здесь или здесь.

© Habrahabr.ru