Pull to refresh

Китайские видеокамеры и TCP: баг или фича?

Debugging *
В связи с узурпацией должности управдома нашего МКД, и необходимости «причесать» общественный беспорядок, потихоньку конструирую систему видеонаблюдения.
Разумеется, финансирование минимальное, планы — грандиозные, поэтому собирается всё из подножного корма.
Подробности чуть позже, а вот один из интересных багов, который заставил перебрать много чего.

Итак, началось всё с малого — простенькая купольная камера, бралась самая паршивая по принципу "только б IP и купол". После пошел процесс перебора софта (тема отдельного поста, будет, опять же, позже)… В конце зафиксировался на Macroscop в один канал. Стояла пару месяцев, есть-пить не просила, наркоманов гонять помогала.

И вот работает вроде всё, но как-то подлагивает периодически. Грешил на всё: на проц, на сеть, на софт, на погоду на марсе… Саппорт по логам говорит, что-де камера периодически отваливается.

Upd

Да были закуплены еще камеры, качеством и ценою значительно выше. Поборовши лень в три прекрасных дня в разные недели было это всё смонтировано на 1м этаже, заведено в комп, подключено. И пошла вторая итерация перебора софта, так чтоб на камер побольше, ценою гуманней, интерфейс удобней… После второго раунда перебора, пока стоит AxxonNext. Да вот только лаги вышли на новый уровень: камеры отваливаются чуть ли не синхронно каждые 5-10 минут если включить TCP; а если держать UDP, то артефакты лезут как не знаю что.

Пожаловался я в жуйке на эту тему, а maxlapshin возьми да и скажи, что это стабильный баг китайских камер, где прошивка создана их собственными силами: если клиент не успевает выгребать, то в поток лезет мусор. Так как продаван на ali прикормленный, было решено попытаться решить проблему капитально. Несколько раз разными словами пытался баг описать, но понял, что английский у нас обоих оставляет желать лучшего, поэтому надо просто «show code».

Первый этап раскопок: найти причину



Итак, сперва берём tcpdump, и ждём ситуацию обрыва. Ждать недолго, за 5 минут поймалось аж 3 штуки. Как понять что произошло? Поток в полтора миллиона пакетов… Для начала фильтруем, оставляя только одну камеру. Затем Ctrl+F => tcp.flags.syn==1 => находим начало реконнекта, откуда листаем вверх, чтобы понять что случилось…



Наблюдаем, что коннект был закрыт со стороны компа… Вот только смущает что перед этим Win=11460, Win=10200, Win=8940 — то есть, похоже, клиент и правда не успевает. Стоп-стоп-стоп. Как это не успевает? AMD Phenom II X6 1100T? Не, странно. IO? Так запись на отдельный диск, и не упёрлось в полку. Да и тогда была бы зависимость от просмотра, например — а её нет… В общем, листаем еще выше чуть-чуть:



Так буквально секунду назад вообще в ZeroWindow уходили. Точно не успевает. Но почему обрыв-то?! Листаем ниже, смотрим другой обрыв…



Так, наблюдаем ZeroWindow в течение аж 2.5 секунд — совсем ни в какие ворота не лезет. Листаем ниже, и, кажется, начинаем понимать:




Смотрим — совсем нехорошо себя повел TCP стек (камеры? вероятно...), после чего RTP поток сбился — в один прекрасный момент вместо DynamicRTP-Type-96 пошли RTSP Continuation.

Итак, делаем вывод: всё что нужно для симуляции, это запросить RTP поток, немного вытянуть, а потом сделать sleep(), и смотреть сломается ли поток.

Поиск частей для франкенштейна



Как поступить, когда надо быстро накидать тестовый кусок? Взять скриптовый язык, набор готовых библиотек, слепить всё это вместе, радоваться. Лезем в гугль. Python+ONVIF. Тухло. Ruby+ONVIF. О, есть ruby-onvif-client. ОК, берём. URL стрима поймали. Отлично, а если :protocol покрутить?.. UDP, HTTP, TCP… итог один — отлично, по onvif ловить URL научился, теперь его бы слить.

Curl? Не жуёт. Ладно, Ruby + RTSP… Ура, есть либа. Скушиваем ему урл, и облом. Пытается авторизоваться исключительно через Basic. Еще немного гугля. Облом. Тогда остаётся один метод — напильник. Впаиваем калёным железом, по пути матерясь на объёмы магии руби (кто мне скажет, как правильно сделать «честную» рекурсию, чтобы работал и return и yield? правильно, а как догадаться по описанию функции, что оно может потребоваться? правильно… проще избавиться от неё — и чище код, и шелковистее волосы).

И снова ликуем, rtsp_client работает. Открываем во втором окне tcpdump… Блин! Я же в ONVIF запрашивал :protocol=>«TCP». Что за черт, почему UDP?

