Веб-консоль для загрузчика U-Boot

—

от автора

В данной статье я расскажу, как совместил U-Boot и TCP/IP стек LWIP, и c использованием LWIP написал веб-консоль на WebSocket, очень простой DHCP-сервер и HTTP-сервер. Код лежит на репозиториях U-Boot и LWIP.

Веб-консоль

Веб-консоль

Всё началось, когда мне подарили для экспериментов роутер Xiaomi Mi Wi-Fi Router 3C.

Xiaomi Mi Wi-Fi Router 3C

Xiaomi Mi Wi-Fi Router 3C

Я начал с компиляции из исходников OpenWRT для роутера, но это быстро наскучило. Так как с прошлых экспериментов у меня оставался программатор и USB-UART-конвертер CH341A, то я решил попробовать поменять загрузчик роутера, залив его напрямую в SPI Flash память роутера.

CH341A и Xiaomi Mi Wi-Fi Router 3C

CH341A и Xiaomi Mi Wi-Fi Router 3C

Загрузчик Breed

На 4PDA была готовая сборка OpenWRT c загрузчиком Breed. Это оказался кастомный загрузчик c Web-GUI и возможностью через браузер загружать прошивки. Но чтобы зайти в Web-GUI, необходимо нажать Enter в UART консоли. Как подключить UART, написано тут. А пользоваться можно через PuTTY. Находим в диспетчере устройств, какой виртуальный COM-порт создал конвертер.

COM порт

COM порт

А дальше вписываем этот номер в PuTTY:

PuTTy

PuTTy

И видим лог загрузчика Breed:

Лог загрузчика Breed

Лог загрузчика Breed

Остановив автозагрузку Linux, мы можем попасть в Web-GUI по адресу 192.168.1.1. Главное, чтобы этот адрес не совпадал с вашим основным роутером.

Информация об устройстве в Web-GUI

Информация об устройстве в Web-GUI

А также попасть в меню загрузки прошивки:

Раздел загрузки в Web-GUI

Раздел загрузки в Web-GUI

Мне очень понравилась идея с Web-GUI и мне захотелось повторить её на основе open source кода. Для этого я использовал популярный open source загрузчик U-Boot.

U-Boot

Роутер построен на базе MediaTek MT7628AN, а для этого чипа в U-Boot есть поддержка. Но даже сборка U-Boot из исходников оказалась нетривиальной задачей для человека, который сталкивается с этим в первый раз. Я буду показывать на примере виртуальной машины с Ubuntu 22.04 LTS.

Необходимо поставить пакеты:

sudo apt update sudo apt upgrade sudo apt install build-essential bison flex libncurses5-dev libncursesw5-dev unzip \ qemu-system-mips gcc-mips-linux-gnu colordiff firefox ncdu dos2unix libssl-dev \  bc u-boot-tools

Выкачать U-Boot:

git clone https://github.com/u-boot/u-boot.git

Выставить параметры кросс-компиляции:

export ARCH=mips export CROSS_COMPILE=mips-linux-gnu-

Применить конфигурацию устройства:

make mt7628_rfb_defconfig

Запустить сборку:

make

Для упрощённого заливания прошивки на роутер, я оставил на нём загрузчик Breed, а загрузчик U-Boot запаковывал в образ ядра Linux. Получается, загрузчик Breed запускал загрузчик U-Boot. А первоначально я залил загрузчик Breed через программатор.

mkimage -A mips -T kernel -C none -O linux -a 0x80200000 -e 0x80200000 \ -n "U-Boot" -d u-boot.bin u-boot.img

Загрузим образ загрузчика U-Boot:

Загрузка u-boot.img

Загрузка u-boot.img

И увидим в PuTTY лог загрузчика U-Boot:

Лог U-Boot

Лог U-Boot

Так как Breed имеет Web-GUI, то для U-Boot захотелось сделать хотя бы веб-консоль.

Для веб-консоли необходим WebSocket, а он в свою очередь основан на TCP. По умолчанию U-Boot умеет передавать данные только через UDP. А значит для использования TCP необходимо воспользоваться внешним TCP/IP стеком. Выбор пал на популярный LWIP.

LWIP

Первым делом я выкачал LWIP в папку /lib проекта U-Boot:

git clone https://github.com/lwip-tcpip/lwip.git

Сборка U-Boot основа на Kconfig, поэтому пришлось добавить сборку через Kconfig для LWIP. Подробнее про Kconfig можно почитать тут.

