Determine country from IP - IPv6

北战南征 提交于 2019-12-11 12:26:28

问题


In my project, I have a function in postgres (plpgsql) that determines country from a given ip address:

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;

This may not be the most efficient, but it does the job. Note that it uses an internal table (ip_to_country), which contains data as below (the numbers n_from and n_to are the long values of the start and end of address ranges:

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

Now we are starting to look at the IPv6 addressing as well - and I need to add similar functionality for IPv6 addresses. I have a similar set of data for IPv6, which looks like this:

 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
...

Now, given an IP address ::1, how do I (1) check that it's a valid IPv6 address and (2) get the corresponding country mapping?


回答1:


I believe I found the solution. It involves modifying the data first and then some massaging of the input. Here's what worked.

First, the data needs to be converted so that all addresses are full, without shortening, with semicolon separators removed. The sample data shown in my question is converted to:

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

This is what is stored in the database.

The next step was to convert the IP address received in the code to be in the same format. This is done in PHP with the following code (assume that $ip_address is the incoming IPv6 address):

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

Now variable $ip_adress wil contain the full IPv6 address, for example

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

and so on.

Now you can simply compare this full address with the ranges in the database. I added a second function to the database to deal with IPv6 addresses, which looks like this:

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;

Finally, in my php code I added the code that calls one or the other Postgres function based on the input ip_address.




回答2:


First, I see a couple things you are doing that will pose problems. The first is this use of varchar and long to represent IP addresses when PostgreSQL has perfectly valid INET and CIDR types that will do what you want only better and faster. Note these do not support GIN indexing properly at present so you can't do exclude constraints on them. If you need that, look at the ip4r extension which does support this.

Note as a patch for now you can cast your varchar to inet. Inet also supports both ipv4 and ipv6 addresses as does cidr, and similar types exist on ip4r.

This will solve the ipv6 validation issue for you, and likely cut down on your storage as well as provide better operational checks and better performance.

As for countries, I am also thinking that the mappings may not be so straight-forward.



来源:https://stackoverflow.com/questions/8404357/determine-country-from-ip-ipv6

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!