Search code examples
objective-cipv6

Determine if IPv6 Address is within range using Objective-C


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

Solution

  • 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)