分享一个自建的高精度且注重隐私的IP地理位置API查询接口。可以在抱抱脸空间一键私有化部署!可以实现Cloudflare workers优雅且安全的反代GitHub!

最近我在抱抱脸空间(Hugging Face Space)搭建了一个 IP 查询接口,使用 Docker 来部署。以下是项目的主要文件和使用案例。

API 地址

你可以通过以下链接访问我搭建的 API:

https://ipgeo-api.hf.space

一键复制抱抱脸空间

如果你需要私有化部署,请复制以下地址以便快速使用:

功能说明

这个接口可以查询 IP 地址的相关信息,并可以实现 Cloudflare Worker 的安全反向代理,防止被 Netcraft 检测为钓鱼网站导致封号。

接口特点

  • 高精度查询:IPv6 地址可以精确到区县级别,移动的大部分IPv4地址也可以。
  • 实时更新:使用开源的 MaxMind 数据库,数据每天检查更新,确保数据的准确性。

当然,以下是接口返回结果的格式示例,单独列出:

接口返回结果格式

接口返回的结果为 JSON 格式,示例:

{"ip":"2409:8a00:1:0:0:0:0:1a2b","as":{"number":56048,"name":"China Mobile Communicaitons Corporation","info":"中国移动"},"addr":"2409:8a00::/37","country":{"code":"CN","name":"中国"},"registered_country":{"code":"CN","name":"中国"},"regions":["北京市","东城区"],"regions_short":["北京","东城区"],"type":"宽带"}

字段示列

字段 示例
ip 2409:8a00:1:0:0:0:0:1a2b
as.number 56048
as.name China Mobile Communications Corporation
as.info 中国移动
addr 2409:8a00::/37
country.code CN
country.name 中国
registered_country.code CN
registered_country.name 中国
regions 北京市, 东城区
regions_short 北京, 东城区
type 宽带

你可以把这个接口拿去开发你的应用程序。

使用方法

直接访问接口可以获取到你当前的 IPv4 地址。请注意,由于抱抱脸空间的限制,接口只能获取到 IPv4 地址。如果需要获取 IPv6 地址,请自行寻找其他方式。查询时使用格式为:

https://ipgeo-api.hf.space/{ip}

使用案例:用于Cloudflare workers反向代理鉴权

以下是一个示例脚本,能够调用该接口实现根据来访ip归属地进行鉴权,实现优雅且安全的反代GitHub防止被 Netcraft 检测为钓鱼网站导致封号

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  
  const clientIP = request.headers.get('cf-connecting-ip');
  const ipInfoResponse = await fetch(`https://ipgeo-api.hf.space/${clientIP}`); //
  const ipInfo = await ipInfoResponse.json();
  
  const regions = ipInfo.regions || [];
  if (!(regions.includes('北京市') || regions.includes('上海市') || regions.includes('陕西省'))) {
    return new Response('404', { status: 404 });
  return new Response(htmlContent, { status: 404, headers: { 'Content-Type': 'text/html' } });
  }
  
  const url = new URL(request.url)
  url.hostname = 'github.com'

  const modifiedRequest = new Request(url, request)
  return fetch(modifiedRequest)
}

示例中是只允许北京市、上海市和陕西省的IP访问。你需要把自己访问的归属地添加进去,不是这个归属地的直接返回403,注意你可以根据自己的使用情况来更改代码中允许访问的IP属地,对于IPV4精确度大部分只能精确到省,但是移动的IPV4能精确到区县,而IPV6精确度可以精确到区县,使用时你要根据自己的实际情况来进行修改

下列是部署这个api接口需要添加的几个主要文件,如果不想复制就可以自己手动创建以下文件来进行创建。

1. Dockerfile

FROM python:3.9 as py-builder
WORKDIR /code

