How to calculate a IPv6 CIDR route prefix in SQL?

2019-05-10 04:45发布

问题:

I've been working on generating a CIDR from ranges of IPv4 and IPv6 addresses using T-SQL running on a SQL Server 2012 instance. Generally our application (outside of the db) is responsible for calculating a CIDR, but I presently have need to do the CIDR calculation in the database. Because IPv6 is too large to store in the bigint datatype we store IP addresses as either binary(4) or binary(16).

Calculating the route prefix for an IPv4 range is relatively simple if a little ugly:

declare @ipv4_begin binary(4)
       ,@ipv4_end binary(4)

set @ipv4_begin = 0xC0A80000 -- '192.168.000.000'
set @ipv4_end = 0xC0A8FFFF   -- '192.168.255.255'

select 32 - LOG(
                Cast(@ipv4_end As bigint)
                - Cast(@ipv4_begin As bigint) + 1, 2
               ) as ipv4_route_prefix

Sadly the same query modified for IPv6 does not work. The reason it does not work is because IPv6 addressees are larger than what can be stored in the bigint datatype (the reason we use binary(4) and binary(16) for storage):

declare @ipv6_begin binary(16)
       ,@ipv6_end binary(16)

set @ipv6_begin = 0xFC000000000000000000000000000000 -- fc00:: 
set @ipv6_end = 0xFC00000000000000FFFFFFFFFFFFFFFF   -- fc00::ffff:ffff:ffff:ffff

-- This will cause error: 'An invalid floating point operation occurred.'
select 128 - LOG(
                 Cast(@ipv6_end As bigint)
                 - Cast(@ipv6_begin As bigint) + 1, 2
                ) as ipv6_route_prefix

Aside from wonky bitwise operations (that ultimately didn't work) I haven't come up anything that will do this calculation in the database.

Can a route prefix for an IPv6 CIDR be calculated from an IPv6 address range in T-SQL?

回答1:

Well, you already have a neat trick for IPv4 -- just chop the value up into the biggest chunk we can handle and repeat the trick.

SELECT ISNULL(MIN(32 - B + N), 128) 
FROM (VALUES
    (LOG(
        CONVERT(BIGINT, SUBSTRING(@ip_end,    1, 4)) - 
        CONVERT(BIGINT, SUBSTRING(@ip_begin,  1, 4)
        ) + 1, 2),  0),
    (LOG(
        CONVERT(BIGINT, SUBSTRING(@ip_end,    5, 4)) - 
        CONVERT(BIGINT, SUBSTRING(@ip_begin,  5, 4)
        ) + 1, 2), 32),
    (LOG(
        CONVERT(BIGINT, SUBSTRING(@ip_end,    9, 4)) - 
        CONVERT(BIGINT, SUBSTRING(@ip_begin,  9, 4)
        ) + 1, 2), 64),
    (LOG(
        CONVERT(BIGINT, SUBSTRING(@ip_end,   13, 4)) - 
        CONVERT(BIGINT, SUBSTRING(@ip_begin, 13, 4)
        ) + 1, 2), 96)
) AS Bits(B, N)
WHERE B <> 0;

We determine the position of the first set bit in each chunk, then pick the lowest such bit -- if there is no such bit, all bits match (the ISNULL covers that case). This works for IPv4 too if you replace the "128" with "32", though obviously you already have an expression for that. We can pack it up in a function that will work for both:

CREATE FUNCTION dbo.CidrPrefixFromRange(@ip_begin VARBINARY(16), @ip_end VARBINARY(16)) 
RETURNS TABLE AS
RETURN
    SELECT ISNULL(MIN(32 - B + N), DATALENGTH(@ip_begin) * 8) AS Prefix
    FROM (VALUES
        (LOG(
            CONVERT(BIGINT, SUBSTRING(@ip_end,    1, 4)) - 
            CONVERT(BIGINT, SUBSTRING(@ip_begin,  1, 4)
            ) + 1, 2),  0),
        (LOG(
            CONVERT(BIGINT, SUBSTRING(@ip_end,    5, 4)) - 
            CONVERT(BIGINT, SUBSTRING(@ip_begin,  5, 4)
            ) + 1, 2), 32),
        (LOG(
            CONVERT(BIGINT, SUBSTRING(@ip_end,    9, 4)) - 
            CONVERT(BIGINT, SUBSTRING(@ip_begin,  9, 4)
            ) + 1, 2), 64),
        (LOG(
            CONVERT(BIGINT, SUBSTRING(@ip_end,   13, 4)) - 
            CONVERT(BIGINT, SUBSTRING(@ip_begin, 13, 4)
            ) + 1, 2), 96)
    ) AS Bits(B, N)
    WHERE B <> 0;

Sample uses:

-- 192.168.100.0 - 192.168.103.255
SELECT * FROM dbo.CidrPrefixFromRange(0xc0a86400, 0xc0a867ff) -- /22

-- 192.168.0.0 - 192.168.255.255
SELECT * FROM dbo.CidrPrefixFromRange(0xC0A80000, 0xC0A8FFFF) -- /16

-- fc00:: - fc00::ffff:ffff:ffff:ffff
SELECT * FROM dbo.CidrPrefixFromRange(
    0xFC000000000000000000000000000000,
    0xFC00000000000000FFFFFFFFFFFFFFFF
) -- /64

-- 127.0.0.1 - 127.0.0.1
SELECT * FROM dbo.CidrPrefixFromRange(0x7f000001, 0x7f000001) -- /32

No promises as to how efficient this is... if you want efficiency, this is not the sort of thing you want to do in T-SQL. :-)

Addendum: the reason I use a table-valued function and not a simpler scalar-valued function (after all, we're only returning one value) is that scalar-valued functions perform far worse inside a query. An inline table-valued function can be efficiently CROSS APPLY'd to a table. For that reason I write every function as an inline TVF as a matter of habit even if I don't foresee such a use -- anything is better than a scalar-valued function.