1

So I have an IP like 45.76.255.14, and I have a table with rows of CIDR stored as a single varchar, how would I select CIDRs that are in the range of that IP address. For example 45.76.255.14/31

So in theory: select CIDR where in range of IP

RumbleFrog
  • 49
  • 2
  • 11
  • It doesn't really make sense.. Ip is not a range. A cidr can be... – Dekel Aug 13 '17 at 00:31
  • @Dekel, You're correct, an IP is not a range, but I'm asking to fetch CIDRs that are in range of the IP, aka includes the IP – RumbleFrog Aug 13 '17 at 06:28
  • In theory: `select * from cidrlist where @ip between startip(cidr) and endip(cidr)`. Practically, it will depend on how you stored your data. If you e.g. store ranges as varchars (`45.76.255.14/31`), it will be harder to do the comparison than if you store them as integers for start and end of a range. But since you didn't give us your data model, the theory should suffice. A hint for doing it practically: MySQL has a function to convert an ip string (without subnet) to int: `INET_ATON()`. Depending on your data, you might need to write functions like `startip()`/`endip()` to convert a range. – Solarflare Aug 13 '17 at 09:07
  • @Solarflare, I'm storing it in a single varchar column, would that be too difficult? – RumbleFrog Aug 13 '17 at 19:12

2 Answers2

3

Storing IP addresses in dotted quad notation in a VARCHAR is not the most optimal way of storing them, since dotted-quad is a human friendly representation of a 32 bit unsigned integer that doesn't lend itself to database indexing. But sometimes it's fundamentally more convenient, and at small scale, the fact that queries require a table scan isn't usually a problem.

MySQL Stored Functions are a good way of encapsulating relatively complex logic behind a simple function that can be referenced in a query, potentially leading to easier-to-understand queries and reducing copy/paste errors.

So, here's a stored function I wrote called find_ip4_in_cidr4(). It works somewhat similarly to the built-in function FIND_IN_SET() -- you give it a value and you give it a "set" (CIDR spec) and it returns a value to indicate whether the value is in the set.

First, an illustration of the function in action:

If the address is inside the block, return the prefix length. Why return the prefix length? Non-zero integers are "true," so we could just return 1, but if you want to sort the matching results to find the shortest or longest of multiple matching prefixes, you can ORDER BY the return value of the function.

