Choosing A Random IP From Any Specific CIDR Range In C

0

I am trying to create a function that'll be able to parse any IP/CIDR range and choose a random IP within this specific range as a string in C (including a /32, which would just return the single IP address each time). As of right now, I'm fine with it including reserved IPs (e.g. broadcast) and if I have trouble excluding those in the future, I'll post a separate question.

I am still fairly new to this area since I don't have much experience with using bitwise operators on integer's bits yet (I understand the bitwise operators themselves, but I'm trying to figure out how to use them with networking and IPs). I've also read most of this question which gives a lot of great advice/guidance (thank you Ron Maupin for providing me with this), but I'm still struggling to get this function completely working.

I have nearly working code, but for some reason using a /8 CIDR or anything smaller than /24 results in odd behavior. Using /16 and /24 works as expected (those are all I've tested so far).

Here's the code I have:

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

This simply chooses a random IP 25 times from the given IP/CIDR. When using a /8 (e.g. 10.0.0.0/8), this is the output I receive:

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

The last octet is always 0. I'd imagine I'm doing something incorrect when shifting to the left by the CIDR range when creating the random IP 32-bit integer. However, I'm not sure what I'm supposed to do here.

When using a /30 range (e.g. 192.168.90.4/30), here's the output I receive:

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

It chooses 192.168.90.4 at times which is correct, but the other three random IPs are outside of the /30 range, but within 192.168.90.0/24.

When using a /16 (e.g. 172.16.0.0/16 in this case), here's the output which is to be expected:

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

This works correctly with a /24 as well (e.g. 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

I was wondering if anybody knew what I was doing wrong here. I apologize if I'm missing something obvious as well.

I am also developing this on Linux (Ubuntu 20.04 on the 5.4.0 kernel).

Any help would be highly appreciated and thank you for your time!

c
networking
cidr

1 Answer

1

I reworked this using host-endian calculations and also moved a lot of things out of the loop that shouldn't have been there in the first place:

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

When seeding random numbers, try and seed once and once only. Don't mess with it unless you have a specific goal relating to generating several reproducible series.

answered on Stack Overflow Oct 26, 2020 by tadman

User contributions licensed under CC BY-SA 3.0