Pull to refresh

Учимся писать модуль ядра (Netfilter) или Прозрачный прокси для HTTPS

Reading time19 min
Views33K
Эта статья нацелена на читателей, которые начинают или только хотят начать заниматься программированием модулей ядра Linux и сетевых приложений. А также может помочь разобраться с прозрачным проксированием HTTPS трафика.

Небольшое оглавление, чтобы Вы могли оценить, стоит ли читать дальше:
  1. Как работает прокси сервер. Постановка задачи.
  2. Клиент – серверное приложение с использованием неблокирующих сокетов.
  3. Написание модуля ядра с использованием библиотеки Netfilter.
  4. Взаимодействие с модулем ядра из пользовательского пространства (Netlink)

P.S. Для тех, кому только хочется посмотреть на прозрачный прокси-сервер для HTTP и HTTPS, достаточно настроить прозрачный прокси-сервер для HTTP, например, Squid с transparent портом 3128, и скачать архив с исходниками Shifter. Скомпилировать (make) и, после удачной компиляции, выполнить ./Start с правами root. При необходимости можно поправить настройки в shifter.h до компиляции.

Постановка задачи


Как и все начинающие в области IT захотелось покопаться немножко в ядре. Тут и область для экспериментов появилась сама собой. Если заглянуть в гугл, то можно заметить, что, если с прозрачным прокси-сервером для HTTP проблем ни у кого не возникает, то в случае с HTTPS некоторые уверены в том, что прозрачного прокси для протокола HTTPS нет. И его никогда не будет. Все это и послужило появлению этой статьи.
Для начала рассмотрим некоторые аспекты работы прокси-сервера, которые нам понадобятся. Когда браузер напрямую обращается к HTTP серверу, то он создает следующий типовой запрос:
GET / HTTP/1.1
Host: www.google.ru
…

Когда в настройках браузера указываем прокси сервер, то браузер начинает соединяться с прокси-сервером, и отсылает ему запрос с полным адресом:
GET http://www.google.ru/ HTTP/1.1
Host: www.google.ru
…

Если прокси сервер получит запрос с неполным адресом, как в первом случае, то он может не знать, кому предназначен этот запрос и возвратит ошибку.
Скажу в двух словах про HTTPS. Браузер устанавливает tcp соединение и, используя протокол SSL: обменивается сертификатами и передает зашифрованный трафик HTTP. Так как протокол SSL как раз направлен на то, чтобы никто не смог посередине прочитать передаваемые данные, то прокси-сервер не может, как в случае HTTP, выяснить с кем следует установить соединение. Для передачи данных по HTTPS через прокси-сервер браузер должен сообщить прокси-серверу с кем он хочет соединиться, используя HTTP метод CONNECT:
CONNECT mail.google.com:443 HTTP/1.1
Host: mail.google.com
…

На что прокси-сервер должен ответить, что соединение успешно установлено:
HTTP/1.0 200 Connection established

В результате браузер получает как бы прямое tcp соединение через прокси-сервер, в котором он может передавать абсолютно любые данные. А прокси-сервер занимается только точной перекладкой данных из tcp соединения, установленного с браузером, в tcp соединение, установленное с указанным хостом в методе CONNECT, и, соответственно, обратной перекладкой.
Когда используется прозрачное проксирование, т.е. браузер даже не подозревает о существование прокси-сервера, то соответственно браузер и не будет так красиво подготавливать свои данные. И прокси-серверу придется самому подумать об этом.
Схематично взглянем на прозрачный прокси для HTTP:

Здесь конечно есть неточности, например, прокси, получая пакет на свой порт 3128, не передает его дальше гуглу, а создает новое соединение с гуглом, но, в общем, схема взаимодействий примерно такова. В данной схеме видно, что NAT начинает направлять пакеты прокси-серверу, которые предназначены не для него, и ему требуется узнать кому их все-таки отсылать. В случае с HTTP трафиком некоторые прокси-сервера неправомерно начинают пользоваться информацией из поля HOST заголовка HTTP запроса, нарушая спецификацию. Чаще всего конечно в HOST содержится именно то имя хоста, которому и адресован запрос, но, в общем случае, HOST может содержать что угодно. Для HTTPS такое решение и вовсе не подходит. Чтобы узнать, кому адресован пакет, сразу напрашивается решение, в котором прокси-сервер заглянул бы в NAT и посмотрел, кому все-таки были предназначены данные и тогда проблем бы ни каких не было. Вот собственно, чем и займемся.
В сам iptables, к сожалению, не полезем, но все равно будут хорошо понятны основные принципы его работы. Схематично это будет выглядеть следующим образом:

