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


  1. GRaAL
    26.09.2023 10:38
    +4

    Моё уважение, отличная нестандартная задумка!


  1. pae174
    26.09.2023 10:38
    +3

    Вы изобрели (ну почти) новый вид памяти на линиях задержки :-)

    В разное время в качестве таких линий (или чего-то похожего на такие линии) выступали следующие штуки:

    • Пьезокристаллы. Данные конвертировались в звук, звуковая волна подавалась с одной стороны кристалла, перемещалась по кристаллу со скоростью звука и через некоторое время достигала противоположенного конца кристалла где и конвертировалась обратно в данные.

    • Ртутные трубки.

    • Специальная проволока, вибрирующая под воздействием магнитного поля.

    • Кольцевые сдвиговые регистры (в микрокалькуляторах). Не совсем линия задержки, но похоже.

    • Обычное такое электромагнитное излучение. Радиоволна или лазер, передаваемые на большое расстояние. Как раз ваш случай.


  1. Zenitchik
    26.09.2023 10:38

    А что делать, если пакет не вернётся? Информация пропадёт? Я когда-то работал монтажником в провайдерской компании. С сетью порой разное случается.


    1. pae174
      26.09.2023 10:38

      Для проверки гипотезы можно тупо сделать сам себе провайдера. Километр оптики, тысяч 15 стоит бухта вроде бы. На 10 гигабитах скорости получится килобайт 40 запихать в одну жилу.


      1. lucius Автор
        26.09.2023 10:38

        Размер ICMP пакета может быть до 64 килобайт. Я пробовал добавлять контент размером около 50 килобайт в один пакет и пинги проходили. Если хост расположен далеко, то может одновременно несколько пингов легко находиться "в пути".


      1. omaxx
        26.09.2023 10:38

        Основная задержка не за счет задержки в волокне, а на маршрутизаторах. Так что надо выбирать хост, чтобы до него как можно больше хопов было.


  1. debagger
    26.09.2023 10:38
    +1

    Допустим все пинги вернулись одновременно, тогда чтобы их обработать и снова отправить надо как минимум места в памяти равное размеру данных. Но идея забавная, спасибо.


  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


  1. ris58h
    26.09.2023 10:38
    +2

    Идея хорошая, но вас уже опередили как минимум на 9 лет.

    https://github.com/yarrick/pingfs


    1. lucius Автор
      26.09.2023 10:38

      Спасибо за наводку! Я честно искал, но не нашел, поэтому и решил реализовать.