从 C 中的任何特定 CIDR 范围中选择随机 IP

Choosing A Random IP From Any Specific CIDR Range In C

我正在尝试创建一个函数,该函数将能够解析任何 IP/CIDR 范围并在该特定范围内选择一个随机 IP 作为 C 中的字符串(包括 /32,其中每次只会 return 一个 IP 地址)。截至目前,我对它包括保留 IP(例如广播)没意见,如果我以后在排除这些 IP 时遇到问题,我会 post 一个单独的问题。

我对这个领域还很陌生,因为我还没有太多在整数位​​上使用按位运算符的经验(我了解按位运算符本身,但我正在尝试弄清楚如何将它们与网络和 IP)。我还阅读了 this 问题的大部分内容,这些问题给出了很多很棒的 advice/guidance (感谢 Ron Maupin 为我提供了这个),但我仍在努力让这个功能完全发挥作用。

我的代码几乎可以正常工作,但出于某种原因,使用 /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/CIDR 中随机选择一个 IP 25 次。使用 /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。我想我在创建随机 IP 32 位整数时向左移动 CIDR 范围时做错了什么。但是,我不确定我应该在这里做什么。

当使用 /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 内核上)。

非常感谢任何帮助,感谢您抽出宝贵时间!

我使用 host-endian 计算重新设计了这个,并且还从循环中移出了很多本来不应该存在的东西:

#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;
}

播种随机数时,尽量只播种一次。除非您有与生成多个可复制系列相关的特定目标,否则不要乱用它。

这里的主题非常有用 - 感谢 Christian,并感谢 Scott 重新访问了初始代码版本。

以防万一有人在这里遇到类似的 IPv6 问题,我已经为 IPv4 和 IPv6 生成器创建了一个简短示例 here

它包括地址和掩码的二进制表示,以便更好地理解。

我目前在 IPv6 中遇到的问题是重用来自单个 32 位随机值的所有可用八位字节,同时迭代需要根据网络掩码更改的 IPv6 八位字节:

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

无论如何,希望它能作为任何感兴趣的人的起点。