За кулисами Google Colab +2


Заглянем во внутренности Google Colab и узнаем, как можно подстроить Colab под свои рабочие нужды, а не подстраиваться под ограничения этого инструмента. Подробности рассказываем к старту флагманского курса по Data Science.


Google Colaboratory, более известная как «Colab», — бесплатная платформа для блокнотов Jupyter. Кроме среды запуска блокнотов Python и R Colab позволяет совместно использовать свободный доступ к ограниченному количеству GPU и TPU.

Colab быстро стала де-факто средой программирования блокнотов Jupyter, но использовать Colab для чего-то кроме блокнотов Jupyter невероятно сложно. Особенно это верно для инженеров ML, желающих создать модели и вывести их со стадии блокнота. Блокнот идеален в исследовании, но плохо сочетается с широким инструментарием MLOps, кодифицирующим обучение в формальный конвейер.

За кулисами

Секретный соус Colab — это его бекенд: инфраструктурные серверы Google позволяют запускать код одним щелчком пальцев или одним нажатием кнопки. Так, наш первый шаг — анализ этого бекенд-API. Самое простое — проверить вызовы API Colab во время его нормальной работы:

  • Запускаем DevTools Chrome, находим вкладку Network и пытаемся запустить ячейку кода.

  • DevTools начинает записывать каждый запрос Colab — и почти сразу находим нечто интересное:

Похоже, URL /tun/m/<id>/socket.io — это прокси-сервер сокета Jupyter на удалённой машине.Если мы запустим панель Files (по умолчанию она показывает каталог /content) из левой панели UI Colab, то получим другой интересный запрос:

На этот раз тело ответа — JSON, перечисляющий файлы на удалённом узле. Похоже, что URL /tun/m/<id>/api/contents/ указывает на сервис, предоставляющий метаданные файла:

Двойной щелчок по файлу на панели Files загружает и отображает этот файл в Colab. Если мы попытаемся нажать на /content/sample_data/README.md, то заметим запрос к /tun/m/<id>/files/, который возвращает содержимое этого файла:

Понятно, что https://colab.research.google.com/tun/m/<id>/ — обратный прокси-сервер относительно сервера, на котором запущен экземпляр Colab, предоставляющий конечные точки /socket.io, /files и /api/contents.

Попробуем посмотреть, работает ли какой-то из сервисов внутри экземпляра контейнера Colab. Для этого запустим lsof, установленную внутри контейнера Colab: lsof -iTCP -sTCP:LISTEN, чтобы перечислить все процессы, которые прослушивают TCP-порт:

Угу. Перспективными для изучения поверхностями выглядят процессы colab-fileshim, node и jupyter-notebook. Мы уже имели дело с панелью Files, поэтому сначала посмотрим colab-fileshim. Его PID — 28, а раз так, проверим файловую систему /proc, чтобы увидеть всю команду CLI:

Следующий шаг — исследовать /usr/local/bin/colab-fileshim.py. По иронии судьбы сделать это можно, перейдя к нему на самой панели Files. В основном программа кажется неинтересным файловым сервером. Мало что понятно, кроме того, что сам сервер отвечает на запрос localhost:3453/files фактическим содержимым файла, а на localhost:3453/api/contents — метаданными JSON). А значит, Colab перенаправляет эти запросы с URL туннеля на порт 3453 самого экземпляра.

На вкладке Network Chrome DevTools мы можем щёлкнуть запрос правой кнопкой мыши, чтобы скопировать соответствующую команду cURL и воспроизвести её. Вот параметры cURL для просмотра README.md:

$ curl 'https://colab.research.google.com/tun/m/m-s-3oy94z70yrj59/files/content/sample_data/README.md?authuser=0' \
  -H 'authority: colab.research.google.com' \
  -H 'x-colab-tunnel: Google' \
  -H 'accept: */*' \
  -H 'dnt: 1' \
  -H 'accept-language: en-US,en;q=0.9' \
  -H 'sec-fetch-site: same-origin' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: https://colab.research.google.com/' \
  -H 'cookie: <<READACTED>>' \
  -H 'range: bytes=0-930' \
  --compressed

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

$ curl 'https://colab.research.google.com/tun/m/m-s-3oy94z70yrj59/files/content/sample_data/README.md?authuser=0' \
  -H 'x-colab-tunnel: Google' \
  -H 'cookie: <<READACTED>>'

Заголовок x-colab-tunnel предназначен помешать нам (или злоумышленникам) делать эти запросы из обычных вкладок браузера, якобы для остановки атак XSS. Заголовок cookie отвечает за аутентификацию Google, которая доказывает, что у нас есть право доступа к экземпляру блокнота. Сookie-файл длинный и громоздкий, поэтому сохраним его в переменной оболочки $COLAB_COOKIE.

$ COLAB_COOKIE="<<PREVIOUSLY REDACTED VALUE>>"
# Usage: $ curl ... -H "cookie: $COLAB_COOKIE"

1. Замена серверов Colab своими серверами

Посмотрим, можно ли использовать обнаруженный обратный прокси для туннелирования запросов. Не будем возиться с существующим сервером colab-fileshim.py, а просто заменим процесс нашим сервером! Запускаем pkill -f colab-fileshim, чтобы убить процесс, а затем на том же порту запустить наш сервер. Демонстрация — HTTP-сервер по умолчанию для обслуживания наших файлов по адресу localhost:3453/files.

Вуаля! Теперь перепишем команду cURL для загрузки наших файлов!

$ curl 'https://colab.research.google.com/tun/m/m-s-3oy94z70yrj59/files/message.txt?authuser=0' \
  -H "x-colab-tunnel: Google" -H "cookie: $COLAB_COOKIE"
Hi! You've reached our own file server!
$ curl 'https://colab.research.google.com/tun/m/m-s-3oy94z70yrj59/files/shadow?authuser=0' \
  -H "x-colab-tunnel: Google" -H "cookie: $COLAB_COOKIE"
root:*:18585:0:99999:7:::
daemon:*:18585:0:99999:7:::
bin:*:18585:0:99999:7:::
sys:*:18585:0:99999:7:::
sync:*:18585:0:99999:7:::
# ...

Обратите внимание на строку лога в ячейке Colab. Она доказывает, что запрос обработал наш сервер:

Serving HTTP on 0.0.0.0 port 3453 (http://0.0.0.0:3453/) ...
172.28.0.1 - - [22/Jun/2022 16:43:10] "GET /files/message.txt HTTP/1.1" 200 -
172.28.0.1 - - [22/Jun/2022 16:43:16] "GET /files/shadow HTTP/1.1" 200 -

К сожалению, из-за требования заголовка x-colab-tunnel: Google легко получить доступ к серверу из браузера нельзя.

Дальнейшая разведка

Взглянем на другой интересный процесс — это node. Проверим /proc/7/cmdline и увидим, что процесс выполняет /datalab/web/app.js. А там мы обнаружим, что /datalab/web содержит довольно стандартное приложение NodeJS. Наряду с маршрутом /socketio/ оно также предоставляет маршрут /_proxy/{port}/. Это должно позволить получить доступ к любому URL с любого порта на экземпляре Colab:

$ curl 'https://colab.research.google.com/tun/m/m-s-3oy94z70yrj59/_proxy/1234/some/path?authuser=0' \
  -H "x-colab-tunnel: Google" -H "cookie: $COLAB_COOKIE"
<html><head><title>Colab Forwarded Server!</title></head><body><h1>Hi from Colab!</h1><h2>path=/some/path</h2></body></html>%

Если бы только мы могли просматривать эту страницу из вкладки браузера… К сожалению, Colab отказывается передавать запросы, если у них нет x-colab-tunnel: Google. В попытке посетить эти URL-адреса из браузера мы увидим HTTP-ошибку 400:

Отображение целых веб-страниц

К счастью, для вставки HTTP-заголовков в запросы браузера можно воспользоваться расширением Chrome. Настроим его на отправку x-colab-tunnel: Google по всем запросам:

И запустим туннельные URL прямо в браузере!

К Jupyter!

Напоследок посмотрим на третий и последний интересный процесс — jupyter-notebook, который слушает порт 9000. Можно попробовать посетить порт из браузера, используя наш прокси и трюк с заголовком, посетить /tun/m/<id>/_proxy/9000. К сожалению, вместо UI Jupyter мы увидим страницу HTTP-ошибки 500.

Странно. Пытаемся запустить !curl -i localhost:9000 из самого блокнота, но всё равно получаем сообщение об ошибке:

Прошлый вывод lsof даёт нам подсказку: вместо прослушивания 0.0.0.0/:: (все IP-адреса по всем интерфейсам) Jupyter прослушивает только приватный IP, предоставленный экземпляру Colab. Так сделано, по-видимому, чтобы избежать раскрытия интерфейса Jupyter. Конечно. Google не старался изо всех сил, чтобы скрыть его.

Чтобы обойти ограничение адреса прослушивания, нужно создать процесс, прослушивающий все интерфейсы и IP, перенаправляющий весь получаемый трафик на конкретный IP, который прослушивает Jupyter. Для этого можно установить socat («Socket Cat») и через него переслать трафик с localhost:9000 на $HOSTNAME:9000 и обратно:

Началось! Перезагрузив URL-адрес в браузере, увидим фрагменты UI Jupyter, но он явно сломан.

Jupyter ожидает, что доступ к нему будет получен в корне домена (/), но наш туннель Colab имеет путь /tun/m/<id>/_proxy/9000. Это портит все абсолютные пути к ресурсам, например CSS и JS-файлам. Простого решения здесь нет — для перенаправления трафика на наш сервер понадобится целый (под)домен.

Показываем UI Jupyter

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

Чтобы узнать, как использовать официальную переадресацию портов Colab, вам нужно открыть вкладку Code Snippets на левой боковой панели и найти фрагмент обработки вывода. Нажмите «View Source Notebook» и вы попадёте в advanced_outputs.ipynb, сборник фрагментов от Colab. Эти фрагменты демонстрируют пугающе документированные функции платформы. Конкретный фрагмент, который нам нужен, можно найти в разделе «Browsing to servers executing on the kernel».

Этим фрагментом мы можем воспользоваться для предоставления пользовательского интерфейса Jupyter на поддомене:

И теперь можно щёлкнуть ссылку, добавить /tree к URL, чтобы унять Jupyter, и увидеть полностью работающий UI Jupyter! Ну, почти полностью. Google, похоже, ограничил официальный прокси только запросами GET, что позволяет просматривать, но не запускать блокноты.

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

Выбрать другую востребованную профессию.




Комментарии (1):

  1. tabtre
    /#24478154

    Интересно, когда вы переводили статью…
    Вы задавались вопросом, чем она полезна?!