Пример части одного из makefile:
ccflags-y += -I$(obj)/../include  obj-y += \     init.o \ def.o \ dns.o \ inet_chksum.o \ ip.o \ mem.o \ memp.o \

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

Пример части lwipopts.h:
#ifndef __LWIPOPTS_H__ #define __LWIPOPTS_H__  #define NO_SYS                  1 #define SYS_LIGHTWEIGHT_PROT    0  #define LWIP_NETCONN                    0 #define LWIP_SOCKET                     0 #define LWIP_DHCP                       1  #define MEM_ALIGNMENT           4 #define MEM_SIZE                        (8 * 1024 * 1024)

А также необходимо создать файл cc.h, который хранит настройки для компилятора.

Пример части cc.h:
#ifndef __ARCH_CC_H__ #define __ARCH_CC_H__  #include <stdio.h> #include <stdlib.h> #include <string.h>  int atoi(const char *str);  #ifdef CONFIG_SYS_BIG_ENDIAN #define BYTE_ORDER BIG_ENDIAN #else #define BYTE_ORDER LITTLE_ENDIAN #endif  #define LWIP_NO_LIMITS_H 1 #define LWIP_NO_CTYPE_H 1  typedef uint8_t     u8_t; typedef int8_t      s8_t;

Методом проб и ошибок эти файлы были созданы.

Теперь необходимо передать пакет от драйвера Ethernet к LWIP.

У U-Boot есть механизм добавления callback-функции обработки входящих пакетов. Для этого необходимо включить API в конфигурации сборки.

make menuconfig
make menuconfig

make menuconfig
Код передачи пакета от драйвера Ethernet к LWIP
void eth_save_packet_lwip(void* packet, int length) { if (length > 0) { struct pbuf* p = pbuf_alloc(PBUF_RAW, length, PBUF_POOL); if (p != NULL) { pbuf_take(p, packet, length); if (netif.input(p, &netif) != ERR_OK) { pbuf_free(p); } } } }
push_packet = eth_save_packet_lwip;

Отправка пакета настраивается через функцию U-Boot eth_send.

Код передачи пакета от LWIP к драйверу Ethernet
err_t netif_output(struct netif* netif, struct pbuf* p) { unsigned char mac_send_buffer[p->tot_len]; pbuf_copy_partial(p, (void*)mac_send_buffer, p->tot_len, 0); eth_send(mac_send_buffer, p->tot_len); return ERR_OK; }

Инициализация LWIP, настройка IP-адреса, маски подсети и шлюза по умолчанию.

Код инициализации LWIP, настройка IP-адреса, маски подсети и шлюза по умолчанию.
eth_halt(); eth_init();  ip4_addr_t addr; ip4_addr_t netmask; ip4_addr_t gw;  IP4_ADDR(&addr, 192, 168, 10, 1); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 10, 2);  lwip_init();  netif_add(&netif, &addr, &netmask, &gw, NULL, netif_set_opts, netif_input);  netif.name[0] = 'e'; netif.name[1] = '0'; netif_set_default(&netif);

Настройка MTU и MAC адреса.

Код настройки MTU и MAC адреса.
err_t netif_set_opts(struct netif* netif) { netif->linkoutput = netif_output; netif->output = etharp_output; netif->mtu = 1500; netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP \     | NETIF_FLAG_ETHERNET | NETIF_FLAG_LINK_UP | NETIF_FLAG_UP; netif->hwaddr_len = 6;  if (env_get("ethaddr")) string_to_enetaddr(env_get("ethaddr"), netif->hwaddr); else memset(netif->hwaddr, 0, 6);  return ERR_OK; }

DHCP-сервер

Для удобной работы с веб-консолью необходимо, чтобы загрузчик выдавал пользователю IP-адрес, для этого пришлось написать очень упрощённый DHC-сервер, который на любой случай отдаёт один и тот же пакет, пользователю выдается IP-адрес 192.168.10.2, а загрузчик имеет адрес 192.168.10.1. Подробнее почитать можно тут. Код приведён в lwip_u_boot_port.c.

