ICMP-туннель на уровне ядра Linux: передаём TCP/UDP-трафик через эхо-запросы

от автора

Недавно я наткнулся на статью о том, что в ICMP-пакеты можно вставлять произвольные данные. Сразу возникла мысль: а почему бы не попробовать загнать весь трафик через ICMP (да, о существовании ICMP-туннеля я тоже ничего не знал). Так появился проект — ICMP-туннель на уровне ядра, который:

  • перехватывает исходящие TCP/UDP-пакеты;

  • инкапсулирует их в ICMP эхо-запросы (тип ICMP_ECHO);

  • на приёмной стороне извлекает оригинальные пакеты и передаёт их дальше.

Цель проекта — углубить знания в:

  • разработке модулей ядра (работа с sk_buff, хуки Netfilter, тасклеты). На данный момент я работал с Linux только из user space;

  • сетевом стеке (IPv4, Ethernet, ICMP, TCP, UDP) и, в частности, его реализации в Linux. До этого моя работа с сетевыми протоколами ограничивалась сокетами;

  • механизмах перехвата и модификации пакетов.

Архитектура проекта

  1. Хук NF_INET_POST_ROUTING — для пакетов, которые покинули пространство user space и готовы отправиться по сети на другую машину:

    1. фильтрует TCP/UDP-трафик;

    2. преобразует пакет в ICMP. В результате преобразования должен получиться пакет следующего вида:

      Packet

      Пакет данных
  2. Хук NF_INET_LOCAL_IN — для пакетов, которые прошли все сетевые фильтры и готовы отправиться в user space:

    1. перехватывает входящие ICMP-пакеты;

    2. извлекает оригинальный TCP/UDP-пакет;

    3. направляет его в loopback-интерфейс (lo) для дальнейшей обработки системой.

  3. Тасклет для отправки пакета в сетевой интерфейс.

Шаг 1. Регистрация хуков

inputHook.hook = input_hook;inputHook.hooknum = NF_INET_LOCAL_IN;inputHook.pf = PF_INET;inputHook.priority = NF_IP_PRI_FIRST;nf_register_net_hook(&init_net, &inputHook);outputHook.hook = output_hook;outputHook.hooknum = NF_INET_POST_ROUTING;outputHook.pf = PF_INET;outputHook.priority = NF_IP_PRI_FIRST;nf_register_net_hook(&init_net, &outputHook);

Пояснения:

  • NF_INET_POST_ROUTING — хук для исходящего трафика;

  • NF_INET_LOCAL_IN — хук для входящего трафика;

  • приоритет NF_IP_PRI_FIRST гарантирует, что наш обработчик сработает первым.

Шаг 2. Задача отправки пакета в сетевой интерфейс

struct task_data {    struct tasklet_struct tasklet;    struct sk_buff *skb;};void send_func (unsigned long d){    struct task_data *data = (struct task_data *)d;    if (dev_queue_xmit(data->skb) != 0)     {        pr_err("dev_queue_xmit failed\n");        kfree_skb(data->skb);    }    kfree (data);}

