Как-то давно я просматривал опции для команды 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 собрать и запустить.
Комментарии (10)
pae174
26.09.2023 10:38+3Вы изобрели (ну почти) новый вид памяти на линиях задержки :-)
В разное время в качестве таких линий (или чего-то похожего на такие линии) выступали следующие штуки:
Пьезокристаллы. Данные конвертировались в звук, звуковая волна подавалась с одной стороны кристалла, перемещалась по кристаллу со скоростью звука и через некоторое время достигала противоположенного конца кристалла где и конвертировалась обратно в данные.
Ртутные трубки.
Специальная проволока, вибрирующая под воздействием магнитного поля.
Кольцевые сдвиговые регистры (в микрокалькуляторах). Не совсем линия задержки, но похоже.
Обычное такое электромагнитное излучение. Радиоволна или лазер, передаваемые на большое расстояние. Как раз ваш случай.
Zenitchik
26.09.2023 10:38А что делать, если пакет не вернётся? Информация пропадёт? Я когда-то работал монтажником в провайдерской компании. С сетью порой разное случается.
pae174
26.09.2023 10:38Для проверки гипотезы можно тупо сделать сам себе провайдера. Километр оптики, тысяч 15 стоит бухта вроде бы. На 10 гигабитах скорости получится килобайт 40 запихать в одну жилу.
lucius Автор
26.09.2023 10:38Размер ICMP пакета может быть до 64 килобайт. Я пробовал добавлять контент размером около 50 килобайт в один пакет и пинги проходили. Если хост расположен далеко, то может одновременно несколько пингов легко находиться "в пути".
omaxx
26.09.2023 10:38Основная задержка не за счет задержки в волокне, а на маршрутизаторах. Так что надо выбирать хост, чтобы до него как можно больше хопов было.
debagger
26.09.2023 10:38+1Допустим все пинги вернулись одновременно, тогда чтобы их обработать и снова отправить надо как минимум места в памяти равное размеру данных. Но идея забавная, спасибо.
omaxx
26.09.2023 10:38+2Не со всем адресами такой фокус пройдет:
❯ ping -s 1300 www.google.com PING www.google.com (142.251.46.164): 1300 data bytes 76 bytes from 142.251.46.164: icmp_seq=0 ttl=39 time=94.199 ms wrong total length 96 instead of 1328
GRaAL
Моё уважение, отличная нестандартная задумка!