mysql> SELECT find_ip4_in_cidr4('203.0.113.123','203.0.113.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('203.0.113.123','203.0.113.0/24') |
+-----------------------------------------------------+
|                                                  24 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT find_ip4_in_cidr4('192.168.100.1','192.168.0.0/16');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','192.168.0.0/16') |
+-----------------------------------------------------+
|                                                  16 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

Not in the block? That returns 0 (false).

mysql> SELECT find_ip4_in_cidr4('192.168.100.1','203.0.113.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','203.0.113.0/24') |
+-----------------------------------------------------+
|                                                   0 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT find_ip4_in_cidr4('192.168.100.1','192.168.0.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','192.168.0.0/24') |
+-----------------------------------------------------+
|                                                   0 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

There's a special case for the all-zeroes address, we return -1 (still "true", but preserves the sort order):

mysql> SELECT find_ip4_in_cidr4('192.168.100.1','0.0.0.0/0');
+------------------------------------------------+
| find_ip4_in_cidr4('192.168.100.1','0.0.0.0/0') |
+------------------------------------------------+
|                                             -1 |
+------------------------------------------------+
1 row in set (0.00 sec)

Nonsense arguments return null:

mysql> SELECT find_ip4_in_cidr4('234.467.891.0','192.168.0.0/24');
+-----------------------------------------------------+
| find_ip4_in_cidr4('234.467.891.0','192.168.0.0/24') |
+-----------------------------------------------------+
|                                                NULL |
+-----------------------------------------------------+
1 row in set (0.00 sec)

Now, teh codez:

DELIMITER $$

DROP FUNCTION IF EXISTS `find_ip4_in_cidr4` $$
CREATE DEFINER=`mezzell`@`%` FUNCTION `find_ip4_in_cidr4`(
  _address VARCHAR(15), 
  _block VARCHAR(18)
) RETURNS TINYINT
DETERMINISTIC /* for a given input, this function always returns the same output */
CONTAINS SQL /* the function does not read from or write to tables */
BEGIN

-- given an IPv4 address and a cidr spec,
-- return -1 for a valid address inside 0.0.0.0/0
-- return prefix length if the address is within the block,
-- return 0 if the address is outside the block,
-- otherwise return null

DECLARE _ip_aton INT UNSIGNED DEFAULT INET_ATON(_address);
DECLARE _cidr_aton INT UNSIGNED DEFAULT INET_ATON(SUBSTRING_INDEX(_block,'/',1));
DECLARE _prefix TINYINT UNSIGNED DEFAULT SUBSTRING_INDEX(_block,'/',-1);
DECLARE _bitmask INT UNSIGNED DEFAULT (0xFFFFFFFF << (32 - _prefix)) & 0xFFFFFFFF;

RETURN CASE /* the first match, not "best" match is used in a CASE expression */
  WHEN _ip_aton IS NULL OR _cidr_aton IS NULL OR /* sanity checks */
       _prefix  IS NULL OR _bitmask IS NULL OR
       _prefix NOT BETWEEN 0 AND 32 OR
       (_prefix = 0 AND _cidr_aton != 0) THEN NULL
  WHEN _cidr_aton = 0 AND _bitmask = 0 THEN -1
  WHEN _ip_aton & _bitmask = _cidr_aton & _bitmask THEN _prefix /* here's the only actual test needed */
  ELSE 0 END;

END $$
DELIMITER ;

An issue that is not specific to stored functions, but rather applies to most functions on most RDBMS platforms is that when a column is used as an argument to a function in WHERE, the server can't "look backwards" through the function to use an index to optimize the query.

Michael - sqlbot
  • 169,571
  • 25
  • 353
  • 427
  • Thanks, I'm not storing the IPs, I'm only storing the CIDR and matching the input with the database. This explains a lot. – RumbleFrog Aug 15 '17 at 02:56
2

With the assist of this question: MySQL query to convert CIDR into IP range

Here's the solution that works for me:

SELECT
    `cidr`
FROM
    cidr_list
WHERE
    INET_ATON('IP') BETWEEN(
        INET_ATON(SUBSTRING_INDEX(`cidr`, '/', 1)) & 0xffffffff ^(
            (
                0x1 <<(
                    32 - SUBSTRING_INDEX(`cidr`, '/', -1)
                )
            ) -1
        )
    ) AND(
        INET_ATON(SUBSTRING_INDEX(`cidr`, '/', 1)) |(
            (
                0x100000000 >> SUBSTRING_INDEX(`cidr`, '/', -1)
            ) -1
        )
    )
RumbleFrog
  • 49
  • 2
  • 11
  • You don't really need a `BETWEEN` test, since all you really need to evaluate is whether network & mask == address & mask. The mask itself provides the "between" by masking the insignificant bits. – Michael - sqlbot Aug 14 '17 at 00:52
  • @Michael-sqlbot, What do you propose I use instead? – RumbleFrog Aug 14 '17 at 03:26
  • Nothing wrong with what you're doing, I'm just saying you don't have to do the end range calculation, because that's the point of the netmask. I use a stored function to encapsulate the logic. – Michael - sqlbot Aug 14 '17 at 04:03
  • @Michael-sqlbot, I'm relatively new to CIDR and Netmasks, if you have a more optimized answer, I'll take it over mine. – RumbleFrog Aug 14 '17 at 05:03
  • It will probably be a few hours before I have an opportunity to do the subject justice -- if you have seen some of my answers, I try to make them pretty thorough -- but I'll be happy to post what I have. I believe I may have a `find_ip4_in_cidr4()` funtion that is logically similar to the built-in `FIND_IN_SET()` function, but for matching address ranges. – Michael - sqlbot Aug 14 '17 at 12:43
  • @Michael-sqlbot, that's fine, I'll just wait cause I have no idea what those functions do – RumbleFrog Aug 14 '17 at 19:24
  • I want to reiterate, I'm not saying there's anything wrong with your answer, only that it can be implemented in a way that's a little easier on the eyes and potentially less error-prone. Hopefully you'll find my answer useful, as well. – Michael - sqlbot Aug 15 '17 at 00:40