Oracle PL/SQL versions of INET6_ATON and NTOA functions?
Asked Answered
O

1

3

Any have any good code for converting a IPv6 address string into an integer? Converting IPv4 seems to be fairly easy, with the one format. However, IPv6 has several different formats to show an address:

  • XXXX:XXXX:XXXX:XXXX::
  • XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX
  • XXXX:XXX:XXXX:0:0:XXXX:XXX:XXXX
  • XXXX:XXX:XXXX::XXXX:XXX:XXXX
  • ::ffff:XXXX:XXX (IPv4 in v6 format)
  • ::ffff:###.#.#.### (also valid IPv4 in v6 format)

I'd like to be able to take one of these strings and translate it into an INTEGER for IP-to-network matching, and allow for any of these formats as the input.

Oakland answered 24/6, 2011 at 15:40 Comment(1)
IPv4 has more than one way to represent an address. Try pinging "10.1" some time.Woodley
O
6

Ended up rolling my own. Also realized that Oracle's 126-bit INTEGER is not enough bits for IPv6's 128-bit addresses. Frankly, I don't know how the original C library's INET6_ATON (or INET_PTON) does it, considering that I've never heard of a 16-byte integer.

I ended up with a 32-byte hex string, which means I have to do some fancy "half-string" math on nettohex and use SUBSTR for the FBIs to work correctly. (Blasted PL/SQL doesn't allow for "RETURN CHAR(32)"...)

Overall, though, it works well, works in all formats, and allows for index-based character comparisons to find out if an IP address is within an IP range.

Here's the full code:

CREATE OR REPLACE FUNCTION ipguess(
   ip_string IN VARCHAR2
) RETURN NATURAL
DETERMINISTIC
IS
BEGIN
   -- Short-circuit the most popular, and also catch the special case of IPv4 addresses in IPv6
   IF    REGEXP_LIKE(ip_string, '\d{1,3}(\.\d{1,3}){3}')                       THEN RETURN 4;
   ELSIF REGEXP_LIKE(ip_string, '[[:xdigit:]]{0,4}(\:[[:xdigit:]]{0,4}){0,7}') THEN RETURN 6;
   ELSE                                                                             RETURN NULL;
   END IF;
END ipguess;

CREATE OR REPLACE FUNCTION iptohex(
   ip_string IN VARCHAR2
) RETURN CHAR     -- INTEGER only holds 126 binary digits, IPv6 has 128
DETERMINISTIC
IS
   iptype NATURAL := ipguess(ip_string);
   ip     VARCHAR2(32);
   ipwork VARCHAR2(64);
   d      INTEGER;
   q      VARCHAR2(3);
BEGIN
   IF    iptype = 4 THEN
      -- Sanity check
      ipwork := REGEXP_SUBSTR(ip_string, '\d{1,3}(\.\d{1,3}){3}');
      IF ipwork IS NULL THEN RETURN NULL; END IF;

      -- Starting prefix
      -- NOTE: 2^48 - 2^32 = 281470681743360 = ::ffff:0.0.0.0
      --       (for compatibility with IPv4 addresses in IPv6)
      ip := '00000000000000000000ffff';

      -- Parse the input
      WHILE LENGTH(ipwork) IS NOT NULL
      LOOP
         d := INSTR(ipwork, '.');  -- find the dot
         IF d > 0 THEN             -- isolate the decimal octet
            q      := SUBSTR(ipwork, 1, d - 1);
            ipwork := SUBSTR(ipwork, d + 1);
         ELSE
            q      := ipwork;
            ipwork := '';
         END IF;

         -- convert to a hex string
         ip := ip || TO_CHAR(TO_NUMBER(q), 'FM0x');

      END LOOP;
   ELSIF iptype = 6 THEN
      -- Short-circuit "::" = 0
      IF ip_string = '::' THEN RETURN LPAD('0', 32, '0'); END IF;

      -- Sanity check
      ipwork := REGEXP_SUBSTR(ip_string, '[[:xdigit:]]{0,4}(\:[[:xdigit:]]{0,4}){0,7}');
      IF ipwork IS NULL THEN RETURN NULL; END IF;

      -- Replace leading zeros
      -- (add a bunch to all of the pairs, then remove only the required ones)
      ipwork := REGEXP_REPLACE(ipwork, '(^|\:)([[:xdigit:]]{1,4})', '\1000\2');
      ipwork := REGEXP_REPLACE(ipwork, '(^|\:)0+([[:xdigit:]]{4})',    '\1\2');

      -- Groups of zeroes
      -- (total length should be 32+Z, so the gap would be the zeroes)
      ipwork := REPLACE(ipwork, '::', 'Z');
      ipwork := REPLACE(ipwork, ':');
      ipwork := REPLACE(ipwork, 'Z', LPAD('0', 33 - LENGTH(ipwork), '0'));
      ip     := LOWER(ipwork);
   ELSE
      RETURN NULL;
   END IF;

   RETURN ip;

END iptohex;

CREATE OR REPLACE FUNCTION nettohex(
   ip_string IN VARCHAR2,
   cidr      IN NATURALN,
   is_end    IN SIGNTYPE DEFAULT 0
) RETURN CHAR
DETERMINISTIC
IS
   iptype   NATURAL  := ipguess(ip_string);
   iphex    CHAR(32) := iptohex(ip_string);
   iphalf1  CHAR(16) := SUBSTR(iphex, 1, 16);
   iphalf2  CHAR(16) := SUBSTR(iphex, 17);
   ipwork   CHAR(16) := iphalf2;
   cidr_exp INTEGER  := 2 ** (iptype + 1) - cidr;
   ipint    INTEGER;
   subnet   INTEGER;
   is_big   SIGNTYPE := 0;
BEGIN
   -- Sanity checks
   IF    iptype IS NULL THEN RETURN NULL;
   ELSIF iphex  IS NULL THEN RETURN NULL;
   END IF;

   IF    cidr_exp >= 64  THEN is_big := 1;
   ELSIF cidr_exp = 0    THEN RETURN iphex;  -- the exact IP, such as /32 on IPv4
   ELSIF cidr_exp <  0   THEN RETURN NULL;
   ELSIF cidr_exp >  128 THEN RETURN NULL;
   END IF;

   -- Change some variables around if we are working with the first/largest half
   IF is_big = 1 THEN
      ipwork   := iphalf1;
      iphalf2  := TO_CHAR((2 ** 64 - 1) * is_end, 'FM0xxxxxxxxxxxxxxx');  -- either all 0 or all F
      cidr_exp := cidr_exp - 64;
   END IF;

   -- Normalize IP to divisions of CIDR
   subnet := 2 ** cidr_exp;
   ipint  := TO_NUMBER(ipwork, 'FM0xxxxxxxxxxxxxxx');
   -- if is_end = 1 then add one net range (then subtract one IP) to get the ending range
   ipwork := TO_CHAR(FLOOR(ipint / subnet + is_end) * subnet - is_end, 'FM0xxxxxxxxxxxxxxx');

   -- Re-integrate
   IF is_big = 0 THEN iphalf2 := ipwork;
   ELSE               iphalf1 := ipwork;
   END IF;

   RETURN SUBSTR(iphalf1 || iphalf2, 1, 32);

END nettohex;

-- WHERE clause:
-- 1. BETWEEN compare:
--    iptohex(a.ip_addy) BETWEEN nettohex(b.net_addy, b.cidr, 0) AND nettohex(b.net_addy, b.cidr, 1)
--
--    Requires three function-based indexes, but all of them would work, as they are all inside the tables.
--
-- 2. CIDR match:
--    nettohex(a.ip_addy, b.cidr) = nettohex(b.net_addy, b.cidr)
--
--    Only two functions and uses exact match, but first one requires an outside variable.  Last one would be only function-based index.
--    An FBI of iptohex(a.ip_addy) could be implemented, but it's questionable if nettohex would use that index.
--
-- Recommended FBIs:
--
-- (SUBSTR(iptohex(a.ip_addy), 1, 32))
-- (SUBSTR(nettohex(b.ip_addy, b.cidr, 0), 1, 32), SUBSTR(nettohex(b.ip_addy, b.cidr, 1), 1, 32))
--
-- NOTE: Will need to use the SUBSTR form for the above WHERE clauses!

UPDATE: Oracle 11g does allow for the SUBSTR entry to be put a virtual column. So, you could have columns like this:

ip              VARCHAR2(39),
cidr            NUMBER(2),
ip_hex          AS (SUBSTR(iptohex(ip),           1, 32)) VIRTUAL,
ip_nethex_start AS (SUBSTR(nettohex(ip, cidr, 0), 1, 32)) VIRTUAL,
ip_nethex_end   AS (SUBSTR(nettohex(ip, cidr, 1), 1, 32)) VIRTUAL,

And indexes like:

CREATE INDEX foobar_iphex_idx ON foobar (ip_hex);
CREATE INDEX foobar_ipnet_idx ON foobar (ip_nethex_start, ip_nethex_end);

Using WHERE clauses like:

a.ip_hex BETWEEN b.ip_nethex_start AND b.ip_nethex_end
nettohex(a.ip, b.cidr) = b.ip_nethex_start  -- not as effective
Oakland answered 4/8, 2011 at 23:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.