Пояснения:

  • В качестве механизма отложенного выполнения задачи я выбрал тасклеты. Подробнее про них можно прочитать в этой статье (https://habr.com/ru/companies/embox/articles/244071/).

  • sk_buff — это основная сетевая структура в ядре Linux.

  • dev_queue_xmit — функция ставит в очередь буфер для передачи на сетевое устройство.

  • В случае неудачи добавления буфера в сетевой интерфейс уничтожаем sk_buff посредством вызова kfree_skb(data->skb);

  • Освобождаем память, выделенную под задачу kfree (data)

Шаг 3. Хук NF_INET_POST_ROUTING

static unsigned int output_hook (void *priv, struct sk_buff *skb, const struct nf_hook_state *state){    struct sk_buff *skb_out = create_packet_output (skb);    if (!skb_out)        return NF_ACCEPT;        struct task_data *data = kmalloc (sizeof(struct task_data), GFP_ATOMIC);    if (!data)    {        pr_err("kmalloc\n");        kfree_skb(skb_out);        return NF_ACCEPT;    }    data->skb = skb_out;    tasklet_init (&data->tasklet, send_func, (unsigned long)data);    tasklet_schedule (&data->tasklet);    return NF_STOLEN;}

Пояснения:

  • Функция create_packet_output;

    • фильтрует TCP/UDP пакеты;

    • преобразует TCP/UDP пакет в ICMP;

    • формирует sk_buff для отправки в сетевой интерфейс.

  • Если пакет не прошёл фильтрацию или не удалось создать sk_buff , то отправляем текущий пакет на следующий этап. Для этого возвращаем значение NF_ACCEPT;

  • Создаём структуру для тасклета.

  • Регистрируем отложенную задачу: tasklet_schedule (&data->tasklet);

  • NF_STOLEN — сообщаем ядру, что пакет мы забрали и дальше его обрабатывать не надо. Важно использовать именно его, а не NF_DROP.

Полный код create_packet_output
static struct sk_buff* create_packet_output(struct sk_buff* in_packet){       struct iphdr* ip_in = ip_hdr(in_packet);   uint8_t mac_out[ETH_ALEN];   uint8_t protocol_type;   uint16_t header_len;   void* transport_hdr;   uint16_t data_len;      if (ip_in->protocol != IPPROTO_UDP && ip_in->protocol != IPPROTO_TCP)    {       return NULL;   }      if (find_mac_addr(mac_out, ip_in->daddr, in_packet->dev) < 0)    {       pr_info("Not found mac\n");       return NULL;   }      if (skb_linearize(in_packet))    {       pr_info("Failed to linearize skb\n");       return NULL;   }      if (ip_in->protocol == IPPROTO_UDP)    {       struct udphdr* in_udp = udp_hdr(in_packet);       protocol_type = 0;         header_len = sizeof(struct udphdr);       transport_hdr = in_udp;       data_len = ntohs(in_udp->len) - sizeof(struct udphdr);   }    else    {        struct tcphdr* in_tcp = tcp_hdr(in_packet);       protocol_type = 1;         header_len = tcp_hdrlen(in_packet);       transport_hdr = in_tcp;       data_len = ntohs(ip_in->tot_len) - (ip_in->ihl * 4) - header_len;   }      int packet_size = sizeof(struct ethhdr)                    + sizeof(struct iphdr)                    + sizeof(struct icmphdr)                   + header_len                   + data_len;      int hh_len = LL_RESERVED_SPACE(in_packet->dev);   int tlen = in_packet->dev->needed_tailroom;   struct sk_buff* skb = netdev_alloc_skb(in_packet->dev, hh_len + tlen + packet_size);   if (unlikely(!skb))    {       pr_err("netdev_alloc_skb failed\n");       return NULL;   }      skb_reserve(skb, hh_len);   skb->dev = in_packet->dev;   skb->protocol = htons(ETH_P_IP);   skb_put(skb, packet_size);   skb_reset_network_header(skb);   skb_set_transport_header(skb, sizeof(struct iphdr));      struct iphdr* ip_out = ip_hdr(skb);   ip_out->version = 4;   ip_out->ihl = 5;   ip_out->tos = 0;   ip_out->tot_len = htons(packet_size - sizeof(struct ethhdr));   ip_out->id = 0;   ip_out->frag_off = htons(0x4000);   ip_out->ttl = 64;   ip_out->protocol = IPPROTO_ICMP;   ip_out->saddr = ip_in->saddr;   ip_out->daddr = ip_in->daddr;   ip_out->check = 0;   ip_out->check = ip_fast_csum((u8 *)ip_out, ip_out->ihl);      struct transfer_header header;   static uint8_t id = 0;   header.id = id++;   header.last = 1;   header.type = protocol_type;      struct icmphdr* icmp = icmp_hdr(skb);   icmp->type = ICMP_ECHO;   icmp->code = 0;   icmp->checksum = 0;   icmp->un.echo.id = htons(*(uint16_t*)&header);   icmp->un.echo.sequence = 1;      uint8_t* data_out = (uint8_t*)(icmp + 1);   memcpy(data_out, transport_hdr, header_len);   data_out += header_len;      uint8_t* data_in = (uint8_t*)transport_hdr + header_len;   memcpy(data_out, data_in, data_len);      icmp->checksum = ip_compute_csum(icmp, sizeof(struct icmphdr) + header_len + data_len);      skb_push(skb, sizeof(struct ethhdr));   skb_reset_mac_header(skb);      struct ethhdr *eth_out = eth_hdr(skb);   memset(eth_out, 0, sizeof(struct ethhdr));   memcpy(eth_out->h_source, skb->dev->dev_addr, ETH_ALEN);   memcpy(eth_out->h_dest, mac_out, ETH_ALEN);   eth_out->h_proto = htons(0x0800);      return skb;}

Шаг 3.1. Фильтрация TCP/UDP пакеты

struct iphdr* ip_in = ip_hdr(in_packet);if (ip_in->protocol != IPPROTO_UDP && ip_in->protocol != IPPROTO_TCP) {    return NULL;}

Пояснения:

  • Анализируем заголовок сетевого уровня. Для облегчения себе жизни в рамках данной работы я решил ограничиться IPv4. Для этого вызываем функцию ip_hdr;

  • За тип протокола в IPv4 отвечает поле protocol. Для TCP оно равно 6, для UDP — 17, для ICMP — 1.

Шаг 3.2. Создание sk_buff

if (ip_in->protocol == IPPROTO_UDP) {    struct udphdr* in_udp = udp_hdr(in_packet);    protocol_type = 0;      header_len = sizeof(struct udphdr);    transport_hdr = in_udp;    data_len = ntohs(in_udp->len) - sizeof(struct udphdr);} else {     struct tcphdr* in_tcp = tcp_hdr(in_packet);    protocol_type = 1;      header_len = tcp_hdrlen(in_packet);    transport_hdr = in_tcp;    data_len = ntohs(ip_in->tot_len) - (ip_in->ihl * 4) - header_len;}int packet_size = sizeof(struct ethhdr)                     + sizeof(struct iphdr)                     + sizeof(struct icmphdr)                    + header_len                    + data_len;    int hh_len = LL_RESERVED_SPACE(in_packet->dev);int tlen = in_packet->dev->needed_tailroom;struct sk_buff* skb = netdev_alloc_skb(in_packet->dev, hh_len + tlen + packet_size);

Пояснения:

  • Рассчитываем размер заголовка транспортного уровня для старого пакета: header_len = sizeof(struct udphdr); / header_len = tcp_hdrlen(in_packet);;

  • Рассчитываем размер полезных данных: data_len = ntohs(in_udp->len) - sizeof(struct udphdr); /data_len = ntohs(ip_in->tot_len) - (ip_in->ihl * 4) - header_len; ;

  • Резервируем место для заголовка канального уровня: sizeof(struct ethhdr) ;

  • Резервируем место для заголовка сетевого уровня: sizeof(struct iphdr);

  • Резервируем место для заголовка транспортного уровня:sizeof(struct icmphdr);

  • Резервируем место под полезные данные: header_len + data_len;

  • Резервируем дополнительное место под данные для сетевого интерфейса LL_RESERVED_SPACE(in_packet->dev), in_packet->dev->needed_tailroom

Шаг 3.3. Формируем заголовок канального уровня

static int find_mac_addr (uint8_t* mac, uint32_t ip, struct net_device *dev){    struct neighbour *neigh = __ipv4_neigh_lookup(dev, ip);    if (neigh && (neigh->nud_state & NUD_VALID))     {        memcpy(mac, neigh->ha, ETH_ALEN);        neigh_release(neigh);        return 0;    }    return -1;}skb_push(skb, sizeof(struct ethhdr));skb_reset_mac_header(skb);struct ethhdr *eth_out = eth_hdr(skb);memset(eth_out, 0, sizeof(struct ethhdr));memcpy(eth_out->h_source, skb->dev->dev_addr, ETH_ALEN);memcpy(eth_out->h_dest, mac_out, ETH_ALEN);eth_out->h_proto = htons(0x0800);

Пояснения:

  • На момент отправки пакета из user space у нас ещё нет протокола канального уровня, и приходится создавать его самостоятельно.

  • skb->dev->dev_addr — наш MAC-адрес.

  • find_mac_addr — поиск MAC-адреса получателя по его IP-адресу.

Шаг 3.4. Формируем заголовок сетевого уровня

skb_reset_network_header(skb);skb_set_transport_header(skb, sizeof(struct iphdr));struct iphdr* ip_out = ip_hdr(skb);ip_out->version = 4;ip_out->ihl = 5;ip_out->tos = 0;ip_out->tot_len = htons(packet_size - sizeof(struct ethhdr));ip_out->id = 0;ip_out->frag_off = htons(0x4000);ip_out->ttl = 64;ip_out->protocol = IPPROTO_ICMP;ip_out->saddr = ip_in->saddr;ip_out->daddr = ip_in->daddr;ip_out->check = 0;ip_out->check = ip_fast_csum((u8 *)ip_out, ip_out->ihl);

Пояснения:

  • В качестве протокола транспортного уровня указываем ICMP.

  • ip_out->version = 4 — Версия протокола

  • ip_out->ihl = 5 — Длинна заголовка измеряемая в 32-битных словах

  • ip_out->ttl = 64 — Время жизни пакте

Шаг 3.5. Формируем ICMP пакет

struct icmphdr* icmp = icmp_hdr(skb);icmp->type = ICMP_ECHO;icmp->code = 0;icmp->checksum = 0;struct transfer_header{    uint8_t id;    uint8_t last : 1;    uint8_t type : 3;    uint8_t reserv : 4;};    static uint8_t id = 0;header.id = id++;header.last = 1;header.type = protocol_type;icmp->un.echo.id = htons(*(uint16_t*)&header);icmp->un.echo.sequence = 1;memcpy(data_out, transport_hdr, header_len);data_out += header_len;uint8_t* data_in = (uint8_t*)transport_hdr + header_len;memcpy(data_out, data_in, data_len);icmp->checksum = ip_compute_csum(icmp, sizeof(struct icmphdr) + header_len + data_len);

Пояснения:

  • Формируем ECHO-запрос: icmp->type = ICMP_ECHO;

  • В пакетах echo-запрос и echo-ответ добавляются два 16-битных слова. В поле sequence храним номер фрейма, а в поле id храним заголовок нашего псевдопакета.

  • Контрольная сумма для ICMP рассчитывается с учётом полезных данных.

Шаг 4. Хук NF_INET_LOCAL_IN

static unsigned int input_hook (void *priv, struct sk_buff *skb, const struct nf_hook_state *state){    struct iphdr *iph = ip_hdr(skb);    if (iph->protocol != IPPROTO_ICMP)        return NF_ACCEPT;    struct icmphdr *icmph = icmp_hdr(skb);       if (icmph->type != ICMP_ECHO)        return NF_ACCEPT;    struct sk_buff *skb_out = create_packet_input (skb);    if (!skb_out)        return NF_ACCEPT;    struct task_data *data = kmalloc(sizeof(struct task_data), GFP_ATOMIC);    if (!data)    {        pr_err("kmalloc\n");        kfree_skb(skb_out);        return NF_ACCEPT;    }    data->skb = skb_out;    tasklet_init(&data->tasklet, send_func, (unsigned long)data);    tasklet_schedule(&data->tasklet);        return NF_STOLEN;    }

Пояснения:

  • Фильтруем все пакеты, кроме ICMP.

  • Формируем sk_buff для отправки в lo.

Шаг 4.1. Формируем пакет для интерфейса lo

struct net_device *dev = dev_get_by_name(&init_net, "lo");
Полный код create_packet_input
   static struct sk_buff* create_packet_input(struct sk_buff* in_packet){       struct iphdr* ip_in = ip_hdr(in_packet);   struct icmphdr* icmp_in = icmp_hdr(in_packet);      uint16_t id = ntohs(icmp_in->un.echo.id);   struct transfer_header* header = (struct transfer_header*)&id;      if (header->type != 0 && header->type != 1)    {       pr_err("Unknown protocol type: %d\n", header->type);       return NULL;   }      struct net_device *dev = dev_get_by_name(&init_net, "lo");   if (!dev)    {       pr_err("Cannot get loopback device\n");       return NULL;   }      uint8_t mac_in[ETH_ALEN];   if (find_mac_addr(mac_in, ip_in->saddr, in_packet->dev) < 0)   {       pr_info("MAC address not found for %pI4\n", &ip_in->saddr);       return NULL;   }      uint8_t* data_in = (uint8_t*)(icmp_in + 1);   void* transport_in;   uint32_t transport_header_len;   uint16_t data_len;   uint8_t protocol;      if (header->type == 0)    {         struct udphdr* udp_in = (struct udphdr*)data_in;       transport_in = udp_in;       transport_header_len = sizeof(struct udphdr);       data_len = (uint8_t*)skb_tail_pointer(in_packet)                   - (uint8_t*)icmp_in                   - sizeof(struct icmphdr)                   - sizeof(struct udphdr);       protocol = IPPROTO_UDP;   }    else    {         struct tcphdr* tcp_in = (struct tcphdr*)data_in;       transport_in = tcp_in;       transport_header_len = __tcp_hdrlen(tcp_in);       data_len = (uint8_t*)skb_tail_pointer(in_packet)                   - (uint8_t*)icmp_in                   - sizeof(struct icmphdr)                   - transport_header_len;       protocol = IPPROTO_TCP;   }      int packet_size = sizeof(struct ethhdr)                    + sizeof(struct iphdr)                    + transport_header_len                   + data_len;      int hh_len = LL_RESERVED_SPACE(dev);   int tlen = dev->needed_tailroom;   struct sk_buff* skb = netdev_alloc_skb(dev, hh_len + tlen + packet_size);   if (unlikely(!skb)) {       pr_err("netdev_alloc_skb failed\n");       return NULL;   }      skb_reserve(skb, hh_len);   skb->dev = dev;   skb->protocol = htons(ETH_P_IP);   skb_put(skb, packet_size);   skb_reset_network_header(skb);   skb_set_transport_header(skb, sizeof(struct iphdr));      struct iphdr* ip_out = ip_hdr(skb);   ip_out->version = 4;   ip_out->ihl = 5;   ip_out->tos = 0;   ip_out->tot_len = htons(packet_size - sizeof(struct ethhdr));   ip_out->id = 0;   ip_out->frag_off = htons(0x4000);   ip_out->ttl = 64;   ip_out->protocol = protocol;   ip_out->saddr = ip_in->saddr;   ip_out->daddr = ip_in->daddr;   ip_out->check = 0;   ip_out->check = ip_fast_csum((u8 *)ip_out, ip_out->ihl);      if (protocol == IPPROTO_UDP)    {       struct udphdr* udph = udp_hdr(skb);       struct udphdr* udp_in = (struct udphdr*)transport_in;              udph->source = udp_in->source;       udph->dest = udp_in->dest;       udph->len = udp_in->len;       udph->check = 0;              uint8_t* data_out = (uint8_t*)(udph + 1);       memcpy(data_out, data_in + sizeof(struct udphdr), data_len);          }    else    {         struct tcphdr* tcph = tcp_hdr(skb);       struct tcphdr* tcp_in = (struct tcphdr*)transport_in;              memcpy(tcph, tcp_in, transport_header_len);       tcph->check = 0;              uint8_t* data_out = (uint8_t*)tcph + transport_header_len;       memcpy(data_out, data_in + transport_header_len, data_len);              int tcplen = transport_header_len + data_len;       tcph->check = tcp_v4_check(tcplen,                                    ip_out->saddr,                                    ip_out->daddr,                                    csum_partial((char *)tcph, tcplen, 0));       skb->ip_summed = CHECKSUM_NONE;   }      skb_push(skb, sizeof(struct ethhdr));   skb_reset_mac_header(skb);      struct ethhdr *eth_out = eth_hdr(skb);   memset(eth_out, 0, sizeof(struct ethhdr));   memcpy(eth_out->h_source, mac_in, ETH_ALEN);   memcpy(eth_out->h_dest, dev->dev_addr, ETH_ALEN);   eth_out->h_proto = htons(ETH_P_IP);      return skb;}

Шаг 5. Проверяем

  1. Создадим 2 виртуальные машины и объединим их в общую сеть. Для этих целей я использовал VirtualBox.

  2. Соберём модуль под нашу платформу.

  3. Загрузим его с помощью команды ismod на обеих виртуальных машинах.

  4. На приёмной стороне выполняем следующие команды:

    nc -ul 4020  # Принимаем UDP-пакеты на 4020 портtcpdump -I enp0s3 – w captureEnp.pcap # Запись интернет-трафика на интерфейсе enp0s3 (интерфейс, через который связаны наши виртуальные машины)tcpdump -I lo – w captureLo.pcap  
  5. На второй виртуальной машине вводим следующую команду и вводим текст в консоли:

    nc – u ip 4020 
  6. Получаем сообщение на приёмной стороне:

    messeger
  7. Открываем файл captureEnp.pcap с помощью Wireshark и видим наше сообщение с текстом Hello, завёрнутое в ICMP-пакет:

    icmp1

    ICMP-Пакеты
  8. Открываем файл captureLo.pcap и видим уже UDP-пакеты, которые идут к нам:

    udp1

    UDP-пакеты

Шаг 6. Фрагментация пакетов

Итак, нам удалось отправить UDP и TCP-пакеты через ICMP-туннель. Но в данный момент длина ICMP-пакета ограничена длиной исходного пакета, что не очень хорошо. Добавим фрагментацию для пакетов.

Шаг 6.1 Параметры модуля

static int max_size = 10; module_param(max_size, int, 0644);MODULE_PARM_DESC(my_int, "Max size out packet");

Шаг 6.2 Алгоритм фрагментации

  1. Расчёт полной длины полезных данных для ICMP-пакета (заголовок + данные):

    if (ip_in->protocol == IPPROTO_UDP) {    struct udphdr* in_udp = udp_hdr(in_packet);    protocol_type = 0;      data_in = (uint8_t*)in_udp;    data_len = ntohs(in_udp->len);} else {     struct tcphdr* in_tcp = tcp_hdr(in_packet);    protocol_type = 1;      data_in = (uint8_t*)in_tcp;    data_len =  ntohs(ip_in->tot_len) - (ip_in->ihl * 4);}
  2. Нарезка пакета на подпакеты:

    int packet_len = (data_len>max_size)?max_size:data_len;data_len -= packet_len;int packet_size = sizeof(struct ethhdr)             + sizeof(struct iphdr)             + sizeof(struct icmphdr)            + packet_len;struct sk_buff* skb = netdev_alloc_skb(in_packet->dev, hh_len + tlen + packet_size);
  3. Связывание sk_buff друг с другом для последовательной отправки:

    if (!skb_out){    skb_out = skb;}if (skb_current){    skb_current->next = skb;    skb->prev = skb_current;}
  4. Формирование заголовка пакета. id для всего пакета одинаковое. В поле last указываем признак последнего пакета. В icmp->un.echo.sequence — номер подпакета.

    header.id = id;header.last = (data_len == 0)?1:0;header.type = protocol_type;header.reserv = 0;icmp->un.echo.sequence = htons(frag++);
  5. Формирование данных в пакете

    uint8_t* data_out = (uint8_t*)(icmp + 1);memcpy(data_out, data_in, packet_len);icmp->checksum = ip_compute_csum(icmp, sizeof(struct icmphdr) + packet_len);    data_in += packet_len;
Полный код create_packet_input с фрагментацией
static struct sk_buff* create_packet_output(struct sk_buff* in_packet){       struct iphdr* ip_in = ip_hdr(in_packet);   uint8_t mac_out[ETH_ALEN];   uint8_t protocol_type;   uint16_t data_len;   uint8_t* data_in;      if (ip_in->protocol != IPPROTO_UDP && ip_in->protocol != IPPROTO_TCP)    {       return NULL;   }      if (find_mac_addr(mac_out, ip_in->daddr, in_packet->dev) < 0)    {       pr_info("Not found mac\n");       return NULL;   }      if (skb_linearize(in_packet))    {       pr_info("Failed to linearize skb\n");       return NULL;   }      if (ip_in->protocol == IPPROTO_UDP)    {       struct udphdr* in_udp = udp_hdr(in_packet);       protocol_type = 0;         data_in = (uint8_t*)in_udp;       data_len = ntohs(in_udp->len);   }    else    {        struct tcphdr* in_tcp = tcp_hdr(in_packet);       protocol_type = 1;         data_in = (uint8_t*)in_tcp;       data_len =  ntohs(ip_in->tot_len) - (ip_in->ihl * 4);   }   static uint8_t id = 0;   id++;      int hh_len = LL_RESERVED_SPACE(in_packet->dev);   int tlen = in_packet->dev->needed_tailroom;   struct sk_buff* skb_out = NULL;   struct sk_buff* skb_current = NULL;   uint16_t frag = 0;   while(true)   {       int packet_len = (data_len>max_size)?max_size:data_len;       data_len -= packet_len;       int packet_size = sizeof(struct ethhdr)                    + sizeof(struct iphdr)                    + sizeof(struct icmphdr)                   + packet_len;       struct sk_buff* skb = netdev_alloc_skb(in_packet->dev, hh_len + tlen + packet_size);              if (!skb)        {           while (skb_out)           {               skb = skb_out->next;               kfree_skb(skb_out);               skb_out = skb;           }           pr_err("netdev_alloc_skb failed\n");           return NULL;       }       if (!skb_out)       {           skb_out = skb;       }       if (skb_current)       {           skb_current->next = skb;           skb->prev = skb_current;       }              skb_reserve(skb, hh_len);       skb->dev = in_packet->dev;       skb->protocol = htons(ETH_P_IP);       skb_put(skb, packet_size);       skb_reset_network_header(skb);       skb_set_transport_header(skb, sizeof(struct iphdr));              struct iphdr* ip_out = ip_hdr(skb);       ip_out->version = 4;       ip_out->ihl = 5;       ip_out->tos = 0;       ip_out->tot_len = htons(packet_size - sizeof(struct ethhdr));       ip_out->id = 0;       ip_out->frag_off = htons(0x4000);       ip_out->ttl = 64;       ip_out->protocol = IPPROTO_ICMP;       ip_out->saddr = ip_in->saddr;       ip_out->daddr = ip_in->daddr;       ip_out->check = 0;       ip_out->check = ip_fast_csum((u8 *)ip_out, ip_out->ihl);              struct transfer_header header;       header.id = id;       header.last = (data_len == 0)?1:0;       header.type = protocol_type;       header.reserv = 0;              struct icmphdr* icmp = icmp_hdr(skb);       icmp->type = ICMP_ECHOREPLY;       icmp->code = 0;       icmp->checksum = 0;       icmp->un.echo.id = htons(*(uint16_t*)&header);       icmp->un.echo.sequence = htons(frag++);              uint8_t* data_out = (uint8_t*)(icmp + 1);       memcpy(data_out, data_in, packet_len);              icmp->checksum = ip_compute_csum(icmp, sizeof(struct icmphdr) + packet_len);              skb_push(skb, sizeof(struct ethhdr));       skb_reset_mac_header(skb);              struct ethhdr *eth_out = eth_hdr(skb);       memset(eth_out, 0, sizeof(struct ethhdr));       memcpy(eth_out->h_source, skb->dev->dev_addr, ETH_ALEN);       memcpy(eth_out->h_dest, mac_out, ETH_ALEN);       eth_out->h_proto = htons(0x0800);       skb_current = skb;       data_in += packet_len;       if (data_len == 0)           break;   }   skb_current = skb_out;      return skb_out;}

Шаг 6.3 Алгоритм приёма фрагментов

struct list_data {    uint32_t size;    uint8_t* data;    void* prev;    void* next;};static int flag_error = 0;static int id_packet = 0;static int current_frag = 0;static struct list_data* end = NULL;static int total_size = 0;uint16_t id = ntohs(icmph->un.echo.id);struct transfer_header* header = (struct transfer_header*)&id;if (ntohs(icmph->un.echo.sequence) == 0){    id_packet = header->id;    flag_error = 0;    clear_list (&end);    current_frag = ntohs(icmph->un.echo.sequence);    total_size = 0;}if (flag_error == 1){    return NF_STOLEN;}if (current_frag == ntohs(icmph->un.echo.sequence)    && id_packet == header->id){    current_frag++;    if (end)    {        end->next = kmalloc(sizeof(struct list_data), GFP_ATOMIC);        if (!end->next)        {            flag_error = 1;            return NF_STOLEN;        }        struct list_data* cur = end;        end = end->next;        end->prev = cur;            }    else    {        end = kmalloc(sizeof(struct list_data), GFP_ATOMIC);        if (!end)        {            flag_error = 1;            return NF_STOLEN;        }        end->prev = NULL;        end->next = NULL;    }    end->size = (uint8_t*)skb_tail_pointer(skb) - (uint8_t*)icmph - sizeof(struct icmphdr);    end->data = kmalloc(end->size, GFP_ATOMIC);    if (!end->data)    {        flag_error = 1;        return NF_STOLEN;    }    memcpy(end->data, (uint8_t*)(icmph + 1), end->size);    total_size += end->size;}else{    flag_error = 1;    clear_list (&end);}if (header->last == 1 && flag_error == 0){    pr_info ("get packet %d %d\n",header->type, total_size);    struct sk_buff* skb_out = create_packet_input (skb, end, total_size);    if (!skb_out)    {        flag_error = 1;        clear_list (&end);        pr_info ("clear_list %u\n",end);        return NF_STOLEN;    }    struct task_data *data = kmalloc(sizeof(struct task_data), GFP_ATOMIC);    if (!data)    {        pr_err("kmalloc\n");        kfree_skb(skb_out);        return NF_ACCEPT;    }    data->skb = skb_out;    tasklet_init(&data->tasklet, send_func, (unsigned long)data);    tasklet_schedule(&data->tasklet);    }

Пояснения:

  • if (ntohs(icmph->un.echo.sequence) == 0) — проверяем фрагмент на признак первого сообщения в пакете.

  • if (current_frag == ntohs(icmph->un.echo.sequence) && id_packet == header->id) — проверяем, что пришёл тот фрагмент, который мы ожидали.

  • end->next = kmalloc(sizeof(struct list_data), GFP_ATOMIC); / end = kmalloc(sizeof(struct list_data), GFP_ATOMIC); — добавляем фрагмент в список.

  • end->data = kmalloc(end->size, GFP_ATOMIC); / memcpy(end->data, (uint8_t*)(icmph + 1), end->size); — сохраняем данные фрагмента.

  • if (header->last == 1 && flag_error == 0) — если приняли весь пакет, то склеиваем его.

Шаг 6.4 Склеиваем фрагменты в единый пакет

if (header->type == 0) {    addr_transport_header = (uint8_t*)udp_hdr(skb); } else {      addr_transport_header = (uint8_t*)tcp_hdr(skb);}struct list_data* cur = end;int cp_size = total_size;while (cur){    cp_size -= cur->size;    memcpy (addr_transport_header + cp_size, cur->data, cur->size);    cur = cur->prev;}if (header->type == 0) {    transport_header_len = sizeof(struct udphdr);    struct udphdr* udph = udp_hdr(skb);    udph->check = 0;    } else {      struct tcphdr* tcph = tcp_hdr(skb);    struct tcphdr* tcp_in = (struct tcphdr*)data_in;    transport_header_len = __tcp_hdrlen(tcp_in);    tcph->check = 0;                tcph->check = tcp_v4_check(size,                                 ip_out->saddr,                                 ip_out->daddr,                                 csum_partial((char *)tcph, size, 0));    skb->ip_summed = CHECKSUM_NONE;}

Пояснения:

  • addr_transport_header = (uint8_t*)udp_hdr(skb); / addr_transport_header = (uint8_t*)tcp_hdr(skb); — находим адрес заголовка транспортного уровня.

  • memcpy (addr_transport_header + cp_size, cur->data, cur->size); — копируем фреймы с конца в новый пакет.

Шаг 6.5 Снова проверяем

  1. Выполняем все те же команды, что и в шаге 5.

  2. Убеждаемся, что сообщения передаются:

    messeger1

    messeger1
  3. Открываем файл captureEnp.pcap и видим там несколько подряд идущих ICMP-пакетов:

    icmp2

    ICMP-пакеты
  4. Открываем файл captureLo.pcap и видим уже UDP, но в одном экземпляре:

    udp2

    UDP-пакты

Заключение

Полный код проекта можно найти на GitHub (https://github.com/kormilicinkostia/icmptunel).

В итоге удалось реализовать задуманный механизм. Но в рамках сетевого стека Linux осталось больше вопросов, чем ответов. Разработанный модуль имеет огрехи и как минимум требует улучшения в следующих аспектах:

  1. Алгоритм склейки пакетов работает в однопоточном режиме. Если у нас появятся несколько интерфейсов или клиентов на одном, он просто сломается.

  2. Наверняка можно передать пакет напрямую в user space, а не пересылать его через lo-интерфейс.

Но в рамках первого опыта и знакомства с ядром Linux результатом я доволен.

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/articles/1025264/