Паяльник в руки… Ха! UDP прибит гвоздями в lib/rtsp/client.rb@request_transport. Так, впаиваем туда теми же гвоздями /TCP. Запускаем — падает. Почему? Куда? Ага, он требует наличия client_port… Какой client_port, если это TCP? Хардкодим rtp_port если его нет в 554. Так, IP надо сервера — хардкодим. Опа, не может, говорит, при'bind()'иться на 554 порт не рутом. Логично. Так, а зачем? Ну-ка… Это в RTP::Receiver… Смотрим на init_socket в режиме :TCP и удивляемся — а зачем ему TCPServer для Receiver? Что-то не то. Явно, вот явно на TCP никто не отлаживал.

После пары минут попыток понять логику, до Зоркого Глаза дошло, что стенки-то нет:
transport:      RTP/AVP/TCP;unicast;destination=172.28.1.199;source=172.28.1.95;interleaved=0-1


Ну-ка ну-ка… что за interleaved? Google: rtp interleaved => Wikipedia => Find on page «interleaved» => Ага! Так RTP и RTSP валятся в этом же самом коннекте!

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

Выращиваем амёбу



Итак, мы снова в нуле: у нас есть rtsp:// урл камеры, есть необходимость его слить и поиграться во время слива с коннектом… Стоп! Сначала. Есть урл. Есть камера. Есть задача воспроизвести на ней баг. Зачем мне ONVIF? Зачем RTSP либы? Надо просто запросить и качать ответ.

Сказано — сделано. DESCRIBE? А зачем он нам… Нам надо только сперва SETUP, откуда забрать Session: затем PLAY с ней.

Первый же опыт показал, что ему даже авторизация тут не требуется.
Отлично! Первый же блин выстрелил сразу: простнький скрипт прекрасно воспроизводил баг — после sleep()'а поток ломался на ура.

Но чтобы поиграться с TCP Window надо бы иметь возможность задать TCP_WINDOW_CLAMP. А для этого надо сделать setsockopt ДО connect но после создания сокета.
А как это в руби сделать? Эм… Заглядываем в гугль… Пусто. Заглядываем в исходник — init_inetsock_internal… фиг там! сперва создаём rsock_socket(), а затем сразу rsock_connect(). Блин.

Ладно, я всё равно руби не очень люблю. За 2 минуты переписываем на питон, добавляем setsockopt. Добавляем анализ номера RTP пакета.

Итог анализа: от размера начального окна меняется объём инфромации, сколько успевает камера передать нормального, прежде чем сломается. Впрочем, неважно, закоммитил итог, и написал багрепорт продавцу, дабы они передали девелоперам прошивки.

Так бы и успокоился, но maxlapshin спрашивал можно ли как-то жить с этим багом. И это потребовало дополнительных раскопок.

В принципе, как с этим жить?


1) Можно отслеживать сбой и пересоединяться самостоятельно
2) Можно отслеживать собственный лаг (не знаю как, но можно) и присоединиться сразу, не дожидаясь поломки потока
3) Можно попытаться восстановить синхронизацию с потоком.

Итак, втыкаем вместо вызова raise — вызов reconnect, и анализируем потери по разнице номеров RTP пакетов. Если действуем по пункту 1, то потери составляют около 1500-1800 RTP пакетов (примерно 600 пакетов на секунду sleep()).

Втыкаем reconnect сразу после sleep(). Итог в точности такой же.

Втыкаем ресинхронизацию методом поиска "$\x00" — работает отвратно. Втыкаем ресинхронизацию методом поиска "$\x00[LEN]{len bytes}$\x00" — работает стабильно, потеря составляет в полтора раза меньше, чем при reconnect. Но самое главное — TCP соединение при этом не падает, а значит, алгоритм адаптации TCP Window и буферов приема продолжают работать. Вследствие чего, спустя 1-2 сбоя в начале соединения, поток просто перестаёт ломаться — sleep() продолжают регулярно усыплять клиента, а поток не падает.

Полученный тест-скрипт качеством кода не блещет, но прекрасно выполняет функцию proof-of-concept.

И теперь, наконец-то, мы пришли к вопросу, заданному в заголовке.

Баг ли это, или фича?



Моё личное мнение — это реально лучше, чем рвать соединение целиком: номера RTP пакетов в коннекте есть, так что без проблем потерянный объём можно замерить.
Если канал тоньше, чем пропускная способность сети — то потери будут расти, таким образом, обнаружив потери больше чем на 5 сек — можно спокойно жаловаться на толщину канала (реконнектиться с меньшим качеством, попросить поменять кабель, взорвать АЭС, или любой другой способ реакции на эту проблему); если же проблема в том, что приёмник просто не успевает по какой-то причине выгребать — resync поведение = наше щасте. Получаем объединение плюсов одновременно UDP и TCP подходов: если совсем что-то плохо — кусочек потеряли; в остальных случаях — ретрансмиты автоматические и проблем не доставляют.

