惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
WordPress大学
WordPress大学
U
Unit 42
I
InfoQ
A
About on SuperTechFans
宝玉的分享
宝玉的分享
J
Java Code Geeks
博客园 - 司徒正美
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
G
Google Developers Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
腾讯CDC
Recent Announcements
Recent Announcements

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
ICMP-туннель на уровне ядра Linux: передаём TCP/UDP-трафи...
kormilicinko · 2026-04-19 · via Все публикации подряд на Хабре

Средний

16 мин

1.1K

Недавно я наткнулся на статью о том, что в 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 результатом я доволен.

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