Недавно я наткнулся на статью о том, что в ICMP-пакеты можно вставлять произвольные данные. Сразу возникла мысль: а почему бы не попробовать загнать весь трафик через ICMP (да, о существовании ICMP-туннеля я тоже ничего не знал). Так появился проект — ICMP-туннель на уровне ядра, который:
-
перехватывает исходящие TCP/UDP-пакеты;
-
инкапсулирует их в ICMP эхо-запросы (тип ICMP_ECHO);
-
на приёмной стороне извлекает оригинальные пакеты и передаёт их дальше.
Цель проекта — углубить знания в:
-
разработке модулей ядра (работа с sk_buff, хуки Netfilter, тасклеты). На данный момент я работал с Linux только из user space;
-
сетевом стеке (IPv4, Ethernet, ICMP, TCP, UDP) и, в частности, его реализации в Linux. До этого моя работа с сетевыми протоколами ограничивалась сокетами;
-
механизмах перехвата и модификации пакетов.
Архитектура проекта
-
Хук
NF_INET_POST_ROUTING— для пакетов, которые покинули пространство user space и готовы отправиться по сети на другую машину:-
фильтрует TCP/UDP-трафик;
-
преобразует пакет в ICMP. В результате преобразования должен получиться пакет следующего вида:
Пакет данных
-
-
Хук
NF_INET_LOCAL_IN— для пакетов, которые прошли все сетевые фильтры и готовы отправиться в user space:-
перехватывает входящие ICMP-пакеты;
-
извлекает оригинальный TCP/UDP-пакет;
-
направляет его в loopback-интерфейс (lo) для дальнейшей обработки системой.
-
-
Тасклет для отправки пакета в сетевой интерфейс.
Шаг 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. Проверяем
-
Создадим 2 виртуальные машины и объединим их в общую сеть. Для этих целей я использовал VirtualBox.
-
Соберём модуль под нашу платформу.
-
Загрузим его с помощью команды
ismodна обеих виртуальных машинах. -
На приёмной стороне выполняем следующие команды:
nc -ul 4020 # Принимаем UDP-пакеты на 4020 портtcpdump -I enp0s3 – w captureEnp.pcap # Запись интернет-трафика на интерфейсе enp0s3 (интерфейс, через который связаны наши виртуальные машины)tcpdump -I lo – w captureLo.pcap -
На второй виртуальной машине вводим следующую команду и вводим текст в консоли:
nc – u ip 4020 -
Получаем сообщение на приёмной стороне:

-
Открываем файл captureEnp.pcap с помощью Wireshark и видим наше сообщение с текстом Hello, завёрнутое в ICMP-пакет:
ICMP-Пакеты -
Открываем файл captureLo.pcap и видим уже UDP-пакеты, которые идут к нам:
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 Алгоритм фрагментации
-
Расчёт полной длины полезных данных для 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);} -
Нарезка пакета на подпакеты:
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); -
Связывание
sk_buffдруг с другом для последовательной отправки:if (!skb_out){ skb_out = skb;}if (skb_current){ skb_current->next = skb; skb->prev = skb_current;} -
Формирование заголовка пакета.
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++); -
Формирование данных в пакете
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 Снова проверяем
-
Выполняем все те же команды, что и в шаге 5.
-
Убеждаемся, что сообщения передаются:
messeger1 -
Открываем файл captureEnp.pcap и видим там несколько подряд идущих ICMP-пакетов:
ICMP-пакеты -
Открываем файл captureLo.pcap и видим уже UDP, но в одном экземпляре:
UDP-пакты
Заключение
Полный код проекта можно найти на GitHub (https://github.com/kormilicinkostia/icmptunel).
В итоге удалось реализовать задуманный механизм. Но в рамках сетевого стека Linux осталось больше вопросов, чем ответов. Разработанный модуль имеет огрехи и как минимум требует улучшения в следующих аспектах:
-
Алгоритм склейки пакетов работает в однопоточном режиме. Если у нас появятся несколько интерфейсов или клиентов на одном, он просто сломается.
-
Наверняка можно передать пакет напрямую в user space, а не пересылать его через lo-интерфейс.
Но в рамках первого опыта и знакомства с ядром Linux результатом я доволен.
Полезные ссылки
ссылка на оригинал статьи https://habr.com/ru/articles/1025264/