Avenger
Avenger

Reputation: 365

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"

Upvotes: 0

Views: 120

Answers (2)

Joel Coehoorn
Joel Coehoorn

Reputation: 416101

IP addresses work the way you describe (for IPv4). From a high-level view, other than needing unsigned integers, the code seems correct. Unfortunately, to my knowledge old vbscript does not offer a native unsigned integer.

Since we already have high-order and low-order portions of the integer value via the dotted quad, you may need to do separate operations (on Long values) to get the desired result.

Upvotes: 0

Avenger
Avenger

Reputation: 365

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.

Upvotes: 2

Related Questions