Пишем key-value storage на пакетах ping’а или храним данные между облаками

—

от автора

Как-то давно я просматривал опции для команды ping и обратил внимание, что можно задавать размер ICMP пакета. «Хм», — подумал я: «Можно же сложить в сам пакет какую-то полезную нагрузку». Эта идея время от времени всплывала у меня в голове, но что именно можно хранить в пакете ICMP придумать не удавалось. Однако, недавно пришло понимание, что если хранить данные в ICMP пакете, то они не будут занимать место в оперативной памяти! То есть можно сделать key-value хранилище, где все данные будут храниться внутри сети.

Схема работы хранилища

Key-value хранилище слушает порт 4242.

  • Записи создаются для POST запросов по адресу /<key> где в body находятся данные для хранения;
  • Записи считываются для GET запросов по адресу /<key>.

Под капотом приложение после чтения POST запроса отправляет ICMP пакет с данными. Когда пакет возвращается, то снова отправляется в сеть и тд. Когда у приложения появляется какой-то запрос на чтение пакета, то приложение ждёт некоторое время, пока из сети не вернётся нужный пакет, затем возвращает данные.

Реализация на nodejs

Я решил реализовать задачу на Nodejs, но думаю что для реального применения нужно использовать какой-то низкоуровневый язык.

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

Создание записей

Приложение запускается и слушает запросы на порту 4242. Новые записи ключ-значение создаются для обработанных POST запросов.

app.post('/:key', (req, res) => {     const key = req.params.key;     const payload = req.body;     // <...>     sendToHost(key, payload, res); // <-- создаём запись });

Основная хранилища находится в callback функции pingHost. Эта функция исполняется, когда отправленный пинг уже вернулся. Аргументы resKey — это идентификатор ICMP пакета, то есть наш ключ. А resValue — это значение, которое мы передавали. То есть как только мы получаем ответ от хоста, то сразу же отправляет данные обратно на хост.

const sendToHost = (key, value, creationResponse) => {     session.pingHost(HOST_IP, key, value, function (error, HOST_IP, _a1, _a2, resKey, resValue) {         creationResponse?.status(201).send('Stored successfully');         // <...>         sendToHost(resKey, resValue); // <-- повторная отправка запроса     }); }

Чтение записей

Все запросы на чтение мы складываем в reqStore и еще добавляем туда timeoutID, чтобы по истечении 2 секунд мы могли отправить сообщение о том, что ключ не был найден. pingHost

app.get('/:key', (req, res) => {     const key = req.params.key;     reqStore[key] = {         response: res,          timeoutID: setTimeout(() => res.status(404).send('Not Found'), 2000)     }; });

Проверка сохраненных в reqStore запросов происходит уже внутри колбэка функции pingHost. Если искомый ключ найден, то значение отправляется в ответе на запрос, таймаут очищается, а ключ удаляется из reqStore

// внутри колбэка pingHost  if (reqStore[resKey]) {     reqStore[resKey].response.send(resValue);     clearTimeout(reqStore[resKey].timeoutID);     delete reqStore[resKey]; }

Удаление записей

Удаление организовано примерно так же, как и чтение. Запросы на удаление записываются в keysToDelete.

app.delete('/:key', (req, res) => {     const key = req.params.key;     keysToDelete[key] = res; });

Проверка сохраненных в keysToDelete запросов происходит так же внутри колбэка функции pingHost. И если ключ вернувшегося ICMP пакета найден, то отправляется ответ об успешном удалении, в противном случае продолжается цикл отправки запросов на целевой хост.

if (keysToDelete[resKey]) {     keysToDelete[resKey].status(200).send('Key deleted')     delete keysToDelete[resKey]; } else {     sendToHost(resKey, resValue); }

Вот собственно и вся логика.

Весь код сервера

const express = require('express'); const bodyParser = require('body-parser'); const ping = require("net-ping");  // enrichment of ping functionality const netPingPlus = require('./net-ping-plus'); netPingPlus.run();  const HOST_IP = '213.59.253.7'; const DEBUG_DELAY_ON = false; // pause before sending each ping const DEBUG_DELAY = 300;    // milliseconds  const FIND_KEY_TIME = 2000; // milliseconds  const session = ping.createSession(); const reqStore = {}; const keysToDelete = {}; const app = express(); const port = 4242;  app.use(bodyParser.text());  const sendToHost = (key, value, creationResponse) => {      session.pingHost(HOST_IP, key, value, function (error, HOST_IP, _a1, _a2, resKey, resValue) {         if (error) {             console.log(HOST_IP + ": " + error.toString());             return;         }          creationResponse?.status(201).send('Stored successfully');          if (reqStore[resKey]) {             reqStore[resKey].responses.forEach(r => r.send(resValue));             clearTimeout(reqStore[resKey].timeoutID);             delete reqStore[resKey];         }          if (keysToDelete[resKey]) {             keysToDelete[resKey].responses.forEach(r => r.status(200).send('Key deleted'));             clearTimeout(keysToDelete[resKey].timeoutID);             delete keysToDelete[resKey];         } else {             if (DEBUG_DELAY_ON) {                 setTimeout(() => sendToHost(resKey, resValue), DEBUG_DELAY);             } else {                 sendToHost(resKey, resValue);             }         }     }); }  app.post('/:key', (req, res) => {     const key = req.params.key;     const payload = req.body;      if (!key || !payload) {         return res.status(400).send('Bad Request');     }      sendToHost(key, payload, res); });  app.get('/:key', (req, res) => {      const key = req.params.key;     // console.log('!GET', key);      if (!key) {         return res.status(400).send('Bad Request');     }      if (!reqStore[key]) {         reqStore[key] = {             responses: [res],             timeoutID: setTimeout(() => {                 res.status(404).send('Not Found');             }, FIND_KEY_TIME)         };     } else {         reqStore[key].responses.push(res);     } });  app.delete('/:key', (req, res) => {      const key = req.params.key;      if (!key) {         return res.status(400).send('Bad Request');     }      if (!keysToDelete[key]) {         keysToDelete[key] = {             responses: [res],             timeoutID: setTimeout(() => {                 res.status(404).send('Not Found');             }, FIND_KEY_TIME)         };     } else {         keysToDelete[key].responses.push(res);     } });  // Запуск сервера app.listen(port, () => {     console.log(`Server is listening on port ${port}`); }); 

Рабочий репозиторий можно найти на Гитхабе.

Проверяем работу

Запускать приложение нужно командой sudo nodejs serv.js, потому что приложению необходимы разрешения для работы с сокетами. Я потестировал создание, получение и удаление записей. Всё работает.

Прикладываю скриншоты из Wireshark, по которым видно, что отправляемые и получаемые пинги содержат нужные данные.

Отправленный ICMP пакет:

Запрос

Принятый ICMP пакет:

Ответ

В приложении я использую только один IP адрес для пинга, который я выбрал совершенно случайно. Но вообще нужно использовать целый пул адресов и неплохо бы дублировать запросы на разные IP, чтобы избежать потери данных.

Заключение

У меня нет идей о том как именно использовать такое хранилище и где это может быть полезно. Возможно какой-то исполняемый код можно будет прятать в сети, чтобы в час X собрать и запустить.


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