Safari сдаётся, а мы — нет: как iPhone 3GS научился читать DTF

У меня есть iPhone 3GS. Телефон 2009 года, iOS 6, последняя, которая на него встала. Захотелось открыть на нём DTF. Спойлер: получилось. Но по дороге выяснилось, что этот телефон в принципе не может зайти на DTF — и это оказалось самой интересной частью

Safari сдаётся, а мы — нет: как iPhone 3GS научился читать DTF

Стена

Первое, что я сделал — открыл DTF в Safari прямо на устройстве.

Safari не удаётся открыть страницу, так как не удалось установить безопасное соединение с сервером.

Формулировка важна. Не «сертификату нельзя доверять», а «не удалось установить соединение» — то есть разговор оборвался на самом первом рукопожатии, до проверки сертификатов дело не дошло вообще

Замерил, что сервер вообще принимает

tls1: отклонён tls1_1: отклонён tls1_2: ПОДДЕРЖИВАЕТСЯ Cipher: ECDHE-RSA-AES128-GCM-SHA256 Issuer: C=US, O=Let's Encrypt, CN=YR1

Три приговора сразу:

  1. Только TLS 1.2. Старые версии протокола сервер не принимает.
  2. Только AES-GCM. Этого режима шифрования в системной криптографии iOS 6 нет — он появился позже.
  3. Сертификат от Let's Encrypt. Его корня в хранилище 2012 года не существовало: сам Let's Encrypt появился в 2015-м.

Любого из трёх пунктов хватило бы. Системная сеть iOS 6 к DTF не подключится никогда — ни в браузере, ни в приложении

Отдельный забавный момент. Сначала я проверял через Opera Mini, и там DTF открывался. Радость была недолгой: в адресной строке значилось global-4-lvs-lamport.ope… — адрес сервера Opera. Opera Mini не ходит на сайты сама, она отправляет адрес на свои серверы, те открывают страницу и присылают телефону готовый результат. То есть с DTF разговаривала Opera, а не мой телефон. Проверка не засчитана

Решение — своя сеть целиком

Раз системному стеку доверять нельзя — не используем его вообще. Внутри приложения работает своя криптография: голый сокет, mbedTLS, свой HTTP и корневые сертификаты, вкомпилированные прямо в бинарник. Ни одной системной сетевой функции

Первым делом я собрал не приложение, а пробу — крошечную программу, которая делает один запрос и отчитывается по каждому этапу отдельно. Чтобы в случае провала сразу знать, обо что именно споткнулись

Вот что она напечатала на 3GS:

[ OK ] 3. TCP connected to api.dtf.ru:443 [ OK ] 4. TLS handshake: TLSv1.2 / TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256 [ OK ] 5. certificate verified against bundled roots [ OK ] 7. response: HTTP/1.1 200 OK >>> received 324588 bytes of feed data >>> RESULT: SUCCESS — iOS 6 CAN talk to DTF

Телефон, который полчаса назад не мог открыть DTF даже в браузере, установил современное шифрование и скачал 324 килобайта живой ленты. Стена пробита

Сборка без Mac

Отдельная история — чем всё это компилировать. У меня Windows и нет ни Mac, ни Xcode.

Оказалось, не нужны. Сборка идёт на бесплатной Linux-машине GitHub:

  • компилятор — clang с линковщиком ld64, собранный под Linux
  • SDK — iPhoneOS 9.3 из публичного репозитория theos, с флагом -miphoneos-version-min=6.0
  • подпись — ldid -S, обычная «фейковая» для джейлбрейка

Итоговая команда выглядит буднично:

clang -target armv7-apple-ios6.0 -isysroot "$SDK" \ -miphoneos-version-min=6.0 -arch armv7 \ -fno-builtin -DMBEDTLS_PLATFORM_MS_TIME_ALT \ -framework UIKit -framework Foundation -framework QuartzCore \ main.m dtf_net.m dtf_api.m vc_*.m mbedtls/library/*.c -o DTF

Но чтобы она заработала, пришлось обойти три грабли, которых нет ни в одной инструкции.

Грабля первая: времени не существует

mbedTLS хочет монотонные часы через clock_gettime. Эта функция появилась в iOS только в 10-й версии — при цели 6.0 код просто не компилируется.

Лечится подменой собственной реализацией:

mbedtls_ms_time_t mbedtls_ms_time(void) { struct timeval tv; gettimeofday(&tv, NULL); return (mbedtls_ms_time_t)tv.tv_sec * 1000 + (mbedtls_ms_time_t)tv.tv_usec / 1000; }

Грабля вторая: библиотека-призрак

Линковщик падал с загадочным:

ld: file not found: /usr/lib/system/liblaunch.dylib for architecture armv7

Системная libSystem ссылается на liblaunch, но в публичном SDK этой библиотеки нет. Описание-заглушку линковщик игнорирует — ему нужен настоящий файл. Собираем пустышку с правильным именем:

echo "static int stub;" > stub.c clang -target armv7-apple-ios6.0 -arch armv7 -nostdlib -dynamiclib \ -Wl,-install_name,/usr/lib/system/liblaunch.dylib \ stub.c -o "$SDK/usr/lib/system/liblaunch.dylib"

Грабля третья: memcpy не существует

Дальше вылезло восемь неразрешённых символов: memcpy, memset, memmove, memcmp, memchr, strcmp, strncmp, strchr. Самые базовые функции языка. При этом сокеты, ввод-вывод и работа с памятью слинковались прекрасно — SDK просто не экспортирует именно эти для armv7.

Пишем свои. Тут важен флаг -fno-builtin, иначе компилятор «оптимизирует» тело memcpy в вызов… memcpy, и получится бесконечная рекурсия:

void *memcpy(void *dst, const void *src, size_t n) { unsigned char *d = dst; const unsigned char *s = src; while (n--) *d++ = *s++; return dst; }

Само приложение

Написано на Objective-C с ручным управлением памятью — автоматическое (ARC) для цели iOS 6 современные компиляторы уже не поддерживают, нужной библиотеки в комплекте больше нет.

Интерфейс — стандартный UIKit тех лет. Скевоморфизм тут получается почти даром: глянцевые панели, выпуклые кнопки и утопленные таблицы UIKit 2012 года рисует сам, достаточно задать цвет. Сверху я добавил градиентные ячейки и гравированный текст с белой подсветкой.

Статья поста рендерится как HTML в веб-просмотре. Это сильно экономит силы: перенос текста, заголовки, списки и цитаты получаются бесплатно, вместо ручного расчёта высоты каждого абзаца. Но есть подвох: у веб-просмотра нет работающей сети — та же стена. Поэтому картинки скачиваю я сам и встраиваю прямо в страницу.

Именно это встраивание и подарило самый эффектный баг.

Как я уронил телефон мегабайтом сердечек

Реакции на DTF — картинки. Я встраивал каждую в текст страницы. На посте со 138 комментариями, у каждого из которых свои реакции, одна и та же иконка повторялась сотни раз, и страница разрасталась за мегабайт.

На устройстве с 256 МБ оперативной памяти это выглядело так: заходишь в пост, на долю секунды видишь реакции во весь экран (стили не успевали примениться), и приложение вылетает.

Дальше я потратил три подхода на починку вслепую — и все три мимо. Пробовал ссылаться на файлы вместо встраивания, менял стили, переставлял порядок загрузки. Не работало ничего, а гадать по скриншотам «а вдруг теперь?» — путь в никуда.

Тогда я сделал экран диагностики: пусть телефон сам расскажет, что у него происходит. Он проверяет каждое звено и рисует картинку двумя способами сразу — тегом и фоном.

Safari сдаётся, а мы — нет: как iPhone 3GS научился читать DTF

Ответ пришёл за один запуск:

rx1.png: 2070 байт base64: 2760 символов, начало iVBORw0KGgoAAAAN внизу проверка: слева картинкой, справа фоном → обе нарисовались аватарка: 1101 байт за 1.3 сек, файл на диске: есть 1-й запрос: 6183 байта за 2.0 сек 2-й запрос: 6183 байта за 0.8 сек

Оказалось: движок рисует картинки обоими способами, картинки скачиваются и ложатся на диск. Виноват был не телефон, а мой способ вывода. Перевёл реакции на обычный тег с ограничением в 70 иконок на страницу — и всё встало на место.

Вывод, который стоил трёх итераций: когда чинишь вслепую, потрать один заход на инструмент, который покажет факты. Дальше починка занимает минуты.

Ещё один невидимый баг

Вход в аккаунт «не работал»: жмёшь кнопку — ничего. Ни индикатора, ни ошибки.

Я долго чинил сеть. А проблема была в арифметике: на экране 3.5 дюйма с нижней панелью остаётся 367 точек по высоте, а я разместил надпись о статусе на 348, а крутилку — на 410. Вход честно шёл и честно сообщал о результате — за нижним краем экрана.

Заодно выяснилось, что сервер на успешный вход отвечает {"message":"logined"}, но имя поля с токеном я не угадал. Теперь приложение не гадает, а обходит весь ответ и пробует все длинные строки, похожие на токен.

Скорость

Полное шифрованное рукопожатие на процессоре 600 МГц — самая дорогая операция во всём приложении. А один экран делает несколько запросов подряд.

Решается переиспользованием сессии: приложение запоминает установленное соединение с сервером, и следующие пропускают обмен ключами и проверку цепочки сертификатов.

/* перед рукопожатием */ DTFSessionSlot *slot = DTFSlotFor(host); if (slot && slot->valid) mbedtls_ssl_set_session(&ssl, &slot->session); /* после успешного */ slot->valid = (mbedtls_ssl_get_session(&ssl, &slot->session) == 0);

Замер выше показывает результат: 2.0 секунды → 0.8. Втрое.

Ещё два ускорения:

  • Картинки просим в JPEG. По умолчанию DTF отдаёт WebP, а его iOS 6 декодировать не умеет вообще — достаточно дописать -/format/jpeg/ к адресу.
  • Кэш на диске. Второй заход в пост открывается мгновенно

Что в итоге

Работает: четыре ленты с бесконечной подгрузкой, посты со всеми типами блоков, комментарии ветками с аватарками и реакциями, свои реакции и ответы, личные сообщения с отправкой, поиск, уведомления, закладки, блоги с баннером и счётчиками, вход по почте

Пока нет: видео и гифок, картинок при публикации, мини-игр, черновиков, написание постов.

Весь пакет — 780 килобайт. Внутри — полноценный TLS и 50 иконок реакций.

Телефон 2009 года, операционная система 2012-го, сервер 2026-го. Между ними полтора десятилетия и криптография, которой на момент выпуска этого iPhone ещё не придумали

Скачать можно будет (ха-ха, как будто кто то собрался ставить дтф на свой старенький айфончик) вскоре в телеграм канале Survival on iOS 6

Safari сдаётся, а мы — нет: как iPhone 3GS научился читать DTF

На самом деле приложение я написал не сколько для себя, а просто потому что люблю пердолинг, а пердолиться мне пришлось много, и по приколу, потому что подписка на клауд у меня есть, а токены тратить не на что 😭 так что развлекаем себя по максимуму !

Щас планирую сделать рабочие клиенты для клауда и чат гпт, а потом может быть сделаю клиент дтф для часов самсунг на веар ос, чтобы уведомления приходили, можно было ленту поскролить или с комментов быстро покекать

а и наверное соберу маленькое приложение, которое будет присылать уведомление на часы, когда клод закончил писать код, а ты в это время где то в другом месте находишься (в другой комнате например или на кухне готовишь и всё такое)

51
11
4
2
1
1
1
1
1