家宽通常只有动态公网 IP 地址。为了能访问家庭内部网络,我们需要通过 DDNS1 技术将变化的公网 IP 地址自动映射到某个固定的域名上。如果 DDNS 工具运行在边缘路由器上,它可以直接获得公网 IP 地址;如果 DDNS 工具是运行在局域网内的一台主机上,获取当前的出口公网 IP 就会麻烦一些。最简单的办法就是通过 curl 访问类似 ip.network 这类可以返回调用方公网 IP 地址的服务。我一直用 ip.network 确认自己的公网 IP。但是今天收到告警邮件,发现我被 ip.network 给封锁了,可能是嫌我每五分钟调一次太多了😂所以我赶紧基于 Nginx 和 MaxMind 提供的免费 IP 地址库搭了一套简易的 IP 查询工具。今天把方案分享给大家。
大家可以直接访问 ip.zz.ac 查看自己的公网 IP 地址。如果想进一步查看 IP 所属的国家和 ASN,则需要访问 ip.zz.ac/more。最完整的 JSON 信息可以通过 ip.zz.ac/json 来查看。如果要查询其他 IP 地址,可以直接通过查询参数指定,比如curl -L ip.zz.ac?ip=1.1.1.1。
✏️Note如果用 curl 访问,请添加
-L参数来自动处理 301 跳转curl -L ip.zz.ac或者指定走 HTTP 协议curl https://ip.zz.ac。
先实现返回当前客户端的公网 IP 地址。该功能非常简单,只需要 Nginx 就能做到。
简单来说就是配置一个特殊的 web 站点,通过return指令和$remote_addr变量来返回客户端的 IP 地址:
server {
listen 443;
server_name ip.zz.ac;
ssl_certificate /etc/nginx/ssl/ip.zz.ac/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/ip.zz.ac/key.pem;
location = / {
return 200 '$remote_addr\n';
}
}
如果你的 Nginx 直接监听公网 IP 地址,有上面这段站点配置就足够了。加载配置后访问 ip.zz.ac就会返回自己的公网 IP 地址。
💡Tip主流操作系统会优先使用 IPv6 通信。如果你的设备支持 IPv6 网络,上面的配置就会返回 IPv6 公网地址。如果返回 IPv4 地址直,需要给 curl 添加
-4参数来强制走 IPv4 通信。为明确起见,我们最好给 curl 指定-6或者-4来显示要求走哪种网络。
如果你的 Nginx 没有直接监听公网 IP 地址,例如是通过另外一个 LoadBalancer 来转发请求的,那么$remote_addr保存的就是 LB 到你 Nginx 通信所用的 IP 地址,而并非客户端的公网 IP 地址。此时你需要通过set_real_ip_from和real_ip_header来更正当前 Nginx 的$remote_addr变量。具体操作请自行AI。
到这一步对 DDNS 来说已经足够了。但如果对外提供服务,还是太单薄了些。最好是在功能上尽量贴近 ip.network。所以我希望 ip.zz.ac 能提供较为丰富的 IP 信息。但优质的信息需要付费。公益服务显然无法承担商业版 IP 地址信息库,于是我想到了 MaxMind 提供的免费数据库 GeoLite2。
GeoLite2 数据库免费提供 IP 地址到国家、城市和运营商的映射信息。与付费版相比,它的数据精度略差,有些数据的字段不完整,有些字段不提供。但总体来说还是比较准确的,尤其是到国家一级,基本没有太大问题。MaxMind 不仅免费提供,而且定期更新,还要什么自行车🚴
但免费归免费,要想获得最新的 GeoLite2 数据,你还需要到 MaxMind 注册账号2,之后才能通过 LicenseKey 来下载数据。
在 ubuntu 下我样可以通过 geoipupdate 来自动更新 GeoLite2 数据。它的配置文件路径为/etc/GeoIP.conf,内容就三行:
AccountID 1000
LicenseKey XXXX
EditionIDs GeoLite2-Country GeoLite2-City GeoLite2-ASN
前两项登录 MaxMind 后台获取,最后一项指定需要同步的数据库。我这里同时下载了国家、城市和 ASN 三个数据库。全都要!配置好之后开启定时更新服务:
systemctl enable --now geoipupdate.service更新的数据目录为/var/lib/GeoIP/,里面有三个对应的mmdb文件。
接下来我们将 GeoLite2 集成到 Nginx 中。
Nginx 官方提供 ngx_http_geoip_module 模块。然而,该模块只能加载老式的 GeoIP 数据,并不支持 GeoLite2🤦♂️Nginx 官方虽然提供 GeoIP23 但仅限 Plus 用户。于是乎便有了开源的 ngx_http_geoip2_module 模块4。
如果你是用 Ubuntu/Debian 源里的 Nginx,可以直接安装 libnginx-mod-http-geoip2 包。但我用的是 nginx.org 源里的 Nginx 包,只能自己手工编译了。
编译动态模块需要同时获得 Nginx 源码和模块源码:
git clone https://github.com/leev/ngx_http_geoip2_module
wget http://nginx.org/download/nginx-VERSION.tar.gz
tar zxvf nginx-VERSION.tar.gz
cd nginx-VERSION
./configure --with-compat --add-dynamic-module=../ngx_http_geoip2_module
make modules编译后的 so 文件位置是objs/ngx_http_geoip2_module.so把它复制到 Nginx 的动态模块目录后再指定加载就可以了。加载指令:
load_module modules/ngx_http_geoip2_module.so;
加载之后就可以配置读取各 GeoLite2 数据:
http {
# 国家信息
geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
auto_reload 2h;
# 国家代码,如 CN
$geoip2_data_country_code country iso_code;
# 国家名字,如 China
$geoip2_data_country_name country names en;
}
# 城市信息
geoip2 /var/lib/GeoIP/GeoLite2-City.mmdb {
auto_reload 2h;
# 名字
$geoip2_data_city_name city names en;
# 纬度
$geoip2_data_city_latitude location latitude;
# 经度
$geoip2_data_city_longitude location longitude;
}
# 网络信息
geoip2 /var/lib/GeoIP/GeoLite2-ASN.mmdb {
auto_reload 2h;
# ASN 编号
$geoip2_data_asn autonomous_system_number;
# ASN 所属组织名称
$geoip2_data_org autonomous_system_organization;
}
}
以上是通过 geoip2 加载对应数据库文件,并为相关的信息指定变量。我们在后面的站点配置中可以引用这些变量。上面只用了一部分字段,更多字段请参考 MaxMind 的官方文档5。
这是 ip.zz.ac/more 的配置,它额外返回了 ASN 和国家编码:
location /more {
return 200 '$remote_addr AS$geoip2_data_asn $geoip2_data_country_code\n';
}
这是 ip.zz.ac/json 的配置,它用 json 格式返回了更多字段:
location /json {
add_header Content-Type "application/json; charset=utf-8";
add_header Access-Control-Allow-Origin "*";
return 200 '{
"ip": "$remote_addr",
"country": "$geoip2_data_country_code",
"city": "$geoip2_data_city_name",
"latitude": "$geoip2_data_city_latitude",
"longitude": "$geoip2_data_city_longitude",
"asn": "$geoip2_data_asn",
"organization": "$geoip2_data_org"
}\n';
}
大家可以结合文档和 AI 自由定制字段。
到现在为止我只能查询自己的公网 IP 地址。有时候还需要查询指定 IP 地址的相关信息。比如我们希望支持 curl -L ip.zz.ac/json?ip=1.1.1.1 返回 1.1.1.1的信息。这就需要为 ngx_http_geoip2_module 模块指定查询变量。
我们可以使用 map 指定来动态设置查询变量。
http {
map $arg_ip $ip_lookup_addr {
"" $remote_addr;
default $arg_ip;
}
}
这里会自动提取 HTTP 请求中的 ip 查询参数并保存在$ip_lookup_addr变量中。如果没有 ip 参数或者参数为空,则自动使用客户端的公网 IP 地址。
接着让 ngx_http_geoip2_module 在查询的时候使用$ip_lookup_addr的值就可以了。
geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
$geoip2_data_country_code source=$ip_lookup_addr country iso_code;
}
注意这里的source=$ip_lookup_addr参数。
ip.zz.ac 服务已经上线,请放心使用,因为我自己就在用它,出问题会及时修复。另外,为了节约服务器资源,该站点不记录访问日志,大家基本不用担心隐私问题。本文就到这里,希望能对大家有所启发🍻











