Pull to refresh
193.28
ua-hosting.company
Хостинг-провайдер: серверы в NL до 300 Гбит/с

Виртуальный сетевой интерфейс

Reading time 13 min
Views 42K
Общеизвестно, что драйверы Linux — это модули ядра. Все драйверы являются модулями, но не все модули — драйверы. Примером одной из таких групп модулей, не являющихся драйверами, и гораздо реже появляющиеся в обсуждениях, являются сетевые фильтры на различных уровнях сетевого стека Linux.

Иногда, и даже достаточно часто, хотелось бы иметь сетевой интерфейс, который мог бы оперировать с трафиком любого другого интерфейса, но каким-то образом дополнительно «окрашивать» этот трафик. Такое может понадобится для дополнительного анализа, или контроля трафика, или его шифрования, …

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

О интерфейсах в пару слов


Сразу понятно, что мы намереваемся «навесить» новый интерфейс, который предстоит создать, на ранее существующий. Поэтому, бегло вспомним то, что касается создания интерфейсов (и что делается, например, драйвером любого сетевого адаптера), потому как там есть несколько нюансов, важных для наших целей.

Сетевой интерфейс — это то место, где:
  • по каждому принятому из интерфейса пакету создаются экземпляры структуры сокетных буферов (struct sk_buff), далее созданный экземпляр структуры продвигается по стеку протоколов вверх, до его получателя в пространстве пользователя, где он и уничтожается;
  • порождённые где-то на верхних уровнях протоколов пользовательского пространства исходящие экземпляры структуры struct sk_buff должны быть отправлены, а сами экземпляры структуры после этого уничтожаются (или утилизируются в пул).

На протяжении, как минимум, 5-6 последних лет, сетевые интерфейсы неизменно создавались макросом:

#define alloc_netdev( sizeof_priv, name, setup )

Здесь (детали будут понятны из примера модуля):
— sizeof_priv — размер приватной области данных интерфейса (struct net_device), которая будет создана ядром без нашего прямого участия;
— name — символьная строка — шаблон имени интерфейса;
— setup — адрес функции инициализации интерфейса;

В таком, практически неизменном, виде процесс создания интерфейса описан везде в публикациях и упоминается в обсуждениях. Но начиная с ядра 3.17 прототип макроса создания интерфейса меняется (<linux/netdevice.h>):

#define alloc_netdev( sizeof_priv, name, name_assign_type, setup ) 

Как легко видеть, теперь вместо 3-х параметров 4, 3-й из которых — константа, определяющая порядок нумерации создаваемых интерфейсов (исходя из шаблона имени), описанная в том же файле определений:
/* interface name assignment types (sysfs name_assign_type attribute) */
#define NET_NAME_UNKNOWN     0 /* unknown origin (not exposed to userspace) */
#define NET_NAME_ENUM        1 /* enumerated by kernel */
#define NET_NAME_PREDICTABLE 2 /* predictably named by the kernel */
#define NET_NAME_USER        3 /* provided by user-space */
#define NET_NAME_RENAMED     4 /* renamed by user-space */

Это первая тонкость на которую следует обратить внимание. Детальнее мы не будем углубляться в эти детали, важно было только отметить их.

Но созданный так интерфейс ещё не дееспособен, он не выполняет никаких действий. Для того, чтобы «придать жизнь» созданному сетевому интерфейсу, нужно реализовать для него соответствующий набор операций. Вся связь сетевого интерфейса с выполняемыми на нём операциями осуществляется через таблицу операций сетевого интерфейса:
struct net_device_ops { 
        int                     (*ndo_init)(struct net_device *dev); 
        void                    (*ndo_uninit)(struct net_device *dev); 
        int                     (*ndo_open)(struct net_device *dev); 
        int                     (*ndo_stop)(struct net_device *dev); 
        netdev_tx_t             (*ndo_start_xmit) (struct sk_buff *skb, 
                                                   struct net_device *dev); 
        ...
        struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);
        ...
}

В ядре 3.09, например, определено 39 операций в struct net_device_ops, и около 50-ти операций в ядре 3.14, но реально разрабатываемые модули реализуют только малую часть из них.

Характерно, что в таблице операций интерфейса присутствует операция передачи сокетного буфера ndo_start_xmit() в физическую среду, но вовсе нет операции приёма пакетов (сокетных буферов). Это совершенно естественно, как мы увидим вскоре: принятые пакеты (например в обработчике аппаратного прерывания IRQ) непосредственно после приёма вызовом netif_rx() (или netif_receive_skb()) тут же помещаются в очередь (ядра) принимаемых пакетов, и далее уже последовательно обрабатываются сетевым стеком. А вот выполнять функцию ndo_start_xmit() — обязательно, хотя бы, как минимум, для вызова API ядра dev_kfree_skb(), который утилизирует (уничтожает) сокетный буфер после успешной (да и безуспешной тоже) операции передачи пакета. Если этого не делать, в системе возникнет слабо выраженная утечка памяти (с каждым пакетом), которая, в конечном итоге, рано или поздно приведёт к краху системы. Это ещё одна тонкость, которую держим в уме.

Последним необходимым нам элементом является структура struct net_device (описана в <linux/netdevice.h>) — описание сетевого интерфейса. Это крупная структура, содержащая не только описание аппаратных средств, но и конфигурационные параметры сетевого интерфейса по отношению к выше лежащим протоколам (пример взят из ядра 3.09):
struct net_device { 
   char  name[ IFNAMSIZ ] ; 
...
   unsigned long  base_addr; /* device I/O address */ 
   unsigned int   irq;       /* device IRQ number  */ 
...
   unsigned       mtu;       /* interface MTU value     */ 
   unsigned short type;      /* interface hardware type */ 
...
   struct net_device_stats stats;
   struct list_head dev_list;
...
   /* Interface address info. */ 
   unsigned char  perm_addr[ MAX_ADDR_LEN ]; /* permanent hw address    */ 
   unsigned char  addr_len;                  /* hardware address length */ 
...
}

Здесь поле type, например, определяет тип аппаратного адаптера с точки зрения ARP-механизма разрешения MAC адресов (<linux/if_arp.h>):
...
#define ARPHRD_ETHER       1    /* Ethernet 10Mbps           */
...
#define ARPHRD_ARCNET      7    /* ARCnet                    */ 
...
#define ARPHRD_IEEE1394   24    /* IEEE 1394 IPv4 - RFC 2734 */
...
#define ARPHRD_IEEE80211 801    /* IEEE 802.11               */

Со структурой сетевого интерфейса обычно создаётся и связывается приватная структура данных (упоминавшаяся ранее), в которой пользователь может размещать произвольные собственные данные любой сложности, ассоциированные с интерфейсом. Это особо актуально, если предполагается, что драйвер может создавать несколько однотипных сетевых интерфейсов. Доступ к приватной структуре данных должен определяться исключительно специально определённой для того функцией netdev_priv(). Ниже показан возможный вид функции — это определение из ядра 3.09, но никто не даст гарантий, что в другом ядре оно радикально не поменяется:
/*     netdev_priv - access network device private data 
 *     Get network device private data 
 */ 
static inline void *netdev_priv( const struct net_device *dev ) { 
        return (char *)dev + ALIGN( sizeof( struct net_device ), NETDEV_ALIGN ); 
} 

Как легко видеть из определения, приватная структура данных дописывается непосредственно в хвост struct net_device — это обычная практика создания структур переменного размера, принятая в языке C начиная с стандарта C89 (и в C99).

Этого строительного материала нам будет достаточно для построения модуля виртуального сетевого интерфейса.

Модуль виртуального интерфейса


Создаём модуль, который будет перехватывать трафик сетевого ввода-вывода с другого, ранее существующего в системе (физического либо логического), интерфейса, и обеспечивать обработку этих потоков (файл virt.c)…
код модуля
#include <linux/module.h> 
#include <linux/version.h>
#include <linux/netdevice.h> 
#include <linux/etherdevice.h> 
#include <linux/moduleparam.h> 
#include <net/arp.h> 

#define ERR(...) printk( KERN_ERR "! "__VA_ARGS__ ) 
#define LOG(...) printk( KERN_INFO "! "__VA_ARGS__ ) 

static char* link = "eth0";      // имя родительского интерфейса
module_param( link, charp, 0 ); 
static char* ifname = "virt";    // имя создаваемого интерфейса
module_param( ifname, charp, 0 ); 

static struct net_device *child = NULL; 
struct priv { 
   struct net_device_stats stats; 
   struct net_device *parent; 
}; 

static rx_handler_result_t handle_frame( struct sk_buff **pskb ) { 
   struct sk_buff *skb = *pskb; 
   if( child ) { 
      struct priv *priv = netdev_priv( child ); 
      priv->stats.rx_packets++; 
      priv->stats.rx_bytes += skb->len; 
      LOG( "rx: injecting frame from %s to %s", skb->dev->name, child->name ); 
      skb->dev = child; 
      return RX_HANDLER_ANOTHER; 
   } 
   return RX_HANDLER_PASS; 
} 

