Top.Mail.Ru

STM32 и Ethernet. Часть 3. Канальный уровень. Протокол ARP.

Всем доброго времени суток, эта неделя будет неделей Ethernet’а, поскольку пора уже дойти до какой-то более-менее финальной точки 🤔 Продвигаемся вверх по стеку протоколов, сегодня нужно разобрать принятые Ethernet фреймы, сформировать ответные и зацепить между делом протокол ARP. Обо всем этом ниже, начинаем.

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

Модель OSI, канальный уровень.

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

Принцип формирования пакетов.

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

Стек протоколов.

Аналогично протекает процесс приема. Приняли некий Ethernet кадр:

Кадр Ethernet.

Формат заголовка известен, анализируем его, узнаем, что именно за данные содержатся в поле Ethernet Data. Для нашего примера это IP-пакет, который также содержит свой собственный заголовок. И процесс «разворачивания» принятых данных продолжается в таком же ключе:

Обработка кадров.

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

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

Обработка принятых кадров Ethernet.

На теории останавливаться нет никакого смысла, формат пакета обсудили наверно раз 10 уже:

Структура пакета Ethernet.

Переходим к проекту, за основу которого мы возьмем версию из предыдущей части про Ethernet. Работу с ENC28J60 мы реализовали полностью, теперь будет непосредственно обработка и подготовка данных. Добавляем в проект два новых файла:

  • ethernet.c
  • ethernet.h
Проект для STM32.

Архитектура будет выстроена следующим образом. Добавим функцию:

void ETH_Process(ENC28J60_Frame* encFrame)

Она принимает в качестве единственного аргумента указатель на экземпляр добавленной нами ранее в проект структуры ENC28J60_Frame. В main.c добавляем переменную, а также, в основной цикл программы, вызов этой самой функции:

/* USER CODE BEGIN PV */
ENC28J60_Frame frame;

/* USER CODE END PV */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{    
  ETH_Process(&frame);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

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

Итого, функция ETH_Process() является сердцем нашей программы, поэтому в ней мы должны осуществить всю обработку данных. И первым делом объявим структуру для хранения Ethernet фреймов:

typedef struct ETH_Frame 
{
  uint8_t destMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t srcMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint16_t etherType;
  uint8_t data[];
} ETH_Frame;

Как видите, поля структуры в точности соответствуют формату кадра, что и не удивительно 👍 Теперь на очереди минимальный функционал в ETH_Process():

/*----------------------------------------------------------------------------*/
void ETH_Process(ENC28J60_Frame* encFrame)
{
  uint16_t responseSize = 0;
  uint16_t requestSize = ENC28J60_ReceiveFrame(encFrame);

  if (requestSize > 0)
  {
    ETH_Frame* ethFrame = (ETH_Frame*)encFrame->data;
    uint16_t etherType = ntohs(ethFrame->etherType);
  }  
}



/*----------------------------------------------------------------------------*/

Собственно, поэтапно:

  • Осуществляем прием пакета при помощи предназначенной для этого функции ENC28J60_ReceiveFrame(). Она была создана все в той же предыдущей статье цикла.
  • Функция вернет 0, если новых фреймов не обнаружено, что и проверяем последующим условием — if (requestSize > 0).
  • Если новый фрейм в наличии, следует дальнейшая обработка, которая на данном этапе заключается лишь в извлечении поля EtherType из принятых данных.

Небольшой и не очень приятный нюанс — в сети используется прямой порядок байт (big endian), работаем же мы с обратным порядком (little endian), поэтому не остается ничего иного, кроме как создать макросы для этих преобразований. Вынесем их в отдельный файл (common.h), поскольку в будущем они очевидно понадобятся нам из разных модулей/частей/файлов программы. По этой же причине в этот файл добавим объявления переменных, хранящих MAC и IP-адреса:

Пример программы для STM32.
/**
  ******************************************************************************
  * @file           : common.h
  * @brief          : Common project data
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef COMMON_H
#define COMMON_H



/* Includes ------------------------------------------------------------------*/



/* Declarations and definitions ----------------------------------------------*/

#define MAC_ADDRESS_BYTES_NUM                                   6
#define IP_ADDRESS_BYTES_NUM                                    4

#define htons(val)                                              ((val << 8) & 0xFF00) | ((val >> 8) & 0xFF)
#define htonl(val)                                              ((val << 8) & 0xFF0000) | ((val >> 8) & 0xFF00) | ((val << 24) & 0xFF000000) | ((val >> 24) & 0xFF)

#define ntohs(val)                                              htons(val)
#define ntohl(val)                                              htonl(val)



extern uint8_t ipAddr[IP_ADDRESS_BYTES_NUM];
extern  uint8_t macAddr[MAC_ADDRESS_BYTES_NUM];



/* Functions -----------------------------------------------------------------*/



#endif // #ifndef COMMON_H
  • htons — little endian to big endian — host to network short (2 байта)
  • htonl — little endian to big endian — host to network long (4 байта)
  • ntohs — big endian to little endian — network to host short (2 байта)
  • ntohl — big endian to little endian — network to host long (4 байта)

В ethernet.h подключим:

#include "common.h"

Итак, приняли кадр Ethernet, далее следует его обработать и отправить ответ. Начнем, вопреки логике, с отправки ответа:

/*----------------------------------------------------------------------------*/
static void ETH_Response(ETH_Frame* ethFrame, uint16_t len)
{
  memcpy(ethFrame->destMacAddr, ethFrame->srcMacAddr, MAC_ADDRESS_BYTES_NUM);
  memcpy(ethFrame->srcMacAddr, macAddr, MAC_ADDRESS_BYTES_NUM);
  
  ENC28J60_TransmitFrame((uint8_t*)ethFrame, len + sizeof(ETH_Frame));
}



/*----------------------------------------------------------------------------*/

Такой вид имеет функция отправки ответа на фрейм. Из полезной деятельности здесь изменение порядка MAC-адресов. Действительно, отправитель из первоначального фрейма теперь становится получателем. В качестве адреса отправителя же мы теперь используем наш собственный MAC-адрес, который сохранен в массиве macAddr[]. После этого запускаем ответный пакет в путь:

ENC28J60_TransmitFrame((uint8_t*)ethFrame, len + sizeof(ETH_Frame));

И теперь в ETH_Process() имеем:

/*----------------------------------------------------------------------------*/
void ETH_Process(ENC28J60_Frame* encFrame)
{
  uint16_t responseSize = 0;
  uint16_t requestSize = ENC28J60_ReceiveFrame(encFrame);

  if (requestSize > 0)
  {
    ETH_Frame* ethFrame = (ETH_Frame*)encFrame->data;
    uint16_t etherType = ntohs(ethFrame->etherType);
    
    // Protocols processing should be there

    if (responseSize > 0)
    {
      ETH_Response(ethFrame, responseSize);
    }
  }  
}



/*----------------------------------------------------------------------------*/

Важным здесь является процесс формирования ответного пакета, ведь непосредственно в функции отправки мы только изменяем MAC-адреса в заголовке. Подразумевается, что по указателю ethFrame уже находятся данные, предназначенные для отправки, а в переменной responseSize — длина этих данных. То есть полный процесс построен так:

  1. Принимаем новый кадр при помощи ENC28J60_ReceiveFrame.
  2. Обрабатываем принятые данные и формируем ответный пакет, при этом оперируем все тем же указателем на все тот же фрейм ethFrame, а также переменной responseSize, в которую мы должны поместить размер данных в ответном фрейме.
  3. Выполняем отправку ответа при помощи ETH_Response().

Шаг 2 на данный момент в коде отсутствует, когда мы его реализуем, все окончательно встанет на свои места.

Протокол ARP.

Но прежде чем двинуться дальше, нужно провести тестирование осуществленного. Для этого зададим в файле enc28j60.c IP-адрес нашей платы и затем пропингуем его из командной строки. IP-адрес:

uint8_t ipAddr[IP_ADDRESS_BYTES_NUM] = {169, 254, 191, 22};
ping command.

Кратко о том, что будет происходить в сети при данных действиях…

Итак, ПК получает сигнал пропинговать определенный IP-адрес, но для отправки Ethernet фрейма ему необходимо знать MAC-адрес целевого устройства, а не IP. И тут в дело вступает протокол ARP, который и предназначен для получения MAC-адреса устройства по известному IP-адресу. Таким образом, у нас в данном случае возникает типичная ситуация, когда устройство-1 хочет отправить данные устройству-2 (в этой роли ENC28J60), зная лишь его IP-адрес.

Для решения этой задачи устройством-1 в сеть отправляется широковещательный запрос, то есть такой Ethernet кадр, в котором в качестве адресата указан широковещательный MAC-адрес — FF:FF:FF:FF:FF:FF. Такие пакеты принимают все устройства в сети, в том числе и наше устройство-2. Суть же пакета словами можно выразить так: «Устройство с IP xxx.xxx.xxx.xxx, сообщите свой MAC-адрес устройству с MAC-адресом YY:YY:YY:YY:YY:YY». В нашем конкретном случае xxx.xxx.xxx.xxx — это IP-адрес, который мы задали в проекте и пытаемся пинговать, а YY:YY:YY:YY:YY:YY — это адрес ПК, с которого мы выполняем команду ping.

В результате вышеобозначенных действий, устройство-2, получив ARP запрос, должно выполнить отправку ответа, содержащего свой собственный MAC-адрес. После чего устройство-1 сможет спокойно осуществлять отправку Ethernet кадров в направлении ставшего ему известным MAC-адреса.

Все это нам предстоит осуществить в проекте, но пока еще пару слов о структуре пакетов ARP:

Структура запроса. Протокол ARP.

Пройдемся по полям:

  • HTYPE — здесь хранится код, который соответствует используемому протоколу канального уровня. У нас — Ethernet, Ethernet — код 0x0001.
  • PTYPE — код сетевого протокола, IPv4 — 0x0800.
  • HLEN — длина физического адреса, кол-во байт. В этой роли выступает MAC-адрес, 6 байт (0x06).
  • PLEN— длина логического адреса, кол-во байт. В этой роли выступает IP-адрес, 4 байта (0x04).
  • OPER — код операции. 0x0001 — ARP запрос, 0x0002 — ответ на запрос.
  • SHA — Sender Hardware Address — MAC-адрес отправителя.
  • SPA— Sender Protocol Address — IP-адрес отправителя.
  • THA — Target Hardware Address — MAC-адрес получателя.
  • TPA— Target Protocol Address — IP-адрес получателя.

Суммарно получаем следующую последовательность байт в сети:

Протокол ARP.

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

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

Все, переходим к коду. Добавляем в проект еще 2 файла:

  • arp.c
  • arp.h
Проект для работы с протоколом ARP.

По аналогии с тем, что мы делали ранее, добавим структуру и для протокола ARP:

typedef struct ARP_Frame
{
  uint16_t hType;
  uint16_t pType;
  uint8_t hLen;
  uint8_t pLen;
  uint16_t opCode;
  uint8_t srcMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t srcIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t destMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t destIpAddr[IP_ADDRESS_BYTES_NUM];
} ARP_Frame;

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

/* Functions -----------------------------------------------------------------*/
uint16_t ARP_Process(ARP_Frame* arpFrame, uint16_t frameLen)
{
  uint16_t newFrameLen = 0;
  
  if (memcmp(arpFrame->destIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM) == 0)
  {
    if (arpFrame->opCode == ntohs(ARP_OP_CODE_REQUEST))
    {
      memcpy(arpFrame->destMacAddr, arpFrame->srcMacAddr, MAC_ADDRESS_BYTES_NUM);
      memcpy(arpFrame->srcMacAddr, macAddr, MAC_ADDRESS_BYTES_NUM);
      
      memcpy(arpFrame->destIpAddr, arpFrame->srcIpAddr, IP_ADDRESS_BYTES_NUM);
      memcpy(arpFrame->srcIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM);
      
      arpFrame->opCode = htons(ARP_OP_CODE_RESPONSE);
      newFrameLen = frameLen;
    }
  }
  
  return newFrameLen;
}



/*----------------------------------------------------------------------------*/

Разбираемся поэтапно и подробно… Аргументы функции:

  • arpFrame — указатель на данные фрейма. Эти данные мы должны проанализировать и, в случае необходимости, изменить. Далее они будут использованы в файле ethernet.c при отправке ответа, но об этом чуть позже.
  • frameLen — полная длина пакета ARP.

В переменную newFrameLen мы поместим размер «нового» ARP фрейма, который предназначен для отправки. Забегая вперед, в данном случае длина запроса ARP в точности равна длине нашего ответа:

newFrameLen = frameLen;

Проведем двухэтапную проверку принятых данных:

  • Во-первых, проверим, что IP-адрес получателя, содержащийся в принятых данных, соответствует нашему IP, который хранится в переменной ipAddr:
  if (memcmp(arpFrame->destIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM) == 0)
  • И, во-вторых, проверим код операции:
if (arpFrame->opCode == ntohs(ARP_OP_CODE_REQUEST))

Допустимые значения у нас предусмотрительно определены в arp.h:

#define ARP_OP_CODE_REQUEST                                     0x0001
#define ARP_OP_CODE_RESPONSE                                    0x0002

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

Начинаем формировать ответное сообщение. Для этого, в частности, меняем местами MAC и IP-адреса отправителя и получателя:

memcpy(arpFrame->destMacAddr, arpFrame->srcMacAddr, MAC_ADDRESS_BYTES_NUM);
memcpy(arpFrame->srcMacAddr, macAddr, MAC_ADDRESS_BYTES_NUM);
      
memcpy(arpFrame->destIpAddr, arpFrame->srcIpAddr, IP_ADDRESS_BYTES_NUM);
memcpy(arpFrame->srcIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM);

И финальный шаг — заменяем код операции: был ARP_OP_CODE_REQUEST — запрос, станет ARP_OP_CODE_RESPONSE — ответ:

arpFrame->opCode = htons(ARP_OP_CODE_RESPONSE);

На данном этапе в части протокола ARP все готово к практическому использованию, осталось добавить вызов созданной функции на уровне выше — то есть в файле ethernet.c:

/*----------------------------------------------------------------------------*/
void ETH_Process(ENC28J60_Frame* encFrame)
{
  uint16_t responseSize = 0;
  uint16_t requestSize = ENC28J60_ReceiveFrame(encFrame);

  if (requestSize > 0)
  {
    ETH_Frame* ethFrame = (ETH_Frame*)encFrame->data;
    uint16_t etherType = ntohs(ethFrame->etherType);
    
    // ARP protocol
    if (etherType == ETH_FRAME_TYPE_ARP)
    {
      uint16_t arpFrameLen = requestSize - sizeof(ETH_Frame);
      responseSize = ARP_Process((ARP_Frame*)ethFrame->data, arpFrameLen);
    }
     
    if (responseSize > 0)
    {
      ETH_Response(ethFrame, responseSize);
    }
  }  
}



/*----------------------------------------------------------------------------*/

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

// ARP protocol
if (etherType == ETH_FRAME_TYPE_ARP)
{
  uint16_t arpFrameLen = requestSize - sizeof(ETH_Frame);
  responseSize = ARP_Process((ARP_Frame*)ethFrame->data, arpFrameLen);
}

При работе с другими протоколами, например, IP, UDP и т. д., архитектура проекта останется неизменной, просто будем добавлять вызов соответствующих обработчиков в ETH_Process() 👍

Собираем проект, прошиваем, запускаем, смотрим. Поставим брейкпоинт:

Проверка программы.

Теперь снова отправим из консоли команду:

ping 169.254.191.22

Попав на брейк, оценим, что за данные у нас в переменной frame:

Все четко в соответствии с теми принципами и аспектами, которые мы разобрали! На команду ping, конечно, ответа не будет, поскольку за это отвечает уже протокол ICMP (сетевой уровень), а это мы осуществим в следующей статье цикла. Корректность же сетевых посылок можно проверить при помощи WireShark. Запускаем захват пакетов и повторяем команду ping, в итоге наблюдаем:

Wireshark.

ПК отправляет широковещательный запрос с нашим IP-адресом (строка 1), в ответ отправляем ему наш MAC-адрес (строка 2). Тут же можно во всех красках и подробностях получить информацию по каждому из пакетов:

Wireshark ARP.

Ну и ожидаемо ICMP ответ на команду ping отсутствует. Все отработало как и планировалось, поэтому на этом заканчиваем сегодняшнюю статью, до встречи в ближайшее время, в следующей части!

/**
  ******************************************************************************
  * @file           : arp.c
  * @brief          : ARP driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "arp.h"
#include <string.h>



/* Declarations and definitions ----------------------------------------------*/




/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
uint16_t ARP_Process(ARP_Frame* arpFrame, uint16_t frameLen)
{
  uint16_t newFrameLen = 0;
  
  if (memcmp(arpFrame->destIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM) == 0)
  {
    if (arpFrame->opCode == ntohs(ARP_OP_CODE_REQUEST))
    {
      memcpy(arpFrame->destMacAddr, arpFrame->srcMacAddr, MAC_ADDRESS_BYTES_NUM);
      memcpy(arpFrame->srcMacAddr, macAddr, MAC_ADDRESS_BYTES_NUM);
      
      memcpy(arpFrame->destIpAddr, arpFrame->srcIpAddr, IP_ADDRESS_BYTES_NUM);
      memcpy(arpFrame->srcIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM);
      
      arpFrame->opCode = htons(ARP_OP_CODE_RESPONSE);
      newFrameLen = frameLen;
    }
  }
  
  return newFrameLen;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : arp.h
  * @brief          : ARP driver interface
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef ARP_H
#define ARP_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"
#include "common.h"



/* Declarations and definitions ----------------------------------------------*/

#define ARP_OP_CODE_REQUEST                                     0x0001
#define ARP_OP_CODE_RESPONSE                                    0x0002



typedef struct ARP_Frame
{
  uint16_t hType;
  uint16_t pType;
  uint8_t hLen;
  uint8_t pLen;
  uint16_t opCode;
  uint8_t srcMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t srcIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t destMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t destIpAddr[IP_ADDRESS_BYTES_NUM];
} ARP_Frame;



/* Functions -----------------------------------------------------------------*/

extern uint16_t ARP_Process(ARP_Frame* arpFrame, uint16_t frameLen);



#endif // #ifndef ARP_H
/**
  ******************************************************************************
  * @file           : ethernet.c
  * @brief          : Ethernet driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "ethernet.h"
#include "arp.h"
#include <string.h>



/* Declarations and definitions ----------------------------------------------*/



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
static void ETH_Response(ETH_Frame* ethFrame, uint16_t len)
{
  memcpy(ethFrame->destMacAddr, ethFrame->srcMacAddr, MAC_ADDRESS_BYTES_NUM);
  memcpy(ethFrame->srcMacAddr, macAddr, MAC_ADDRESS_BYTES_NUM);
  
  ENC28J60_TransmitFrame((uint8_t*)ethFrame, len + sizeof(ETH_Frame));
}



/*----------------------------------------------------------------------------*/
void ETH_Process(ENC28J60_Frame* encFrame)
{
  uint16_t responseSize = 0;
  uint16_t requestSize = ENC28J60_ReceiveFrame(encFrame);

  if (requestSize > 0)
  {
    ETH_Frame* ethFrame = (ETH_Frame*)encFrame->data;
    uint16_t etherType = ntohs(ethFrame->etherType);
    
    // ARP protocol
    if (etherType == ETH_FRAME_TYPE_ARP)
    {
      uint16_t arpFrameLen = requestSize - sizeof(ETH_Frame);
      responseSize = ARP_Process((ARP_Frame*)ethFrame->data, arpFrameLen);
    }

    if (responseSize > 0)
    {
      ETH_Response(ethFrame, responseSize);
    }
  }
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : ethernet.h
  * @brief          : Ethernet driver interface
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef ETHERNET_H
#define ETHERNET_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"
#include "enc28j60.h"
#include "common.h"



/* Declarations and definitions ----------------------------------------------*/

#define ETH_FRAME_TYPE_ARP                                      0x0806
#define ETH_FRAME_TYPE_IP                                       0x0800



typedef struct ETH_Frame 
{
  uint8_t destMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint8_t srcMacAddr[MAC_ADDRESS_BYTES_NUM];
  uint16_t etherType;
  uint8_t data[];
} ETH_Frame;



/* Functions -----------------------------------------------------------------*/

void ETH_Process(ENC28J60_Frame* encFrame);



#endif // #ifndef ETHERNET_H

Ссылка на проект — MT_ENC28J60_Part_3

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x