Для намеченных целей, будет достаточно написать крохотный модуль ядра (Module Shifter) и реализовать небольшую программную прослойку (Shifter) в пользовательском пространстве для взаимодействия с нашим модулем, которая и будет подготавливать данные для прокси-сервера.

Клиент – серверное приложение (Shifter)


Напишем маленькое клиент-серверное приложение, которое я назвал Shifter. Shifter будет висеть на своем порту и, когда кто-нибудь установит tcp соединение с ним, то он создаст tcp соединение с прокси-сервером и пошлет ему метод CONNECT (HTTP), а далее будет заниматься только точной передачей данных между этими двумя соединениями. В случае если клиент или прокси закроет соединение с ним, то он закроет эту пару сокетов.
Для отправки метода CONNECT нужен ip адрес удаленного узла, с которым на самом деле хотел установить соединение клиент. Чтобы получить эту информацию Shifter будет связываться с модулем ядра (Module Shifter) используя библиотеку Netlink. Об этом будет рассказано в последней части этой статьи.
Данным приложением мы подготовим прокси-сервер к приему данных (HTTPS) от клиента, который ничего не знает о прокси-сервере. Так как имеется много ресурсов, где подробно написано про сокеты, здесь лишь приведу ссылку на часть исходного кода Shifter'а с подробными комментариями.

Kernel module Shifter


Для того чтобы узнать, что такое модуль ядра можно прочитать, например, Работаем с модулями ядра в Linux. Приступим к написанию модуля. Для начала нужно выполнить инициализацию модуля, которая будет происходить один раз, когда модуль будет подгружаться в ядро. А также следует выполнить корректное завершение работы модуля, чтобы не оставлять всякие ненужные следы прошлого присутствия, когда модуль выгрузят из ядра. Для этих целей в библиотеке <linux/module.h>:<linux/init.h> есть два макроса module_init и module_exit, которые в качестве параметра принимают имя функции для этих целей. Также имеются макросы MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_LICENSE, чтобы увековечить свое имя :) Еще есть очень полезная функция вывода int printk(const char *fmt, ...) в библиотеке <linux\kernel.h>:<linux\printk.h>. Она мало чем отличается от обычного printf(..). Так как модуль находится в ядре, а не запущен в консоли, то сообщения соответственно выводятся в логи (для просмотра можно использовать команду dmesg). Сообщения можно выводить различных типов:
#define KERN_EMERG	"<0>"	/* system is unusable			*/
#define KERN_ALERT	"<1>"	/* action must be taken immediately	*/
#define KERN_CRIT	"<2>"	/* critical conditions			*/
#define KERN_ERR	"<3>"	/* error conditions			*/
#define KERN_WARNING	"<4>"	/* warning conditions			*/
#define KERN_NOTICE	"<5>"	/* normal but significant condition	*/
#define KERN_INFO	"<6>"	/* informational			*/
#define KERN_DEBUG	"<7>"	/* debug-level messages			*/
#define KERN_DEFAULT	"<d>"	/* Use the default kernel loglevel 	*/

Теперь у нас имеется вся информация, чтобы написать каркас своего первого модуля:
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_AUTHOR("Denis Dolgikh <sindo@sibmail.com>");
MODULE_DESCRIPTION("Module for the demonstration");
MODULE_LICENSE("GPL");

int Init(void)
{
    printk(KERN_INFO "Init my module\n");
    printk("Hello, World!\n");
    return 0;
}

void Exit(void)
{
    printk(KERN_INFO "Exit my module\n");
}

module_init(Init);
module_exit(Exit);

