Определить страну из IP - IPv6 - PullRequest
1 голос
/ 06 декабря 2011

В моем проекте у меня есть функция в postgres (plpgsql), которая определяет страну по данному IP-адресу:

CREATE OR REPLACE FUNCTION get_country_for_ip(character varying)
  RETURNS character varying AS
$BODY$
declare
    ip  ALIAS for $1;
    ccode   varchar;
    cparts  varchar[];
    nparts  bigint[];
    addr    bigint;
begin
    cparts := string_to_array(ip, '.');
    if array_upper(cparts, 1) <> 4 then
        raise exception 'gcfi01: Invalid IP address: %', ip;
    end if;
    nparts := array[a2i(cparts[1])::bigint, a2i(cparts[2])::bigint, a2i(cparts[3])::bigint, a2i(cparts[4])::bigint];
    if(nparts[1] is null or nparts[1] < 0 or nparts[1] > 255 or
       nparts[2] is null or nparts[2] < 0 or nparts[2] > 255 or
       nparts[3] is null or nparts[3] < 0 or nparts[3] > 255 or
       nparts[4] is null or nparts[4] < 0 or nparts[4] > 255) then
        raise exception 'gcfi02: Invalid IP address: %', ip;
    end if;

    addr := (nparts[1] << 24) | (nparts[2] << 16) | (nparts[3] << 8) | nparts[4];
    addr := nparts[1] * 256 * 65536 + nparts[2] * 65536 + nparts[3] * 256 + nparts[4];

    select into ccode t_country_code from ip_to_country where addr between n_from and n_to limit 1;
    if ccode is null then
        ccode := '';
    end if;
    return ccode;
end;$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;

Это может быть не самым эффективным, но это делает работу. Обратите внимание, что он использует внутреннюю таблицу (ip_to_country), которая содержит данные, как показано ниже (числа n_from и n_to являются значениями long начала и конца диапазона адресов:

  n_from  |   n_to   | t_country_code 
----------+----------+----------------
        0 | 16777215 | ZZ
 16777216 | 16777471 | AU
...

Теперь мы начинаем изучать и адресацию IPv6, и мне нужно добавить аналогичные функции для адресов IPv6. У меня есть аналогичный набор данных для IPv6, который выглядит следующим образом:

 t_start     | t_end                                   | t_country_code
-------------+-----------------------------------------+----------------
 ::          | ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff   | ZZ
 100::       | 1ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff  | ZZ
...
 2000::      | 2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff | ZZ
...
 2001:1200:: | 2001:1200:ffff:ffff:ffff:ffff:ffff:ffff | MX
...

Теперь, учитывая IP-адрес ::1, как (1) проверить, является ли он действительным адресом IPv6, и (2) получить сопоставление соответствующей страны?

Ответы [ 2 ]

1 голос
/ 21 марта 2013

Мне кажется, я нашел решение. Это включает в себя изменение данных, а затем некоторый массаж ввода. Вот что сработало.

Во-первых, необходимо преобразовать данные так, чтобы все адреса были полными, без сокращения, с удалением разделителей точки с запятой. Образцы данных, показанные в моем вопросе, преобразуются в:

 t_start                          | t_end                            | t_country_code
----------------------------------+----------------------------------+----------------
 00000000000000000000000000000000 | 00ffffffffffffffffffffffffffffff | ZZ
 01000000000000000000000000000000 | 01ffffffffffffffffffffffffffffff | ZZ
...
 20000000000000000000000000000000 | 2000ffffffffffffffffffffffffffff | ZZ
...
 20011200000000000000000000000000 | 20011200ffffffffffffffffffffffff | MX
...

Это то, что хранится в базе данных.

Следующим шагом было преобразование IP-адреса, полученного в коде, в тот же формат. Это делается в PHP с помощью следующего кода (предположим, что $ip_address является входящим адресом IPv6):

$addr_bin = inet_pton($ip_address);                                                                                                              
$bytes = unpack('n*', $addr_bin);
$ip_address = implode('', array_map(function ($b) {return sprintf("%04x", $b); }, $bytes));

Теперь переменная $ip_adress будет содержать полный адрес IPv6, например

:: => 00000000000000000000000000000000
2001:1200::ab => 200112000000000000000000000000ab

и т. Д.

Теперь вы можете просто сравнить этот полный адрес с диапазонами в базе данных. Я добавил в базу данных вторую функцию для работы с адресами IPv6, которая выглядит следующим образом:

CREATE OR REPLACE FUNCTION get_country_for_ipv6(character varying)
  RETURNS character varying AS
$BODY$
declare
    ip  ALIAS for $1;
    ccode   varchar;
begin
    select into ccode t_country_code from ipv6_to_country where addr between n_from and n_to limit 1;
    if ccode is null then
        ccode := '';
    end if;
    return ccode;
end;$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;

Наконец, в своем php-коде я добавил код, который вызывает ту или иную функцию Postgres на основе ввода ip_address.

0 голосов
/ 21 марта 2013

Во-первых, я вижу пару вещей, которые вы делаете, которые создадут проблемы.Во-первых, это использование varchar и long для представления IP-адресов, когда PostgreSQL имеет совершенно корректные типы INET и CIDR, которые будут выполнять то, что вы хотите, только лучше и быстрее.Обратите внимание, что в настоящее время они не поддерживают индексацию GIN должным образом, поэтому вы не можете исключить ограничения для них.Если вам это нужно, посмотрите на расширение ip4r, которое поддерживает это.

Обратите внимание, что в качестве патча сейчас вы можете привести свой varchar к inet.Inet также поддерживает адреса ipv4 и ipv6, как и cidr, и аналогичные типы существуют в ip4r.

Это решит проблему проверки ipv6 и, вероятно, сократит объем вашего хранилища, а также обеспечит более эффективные проверки илучшая производительность.

Что касается стран, я также думаю, что сопоставления могут быть не такими прямыми.

...