Предыдущую часть обсуждения мы завершили на такой вот оптимистической ноте: «Подобным образом мы можем изменить поведение любого системного вызова Linux». И тут я слукавил — любого… да не любого. Исключение составляют (могут составлять) группа сетевых системных вызовов, работающих с BSD сокетами. Когда сталкиваешься с этим артефактом в первый раз — это изрядно озадачивает.
Для прояснения картины воспользуемся заметками одного из непосредственных разработчиков сетевой подсистемы Linux:
Network systems calls on Linux (2008 год). Я коротко перескажу её основное содержание (в интересующей нас части), кому это не интересно может воспользоваться оригиналом.
Когда поддержка BSD сокетов были добавлена в ядро Linux, разработчики решили добавить их единовременно все 17 (на сегодня 20) сокетных вызовов, и добавили для этих вызовов один дополнительный уровень косвенности. Для всей группы этих вызовов введен один новый, редко упоминаемый, системный вызов (см. man socketcall(2)):
где:
— call — численный номер сетевого вызова (SYS_CONNECT, SYS_ACCEPT… мы их увидим вскоре);
— args — указатель 6-ти элементного массива (блок параметров), в который последовательно упакованы все параметры любого из системных вызовов этой группы (сетевой), без различения их типа (приведенные к unsigned long);
А вот такой макрос в ядре (<net/socket.c>), в котором «зашито» сколько фактически параметров должен использовать каждый из сокетных вызовов в зависимости от его номера (в диапазоне от 1 до 20):
(Причём, narg[ 0 ] вообще не используется, потому размерность его и 21.)
Номер сокетного вызова в пространство ядра (int 0x80 или sysenter) передаётся в регистре eax. Значения самих этих констант мы можем подсмотреть в заголовках пространства пользователя (<linux/net.h>):
Собственно, схема обработки к этому моменту уже должна быть понятна:
— необходимое число параметров системного вызова пакуется в массив unsigned long, наибольшее число параметров (6) для SYS_SENDTO=11 (nargs[ 11 ]):
— адрес сформированного массива передаётся 2-м параметром системного вызова, первым параметром передаётся номер сокетного вызова (например SYS_SENDTO);
— все сокетные вызовы обрабатываются единственным обработчиком ядра sys_socketcall() (__NR_socketcall = 102);
— обработчик сначала копирует из пространства пользователя массив значений-параметров, а далее, в зависимости от eax, копирует из пространства пользователя вослед и области данных, указываемые (возможно) значениями указателей из этого массива параметров.
Некоторые новые архитектуры (так в оригинале) не используют такой непрямой способ вызова, а используют для этих вызовов такую же реализацию, как и для всех остальных системных вызовов. Так это реализовано, в частности, для X86_64 и ARM. Таким образом, даже 64-битовые и 32-битовые (эмулируемые в системе X86_64) приложения будут выполняться по разной схеме. Но не станем на это пока отвлекаться…
Удостовериться в том, что обслуживание сокетных вызовов в 32 и 64 битовых системах осуществляется принципиально по-разному, можно если в каталоге приложений пространства пользователя (заголовочные файлы библиотек языка C, <i386-linux-gnu/asm>) рассмотреть, для сравнения, определения набора системных вызовов для 32 и 64 битовых режимов:
В 32-бит системе присутствует вызов sys_socketcall(), но отсутствуют вызовы для каждого из 20 сокетых вызовов. И напротив, в 64-бит системе отсутствует такой системный вызов как sys_socketcall(), но присутствует весь полный набор системных вызовов для каждого из 20-ти сокетных вызовов.
Сам же автор заметки в завершение, в качестве оценки, пишет следующее: Данная методика кажется довольно уродливой (rather ugly) на первый взгляд, при сравнении с современными методами объектно-ориентированного программирования, но есть и определенная простота в нем. Он, также, хранит данные компактно, что улучшает попадание в кэши. Единственная проблема заключается в том, что выборка должна быть выполнена вручную, а это означает, что здесь легко выстрелить себе в ногу.
Возможность перехвата сетевых системных вызовов будем иллюстрировать на макете распределённого файервола (максимально его упростив). Одно время с этой идеей очень сильно носились, в качестве реализации файервола для больших и сверхбольших сетей (особенно в окружении Cisco). Существует много публикаций на эту тему, например, две из них, дающие полное представление о том, что понимается как распределённый файервол: Implementing a Distributed Firewall и
Automated Implementation of Stateful Firewalls in Linux.
Предложение состоит в том, чтобы контролировать не весь TCP/IP трафик на уровне IP пакетов, а осуществлять регламент на каждом хосте сверхбольшой сети только для протокола TCP и только в момент установления соединения. Под контроль попадают только 2 системных вызова: accept() и connect(). Более глубокое обсуждение распределённого файервола увело бы нас очень далеко от наших целей … рассмотрим только то как мы могли бы контролировать эти сетевые сетевые вызовы.
В качестве иллюстрации реализации перехвата сокетных вызовов был реализован модуль такого сетевого фильтра я ядре для вызовов accept() и connect(). Сделан этот модуль в максимально упрощенной (усечённой) реализации: в качестве параметров при загрузке модуль получает IP адрес (параметр deny) и TCP порт (параметр port), соединения с которыми должны быть запрещены (и ещё один дополнительный параметр debug — уровень диагностического вывода).
Примечание: В тестируемом варианте запрещённые IP адреса и TCP порты допускались множественными, хранились в циклическом списке типа struct list_head (как это и принято повсеместно в ядре), а помещались (или удалялись) они туда отдельным приложением — демоном политики в пространстве пользователя. Фильтр в ядре и должен функционировать некоторым подобным образом, но это слишком громоздко для статьи, описывающей принцип, тем более, что не принцип файервола, а принцип работы с сетевыми системными вызовами. При всех упрощениях код всё ещё великоват, поэтому я помеаю его под спойлер.
Код максимально упрощён, такие вещи, как макросы диагностики LOG(), ERR() уже показывались, отчасти, в предыдущих частях. Функция find() тоже уже обсуждалась. Для записи в защищённую о�� записи область таблицы sys_call_table существует, как минимум, 3-4 альтернативных варианта, все они назывались и давались ссылками в обсуждениях предыдущей части. Защита от выгрузки модуля на время обслуживания системных вызовов, путём инкремента счётчика ссылок модуля, тоже не показана (называлось в предыдущей части). Все эти подробности присутствуют в кодах прилагаемого архива. Кроме того, коды в архиве обильно пересыпаны комментариями, содержащими выдержки из исходников ядра, с указанием файлов в дереве кодов ядра — это подсказывает требуемые структуры данных.
И всё же при всех упрощениях код остаётся достаточно громоздким (не сложным, а громоздким). Но можно и не вникать в собственно код, последовательность обработки модифицированных сетевых системных вызовов следующая:
Некоторую дополнительную сложность создаёт тот факт, что для вызова accept() проверку приходится выполнять дважды:
Как это выглядит в работе? Как-то так:
Естественно, для того, чтобы наблюдать работу сетевого фильтра ядра в действии, нам необходимы TCP клиент и сервер (например, ncat). Но для детального тестирования были подготовлены специальные ретранслирующий сервер (tcpserv) и клиент (tcpcli). Не считая некоторых мелочей, заточенных под эту работу, они ничего особенного не представляют и рассматриваться здесь не будут (но они есть в прилагаемом архиве).
Вот как будут выглядеть некоторые из попыток установления запрещённых TCP соединений:
— Запуск сервера, прослушивающего запрещённый порт:
— Попытка подключения клиента к запрещённому порту:
Ну и так далее — задача предоставляет широкое и увлекательное поле для экспериментирования…
(Здесь в протоколе специально сохранено и показано обращение в это же время к DNS по порту 53. Точно также, во время экспериментов с фильтрацией можно наблюдать множество соединений к TCP порту 80 — всё время не нарушая работы идёт HTTP трафик.)
Важно то, что после выгрузки модуля работа системы восстанавливается в исходное состояние:
Вот так, несколько с выдумкой, осуществляется в Linux обработка сетевых системных вызовов … по крайней мере, в 32 бит реализации. При первом столкновении с этими системными вызовами способ их работы несколько обескураживает.
Эта часть обсуждения получилась затянутой и скучной, но такой артефакт, как вот такая работа системных вызовов — его нужно знать и учитывать.
Маленький архив кода (и обширный журнал тестирования) для экспериментов можно взять здесь или здесь.
Как происходит сокетный вызов
Для прояснения картины воспользуемся заметками одного из непосредственных разработчиков сетевой подсистемы Linux:
Network systems calls on Linux (2008 год). Я коротко перескажу её основное содержание (в интересующей нас части), кому это не интересно может воспользоваться оригиналом.
Когда поддержка BSD сокетов были добавлена в ядро Linux, разработчики решили добавить их единовременно все 17 (на сегодня 20) сокетных вызовов, и добавили для этих вызовов один дополнительный уровень косвенности. Для всей группы этих вызовов введен один новый, редко упоминаемый, системный вызов (см. man socketcall(2)):
int socketcall( int call, unsigned long *args );
где:
— call — численный номер сетевого вызова (SYS_CONNECT, SYS_ACCEPT… мы их увидим вскоре);
— args — указатель 6-ти элементного массива (блок параметров), в который последовательно упакованы все параметры любого из системных вызовов этой группы (сетевой), без различения их типа (приведенные к unsigned long);
А вот такой макрос в ядре (<net/socket.c>), в котором «зашито» сколько фактически параметров должен использовать каждый из сокетных вызовов в зависимости от его номера (в диапазоне от 1 до 20):
/* Argument list sizes for sys_socketcall */ #define AL(x) ((x) * sizeof(unsigned long)) static const unsigned char nargs[ 21 ] = { AL(0),AL(3),AL(3),AL(3),AL(2),AL(3), AL(3),AL(3),AL(4),AL(4),AL(4),AL(6), AL(6),AL(2),AL(5),AL(5),AL(3),AL(3), AL(4),AL(5),AL(4) }; #undef AL
(Причём, narg[ 0 ] вообще не используется, потому размерность его и 21.)
Номер сокетного вызова в пространство ядра (int 0x80 или sysenter) передаётся в регистре eax. Значения самих этих констант мы можем подсмотреть в заголовках пространства пользователя (<linux/net.h>):
#define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ #define SYS_CONNECT 3 /* sys_connect(2) */ #define SYS_LISTEN 4 /* sys_listen(2) */ #define SYS_ACCEPT 5 /* sys_accept(2) */ ... #define SYS_SENDMSG 16 /* sys_sendmsg(2) */ #define SYS_RECVMSG 17 /* sys_recvmsg(2) */ #define SYS_ACCEPT4 18 /* sys_accept4(2) */ #define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */ #define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */
Собственно, схема обработки к этому моменту уже должна быть понятна:
— необходимое число параметров системного вызова пакуется в массив unsigned long, наибольшее число параметров (6) для SYS_SENDTO=11 (nargs[ 11 ]):
ssize_t sendto( int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen );
— адрес сформированного массива передаётся 2-м параметром системного вызова, первым параметром передаётся номер сокетного вызова (например SYS_SENDTO);
— все сокетные вызовы обрабатываются единственным обработчиком ядра sys_socketcall() (__NR_socketcall = 102);
— обработчик сначала копирует из пространства пользователя массив значений-параметров, а далее, в зависимости от eax, копирует из пространства пользователя вослед и области данных, указываемые (возможно) значениями указателей из этого массива параметров.
Некоторые новые архитектуры (так в оригинале) не используют такой непрямой способ вызова, а используют для этих вызовов такую же реализацию, как и для всех остальных системных вызовов. Так это реализовано, в частности, для X86_64 и ARM. Таким образом, даже 64-битовые и 32-битовые (эмулируемые в системе X86_64) приложения будут выполняться по разной схеме. Но не станем на это пока отвлекаться…
Удостовериться в том, что обслуживание сокетных вызовов в 32 и 64 битовых системах осуществляется принципиально по-разному, можно если в каталоге приложений пространства пользователя (заголовочные файлы библиотек языка C, <i386-linux-gnu/asm>) рассмотреть, для сравнения, определения набора системных вызовов для 32 и 64 битовых режимов:
$ cat unistd_32.h | grep socketcall #define __NR_socketcall 102 $ cat unistd_32.h | grep connect
$ cat unistd_64.h | grep socketcall $ cat unistd_64.h | grep connect #define __NR_connect 42
В 32-бит системе присутствует вызов sys_socketcall(), но отсутствуют вызовы для каждого из 20 сокетых вызовов. И напротив, в 64-бит системе отсутствует такой системный вызов как sys_socketcall(), но присутствует весь полный набор системных вызовов для каждого из 20-ти сокетных вызовов.
Сам же автор заметки в завершение, в качестве оценки, пишет следующее: Данная методика кажется довольно уродливой (rather ugly) на первый взгляд, при сравнении с современными методами объектно-ориентированного программирования, но есть и определенная простота в нем. Он, также, хранит данные компактно, что улучшает попадание в кэши. Единственная проблема заключается в том, что выборка должна быть выполнена вручную, а это означает, что здесь легко выстрелить себе в ногу.
Реализация
Возможность перехвата сетевых системных вызовов будем иллюстрировать на макете распределённого файервола (максимально его упростив). Одно время с этой идеей очень сильно носились, в качестве реализации файервола для больших и сверхбольших сетей (особенно в окружении Cisco). Существует много публикаций на эту тему, например, две из них, дающие полное представление о том, что понимается как распределённый файервол: Implementing a Distributed Firewall и
Automated Implementation of Stateful Firewalls in Linux.
Предложение состоит в том, чтобы контролировать не весь TCP/IP трафик на уровне IP пакетов, а осуществлять регламент на каждом хосте сверхбольшой сети только для протокола TCP и только в момент установления соединения. Под контроль попадают только 2 системных вызова: accept() и connect(). Более глубокое обсуждение распределённого файервола увело бы нас очень далеко от наших целей … рассмотрим только то как мы могли бы контролировать эти сетевые сетевые вызовы.
В качестве иллюстрации реализации перехвата сокетных вызовов был реализован модуль такого сетевого фильтра я ядре для вызовов accept() и connect(). Сделан этот модуль в максимально упрощенной (усечённой) реализации: в качестве параметров при загрузке модуль получает IP адрес (параметр deny) и TCP порт (параметр port), соединения с которыми должны быть запрещены (и ещё один дополнительный параметр debug — уровень диагностического вывода).
Примечание: В тестируемом варианте запрещённые IP адреса и TCP порты допускались множественными, хранились в циклическом списке типа struct list_head (как это и принято повсеместно в ядре), а помещались (или удалялись) они туда отдельным приложением — демоном политики в пространстве пользователя. Фильтр в ядре и должен функционировать некоторым подобным образом, но это слишком громоздко для статьи, описывающей принцип, тем более, что не принцип файервола, а принцип работы с сетевыми системными вызовами. При всех упрощениях код всё ещё великоват, поэтому я помеаю его под спойлер.
Итак, код модуля-примера:
static int debug = 0; // debug output level: 0, 1, 2 module_param( debug, uint, 0 ); static char* deny; // string parameter: denied IPv4 module_param( deny, charp, 0 ); static int port = 0; // denied port module_param( port, int, 0 ); static void **taddr; // table sys_call_table address u32 ipdeny; // denied IP #include "find.c" #include "CR0.c" inline char* in4_ntoa( uint32_t ip ) { // mapping IP to a string static char saddr[ MAX_ADDR_LEN ]; sprintf( saddr, "%d.%d.%d.%d", ( ip >> 24 ) & 0xFF, ( ip >> 16 ) & 0xFF, ( ip >> 8 ) & 0xFF, ( ip ) & 0xFF ); return saddr; } asmlinkage long (*old_sys_socketcall) ( int call, unsigned long __user *args ); asmlinkage long new_sys_socketcall( int call, unsigned long __user *args ) { #define PARMS 3 static unsigned long a[ PARMS ]; // accept() and connect() have the same number of parameters 3 static struct sockaddr sa; // ----------- nested functions are a GCC extension --------- long get_addr( void ) { const unsigned int len = PARMS * sizeof( unsigned long ); if( copy_from_user( a, args, len ) ) return -EFAULT; if( copy_from_user( &sa, (struct sockaddr __user*)a[ 1 ], sizeof( struct sockaddr ) ) ) return -EFAULT; return 0; } // ---------------------------------------------------------- long ret; if( SYS_ACCEPT == call ) { // accept() before syscall long err; if( ( err = get_addr() ) < 0 ) return err; if( AF_INET == sa.sa_family ) { // only IPv4 struct sockaddr_in *usin = (struct sockaddr_in *)&sa; if( ntohs( usin->sin_port ) == port ) { LOG( "accept from denied port %d\n", ntohs( usin->sin_port ) ); return -EIO; } } } if( SYS_CONNECT == call ) { // connect() before syscall long err; if( ( err = get_addr() ) < 0 ) return err; if( AF_INET == sa.sa_family ) { // only IPv4 struct sockaddr_in *usin = (struct sockaddr_in *)&sa; DEB( "connect to %s:%d\n", in4_ntoa( ntohl( usin->sin_addr.s_addr ) ), ntohs( usin->sin_port ) ); if( ( deny != NULL && ntohl( usin->sin_addr.s_addr ) == ipdeny ) || ( port != 0 && ntohs( usin->sin_port ) == port ) ) { LOG( "connect to %s:%d denied\n", in4_ntoa( ntohl( usin->sin_addr.s_addr ) ), ntohs( usin->sin_port ) ); return -EACCES; } } } ret = old_sys_socketcall( call, args ); // retranslate to original sys_socketcall() if( SYS_ACCEPT == call ) { // accepr() after syscall long err; if( ( err = get_addr() ) < 0 ) return err; if( AF_INET == sa.sa_family ) { // only IPv4 struct sockaddr_in *usin = (struct sockaddr_in *)&sa; DEB( "accept from %s:%d\n", in4_ntoa( ntohl( usin->sin_addr.s_addr ) ), ntohs( usin->sin_port ) ); if( ( deny != NULL && ntohl( usin->sin_addr.s_addr ) == ipdeny ) || ( port != 0 && ntohs( usin->sin_port ) == port ) ) { LOG( "accept from %s:%d denied\n", in4_ntoa( ntohl( usin->sin_addr.s_addr ) ), ntohs( usin->sin_port ) ); return -EACCES; } } } return ret; } static int __init init( void ) { void *waddr; // ----------- nested functions are a GCC extension --------- int pos_in_table( const char *symbol ) { // position in sys_call_table (__NR_*) const int last = __NR_process_vm_writev; // near last syscall in i386 int n; waddr = find_sym( symbol ); if( NULL == waddr ) return -1; for( n = 0; n <= last; n++ ) if( taddr[ n ] == waddr ) break; return n <= last ? n : -1; } // -------------------------------------------------------- void show_in_table( char *symb ) { // print info about symbol waddr = find_sym( symb ); if( NULL == waddr ) { DEB( "symbol %s not found in kernel\n", symb ); } else { int n = pos_in_table( symb ); if( n > 0 ) DEB( "symbol %s address = %p, position in sys_call_table = %d\n", symb, waddr, n ); else DEB( "symbol %s address = %p, not found in sys_call_table\n", symb, waddr ); } } // -------------------------------------------------------- ipdeny = ntohl( deny != NULL ? in_aton( deny ) : in_aton( "0.0.0.0" ) ); LOG( "denied IP: %s\n", deny != NULL ? in4_ntoa( ipdeny ) : "no" ); if( port != 0 ) LOG( "denied TCP port: %d\n", port ); if( NULL == ( taddr = find_sym( "sys_call_table" ) ) ) { ERR( "sys_call_table not found\n" ); return -EINVAL; } DEB( "sys_call_table address = %p\n", taddr ); show_in_table( "sys_accept" ); show_in_table( "sys_connect" ); show_in_table( "sys_socketcall" ); // only diagnostic old_sys_socketcall = (void*)taddr[ __NR_socketcall ]; if( NULL == ( waddr = find_sym( "sys_socketcall" ) ) ) { // sys_socketcall not exported ERR( "sys_socketcall not found\n" ); return -EINVAL; } if( old_sys_socketcall != waddr ) { // reinsurance! ERR( "Oooops! I don't understand: addresses not equal\n" ); return -EINVAL; } if( debug ) show_cr0(); rw_enable(); taddr[ __NR_socketcall ] = new_sys_socketcall; if( debug ) show_cr0(); rw_disable(); if( debug ) show_cr0(); LOG( "install new sys_socketcall handler: %p\n", &new_sys_socketcall ); return 0; } static void __exit exit( void ) { LOG( "sys_socketcall handler before unload: %p\n", (void*)taddr[ __NR_socketcall ] ); rw_enable(); taddr[ __NR_socketcall ] = old_sys_socketcall; rw_disable(); LOG( "restore old sys_socketcall handler: %p\n", (void*)taddr[ __NR_socketcall ] ); return; } module_init( init ); module_exit( exit );
Код максимально упрощён, такие вещи, как макросы диагностики LOG(), ERR() уже показывались, отчасти, в предыдущих частях. Функция find() тоже уже обсуждалась. Для записи в защищённую о�� записи область таблицы sys_call_table существует, как минимум, 3-4 альтернативных варианта, все они назывались и давались ссылками в обсуждениях предыдущей части. Защита от выгрузки модуля на время обслуживания системных вызовов, путём инкремента счётчика ссылок модуля, тоже не показана (называлось в предыдущей части). Все эти подробности присутствуют в кодах прилагаемого архива. Кроме того, коды в архиве обильно пересыпаны комментариями, содержащими выдержки из исходников ядра, с указанием файлов в дереве кодов ядра — это подсказывает требуемые структуры данных.
И всё же при всех упрощениях код остаётся достаточно громоздким (не сложным, а громоздким). Но можно и не вникать в собственно код, последовательность обработки модифицированных сетевых системных вызовов следующая:
- взять под контроль (сменить обработчик) системного вызова sys_socketcall();
- если код вызова (1-й параметр sys_socketcall()) равен SYS_ACCEPT или SYS_CONNECT, то скопировать из пространства пользователя 3-х элементный массив параметров unsigned long (в общем случае 6 элементов, для SYS_SENDMSG, например);
- 2-й элемент массива (соответствующий 2-му параметру accept() или connect()), хоть он и выглядит как unsigned long — это указатель на struct sockaddr в адресном пространстве пользователя, вторым шагом доступа к параметрам копируем структуру из адресного пространства пользователя;
- структура содержит параметры IP адрес и TCP порт, если они попадают в перечень запрещённых — возвращаем код ошибки и отменяется операция, если нет — вызываем оригинальный обработчик системного вызова;
- для всех остальных (18-ти, не SYS_ACCEPT и SYS_CONNECT) сокетных вызовов просто осуществляем транзитом вызов оригинального sys_socketcall();
- запросы, не относящиеся к протоколу IPv4 без модификации передаются сетевому стеку;
Некоторую дополнительную сложность создаёт тот факт, что для вызова accept() проверку приходится выполнять дважды:
- номер TCP порта раньше оригинального системного вызова, когда сервер начинает прослушивать не присоединенный сокет;
- IP адрес источника после установления соединения для сокета, после возврата из функции оригинального системного вызова;
Как это выглядит в работе? Как-то так:
$ sudo insmod fwnet.ko deny=192.168.56.101 port=10000 debug=1 $ lsmod | head -n2 Module Size Used by fwnet 13116 0 $ dmesg | tail -n10 [ 786.609568] ! denied IP: 192.168.56.101 [ 786.609572] ! denied TCP port: 10000 [ 786.613047] ! sys_call_table address = c15b4000 [ 786.636336] ! symbol sys_accept address = c149a070, not found in sys_call_table [ 786.656437] ! symbol sys_connect address = c149a0a0, not found in sys_call_table [ 786.661444] ! symbol sys_socketcall address = c149acd0, position in sys_call_table = 102 [ 786.663994] ! CR0 = 8005003b [ 786.664090] ! CR0 = 8004003b [ 786.664096] ! CR0 = 8005003b [ 786.664100] ! install new sys_socketcall handler: e1ad50d0
Естественно, для того, чтобы наблюдать работу сетевого фильтра ядра в действии, нам необходимы TCP клиент и сервер (например, ncat). Но для детального тестирования были подготовлены специальные ретранслирующий сервер (tcpserv) и клиент (tcpcli). Не считая некоторых мелочей, заточенных под эту работу, они ничего особенного не представляют и рассматриваться здесь не будут (но они есть в прилагаемом архиве).
Вот как будут выглядеть некоторые из попыток установления запрещённых TCP соединений:
— Запуск сервера, прослушивающего запрещённый порт:
$ ./tcpserv -v -p10000 listening on the TCP port 10000 denied TCP port: Input/output error $ dmesg | tail -n5 ... [11213.888556] ! accept before: port = 10000 [11213.888562] ! accept from denied port 10000
— Попытка подключения клиента к запрещённому порту:
$ ./tcpcli -v -h 127.0.0.1 -p 10000 client: can't connect to server: Permission denied $ dmesg | tail -n5 ... [10984.082051] ! connect to 127.0.0.1:10000 [10984.082060] ! connect to 127.0.0.1:10000 denied [11166.236948] ! connect to 127.0.0.1:53 ...
Ну и так далее — задача предоставляет широкое и увлекательное поле для экспериментирования…
(Здесь в протоколе специально сохранено и показано обращение в это же время к DNS по порту 53. Точно также, во время экспериментов с фильтрацией можно наблюдать множество соединений к TCP порту 80 — всё время не нарушая работы идёт HTTP трафик.)
Важно то, что после выгрузки модуля работа системы восстанавливается в исходное состояние:
$ sudo rmmod fwnet $ dmesg | grep \! | tail -n2 [ 2890.602419] ! sys_socketcall handler before unload: e1ad50d0 [ 2890.602439] ! restore old sys_socketcall handler: c149acd0
Обсуждение
Вот так, несколько с выдумкой, осуществляется в Linux обработка сетевых системных вызовов … по крайней мере, в 32 бит реализации. При первом столкновении с этими системными вызовами способ их работы несколько обескураживает.
Эта часть обсуждения получилась затянутой и скучной, но такой артефакт, как вот такая работа системных вызовов — его нужно знать и учитывать.
Маленький архив кода (и обширный журнал тестирования) для экспериментов можно взять здесь или здесь.