# 拷贝依赖文件并安装依赖
COPY ./requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.9-slim
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 安装必要的系统依赖
RUN apt-get update && apt-get install -y curl procps && rm -rf /var/lib/apt/lists/*

WORKDIR /code

# 拷贝依赖和应用代码
COPY --from=py-builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=py-builder /usr/local/bin /usr/local/bin

COPY ./main.py .
COPY ./update_and_restart.sh .

# 设置所有文件和目录的权限为 777
RUN chmod -R 777 /code

# 确保启动脚本可执行
RUN chmod +x /code/update_and_restart.sh

# 容器启动命令
CMD ["sh", "/code/update_and_restart.sh"]

2. README.md

---
title: API
emoji: 📊
colorFrom: green
colorTo: indigo
sdk: docker
pinned: false
app_port: 8080
---

Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference

3. main.py

import ipaddress
import maxminddb
from fastapi import FastAPI, Request

city_reader = maxminddb.open_database('GeoLite2-City.mmdb')
asn_reader = maxminddb.open_database('GeoLite2-ASN.mmdb')
cn_reader = maxminddb.open_database('GeoCN.mmdb')
lang = ["zh-CN","en"]
asn_map = {
    9812:"东方有线",
    9389:"中国长城",
    17962:"天威视讯",
    17429:"歌华有线",
    7497:"科技网",
    24139:"华数",
    9801:"中关村",
    4538:"教育网",
    24151:"CNNIC",
    
    38019:"中国移动",139080:"中国移动",9808:"中国移动",24400:"中国移动",134810:"中国移动",24547:"中国移动",
    56040:"中国移动",56041:"中国移动",56042:"中国移动",56044:"中国移动",132525:"中国移动",56046:"中国移动",
    56047:"中国移动",56048:"中国移动",59257:"中国移动",24444:"中国移动",
    24445:"中国移动",137872:"中国移动",9231:"中国移动",58453:"中国移动",
    
    4134:"中国电信",4812:"中国电信",23724:"中国电信",136188:"中国电信",137693:"中国电信",17638:"中国电信",
    140553:"中国电信",4847:"中国电信",140061:"中国电信",136195:"中国电信",17799:"中国电信",139018:"中国电信",
    133776:"中国电信",58772:"中国电信",146966:"中国电信",63527:"中国电信",58539:"中国电信",58540:"中国电信",
    141998:"中国电信",138169:"中国电信",139203:"中国电信",58563:"中国电信",137690:"中国电信",63838:"中国电信",
    137694:"中国电信",137698:"中国电信",136167:"中国电信",148969:"中国电信",134764:"中国电信",
    134770:"中国电信",148981:"中国电信",134774:"中国电信",136190:"中国电信",140647:"中国电信",
    132225:"中国电信",140485:"中国电信",4811:"中国电信",131285:"中国电信",137689:"中国电信",
    137692:"中国电信",140636:"中国电信",140638:"中国电信",140345:"中国电信",38283:"中国电信",
    140292:"中国电信",140903:"中国电信",17897:"中国电信",134762:"中国电信",139019:"中国电信",
    141739:"中国电信",141771:"中国电信",134419:"中国电信",140276:"中国电信",58542:"中国电信",
    140278:"中国电信",139767:"中国电信",137688:"中国电信",137691:"中国电信",4809:"中国电信",
    58466:"中国电信",137687:"中国电信",134756:"中国电信",134760:"中国电信",
    133774:"中国电信",133775:"中国电信",4816:"中国电信",134768:"中国电信",
    58461:"中国电信",58519:"中国电信",58520:"中国电信",131325:"中国电信",

    4837:"中国联通",4808:"中国联通",134542:"中国联通",134543:"中国联通",10099:"中国联通",
    140979:"中国联通",138421:"中国联通",17621:"中国联通",17622:"中国联通",17816:"中国联通",
    140726:"中国联通",17623:"中国联通",136958:"中国联通",9929:"中国联通",58519:"中国联通",
    140716:"中国联通",4847:"中国联通",136959:"中国联通",135061:"中国联通",139007:"中国联通",

    59019:"金山云",
    135377:"优刻云",
    45062:"网易云",
    137718:"火山引擎",
    37963:"阿里云",45102:"阿里云国际",
    45090:"腾讯云",132203:"腾讯云国际",
    55967:"百度云",38365:"百度云",
    58519:"华为云", 55990:"华为云",136907:"华为云",
    4609:"澳門電訊",
    134773:"珠江宽频",
    1659:"台湾教育网",
    8075:"微软云",
    17421:"中华电信",
    3462:"HiNet",
    13335:"Cloudflare",
    55960:"亚马逊云",14618:"亚马逊云",16509:"亚马逊云",
    15169:"谷歌云",396982:"谷歌云",36492:"谷歌云",
}

def get_as_info(number):
    r = asn_map.get(number)
    if r:
        return r
    
def get_des(d):
    for i in lang:
        if i in d['names']:
            return d['names'][i]
    return d['names']['en']

def get_country(d):
    r = get_des(d)
    if r in ["香港", "澳门", "台湾"]:
        return "中国" + r
    return r

def province_match(s):
    arr=['内蒙古','黑龙江','河北','山西','吉林','辽宁','江苏','浙江','安徽','福建','江西','山东','河南','湖北','湖南','广东','海南','四川','贵州','云南','陕西','甘肃','青海','广西','西藏','宁夏','新疆','北京','天津','上海','重庆']
    for i in arr:
        if i in s:
            return i
    return ''

def de_duplicate(regions):
    regions = filter(bool,regions)
    ret = []
    [ret.append(i) for i in regions if i not in ret]
    return ret

def get_addr(ip, mask):
    network = ipaddress.ip_network(f"{ip}/{mask}", strict=False)
    first_ip = network.network_address
    return f"{first_ip}/{mask}"

def get_maxmind(ip: str):
    ret = {"ip":ip}
    asn_info = asn_reader.get(ip)
    if asn_info:
        as_ = {"number":asn_info["autonomous_system_number"],"name":asn_info["autonomous_system_organization"]}
        info = get_as_info(as_["number"])
        if info:
            as_["info"] = info
        ret["as"] = as_

    city_info, prefix = city_reader.get_with_prefix_len(ip)
    ret["addr"] = get_addr(ip, prefix)
    if not city_info:
        return ret
    if "country" in city_info:
        country_code = city_info["country"]["iso_code"]
        country_name = get_country(city_info["country"])
        ret["country"] = {"code":country_code,"name":country_name}
    
    if "registered_country" in city_info:
        registered_country_code = city_info["registered_country"]["iso_code"]
        ret["registered_country"] = {"code":registered_country_code,"name":get_country(city_info["registered_country"])}
        
    regions = [get_des(i) for i in city_info.get('subdivisions', [])]

    
    if "city" in city_info:
        c = get_des(city_info["city"])
        if (not regions or c not in regions[-1])and c not in country_name:
            regions.append(c)
            
    regions = de_duplicate(regions)
    if regions:
        ret["regions"] = regions
    
    return ret

def get_cn(ip:str, info={}):
    ret, prefix = cn_reader.get_with_prefix_len(ip)
    if not ret:
        return
    info["addr"] = get_addr(ip, prefix)
    regions = de_duplicate([ret["province"],ret["city"],ret["districts"]])
    if regions:
        info["regions"] = regions
        info["regions_short"] = de_duplicate([province_match(ret["province"]),ret["city"].replace('市',''),ret["districts"]])
    if "as" not in info:
        info["as"] = {}
    info["as"]["info"] = ret['isp']
    if ret['net']:
        info["type"] = ret['net']
    return ret

def get_ip_info(ip):
    info = get_maxmind(ip)
    if "country" in info and info["country"]["code"] == "CN" and ("registered_country" not in info or info["registered_country"]["code"] == "CN"):
        get_cn(ip,info)
    return info

def query():
    while True:
        try:
            ip = input('IP:   \t').strip()
            info = get_ip_info(ip)
                
            print(f"网段:\t{info['addr']}")
                
            if "as" in info:
                print(f"ISP:\t",end=' ')
                if "info" in info["as"]:
                    print(info["as"]["info"],end=' ')
                else:
                    print(info["as"]["name"],end=' ')
                if "type" in info:
                    print(f"({info['type']})",end=' ')
                print(f"ASN{info['as']['number']}",end=' ')
                print(info['as']["name"])
                
            if "registered_country" in info and ("country" not in info or info["country"]["code"] != info["registered_country"]["code"]):
                print(f"注册地:\t{info['registered_country']['name']}")
                
            if "country" in info:
                print(f"使用地:\t{info['country']['name']}")
                
            if "regions" in info:
                print(f"位置:    \t{' '.join(info['regions'])}")
                
        except Exception as e:
            print(e)
            raise e
        finally:
            print("\n")
            
app = FastAPI()

@app.get("/")
def api(request: Request, ip: str = None):
    if not ip:
        ip = request.headers.get("x-forwarded-for") or request.headers.get("x-real-ip") or request.client.host
    return get_ip_info(ip.strip())

@app.get("/{ip}")
def path_api(ip):
    return get_ip_info(ip)

if __name__ == '__main__':
    query()
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080, server_header=False, proxy_headers=True)

4. requirements.txt

maxminddb
fastapi
uvicorn

5. update_and_restart.sh

#!/bin/sh
while true;
do
    date
    echo "updating GeoLite2-City.mmdb..."
    curl  -L -o "GeoLite2-City.mmdb" "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb"
    echo "updating GeoLite2-ASN.mmdb..."
    curl  -L -o "GeoLite2-ASN.mmdb" "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-ASN.mmdb"
    echo "updating GeoCN.mmdb..."
    curl  -L -o "GeoCN.mmdb" "http://github.com/ljxi/GeoCN/releases/download/Latest/GeoCN.mmdb"

    echo "restarting uvicorn..."
    pkill -f "uvicorn"
    nohup uvicorn main:app --host 0.0.0.0 --port 8080 --no-server-header --proxy-headers &
    sleep 86400;
done

最后提醒

本API接口特别注重隐私的保护,使用的是开源的离线数据库,每天都是全量更新,查询时不依赖任何外部服务,并且日志不会记录你的真实IP,取而代之的是内网的IP,不会记录任何的查询结果,不信的话你可以自己复制一下我的抱抱脸空间查询一下日志就知道了。

贴一张运行日志的截图

再附上一个API状态监控页面

欢迎大家提出建议和反馈!也可以在评论区分享你自己部署的API哦!

118 个赞

感谢分享大佬厉害啊

5 个赞

这个开代理后能拿到源IP吗

3 个赞

你可以点进这个网址看看。

4 个赞

不太行,拿到的ip还是代理后的,源ip好像在请求头x-forward最前面

3 个赞

开代理不就是为了保护你的源ip吗?要是能获取到你的源ip那你的代理有什么作用呢?我这个接口搭配Cloudflare才会用的爽,上面有一个案例就是worker反代GitHub鉴权,你你可以看看。

4 个赞

我拿到了源ip :tieba_087:

1 个赞

你是不是代理没有正常开启呀!

3 个赞

看了看日志,分流没做好,这个域名走到直连去了,分流改到代理就拿不到源ip

1 个赞

唉我去了 我现在用的是纯真免费版的 很难用 然后 我就找了一个别人部署的 花了少量钱 没想到你还有这么高级的项目

喜欢的话,就自行复制搭建一个吧!反正都是免费的服务器不薅白不薅。

这个还挺准啊 我自己搭建的 惨不忍睹


同一个IP 我的查不到 你的定位准确

103.36.165.67

每天都更新当然精准,试试ipv6更恐怖!可惜由于抱抱脸服务器不支持ipv6,所以就没有办法自动获取你的ipv6,但是CF workers可以获取你的IPV4和IPV6地址,通过https://ipgeo-api.hf.space/{ip} 这种格式传入就可以查询。

1 个赞

mark一下!大佬牛逼

我先自己部署一下吧,免得接口有限制,我每天请求大约2~3万次

4 个赞

你可以注册10个账号,每个账号部署一个,然后轮询。虽然我用下来好像也没有见到抱抱脸空间有什么限制的,不过我在这边提醒一下啊,抱抱脸空间有时候会莫名其妙的杠机,你最好也在其他平台上部署一个作为备用,以防抱抱脸空间故障影响你的正常使用。

5 个赞

感谢大佬的分享

这么好的喂饭教程!感谢分享! :+1:

1 个赞

不明觉厉,感谢分享!

1 个赞

学习了 :call_me_hand: :call_me_hand: