【问题标题】:Determine country from IP - IPv6从 IP 确定国家 - IPv6
【发布时间】:2012-01-14 07:37:09
【问题描述】:

在我的项目中,我在 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) 获取相应的国家/地区映射?

【问题讨论】:

  • 您确定 IPv6 将以与 IPv4 类似的方式映射吗?有足够的 IPv6 地址供地球上的所有人口使用,还有一些。如果重新使用 IP 地址,您的映射技术似乎将无效。你的数据源是什么?有一些正则表达式可以帮助您验证 IPv6 是否有效。
  • @Ramhound:我们正在使用来自 Webnet77 (webnet77.com) 的信息,它为我们提供了很好的 IPv4 服务,因此我们从他们的 IPv6 数据库开始。我不太担心 100% 的时间是否正确(目前),但需要从某个地方开始。

标签: validation postgresql ip-address ipv6


【解决方案1】:

我相信我找到了解决方案。它涉及首先修改数据,然后对输入进行一些按摩。这就是有效的方法。

首先,需要对数据进行转换,使所有地址都是完整的,不缩短,删除分号分隔符。我的问题中显示的示例数据转换为:

 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 代码中,我添加了基于输入 ip_address 调用一个或另一个 Postgres 函数的代码。

【讨论】:

    【解决方案2】:

    首先,我看到您正在做的一些事情会带来问题。第一个是当 PostgreSQL 具有完全有效的 INET 和 CIDR 类型时,使用 varchar 和 long 来表示 IP 地址,它们只会更好更快地完成您想要的工作。请注意,这些目前不支持正确的 GIN 索引,因此您不能对它们进行排除约束。如果需要,请查看支持此功能的 ip4r 扩展。

    请注意,现在您可以将 varchar 转换为 inet。 Inet 和 cidr 一样也支持 ipv4 和 ipv6 地址,类似的类型也存在于 ip4r 上。

    这将为您解决 ipv6 验证问题,并可能减少您的存储空间,并提供更好的操作检查和更好的性能。

    至于国家,我也认为映射可能不是那么直截了当。

    【讨论】:

    • 感谢您的回答,但这一点帮助也没有。尽管它需要跨数据库,至少在 postgres 和 mysql 中工作,但它的实现方式是有充分的理由的;细节不同,但数据类型相同。我没有要求对我的实现发表评论,我专门询问了处理 ipv6 的问题——你所说的只是“我认为映射可能不是那么简单”。那不是答案,那是评论。
    • 至少我们解决了这个问题,对吧?但是 pl/pgsql 不能在 MySQL 上工作,所以一旦你走这条路,我认为你真的想使用 inet 类型。另外它们解决了您的验证问题。如果没有别的,您可以简单地从 varchar 转换为类型并运行函数。
    • @Aleks G:如果您需要限制答案以便它们可以工作/轻松转换为 MySQL,那么请在您的问题中这样做。不是在 cmets 中,当然也不是在人们浪费时间试图回答 Postgres 问题之后,而您想要一个跨数据库的解决方案。
    • @ypercube 你似乎误解了我的意思。这里的主要问题是你没有回答问题,你在评论我没有问的事情。
    • @AleksG 你问如何验证 ipv6 地址。我建议为此使用一种类型。怎么不回答?
    猜你喜欢
    • 2012-02-22
    • 1970-01-01
    • 1970-01-01
    • 2013-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-06
    • 1970-01-01
    相关资源
    最近更新 更多