Код DHCP-сервера
struct udp_pcb *dhcp = udp_new(); udp_bind(dhcp, IP_ADDR_ANY, 67); udp_recv(dhcp , dhcp_recv, NULL);
void dhcp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,                 const ip_addr_t *addr, u16_t port) { if(p == NULL) return;  dhcps_msg dhcp_rec;  int data_len = p->tot_len; pbuf_copy_partial(p, (void*)&dhcp_rec, data_len, 0); pbuf_free(p);  int i = 4; while(dhcp_rec.options[i] != 255 && dhcp_rec.options[i] != 53) { i += dhcp_rec.options[i+1] + 2; }

HTTP-сервер

Чтобы веб-консоль работала, необходимо отдать http-страницу. Так как у нас статичная http-страница, то нам хватит самого простого http-сервера, который на любой случай отдаёт один и тот же запрос. Подробнее можно почитать тут. Но так как страница содержала в себе зависимости, пришлось их вставить напрямую в страницу для работы без интернета. Для этого очень пригодилась возможность команды xxd переводить файл в массив для C. Подробнее код приведён в lwip_u_boot_port.c.

xxd -include index.html
Пример вывода xxd
unsigned char http_ans[] = {   0x3c, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x0a, 0x3c, 0x68, 0x65, 0x61, 0x64,   0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3e, 0x0a, 0x09,   0x2e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x7b, 0x66, 0x6f, 0x6e, 0x74, 0x2d,   0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x2d, 0x73, 0x65, 0x74, 0x74,   0x69, 0x6e, 0x67, 0x73, 0x3a, 0x22, 0x6c, 0x69, 0x67, 0x61, 0x22, 0x20,   0x30, 0x3b, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x72,   0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x3b, 0x75, 0x73, 0x65, 0x72,   0x2d, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x3a, 0x6e, 0x6f, 0x6e, 0x65,   0x3b, 0x2d, 0x6d, 0x73, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73, 0x65,   0x6c, 0x65, 0x63, 0x74, 0x3a, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, 0x2d, 0x77,   0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73,   0x65, 0x6c, 0x65, 0x63, 0x74, 0x3a, 0x6e, 0x6f, 0x6e, 0x65, 0x7d, 0x2e,   0x78, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2c,   0x2e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x3a, 0x66, 0x6f, 0x63, 0x75, 0x73,   0x7b, 0x6f, 0x75, 0x74, 0x6c, 0x69, 0x6e, 0x65, 0x3a, 0x30, 0x7d, 0x2e,   0x78, 0x74, 0x65, 0x72, 0x6d, 0x20, 0x2e, 0x78, 0x74, 0x65, 0x72, 0x6d,   0x2d, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x73, 0x7b, 0x70, 0x6f, 0x73,   0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75,   0x74, 0x65, 0x3b, 0x74, 0x6f, 0x70, 0x3a, 0x30, 0x3b, 0x7a, 0x2d, 0x69,   0x6e, 0x64, 0x65, 0x78, 0x3a, 0x31, 0x30, 0x7d, 0x2e, 0x78, 0x74, 0x65,
unsigned int http_ans_len = 227384;

Страница без вставки зависимостей
<html> <head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css" />     <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.js"></script> </head> <body>     <div id="terminal"></div>     <script>         var opts = { cols: 80, rows: 54, convertEol : 1         }          var term = new Terminal(opts);         term.open(document.getElementById('terminal'));  var ws = new WebSocket("ws://192.168.10.1:3000"); ws.binaryType = "arraybuffer";  ws.addEventListener('message', function (event) { term.write(event.data); });          term.on("key", function(key, ev) { if (ev.keyCode === 13) { ws.send("\n"); } else if (ev.keyCode === 8) { ws.send("\b"); } else { ws.send(key); }         });     </script> </body> </html>

Код HTTP сервера
struct tcp_pcb* http = tcp_new(); tcp_bind(http, IP_ADDR_ANY, 80); http = tcp_listen_with_backlog(http, TCP_DEFAULT_LISTEN_BACKLOG); tcp_accept(http, http_accept);
err_t http_recv(void* arg, struct tcp_pcb* tpcb, struct pbuf* p,                 err_t err) { int data_len = p->tot_len; char tcp_rec[data_len]; pbuf_copy_partial(p, (void*)tcp_rec, data_len, 0); tcp_recved(tpcb, data_len); pbuf_free(p);  char answer_html[1000]; sprintf(answer_html, HTTP_RSP, http_ans_len);  tcp_write(tpcb, answer_html, strlen(answer_html), 0x01);  http_ans_sended = 0;  return ERR_OK; }
const char HTTP_RSP[] = \ "HTTP/1.1 200 OK\r\n" \ "Content-Length: %d\r\n" \ "Content-Type: text/html\r\n\r\n";

WebSocket-сервер

С WebSocket я работал в первый раз, пришлось вникать, как он работает.  Протокол используем SHA1 и Base64, эти библиотеки необходимо было добавить в исходники. Подробнее, как работает WebSocket-протокол, можно почитать тут. Подробнее код приведён в lwip_u_boot_port.c.

Код WebSocket-сервера
const char WS_RSP[] = \ "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade: websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n\r\n"; const char WS_GUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const char WS_KEY[] = "Sec-WebSocket-Key: ";
err_t websocket_recv(void* arg, struct tcp_pcb* tpcb, struct pbuf* p,                      err_t err) { if (p == NULL) { web_socket_open = 0; globa_tcp = NULL; tcp_get = 0; return ERR_OK; }  int data_len = p->tot_len; char tcp_rec[data_len]; pbuf_copy_partial(p, (void*)tcp_rec, data_len, 0); tcp_recved(tpcb, data_len);  if(web_socket_open == 0){ char * sec_websocket_position_start = strstr(tcp_rec, WS_KEY); if(sec_websocket_position_start){

Веб-консоль

Теперь, когда у нас есть все составляющие, мы можем найти место ввода/вывода обычной консоли U-Boot и заменить на символы пришедшие/ушедшие из WebSocket. Инициализацию веб-консоли мы вставляем перед бесконечным циклом, который ждет команд, файл main.c. А сама консоль находится в файле console.c. Отправляемый символ или символы необходимо преобразовать, используя правила WebSocket.

Код веб-консоли
lwip_u_boot_port();  cli_loop();
#ifndef CONFIG_SPL_BUILD if(push_packet) { eth_rx(); char tmp = tcp_get; tcp_get = 0; if(tmp) return tmp; } #endif
#ifndef CONFIG_SPL_BUILD if(globa_tcp) { unsigned char buf[3]; buf[0] = 0x80 | 0x01; buf[1] = 1; buf[2] = c; tcp_write(globa_tcp, buf, 3, 1); tcp_output(globa_tcp); } else { #endif
#ifndef CONFIG_SPL_BUILD if(globa_tcp) { int len = strlen(s); unsigned char buf[150]; while (len) { int send_len = min(len, 125); buf[0] = 0x80 | 0x01; buf[1] = send_len; memcpy(&buf[2], s, send_len); tcp_write(globa_tcp, buf, send_len + 2, 1); len -= send_len; s += send_len; } tcp_output(globa_tcp); } else { #endif

Запуск на эмуляторе QEMU

Каждый раз заливать загрузчик на роутер — не самая быстрая затея, и для удобной проверки работоспособности я решил настроить U-Boot под QEMU.

Необходимо поставить пакеты:

sudo apt update sudo apt upgrade sudo apt install build-essential bison flex libncurses5-dev libncursesw5-dev unzip \ qemu-system-mips gcc-mips-linux-gnu colordiff firefox ncdu dos2unix libssl-dev \  bc u-boot-tools

Для удобства выкачивания проекта я добавил LWIP как подмодуль для U-Boot. Выкачать их вместе можно командой:

git clone --recurse-submodules https://github.com/karen07/u-boot.git

Запускаем конфигурацию под другое устройство Malta:

make malta_defconfig

Включаем U-Boot API в make menuconfig, как на примере повыше, а также для удобства можно выключить автозагрузку в меню “Boot options” -> “Autoboot options” -> ”Autoboot” выключить.

Autoboot опция

Autoboot опция

Запускаем сборку:

make

Далее необходимо создать виртуальный  Ethernet:

sudo ip tuntap add dev tap0 mode tap && sudo ip link set dev tap0 up

Запускаем U-Boot в Qemu:

sudo qemu-system-mips -M malta -m 256 --nographic -net nic \ -net tap,ifname=tap0,script=no,downscript=no -bios u-boot.bin

Получаем IP-адрес:

sudo dhclient tap0

Запускаем браузер и заходим на 192.168.10.1:

Веб-консоль

Веб-консоль

Тем самым каждый можем повторить у себя запуск веб-консоли в эмуляторе QEMU.

Выводы

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


ссылка на оригинал статьи https://habr.com/ru/articles/757614/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *