Выбор случайного IP-адреса из любого определенного диапазона CIDR в C

Я пытаюсь создать функцию, которая сможет анализировать любой диапазон IP / CIDR и выбирать случайный IP в этом конкретном диапазоне в виде строки на C (включая /32, который будет просто возвращать один IP-адрес каждый раз). На данный момент меня это устраивает, включая зарезервированные IP-адреса (например, широковещательные), и если у меня возникнут проблемы с их исключением в будущем, я отправлю отдельный вопрос.

Я все еще довольно новичок в этой области, так как у меня еще нет большого опыта использования побитовых операторов с целочисленными битами (я понимаю сами побитовые операторы, но я пытаюсь понять, как использовать их с сетями и IP-адресами). Я также прочитал большую часть этот вопрос, который дает много отличных советов / указаний (спасибо Рону Мопину за предоставленный мне это), но я все еще пытаюсь заставить эту функцию полностью работать.

У меня почти рабочий код, но по какой-то причине использование /8 CIDR или чего-либо меньшего, чем /24, приводит к странному поведению. Использование /16 и /24 работает должным образом (это все, что я тестировал до сих пор).

Вот код, который у меня есть:

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <time.h>

int main()
{
    for (int i = 0; i < 25; i++)
    {
        // IP/CIDR.
        char *sip = "10.0.0.0";
        uint8_t cidr = 8;

        // Randomize the rand() seed.
        time_t t;
        srand((unsigned) time(&t) + i);

        // Create in_addr and convert the IP string to a 32-bit integer.
        struct in_addr inaddr;
        inet_aton(sip, &inaddr);
        uint32_t ipaddr = inaddr.s_addr;

        // Get the mask (the complement of 2 to the power of the CIDR minus one).
        uint32_t mask = ((1 << cidr) - 1);

        // Generate a random number using rand().
        uint32_t randnum = rand(); // Also tried rand() % 256.

        // Attempt to pick a random IP from the CIDR range. We shift left by the CIDR range since it's big endian. 
        uint32_t newIP = ipaddr & mask | ((0x0000ffff & randnum) << cidr);

        // Convert the new IP to a string and print it.
        struct in_addr ip;
        ip.s_addr = newIP;

        fprintf(stdout, "%s\n", inet_ntoa(ip));
    }

    return 0;
}

Это просто выбирает случайный IP 25 раз из данного IP / CIDR. При использовании /8 (например, 10.0.0.0/8) я получаю следующие выходные данные:

10.220.186.0
10.180.229.0
10.231.159.0
10.24.70.0
10.217.108.0
10.50.250.0
10.170.108.0
10.48.139.0
10.183.205.0
10.61.48.0
10.3.221.0
10.161.252.0
10.48.1.0
10.146.183.0
10.138.139.0
10.33.27.0
10.19.70.0
10.109.253.0
10.5.8.0
10.124.154.0
10.109.145.0
10.53.29.0
10.223.111.0
10.18.229.0
10.255.99.0

Последний октет всегда 0. Я мог бы предположить, что делаю что-то неправильно при смещении влево по диапазону CIDR при создании случайного 32-битного целого числа IP. Однако я не уверен, что мне здесь делать.

При использовании диапазона /30 (например, 192.168.90.4/30) я получаю следующие выходные данные:

192.168.90.68
192.168.90.196
192.168.90.68
192.168.90.68
192.168.90.68
192.168.90.4
192.168.90.196
192.168.90.68
192.168.90.196
192.168.90.68
192.168.90.132
192.168.90.4
192.168.90.196
192.168.90.68
192.168.90.196
192.168.90.196
192.168.90.4
192.168.90.68
192.168.90.132
192.168.90.4
192.168.90.68
192.168.90.68
192.168.90.132
192.168.90.196
192.168.90.196

Иногда он выбирает 192.168.90.4, что является правильным, но три других случайных IP-адреса находятся за пределами диапазона /30, но в пределах 192.168.90.0/24.

При использовании /16 (например, 172.16.0.0/16 в данном случае) ожидаемый результат:

172.16.35.154
172.16.97.234
172.16.31.37
172.16.201.87
172.16.57.212
172.16.254.128
172.16.183.172
172.16.54.210
172.16.248.145
172.16.186.83
172.16.250.34
172.16.250.160
172.16.23.185
172.16.125.238
172.16.206.16
172.16.57.32
172.16.65.137
172.16.202.94
172.16.164.138
172.16.241.182
172.16.154.186
172.16.197.103
172.16.184.21
172.16.96.172
172.16.195.86

Это правильно работает и с /24 (например, 192.168.90.0/24):

192.168.90.253
192.168.90.156
192.168.90.65
192.168.90.189
192.168.90.22
192.168.90.238
192.168.90.150
192.168.90.106
192.168.90.63
192.168.90.64
192.168.90.64
192.168.90.54
192.168.90.104
192.168.90.110
192.168.90.34
192.168.90.187
192.168.90.202
192.168.90.73
192.168.90.206
192.168.90.13
192.168.90.15
192.168.90.220
192.168.90.114
192.168.90.125
192.168.90.70

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

Я также разрабатываю это для Linux (Ubuntu 20.04 на ядре 5.4.0).

Любая помощь будет принята с благодарностью и спасибо за ваше время!


person Christian Deacon    schedule 26.10.2020    source источник
comment
Первый вопрос: что такое RAND_MAX в вашей системе? Второй вопрос: почему статическая маска для основной части? Она должна быть инверсией другой маски, как в 0xFFFF0000 и 0x0000FFFF. Сетевая маска против маски хоста.   -  person tadman    schedule 26.10.2020
comment
Расшифруйте IPv4 до uint32_t нативного, сделайте свои вычисления, а затем верните его обратно к сетевому порядку байтов.   -  person tadman    schedule 26.10.2020
comment
@tadman В /usr/include/stdlib.h, RAND_MAX определяется как 2147483647. Я также пробовал отменить определение и переопределить его в своей программе как 2147483647 на всякий случай и получил те же результаты, что и выше.   -  person Christian Deacon    schedule 26.10.2020
comment
По крайней мере, это не что-то глупое и маленькое, вроде 32768. Вам, вероятно, следует сгенерировать 4 байта rand(), а затем объединить их в случайный IPv4-адрес, который вы можете при необходимости замаскировать для другого.   -  person tadman    schedule 26.10.2020
comment
Я думаю, было бы просто получить случайное 32-битное целое число без знака, затем сдвинуть его вправо на количество битов маски, а затем добавить его к 32-битному сетевому адресу без знака. Например, 10.0.0.0/8, вы должны сдвинуть случайное число вправо на 8 бит (/8), а затем добавить его к 32-битному целому числу без знака 10.0.0.0 (10100000000000000000000000000000).   -  person Ron Maupin    schedule 26.10.2020
comment
@tadman, это действительно не работает, если маска заканчивается октетом, только если она попадает на границу октета. На самом деле, случайное число просто должно быть длиной 32 - mask length, а затем добавляться к сетевому адресу.   -  person Ron Maupin    schedule 26.10.2020
comment
@RonMaupin Это работает, если вы используете для вычислений нативный порядок байтов. Попытка сделать это с порядком байтов по сети - всего лишь рецепт беспорядка.   -  person tadman    schedule 26.10.2020
comment
@tadman, это не имеет значения, потому что преобразование текста в целое число может быть выполнено изначально, случайное и сложение - изначально, а затем преобразование обратно в текст. Это действительно очень просто.   -  person Ron Maupin    schedule 26.10.2020
comment
@RonMaupin Это от текста к сетевому порядку, а не порядку хоста.   -  person tadman    schedule 26.10.2020
comment
@tadman, если вы выполните математические вычисления для преобразования текста в целое число без знака, это будет собственное целое число, не обязательно сетевой порядок байтов. Затем вы выполняете собственные математические вычисления, чтобы получить исходный результат, а затем выполняете математические операции, чтобы преобразовать его обратно в текст. Я делаю это все время, и это хорошо работает.   -  person Ron Maupin    schedule 26.10.2020
comment
@RonMaupin Зачем делать математику, когда inet_aton выполняет правильное преобразование в форму хоста, и вы можете перевернуть это с помощью ntohl?   -  person tadman    schedule 26.10.2020
comment
@tadman, потому что мне не нужно загружать библиотеку, которая выполняет ту же математику, плюс много других вещей, которые мне действительно не нужны, чтобы просто манипулировать IP-адресами. По-вашему, это выглядит проще, но мне не нужны все остальные вещи в библиотеке, и он действительно выполняет те же вычисления, что и я, поэтому проще и быстрее делать это по-своему, когда мне не нужно все остальное в библиотека.   -  person Ron Maupin    schedule 26.10.2020
comment
@RonMaupin Ваши комментарии здесь кажутся неуместными. Это не какая-то экзотическая библиотека, это часть стандарта POSIX, которая загружается, нравится вам это или нет. Это часть libc. Так делалось десятилетиями. Если вы анализируете IP-адреса своим собственным кодом, вы ошибаетесь. IPv4 может быть выражен разными способами, а не только x.x.x.x.   -  person tadman    schedule 27.10.2020
comment
@tadman, это действительно зависит от языка, который вы используете. Я знаю, как анализировать все допустимые формы IPv4 и IPv6, и использую его постоянно. Поскольку я понимаю, как это делать и как это работает, я могу делать это на разных языках программирования.   -  person Ron Maupin    schedule 27.10.2020
comment
@RonMaupin Это вопрос C, поэтому сосредоточьтесь на нем, но даже тогда, поскольку C является общей основой, вы найдете эти функции во многих других языках.   -  person tadman    schedule 27.10.2020
comment
@tadman, я не публиковал ответ, я предоставил комментарии, которые объяснили принципы работы адресации IPv4, чтобы OP действительно мог это понять. Фактически, под обложками ваш ответ завершается выполнением большего количества вычислений (от текста к сети для размещения порядка), чем то, что я объяснил (от текста к порядку размещения).   -  person Ron Maupin    schedule 27.10.2020


Ответы (2)


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

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <time.h>

int main(int argc, char** argv)
{
  if (argc < 3) {
    printf("Usage: cidrrand net cidr_size\n");
    exit(-1);
  }

  char *sip = argv[1];
  uint8_t cidr = atoi(argv[2]);

  srand(time(NULL));

  struct in_addr inaddr;
  inet_aton(sip, &inaddr);
  uint32_t ipaddr = ntohl(inaddr.s_addr);
  uint32_t host_mask = (1 << (32 - cidr)) - 1;

  for (int i = 0; i < 25; i++)
  {
    uint32_t host_rand = rand();

    // Attempt to pick a random IP from the CIDR range. We shift left by the CIDR range since it's big endian.
    uint32_t newIP = (ipaddr & ~host_mask) | (host_mask & host_rand);

    // Convert the new IP to a string and print it.
    struct in_addr ip;
    ip.s_addr = htonl(newIP);

    fprintf(stdout, "%s\n", inet_ntoa(ip));
  }

  return 0;
}

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

person tadman    schedule 26.10.2020

Действительно полезная тема здесь - спасибо Кристиану и спасибо Скотту за пересмотренную версию исходного кода.

На всякий случай, если кто-нибудь придет сюда с аналогичным вопросом для IPv6, я создал короткие примеры для генераторов IPv4 и IPv6 здесь.

Он включает двоичное представление адресов и масок для лучшего понимания.

То, с чем я сейчас борюсь в IPv6, - это повторное использование всех доступных октетов из одного 32-битного случайного значения при повторении октетов IPv6, которые необходимо изменить в соответствии с сетевой маской:

https://github.com/defanator/cidr-random/blob/master/cidr_random6.c#L133-L150

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

person Andrei Belov    schedule 12.03.2021