Matching IPv6 addresses against CIDR masks in MySQL
CIDR matching is a common task operating large sets of IP addresses. As more networks are getting IPv6 support, we start receiving IPv6 datasets for analysis and need to match those against CIDR masks as one of steps.
Doing so in MySQL is impossible in the most common way - by applying bitmasks. Binary representation of IPv6 addresses are 128-bit numbers, while longest integer type in MySQL is BIGINT
, 8 bytes (64 bits) long. That’s why result of INET6_ATON()
is VARBINARY
, and you can’t have MySQL apply bitwise operators on it.
Another approach is to calculate first and last IPv6 addresses matching given CIDR mask, and match those entries falling between the two. This is discussed on stackoverflow, but the proposed implementation is buggy. Here’s the proper solution we coded for our project:
DELIMITER $$
CREATE FUNCTION `FirstIPv6MatchingCIDR` (`ip` VARCHAR(46), `mask` INT(2) UNSIGNED) RETURNS VARCHAR(39) DETERMINISTIC
BEGIN
RETURN INET6_NTOA(UNHEX(RPAD(
CONCAT(
SUBSTR(HEX(INET6_ATON(`ip`)), 1, FLOOR(`mask` / 4)),
HEX(
((POW(2, `mask` % 4) - 1) * POW(2, 4 - (`mask` % 4)))
& COALESCE(CONV(SUBSTR(HEX(INET6_ATON(`ip`)), FLOOR(`mask` / 4) + 1, 1), 16, 10), '')
)
),
32, 0
)));
END$$
CREATE FUNCTION `LastIPv6MatchingCIDR` (`ip` VARCHAR(46), `mask` INT(2) UNSIGNED) RETURNS VARCHAR(39) DETERMINISTIC
BEGIN
DECLARE `ipNumber` VARBINARY(16);
DECLARE `last` VARCHAR(39) DEFAULT '';
DECLARE `flexBits`, `counter`, `deci`, `newByte` INT UNSIGNED;
DECLARE `hexIP` VARCHAR(32);
SET `ipNumber` = INET6_ATON(`ip`);
SET `hexIP` = HEX(`ipNumber`);
SET `flexBits` = 128 - `mask`;
SET `counter` = 32;
WHILE (`flexBits` > 0) DO
SET `deci` = CONV(SUBSTR(`hexIP`, `counter`, 1), 16, 10);
SET `newByte` = `deci` | (POW(2, LEAST(4, `flexBits`)) - 1);
SET `last` = CONCAT(CONV(`newByte`, 10, 16), `last`);
IF `flexBits` >= 4 THEN
SET `flexBits` = `flexBits` - 4;
ELSE
SET `flexBits` = 0;
END IF;
SET `counter` = `counter` - 1;
END WHILE;
SET `last` = CONCAT(SUBSTR(`hexIP`, 1, `counter`), `last`);
RETURN INET6_NTOA(UNHEX(`last`));
END $$
DELIMITER ;
Example SELECT
:
SELECT @firstIP := INET6_ATON(FirstIPv6MatchingCIDR('::ffff:0:0', 96));
SELECT @lastIP := INET6_ATON(LastIPv6MatchingCIDR('::ffff:0:0', 96));
SELECT `ip` FROM `ip_db` WHERE INET6_ATON(`ip`) BETWEEN @firstIP AND @lastIP;