В данной статье я расскажу, как совместил 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. Но код реализации не является эталоном, так как там очень много упрощений и условностей, и был сделан просто чтобы консоль хоть как-то заработала.

Комментарии (10)


  1. dlinyj
    29.08.2023 10:27
    +4

    Какой зачётный велосипед, но прикольно. Работает только если в u-boot есть драйвера сетевой карты (в маршрутизаторах должны быть, но не всегда бывают в самоделках.


  1. MinimumLaw
    29.08.2023 10:27
    +4

    Да, бог с ней с секьюрностью. Но статья про расширение возможностей U-Boot с использованием U-Boot API однозначно достойна плюса.


    1. karen07 Автор
      29.08.2023 10:27
      +2

      Были мысли насчет TLS, но как представил сколько придется писать, перехотелось)


      1. MinimumLaw
        29.08.2023 10:27
        +3

        Не, идея безусловно интересная. Правда обычно ту самую консоль всячески спрятать пытаются. Уж больно мощный инструмент. Можно всякого натворить... А про U-Boot API многие слышали (из тех, конечно, кто вообще про U-boot слышал), но не многие пользуются.

        У меня однажды было расширение функционала таким образом. Но оно было направлено как раз на аварийное восстановление прошивки и было это уже лет десять назад. Сегодня скорее смотрим на UEFI в том же U-Boot'е. Кто бы мог подумать несколько лет назад, что grub будет востребован и в мире embedded. А все идет семимильными шагами именно к этому.


      1. apevzner
        29.08.2023 10:27
        +1

        А смысл? Все равно, TLS-сертификаты привязаны к домену, а устройство вряд ли имеет стабильное доменное имя.


        1. karen07 Автор
          29.08.2023 10:27

          Можно будет реализовать очень простой DNS сервер на загрузчике)


          1. apevzner
            29.08.2023 10:27

            Тогда придется еще и реальный домен зарегистрировать и получить на него сертификат.

            Сетевые принтеры и сканеры обычно умеют в TLS, но прошитый в них на фабрике сертификат - self-signed, стандартную проверку, очевидно, не проходит...


        1. arren
          29.08.2023 10:27
          +1

          Не обязательно к доменам, можно указать в SAN IP-адреса, а он у автора вроде как фиксированый получился даже, более того, сертификаты еще можно использовать и для аутентификации входящих соединений по CA


  1. NutsUnderline
    29.08.2023 10:27

    для breed есть специальная утилита breedenter которая позволяет зайти в него без использования gpio и uart. Но вообще отдельный спорт: патчинг бинарника breed под нужный gpio для входа (который на каждом SOC/роутере свой)


  1. NutsUnderline
    29.08.2023 10:27
    +1

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