Расскажу еще немного о том, как это все скомпилировать и запустить. Скажу сразу, что у меня установлена Ubuntu 11.10 (x86) Kernel 3.0.
В системе имеется каталог /lib/modules/[версия ядра]/ в нем находятся модули для соответствующей версии ядра. Также там имеется build – это символическая ссылка на заголовки библиотек ядра (kernel-headers), которые находятся в каталоге /usr/src/linux-headers-[версия ядра]/. Если у вас еще нет kernel-headers, то их нужно скачать (sudo apt-get install linux-headers-[версия ядра]). Чтобы узнать версию текущего ядра, в котором Вы работаете, можно выполнить команду uname –r. Если будете использовать библиотеку с версией отличной от версии текущего ядра, то такой скомпилированный модуль может замечательно работать, а может в лучшем случае не запуститься. Это все зависит от того, какие произошли изменения в ядре, и какие Вы используете в модуле функции.
Для компиляции модуля напишем Makefile. Создадим две цели: all (сборка модуля) и clean (очистка проекта), для их написания воспользуемся уже написанными для нас целями: modules и clean в Makefile из kernel-headers.
# Добавляем модуль к списку модулей для компилирования
# module_test.o – означает, что модуль нужно собрать автоматически из module_test.с
obj-m += module_test.o

# Подключаем Makefile из kernel-headers с помошью -С,
# который лежит в каталоге /lib/modules/[версия ядра]/build
# используем из него цель modules для сборки модулей в списке obj-m
# M=$(PWD) передаем текущий каталог, где лежат исходники нашего модуля
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Теперь можем легко скомпилировать модуль с помощью команды make и очистить проект make clean. В результате получим готовый модуль module_test.ko, который можно сразу загрузить в ядро, используя команду insmod ./module_test.ko. Есть еще один вариант загрузки модуля в ядро — командой modprobe module_test. Для этого нужно положить модуль module_test.ko в каталог /lib/modules/[текущая версия ядра]/[любой каталог]/ и не забыть выполнить depmod, т.к. эта команда создает список зависимостей модулей в /lib/modules/. Даже если у модуля нет зависимостей, modprobe не увидит добавленный модуль без depmod. Основные различия между insmod и modprobe в следующем: modprobe при загрузке модуля автоматически загружает все модули, от которых он зависит, зато insmod может подгружать модуль из любого каталога. Удалить модуль из ядра: rmmod module_test. Посмотреть информацию о модуле: modinfo module_test или modinfo ./module_test.ko.

Библиотека Netfilter

И так вернемся к прокси для HTTPS. Наша задача — сделать так, чтобы модуль просматривал сетевые пакеты до того, как их обработает DNAT (iptables). Для этого нам поможет библиотека Netfilter. (Для более детального понимания рекомендую дополнительно прочитать о Netfilter в других источниках, хотя бы в Википедии) Рассмотрим, как выглядит путь сетевого пакета в ядре:

Более подробно можно посмотреть вот здесь.
Netfilter <linux/netfilter.h> предоставляет 5 hook функций, которые дают доступ к сетевому пакету в 5 различных местах:
  1. NF_INET_PRE_ROUTING – функция ловит абсолютно все входные пакеты, до этого пакеты уже прошли простые проверки (пакеты не потеряны, IP checksum в порядке и т.д.);
    Далее пакет проходит через маршрутизацию, которая решает, предназначен ли пакет для другого интерфейса или локального процесса. Маршрутизация может отбрасывать пакет, если он не поддается маршрутизации.
  2. NF_INET_LOCAL_IN – вызывается, если пакет предназначен для локального процесса, перед передачей пакета ему;
  3. NF_INET_FORWARD – когда пакет маршрутизируется с одного интерфейса на другой;
  4. NF_INET_LOCAL_OUT – ловушка для пакетов которые создают локальные процессы;
  5. NF_INET_POST_ROUTING – заключительная точка прежде, чем послать пакет драйверу сетевой карты.

Модуль ядра может зарегистрировать свою функцию в любом из этих 5 мест. При регистрации модуль должен указать приоритет своей функции в этом месте. В библиотеке <linux/netfilter_ipv4.h> можно найти приоритет для различных стандартных задач:
enum nf_ip_hook_priorities {
	NF_IP_PRI_FIRST = INT_MIN,
	NF_IP_PRI_CONNTRACK_DEFRAG = -400,
	NF_IP_PRI_RAW = -300,
	NF_IP_PRI_SELINUX_FIRST = -225,
	NF_IP_PRI_CONNTRACK = -200,
	NF_IP_PRI_MANGLE = -150,
	NF_IP_PRI_NAT_DST = -100,
	NF_IP_PRI_FILTER = 0,
	NF_IP_PRI_SECURITY = 50,
	NF_IP_PRI_NAT_SRC = 100,
	NF_IP_PRI_SELINUX_LAST = 225,
	NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
	NF_IP_PRI_LAST = INT_MAX,
};

