В данной статье я расскажу, как совместил U-Boot и TCP/IP стек LWIP, и c использованием LWIP написал веб-консоль на WebSocket, очень простой DHCP-сервер и HTTP-сервер. Код лежит на репозиториях U-Boot и LWIP.
Всё началось, когда мне подарили для экспериментов роутер Xiaomi Mi Wi-Fi Router 3C.
Я начал с компиляции из исходников OpenWRT для роутера, но это быстро наскучило. Так как с прошлых экспериментов у меня оставался программатор и USB-UART-конвертер CH341A, то я решил попробовать поменять загрузчик роутера, залив его напрямую в SPI Flash память роутера.
Загрузчик Breed
На 4PDA была готовая сборка OpenWRT c загрузчиком Breed. Это оказался кастомный загрузчик c Web-GUI и возможностью через браузер загружать прошивки. Но чтобы зайти в Web-GUI, необходимо нажать Enter в UART консоли. Как подключить UART, написано тут. А пользоваться можно через PuTTY. Находим в диспетчере устройств, какой виртуальный COM-порт создал конвертер.
А дальше вписываем этот номер в PuTTY:
И видим лог загрузчика Breed:
Остановив автозагрузку Linux, мы можем попасть в Web-GUI по адресу 192.168.1.1
. Главное, чтобы этот адрес не совпадал с вашим основным роутером.
А также попасть в меню загрузки прошивки:
Мне очень понравилась идея с 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:
И увидим в PuTTY лог загрузчика 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
Код передачи пакета от драйвера 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” выключить.
Запускаем сборку:
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. Но код реализации не является эталоном, так как там очень много упрощений и условностей, и был сделан просто чтобы консоль хоть как-то заработала.
Комментарии (10)
MinimumLaw
29.08.2023 10:27+4Да, бог с ней с секьюрностью. Но статья про расширение возможностей U-Boot с использованием U-Boot API однозначно достойна плюса.
karen07 Автор
29.08.2023 10:27+2Были мысли насчет TLS, но как представил сколько придется писать, перехотелось)
MinimumLaw
29.08.2023 10:27+3Не, идея безусловно интересная. Правда обычно ту самую консоль всячески спрятать пытаются. Уж больно мощный инструмент. Можно всякого натворить... А про U-Boot API многие слышали (из тех, конечно, кто вообще про U-boot слышал), но не многие пользуются.
У меня однажды было расширение функционала таким образом. Но оно было направлено как раз на аварийное восстановление прошивки и было это уже лет десять назад. Сегодня скорее смотрим на UEFI в том же U-Boot'е. Кто бы мог подумать несколько лет назад, что grub будет востребован и в мире embedded. А все идет семимильными шагами именно к этому.
apevzner
29.08.2023 10:27+1А смысл? Все равно, TLS-сертификаты привязаны к домену, а устройство вряд ли имеет стабильное доменное имя.
karen07 Автор
29.08.2023 10:27Можно будет реализовать очень простой DNS сервер на загрузчике)
apevzner
29.08.2023 10:27Тогда придется еще и реальный домен зарегистрировать и получить на него сертификат.
Сетевые принтеры и сканеры обычно умеют в TLS, но прошитый в них на фабрике сертификат - self-signed, стандартную проверку, очевидно, не проходит...
arren
29.08.2023 10:27+1Не обязательно к доменам, можно указать в SAN IP-адреса, а он у автора вроде как фиксированый получился даже, более того, сертификаты еще можно использовать и для аутентификации входящих соединений по CA
NutsUnderline
29.08.2023 10:27для breed есть специальная утилита breedenter которая позволяет зайти в него без использования gpio и uart. Но вообще отдельный спорт: патчинг бинарника breed под нужный gpio для входа (который на каждом SOC/роутере свой)
NutsUnderline
29.08.2023 10:27+1буквально вчера пытался просто откомпилировать uboot (готовый кастом, но все равно не осилил) - так что восхищен масштабом происходящего, так сказать, сидя в первом ряде
dlinyj
Какой зачётный велосипед, но прикольно. Работает только если в u-boot есть драйвера сетевой карты (в маршрутизаторах должны быть, но не всегда бывают в самоделках.