static int open( struct net_device *dev ) { 
   netif_start_queue( dev ); 
   LOG( "%s: device opened", dev->name ); 
   return 0; 
} 

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

static netdev_tx_t start_xmit( struct sk_buff *skb, struct net_device *dev ) { 
   struct priv *priv = netdev_priv( dev ); 
   priv->stats.tx_packets++; 
   priv->stats.tx_bytes += skb->len; 
   if( priv->parent ) { 
      skb->dev = priv->parent; 
      skb->priority = 1; 
      dev_queue_xmit( skb ); 
      LOG( "tx: injecting frame from %s to %s", dev->name, skb->dev->name ); 
      return 0; 
   } 
   return NETDEV_TX_OK; 
} 

static struct net_device_stats *get_stats( struct net_device *dev ) { 
   return &( (struct priv*)netdev_priv( dev ) )->stats; 
} 

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

static void setup( struct net_device *dev ) { 
   int j; 
   ether_setup( dev ); 
   memset( netdev_priv(dev), 0, sizeof( struct priv ) ); 
   dev->netdev_ops = &crypto_net_device_ops; 
   for( j = 0; j < ETH_ALEN; ++j ) // fill in the MAC address with a phoney 
      dev->dev_addr[ j ] = (char)j; 
} 

int __init init( void ) { 
   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 ); // parent interface  
   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; 
   } 
   /* also, and clone its IP, MAC and other information */ 
   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 ); 
   rtnl_lock(); 
   netdev_rx_handler_register( priv->parent, &handle_frame, NULL ); 
   rtnl_unlock(); 
   LOG( "module %s loaded", THIS_MODULE->name ); 
   LOG( "%s: create link %s", THIS_MODULE->name, child->name ); 
   LOG( "%s: registered rx handler for %s", THIS_MODULE->name, priv->parent->name ); 
   return 0; 
err: 
   free_netdev( child ); 
   return err; 
} 

void __exit exit( void ) { 
   struct priv *priv = netdev_priv( child ); 
   if( priv->parent ) { 
      rtnl_lock(); 
      netdev_rx_handler_unregister( priv->parent ); 
      rtnl_unlock(); 
      LOG( "unregister rx handler for %s\n", priv->parent->name ); 
   } 
   unregister_netdev( child ); 
   free_netdev( child ); 
   LOG( "module %s unloaded", THIS_MODULE->name ); 
} 

module_init( init ); 
module_exit( exit ); 

MODULE_AUTHOR( "Oleg Tsiliuric" ); 
MODULE_AUTHOR( "Nikita Dorokhin" ); 
MODULE_LICENSE( "GPL v2" ); 
MODULE_VERSION( "2.1" ); 


Здесь всё достаточно просто, но некоторых отдельных комментариев заслуживают следующие моменты:
  • После создания интерфейса alloc_netdev() мы связываем его операции через таблицу crypto_net_device_ops. Здесь определены операции (поля): .ndo_open и .ndo_stop (которые вызываются при запуске и остановке интерфейса командой ifconfig up/down), .ndo_get_stats (запрос статистики интерфейса) и .ndo_start_xmit (передача пакета).
  • Через приватную область данных мы сохраняем связь с родительским интерфейсом в нами определённой структуре struct priv (в файлах примеров показано несколько различных вариантов использования приватной области для связывания).
  • В таблице операций нет (да и быть не может по логике) функции приёма сокетных буферов. Но вызовом netdev_rx_handler_register() (который появился только в ядре 2.6.36) мы можем добавить в очередь обработки принимаемых пакетов (для родительского интерфейса) собственную функцию-фильтр handle_frame(), которая будет вызываться для каждого приходящего с этого интерфейса пакета.
  • На время добавления фильтра к очереди, нам необходимо кратковременно заблокировать доступ к очереди (иначе нас может ожидать аварийный результат). Это достигается вызовами rtnl_lock() и rtnl_unlock().
  • При передаче исходящего сокетного буфера в сеть (функция start_xmit()) мы просто подменяем в структуре сокетного буфера интерфейс, через который физически должна производиться отправка.
  • При приёме, наоборот, сокетные буфера, создаваемые в родительском интерфейсе, подменяются на виртуальный.


Как это работает?


Выберем любой существующий и работоспособный сетевой интерфейс (в Fedora 16 один из Ethernet интерфейсов назывался как p7p1 — это хорошая иллюстрация того, что интерфейсы могут иметь очень разнообразные имена):
$ ip addr show dev p7p1
3: p7p1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 08:00:27:9e:02:02 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.101/24 brd 192.168.56.255 scope global p7p1
    inet6 fe80::a00:27ff:fe9e:202/64 scope link
    valid_lft forever preferred_lft forever 

Установим на него свой новый виртуальный интерфейс и конфигурируем его на IP подсеть (192.168.50.0/24), отличную от исходной подсети интерфейса p7p1:
$ sudo insmod virt2.ko link=p7p1 
$ sudo ifconfig virt0 192.168.50.2 
$ ifconfig virt0 
virt0     Link encap:Ethernet  HWaddr 08:00:27:9E:02:02 
          inet addr:192.168.50.2  Bcast:192.168.50.255  Mask:255.255.255.0 
          inet6 addr: fe80::a00:27ff:fe9e:202/64 Scope:Link 
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1 
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0 
          TX packets:27 errors:0 dropped:0 overruns:0 carrier:0 
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 b)  TX bytes:5027 (4.9 KiB) 

Самый простой и быстрый способ создать ответный конец коммуникации (нам ведь нужно как-то тестировать свою работу?) для такой новой (192.168.50.2/24) подсети на другом хосте LAN, это создать алиасный IP для сетевого интерфейса этого удалённого хоста, по типу:
$ sudo ifconfig vboxnet0:1 192.168.50.1
$ ifconfig 
... 
vboxnet0  Link encap:Ethernet  HWaddr 0A:00:27:00:00:00  
          inet addr:192.168.56.1  Bcast:192.168.56.255  Mask:255.255.255.0 
          inet6 addr: fe80::800:27ff:fe00:0/64 Scope:Link 
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1 
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0 
          TX packets:223 errors:0 dropped:0 overruns:0 carrier:0 
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 b)  TX bytes:36730 (35.8 KiB) 
vboxnet0:1 Link encap:Ethernet  HWaddr 0A:00:27:00:00:00  
          inet addr:192.168.50.1  Bcast:192.168.50.255  Mask:255.255.255.0 
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1  

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

Теперь из вновь созданного виртуального интерфейса мы можем проверить прозрачность сети посылкой ICMP:
$ ping 192.168.50.1 
PING 192.168.50.1 (192.168.50.1) 56(84) bytes of data. 
64 bytes from 192.168.50.1: icmp_req=1 ttl=64 time=0.371 ms 
64 bytes from 192.168.50.1: icmp_req=2 ttl=64 time=0.210 ms 
64 bytes from 192.168.50.1: icmp_req=3 ttl=64 time=0.184 ms 
64 bytes from 192.168.50.1: icmp_req=4 ttl=64 time=0.242 ms 
^C 
--- 192.168.50.1 ping statistics --- 
4 packets transmitted, 4 received, 0% packet loss, time 3001ms 
rtt min/avg/max/mdev = 0.184/0.251/0.371/0.074 ms 
$ sudo tcpdump -i virt0 
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 
listening on virt0, link-type EN10MB (Ethernet), capture size 65535 bytes 
00:13:02.228615 IP 192.168.50.1 > 192.168.50.2: ICMP echo request, id 5609, seq 1, length 64 
00:13:02.228716 ARP, Request who-has 192.168.50.1 tell 192.168.50.2, length 28 
00:13:02.228786 ARP, Reply 192.168.50.1 is-at 0a:00:27:00:00:00 (oui Unknown), length 46 
00:13:02.228803 IP 192.168.50.2 > 192.168.50.1: ICMP echo reply, id 5609, seq 1, length 64 
00:13:03.227996 IP 192.168.50.1 > 192.168.50.2: ICMP echo request, id 5609, seq 2, length 64 
00:13:03.228059 IP 192.168.50.2 > 192.168.50.1: ICMP echo reply, id 5609, seq 2, length 64 
00:13:04.228016 IP 192.168.50.1 > 192.168.50.2: ICMP echo request, id 5609, seq 3, length 64 
...
00:14:09.236014 ARP, Request who-has 192.168.50.2 tell 192.168.50.1, length 46 
00:14:09.236052 ARP, Reply 192.168.50.2 is-at 08:00:27:9e:02:02 (oui Unknown), length 28 
tcpdump: pcap_loop: The interface went down 
16 packets captured 
16 packets received by filter 
0 packets dropped by kernel 

И далее создать (теперь уже наоборот, на удалённом хосте) полноценную сессию SSH к новому виртуальному интерфейсу:
$ ssh 192.168.50.2 
Nasty PTR record "192.168.50.2" is set up for 192.168.50.2, ignoring 
olej@192.168.50.2's password: 
Last login: Tue Apr  3 10:21:28 2012 from 192.168.1.5 
$ uname -a 
Linux fedora16vm.localdomain 3.3.0-8.fc16.i686 #1 SMP Thu Mar 29 18:33:55 UTC 2012 i686 i686 i386 GNU/Linux 
$ exit 
logout 
Connection to 192.168.50.2 closed. 
$

С таким, вновь созданным, виртуальным интерфейсом можно проделать множество увлекательных экспериментов в самых разнообразных сетевых конфигурациях!

Что дальше?


Проницательный читатель, да ещё если он внимательно читал предыдущий текст, вправе в этом месте воскликнуть: «Но ведь ваш виртуальный интерфейс не дополняет, а замещает родительский?». Да, в показанном варианте именно так: загрузка такого модуля запрещает трафик по родительскому интерфейсу, но выгрузка модуля опять восстанавливает его.

Этому несчастью легко помочь. Для того чтобы создаваемый виртуальный сетевой интерфейс мог работать независимо в дополнение к родительскому, необходимо:
  • В фильтрах (и приёма и передачи) анализировать поле IP-адреса в структуре сокетного буфере и производить подмену интерфейса только для IP, принадлежащего виртуальному интерфейсу.
  • На приёме разделить обработку сокетных буферов, соответствующим протоколам IP и ARP, потому как структуры данных этих протоколов, естественно, отличаются (поле struct sk_buff*->protocol).

Это выглядит, возможно, сложновато в словесном описании, но в коде модуля всё достаточно просто, и добавляет не более 25 строк кода. И такой вариант приведен в архиве примеров (подкаталог virt-full, здесь этот код не приводится, чтобы не перегружать текст):
$ ping 192.168.50.2 
PING 192.168.50.2 (192.168.50.2) 56(84) bytes of data. 
64 bytes from 192.168.50.2: icmp_req=1 ttl=64 time=0.473 ms 
64 bytes from 192.168.50.2: icmp_req=2 ttl=64 time=0.256 ms 
64 bytes from 192.168.50.2: icmp_req=3 ttl=64 time=0.281 ms 
^C 
--- 192.168.50.2 ping statistics --- 
3 packets transmitted, 3 received, 0% packet loss, time 1999ms 
rtt min/avg/max/mdev = 0.256/0.336/0.473/0.099 ms 
$ ping 192.168.56.101 
PING 192.168.56.101 (192.168.56.101) 56(84) bytes of data. 
64 bytes from 192.168.56.101: icmp_req=1 ttl=64 time=2.63 ms 
64 bytes from 192.168.56.101: icmp_req=2 ttl=64 time=0.306 ms 
64 bytes from 192.168.56.101: icmp_req=3 ttl=64 time=0.225 ms 
^C 
--- 192.168.56.101 ping statistics --- 
3 packets transmitted, 3 received, 0% packet loss, time 2002ms 
rtt min/avg/max/mdev = 0.225/1.053/2.630/1.115 ms 
$ dmesg | tail -n19 
[58382.498200] virt0: no IPv6 routers present 
[58391.368273] device virt0 entered promiscuous mode 
[58409.904046] ! rx: IP4 to IP=192.168.50.2 
[58409.904050] ! rx: injecting frame from p7p1 to virt0 
[58409.904197] ! tx: injecting frame from virt0 to p7p1 
[58409.904212] ! rx: ARP for 192.168.50.2 
[58409.904214] ! rx: injecting frame from p7p1 to virt0 
[58409.904262] ! tx: injecting frame from virt0 to p7p1 
[58410.903427] ! rx: IP4 to IP=192.168.50.2 
[58410.903431] ! rx: injecting frame from p7p1 to virt0 
[58410.903531] ! tx: injecting frame from virt0 to p7p1 
[58411.903447] ! rx: IP4 to IP=192.168.50.2 
[58411.903451] ! rx: injecting frame from p7p1 to virt0 
[58411.903547] ! tx: injecting frame from virt0 to p7p1 
[58414.694485] ! rx: ARP for 192.168.56.101 
[58414.696846] ! rx: IP4 to IP=192.168.56.101 
[58415.696508] ! rx: IP4 to IP=192.168.56.101 
[58416.696572] ! rx: IP4 to IP=192.168.56.101 
[58419.712245] ! rx: ARP for 192.168.56.101 

Архив кодов для продолжения экспериментирования можете взять здесь или здесь.
Tags:
Hubs:
+20
Comments 11
Comments Comments 11

Articles

Information

Website
ua-hosting.company
Registered
Founded
Employees
11–30 employees
Location
Латвия
Representative
HostingManager