
Как-то давно я просматривал опции для команды 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 собрать и запустить.