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