Ну и на закуску, поговорим о том, что за баг в прошивке… А баг прост: send(somedata, somelen) возвращает <0 в случае ошибки, число байт, если отработало. И любмая ошибка всех начинающих сетевых разработчиков: send(somedata, somelen) может вернуть что-то МЕНЬШЕ, чем somelen — в буфер не влезло.

Если это не обрабатывать, хвост от somedata просто теряется — его и не отправили, и выбросили.

Как правильно починить?



Починить так: надо запомнить недоотправленный кусок, и прекратить посылать что бы то ни было, пока send() этого остатка не отправит всё целиком. После этого надо начать посылать _следующие_ пакеты (выбросив те, которые были всё время, что буфер был занят). Тогда мы получим то самое поведение помеси TCP и UDP, но без необходимости клиенту заниматься магической ресинхронизацией с границами пакетов.

Надеюсь, производители поправят этот баг, и через некоторое время, все китайские прошивки для китайских камер будут радовать корректной работой без единого разрыва&tm;

Источник радости: LIVE555 юрского периода



Саппорт macroscop обратил моё глупое невнимание на тот факт, что LIVE555 анонсируется 2011 года… Так что через binwalk+dd+mount заглянул в потроха прошивки. rtsp_streamer и правда, от 2011 года.
Заглянул в diff между двумя версиями live.2013.10.09.tar.gz и live.2014.02.19.tar.gz:
diff --git a/liveMedia/RTPInterface.cpp b/liveMedia/RTPInterface.cpp
index d45e5a8..3d88d55 100644
--- a/liveMedia/RTPInterface.cpp
+++ b/liveMedia/RTPInterface.cpp
@@ -324,19 +325,23 @@ Boolean RTPInterface::sendRTPorRTCPPacketOverTCP(u_int8_t* packet, unsigned p
 }
 
 Boolean RTPInterface::sendDataOverTCP(int socketNum, u_int8_t const* data, unsigned dataSize, Bool
-  if (send(socketNum, (char const*)data, dataSize, 0/*flags*/) != (int)dataSize) {
-    // The TCP send() failed.
-
-    if (forceSendToSucceed && envir().getErrno() == EAGAIN) {
-      // The OS's TCP send buffer has filled up (because the stream's bitrate has exceeded the cap
+  int sendResult = send(socketNum, (char const*)data, dataSize, 0/*flags*/);
+  if (sendResult < (int)dataSize) {
+    // The TCP send() failed - at least partially.
+
+    unsigned numBytesSentSoFar = sendResult < 0 ? 0 : (unsigned)sendResult;
+    if (numBytesSentSoFar > 0 || (forceSendToSucceed && envir().getErrno() == EAGAIN)) {
+      // The OS's TCP send buffer has filled up (because the stream's bitrate has exceeded
+      // the capacity of the TCP connection!).
       // Force this data write to succeed, by blocking if necessary until it does:
+      unsigned numBytesRemainingToSend = dataSize - numBytesSentSoFar;
 #ifdef DEBUG_SEND
-      fprintf(stderr, "sendDataOverTCP: resending %d-byte send (blocking)\n", dataSize); fflush(st
+      fprintf(stderr, "sendDataOverTCP: resending %d-byte send (blocking)\n", numBytesRemainingToS
 #endif
       makeSocketBlocking(socketNum);
-      Boolean sendSuccess = send(socketNum, (char const*)data, dataSize, 0/*flags*/) == (int)dataS
+      sendResult = send(socketNum, (char const*)(&data[numBytesSentSoFar]), numBytesRemainingToSen
       makeSocketNonBlocking(socketNum);
-      return sendSuccess;
+      return sendResult == (int)numBytesRemainingToSend;
     }
     return False;
   }


А вот вырезка из changelog.txt:
2013.12.04:
- Updated the "sendDataOverTCP()" function (in "RTPInterface.cpp") to allow for the possibility of
one of the "send()" calls partially succeeding - i.e., writing some, but not all, of its data.
- Fixed a couple of minor bugs. (Thanks to "maksqwe1<at>ukr.net".)


Выводы? Выводы слегка не цензурные. Причем как относительно производителя прошивки, так и относительно самого LIVE555 сервера.

К слову, производитель прошивки и модулей — topsee; а используемый внутри линукс и обвязки от MontaVista. Написал всем участникам этой цепочки, жду надеюсь и верю.
Tags:
Hubs:
Total votes 110: ↑104 and ↓6 +98
Views 77K
Comments Comments 77