Пользователь
0,0
рейтинг
16 февраля 2012 в 17:08

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

Эта статья нацелена на читателей, которые начинают или только хотят начать заниматься программированием модулей ядра 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
Долгих Денис @sindo
карма
20,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (20)

  • +2
    Очень объемно и интересно — положил в закладки и избранное(внимательно читать буду дома).
    пс:
    Вопрос для меня весьма актуален: как раз сейчас стоит задача пробросить https траффик прозрачно через squid.
    • 0
      На сколько я помню, в ранних версия у SQUID была возможность проксировать HTTPS, которую закрыли позже из-за возможной атаки man-in-middle.
      • 0
        Вы имеете ввиду способ описанный на opennet-е?
        через генерацию ключей на самом proxy-сервере
        openssl genrsa -out /etc/squid/ssl/squid.key
        openssl req -new -key /etc/squid/ssl/squid.key -out /etc/squid/ssl/squid.csr
        openssl x509 -req -days 3650 -in /etc/squid/ssl/squid.csr -signkey /etc/squid/ssl/squid.key -out /etc/squid/ssl/squid.pem


        с последующим указанием их suid-у

        https_port 192.168.0.254:3129 transparent key=/etc/squid/ssl/squid.key cert=/etc/squid/ssl/squid.pem
  • +1
    Пример работы с ядром интересный, правда на практике лучше просто маршрутизировать трафик куда надо, минуя squid. Все равно он (squid) с HTTPS соединениями ничего полезного, например фильтрации и кэширования, сделать не сможет.
    • 0
      Может подскажите про фильтрацию https: какие-нибудь интересные материалы?
      • +1
        Речь как раз о том что нечего его гнать через прокси, все равно толку не будет :)
        • 0
          Через прокси его удобно гнать в том случае, если подсчёт трафика ведётся анализом access-лога squid.
    • 0
      Полностью поддерживаю. Я вообще не любитель прокси серверов, особенно когда их использует в не нужных местах. Но бывают случаи, когда это может оказаться полезным. У меня на работе действует прокси с ntlm авторизацией и, чтобы избавится от него, появилась идея организовать свой прозрачный прокси перед ним. Но в итоге прокинул openvpn через этот прокси до домашнего маршрутизатора, а идея с прозрачным прокси для HTTPS вылилась в учебную статью для написания модулей :)
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Как обучалка написанию модулей для линукса — неплохо. Подробно и интересно написано.

    Но практическая ценность сомнительна.
    1) В линуксе для getsockopt существует опция SO_ORIGINAL_DST которая как раз позволяет получить первоначальный адрес сервера до перенаправления.
    2) Для IPv6 -j REDIRECT не работает, т.к. REDIRECT это NAT который для IPv6 не реализован (хотя и существуют пока незавершенные проекты по устранению этого недочета).
    Так вот из-за этого для перехвата IPv6 вместо REDIRECT используют таргет TPROXY который вобщем-то по сути тоже перехватывает соединение, но не через NAT а просто внаглую безо всяких преобразований адресов направляет в локальный слушающий сокет (специальным образом открытый с использованием опции IP_TRANSPARENT).
    И поэтому получение первоначального конечного адреса для схемы с TPROXY тривиально — просто получить локальный адрес сокета.
    Да и ничего не мешает использовать TPROXY для IPv4.

    Т.е. как минимум две штатные для линукса схемы позволяют организовать прозрачное проксирование без написания своих модулей ядра. И squid например поддерживает обе.
    • 0
      Нереализованность NAT в IPv6 не есть недочёт, просто он в большинстве случаев либо не нужен, либо вреден.
      • 0
        Ну так то ж в большинстве случаев.
        А вот для оставшегося меньшинства — недочет.
        • 0
          Ну, под меньшинством, в общем-то, я подразумеваю port redirect, который иногда бывает полезен. Всё остальное (anarsoul, привет!), как правило, приводит к плохим последствиям.
          • 0
            Есть куча возможностей которые могут привести к плохим последствиям.
            Но это не значит что у нас не должно быть выбора.
  • 0
    Учимся прогить это всё конечно хорошо…
    Но, опачки лезем ни куда-нибудь а в доку по Squid… Profit.
    Пример статьи на тему, описано всё примитивным языком, а в дополнении с оф докой — подробнее уже некуда.
    • 0
      вы собствено всю малину и обосрали =)
      • 0
        Надо же открыть глаза публике — что squid мощная штука и ограничили его разработчики намеренно — MITM их не прельщает ;-)
    • +1
      В приведенной статье не много не то, там осуществляется атака человек по середине. В результате браузер будет получать сертификат не гугла, а прокси сервера. Что выглядело бы весьма подозрительно, как для браузера, так и для пользователя :)
      • 0
        Если это корпоративный пользователь, то ставите ему предварительно этот сертификат и предупреждений быть не должно, т.к. прокси инициирует соединение не от имени удаленного ssl сервера, а от своего, т.о. его сертификат является валидным.
        • 0
          Я об этом и говорю :) Но не всем это может понравится. Нет ни какой уверенности, что прокси-сервер проверяет полученный сертификат от гугла, т.е. что после прокси-сервера ни кто не реализовал еще раз атаку человек по середине. И к тому же я бы лично не хотел, чтобы даже мой работодатель мог просматривать мои личные данные, когда я захожу в свой счет в сбербанке или там альфа-банке. И вдобавок, если вы идете на какой либо ресурс, который требует ваш личный сертификат, чтобы удостовериться что вы это вы, то здесь опять ожидает провал :) Я не говорю, что предложенная Вами статья не имеет право на существование, я просто обратил внимание, что там не много не то и пропадает весь смысл HTTPS соединения.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.