Вступление
Нетерпеливым эти лирические отступления можно не читать.
Некоторое время назад меня посетила мысль: «Как сделать так, чтобы закрыть доступ к интернету (или к какому-то конкретному хосту) какой-нибудь одной программе в Linux?». Эта мысль меня посетила и полетела дальше по своим делам. И вот сегодня я получил в свой RSS-ридер один вопрос на askdev.ru. Ба! Да это как раз то, о чем я думал! Надо помочь человеку, заодно и самому разобраться в вопросе.
Полез я в гугл смотреть, нету ли каких-нибудь намеков на решение. Оттуда я узнал, что штатным iptables это с некоторых пор сделать стало невозможно, а народ рекомендует посмотреть в сторону AppArmor. «Горя огромным желанием изучить AppArmor», я стал искать дальше и почти случайно наткнулся на сообщение на ЛОРе, в котором описывался довольно интересный метод.
Метод
Заключался он в том, чтобы «подменить» функцию соединения на свою, которая решает, разрешить ли соединение и «пропустить» запрос к настоящей функции или нет — вернуть ошибку. В сообщении многоуважаемого Chaoser'а использовалась низкоуровневая блокировка сокетов, которая не давала возможности принимать решение в зависимости от адреса сервера и/или порта. Это меня не устраивало, мне нужно было запретить доступ только для одного порта — 80-го. Запустив telnet, под strace'ом, я почти сразу увидел подходящую жертву — фунцию connect. strace ее описал так:
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("87.250.251.3")}, 16) = 0
В этом описании отчетливо видно, что есть все необходимые мне составляющие: и IP-адрес, и порт, и тип подключения (AF_INET).
Что ж, приступим.
Решение
Для реализации данного метода, мы напишем библиотеку, которая будет загружаться раньше других при запуске приложения, и, таким образом, перехватывать функции, которые в ней определены (и которые предназначены для других библиотек).
Для начала выложу весь код, а затем будем разбирать все по отдельности.
- #define _GNU_SOURCE
- #include <dlfcn.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <errno.h>
-
- static int (*real_connect)(int sockfd, const struct sockaddr *addr,
- socklen_t addrlen) = 0;
-
- int connect(int sockfd, const struct sockaddr *addr,
- socklen_t addrlen)
- {
- printf("NF_DEBUG: -----------------------------------------------\n");
- int sa_family = addr->sa_family;
- printf("NF_DEBUG: Address family: %d (AF_INET = %d)\n", sa_family, AF_INET);
-
- if (sa_family == AF_INET)
- {
- struct sockaddr_in *addr_in = (struct sockaddr_in*)(addr);
-
- struct in_addr sin_addr = addr_in->sin_addr;
- uint16_t sin_port = addr_in->sin_port;
- uint16_t sin_port_h = ntohs(sin_port);
-
- printf("NF_DEBUG: IP: %s\n", inet_ntoa(sin_addr));
- printf("NF_DEBUG: Port: %d\n", sin_port_h);
-
- if (sin_port_h == 80)
- {
- printf("NF_DEBUG: Rejected!\n");
- printf("NF_DEBUG: -----------------------------------------------\n");
- errno = ENETUNREACH;
- return -1;
- }
- }
-
-
- if(!real_connect)
- real_connect = dlsym(RTLD_NEXT, "connect");
-
- printf("NF_DEBUG: Accepted\n");
- printf("NF_DEBUG: -----------------------------------------------\n");
- return real_connect(sockfd, addr, addrlen);
- }
* This source code was highlighted with Source Code Highlighter.
Разбор кода
Те, кто поняли, как работает вышеприведенная программа, могут этот раздел совершенно спокойно пропустить. В нем будет описано, какая строчка что делает. Это должно быть полезно новичкам. Тем, кто хорошо знает C, из этого, возможно, будут интересны лишь некоторые моменты.
Строки 1-8 — директивы препроцессора, ничего интересного в них нет. Разве что директива #define _GNU_SOURCE, которая подключает расширения GNU, необходимые для функции dlsym.
В строках 10-11 мы объявляем указатель на «настоящую» функцию connect. У нас она будет называться real_connect. Описание функции взято из man connect.
Со строки 13 начинается уже новая функция connect, которую будут вызывать приложения и которая будет принимать решение, пропустить это приложение или нет. Ее описание полностью соответствует оригинальной connect и взято из того же мануала.
В 17-й строке из структуры, которая содержит все необходимые нам данные (и описание которой я взял здесь) получаем тип адреса.
Если тип адреса AF_INET (строка 20), то есть приложение «просится наружу» по протоколу IPv4, то мы применяем фильтрацию (строки 22-37), иначе — сразу пропускаем это соединение к «настоящей» функции connect.
Для того, чтобы отфильтровать подключения, необходимо преобразовать структуру addr к типу sockaddr_in, который дает возможность доступа к необходимым полям. Это происходит в строке 22.
Далее в строках 24-25 получаем значения адреса и порта, соответственно. В строке 26 мы меняем порядок следования байт в номере порта, тем самым получая обычный формат номера порта (80, 110, 25) из формата, который используется при соединении.
В строке 28 полученный адрес приводим к текстовому виду и выводим его, в 29-й строке выводим номер порта.
Далее в 31-й строке проверяем, соответствует ли номер порта тому, который мы хотим заблокировать, если да, то сообщаем об этом (строки 33-34), устанавливаем номер ошибки (строка 35; в данном случае будет ошибка «Сеть недоступна») и возвращаем ошибку (строка 36).
Если же все в порядке, то есть приложение обратилось, используя либо разрешенный порт, либо разрешенный тип адреса, то получаем адрес (точку входа) «настоящей» функции connect (строка 42), если до того мы его не получили (строка 41; не забываем, что real_connect у нас переменная глобальная), пишем, что все хорошо (строки 44-45), и передаем управление «настоящей» функции connect.
При получении адреса «настоящей» функции connect, мы используем параметр RTLD_NEXT для того, чтобы получить этот адрес из следующей загруженной библиотеки, а не из первой (нашей), иначе получим бесконечную рекурсию.
Использование
Скомпилировать библиотеку:
gcc -fPIC -shared -Wl,-soname,nonet.so -o nonet.so nonet.c
и запустить нужное приложение следующим образом:
LD_PRELOAD=/<полный путь к полученной библиотеке>/nonet.so <приложение>
Например, если библиотека лежит в /tmp, то следующей строкой можно запустить firefox без доступа к интернету (естественно, только через 80-й порт):
LD_PRELOAD=/tmp/nonet.so firefox
Заключение
Данный метод ни в коем случае не претендует на универсальность и всеприменимость, равно как и статья на Нобелевскую премию. Здесь я всего лишь осветил две темы, которые интересны мне, и, надеюсь, будут интересны еще кому-нибудь: ограничение доступа к сети опредеделенным программам и перехват вызовов функций в Linux.
Спасибо за внимание!
P.S. Чтобы меня не считали бесчестным человеком, укравшим код у digital'а c askdev.ru, думаю следует упомянуть, что digital и я — одно лицо.
UPD Большое спасибо за инвайт хабрачеловеку peter23. Перенес пост в блог «Linux для всех».