Как я поднял свой сервер без возможности выставить для него статический IP адрес

Родился я в одном городе, позже переехал жить в другой. В родном городе остался ПК, который стоит без дела. В один прекрасный день решил я из него сделать многофункциональную удаленную машину: чтобы и кодить, и файлы хранить, и сайты/ботов хостить. Идея мне понравилась, я накатил на машину линукс, поставил все валявшиеся без дела диски и начал все это проверять. Но тут оказалось, что в родительском доме интернет тариф не поддерживает возможность установки статического IP адреса по умолчанию — адрес выдается провайдером в случайные моменты времени. Это означало, что я не мог, например, хостить какой-нибудь сервер на этой машине. Более того, я даже банально не мог к ней по SSH подключиться после смены ее адреса.

Попытки найти решение моей проблемы на просторах интернета закончились неудачей. Я не видел иного способа, кроме как поменять тариф, но это тоже представлялось невозможным.

В голову пришла идея…

Я думал над неким механизмом, который позволил бы мне узнать внешний IP адрес машины, не заставляя родителей учиться пользоваться командой ifconfig. И тогда в голову пришла идея написать телеграм-бота, который бы хостился на моей машине. Ведь к его интерфейсу доступ есть всегда, независимо от адреса ПК.

Итак, перейдем к боту. Мой основной язык на работе и для пет-проектов — C++. Поэтому для написания бота я взял библиотеку tgbot-cpp. Логика бота проста: получаем внешний адрес и отправляем его пользователю.

void OnGetIPCommandAction(TgBot::Message::Ptr message) const noexcept
try
{
    std::cerr << "[DEBUG] Processing getip command..." << std::endl;

    if (!IsUserLegal(message->from))
    {
        return;
    }

    const auto ip{GetExternalIP()};

    m_bot.getApi().sendMessage(message->chat->id, ip ? ip.value() : "Could not get IP");
}
catch (const std::exception& e)
{
    std::cerr << "[ERROR] " << __func__ << " failed: " << e.what() << std::endl;
}

Так как бот доступен любому, кто его найдет, необходимо было реализовать механизм ограничения доступа. Согласно описанию TelegramAPI, каждый пользователь обладает уникальным идентификатором. Этот идентификатор можно использовать для авторизации пользователей. Бот перед обработки каждой команды зовет метод IsUserLegal, который проверяет наличие идентификатора текущего пользователя во множестве разрешенных. Код метода тривиален — показывать здесь не будут.

Метод GetExternalIP отвечает за получение внешнего адреса. Для этого он идет в интернет и получает содержимое страницы http://myexternalip.com/raw, которая этот адрес содержит.

static 
std::optional GetExternalIP()
{
    constexpr std::string_view URL = "http://myexternalip.com/raw";

    CURL* curlHandle{curl_easy_init()};
    if (!curlHandle)
    {
        return std::nullopt;
    }

    const static auto downloadFunc = [](char* ptr, size_t size, size_t nmemb, std::string* data) -> size_t
    {
        assert(data);
        auto& str = *data;

        try
        {
            str.append(ptr, size * nmemb);
        }
        catch(const std::exception& e)
        {
            std::cerr << "[ERROR] downloadFunc failed: " << e.what() << std::endl;
            return 0;
        }

        return size * nmemb;
    };

    std::string ipStr;
    curl_easy_setopt(curlHandle, CURLOPT_URL, URL.data());
    curl_easy_setopt(curlHandle, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, (size_t(*)(char*, size_t, size_t, std::string*))downloadFunc);
    curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, &ipStr);

    if (const auto retcode{curl_easy_perform(curlHandle)}; retcode != CURLE_OK)
    {
        std::cerr << "[ERROR] " << "curl_easy_perform() failed: " << curl_easy_strerror(retcode) << std::endl;
    }

    curl_easy_cleanup(curlHandle);

    return ipStr;
}

Итак, бот написан, можно проверять.

Чат с ботом

Чат с ботом

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

[Unit]
Description=MyPc Telegram Bot
Requires=network.target
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/pcbot
Restart=always

[Install]
WantedBy=multi-user.target

Готово. Теперь бот запускается вместе с компьютером, перезапускается в случае падения и по запросу сообщает его адрес. Теперь я могу использовать компьютер так, как изначально хотел.

Вывод

Не берусь утверждать, что такое решение является наиболее простым и адекватным. Вероятно, есть более прямолинейный путь достижения моей цели. Но прошу меня не винить — иного не нашел. Здесь я лишь хотел поделиться своим опытом.

Меня очень интересует тема создания телеграм-ботов. Поэтому, если у вас есть идея, как можно с их помощью создать нечто полезное и интересное, то, пожалуйста, напишите об этом к комментариях.

© Habrahabr.ru