Что такое секретный контур?
Это компьютер, который отделён от сети через «диод» (устройство однонаправленной передачи данных). Из него ничего не может выходить, а входить может только по одному каналу, с одного разрешённог о IP, по определённым, строго перечисленным портам.
Разрешённым является IP диода, который связан с секретным контуром отдельной сетью, поднятой для этой пары серверов.
Зачем он такой нужен? Для чего делают секретный контур?
Обычно, это копия рабочей среды, на которую, помимо общего, помещают также секретные данные, работа с которыми строго отслеживается протоколами и специальными процедурами. За добавление и изменение секретных данных отвечают отдельные люди. Секретные данные ни при каких обстоятельствах не должны покидать компьютер, кроме как в обработанном виде и на бумажном носителе. Опять же с соблюдением всех процедур.
Как передавать данные?
Передавать данные может любой компьютер в сети. Передача производится на диод. Далее он перенаправляет данные уже на секретный контур. В настройках диода определяют разрешённые для приёма адреса и порты.
Передача производится по протоколу UDP, который не гарантирует доставку без потерь. Поэтому одно и то же передаётся несколько раз подряд с небольшой задержкой между итерациями. Также, данные передаются не целиком, а с разбивкой на пакеты. Каждый пакет необходимо оформить, так же как и метаданные о пакетах, чтобы иметь возможность одновременно принимать и обрабатывать данные из нескольких источников. Блоки передаются вместе с хэшами, чтобы проверять целостность доставки.
Производить передачу надо несколько раз (например, 5 раз) с задержками между передачей (например, одну секунду).
Передача данных
Итак, напишем хелпер, ответственный за отправку данных. Далее тезисно...
Длина пакета
const SEND_BYTES_MAX = 1500;
Адрес для отправки
public function __construct($host, $port)
Отправка данных состоит из следующих шагов
public function send($data)
{
$this->createSocket(); // поднять сокет для передачи через UDP
$this->splitWithUnicode($data); // разбить данные на блоки
$this->sendMetadata(); // отправить метаданные
$this->sendSplits(); // отправить блоки данных
$this->sendEnding(); // отправить завершение транзакции
$this->closeSocket(); // закрыть сокет
}
Создание сокета
protected function createSocket()
{
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($this->socket === false) {
$errorCode = socket_last_error();
$errorMessage = socket_strerror($errorCode);
$this->logger->critical('Невозможно создать сокет для отправки данных.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
throw new RuntimeException('Невозможно создать сокет для отправки данных.');
}
}
Разбивка данных на блоки с учётом кодировки. Максимальная длина блока задаётся константой. Каждый блок будет содержать не более указанной длины байт. Кодировка нужна для того, чтобы избежать разбивки символов на разные блоки.
К сожалению, функция mb_str_split доступна только начиная с PHP 7.4.
protected function splitWithUnicode($data)
{
$this->splits = [];
$maxLength = self::SEND_BYTES_MAX / 2;
$dataLength = mb_strlen($data, "UTF-8");
for ($i = 0; $i < $dataLength; $i += $maxLength) {
$this->splits[] = mb_substr($data, $i, $maxLength, "UTF-8");
}
}
Отправка метаданных
protected function sendMetadata()
{
$metadata = [
'countSplits' => count($this->splits), // количество блоков
];
if ($this->code) {
$metadata['code'] = $this->code; // код транзакции
}
if ($this->name) {
$metadata['name'] = $this->name; // например, имя передаваемого файла, или наименование блока
}
$this->sendDataWithHash(json_encode($metadata)); // Отправить JSON-строку метаданных
}
Отправка данных с хэшем. 8 байт впереди блока содержат хэш на основе передаваемых данных и прикрепляются в начало блока.
protected function sendDataWithHash($data)
{
$hash = hash('crc32', $data);
$hashAndData = $hash . $data;
$this->sendData($hashAndData);
}
Отправка данных
protected function sendData($data)
{
$sent = socket_sendto($this->socket, $data, strlen($data), 0, $this->host, $this->port);
if ($sent === false) {
$errorCode = socket_last_error();
$errorMessage = socket_strerror($errorCode);
$this->logger->warning('Данные через сокет не отправлены.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
}
time_sleep_until(microtime(true) + .001); // небольшая задержка между отправками блоков по сети. Примерно получается 1,5 мегабайта за одну секунду.
}
Отправка блоков
protected function sendSplits()
{
foreach ($this->splits as $split) {
$this->sendDataWithHash($split);
}
}
Отправка строки окончания
protected function sendEnding()
{
$endingString = chr(0);
$this->sendData($endingString);
}
Закрытие сокета
protected function closeSocket()
{
socket_close($this->socket);
}
Весь класс под спойлером.
Hidden text
<?php
namespace Application\Workers\Udp;
use Application\Logger\AppLogger;
use RuntimeException;
/**
* Отправка данных через UDP.
*/
class UdpSender
{
const SEND_BYTES_MAX = 1500;
/** @var string */
protected $host;
/** @var integer */
protected $port;
/** @var AppLogger */
protected $logger;
/** @var integer|string */
protected $code;
/** @var string */
protected $name;
/** @var resource */
protected $socket;
/** @var array */
protected $splits;
/**
* @param string $host
* @param integer $port
*/
public function __construct($host, $port)
{
$this->host = $host;
$this->port = $port;
$this->logger = new AppLogger('Request');
}
/**
* Применить код отправляемого файла.
*
* @param integer|string $code
* @return UdpSender
*/
public function withCode($code)
{
$this->code = $code;
return $this;
}
/**
* Применить наименование отправляемого файла.
*
* @param string $name
* @return UdpSender
*/
public function withName($name)
{
$this->name = $name;
return $this;
}
/**
* Отправить данные.
*
* @param string $data
* @return void
*/
public function send($data)
{
$this->createSocket();
$this->splitWithUnicode($data);
$this->sendMetadata();
$this->sendSplits();
$this->sendEnding();
$this->closeSocket();
}
/**
* Создать сокет для передачи данных.
*
* @return void
*/
protected function createSocket()
{
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($this->socket === false) {
$errorCode = socket_last_error();
$errorMessage = socket_strerror($errorCode);
$this->logger->critical('Невозможно создать сокет для отправки данных.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
throw new RuntimeException('Невозможно создать сокет для отправки данных.');
}
}
/**
* Разбить строку на символы с учётом Unicode.
*
* @param string $data
* @return void
*/
protected function splitWithUnicode($data)
{
$this->splits = [];
$maxLength = self::SEND_BYTES_MAX / 2;
$dataLength = mb_strlen($data, "UTF-8");
for ($i = 0; $i < $dataLength; $i += $maxLength) {
$this->splits[] = mb_substr($data, $i, $maxLength, "UTF-8");
}
}
/**
* Отправить метаданные.
*
* @return void
*/
protected function sendMetadata()
{
$metadata = [
'countSplits' => count($this->splits),
];
if ($this->code) {
$metadata['code'] = $this->code;
}
if ($this->name) {
$metadata['name'] = $this->name;
}
$this->logger->debug('Отправка метаданных.', ['metadata' => $metadata]);
$this->sendDataWithHash(json_encode($metadata));
}
/**
* Отправить данные с хэшем.
*
* @param string $data
* @return void
*/
protected function sendDataWithHash($data)
{
$hash = hash('crc32', $data);
$hashAndData = $hash . $data;
$this->sendData($hashAndData);
}
/**
* Отправить данные.
*
* @param string $data
* @return void
*/
protected function sendData($data)
{
$sent = socket_sendto($this->socket, $data, strlen($data), 0, $this->host, $this->port);
if ($sent === false) {
$errorCode = socket_last_error();
$errorMessage = socket_strerror($errorCode);
$this->logger->warning('Данные через сокет не отправлены.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
}
time_sleep_until(microtime(true) + .001);
}
/**
* Отправить части данных.
*
* @return void
*/
protected function sendSplits()
{
$this->logger->debug('Отправка данных через сокет.', ['countSplits' => count($this->splits)]);
foreach ($this->splits as $split) {
$this->sendDataWithHash($split);
}
}
/**
* Отправить строку окончания.
*
* @return void
*/
protected function sendEnding()
{
$endingString = chr(0);
$this->sendData($endingString);
}
/**
* Закрыть сокет.
*
* @return void
*/
protected function closeSocket()
{
socket_close($this->socket);
}
}
Принятие данных на стороне секретного контура
Запускаем сервис. Описание сервиса не входит в данную статью. Если коротко, то создаётся демон, который автоматически запускается вместе с ОС. Он в свою очередь запускает наш PHP-скрипт, отвечающий за приём данных. При падении PHP-скрипта демон автоматически перезапускает сервис.
Скрипт циклически прослушивает сокет на входящие данные.
Выполнение сервиса
public function executeService()
{
$this->connectToSocket();
while (true) { // Вечный цикл
$this->receiveData(); // Получение
$this->defineData(); // Обработка
}
}
Подключение к сокету на адрес и порт
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if (@socket_bind($this->socket, $host, $port) === false) {
// ... ошибка
exit("Failed to bind socket on $host:$port, $errorCode: $errorMessage" . PHP_EOL); // Завершение скрипта, чтобы сервис смог перезапуститься
}
Получение данных производится следующим образом:
protected function receiveData()
{
@socket_recvfrom($this->socket, $this->buffer, 52428800, 0, $ip, $port);
$this->address = "$ip:$port";
$this->logger->debug('Приняты данные', ['address' => $this->address, 'length' => strlen($this->buffer)]);
}
Обработка данных
protected function defineData()
{
if ($this->checkForFirstBlock()) { // Новые данные ?
$this->initializeTransmitting(); // Установка настроек приёма с этого адреса
} elseif ($this->checkReceiveComplete()) { // Конец данных ?
$this->completeReceive(); // Завершение транзакции
$this->resetFile(); // Сброс настроек приёма с этого адреса
} else {
$this->addSection(); // Добавление очередного принятого блока
}
}
Каждый блок кроме завершающего содержит в начале хэш, который сравнивается с содержим оставшейся принятой части данных. Таким образом соблюдается целостность принятия каждого отдельного блока.
Сами метаданные в первом, заголовочном блоке, содержат информацию о количестве ожидаемых блоков. Также ведётся проверка на существование обязательных параметров, принятых с метаданными.
Завершающий блок состоит из одного байта и содержит нулевой символ (с кодом 0).
Какие могут быть проблемы?
Если метаданные придут битые, то инициализация транзакции не произойдёт. Все очередные блоки не будут распознаны, как метаданные. Блок завершения так же будет проигнорирован.
Если не все блоки пришли или некоторые блоки пропущены из-за несовпадения хэшей, то количество принятых блоков не сойдётся и завершение транзакции произойдёт с неудачей.
Если завершающий символ не придёт, то следующая транзакция будет прилеплена к данной и завершится неудачей. В ней опять же количество блоков не сойдётся.
С одного адреса не должно приходить несколько сообщений одновременно. Но если такое произойдёт, то блоки данных окажутся перепутаны и их количество не совпадёт с метаданными, что приведёт к отказу от приёма обеих транзакций. Одновременный приём с разных адресов будет работать.
Завершающий блок приводит к проверке всех накопленных данных, объединению и сохранению полученных данных.
Ограниченный код класса приведён ниже. В нём пропущены методы, ответственные за сохранение полученной транзакции.
Hidden text
<?php
namespace Application\Services\ReceiveFiles;
use Application\Logger\AppLogger;
use RuntimeException;
class ReceiveFiles
{
const METADATA_KEYS_REQUIRED = ['name', 'code', 'countSplits'];
/** @var AppLogger */
protected $logger;
/** @var array */
protected $config;
/** @var resource */
protected $socket;
/** @var string */
protected $buffer;
/** @var string */
protected $address;
/** @var array */
protected $splits = [];
/** @var array */
protected $metadata = [];
/** @var array */
protected $hasErrors = [];
public function initialize(array $config = [])
{
$this->logger = new AppLogger('Request');
}
public function initConfig(array $config)
{
$this->config = $config;
}
public function executeService()
{
$this->connectToSocket();
while (true) {
$this->receiveData();
$this->defineData();
}
}
protected function connectToSocket()
{
$host = $this->config['host'];
$port = $this->config['port'];
$this->logger->debug('Подключение к сокету.', ['host' => $host, 'port' => $port]);
$this->socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if (@socket_bind($this->socket, $host, $port) === false) {
$errorCode = socket_last_error();
$errorMessage = socket_strerror($errorCode);
$this->logger->critical('Невозможно подключиться к сокету.', ['host' => $host, 'port' => $port, 'code' => $errorCode, 'message' => $errorMessage]);
exit("Failed to bind socket on $host:$port, $errorCode: $errorMessage" . PHP_EOL);
}
$this->logger->info('Подключено к сокету.', ['host' => $host, 'port' => $port]);
}
protected function receiveData()
{
@socket_recvfrom($this->socket, $this->buffer, 52428800, 0, $ip, $port);
$this->address = "$ip:$port";
$this->logger->debug('Приняты данные', ['address' => $this->address, 'length' => strlen($this->buffer)]);
}
protected function defineData()
{
if ($this->checkForFirstBlock()) {
$this->initializeTransmitting();
} elseif ($this->checkReceiveComplete()) {
$this->completeReceive();
$this->resetFile();
} else {
$this->addSection();
}
}
protected function checkForFirstBlock()
{
$firstBlock = !isset($this->splits[$this->address]);
if ($firstBlock) {
$this->logger->debug('Получен первый блок данных.', ['address' => $this->address]);
}
return $firstBlock;
}
protected function initializeTransmitting()
{
$this->splits[$this->address] = [];
$data = $this->fetchDataFromBufferWithHash();
$metadata = json_decode($data, true);
if (!isset($metadata)) {
$this->logger->error('Не удалось извлечь метаданные из первого блока.', ['address' => $this->address]);
return;
}
foreach (self::METADATA_KEYS_REQUIRED as $checkKey) {
if (empty($metadata[$checkKey])) {
$this->logger->error('Отсутствует необходимый ключ в метаданных.', ['keyAbsent' => $checkKey, 'metadata' => $metadata]);
}
}
$this->logger->debug('Сохранены метаданные.', ['address' => $this->address, 'metadata' => $metadata]);
$this->metadata[$this->address] = $metadata;
}
protected function fetchDataFromBufferWithHash()
{
$hash = substr($this->buffer, 0, 8);
$data = substr($this->buffer, 8);
if ($hash !== hash('crc32', $data)) {
$this->logger->debug('Контрольная сумма блока не совпадает!', ['address' => $this->address]);
$this->hasErrors[$this->address] = true;
return false;
}
return $data;
}
protected function checkReceiveComplete()
{
if (!isset($this->metadata[$this->address])) {
return false;
}
$complete = ord($this->buffer) === 0 && strlen($this->buffer) === 1;
if ($complete) {
$this->logger->debug('Получен код завершения передачи файла.', ['address' => $this->address]);
}
return $complete;
}
protected function completeReceive()
{
if ($this->hasErrors[$this->address]) {
$this->logger->info('Отмена сохранения результата из-за ошибок.', ['address' => $this->address, 'metadata' => $this->metadata[$this->address]]);
return;
}
if ($this->metadata[$this->address]['countSplits'] !== count($this->splits[$this->address])) {
$this->logger->info('Количество принятых секций не совпадает.', ['address' => $this->address, 'countSplits' => count($this->splits[$this->address]), 'countExpected' => $this->metadata[$this->address]['countSplits']]);
return;
}
$this->logger->info('Завершён приём файла.', ['address' => $this->address, 'metadata' => $this->metadata[$this->address]]);
// ... здесь пропущены строки. Предполагается сохранение полученных данных: implode('', $this->splits[$this->address])
}
protected function resetFile()
{
unset($this->splits[$this->address]);
unset($this->metadata[$this->address]);
unset($this->hasErrors[$this->address]);
}
protected function addSection()
{
$data = $this->fetchDataFromBufferWithHash();
if ($data) {
$this->splits[$this->address][] = $data;
$this->logger->debug('Добавлен блок данных.', ['address' => $this->address, 'iterator' => count($this->splits[$this->address])]);
}
}
}
P.S. За пределами статьи остались дополнительные возможности, такие как сохранение принятых данных в файловом хранилище и регистрация полученной транзакции в базе данных.
P.P.S. Не судите очень строго. Это моя третья попытка вырваться из песочницы.
Комментарии (26)
AlexTheCleaner
01.09.2023 10:00-1PHP для секретного контура... ну такое... )
anthonysnow887
01.09.2023 10:00+2С чего же вдруг он там плох? Не все языки программированию сертифицированы для использования в закрытом (секретном) контуре. Тот же PHP его имеет, а вот Go, Java, NodeJS, Ruby нет, а сам процесс сертификации весьма сложен и дорогостоящий (да и занять может до нескольких лет).
Соглашусь что для такой задачи скорее всего подошел бы С++, но на вкус и цвет как говорится, да и от ТЗ все зависит.
PS: А вот по поводу реализации есть нюанс на мой взгляд. Если завершающий пакет придет в середине передачи (а мы знаем что UDP не гарантирует порядок пакетов), то сессия полагаю так же отвалится с ошибкой.
creker
01.09.2023 10:00Ага. Такие вещи нужно делать с накоплением всех пакетов в памяти или во внешнем хранилище. Чтобы и дедупликацию сделать, и на порядок пакетов было пофиг. У нас редис стоял под эти цели. Что заодно позволило сделать схему отказоустойчивой и масштабируемой.
Tuman_2
01.09.2023 10:00Ого, не знал такое про PHP. А можете немного подробнее рассказать про эту сертификацию? А то что-то ничего не нагуглил. Стало интересно какие языки еще.
anthonysnow887
01.09.2023 10:00+2Есть сертифицированные ОС (для ФСТЭК, ФСБ и т.д.), в рамках которых предоставляется различный набор для разработчиков. Те же Astra Linux, Alt Linux смогли сертифицировать в рамках своих поставок c++, Qt, php, python3. По всем остальным все печально - либо сертификата нет, либо оооочень древние версии (отставание местами лет на 7 от актуальных версий). Для того чтобы сертифицировать что-то свое требуется полно и подробно описать все, что предоставляет язык, как это работает, как идет взаимодействие с памятью и прочее. В общем огромный пласт работ. В дальнейшем материалы могут быть поданы на сертификацию в организацию с лабораторией, имеющей лицензию на проведение таких действий. Если все проходит отлично, то через какое-то время выдается сертификат для использования в тех или иных областях. Это если коротко :)
AlexTheCleaner
01.09.2023 10:00Я вот такое на плюсах бы предпочёл как заказчик, всёж не в бирюльки играем. По поводу реализации там отдельные вопросы, да.)
zubrbonasus
01.09.2023 10:00+1Мне показалось важным, существование проверки данных ожидаемым по ожидаемой матрице типов или же использовать блокчейн технологии. Иначе может просочится вредоносный код. Наверное это сделано в модуле сохранения данных.
Со стороны php: у вас низкоуровневая работа с сокетом и высокоуровневая отправка данных в сокет реализована в одном классе, что нарушает принцип единой ответственности. Такие вещи надо разделять по разным классам.
vadimr
UDP работает поверх ARP, который при обычных условиях выполняет двусторонний обмен пакетами Ethernet, в хвосте которых (и собственно в поле mac адреса arp ответа) можно передавать любую информацию с секретного компьютера. Так что диода у вас не получилось. Ну и собственно раз arp пакеты проходят, то и другие ethernet фреймы пройдут. Тема пакетов ICMP также не раскрыта.
Диод надо ставить на уровне Ethernet, а не UDP.
creker
Не имеет значения, что поверх чего работает. Сертифицированное устройство однонаправленной передачи делает физически невозможным движение пакетов в обратную сторону. Принципиально. Там линий передачи просто нет в обратную сторону.
vadimr
Без линий передачи в обратную сторону протокол UDP работать не будет. Даже при однонаправленной передаче пакетов.
Точнее, надо много крутить руками. Мы на этот случай писали свою собственную реализацию стека UDP.
creker
Прекрасно работает. Я с таким устройством лично работал. Фокус в том, что на ARP и ICMP отвечает каждая сторона этого устройства независимо, будто это две реальных сетевых железки со своим айпишником и маком
vadimr
Тогда это называется межсетевым экраном с функцией однонаправленного шлюза. Он сам выполняет функции маршрутизации по заданным ему правилам, и тогда непонятно, к чему весь этот код на php.
creker
Нет, это называет диод. Он все так же пропустит через себя пакеты только в одну сторону, потому что внутри он физически не имеет обратного канала передачи. Иначе бы сертификат не выдали.
Поэтому без всех этих танцев с бубнов с делением на пакеты, подсчетом сумм, периодическим подсчетом потерь на периоды времени, переотправкой и прочим гемором не обойтись. Мы это все делали тоже.
vadimr
Посмотрите, например, ПАК "Рубикон". Он сертифицирован вплоть до грифа “Сов. секретно“.
Я не оспариваю необходимость деления на пакеты и подсчёта потерь. Я говорю, что уровень OSI выбран не тот.
creker
Знаю эту штуку, работал, использовали. Для отделения секретного контура он не подходит, ибо там нет физического разделения, это всего лишь фаервол. Без диода не пропустят. Более того, даже его одного недостаточно было. Сертификат рубикона дает право его использовать внутри секретных контуров, это да.
creker
Почему? На уровне приложений наиболее удобно работать с транспортными протоколами. Раз у нас диод, то ничего кроме UDP не прокатит. Вот его и используем. Других нормальных вариантов просто и нет.
Диод же работает на L2 уровне. Принимает фреймы, подменяет маки, пуляет дальше.
dravor
Межсетевому экрану тоже можно сказать пропускать пакеты только в одну сторону между сетевыми интерфейсами.
Только его в любой момент можно опять настроить иначе "на пару минуточек". Устройство передачи данных в секретную комнату предполагает, что ничего поменять нельзя даже группой лиц, например "железная реализация черной коробочки". У вас же в статье вообще PHP - даже перекомпилировать ничего не нужно, поменял пару строк и всё.
creker
Я специально написал про физическое ограничение, а не программное. Поэтому диод это специальная железка, которая никакой не межсетевой экран. По-другому просто такое решение никто не допустит.
Во-первых, статья не моя. Во-вторых, вроде в статье речь как раз о диоде как о специальной железке, через которую пропускают пакеты. PHP код в статье - это клиент, который эти пакеты сквозь нее и посылает, и сервер, который их принимает и собирает обратно. Все как надо.
neenik
Вот окажется забавно, если физического канала нет, а возможность обнаружится. Образно говоря, при подаче сигнала определённой частоты на принимающий фотоэлемент произойдёт возбуждение (изменение сопротивления) отправляющего светодиода.
resetsa
Поправьте пожалуйста, UDP работает поверх IP. ARP только занимается разрешением IP-MAC. Глаз просто режет, не сдержался.
vadimr
UDP работает поверх не только IP, но и ARP. Функция UDP send при первом использовании уходит внутри себя в вызов ARP для получения mac адреса приёмника, и это, в свою очередь, вызывает двусторонний обмен пакетами канального уровня, хотя на транспортном уровне взаимодействие однонаправленное.
resetsa
Если в кеше уже есть соот. IP=MAC, тогда запроса не будет.
В отдельных случаях, вообще можно таблицу руками набить и выключить ARP на интерфейсе.
Иначе можно говорить и HTTP работает поверх DNS. Все таки поверх подразумевает инкапсуляцию в протокол нижнего уровня.
vadimr
UDP не знает про таблицу, он обращается непосредственно к функции ARP (через ioctl SIOCGARP в BSD стеке). А ARP внутри себя уже разбирается, надо посылать запрос или не надо, и управляется с таблицей. Вызов ARP, таким образом, является частью стандартной реализации UDP.
HTTP, с другой стороны, работает с NSS при помощи функции getaddrinfo. А NSS может обращаться или не обращаться к DNS в зависимости от своей конфигурации.
resetsa
Нет, не согласен. Вы говорите про детали реализации, я про сам протокол.
UDP в принципе не важен даже протокол нижележащего уровня, в вашем случае это IP. Но также может быть и другой, например IPv6.
И это именно IP использует ARP для получения связки MAC=IP, при этом он не построен поверх него.
В IPv6 нет ARP вообще и там используется другая методика маппинга, и UDP там также работает.
Все-таки на досуге гляньте модель OSI или TCP/IP + rfc 768 (UDP)