├── .github └── workflows │ └── update.yml ├── LICENSE ├── README.md ├── go.mod ├── iploc.go ├── iploc_be.go ├── iploc_le.go ├── iploc_test.go ├── ipv4.txt ├── ipv4be.bin ├── ipv4le.bin ├── ipv6.txt ├── ipv6be.gz ├── ipv6le.gz └── update-db.py /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update 2 | 3 | on: 4 | schedule: 5 | - cron: '30 11 * * *' 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: '1.22.1' 14 | - name: Update 15 | run: | 16 | git clone https://${GEOIP_TOKEN}@github.com/${GITHUB_REPOSITORY} 17 | cd iploc 18 | python3 update-db.py 19 | go test -v 20 | git config user.name "github-actions[bot]" 21 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 22 | if git commit -m "update db" -a; then 23 | tag=$(date -u +v1.0.%Y%m%d) 24 | sed -i -E "s#const Version = .+#const Version = \"$tag\"#g" iploc.go 25 | git commit --amend -a --no-edit 26 | git tag ${tag} 27 | git push origin master 28 | git push origin --tags 29 | fi 30 | true 31 | env: 32 | GEOIP_TOKEN: ${{ secrets.GEOIP_TOKEN }} 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Phus Lu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iploc - fastest ip country library 2 | 3 | [![godoc][godoc-img]][godoc] [![release][release-img]][release] [![goreport][goreport-img]][goreport] 4 | 5 | ### Getting Started 6 | 7 | try on https://play.golang.org/p/T_7jfSr0cE1 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "net/netip" 14 | "github.com/phuslu/iploc" 15 | ) 16 | 17 | func main() { 18 | fmt.Printf("%s", iploc.IPCountry(netip.MustParseAddr("1.1.1.1")) 19 | } 20 | 21 | // Output: US 22 | ``` 23 | 24 | ### Benchmarks 25 | ``` 26 | goos: windows 27 | goarch: amd64 28 | pkg: github.com/phuslu/iploc 29 | cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz 30 | BenchmarkIPCountryForIPv4 31 | BenchmarkIPCountryForIPv4-8 80750439 13.57 ns/op 0 B/op 0 allocs/op 32 | BenchmarkIPCountryForIPv6 33 | BenchmarkIPCountryForIPv6-8 57166812 20.44 ns/op 0 B/op 0 allocs/op 34 | PASS 35 | ok github.com/phuslu/iploc 2.360s 36 | ``` 37 | 38 | ### Acknowledgment 39 | This site or product includes IP2Location LITE data available from http://www.ip2location.com. 40 | 41 | ### How often are the inlined ip data updated? 42 | Following IP2Location Lite Database, usually **monthly**. 43 | 44 | [godoc-img]: http://img.shields.io/badge/godoc-reference-blue.svg 45 | [godoc]: https://godoc.org/github.com/phuslu/iploc 46 | [release-img]: https://img.shields.io/github/v/tag/phuslu/iploc?label=release 47 | [release]: https://github.com/phuslu/iploc/releases 48 | [goreport-img]: https://goreportcard.com/badge/github.com/phuslu/iploc 49 | [goreport]: https://goreportcard.com/report/github.com/phuslu/iploc 50 | [coverage-img]: http://gocover.io/_badge/github.com/phuslu/iploc 51 | [coverage]: https://gocover.io/github.com/phuslu/iploc 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phuslu/iploc 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /iploc.go: -------------------------------------------------------------------------------- 1 | // Package iploc provides fastest IP to Country library for Go. 2 | // 3 | // package main 4 | // 5 | // import ( 6 | // "fmt" 7 | // "net/netip" 8 | // "github.com/phuslu/iploc" 9 | // ) 10 | // 11 | // func main() { 12 | // fmt.Printf("%s", iploc.IPCountry(netip.MustParseAddr("1.1.1.1")) 13 | // } 14 | // 15 | // // Output: US 16 | package iploc 17 | 18 | import ( 19 | "bytes" 20 | "compress/gzip" 21 | _ "embed" // for ip data 22 | "io" 23 | "net" 24 | "net/netip" 25 | "os" 26 | "reflect" 27 | "sync" 28 | "unsafe" 29 | ) 30 | 31 | // Version is iplocation database version. 32 | const Version = "v1.0.20250601" 33 | 34 | //go:embed ipv4.txt 35 | var ip4txt []byte 36 | 37 | //go:embed ipv6.txt 38 | var ip6txt []byte 39 | 40 | var ipv6once sync.Once 41 | var ipv4only bool 42 | 43 | // IPCountry return ISO 3166-1 alpha-2 country code of IP. 44 | func IPCountry(ip netip.Addr) (country string) { 45 | if ip.Is4() { 46 | b := ip.As4() 47 | n := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) 48 | i, j := 0, len(ip4bin)/4 49 | for i < j { 50 | h := (i + j) >> 1 51 | if *(*uint32)(unsafe.Add(unsafe.Pointer(&ip4bin[0]), uintptr(h*4))) > n { 52 | j = h 53 | } else { 54 | i = h + 1 55 | } 56 | } 57 | // country = ip4txt[i*2-2 : i*2] 58 | sh := (*reflect.StringHeader)(unsafe.Pointer(&country)) 59 | sh.Data = uintptr(unsafe.Add(unsafe.Pointer(&ip4txt[0]), uintptr(i*2-2))) 60 | sh.Len = 2 61 | return 62 | } 63 | 64 | ipv6once.Do(func() { 65 | ipv4only = os.Getenv("IPLOC_IPV4ONLY") != "" 66 | if !ipv4only { 67 | r, _ := gzip.NewReader(bytes.NewReader(ip6bin)) 68 | ip6bin, _ = io.ReadAll(r) 69 | } 70 | }) 71 | 72 | if ipv4only { 73 | return 74 | } 75 | 76 | b := ip.As16() 77 | high := uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 | uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7]) 78 | low := uint64(b[8])<<56 | uint64(b[9])<<48 | uint64(b[10])<<40 | uint64(b[11])<<32 | uint64(b[12])<<24 | uint64(b[13])<<16 | uint64(b[14])<<8 | uint64(b[15]) 79 | i, j := 0, len(ip6bin)/8 80 | for i < j { 81 | h := (i + j) >> 1 & ^1 82 | n := *(*uint64)(unsafe.Add(unsafe.Pointer(&ip6bin[0]), uintptr(h*8))) 83 | if n > high || (n == high && *(*uint64)(unsafe.Add(unsafe.Pointer(&ip6bin[0]), uintptr((h+1)*8))) > low) { 84 | j = h 85 | } else { 86 | i = h + 2 87 | } 88 | } 89 | // country = ip6txt[i-2 : i] 90 | sh := (*reflect.StringHeader)(unsafe.Pointer(&country)) 91 | sh.Data = uintptr(unsafe.Add(unsafe.Pointer(&ip6txt[0]), uintptr(i-2))) 92 | sh.Len = 2 93 | return 94 | } 95 | 96 | // Country return ISO 3166-1 alpha-2 country code of IP. 97 | func Country(ip net.IP) (country string) { 98 | if addr, ok := netip.AddrFromSlice(ip); ok { 99 | return IPCountry(addr) 100 | } 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /iploc_be.go: -------------------------------------------------------------------------------- 1 | //go:build ppc64 || mips || mips64 || mips64p32 2 | // +build ppc64 mips mips64 mips64p32 3 | 4 | package iploc 5 | 6 | import ( 7 | _ "embed" // for ip data 8 | ) 9 | 10 | //go:embed ipv4be.bin 11 | var ip4bin []byte 12 | 13 | //go:embed ipv6be.gz 14 | var ip6bin []byte 15 | -------------------------------------------------------------------------------- /iploc_le.go: -------------------------------------------------------------------------------- 1 | //go:build 386 || amd64 || arm || amd64p32 || arm64 || ppc64le || mipsle || mips64le || mips64p32le || riscv64 || loong64 2 | // +build 386 amd64 arm amd64p32 arm64 ppc64le mipsle mips64le mips64p32le riscv64 loong64 3 | 4 | package iploc 5 | 6 | import ( 7 | _ "embed" // for ip data 8 | ) 9 | 10 | //go:embed ipv4le.bin 11 | var ip4bin []byte 12 | 13 | //go:embed ipv6le.gz 14 | var ip6bin []byte 15 | -------------------------------------------------------------------------------- /iploc_test.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "testing" 7 | ) 8 | 9 | func TestCountry(t *testing.T) { 10 | cases := []struct { 11 | IP string 12 | Country string 13 | }{ 14 | {"", ""}, 15 | {"0.0.0.0", "ZZ"}, 16 | {"0.1.1.1", "ZZ"}, 17 | // {"1.1.1.1", "AU"}, 18 | {"121.229.143.64", "CN"}, 19 | {"122.96.43.186", "CN"}, 20 | {"153.3.123.160", "CN"}, 21 | {"153.3.131.201", "CN"}, 22 | {"180.109.81.198", "CN"}, 23 | {"180.111.103.88", "CN"}, 24 | {"183.206.11.225", "CN"}, 25 | {"192.210.171.249", "US"}, 26 | {"223.112.9.2", "CN"}, 27 | {"23.16.28.232", "CA"}, 28 | {"58.240.115.210", "CN"}, 29 | {"61.155.4.66", "CN"}, 30 | {"255.255.255.255", "ZZ"}, 31 | // {"2001:4860:4860::8888", "US"}, 32 | {"2001::6ca0:a535", "ZZ"}, 33 | {"2001:dc7:1000::1", "CN"}, 34 | {"2400:3200::1", "CN"}, 35 | {"2400:da00::6666", "CN"}, 36 | {"2404:6800:4008:801::2004", "TW"}, 37 | {"2404:6800:4012:1::200e", "AU"}, 38 | {"240C::6666", "CN"}, 39 | {"240e:4c:4008::1", "CN"}, 40 | {"240e:e8:f089:4877:70d2:775c:91d1:ab12", "CN"}, 41 | {"2620:0:2d0:200::7", "US"}, 42 | {"2a04:4e42:600::223", "NL"}, 43 | {"::", "ZZ"}, 44 | {"::1", "ZZ"}, 45 | } 46 | 47 | for _, c := range cases { 48 | country := Country(net.ParseIP(c.IP)) 49 | if country != c.Country { 50 | t.Errorf("Country(%#v) return \"%s\", expect %#v", c.IP, country, c.Country) 51 | } 52 | } 53 | } 54 | 55 | func TestIPCountry(t *testing.T) { 56 | cases := []struct { 57 | IP string 58 | Country string 59 | }{ 60 | {"0.0.0.0", "ZZ"}, 61 | {"0.1.1.1", "ZZ"}, 62 | // {"1.1.1.1", "AU"}, 63 | {"121.229.143.64", "CN"}, 64 | {"122.96.43.186", "CN"}, 65 | {"153.3.123.160", "CN"}, 66 | {"153.3.131.201", "CN"}, 67 | {"180.109.81.198", "CN"}, 68 | {"180.111.103.88", "CN"}, 69 | {"183.206.11.225", "CN"}, 70 | {"192.210.171.249", "US"}, 71 | {"223.112.9.2", "CN"}, 72 | {"23.16.28.232", "CA"}, 73 | {"58.240.115.210", "CN"}, 74 | {"61.155.4.66", "CN"}, 75 | {"255.255.255.255", "ZZ"}, 76 | // {"2001:4860:4860::8888", "US"}, 77 | {"2001::6ca0:a535", "ZZ"}, 78 | {"2001:dc7:1000::1", "CN"}, 79 | {"2400:3200::1", "CN"}, 80 | {"2400:da00::6666", "CN"}, 81 | {"2404:6800:4008:801::2004", "TW"}, 82 | {"2404:6800:4012:1::200e", "AU"}, 83 | {"240C::6666", "CN"}, 84 | {"240e:4c:4008::1", "CN"}, 85 | {"240e:e8:f089:4877:70d2:775c:91d1:ab12", "CN"}, 86 | {"2620:0:2d0:200::7", "US"}, 87 | {"2a04:4e42:600::223", "NL"}, 88 | {"::", "ZZ"}, 89 | {"::1", "ZZ"}, 90 | } 91 | 92 | for _, c := range cases { 93 | country := IPCountry(netip.MustParseAddr(c.IP)) 94 | // println(c.IP, country) 95 | if country != c.Country { 96 | t.Errorf("Country(%#v) return \"%s\", expect %#v", c.IP, country, c.Country) 97 | } 98 | } 99 | } 100 | 101 | func BenchmarkIPCountryForIPv4(b *testing.B) { 102 | ip := netip.MustParseAddr("9.8.8.8") 103 | 104 | b.ReportAllocs() 105 | b.ResetTimer() 106 | for i := 0; i < b.N; i++ { 107 | IPCountry(ip) 108 | } 109 | } 110 | 111 | func BenchmarkIPCountryForIPv6(b *testing.B) { 112 | ip := netip.MustParseAddr("2001:4860:4860::8888") 113 | 114 | b.ReportAllocs() 115 | b.ResetTimer() 116 | for i := 0; i < b.N; i++ { 117 | IPCountry(ip) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ipv4be.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuslu/iploc/92fc3d93ab59d6a5ff3175e7e03a876a3ad484d5/ipv4be.bin -------------------------------------------------------------------------------- /ipv4le.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuslu/iploc/92fc3d93ab59d6a5ff3175e7e03a876a3ad484d5/ipv4le.bin -------------------------------------------------------------------------------- /ipv6be.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuslu/iploc/92fc3d93ab59d6a5ff3175e7e03a876a3ad484d5/ipv6be.gz -------------------------------------------------------------------------------- /ipv6le.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuslu/iploc/92fc3d93ab59d6a5ff3175e7e03a876a3ad484d5/ipv6le.gz -------------------------------------------------------------------------------- /update-db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=too-many-statements, line-too-long, W0703 3 | 4 | import gzip 5 | import io 6 | import struct 7 | import urllib.request 8 | import zipfile 9 | 10 | 11 | def get(url: str) -> (list, list): 12 | """extract country list and ip list from ip2loc url""" 13 | zfile = zipfile.ZipFile(io.BytesIO(urllib.request.urlopen(url).read())) 14 | text = zfile.read(next(x.filename for x in zfile.filelist if x.filename.endswith('.CSV'))) 15 | iplist, geo = [], b'' 16 | for line in io.BytesIO(text): 17 | parts = line.strip().decode().split(',') 18 | ip = parts[0].strip('"') 19 | country = parts[2].strip('"') 20 | if country == '-': 21 | country = 'ZZ' 22 | iplist.append(ip) 23 | geo += country.encode() 24 | return iplist, geo 25 | 26 | 27 | def gen_ip4_data() -> bytes: 28 | """generate ipv4 to binary data""" 29 | iplist, geo = get('https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.CSV.ZIP') 30 | pack = lambda e, ip: struct.pack(e+'I', ip) 31 | with open('ipv4.txt', 'wb') as file: 32 | file.write(geo) 33 | with open('ipv4le.bin', 'wb') as file: 34 | file.write(b''.join(pack('<', int(ip)) for ip in iplist)) 35 | with open('ipv4be.bin', 'wb') as file: 36 | file.write(b''.join(pack('>', int(ip)) for ip in iplist)) 37 | 38 | 39 | def gen_ip6_data() -> bytes: 40 | """generate ipv6 to binary data""" 41 | iplist, geo = get('https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.IPV6.CSV.ZIP') 42 | pack = lambda e, ip: struct.pack(e+'Q', ip >> 64) + struct.pack(e+'Q', ip & 0xFFFFFFFFFFFFFFFF) 43 | with open('ipv6.txt', 'wb') as file: 44 | file.write(geo) 45 | with gzip.GzipFile('ipv6le.gz', 'wb', mtime=1) as file: 46 | file.write(b''.join(pack('<', int(ip)) for ip in iplist)) 47 | with gzip.GzipFile('ipv6be.gz', 'wb', mtime=1) as file: 48 | file.write(b''.join(pack('>', int(ip)) for ip in iplist)) 49 | 50 | 51 | def main(): 52 | """convert ip2location country csv to binary data""" 53 | gen_ip4_data() 54 | gen_ip6_data() 55 | 56 | 57 | if __name__ == '__main__': 58 | main() 59 | --------------------------------------------------------------------------------