network-programmingvbscriptasp-classiciis-6ipv4

Classic ASP: Check if a IP is within an IP range / CIDR


Using Classic ASP, I want to check if a given IP is within an IP range in CIDR (e.g. 192.168.0.0/24).

There are already some questions covering scenarios where I could get IP ranges from their CIDR, but that would still leave me with math to do to tell a given IP address is within the provided CIDR.

I know by routing protocols that binary calculation could be done to tell at once whether an address belongs to a given CIDR (route), so I wouldn't like to get a range, split it in four twice, and check every number for greater than or smaller than. But I couldn't find any such implementation for Classic ASP / VBScript.

Some examples would be:

And, of course, going out with the bitmasks from /32 to /0 (well the latter one should say "yes" to everything)

The actual use case is to check if a CloudFlare-proxied connection claiming CF-Connecting-IP header is actually coming from CloudFlare by the list they provide of ipv4 endpoints.

As I didn't find anything doing what I needed, I've written it my own the way I believe IPv4 works, getting to this:

function ip4toint(ip)
 ip_fields = split(ip, ".")
 ip4toint = cint(ip_fields(0))*(256^3) + cint(ip_fields(1))*(256^2) + cint(ip_fields(2))*(256) + cint(ip_fields(3))
end function

function ip4netmask(ip, cidr)
 cidr_fields = split(cidr, "/")
 cidr_ip = cidr_fields(0)
 cidr_int = ip4toint(cidr_ip)
 mask_bits = cidr_fields(1)

 if mask_bits = 0 then ' no need for more math. always true
  ip4netmask = true
  exit function
 end if
 
 ip_int = ip4toint(ip)

 mask_int = cint("&hffffffff")
 if mask_bits < 32 and mask_bits > 0 then
  mask_int = mask_int - (2^(32 - cint(mask_bits)) - 1)
 end if

 ip4netmask = (cidr_int and mask_int) = (ip_int and mask_int)
end function

Thus I used this to test the results:

function testip4n(ip, range)
 testip4n = "ip:" & ip & " range cidr: " & range & " ip in range: " & ip4netmask(ip, range)
end function

Then calling it from a test page with various values and the expected result:

<%=testip4n("10.3.172.198", "10.3.172.198/32")%> [expect:true]<br />
<%=testip4n("10.3.172.198", "10.3.172.199/32")%> [expect:false]<br />
<%=testip4n("10.3.172.198", "10.3.172.197/32")%> [expect:false]<br />
<%=testip4n("10.3.172.198", "10.3.172.199/31")%> [expect:true]<br />
<%=testip4n("10.3.172.198", "10.3.172.197/31")%> [expect:false]<br />
<%=testip4n("10.3.172.198", "10.3.172.100/24")%> [expect:true]<br />
<%=testip4n("10.3.172.198", "10.3.172.205/24")%> [expect:true]<br />
<%=testip4n("10.3.172.198", "10.3.174.198/24")%> [expect:false]<br />
<%=testip4n("10.3.172.198", "10.3.170.198/20")%> [expect:true]<br />
<%=testip4n("10.3.172.198", "10.3.170.198/0")%> [expect:true]<br />
<%=testip4n("10.1.172.198", "10.3.170.198/0")%> [expect:true]<br />
<%=testip4n("10.3.112.198", "10.3.170.198/0")%> [expect:true]<br />

So far, so good. But once I go wild and compare these against actual IP addresses, I'm haunted by overflow exceptions in ASP. That's because I'm going as far as (2^32 - 1) whereas ASP's integer (which is not unsigned) goes as far as 2^31 (from -2^31 + 1, or the other way around)

I guess unless I find a smart byte-concatenation way to do this, I might need to "encapsulate" the parts in strings or half-integers, which doesn't look (and perform) good at all.

EDIT: as the answer I proposed myself has an "existential flaw", I'm moving it here as "what I've tried"


Solution

  • For now I'm going with the "packing into" two signed integers, although a waste of memory, I believe this to be better performance-wise than if I went all out with string or maybe possible byte sequences.

    function ip4netmask(ip, cidr)
     cidr_fields = split(cidr, "/")
     cidr_ip = cidr_fields(0)
     mask_bits = cidr_fields(1)
    
     cidr_ip_parts = split(cidr_ip, ".")
     cidr_int0 = cidr_ip_parts(0)*256 + cidr_ip_parts(1)
     cidr_int1 = cidr_ip_parts(2)*256 + cidr_ip_parts(3)
      
     if mask_bits = 0 then ' no need for more math. always true
      ip4netmask = true
      exit function
     end if
     
     ip_parts = split(ip, ".")
     ip_int0 = ip_parts(0)*256 + ip_parts(1)
     ip_int1 = ip_parts(2)*256 + ip_parts(3)
     
     mask_int0 = 65535 ' 2^16 - 1
     mask_int1 = 65535
     if mask_bits < 16 then
      mask_int0 = mask_int0 - (2^(16 - mask_bits)) + 1
      mask_int1 = 0
     elseif mask_bits < 32 then
      mask_int1 = mask_int1  - (2^(32 - mask_bits)) + 1
     end if
    
     ip4netmask = (cidr_int0 and mask_int0) = (ip_int0 and mask_int0) and (cidr_int1 and mask_int1) = (ip_int1 and mask_int1)
    end function
    

    The same testip4n(ip, range) function should work to test this one. I got rid of the ip4toint() function as I would no longer benefit from a single split() call, so hopefully I'm not compromising too much in terms of speed here.