1

I'm trying to identify the most efficient way to determine if an IPv6 address is within a range of addresses (including the start and end addresses). Given the sheer quantity of addresses that could potentially exist between the start and end addresses, I certainly don't want to iterate through the range looking for a match.

I've received some advice that trying to do this sort of range matching is not a good approach, or not practical/useful for IPv6, and that instead I should look into prefix matching. Before going that route, I'd like to see what efficient solutions may exist for determining if the IPv6 address is with a given range.

I am not a networking expert, so please excuse some of my ignorance on this matter.

I found this Stack Overflow post with an unaccepted answer indicating the best way to do this is to convert all of the hexadecimals of the IPv6 address into binary and then do a compare. Two issues with this post: 1. I'm not sure how to adapt the PHP script into Objective-C, and 2. This answer has some upvotes, but is not an accepted answer so I'm uncertain on whether this is worth pursuing.

I also found this other Stack Overflow post which seems to work well for converting hexadecimals into binary, but again, I'm not certain how to do a compare between the resulting binary values to determine range. Note that I'm using the sample code from the question, not the answer. Adapting this code for my purpose gives me output like the following using example IPv6 address fe80::34cb:9850:4868:9d2c:

fe80 = 1111111010000000
0000 = 0
0000 = 0
0000 = 0
34cb = 11010011001011
9850 = 1001100001010000
4868 = 100100001101000
9d2c = 1001110100101100

The end result I am trying to achieve is to be able to react when a Mac's IP address is within a defined range. I've got the solution in place for IPv4 addresses, but I want add IPv6 support to my free macOS app, Amphetamine. Thanks in advance for any assistance. This is what I'm using for IPv4 range checking... not sure if it could be adapted for use with IPv6:

- (bool) ipAddress: (NSString *)ipAddress isBetweenIpAddress: (NSString *)rangeStart andIpAddress: (NSString *)rangeEnd
{
    uint32_t ip = [self convertIpAddress: ipAddress];
    uint32_t start = [self convertIpAddress: rangeStart];
    uint32_t end = [self convertIpAddress: rangeEnd];
    return ip >= start && ip <= end;
}

- (uint32_t) convertIpAddress: (NSString *) ipAddress
{
    struct sockaddr_in sin;
    inet_aton([ipAddress UTF8String], &sin.sin_addr);
    return ntohl(sin.sin_addr.s_addr);
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • A simple determination if an IP address is between two other IP addresses really is not all that useful. A better approach is to see if an IP address falls within a network. That can be accomplished using a bitwise AND of the address and mask to see if the result matches the network address. The IPv4 math is all explained in [this two-part answer](https://networkengineering.stackexchange.com/a/53994/8499), and IPv6 math is the same, except it is an unsigned 128-bit integer (or two unsigned 64-bit integers) instead of one unsigned 32-bit integer. – Ron Maupin Feb 01 '20 at 22:28
  • Hi Ron, Thanks for your comment and link to a very interesting write-up. To be honest though, a lot of it went well over my head. I think I'll need to stick with the range approach for now, 1. given that I understand it, and 2. I've already implemented this in my app for ipv4 addresses. People tend to freak out when I remove features. That said, I expect that the approach you are suggesting may indeed accomplish what users of my app want - determining what network they are on via IP address. Technically though, can this not also be accomplished using range? – William Gustafson Feb 02 '20 at 01:17
  • The problem with the range is that is may not actually be a network, and it could be a small subset of a network, or it could straddle two networks. I have never really seen a need for testing if an address is in a range that is not a network range, but it is extremely common to test if an address is in a network. In any case, the link explains all the IPv4 address and subnetting math, and it is easy to implement in almost any programming language. Really the language just needs to support bitwise AND an OR. – Ron Maupin Feb 02 '20 at 02:22

1 Answers1

1

This problem requires two steps: (1) parse the address and range start/end from strings; (2) compare the values. The APIs available for both parts are C-based and so are not particularly user-friendly.

  • inet_pton can be used to parse the string and put it in a in6_addr struct.
  • The in6_addr contains a .s6_addr which is an array of 16 bytes in big-endian order,
  • …which can be compared to another address with memcmp.

Putting it together:

NSString *rangeStart = @"2001:db8::";
NSString *rangeEnd = @"2001:db8::ffff:ffff:ffff:ffff";

NSString *address = @"2001:db8::abc";

// Parse strings into in6_addrs
struct in6_addr rangeStartAddr;
struct in6_addr rangeEndAddr;
struct in6_addr addr;
if (inet_pton(AF_INET6, rangeStart.UTF8String, &rangeStartAddr) != 1) {
    abort();
}
if (inet_pton(AF_INET6, rangeEnd.UTF8String, &rangeEndAddr) != 1) {
    abort();
}
if (inet_pton(AF_INET6, address.UTF8String, &addr) != 1) {
    abort();
}

// Use memcmp to compare binary values
if (memcmp(rangeStartAddr.s6_addr, addr.s6_addr, 16) <= 0
    && memcmp(addr.s6_addr, rangeEndAddr.s6_addr, 16) <= 0) {
    NSLog(@"In range");
} else {
    NSLog(@"Not in range");
}

It would also be possible, but messy, to convert each address to a single __uint128_t number. However, if all you need to do is compare, memcmp seems sufficient.


The same can be done in Swift using UnsafeRawBufferPointer.lexicographicallyPrecedes instead of memcmp:

import Darwin

extension in6_addr {
    init?(_ str: String) {
        self.init()
        if inet_pton(AF_INET6, str, &self) != 1 {
            return nil
        }
    }
}

extension in6_addr: Comparable {
    public static func ==(lhs: in6_addr, rhs: in6_addr) -> Bool {
        return withUnsafeBytes(of: lhs) { lhsBytes in
            withUnsafeBytes(of: rhs) { rhsBytes in
                lhsBytes.prefix(16).elementsEqual(rhsBytes.prefix(16))
            }
        }
    }
    public static func <(lhs: in6_addr, rhs: in6_addr) -> Bool {
        return withUnsafeBytes(of: lhs) { lhsBytes in
            withUnsafeBytes(of: rhs) { rhsBytes in
                lhsBytes.prefix(16).lexicographicallyPrecedes(rhsBytes.prefix(16))
            }
        }
    }
}

var rangeStartAddr = in6_addr("2001:db8::")!
var rangeEndAddr = in6_addr("2001:db8::ffff:ffff:ffff:ffff")!
var addr = in6_addr("2001:db8::abc")!

(rangeStartAddr...rangeEndAddr).contains(addr)
jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • This appears to work great. Thanks so much for sharing. It this ends up being implemented in Amphetamine, would you like a credit for the contribution? If so, please let me know how you'd like the credit to read either in a comment or message. Thanks again! – William Gustafson Feb 02 '20 at 00:41
  • Happy to help! I’m not picky, but if you’d like to credit me you’d be welcome to link to my [github profile](https://github.com/jtbandes) or [personal website](https://bandes-stor.ch) using either my username or full name. Or you could just link back to this answer :) – jtbandes Feb 02 '20 at 03:19