Из списка видно, что DNAT имеет приоритет -100, поэтому для нашей цели подойдет любой приоритет < -100. Если же приоритет у функции будет > -100, то она будет получать пакеты с уже измененным IP адресом назначения (получателя).
Каждая зарегистрированная функция в любой из 5 точек должна возвращать одно из следующих значений, которое определяет дальнейшую судьбу переданного ей пакета:
#define NF_DROP 0	/* discarded the packet */
#define NF_ACCEPT 1	/* the packet passes, continue iterations */
#define NF_STOLEN 2	/* gone away */
#define NF_QUEUE 3	/* inject the packet into a different queue (the target queue number is in the high 16 bits of the verdict) */
#define NF_REPEAT 4	/* iterate the same cycle once more */
#define NF_STOP 5	/* accept, but don't continue iterations */

В моем вольном переводе это будет звучать так:
  • NF_DROP – удалить этот пакет
  • NF_ACCEPT – пакет проходит дальше, продолжаются итерации
  • NF_STOLEN – бросить этот пакет (ядро его больше не будет обрабатывать, модуль должен сам освободить память выделенную под этот пакет)
  • NF_QUEUE – поставить пакет в очередь (обычно для обработки пакета в пользовательском пространстве)
  • NF_REPEAT – повторить итерацию (повторный вызов функции с этим же пакетом)
  • NF_STOP – пропускать пакет дальше, но итерации не продолжать
В данном случае под итерацией понимается переход пакета от функции к функции, т.к. в одной точке может быть зарегистрировано много функций, которые в свою очередь могут быть разделены по приоритетам.

С теорией разобрались, теперь продолжим писать модуль. При загрузке модуля в ядро нужно зарегистрировать функцию, назовем ее Hook_Func, которая будет просматривать все входящие пакеты, а при завершении эту функцию надо разрегистрировать, иначе ядро будет пытаться вызывать несуществующую функцию.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

/* Структура для регистрации функции перехватчика входящих ip пакетов */
struct nf_hook_ops bundle;

int Init(void)
{
    printk(KERN_INFO "Start module Shifter\n");

    /* Заполняем структуру для регистрации hook функции */
    /* Указываем имя функции, которая будет обрабатывать пакеты */
    bundle.hook = Hook_Func;
    /* Устанавливаем указатель на модуль, создавший hook */
    bundle.owner = THIS_MODULE;
    /* Указываем семейство протоколов */
    bundle.pf = PF_INET;
    /* Указываем, в каком месте будет срабатывать функция */
    bundle.hooknum = NF_INET_PRE_ROUTING;
    /* Выставляем самый высокий приоритет для функции */
    bundle.priority = NF_IP_PRI_FIRST;
    /* Регистрируем */
    nf_register_hook(&bundle);
    return 0;
}

void Exit(void)
{
    /* Удаляем из цепочки hook функцию */
    nf_unregister_hook(&bundle);
    printk(KERN_INFO "End module Shifter\n");
}

module_init(Init);
module_exit(Exit);

Теперь все что нам осталось — это написать саму функцию Hook_Func. Она должна иметь следующий прототип:
unsigned int Hook_Func(uint hooknum,
                  struct sk_buff *skb,
                  const struct net_device *in,
                  const struct net_device *out,
                  int (*okfn)(struct sk_buff *)  )
{
	/* Получаем очень надежный firewall */
	/* который будет удалять (блокировать) абсолютно все входящие пакеты :) */
	return NF_DROP;
}

Рассмотрим ее параметры:
  • uint hooknum содержит одно из следующих значений: {NF_INET_PRE_ROUTING = 0, NF_INET_LOCAL_IN = 1, NF_INET_FORWARD = 2, NF_INET_LOCAL_OUT = 3, NF_INET_POST_ROUTING = 4}. Этот параметр нужен, чтобы узнать из какого места вызвали функцию, т.к. можно зарегистрировать одну и ту же функцию в нескольких местах.
  • struct sk_buff *skb указатель на структуру пакета.
  • const struct net_device *in информация о входном интерфейсе. Если это исходящий пакет, тогда параметр равен NULL.
  • const struct net_device *out информация о выходном интерфейсе. Если это входящий пакет, тогда параметр равен NULL.
  • int (*okfn)(struct sk_buff *) — callback функция, которая вызывается с пакетом, когда все итерации вернут положительный ответ.

Теперь подробнее об указателе на пакет struct sk_buff *skb.
sk_buff – это буфер для работы с пакетами. Как только приходит пакет или появляется необходимость его отправить, создается sk_buff, куда и помещается пакет, а также сопутствующая информация, откуда, куда, для чего… На протяжении всего путешествия пакета в сетевом стеке используется sk_buff. Как только пакет отправлен, или данные переданы пользователю, структура уничтожается, тем самым освобождая память.
Cтруктура sk_buff описывается в <linux/skbuff.h>, там же описаны еще различные функции для приятной работы с ней.
При работе с tcp можно воспользоваться еще двумя структурами — это struct iphdr и struct tcphdr.
Как видно из названия, эти структуры предназначены для работы с IP заголовком и, соответственно, с TCP заголовком. Чтобы получить указатели на эти структуры из skb_buff можно воспользоваться двумя функциями:
static inline unsigned char *skb_network_header(const struct sk_buff *skb);
static inline unsigned char *skb_transport_header(const struct sk_buff *skb);

В этом месте я хотел бы обратиться к читателю. Мне осталось не совсем понятным, кто именно должен устанавливать указатель transport_header в структуре sk_buff, т.к. в точках NF_INET_PRE_ROUTING и NF_INET_LOCAL_IN (с любыми приоритетами) мне не удалось с помощью skb_transport_header получить структуру на tcp заголовок, хотя в остальных точках это работало прекрасно. Пришлось вручную указывать смещение для transport_header от указателя sk_buff->data, воспользовавшись void skb_set_transport_header(skb, offset).
Упомянутый указатель sk_buff->data — это указатель на содержимое пакета, т.е. указывает на область памяти после Ethernet протокола, например, сразу на структуру IP заголовка, а после нее может следовать структура TCP заголовка или Ваш собственный протокол.
Так как везде используются указатели на данные в самом пакете, то можно не только читать различные поля, но и изменять их. Однако нужно помнить, что при изменении, например, IP адреса отправителя или получателя следует еще пересчитать контрольную сумму в заголовке IP пакета.
И так, наша функция будет сохранять IP адрес назначения только тогда, когда она увидит IP-TCP пакет, идущий на порт 443 (HTTPS) и содержащий флаг SYN, который говорит, что клиент хочет установить TCP соединение для протокола HTTPS. И удалять, когда появляются пакеты содержащие флаги FIN или RST, которые сообщают что tcp соединение разорвано и больше этот IP адрес нам не нужен.
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#define uchar unsigned char
#define ushort unsigned short
#define uint unsigned int

/* Hook_Func - функция, которая будет просматривать все входящие пакеты */
/* Запоминает IP адрес получателя, если пакет: */
/* - является tcp пакетом */
/* - идет на 443 порт (HTTPS) */
/* - содержит флаг SYN (установка tcp соединения) */
uint Hook_Func(uint hooknum,
                  struct sk_buff *skb,
                  const struct net_device *in,
                  const struct net_device *out,
                  int (*okfn)(struct sk_buff *)  )
{
    /* Указатель на структуру заголовка протокола ip в пакете */
    struct iphdr *ip;
    /* Указатель на структуру заголовка протокола tcp в пакете */
    struct tcphdr *tcp;

    /* Проверяем что это IP пакет */
    if (skb->protocol == htons(ETH_P_IP))
    {
	/* Сохраняем указатель на структуру заголовка IP */
	ip = (struct iphdr *)skb_network_header(skb);
	/* Проверяем что это IP версии 4 и внутри TCP пакет */
	if (ip->version == 4 && ip->protocol == IPPROTO_TCP)
	{
	    /* Задаем смещение в байтах для указателя на TCP заголовок */
	    /* ip->ihl - длина IP заголовка в 32-битных словах */
	    skb_set_transport_header(skb, ip->ihl * 4);
	    /* Сохраняем указатель на структуру заголовка TCP */
	    tcp = (struct tcphdr *)skb_transport_header(skb);
	    /* Если пакет идет на 443 порт (HTTPS) */
	    if (tcp->dest == htons(443))
	    {
		/* Если выставлен флаг SYN, то сохраняем IP адрес назначения */
		if (tcp->syn)
		    AddTable((uint)ip->saddr, (ushort)tcp->source, (uint)ip->daddr);
		/* Если имеется флаг FIN или RST, то можно удалить IP адрес назначения */
		if (tcp->fin || tcp->rst)
		    DelTable((uint)ip->saddr, (ushort)tcp->source, (uint)ip->daddr);
	    }
	}
    }
    /* Пропускаем дальше все пакеты */
    return NF_ACCEPT;
}

Здесь есть две функции AddTable и DelTable, которые должны сохранять и удалять IP адрес получателя из памяти, для каждого IP и порта отправителя. Это нужно для того, чтобы клиент-сервер Shifter смог связаться с модулем Shifter и воспользовавшись функцией ReadTable, узнать по IP и порту клиента с каким IP адресом на самом деле хотел он связаться. Я не стал сильно раздумывать над типом данных для сохранения IP и воспользовался обычным статическим массивом с использованием элементарной хэш-функции. Хэш-функция (KeyHash) получает на входе ip и порт отправителя и возвращает индекс массива, где хранится ip адрес назначения. Она написана с учетом того, что клиент находится за натом и имеет подсеть с маской 255.255.255.0, поэтому я использую только последний байт ip отправителя, и еще этот байт накладывается 3 битами на два байта порта. В результате мне удалось сжать массив до размера 0x1FFFFF (~8 Мб). Конечно, нужно учитывать, что теперь после загрузки в ядро этот модуль будет занимать не меньше 8 Мб памяти, а это может оказаться слишком много для каких-нибудь встраиваемых систем. И еще не забываем про коллизию :) Но для моего демонстрационного примера все это окупается простотой и, вдобавок, DelTable вообще осталась пустой.
/* Статический массив для хранения IP адреса получателя */
#define MaxTable 0x1FFFFF
uint Table[MaxTable];

/* KeyHash - возвращает индекс в массиве */
/* который соответсвует заданным IP и Порту отправителя */
uint KeyHash(uint src_IP, ushort src_Port)
{
    return (uint)(((src_IP & 0xFF000000) >> 11) ^ (uint)src_Port) % MaxTable;
}

/* AddTable - добавляет IP адрес получателя в таблицу */
/* который будет соответствовать заданным IP и Порту отправителя */
void AddTable(uint src_IP, ushort src_Port, uint dst_IP)
{
    Table[KeyHash(src_IP, src_Port)] = dst_IP;
}

void DelTable(uint src_IP, ushort src_Port, uint dst_IP)
{
    /* Если вы используете динамически выделяемую память для хранения IP получателя */
    /* то здесь можно ее удалять */
}

/* ReadTable - возвращает IP адрес получателя */
/* который соответствует заданным IP и Порту отправителя */
uint ReadTable(uint src_IP, ushort src_Port)
{
    return Table[KeyHash(src_IP, src_Port)];
}

На этом эта длинная часть статьи закончена и осталось только связать клиент-сервер Shifter c модулем ядра Shifter.

Взаимодействие с модулем ядра из пользовательского пространства (Netlink)


Теперь наша цель создать по одному сокету в Shifter и в модуле Shifter и соединить их между собой. Протокол обмена между модулем и сервером Shifter будет простым. Shifter будет отсылать 4 байта IP клиента и 2 байта Порт клиента, а модуль будет отвечать 4 байтами IP назначением, взятым из своей таблицы. Для этого воспользуемся библиотекой Netlink.
Хочу обратить внимание, что заголовок <linux/netlink.h> для пользовательских приложений /usr/include/linux/netlink.h и для модулей ядра /usr/src/linux-headers-[версия ядра]/include/linux/netlink.h имеет множество различий.

Netlink in user space

Что касается netlink со стороны пользовательского пространства в сети много информации, например вот:
RFC 3549 — Linux Netlink как протокол для служб IP
Работа с NetLink в Linux. Часть 1
Простой монитор сетевых интерфейсов Linux, с помощью netlink
Поэтому здесь я расскажу только то, что нам понадобится, чтобы обеспечить обмен информацией между модулем ядра и программами пользовательского пространства.
Netlink сокет создается привычной функцией: int socket(PF_NETLINK, socket_type, netlink_family);
Где в качестве socket_type могут использоваться как SOCK_RAW, так и SOCK_DGRAM; несмотря на это, протокол netlink не проводит границы между датаграммными и raw-сокетами. А netlink_family выбирает модуль ядра или группу netlink для связи. В <linux/netlink.h> можно посмотреть полный список семейств.
Сообщения netlink представляют собой поток байтов с одним или несколькими заголовками nlmsghdr (netlink message header). Для доступа к байтовым потокам следует использовать только макросы NLMSG_*. Хочу еще обратить внимание, что протокол netlink не обеспечивает гарантированной доставки сообщений. При нехватке памяти или возникновении иных ошибок протокол может отбрасывать пакеты.
Рассмотрим структуры sockaddr_nl, nlmsghdr (<linux/netlink.h>), iovec и msghdr (<sys/socket.h>), с которыми будем работать:

struct sockaddr_nl — описывает адреса netlink для пользовательских программ и модулей ядра. Структура используется для описания отправителя или получателя данных.
struct sockaddr_nl
{
	sa_family_t	nl_family;	/* семейство протоколов AF_NETLINK */
	unsigned short	nl_pad;		/* заполнение нулями */
	__u32		nl_pid;		/* содержит идентификатор процесса или 0, если сообщение адресовано ядру, либо отправитель является ядром */
       	__u32		nl_groups;	/* netlink имеет 32 multicast-группы. nl_groups содержит битовую маску групп, которым следует слышать сообщение по умолчанию установлено нулевое значение, которое отключает групповую передачу сообщений */
};

struct nlmsghdr – заголовок сообщения netlink и сразу за структурой в памяти расположены отправляемые/принимаемые данные, для доступа к ним используйте NLMSG_DATA(struct nlmsghdr *) макрос.
struct nlmsghdr
{
	__u32		nlmsg_len;	/* размер сообщения с учетом заголовка */
	__u16		nlmsg_type;	/* тип сообщения (содержимое) */
	__u16		nlmsg_flags;	/* стандартные и дополнительные флаги */
	__u32		nlmsg_seq;	/* порядковый номер (сообщение может быть разбито на несколько структур) */
	__u32		nlmsg_pid;	/* идентификатор процесса (PID), открывшего сокет */
};

struct iovec — находится в msghdr, в ней будет содержаться указатель на структуру nlmsghdr.
struct iovec
{
    void *		iov_base;	/* указатель на данные (указатель на nlmsghdr) */
    size_t 		iov_len;	/* длина (размер) данных */
};

struct msghdr – содержит указатель на адрес (sockaddr_nl) и данные (iovec)
struct msghdr
{
 	void *		msg_name;	/* адрес отправителя или получателя  */
	socklen_t	msg_namelen;	/* длина адреса */

	struct iovec *	msg_iov;	/* указатель на данные  */
	size_t		msg_iovlen;	/* длинна данных */

	void *		msg_control;	/* буфер для разнообразных вспомогательных  данных */
	size_t		msg_controllen;	/* длина буфера вспомогательных данных */

	int		msg_flags;	/* флаги принятого сообщения */
};

Рассмотрим некоторые макросы Netlink:
int NLMSG_ALIGN(size_t len);
Округляет размер сообщения netlink до ближайшего большего значения, выровненного по границе.

int NLMSG_LENGTH(size_t len);
Принимает в качестве параметра размер поля данных и возвращает выровненное по границе значение размера для записи в поле nlmsg_len заголовка nlmsghdr.

int NLMSG_SPACE(size_t len);
Возвращает размер памяти (в байтах), который займет структура nlmsghdr плюс данные указанной длины len (в байтах) в пакете netlink.

void *NLMSG_DATA(struct nlmsghdr *nlh);
Возвращает указатель на данные, связанные с переданным заголовком nlmsghdr.

struct nlmsghdr *NLMSG_NEXT(struct nlmsghdr *nlh, int len);
Возвращает следующую часть сообщения, состоящего из множества частей. Макрос принимает следующий заголовок nlmsghdr в сообщении, состоящем из множества частей. Вызывающее приложение должно проверить наличие в текущем заголовке nlmsghdr флага NLMSG_DONE – функция не возвращает значение NULL при завершении обработки сообщения. Второй параметр задает размер оставшейся части буфера сообщения. Макрос уменьшает это значение на размер заголовка сообщения.

int NLMSG_OK(struct nlmsghdr *nlh, int len);
Возвращает значение TRUE (1), если сообщение не было усечено и его разборка прошла успешно.

int NLMSG_PAYLOAD(struct nlmsghdr *nlh, int len);
Возвращает размер данных, связанных с заголовком nlmsghdr.

Часть исходного кода сервера Shifter с Netlink.

Netlink Kernel Space

При использовании библиотеки netlink в модулях ядра есть некоторые отличия, например, структура nlmsghdr из <linux/netlink.h> остается той же, но уже заворачивается в хорошо знакомую нам структуру sk_buff. И вместо привычных функций для работы с сокетами будем использовать новый набор функций. Рассмотрим некоторые из них.
В модуле сокет представляется не типом int, а структурой sock из <net/sock.h>. struct sock – очень большая я не буду ее описывать, к тому же ее описание и не понадобиться.
Для создания netlink сокета в <linux/netlink.h> имеется функция netlink_kernel_create. Она не только создает netlink сокет, но и регистрирует функцию, которая будет вызываться всякий раз, когда поступят данные.
struct sock *netlink_kernel_create(
	struct net *net,
	int unit,
	unsigned int groups,
	void (*input)(struct sk_buff *skb),
	struct mutex *cb_mutex,
	struct module *module);

Чтобы закрыть netlink сокет и удалить «регистрацию функции» воспользуйтесь:
void netlink_kernel_release(struct sock *sk);

Еще нам понадобятся функции из <net/netlink.h>, там вы можете найти функции с изумительным описанием.
Я просто сделаю перевод некоторых нужных функций:
/**
 * nlmsg_new – выделяет память для нового netlink сообщения
 * @payload: размер данных сообщения
 * @flags: тип памяти для выделения
 *
 * Используйте NLMSG_DEFAULT_SIZE, если размер данных не известен
 */
static inline struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
{
	return alloc_skb(nlmsg_total_size(payload), flags);
}
/**
 * nlmsg_put - Добавляет новое сообщение NetLink в skb пакет
 * @skb: буфер сетевого пакета для netlink сообщения
 * @pid: идентификатор процесса
 * @seq: порядковый номер сообщения
 * @type: тип сообщения
 * @payload: длина передаваемых данных (полезных данных)
 * @flags: флаги сообщений
 *
 * Возвращает NULL, если для skb пакета выделено меньше памяти,
 * чем необходимо для размещения заголовка и данных netlink сообщения,
 * иначе указатель на netlink сообщение
 */
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq,
					 int type, int payload, int flags)
/**
 * nlmsg_unicast – передает индивидуальное netlink сообщение
 * @sk: netlink сокет
 * @skb: сетевой пакет с netlink сообщением
 * @pid: netlink идентификатор сокета назначения
 */
static inline int nlmsg_unicast(struct sock *sk, struct sk_buff *skb, u32 pid)

/**
 * nlmsg_data – возвращает указатель на полезные данные
 * @nlh: заголовок netlink сообщение
 */
static inline void *nlmsg_data(const struct nlmsghdr *nlh)
{
	return (unsigned char *) nlh + NLMSG_HDRLEN;
}

Осталось дописать Module Shifter.

Заключение


В заключение скажу, чтобы перенаправить пакеты на прокси-сервер, достаточно на шлюзе добавить правило в iptables:
# для таблицы nat добавить правило (-A) в точку PREROUTING
# для протокола tcp и порта назначения 443 сделать редирект на 443 порт
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 443

Скачать архив с исходниками Shifter
Конец.


Используемая и полезная литература


NetFilter.org
Linux, Kernel, Firewall
Kernel Korner — Why and How to Use Netlink Socket
RFC 3549 — Linux Netlink как протокол для служб IP
Работа с NetLink в Linux. Часть 1
Протокол netlink в Linux
Tags:
Hubs:
+54
Comments20

Articles

Change theme settings