Pull to refresh

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

Level of difficultyMedium
Reading time5 min
Views3.2K

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

Tags:
Hubs:
Total votes 15: ↑14 and ↓1+13
Comments10

Articles