├── internal ├── migration │ ├── mirgration.go │ ├── v7.go │ └── v6.go ├── constant │ ├── version.go │ └── path.go ├── db │ ├── cache.go │ ├── default.go │ ├── db.go │ ├── update.go │ └── type.go └── config │ └── config.go ├── main.go ├── pow ├── pow_test.go └── pow.go ├── Dockerfile ├── .gitignore ├── pkg ├── common │ ├── savefile.go │ ├── scan.go │ └── httpclient.go ├── download │ └── download.go ├── re │ ├── re.go │ └── re_test.go ├── dbif │ └── db.go ├── wry │ ├── parse.go │ ├── index.go │ └── wry.go ├── ipip │ └── ipip.go ├── ip2location │ └── ip2location.go ├── geoip │ └── geoip.go ├── entity │ ├── entity.go │ └── parse.go ├── ip2region │ └── ip2region.go ├── zxipv6wry │ ├── update.go │ └── zxipv6wry.go ├── qqwry │ └── qqwry.go ├── cdn │ └── cdn.go └── leomoeapi │ └── leomoeapi.go ├── .github ├── scripts │ └── upload_s3.sh ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── go.yml │ ├── go-release.yml │ └── build.yml ├── cmd ├── update.go ├── info.go └── root.go ├── LICENSE ├── assets └── GoLand.svg ├── go.mod ├── Makefile ├── README.md ├── README_en.md └── go.sum /internal/migration/mirgration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | func init() { 4 | migration2v6() 5 | migration2v7() 6 | } 7 | -------------------------------------------------------------------------------- /internal/constant/version.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var ( 4 | // Version like 1.0.1 5 | Version = "unknown version" 6 | ) 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/zu1k/nali/internal/constant" 5 | 6 | "github.com/zu1k/nali/cmd" 7 | "github.com/zu1k/nali/internal/config" 8 | 9 | _ "github.com/zu1k/nali/internal/migration" 10 | ) 11 | 12 | func main() { 13 | config.ReadConfig(constant.ConfigDirPath) 14 | cmd.Execute() 15 | } 16 | -------------------------------------------------------------------------------- /pow/pow_test.go: -------------------------------------------------------------------------------- 1 | package pow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestGetToken(t *testing.T) { 10 | token, err := GetToken("45.88.195.154", "origin-fallback.nxtrace.org", "443") 11 | fmt.Println(token, err) 12 | assert.NoError(t, err, "GetToken() returned an error") 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | RUN apk add --no-cache make git 4 | WORKDIR /nali-src 5 | COPY . /nali-src 6 | RUN go mod download && \ 7 | make docker && \ 8 | mv ./bin/nali-docker /nali 9 | 10 | FROM alpine:latest 11 | 12 | RUN apk add --no-cache ca-certificates 13 | COPY --from=builder /nali / 14 | ENTRYPOINT ["/nali"] -------------------------------------------------------------------------------- /internal/db/cache.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/zu1k/nali/pkg/dbif" 7 | ) 8 | 9 | var ( 10 | dbNameCache = make(map[string]dbif.DB) 11 | dbTypeCache = make(map[dbif.QueryType]dbif.DB) 12 | queryCache = sync.Map{} 13 | ) 14 | 15 | var ( 16 | NameDBMap = make(NameMap) 17 | TypeDBMap = make(TypeMap) 18 | ) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # dep 16 | vendor 17 | 18 | # GoLand 19 | .idea/* 20 | .vscode 21 | 22 | # macOS file 23 | .DS_Store 24 | nali -------------------------------------------------------------------------------- /pkg/common/savefile.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func SaveFile(path string, data []byte) (err error) { 10 | // Remove file if exist 11 | _, err = os.Stat(path) 12 | if err == nil { 13 | err = os.Remove(path) 14 | if err != nil { 15 | log.Fatalln("旧文件删除失败", err.Error()) 16 | } 17 | } 18 | 19 | // save file 20 | return ioutil.WriteFile(path, data, 0644) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/download/download.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "errors" 5 | "github.com/zu1k/nali/pkg/common" 6 | ) 7 | 8 | func Download(filePath string, urls ...string) (data []byte, err error) { 9 | if len(urls) == 0 { 10 | return nil, errors.New("未指定下载 url") 11 | } 12 | 13 | data, err = common.GetHttpClient().Get(urls...) 14 | if err != nil { 15 | return 16 | } 17 | 18 | err = common.SaveFile(filePath, data) 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /.github/scripts/upload_s3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | aws configure --profile s3 <<-EOF > /dev/null 2>&1 6 | ${S3_ACCESS_KEY_ID} 7 | ${S3_SECRET_ACCESS_KEY} 8 | auto 9 | text 10 | EOF 11 | 12 | VERSION=$(git describe --tags || echo "unknown version") 13 | aws s3 cp bin s3://$S3_BUCKET/$VERSION/ --profile s3 --no-progress --endpoint-url $S3_ENDPOINT --recursive 14 | 15 | aws configure --profile s3 <<-EOF > /dev/null 2>&1 16 | null 17 | null 18 | null 19 | text 20 | EOF 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 0 8 | 9 | - package-ecosystem: "docker" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 0 14 | 15 | - package-ecosystem: "gomod" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | open-pull-requests-limit: 0 20 | 21 | -------------------------------------------------------------------------------- /pkg/common/scan.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // ScanLines scan lines but keep the suffix \r and \n 8 | func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 9 | if atEOF && len(data) == 0 { 10 | return 0, nil, nil 11 | } 12 | 13 | if i := bytes.IndexByte(data, '\n'); i >= 0 { 14 | return i + 1, data[:i+1], nil 15 | } 16 | if i := bytes.IndexByte(data, '\r'); i >= 0 { 17 | return i + 1, data[:i+1], nil 18 | } 19 | 20 | // If we're at EOF, we have a final, non-terminated line. Return it. 21 | if atEOF { 22 | return len(data), data, nil 23 | } 24 | // Request more data. 25 | return 0, nil, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/re/re.go: -------------------------------------------------------------------------------- 1 | package re 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | DomainRe = regexp.MustCompile(`([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z][-a-zA-Z]{0,62})`) 10 | 11 | IPv4Re = regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`) 12 | IPv6Re = regexp.MustCompile(`fe80:(:[0-9a-fA-F]{1,4}){0,4}(%\w+)?|([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::[fF]{4}:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?`) 13 | ) 14 | 15 | func MaybeRegexp(s string) bool { 16 | return strings.ContainsAny(s, "[]{}()?") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/dbif/db.go: -------------------------------------------------------------------------------- 1 | package dbif 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zu1k/nali/pkg/cdn" 7 | "github.com/zu1k/nali/pkg/geoip" 8 | "github.com/zu1k/nali/pkg/ip2location" 9 | "github.com/zu1k/nali/pkg/ip2region" 10 | "github.com/zu1k/nali/pkg/ipip" 11 | "github.com/zu1k/nali/pkg/qqwry" 12 | "github.com/zu1k/nali/pkg/zxipv6wry" 13 | ) 14 | 15 | type QueryType uint 16 | 17 | const ( 18 | TypeIPv4 = iota 19 | TypeIPv6 20 | TypeDomain 21 | ) 22 | 23 | type DB interface { 24 | Find(query string, params ...string) (result fmt.Stringer, err error) 25 | } 26 | 27 | var ( 28 | _ DB = &qqwry.QQwry{} 29 | _ DB = &zxipv6wry.ZXwry{} 30 | _ DB = &ipip.IPIPFree{} 31 | _ DB = &geoip.GeoIP{} 32 | _ DB = &ip2region.Ip2Region{} 33 | _ DB = &ip2location.IP2Location{} 34 | _ DB = &cdn.CDN{} 35 | ) 36 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/zu1k/nali/internal/db" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // updateCmd represents the update command 12 | var updateCmd = &cobra.Command{ 13 | Use: "update", 14 | Short: "update qqwry, zxipv6wry, ip2region ip database and cdn", 15 | Long: `update qqwry, zxipv6wry, ip2region ip database and cdn. Use commas to separate`, 16 | Example: "nali update --db qqwry,cdn", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | DBs, _ := cmd.Flags().GetString("db") 19 | var DBNameArray []string 20 | if DBs != "" { 21 | DBNameArray = strings.Split(DBs, ",") 22 | } 23 | db.UpdateDB(DBNameArray...) 24 | }, 25 | } 26 | 27 | func init() { 28 | updateCmd.PersistentFlags().String("db", "", "choose db you want to update") 29 | rootCmd.AddCommand(updateCmd) 30 | } 31 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zu1k/nali/internal/constant" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // infoCmd represents the info command 13 | var infoCmd = &cobra.Command{ 14 | Use: "info", 15 | Short: "get the necessary information of nali", 16 | Long: `get the necessary information of nali`, 17 | Example: "nali info", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Println("Nali Version: ", constant.Version) 20 | fmt.Println("Config Dir Path: ", constant.ConfigDirPath) 21 | fmt.Println("DB Data Dir Path: ", constant.DataDirPath) 22 | 23 | fmt.Println("Selected IPv4 DB: ", viper.GetString("selected.ipv4")) 24 | fmt.Println("Selected IPv6 DB: ", viper.GetString("selected.ipv6")) 25 | fmt.Println("Selected CDN DB: ", viper.GetString("selected.cdn")) 26 | }, 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(infoCmd) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "13 12 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ go ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /pkg/wry/parse.go: -------------------------------------------------------------------------------- 1 | package wry 2 | 3 | const ( 4 | // RedirectMode1 [IP][0x01][国家和地区信息的绝对偏移地址] 5 | RedirectMode1 = 0x01 6 | // RedirectMode2 [IP][0x02][信息的绝对偏移][...] or [IP][国家][...] 7 | RedirectMode2 = 0x02 8 | ) 9 | 10 | func (r *Reader) Parse(offset uint32) { 11 | if offset != 0 { 12 | r.seekAbs(offset) 13 | } 14 | 15 | switch r.readMode() { 16 | case RedirectMode1: 17 | r.readOffset(true) 18 | r.Parse(0) 19 | case RedirectMode2: 20 | r.Result.Country = r.parseRedMode2() 21 | r.Result.Area = r.readArea() 22 | default: 23 | r.seekBack() 24 | r.Result.Country = r.readString(true) 25 | r.Result.Area = r.readArea() 26 | } 27 | } 28 | 29 | func (r *Reader) parseRedMode2() string { 30 | r.readOffset(true) 31 | str := r.readString(false) 32 | r.seekBack() 33 | return str 34 | } 35 | 36 | func (r *Reader) readArea() string { 37 | mode := r.readMode() 38 | if mode == RedirectMode1 || mode == RedirectMode2 { 39 | offset := r.readOffset(true) 40 | if offset == 0 { 41 | return "" 42 | } 43 | } else { 44 | r.seekBack() 45 | } 46 | return r.readString(false) 47 | } 48 | -------------------------------------------------------------------------------- /pow/pow.go: -------------------------------------------------------------------------------- 1 | package pow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tsosunchia/powclient" 6 | "github.com/zu1k/nali/internal/constant" 7 | "net/url" 8 | "os" 9 | "runtime" 10 | ) 11 | 12 | const ( 13 | baseURL = "/v3/challenge" 14 | ) 15 | 16 | var UserAgent = fmt.Sprintf("Nali-NextTrace %s/%s/%s", constant.Version, runtime.GOOS, runtime.GOARCH) 17 | 18 | func GetToken(fastIp string, host string, port string) (string, error) { 19 | getTokenParams := powclient.NewGetTokenParams() 20 | u := url.URL{Scheme: "https", Host: fastIp + ":" + port, Path: baseURL} 21 | getTokenParams.BaseUrl = u.String() 22 | getTokenParams.SNI = host 23 | getTokenParams.Host = host 24 | getTokenParams.UserAgent = UserAgent 25 | var err error 26 | // 尝试三次RetToken,如果都失败了,异常退出 27 | for i := 0; i < 3; i++ { 28 | token, err := powclient.RetToken(getTokenParams) 29 | //fmt.Println(token, err) 30 | if err != nil { 31 | continue 32 | } 33 | return token, nil 34 | } 35 | if err != nil { 36 | fmt.Println(err) 37 | } 38 | fmt.Println("RetToken failed 3 times, exit") 39 | os.Exit(1) 40 | return "", nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/re/re_test.go: -------------------------------------------------------------------------------- 1 | package re 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var domainList = []string{ 9 | "a.a.qiniudns.com", 10 | "a.com.qiniudns.com", 11 | "a.com.cn.qiniudns.com", 12 | "看这里:a.com.cn.qiniudns.com行不行", 13 | } 14 | 15 | func TestDomainRe(t *testing.T) { 16 | for _, domain := range domainList { 17 | if !DomainRe.MatchString(domain) { 18 | t.Error(domain) 19 | t.Fail() 20 | } 21 | fmt.Println(DomainRe.FindAllString(domain, -1)) 22 | } 23 | } 24 | 25 | var validIPv6List = []string{ 26 | "::ffff:104.26.11.119", 27 | } 28 | 29 | func TestIPv6Re(t *testing.T) { 30 | for _, ip := range validIPv6List { 31 | if !IPv6Re.MatchString(ip) { 32 | t.Error(ip) 33 | t.Fail() 34 | } 35 | fmt.Println(IPv6Re.FindAllString(ip, -1)) 36 | } 37 | } 38 | 39 | var maybeRegexList = []string{ 40 | "[a-z]*\\.example.com", 41 | "kunlun[^.]+.com", 42 | "gtm-a[1-7]b[1-9].com", 43 | } 44 | 45 | func TestMaybeRegexp(t *testing.T) { 46 | if MaybeRegexp("abc.com") { 47 | t.Fail() 48 | } 49 | 50 | for _, str := range maybeRegexList { 51 | if !MaybeRegexp(str) { 52 | t.Fail() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | "github.com/zu1k/nali/internal/db" 8 | ) 9 | 10 | func ReadConfig(basePath string) { 11 | viper.SetDefault("databases", db.GetDefaultDBList()) 12 | viper.SetDefault("selected.ipv4", "qqwry") 13 | viper.SetDefault("selected.ipv6", "zxipv6wry") 14 | viper.SetDefault("selected.cdn", "cdn") 15 | viper.SetDefault("selected.lang", "zh-CN") 16 | 17 | viper.SetConfigName("config") 18 | viper.SetConfigType("yaml") 19 | viper.AddConfigPath(basePath) 20 | err := viper.ReadInConfig() 21 | if err != nil { 22 | err = viper.SafeWriteConfig() 23 | if err != nil { 24 | panic(err) 25 | } 26 | } 27 | 28 | _ = viper.BindEnv("selected.ipv4", "NALI_DB_IP4") 29 | _ = viper.BindEnv("selected.ipv6", "NALI_DB_IP6") 30 | _ = viper.BindEnv("selected.cdn", "NALI_DB_CDN") 31 | _ = viper.BindEnv("selected.lang", "NALI_LANG") 32 | 33 | dbList := db.List{} 34 | err = viper.UnmarshalKey("databases", &dbList) 35 | if err != nil { 36 | log.Fatalln("Config invalid:", err) 37 | } 38 | 39 | db.NameDBMap.From(dbList) 40 | db.TypeDBMap.From(dbList) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020-2022 zu1k i@zu1k.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/constant/path.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/adrg/xdg" 9 | ) 10 | 11 | var ( 12 | ConfigDirPath string 13 | DataDirPath string 14 | ) 15 | 16 | func init() { 17 | if naliHome := os.Getenv("NALI_HOME"); len(naliHome) != 0 { 18 | ConfigDirPath = naliHome 19 | DataDirPath = naliHome 20 | } else { 21 | ConfigDirPath = os.Getenv("NALI_CONFIG_HOME") 22 | if len(ConfigDirPath) == 0 { 23 | ConfigDirPath = filepath.Join(xdg.ConfigHome, "nali") 24 | } 25 | 26 | DataDirPath = os.Getenv("NALI_DB_HOME") 27 | if len(DataDirPath) == 0 { 28 | DataDirPath = filepath.Join(xdg.DataHome, "nali") 29 | } 30 | } 31 | 32 | prepareDir(ConfigDirPath) 33 | prepareDir(DataDirPath) 34 | 35 | _ = os.Chdir(DataDirPath) 36 | } 37 | 38 | func prepareDir(dir string) { 39 | stat, err := os.Stat(dir) 40 | if err != nil && os.IsNotExist(err) { 41 | if err := os.MkdirAll(dir, 0755); err != nil { 42 | log.Fatal("can not create config dir:", dir) 43 | } 44 | } else { 45 | if !stat.IsDir() { 46 | log.Fatal("path already exists, but not a dir:", dir) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/migration/v7.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/zu1k/nali/internal/constant" 9 | "github.com/zu1k/nali/internal/db" 10 | "github.com/zu1k/nali/pkg/qqwry" 11 | ) 12 | 13 | func migration2v7() { 14 | viper.SetConfigName("config") 15 | viper.SetConfigType("yaml") 16 | viper.AddConfigPath(constant.ConfigDirPath) 17 | 18 | err := viper.ReadInConfig() 19 | if err != nil { 20 | return 21 | } 22 | 23 | dbList := db.List{} 24 | err = viper.UnmarshalKey("databases", &dbList) 25 | if err != nil { 26 | log.Fatalln("Config invalid:", err) 27 | } 28 | 29 | needOverwrite := false 30 | for _, adb := range dbList { 31 | if adb.Name == "qqwry" { 32 | if len(adb.DownloadUrls) == 0 || 33 | adb.DownloadUrls[0] == "https://99wry.cf/qqwry.dat" || 34 | strings.Contains(adb.DownloadUrls[0], "sspanel-uim") { 35 | needOverwrite = true 36 | adb.DownloadUrls = qqwry.DownloadUrls 37 | } 38 | } 39 | } 40 | 41 | if needOverwrite { 42 | viper.Set("databases", dbList) 43 | err = viper.WriteConfig() 44 | if err != nil { 45 | log.Println(err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/ipip/ipip.go: -------------------------------------------------------------------------------- 1 | package ipip 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/ipipdotnet/ipdb-go" 9 | ) 10 | 11 | type IPIPFree struct { 12 | *ipdb.City 13 | } 14 | 15 | func NewIPIP(filePath string) (*IPIPFree, error) { 16 | _, err := os.Stat(filePath) 17 | if err != nil && os.IsNotExist(err) { 18 | log.Printf("IPIP数据库不存在,请手动下载解压后保存到本地: %s \n", filePath) 19 | log.Println("下载链接: https://www.ipip.net/product/ip.html") 20 | return nil, err 21 | } else { 22 | db, err := ipdb.NewCity(filePath) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &IPIPFree{City: db}, nil 27 | } 28 | } 29 | 30 | type Result struct { 31 | Country string 32 | Region string 33 | City string 34 | } 35 | 36 | func (r Result) String() string { 37 | if r.City == "" { 38 | return fmt.Sprintf("%s %s", r.Country, r.Region) 39 | } 40 | return fmt.Sprintf("%s %s %s", r.Country, r.Region, r.City) 41 | } 42 | 43 | func (db IPIPFree) Find(query string, params ...string) (result fmt.Stringer, err error) { 44 | info, err := db.FindInfo(query, "CN") 45 | if err != nil || info == nil { 46 | return nil, err 47 | } else { 48 | // info contains more info 49 | result = Result{ 50 | Country: info.CountryName, 51 | Region: info.RegionName, 52 | City: info.CityName, 53 | } 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/ip2location/ip2location.go: -------------------------------------------------------------------------------- 1 | package ip2location 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | 10 | "github.com/ip2location/ip2location-go/v9" 11 | ) 12 | 13 | // IP2Location 14 | type IP2Location struct { 15 | db *ip2location.DB 16 | } 17 | 18 | // new IP2Location from database file 19 | func NewIP2Location(filePath string) (*IP2Location, error) { 20 | _, err := os.Stat(filePath) 21 | if err != nil && os.IsNotExist(err) { 22 | log.Println("文件不存在,请自行下载 IP2Location 库,并保存在", filePath) 23 | return nil, err 24 | } else { 25 | db, err := ip2location.OpenDB(filePath) 26 | 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | return &IP2Location{db: db}, nil 31 | } 32 | } 33 | 34 | func (x IP2Location) Find(query string, params ...string) (result fmt.Stringer, err error) { 35 | ip := net.ParseIP(query) 36 | if ip == nil { 37 | return nil, errors.New("Query should be valid IP") 38 | } 39 | record, err := x.db.Get_all(ip.String()) 40 | 41 | if err != nil { 42 | return 43 | } 44 | 45 | result = Result{ 46 | Country: record.Country_long, 47 | Region: record.Region, 48 | City: record.City, 49 | } 50 | return 51 | } 52 | 53 | type Result struct { 54 | Country string 55 | Region string 56 | City string 57 | } 58 | 59 | func (r Result) String() string { 60 | return fmt.Sprintf("%s %s %s", r.Country, r.Region, r.City) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/geoip/geoip.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | 10 | "github.com/oschwald/geoip2-golang" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // GeoIP2 15 | type GeoIP struct { 16 | db *geoip2.Reader 17 | } 18 | 19 | // new geoip from database file 20 | func NewGeoIP(filePath string) (*GeoIP, error) { 21 | // 判断文件是否存在 22 | _, err := os.Stat(filePath) 23 | if err != nil && os.IsNotExist(err) { 24 | log.Println("文件不存在,请自行下载 Geoip2 City库,并保存在", filePath) 25 | return nil, err 26 | } else { 27 | db, err := geoip2.Open(filePath) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | return &GeoIP{db: db}, nil 32 | } 33 | } 34 | 35 | func (g GeoIP) Find(query string, params ...string) (result fmt.Stringer, err error) { 36 | ip := net.ParseIP(query) 37 | if ip == nil { 38 | return nil, errors.New("Query should be valid IP") 39 | } 40 | record, err := g.db.City(ip) 41 | if err != nil { 42 | return 43 | } 44 | 45 | lang := viper.GetString("selected.lang") 46 | if lang == "" { 47 | lang = "zh-CN" 48 | } 49 | 50 | result = Result{ 51 | Country: record.Country.Names[lang], 52 | City: record.City.Names[lang], 53 | } 54 | return 55 | } 56 | 57 | type Result struct { 58 | Country string 59 | City string 60 | } 61 | 62 | func (r Result) String() string { 63 | if r.City == "" { 64 | return r.Country 65 | } else { 66 | return fmt.Sprintf("%s %s", r.Country, r.City) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/common/httpclient.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | const UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" 11 | 12 | type HttpClient struct { 13 | *http.Client 14 | } 15 | 16 | var httpClient *HttpClient 17 | 18 | func init() { 19 | httpClient = &HttpClient{http.DefaultClient} 20 | httpClient.Timeout = time.Second * 60 21 | httpClient.Transport = &http.Transport{ 22 | TLSHandshakeTimeout: time.Second * 5, 23 | IdleConnTimeout: time.Second * 10, 24 | ResponseHeaderTimeout: time.Second * 10, 25 | ExpectContinueTimeout: time.Second * 20, 26 | Proxy: http.ProxyFromEnvironment, 27 | } 28 | } 29 | 30 | func GetHttpClient() *HttpClient { 31 | c := *httpClient 32 | return &c 33 | } 34 | 35 | func (c *HttpClient) Get(urls ...string) (body []byte, err error) { 36 | var req *http.Request 37 | var resp *http.Response 38 | 39 | for _, url := range urls { 40 | req, err = http.NewRequest(http.MethodGet, url, nil) 41 | if err != nil { 42 | log.Println(err) 43 | continue 44 | } 45 | req.Header.Set("User-Agent", UserAgent) 46 | resp, err = c.Do(req) 47 | 48 | if err == nil && resp != nil && resp.StatusCode == 200 { 49 | defer resp.Body.Close() 50 | body, err = ioutil.ReadAll(resp.Body) 51 | if err != nil { 52 | continue 53 | } 54 | return 55 | } 56 | } 57 | 58 | return nil, err 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | test-build: 6 | name: Test and Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Setup Go 10 | uses: actions/setup-go@v3 11 | with: 12 | go-version: '^1.20' 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v3 15 | - name: Cache go module 16 | uses: actions/cache@v3 17 | with: 18 | path: ~/go/pkg/mod 19 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | ${{ runner.os }}-go- 22 | - name: Get dependencies and run test 23 | run: | 24 | go test ./... 25 | - name: Build 26 | run: go build 27 | 28 | release-aur-git: 29 | name: Release Aur Git 30 | needs: [test-build] 31 | if: github.event_name == 'push' 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Check out code 35 | uses: actions/checkout@v3 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Get version 40 | id: version 41 | run: echo ::set-output name=version::$(git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g') 42 | 43 | - name: Publish AUR package nali-go-git 44 | uses: zu1k/aur-publish-action@master 45 | with: 46 | package_name: nali-go-git 47 | commit_username: 'zu1k' 48 | commit_email: 'i@zu1k.com' 49 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 50 | new_release: ${{ steps.version.outputs.version }} 51 | -------------------------------------------------------------------------------- /pkg/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/fatih/color" 7 | "github.com/zu1k/nali/pkg/dbif" 8 | ) 9 | 10 | type EntityType uint 11 | 12 | const ( 13 | TypeIPv4 = dbif.TypeIPv4 14 | TypeIPv6 = dbif.TypeIPv6 15 | TypeDomain = dbif.TypeDomain 16 | 17 | TypePlain = 100 18 | ) 19 | 20 | type Entity struct { 21 | Loc [2]int // s[Loc[0]:Loc[1]] 22 | Type EntityType 23 | 24 | Text string 25 | Info string 26 | } 27 | 28 | func (e Entity) ParseInfo() error { 29 | return nil 30 | } 31 | 32 | type Entities []*Entity 33 | 34 | func (es Entities) Len() int { 35 | return len(es) 36 | } 37 | 38 | func (es Entities) Less(i, j int) bool { 39 | return es[i].Loc[0] < es[j].Loc[0] 40 | } 41 | 42 | func (es Entities) Swap(i, j int) { 43 | es[i], es[j] = es[j], es[i] 44 | } 45 | 46 | func (es Entities) String() string { 47 | var result strings.Builder 48 | for _, entity := range es { 49 | result.WriteString(entity.Text) 50 | if entity.Type != TypePlain && len(entity.Info) > 0 { 51 | result.WriteString("[" + entity.Info + "] ") 52 | } 53 | } 54 | return result.String() 55 | } 56 | 57 | func (es Entities) ColorString() string { 58 | var line strings.Builder 59 | for _, e := range es { 60 | s := e.Text 61 | switch e.Type { 62 | case TypeIPv4: 63 | s = color.GreenString(e.Text) 64 | case TypeIPv6: 65 | s = color.BlueString(e.Text) 66 | case TypeDomain: 67 | s = color.YellowString(e.Text) 68 | } 69 | if e.Type != TypePlain && len(e.Info) > 0 { 70 | s += " [" + color.RedString(e.Info) + "] " 71 | } 72 | line.WriteString(s) 73 | } 74 | return line.String() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/wry/index.go: -------------------------------------------------------------------------------- 1 | package wry 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | func (db *IPDB[uint32]) SearchIndexV4(ip uint32) uint32 { 8 | ipLen := db.IPLen 9 | entryLen := uint32(db.OffLen + db.IPLen) 10 | 11 | buf := make([]byte, entryLen) 12 | l, r, mid, ipc := db.IdxStart, db.IdxEnd, uint32(0), uint32(0) 13 | 14 | for { 15 | mid = (r-l)/entryLen/2*entryLen + l 16 | buf = db.Data[mid : mid+entryLen] 17 | ipc = uint32(binary.LittleEndian.Uint32(buf[:ipLen])) 18 | 19 | if r-l == entryLen { 20 | if ip >= uint32(binary.LittleEndian.Uint32(db.Data[r:r+uint32(ipLen)])) { 21 | buf = db.Data[r : r+entryLen] 22 | } 23 | return uint32(Bytes3ToUint32(buf[ipLen:entryLen])) 24 | } 25 | 26 | if ipc > ip { 27 | r = mid 28 | } else if ipc < ip { 29 | l = mid 30 | } else if ipc == ip { 31 | return uint32(Bytes3ToUint32(buf[ipLen:entryLen])) 32 | } 33 | } 34 | } 35 | 36 | func (db *IPDB[uint64]) SearchIndexV6(ip uint64) uint32 { 37 | ipLen := db.IPLen 38 | entryLen := uint64(db.OffLen + db.IPLen) 39 | 40 | buf := make([]byte, entryLen) 41 | l, r, mid, ipc := db.IdxStart, db.IdxEnd, uint64(0), uint64(0) 42 | 43 | for { 44 | mid = (r-l)/entryLen/2*entryLen + l 45 | buf = db.Data[mid : mid+entryLen] 46 | ipc = uint64(binary.LittleEndian.Uint64(buf[:ipLen])) 47 | 48 | if r-l == entryLen { 49 | if ip >= uint64(binary.LittleEndian.Uint64(db.Data[r:r+uint64(ipLen)])) { 50 | buf = db.Data[r : r+entryLen] 51 | } 52 | return Bytes3ToUint32(buf[ipLen:entryLen]) 53 | } 54 | 55 | if ipc > ip { 56 | r = mid 57 | } else if ipc < ip { 58 | l = mid 59 | } else if ipc == ip { 60 | return Bytes3ToUint32(buf[ipLen:entryLen]) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/migration/v6.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/google/martian/log" 8 | "github.com/zu1k/nali/internal/constant" 9 | ) 10 | 11 | func migration2v6() { 12 | homeDir, err := os.UserHomeDir() 13 | if err != nil { 14 | return 15 | } 16 | oldDefaultWorkPath := filepath.Join(homeDir, ".nali") 17 | 18 | oldDefaultWorkPath, err = filepath.Abs(oldDefaultWorkPath) 19 | if err != nil { 20 | log.Errorf("Get absolute path for oldDefaultWorkPath failed: %s\n", err) 21 | } 22 | mewWorkPath, err := filepath.Abs(constant.ConfigDirPath) 23 | if err != nil { 24 | log.Errorf("Get absolute path for mewWorkPath failed: %s\n", err) 25 | } 26 | if oldDefaultWorkPath == mewWorkPath { 27 | // User chooses to continue using old directory 28 | return 29 | } 30 | 31 | _, err = os.Stat(oldDefaultWorkPath) 32 | if err == nil { 33 | println("Old data directories are detected and will attempt to migrate automatically") 34 | 35 | oldDefaultConfigPath := filepath.Join(oldDefaultWorkPath, "config.yaml") 36 | stat, err := os.Stat(oldDefaultConfigPath) 37 | if err == nil { 38 | if stat.Mode().IsRegular() { 39 | _ = os.Rename(oldDefaultConfigPath, filepath.Join(constant.ConfigDirPath, "config.yaml")) 40 | } 41 | } 42 | 43 | files, err := os.ReadDir(oldDefaultWorkPath) 44 | if err == nil { 45 | for _, file := range files { 46 | if file.Type().IsRegular() { 47 | _ = os.Rename(filepath.Join(oldDefaultWorkPath, file.Name()), filepath.Join(constant.DataDirPath, file.Name())) 48 | } 49 | } 50 | } 51 | 52 | err = os.RemoveAll(oldDefaultWorkPath) 53 | if err != nil { 54 | log.Errorf("Auto migration failed: %s\n", err) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/ip2region/ip2region.go: -------------------------------------------------------------------------------- 1 | package ip2region 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/zu1k/nali/pkg/download" 12 | "github.com/zu1k/nali/pkg/wry" 13 | 14 | "github.com/lionsoul2014/ip2region/binding/golang/xdb" 15 | ) 16 | 17 | var DownloadUrls = []string{ 18 | "https://cdn.jsdelivr.net/gh/lionsoul2014/ip2region/data/ip2region.xdb", 19 | "https://raw.githubusercontent.com/lionsoul2014/ip2region/master/data/ip2region.xdb", 20 | } 21 | 22 | type Ip2Region struct { 23 | seacher *xdb.Searcher 24 | } 25 | 26 | func NewIp2Region(filePath string) (*Ip2Region, error) { 27 | _, err := os.Stat(filePath) 28 | if err != nil && os.IsNotExist(err) { 29 | log.Println("文件不存在,尝试从网络获取最新 ip2region 库") 30 | _, err = download.Download(filePath, DownloadUrls...) 31 | if err != nil { 32 | return nil, err 33 | } 34 | } 35 | 36 | f, err := os.OpenFile(filePath, os.O_RDONLY, 0400) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer f.Close() 41 | 42 | data, err := ioutil.ReadAll(f) 43 | if err != nil { 44 | return nil, err 45 | } 46 | searcher, err := xdb.NewWithBuffer(data) 47 | if err != nil { 48 | fmt.Printf("无法解析 ip2region xdb 数据库: %s\n", err) 49 | return nil, err 50 | } 51 | return &Ip2Region{ 52 | seacher: searcher, 53 | }, nil 54 | } 55 | 56 | func (db Ip2Region) Find(query string, params ...string) (result fmt.Stringer, err error) { 57 | if db.seacher != nil { 58 | res, err := db.seacher.SearchByStr(query) 59 | if err != nil { 60 | return nil, err 61 | } else { 62 | return wry.Result{ 63 | Country: strings.ReplaceAll(res, "|0", ""), 64 | }, nil 65 | } 66 | } 67 | 68 | return nil, errors.New("ip2region 未初始化") 69 | } 70 | -------------------------------------------------------------------------------- /pkg/entity/parse.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "net/netip" 5 | "sort" 6 | 7 | "github.com/zu1k/nali/internal/db" 8 | "github.com/zu1k/nali/pkg/dbif" 9 | "github.com/zu1k/nali/pkg/re" 10 | ) 11 | 12 | // ParseLine parse a line into entities 13 | func ParseLine(line string) Entities { 14 | ip4sLoc := re.IPv4Re.FindAllStringIndex(line, -1) 15 | ip6sLoc := re.IPv6Re.FindAllStringIndex(line, -1) 16 | domainsLoc := re.DomainRe.FindAllStringIndex(line, -1) 17 | 18 | tmp := make(Entities, 0, len(ip4sLoc)+len(ip6sLoc)+len(domainsLoc)) 19 | for _, e := range ip4sLoc { 20 | tmp = append(tmp, &Entity{ 21 | Loc: *(*[2]int)(e), 22 | Type: TypeIPv4, 23 | Text: line[e[0]:e[1]], 24 | }) 25 | } 26 | for _, e := range ip6sLoc { 27 | text := line[e[0]:e[1]] 28 | if ip, _ := netip.ParseAddr(text); !ip.Is4In6() { 29 | tmp = append(tmp, &Entity{ 30 | Loc: *(*[2]int)(e), 31 | Type: TypeIPv6, 32 | Text: text, 33 | }) 34 | } 35 | } 36 | for _, e := range domainsLoc { 37 | tmp = append(tmp, &Entity{ 38 | Loc: *(*[2]int)(e), 39 | Type: TypeDomain, 40 | Text: line[e[0]:e[1]], 41 | }) 42 | } 43 | 44 | sort.Sort(tmp) 45 | es := make(Entities, 0, len(tmp)) 46 | 47 | idx := 0 48 | for _, e := range tmp { 49 | start := e.Loc[0] 50 | if start >= idx { 51 | if start > idx { 52 | es = append(es, &Entity{ 53 | Loc: [2]int{idx, start}, 54 | Type: TypePlain, 55 | Text: line[idx:start], 56 | }) 57 | } 58 | e.Info = db.Find(dbif.QueryType(e.Type), e.Text) 59 | es = append(es, e) 60 | idx = e.Loc[1] 61 | } 62 | } 63 | if total := len(line); idx < total { 64 | es = append(es, &Entity{ 65 | Loc: [2]int{idx, total}, 66 | Type: TypePlain, 67 | Text: line[idx:total], 68 | }) 69 | } 70 | 71 | return es 72 | } 73 | -------------------------------------------------------------------------------- /assets/GoLand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/db/default.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/zu1k/nali/pkg/cdn" 5 | "github.com/zu1k/nali/pkg/ip2region" 6 | "github.com/zu1k/nali/pkg/qqwry" 7 | ) 8 | 9 | func GetDefaultDBList() List { 10 | return List{ 11 | &DB{ 12 | Name: "qqwry", 13 | NameAlias: []string{ 14 | "chunzhen", 15 | }, 16 | Format: FormatQQWry, 17 | File: "qqwry.dat", 18 | Languages: LanguagesZH, 19 | Types: TypesIPv4, 20 | DownloadUrls: qqwry.DownloadUrls, 21 | }, 22 | &DB{ 23 | Name: "zxipv6wry", 24 | NameAlias: []string{ 25 | "zxipv6", 26 | "zx", 27 | }, 28 | Format: FormatZXIPv6Wry, 29 | File: "zxipv6wry.db", 30 | Languages: LanguagesZH, 31 | Types: TypesIPv6, 32 | }, 33 | &DB{ 34 | Name: "geoip", 35 | NameAlias: []string{ 36 | "geoip2", 37 | "geolite", 38 | "geolite2", 39 | }, 40 | Format: FormatMMDB, 41 | File: "GeoLite2-City.mmdb", 42 | Languages: LanguagesAll, 43 | Types: TypesIP, 44 | }, 45 | &DB{ 46 | Name: "dbip", 47 | NameAlias: []string{ 48 | "db-ip", 49 | }, 50 | Format: FormatMMDB, 51 | File: "dbip.mmdb", 52 | Languages: LanguagesAll, 53 | Types: TypesIP, 54 | }, 55 | &DB{ 56 | Name: "ipip", 57 | Format: FormatIPIP, 58 | File: "ipipfree.ipdb", 59 | Languages: LanguagesZH, 60 | Types: TypesIP, 61 | }, 62 | &DB{ 63 | Name: "ip2region", 64 | NameAlias: []string{ 65 | "i2r", 66 | }, 67 | Format: FormatIP2Region, 68 | File: "ip2region.xdb", 69 | Languages: LanguagesZH, 70 | Types: TypesIPv4, 71 | DownloadUrls: ip2region.DownloadUrls, 72 | }, 73 | &DB{ 74 | Name: "ip2location", 75 | Format: FormatIP2Location, 76 | File: "IP2LOCATION-LITE-DB3.IPV6.BIN", 77 | Languages: LanguagesEN, 78 | Types: TypesIP, 79 | }, 80 | 81 | &DB{ 82 | Name: "cdn", 83 | Format: FormatCDNYml, 84 | File: "cdn.yml", 85 | Languages: LanguagesZH, 86 | Types: TypesCDN, 87 | DownloadUrls: cdn.DownloadUrls, 88 | }, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "github.com/spf13/cobra" 12 | "golang.org/x/text/encoding/simplifiedchinese" 13 | "golang.org/x/text/transform" 14 | 15 | "github.com/zu1k/nali/internal/constant" 16 | "github.com/zu1k/nali/pkg/common" 17 | "github.com/zu1k/nali/pkg/entity" 18 | ) 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "nali", 22 | Short: "An offline tool for querying IP geographic information", 23 | Long: `An offline tool for querying IP geographic information. 24 | 25 | Find document on: https://github.com/zu1k/nali 26 | 27 | #1 Query a simple IP address 28 | 29 | $ nali 1.2.3.4 30 | 31 | or use pipe 32 | 33 | $ echo IP 6.6.6.6 | nali 34 | 35 | #2 Query multiple IP addresses 36 | 37 | $ nali 1.2.3.4 4.3.2.1 123.23.3.0 38 | 39 | #3 Interactive query 40 | 41 | $ nali 42 | 123.23.23.23 43 | 123.23.23.23 [越南 越南邮电集团公司] 44 | quit 45 | 46 | #4 Use with dig 47 | 48 | $ dig nali.zu1k.com +short | nali 49 | 50 | #5 Use with nslookup 51 | 52 | $ nslookup nali.zu1k.com 8.8.8.8 | nali 53 | 54 | #6 Use with any other program 55 | 56 | bash abc.sh | nali 57 | 58 | #7 IPV6 support 59 | `, 60 | Version: constant.Version, 61 | Args: cobra.MinimumNArgs(0), 62 | Run: func(cmd *cobra.Command, args []string) { 63 | gbk, _ := cmd.Flags().GetBool("gbk") 64 | 65 | if len(args) == 0 { 66 | stdin := bufio.NewScanner(os.Stdin) 67 | stdin.Split(common.ScanLines) 68 | for stdin.Scan() { 69 | line := stdin.Text() 70 | if gbk { 71 | line, _, _ = transform.String(simplifiedchinese.GBK.NewDecoder(), line) 72 | } 73 | if line := strings.TrimSpace(line); line == "quit" || line == "exit" { 74 | return 75 | } 76 | _, _ = fmt.Fprintf(color.Output, "%s", entity.ParseLine(line).ColorString()) 77 | } 78 | } else { 79 | _, _ = fmt.Fprintf(color.Output, "%s\n", entity.ParseLine(strings.Join(args, " ")).ColorString()) 80 | } 81 | }, 82 | } 83 | 84 | // Execute parse subcommand and run 85 | func Execute() { 86 | if err := rootCmd.Execute(); err != nil { 87 | log.Fatal(err.Error()) 88 | } 89 | } 90 | 91 | func init() { 92 | rootCmd.Flags().Bool("gbk", false, "Use GBK decoder") 93 | } 94 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/zu1k/nali/pkg/leomoeapi" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/spf13/viper" 11 | 12 | "github.com/zu1k/nali/pkg/cdn" 13 | "github.com/zu1k/nali/pkg/dbif" 14 | "github.com/zu1k/nali/pkg/geoip" 15 | "github.com/zu1k/nali/pkg/qqwry" 16 | "github.com/zu1k/nali/pkg/zxipv6wry" 17 | ) 18 | 19 | func GetDB(typ dbif.QueryType) (db dbif.DB) { 20 | if db, found := dbTypeCache[typ]; found { 21 | return db 22 | } 23 | 24 | lang := viper.GetString("selected.lang") 25 | if lang == "" { 26 | lang = "zh-CN" 27 | } 28 | 29 | var err error 30 | switch typ { 31 | case dbif.TypeIPv4: 32 | selected := viper.GetString("selected.ipv4") 33 | if selected != "" { 34 | db = getDbByName(selected).get() 35 | break 36 | } 37 | 38 | if lang == "zh-CN" { 39 | db, err = qqwry.NewQQwry(getDbByName("qqwry").File) 40 | } else { 41 | db, err = geoip.NewGeoIP(getDbByName("geoip").File) 42 | } 43 | case dbif.TypeIPv6: 44 | selected := viper.GetString("selected.ipv6") 45 | if selected != "" { 46 | db = getDbByName(selected).get() 47 | break 48 | } 49 | 50 | if lang == "zh-CN" { 51 | db, err = zxipv6wry.NewZXwry(getDbByName("zxipv6wry").File) 52 | } else { 53 | db, err = geoip.NewGeoIP(getDbByName("geoip").File) 54 | } 55 | case dbif.TypeDomain: 56 | selected := viper.GetString("selected.cdn") 57 | if selected != "" { 58 | db = getDbByName(selected).get() 59 | break 60 | } 61 | 62 | db, err = cdn.NewCDN(getDbByName("cdn").File) 63 | default: 64 | panic("Query type not supported!") 65 | } 66 | 67 | if err != nil || db == nil { 68 | log.Fatalln("Database init failed:", err) 69 | } 70 | 71 | dbTypeCache[typ] = db 72 | return 73 | } 74 | 75 | func Find(typ dbif.QueryType, query string) string { 76 | if result, found := queryCache.Load(query); found { 77 | return result.(string) 78 | } 79 | var err error 80 | var result fmt.Stringer 81 | nali := os.Getenv("NALI") 82 | if nali == "1" { 83 | result, err = GetDB(typ).Find(query) 84 | } else { 85 | result, err = leomoeapi.Find(query) 86 | } 87 | if err != nil { 88 | return "" 89 | } 90 | r := strings.Trim(result.String(), " ") 91 | queryCache.Store(query, r) 92 | return r 93 | } 94 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zu1k/nali 2 | 3 | go 1.22.6 4 | 5 | require ( 6 | github.com/adrg/xdg v0.5.0 7 | github.com/fatih/color v1.17.0 8 | github.com/google/martian v2.1.0+incompatible 9 | github.com/gorilla/websocket v1.5.3 10 | github.com/ip2location/ip2location-go/v9 v9.7.0 11 | github.com/ipipdotnet/ipdb-go v1.3.3 12 | github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 13 | github.com/nxtrace/NTrace-core v1.3.2 14 | github.com/oschwald/geoip2-golang v1.11.0 15 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda 16 | github.com/spf13/cobra v1.8.1 17 | github.com/spf13/viper v1.19.0 18 | github.com/stretchr/testify v1.9.0 19 | github.com/tsosunchia/powclient v0.1.5 20 | golang.org/x/text v0.17.0 21 | gopkg.in/yaml.v2 v2.4.0 22 | ) 23 | 24 | require ( 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/fsnotify/fsnotify v1.7.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/magiconair/properties v1.8.7 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/mitchellh/mapstructure v1.5.0 // indirect 33 | github.com/oschwald/maxminddb-golang v1.13.1 // indirect 34 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 35 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 36 | github.com/sagikazarmark/locafero v0.6.0 // indirect 37 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 38 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f // indirect 39 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f // indirect 40 | github.com/sourcegraph/conc v0.3.0 // indirect 41 | github.com/spf13/afero v1.11.0 // indirect 42 | github.com/spf13/cast v1.7.0 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/subosito/gotenv v1.6.0 // indirect 45 | github.com/ulikunitz/xz v0.5.12 // indirect 46 | go.uber.org/multierr v1.11.0 // indirect 47 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect 48 | golang.org/x/sys v0.24.0 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | lukechampine.com/uint128 v1.3.0 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /pkg/zxipv6wry/update.go: -------------------------------------------------------------------------------- 1 | package zxipv6wry 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | 10 | "github.com/saracen/go7z" 11 | "github.com/zu1k/nali/pkg/common" 12 | ) 13 | 14 | func Download(filePath ...string) (data []byte, err error) { 15 | data, err = getData() 16 | if err != nil { 17 | log.Printf("ZX IPv6数据库下载失败,请手动下载解压后保存到本地: %s \n", filePath) 18 | log.Println("下载链接: https://ip.zxinc.org/ip.7z") 19 | return 20 | } 21 | 22 | if !CheckFile(data) { 23 | log.Printf("ZX IPv6数据库下载出错,请手动下载解压后保存到本地: %s \n", filePath) 24 | log.Println("下载链接: https://ip.zxinc.org/ip.7z") 25 | return nil, errors.New("数据库下载内容出错") 26 | } 27 | 28 | if len(filePath) == 1 { 29 | if err := common.SaveFile(filePath[0], data); err == nil { 30 | log.Println("已将最新的 ZX IPv6数据库 保存到本地:", filePath) 31 | } 32 | } 33 | return 34 | } 35 | 36 | const ( 37 | zx = "https://ip.zxinc.org/ip.7z" 38 | ) 39 | 40 | func getData() (data []byte, err error) { 41 | data, err = common.GetHttpClient().Get(zx) 42 | 43 | file7z, err := os.CreateTemp("", "*") 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer os.Remove(file7z.Name()) 48 | if err := os.WriteFile(file7z.Name(), data, 0644); err == nil { 49 | return Un7z(file7z.Name()) 50 | } 51 | return 52 | } 53 | 54 | func Un7z(filePath string) (data []byte, err error) { 55 | sz, err := go7z.OpenReader(filePath) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer sz.Close() 60 | 61 | fileNoNeed, err := os.CreateTemp("", "*") 62 | if err != nil { 63 | return nil, err 64 | } 65 | fileNeed, err := os.CreateTemp("", "*") 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | if err != nil { 71 | return nil, err 72 | } 73 | for { 74 | hdr, err := sz.Next() 75 | if err == io.EOF { 76 | break // IdxEnd of archive 77 | } 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if hdr.Name == "ipv6wry.db" { 83 | if _, err := io.Copy(fileNeed, sz); err != nil { 84 | log.Fatalln("ZX ipv6数据库解压出错:", err.Error()) 85 | } 86 | } else { 87 | if _, err := io.Copy(fileNoNeed, sz); err != nil { 88 | log.Fatalln("ZX ipv6数据库解压出错:", err.Error()) 89 | } 90 | } 91 | } 92 | err = fileNoNeed.Close() 93 | if err != nil { 94 | return nil, err 95 | } 96 | defer os.Remove(fileNoNeed.Name()) 97 | defer os.Remove(fileNeed.Name()) 98 | return ioutil.ReadFile(fileNeed.Name()) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/zxipv6wry/zxipv6wry.go: -------------------------------------------------------------------------------- 1 | package zxipv6wry 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | 12 | "github.com/zu1k/nali/pkg/wry" 13 | ) 14 | 15 | type ZXwry struct { 16 | wry.IPDB[uint64] 17 | } 18 | 19 | func NewZXwry(filePath string) (*ZXwry, error) { 20 | var fileData []byte 21 | 22 | _, err := os.Stat(filePath) 23 | if err != nil && os.IsNotExist(err) { 24 | log.Println("文件不存在,尝试从网络获取最新ZX IPv6数据库") 25 | fileData, err = Download(filePath) 26 | if err != nil { 27 | return nil, err 28 | } 29 | } else { 30 | fileBase, err := os.OpenFile(filePath, os.O_RDONLY, 0400) 31 | if err != nil { 32 | return nil, err 33 | } 34 | defer fileBase.Close() 35 | 36 | fileData, err = io.ReadAll(fileBase) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | 42 | if !CheckFile(fileData) { 43 | log.Fatalln("ZX IPv6数据库存在错误,请重新下载") 44 | } 45 | 46 | header := fileData[:24] 47 | offLen := header[6] 48 | ipLen := header[7] 49 | 50 | start := binary.LittleEndian.Uint64(header[16:24]) 51 | counts := binary.LittleEndian.Uint64(header[8:16]) 52 | end := start + counts*11 53 | 54 | return &ZXwry{ 55 | IPDB: wry.IPDB[uint64]{ 56 | Data: fileData, 57 | 58 | OffLen: offLen, 59 | IPLen: ipLen, 60 | IPCnt: counts, 61 | IdxStart: start, 62 | IdxEnd: end, 63 | }, 64 | }, nil 65 | } 66 | 67 | func (db *ZXwry) Find(query string, _ ...string) (result fmt.Stringer, err error) { 68 | ip := net.ParseIP(query) 69 | if ip == nil { 70 | return nil, errors.New("query should be IPv6") 71 | } 72 | ip6 := ip.To16() 73 | if ip6 == nil { 74 | return nil, errors.New("query should be IPv6") 75 | } 76 | ip6 = ip6[:8] 77 | ipu64 := binary.BigEndian.Uint64(ip6) 78 | 79 | offset := db.SearchIndexV6(ipu64) 80 | reader := wry.NewReader(db.Data) 81 | reader.Parse(offset) 82 | return reader.Result, nil 83 | } 84 | 85 | func CheckFile(data []byte) bool { 86 | if len(data) < 4 { 87 | return false 88 | } 89 | if string(data[:4]) != "IPDB" { 90 | return false 91 | } 92 | 93 | if len(data) < 24 { 94 | return false 95 | } 96 | header := data[:24] 97 | start := binary.LittleEndian.Uint64(header[16:24]) 98 | counts := binary.LittleEndian.Uint64(header[8:16]) 99 | end := start + counts*11 100 | if start >= end || uint64(len(data)) < end { 101 | return false 102 | } 103 | 104 | return true 105 | } 106 | -------------------------------------------------------------------------------- /internal/db/update.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strings" 7 | "time" 8 | 9 | "github.com/zu1k/nali/pkg/download" 10 | "github.com/zu1k/nali/pkg/qqwry" 11 | "github.com/zu1k/nali/pkg/zxipv6wry" 12 | ) 13 | 14 | func UpdateDB(dbNames ...string) { 15 | if len(dbNames) == 0 { 16 | dbNames = DbNameListForUpdate 17 | } 18 | 19 | done := make(map[string]struct{}) 20 | for _, dbName := range dbNames { 21 | update, name := getUpdateFuncByName(dbName) 22 | if _, found := done[name]; !found { 23 | done[name] = struct{}{} 24 | if err := update(); err != nil { 25 | continue 26 | } 27 | } 28 | } 29 | } 30 | 31 | var DbNameListForUpdate = []string{ 32 | "qqwry", 33 | "zxipv6wry", 34 | "ip2region", 35 | "cdn", 36 | } 37 | 38 | var DbCheckFunc = map[Format]func([]byte) bool{ 39 | FormatQQWry: qqwry.CheckFile, 40 | FormatZXIPv6Wry: zxipv6wry.CheckFile, 41 | } 42 | 43 | func getUpdateFuncByName(name string) (func() error, string) { 44 | name = strings.TrimSpace(name) 45 | if db := getDbByName(name); db != nil { 46 | // direct download if download-url not null 47 | if len(db.DownloadUrls) > 0 { 48 | return func() error { 49 | log.Printf("正在下载最新 %s 数据库...\n", db.Name) 50 | data, err := download.Download(db.File, db.DownloadUrls...) 51 | if err != nil { 52 | log.Printf("%s 数据库下载失败,请手动下载解压后保存到本地: %s \n", db.Name, db.File) 53 | log.Println("下载链接:", db.DownloadUrls) 54 | log.Println("error:", err) 55 | return err 56 | } else { 57 | if check, ok := DbCheckFunc[db.Format]; ok { 58 | if !check(data) { 59 | log.Printf("%s 数据库下载失败,请手动下载解压后保存到本地: %s \n", db.Name, db.File) 60 | log.Println("下载链接:", db.DownloadUrls) 61 | return errors.New("数据库内容出错") 62 | } 63 | } 64 | log.Printf("%s 数据库下载成功: %s\n", db.Name, db.File) 65 | return nil 66 | } 67 | }, string(db.Format) 68 | } 69 | 70 | // intenel download func 71 | switch db.Format { 72 | case FormatZXIPv6Wry: 73 | return func() error { 74 | log.Println("正在下载最新 ZX IPv6数据库...") 75 | _, err := zxipv6wry.Download(getDbByName("zxipv6wry").File) 76 | if err != nil { 77 | log.Println("数据库 ZXIPv6Wry 下载失败:", err) 78 | } 79 | return err 80 | }, FormatZXIPv6Wry 81 | default: 82 | return func() error { 83 | log.Println("暂不支持该类型数据库的自动更新") 84 | log.Println("可通过指定数据库的 download-urls 从特定链接下载数据库文件") 85 | return nil 86 | }, time.Now().String() 87 | } 88 | } else { 89 | return func() error { 90 | log.Fatalln("该名称的数据库未找到:", name) 91 | return nil 92 | }, time.Now().String() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/qqwry/qqwry.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | 12 | "github.com/zu1k/nali/pkg/download" 13 | "github.com/zu1k/nali/pkg/wry" 14 | ) 15 | 16 | var DownloadUrls = []string{ 17 | "https://gh-release.zu1k.com/HMBSbige/qqwry/qqwry.dat", // redirect to HMBSbige/qqwry 18 | // Other repo: 19 | // https://github.com/HMBSbige/qqwry 20 | // https://github.com/metowolf/qqwry.dat 21 | } 22 | 23 | type QQwry struct { 24 | wry.IPDB[uint32] 25 | } 26 | 27 | // NewQQwry new database from path 28 | func NewQQwry(filePath string) (*QQwry, error) { 29 | var fileData []byte 30 | 31 | _, err := os.Stat(filePath) 32 | if err != nil && os.IsNotExist(err) { 33 | log.Println("文件不存在,尝试从网络获取最新纯真 IP 库") 34 | fileData, err = download.Download(filePath, DownloadUrls...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | } else { 39 | fileBase, err := os.OpenFile(filePath, os.O_RDONLY, 0400) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer fileBase.Close() 44 | 45 | fileData, err = io.ReadAll(fileBase) 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | 51 | if !CheckFile(fileData) { 52 | log.Fatalln("纯真 IP 库存在错误,请重新下载") 53 | } 54 | 55 | header := fileData[0:8] 56 | start := binary.LittleEndian.Uint32(header[:4]) 57 | end := binary.LittleEndian.Uint32(header[4:]) 58 | 59 | return &QQwry{ 60 | IPDB: wry.IPDB[uint32]{ 61 | Data: fileData, 62 | 63 | OffLen: 3, 64 | IPLen: 4, 65 | IPCnt: (end-start)/7 + 1, 66 | IdxStart: start, 67 | IdxEnd: end, 68 | }, 69 | }, nil 70 | } 71 | 72 | func (db QQwry) Find(query string, params ...string) (result fmt.Stringer, err error) { 73 | ip := net.ParseIP(query) 74 | if ip == nil { 75 | return nil, errors.New("query should be IPv4") 76 | } 77 | ip4 := ip.To4() 78 | if ip4 == nil { 79 | return nil, errors.New("query should be IPv4") 80 | } 81 | ip4uint := binary.BigEndian.Uint32(ip4) 82 | 83 | offset := db.SearchIndexV4(ip4uint) 84 | if offset <= 0 { 85 | return nil, errors.New("query not valid") 86 | } 87 | 88 | reader := wry.NewReader(db.Data) 89 | reader.Parse(offset + 4) 90 | return reader.Result.DecodeGBK(), nil 91 | } 92 | 93 | func CheckFile(data []byte) bool { 94 | if len(data) < 8 { 95 | return false 96 | } 97 | 98 | header := data[0:8] 99 | start := binary.LittleEndian.Uint32(header[:4]) 100 | end := binary.LittleEndian.Uint32(header[4:]) 101 | 102 | if start >= end || uint32(len(data)) < end+7 { 103 | return false 104 | } 105 | 106 | return true 107 | } 108 | -------------------------------------------------------------------------------- /pkg/wry/wry.go: -------------------------------------------------------------------------------- 1 | package wry 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | "golang.org/x/text/encoding/simplifiedchinese" 9 | ) 10 | 11 | // IPDB common ip database 12 | type IPDB[T ~uint32 | ~uint64] struct { 13 | Data []byte 14 | 15 | OffLen uint8 16 | IPLen uint8 17 | IPCnt T 18 | IdxStart T 19 | IdxEnd T 20 | } 21 | 22 | type Reader struct { 23 | s []byte 24 | i uint32 // current reading index 25 | l uint32 // last reading index 26 | 27 | Result Result 28 | } 29 | 30 | func NewReader(data []byte) Reader { 31 | return Reader{s: data, i: 0, l: 0, Result: Result{ 32 | Country: "", 33 | Area: "", 34 | }} 35 | } 36 | 37 | func (r *Reader) seekAbs(offset uint32) { 38 | r.l = r.i 39 | r.i = offset 40 | } 41 | 42 | func (r *Reader) seek(offset int64) { 43 | r.l = r.i 44 | r.i = uint32(int64(r.i) + offset) 45 | } 46 | 47 | // seekBack: seek to last index, can only call once 48 | func (r *Reader) seekBack() { 49 | r.i = r.l 50 | } 51 | 52 | func (r *Reader) read(length uint32) []byte { 53 | rs := make([]byte, length) 54 | copy(rs, r.s[r.i:]) 55 | r.l = r.i 56 | r.i += length 57 | return rs 58 | } 59 | 60 | func (r *Reader) readMode() (mode byte) { 61 | mode = r.s[r.i] 62 | r.l = r.i 63 | r.i += 1 64 | return 65 | } 66 | 67 | // readOffset: read 3 bytes as uint32 offset 68 | func (r *Reader) readOffset(follow bool) uint32 { 69 | buf := r.read(3) 70 | offset := Bytes3ToUint32(buf) 71 | if follow { 72 | r.l = r.i 73 | r.i = offset 74 | } 75 | return offset 76 | } 77 | 78 | func (r *Reader) readString(seek bool) string { 79 | length := bytes.IndexByte(r.s[r.i:], 0) 80 | str := string(r.s[r.i : r.i+uint32(length)]) 81 | if seek { 82 | r.l = r.i 83 | r.i += uint32(length) + 1 84 | } 85 | return str 86 | } 87 | 88 | type Result struct { 89 | Country string 90 | Area string 91 | } 92 | 93 | func (r *Result) DecodeGBK() *Result { 94 | enc := simplifiedchinese.GBK.NewDecoder() 95 | r.Country, _ = enc.String(r.Country) 96 | r.Area, _ = enc.String(r.Area) 97 | return r 98 | } 99 | 100 | func (r *Result) Trim() *Result { 101 | r.Country = strings.TrimSpace(strings.ReplaceAll(r.Country, "CZ88.NET", "")) 102 | r.Area = strings.TrimSpace(strings.ReplaceAll(r.Area, "CZ88.NET", "")) 103 | return r 104 | } 105 | 106 | func (r Result) String() string { 107 | r.Trim() 108 | return strings.TrimSpace(fmt.Sprintf("%s %s", r.Country, r.Area)) 109 | } 110 | 111 | func Bytes3ToUint32(data []byte) uint32 { 112 | i := uint32(data[0]) & 0xff 113 | i |= (uint32(data[1]) << 8) & 0xff00 114 | i |= (uint32(data[2]) << 16) & 0xff0000 115 | return i 116 | } 117 | -------------------------------------------------------------------------------- /pkg/cdn/cdn.go: -------------------------------------------------------------------------------- 1 | package cdn 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/zu1k/nali/pkg/download" 13 | "github.com/zu1k/nali/pkg/re" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | var DownloadUrls = []string{ 18 | "https://cdn.jsdelivr.net/gh/4ft35t/cdn/src/cdn.yml", 19 | "https://raw.githubusercontent.com/4ft35t/cdn/master/src/cdn.yml", 20 | "https://raw.githubusercontent.com/SukkaLab/cdn/master/src/cdn.yml", 21 | } 22 | 23 | type CDN struct { 24 | Map map[string]CDNResult 25 | ReMap []CDNReTuple 26 | } 27 | 28 | type CDNReTuple struct { 29 | *regexp.Regexp 30 | CDNResult 31 | } 32 | 33 | type CDNResult struct { 34 | Name string `yaml:"name"` 35 | Link string `yaml:"link"` 36 | } 37 | 38 | func (r CDNResult) String() string { 39 | return r.Name 40 | } 41 | 42 | func NewCDN(filePath string) (*CDN, error) { 43 | fileData := make([]byte, 0) 44 | _, err := os.Stat(filePath) 45 | if err != nil && os.IsNotExist(err) { 46 | log.Println("文件不存在,尝试从网络获取最新CDN数据库") 47 | fileData, err = download.Download(filePath, DownloadUrls...) 48 | if err != nil { 49 | return nil, err 50 | } 51 | } else { 52 | cdnFile, err := os.OpenFile(filePath, os.O_RDONLY, 0400) 53 | if err != nil { 54 | return nil, err 55 | } 56 | defer cdnFile.Close() 57 | 58 | fileData, err = ioutil.ReadAll(cdnFile) 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | cdnMap := make(map[string]CDNResult) 65 | err = yaml.Unmarshal(fileData, &cdnMap) 66 | if err != nil { 67 | return nil, err 68 | } 69 | cdnReMap := make([]CDNReTuple, 0) 70 | for k, v := range cdnMap { 71 | if re.MaybeRegexp(k) { 72 | rex, err := regexp.Compile(k) 73 | if err != nil { 74 | log.Printf("[CDN Database] entry %s not a valid regexp", k) 75 | } 76 | cdnReMap = append(cdnReMap, CDNReTuple{ 77 | Regexp: rex, 78 | CDNResult: v, 79 | }) 80 | } 81 | } 82 | 83 | return &CDN{Map: cdnMap, ReMap: cdnReMap}, nil 84 | } 85 | 86 | func (db CDN) Find(query string, params ...string) (result fmt.Stringer, err error) { 87 | baseCname := parseBaseCname(query) 88 | for _, domain := range baseCname { 89 | if domain != "" { 90 | cdnResult, found := db.Map[domain] 91 | if found { 92 | return cdnResult, nil 93 | } 94 | } 95 | 96 | for _, entry := range db.ReMap { 97 | if entry.Regexp.MatchString(domain) { 98 | return entry.CDNResult, nil 99 | } 100 | } 101 | } 102 | 103 | return nil, errors.New("not found") 104 | } 105 | 106 | func parseBaseCname(domain string) (result []string) { 107 | parts := strings.Split(domain, ".") 108 | size := len(parts) 109 | if size == 0 { 110 | return []string{} 111 | } 112 | domain = parts[size-1] 113 | result = append(result, domain) 114 | for i := len(parts) - 2; i >= 0; i-- { 115 | domain = parts[i] + "." + domain 116 | result = append(result, domain) 117 | } 118 | return result 119 | } 120 | -------------------------------------------------------------------------------- /.github/workflows/go-release.yml: -------------------------------------------------------------------------------- 1 | name: Go Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | release-binary: 8 | name: Release Binary 9 | runs-on: ubuntu-latest 10 | environment: s3 11 | steps: 12 | - name: Setup Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: '^1.20' 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v3 19 | 20 | - name: Cache go module 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/go/pkg/mod 24 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | ${{ runner.os }}-go- 27 | 28 | - name: Get dependencies and run test 29 | run: | 30 | go test ./... 31 | 32 | - name: Build 33 | if: startsWith(github.ref, 'refs/tags/') 34 | env: 35 | NAME: nali 36 | BINDIR: bin 37 | run: make -j releases && make sha256sum 38 | 39 | - name: Upload Release 40 | uses: softprops/action-gh-release@v1 41 | if: startsWith(github.ref, 'refs/tags/') 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | files: bin/* 46 | draft: true 47 | prerelease: true 48 | 49 | - uses: actions/upload-artifact@v3 50 | if: startsWith(github.ref, 'refs/tags/') 51 | with: 52 | name: build 53 | path: bin 54 | 55 | - name: Upload to s3 56 | if: startsWith(github.ref, 'refs/tags/') 57 | run: sh .github/scripts/upload_s3.sh 58 | env: 59 | S3_BUCKET: ${{ secrets.S3_BUCKET }} 60 | S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} 61 | S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} 62 | S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }} 63 | 64 | release-aur-bin: 65 | name: Release Aur bin 66 | needs: [release-binary] 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Get version 70 | id: version 71 | run: echo ::set-output name=version::${GITHUB_REF##*/v} 72 | 73 | - name: Publish AUR package nali-go-bin 74 | uses: zu1k/aur-publish-action@master 75 | with: 76 | package_name: nali-go-bin 77 | commit_username: 'zu1k' 78 | commit_email: 'i@zu1k.com' 79 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 80 | new_release: ${{ steps.version.outputs.version }} 81 | 82 | release-aur: 83 | name: Release Aur 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Get version 87 | id: version 88 | run: echo ::set-output name=version::${GITHUB_REF##*/v} 89 | 90 | - name: Publish AUR package nali-go 91 | uses: zu1k/aur-publish-action@master 92 | with: 93 | package_name: nali-go 94 | commit_username: 'zu1k' 95 | commit_email: 'i@zu1k.com' 96 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 97 | new_release: ${{ steps.version.outputs.version }} 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=nali 2 | BINDIR=bin 3 | VERSION=$(shell git describe --tags || echo "unknown version") 4 | GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/zu1k/nali/internal/constant.Version=$(VERSION)" -w -s' 5 | 6 | PLATFORM_LIST = \ 7 | darwin-arm64 \ 8 | darwin-amd64 \ 9 | linux-386 \ 10 | linux-amd64 \ 11 | linux-armv5 \ 12 | linux-armv6 \ 13 | linux-armv7 \ 14 | linux-armv8 \ 15 | linux-mips-softfloat \ 16 | linux-mips-hardfloat \ 17 | linux-mipsle-softfloat \ 18 | linux-mipsle-hardfloat \ 19 | linux-mips64 \ 20 | linux-mips64le \ 21 | freebsd-386 \ 22 | freebsd-amd64 23 | 24 | WINDOWS_ARCH_LIST = \ 25 | windows-386 \ 26 | windows-amd64 27 | 28 | all: linux-amd64 darwin-amd64 windows-amd64 # Most used 29 | 30 | docker: 31 | $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 32 | 33 | darwin-arm64: 34 | GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 35 | 36 | darwin-amd64: 37 | GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 38 | 39 | linux-386: 40 | GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 41 | 42 | linux-amd64: 43 | GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 44 | 45 | linux-armv5: 46 | GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 47 | 48 | linux-armv6: 49 | GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 50 | 51 | linux-armv7: 52 | GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 53 | 54 | linux-armv8: 55 | GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 56 | 57 | linux-mips-softfloat: 58 | GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 59 | 60 | linux-mips-hardfloat: 61 | GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 62 | 63 | linux-mipsle-softfloat: 64 | GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 65 | 66 | linux-mipsle-hardfloat: 67 | GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 68 | 69 | linux-mips64: 70 | GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 71 | 72 | linux-mips64le: 73 | GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 74 | 75 | freebsd-386: 76 | GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 77 | 78 | freebsd-amd64: 79 | GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 80 | 81 | windows-386: 82 | GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe 83 | 84 | windows-amd64: 85 | GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe 86 | 87 | gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) 88 | zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) 89 | 90 | $(gz_releases): %.gz : % 91 | chmod +x $(BINDIR)/$(NAME)-$(basename $@) 92 | gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@) 93 | 94 | $(zip_releases): %.zip : % 95 | zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe 96 | 97 | all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) 98 | 99 | releases: $(gz_releases) $(zip_releases) 100 | 101 | sha256sum: 102 | cd $(BINDIR); for file in *; do sha256sum $$file > $$file.sha256; done 103 | 104 | clean: 105 | rm $(BINDIR)/* 106 | -------------------------------------------------------------------------------- /internal/db/type.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/zu1k/nali/pkg/cdn" 7 | "github.com/zu1k/nali/pkg/dbif" 8 | "github.com/zu1k/nali/pkg/geoip" 9 | "github.com/zu1k/nali/pkg/ip2location" 10 | "github.com/zu1k/nali/pkg/ip2region" 11 | "github.com/zu1k/nali/pkg/ipip" 12 | "github.com/zu1k/nali/pkg/qqwry" 13 | "github.com/zu1k/nali/pkg/zxipv6wry" 14 | ) 15 | 16 | type DB struct { 17 | Name string 18 | NameAlias []string `yaml:"name-alias,omitempty" mapstructure:"name-alias"` 19 | Format Format 20 | File string 21 | 22 | Languages []string 23 | Types []Type 24 | 25 | DownloadUrls []string `yaml:"download-urls,omitempty" mapstructure:"download-urls"` 26 | } 27 | 28 | func (d *DB) get() (db dbif.DB) { 29 | if db, found := dbNameCache[d.Name]; found { 30 | return db 31 | } 32 | 33 | filePath := d.File 34 | 35 | var err error 36 | switch d.Format { 37 | case FormatQQWry: 38 | db, err = qqwry.NewQQwry(filePath) 39 | case FormatZXIPv6Wry: 40 | db, err = zxipv6wry.NewZXwry(filePath) 41 | case FormatIPIP: 42 | db, err = ipip.NewIPIP(filePath) 43 | case FormatMMDB: 44 | db, err = geoip.NewGeoIP(filePath) 45 | case FormatIP2Region: 46 | db, err = ip2region.NewIp2Region(filePath) 47 | case FormatIP2Location: 48 | db, err = ip2location.NewIP2Location(filePath) 49 | case FormatCDNYml: 50 | db, err = cdn.NewCDN(filePath) 51 | default: 52 | panic("DB format not supported!") 53 | } 54 | 55 | if err != nil || db == nil { 56 | log.Fatalln("Database init failed:", err) 57 | } 58 | 59 | dbNameCache[d.Name] = db 60 | return 61 | } 62 | 63 | type Format string 64 | 65 | const ( 66 | FormatMMDB Format = "mmdb" 67 | FormatQQWry = "qqwry" 68 | FormatZXIPv6Wry = "zxipv6wry" 69 | FormatIPIP = "ipip" 70 | FormatIP2Region = "ip2region" 71 | FormatIP2Location = "ip2location" 72 | 73 | FormatCDNYml = "cdn-yml" 74 | ) 75 | 76 | var ( 77 | LanguagesAll = []string{"ALL"} 78 | LanguagesZH = []string{"zh-CN"} 79 | LanguagesEN = []string{"en"} 80 | ) 81 | 82 | type Type string 83 | 84 | const ( 85 | TypeIPv4 Type = "IPv4" 86 | TypeIPv6 = "IPv6" 87 | TypeCDN = "CDN" 88 | ) 89 | 90 | var ( 91 | TypesAll = []Type{TypeIPv4, TypeIPv6, TypeCDN} 92 | TypesIP = []Type{TypeIPv4, TypeIPv6} 93 | TypesIPv4 = []Type{TypeIPv4} 94 | TypesIPv6 = []Type{TypeIPv6} 95 | TypesCDN = []Type{TypeCDN} 96 | ) 97 | 98 | type List []*DB 99 | type NameMap map[string]*DB 100 | type TypeMap map[Type][]*DB 101 | 102 | func (m *NameMap) From(dbs List) { 103 | for _, db := range dbs { 104 | (*m)[db.Name] = db 105 | 106 | if alias := db.NameAlias; alias != nil { 107 | for _, aName := range alias { 108 | (*m)[aName] = db 109 | } 110 | } 111 | } 112 | } 113 | 114 | func (m *TypeMap) From(dbs List) { 115 | for _, db := range dbs { 116 | for _, typ := range db.Types { 117 | dbsInType := (*m)[typ] 118 | if dbsInType == nil { 119 | dbsInType = []*DB{db} 120 | } else { 121 | dbsInType = append(dbsInType, db) 122 | } 123 | (*m)[typ] = dbsInType 124 | } 125 | } 126 | } 127 | 128 | func getDbByName(name string) (db *DB) { 129 | if dbInfo, found := NameDBMap[name]; found { 130 | return dbInfo 131 | } 132 | 133 | defaultNameDBMap := NameMap{} 134 | defaultNameDBMap.From(GetDefaultDBList()) 135 | if dbInfo, found := defaultNameDBMap[name]; found { 136 | return dbInfo 137 | } 138 | 139 | log.Fatalf("DB with name %s not found!\n", name) 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - "v*" 10 | paths: 11 | - "**/*.go" 12 | - "go.mod" 13 | - "go.sum" 14 | - ".github/workflows/*.yml" 15 | pull_request: 16 | types: [opened, synchronize, reopened] 17 | paths: 18 | - "**/*.go" 19 | - "go.mod" 20 | - "go.sum" 21 | - ".github/workflows/*.yml" 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | # Include amd64 on all platforms. 28 | goos: [windows, freebsd, openbsd, linux, dragonfly, darwin] 29 | goarch: [amd64, 386] 30 | exclude: 31 | # Exclude i386 on darwin and dragonfly. 32 | - goarch: 386 33 | goos: dragonfly 34 | - goarch: 386 35 | goos: darwin 36 | include: 37 | # BEIGIN MacOS ARM64 38 | - goos: darwin 39 | goarch: arm64 40 | # END MacOS ARM64 41 | # BEGIN Linux ARM 5 6 7 42 | - goos: linux 43 | goarch: arm 44 | goarm: 7 45 | - goos: linux 46 | goarch: arm 47 | goarm: 6 48 | - goos: linux 49 | goarch: arm 50 | goarm: 5 51 | # END Linux ARM 5 6 7 52 | # BEGIN Android ARM 8 53 | - goos: android 54 | goarch: arm64 55 | # END Android ARM 8 56 | # Windows ARM 57 | - goos: windows 58 | goarch: arm64 59 | - goos: windows 60 | goarch: arm 61 | goarm: 7 62 | # BEGIN Other architectures 63 | # BEGIN riscv64 & ARM64 64 | - goos: linux 65 | goarch: arm64 66 | - goos: linux 67 | goarch: riscv64 68 | # END riscv64 & ARM64 69 | # BEGIN MIPS 70 | - goos: linux 71 | goarch: mips64 72 | - goos: linux 73 | goarch: mips64le 74 | - goos: linux 75 | goarch: mipsle 76 | - goos: linux 77 | goarch: mips 78 | # END MIPS 79 | # BEGIN PPC 80 | - goos: linux 81 | goarch: ppc64 82 | - goos: linux 83 | goarch: ppc64le 84 | # END PPC 85 | # BEGIN FreeBSD ARM 86 | - goos: freebsd 87 | goarch: arm64 88 | - goos: freebsd 89 | goarch: arm 90 | goarm: 7 91 | # END FreeBSD ARM 92 | # BEGIN S390X 93 | - goos: linux 94 | goarch: s390x 95 | # END S390X 96 | # END Other architectures 97 | # BEGIN OPENBSD ARM 98 | - goos: openbsd 99 | goarch: arm64 100 | - goos: openbsd 101 | goarch: arm 102 | goarm: 7 103 | env: 104 | GOOS: ${{ matrix.goos }} 105 | GOARCH: ${{ matrix.goarch }} 106 | GOARM: ${{ matrix.goarm }} 107 | CGO_ENABLED: 0 108 | steps: 109 | - name: Checkout codebase 110 | uses: actions/checkout@v3 111 | - name: Show workflow information 112 | run: | 113 | if [ ! -z $GOARM ]; then 114 | export GOARM=v$GOARM 115 | fi 116 | export _NAME="nali-nt_${GOOS}_${GOARCH}${GOARM}" 117 | if [ "$GOOS" == "windows" ]; then 118 | export _NAME="$_NAME.exe" 119 | fi 120 | echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, RELEASE_NAME: $_NAME" 121 | echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV 122 | echo "BUILD_VERSION=$(git describe --tags --always)" >> $GITHUB_ENV 123 | echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV 124 | echo "COMMIT_SHA1=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 125 | - name: Set up Go 126 | uses: actions/setup-go@v4 127 | with: 128 | go-version: '1.22' 129 | - name: Get project dependencies 130 | run: go mod download 131 | - name: Build 132 | run: | 133 | go build -trimpath -o dist/${ASSET_NAME} \ 134 | -ldflags "-w -s" 135 | - name: Upload files to Artifacts 136 | uses: actions/upload-artifact@v3 137 | with: 138 | name: ${{ env.ASSET_NAME }} 139 | path: | 140 | dist/${{ env.ASSET_NAME }} 141 | - name: Release 142 | if: startsWith(github.ref, 'refs/tags/v') 143 | uses: softprops/action-gh-release@v1 144 | with: # 将下述可执行文件 release 上去 145 | draft: false # Release草稿 146 | files: | 147 | dist/* 148 | env: 149 | GITHUB_TOKEN: ${{ secrets.GT_Token }} 150 | -------------------------------------------------------------------------------- /pkg/leomoeapi/leomoeapi.go: -------------------------------------------------------------------------------- 1 | package leomoeapi 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gorilla/websocket" 8 | "github.com/nxtrace/NTrace-core/util" 9 | "github.com/zu1k/nali/pow" 10 | "log" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | type IPGeoData struct { 19 | IP string `json:"ip"` 20 | Asnumber string `json:"asnumber"` 21 | Country string `json:"country"` 22 | CountryEn string `json:"country_en"` 23 | Prov string `json:"prov"` 24 | ProvEn string `json:"prov_en"` 25 | City string `json:"city"` 26 | CityEn string `json:"city_en"` 27 | District string `json:"district"` 28 | Owner string `json:"owner"` 29 | Isp string `json:"isp"` 30 | Domain string `json:"domain"` 31 | Whois string `json:"whois"` 32 | Lat float64 `json:"lat"` 33 | Lng float64 `json:"lng"` 34 | Prefix string `json:"prefix"` 35 | Router map[string][]string `json:"router"` 36 | Source string `json:"source"` 37 | } 38 | 39 | var tokenCache = make(map[string]string) 40 | var conn *websocket.Conn 41 | 42 | func FetchIPInfo(ip string) (*IPGeoData, error) { 43 | token, ok := tokenCache["api.leo.moe"] 44 | if !ok { 45 | token = "" 46 | } 47 | 48 | var host, port, fastIp string 49 | host, port = util.GetHostAndPort() 50 | // 如果 host 是一个 IP 使用默认域名 51 | if valid := net.ParseIP(host); valid != nil { 52 | host = "origin-fallback.nxtrace.org" 53 | } else { 54 | // 默认配置完成,开始寻找最优 IP 55 | fastIp = util.GetFastIP("api.nxtrace.org", port, false) 56 | } 57 | //host, port, fastIp = "103.120.18.35", "api.leo.moe", "443" 58 | envToken := util.EnvToken 59 | jwtToken := token 60 | ua := []string{pow.UserAgent} 61 | if envToken != "" { 62 | ua = []string{"Privileged Client"} 63 | } 64 | 65 | if token == "" { 66 | // 如果没有传入 token,尝试从环境变量中获取 67 | jwtToken = envToken 68 | err := error(nil) 69 | if envToken == "" { 70 | // 如果环境变量中没有 token,尝试从 pow 获取 71 | jwtToken, err = pow.GetToken(fastIp, host, port) 72 | if err != nil { 73 | log.Println(err) 74 | os.Exit(1) 75 | } 76 | 77 | } 78 | } 79 | 80 | tokenCache["api.leo.moe"] = jwtToken 81 | requestHeader := http.Header{ 82 | "Host": []string{host}, 83 | "User-Agent": ua, 84 | "Authorization": []string{"Bearer " + jwtToken}, 85 | } 86 | dialer := websocket.DefaultDialer 87 | dialer.TLSClientConfig = &tls.Config{ 88 | ServerName: host, 89 | } 90 | u := url.URL{Scheme: "wss", Host: fastIp + ":" + port, Path: "/v3/ipGeoWs"} 91 | 92 | var c *websocket.Conn 93 | var err error 94 | 95 | if conn == nil { 96 | //fmt.Println("new dialing") 97 | c, _, err = websocket.DefaultDialer.Dial(u.String(), requestHeader) 98 | if err != nil { 99 | return nil, fmt.Errorf("websocket dial: %w", err) 100 | } 101 | c.SetCloseHandler(func(code int, text string) error { 102 | conn = nil // 将全局的 conn 设为 nil 103 | return nil 104 | }) 105 | // ws留给下次复用 106 | conn = c 107 | } else { 108 | c = conn 109 | } 110 | 111 | //defer func(c *websocket.Conn) { 112 | // err := c.Close() 113 | // if err != nil { 114 | // log.Println(err) 115 | // } 116 | //}(c) 117 | // TODO: 现在是一直不关闭,以后想办法在程序退出时关闭 118 | // 在这种情况下,你可以考虑使用Go的 os/signal 包来监听操作系统发出的终止信号。当程序收到这样的信号时, 119 | // 比如 SIGINT(即 Ctrl+C)或 SIGTERM,你可以优雅地关闭你的 WebSocket 连接。 120 | 121 | if err := c.WriteMessage(websocket.TextMessage, []byte(ip)); err != nil { 122 | return nil, fmt.Errorf("write message: %w", err) 123 | } 124 | 125 | _, message, err := c.ReadMessage() 126 | if err != nil { 127 | return nil, fmt.Errorf("read message: %w", err) 128 | } 129 | 130 | var data IPGeoData 131 | if err := json.Unmarshal(message, &data); err != nil { 132 | return nil, fmt.Errorf("json unmarshal: %w", err) 133 | } 134 | 135 | return &data, nil 136 | } 137 | 138 | type Result struct { 139 | Data string 140 | } 141 | 142 | func (r Result) String() string { 143 | return r.Data 144 | } 145 | 146 | func isPrivateOrReserved(ip net.IP) bool { 147 | privateIPv4 := []string{ 148 | "10.0.0.0/8", 149 | "172.16.0.0/12", 150 | "192.168.0.0/16", 151 | "100.64.0.0/10", // Shared Address Space (also known as Carrier-Grade NAT, or CGN) 152 | "198.18.0.0/15", // Network Interconnect Device Benchmark Testing 153 | "198.51.100.0/24", // TEST-NET-2 154 | "203.0.113.0/24", // TEST-NET-3 155 | "240.0.0.0/4", // Reserved for future use 156 | } 157 | 158 | privateIPv6 := []string{ 159 | "FC00::/7", // Unique Local Address 160 | "FE80::/10", // Link-local address 161 | } 162 | 163 | reservedIPv4 := []string{ 164 | "0.0.0.0/8", 165 | "6.0.0.0/7", 166 | "11.0.0.0/8", 167 | "21.0.0.0/8", 168 | "22.0.0.0/8", 169 | "26.0.0.0/8", 170 | "28.0.0.0/8", 171 | "29.0.0.0/8", 172 | "30.0.0.0/8", 173 | "33.0.0.0/8", 174 | "55.0.0.0/8", 175 | "214.0.0.0/8", 176 | "215.0.0.0/8", 177 | } 178 | 179 | reservedIPv6 := []string{ 180 | "::1/128", // loopback address 181 | "::/128", // unspecified address 182 | "FF00::/8", // multicast address 183 | } 184 | 185 | for _, cidr := range append(privateIPv4, reservedIPv4...) { 186 | _, network, _ := net.ParseCIDR(cidr) 187 | if network.Contains(ip) { 188 | return true 189 | } 190 | } 191 | 192 | if ip.To4() == nil { 193 | for _, cidr := range append(privateIPv6, reservedIPv6...) { 194 | _, network, _ := net.ParseCIDR(cidr) 195 | if network.Contains(ip) { 196 | return true 197 | } 198 | } 199 | } 200 | 201 | return false 202 | } 203 | 204 | func Find(query string) (result fmt.Stringer, err error) { 205 | if net.ParseIP(query) == nil { 206 | return Result{""}, nil // 如果 query 不是一个有效的 IP 地址,返回空字符串 207 | } 208 | if isPrivateOrReserved(net.ParseIP(query)) { 209 | return Result{""}, nil // 如果 query 是一个私有或保留地址,返回空字符串 210 | } 211 | i := 0 212 | var res *IPGeoData 213 | for i = 0; i < 3; i++ { 214 | res, err = FetchIPInfo(query) 215 | if err != nil { 216 | continue 217 | } 218 | break 219 | } 220 | if i == 3 { 221 | return nil, err 222 | } 223 | 224 | result = Result{ 225 | Data: strings.Join(func() []string { 226 | dataSlice := make([]string, 0, 7) 227 | fields := []string{ 228 | "AS" + res.Asnumber, 229 | res.Country, 230 | res.Prov, 231 | res.City, 232 | res.District, 233 | } 234 | for _, field := range fields { 235 | if field != "" { 236 | dataSlice = append(dataSlice, field) 237 | } 238 | } 239 | if res.Owner != "" { 240 | dataSlice = append(dataSlice, res.Owner) 241 | } else { 242 | dataSlice = append(dataSlice, res.Isp) 243 | } 244 | return dataSlice 245 | }(), ";"), 246 | } 247 | 248 | return result, nil 249 | } 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
Nali x NextTrace
3 |

4 | 5 | #### [English](https://github.com/zu1k/nali/blob/master/README_en.md) 6 | 7 | ## 相比原版nali的特别功能 8 | - 支持**NextTrace的LEOMOEAPI** (默认使用,若要使用其他API,请先设置环境变量NALI=1) 9 | - 支持管道处理 尤其是**配合MTR使用有奇效** 10 | 11 | 举个例子: 12 | 13 | ```bash 14 | mtr -n4 tj.189.cn | ./nali-nt_linux_amd64 15 | ``` 16 | 17 | image 18 | 19 | ```bash 20 | mtr -n6 tj.10086.cn | ./nali-nt_linux_amd64 21 | ``` 22 | 23 | image 24 | 25 | ```bash 26 | #在您的.bashrc 或者 .zshrc中添加如下代码可以方便您的使用 27 | ntr(){ 28 | mtr $* -n | /path/to/your/nali-nt 29 | } 30 | #然后就能用如下捷径利用mtr x nexttrace了 31 | ntr 1.1.1.1 32 | #甚至不影响您使用mtr的其他参数,比如: 33 | ntr 1.1.1.1 -T -c 10 34 | ``` 35 | 36 | **以上操作需要您先自行安装mtr 37 | 38 | ## 功能 39 | - CDN 服务提供商查询 40 | - 支持交互式查询 41 | - 同时支持IPv4和IPv6 42 | - 支持多语言 43 | - 查询完全离线 44 | - 全平台支持 45 | - 支持彩色输出 46 | - 支持多种数据库 47 | - 纯真 IPv4 离线数据库 48 | - ZX IPv6 离线数据库 49 | - Geoip2 城市数据库 (可选) 50 | - IPIP 数据库 (可选) 51 | - ip2region 数据库 (可选) 52 | - DB-IP 数据库 (可选) 53 | - IP2Location DB3 LITE 数据库 (可选) 54 | 55 | ## 安装 56 | 57 | ### 下载预编译的可执行程序 58 | 59 | 可以从Release页面下载预编译好的可执行程序: [Release](https://github.com/zu1k/nali/releases) 60 | 61 | 你需要选择适合你系统和硬件架构的版本下载,解压后可直接运行 62 | 63 | ## 使用说明 64 | 65 | ### 查询一个IP的地理信息 66 | 67 | ``` 68 | $ nali 1.2.3.4 69 | 1.2.3.4 [澳大利亚 APNIC Debogon-prefix网络] 70 | ``` 71 | 72 | #### 或者 使用 `管道` 73 | 74 | ``` 75 | $ echo IP 6.6.6.6 | nali 76 | IP 6.6.6.6 [美国 亚利桑那州华楚卡堡市美国国防部网络中心] 77 | ``` 78 | 79 | ### 同时查询多个IP的地理信息 80 | 81 | ``` 82 | $ nali 1.2.3.4 4.3.2.1 123.23.3.0 83 | 1.2.3.4 [澳大利亚 APNIC Debogon-prefix网络] 84 | 4.3.2.1 [美国 新泽西州纽瓦克市Level3Communications] 85 | 123.23.3.0 [越南 越南邮电集团公司] 86 | ``` 87 | 88 | ### 交互式查询 89 | 90 | 使用 `exit` 或 `quit` 退出查询 91 | 92 | ``` 93 | $ nali 94 | 123.23.23.23 95 | 123.23.23.23 [越南 越南邮电集团公司] 96 | 1.0.0.1 97 | 1.0.0.1 [美国 APNIC&CloudFlare公共DNS服务器] 98 | 8.8.8.8 99 | 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器] 100 | quit 101 | ``` 102 | 103 | ### 与 `dig` 命令配合使用 104 | 105 | 需要你系统中已经安装好 dig 程序 106 | 107 | ``` 108 | $ dig nali.zu1k.com +short | nali 109 | 104.28.2.115 [美国 CloudFlare公司CDN节点] 110 | 104.28.3.115 [美国 CloudFlare公司CDN节点] 111 | 172.67.135.48 [美国 CloudFlare节点] 112 | ``` 113 | 114 | ### 与 `nslookup` 命令配合使用 115 | 116 | 需要你系统中已经安装好 nslookup 程序 117 | 118 | ``` 119 | $ nslookup nali.zu1k.com 8.8.8.8 | nali 120 | Server: 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器] 121 | Address: 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器]#53 122 | 123 | Non-authoritative answer: 124 | Name: nali.zu1k.com 125 | Address: 104.28.3.115 [美国 CloudFlare公司CDN节点] 126 | Name: nali.zu1k.com 127 | Address: 104.28.2.115 [美国 CloudFlare公司CDN节点] 128 | Name: nali.zu1k.com 129 | Address: 172.67.135.48 [美国 CloudFlare节点] 130 | ``` 131 | 132 | ### 与任意程序配合使用 133 | 134 | 因为 nali 支持管道处理,所以可以和任意程序配合使用 135 | 136 | ``` 137 | bash abc.sh | nali 138 | ``` 139 | 140 | Nali 将在 IP后面插入IP地理信息,CDN域名后面插入CDN服务提供商信息 141 | 142 | ### 支持IPv6 143 | 144 | 和 IPv4 用法完全相同 145 | 146 | ``` 147 | $ nslookup google.com | nali 148 | Server: 127.0.0.53 [局域网 IP] 149 | Address: 127.0.0.53 [局域网 IP]#53 150 | 151 | Non-authoritative answer: 152 | Name: google.com 153 | Address: 216.58.211.110 [美国 Google全球边缘网络] 154 | Name: google.com 155 | Address: 2a00:1450:400e:809::200e [荷兰Amsterdam Google Inc. 服务器网段] 156 | ``` 157 | 158 | ### 查询 CDN 服务提供商 159 | 160 | 因为 CDN 服务通常使用 CNAME 的域名解析方式,所以推荐与 `nslookup` 或者 `dig` 配合使用,在已经知道 CNAME 后可单独使用 161 | 162 | ``` 163 | $ nslookup www.gov.cn | nali 164 | Server: 127.0.0.53 [局域网 IP] 165 | Address: 127.0.0.53 [局域网 IP]#53 166 | 167 | Non-authoritative answer: 168 | www.gov.cn canonical name = www.gov.cn.bsgslb.cn [白山云 CDN]. 169 | www.gov.cn.bsgslb.cn [白山云 CDN] canonical name = zgovweb.v.bsgslb.cn [白山云 CDN]. 170 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 171 | Address: 103.104.170.25 [新加坡 ] 172 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 173 | Address: 2001:428:6402:21b::5 [美国Louisiana州Monroe Qwest Communications Company, LLC (CenturyLink)] 174 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 175 | Address: 2001:428:6402:21b::6 [美国Louisiana州Monroe Qwest Communications Company, LLC (CenturyLink)] 176 | ``` 177 | 178 | ## 用户交互 179 | 180 | 程序第一次运行后,会在工作目录生成配置文件 `config.yaml` (默认`~/.nali/config.yaml`),配置文件定义了数据库信息,默认用户无需进行修改 181 | 182 | 数据库格式默认如下: 183 | 184 | ```yaml 185 | - name: geoip 186 | name-alias: 187 | - geolite 188 | - geolite2 189 | format: mmdb 190 | file: GeoLite2-City.mmdb 191 | languages: 192 | - ALL 193 | types: 194 | - IPv4 195 | - IPv6 196 | ``` 197 | 198 | 其中,`languages` 和 `types` 表示该数据库支持的语言和查询类型。 如果你需要增加数据库,需小心修改配置文件,如果有任何问题,欢迎提 issue 询问。 199 | 200 | ### 查看帮助 201 | 202 | ``` 203 | $ nali --help 204 | Usage: 205 | nali [flags] 206 | nali [command] 207 | 208 | Available Commands: 209 | help Help about any command 210 | update update qqwry, zxipv6wry, ip2region ip database and cdn 211 | 212 | Flags: 213 | -h, --help help for nali 214 | -t, --toggle Help message for toggle 215 | 216 | Use "nali [command] --help" for more information about a command. 217 | ``` 218 | 219 | ### 更新数据库 220 | 221 | 更新所有可以自动更新的数据库 222 | 223 | ``` 224 | $ nali update 225 | 2020/07/17 12:53:46 正在下载最新纯真 IP 库... 226 | 2020/07/17 12:54:05 已将最新的纯真 IP 库保存到本地 /root/.nali/qqwry.dat 227 | ``` 228 | 229 | 或者指定数据库 230 | 231 | ``` 232 | $ nali update --db qqwry,cdn 233 | 2020/07/17 12:53:46 正在下载最新纯真 IP 库... 234 | 2020/07/17 12:54:05 已将最新的纯真 IP 库保存到本地 /root/.nali/qqwry.dat 235 | ``` 236 | 237 | ### 自选数据库 238 | 239 | 用户可以指定使用哪个数据库,需要设置环境变量: `NALI_DB_IP4`、`NALI_DB_IP6` 或者两个同时设置 240 | 241 | 支持的变量内容: 242 | 243 | - Geoip2 `['geoip', 'geoip2']` 244 | - Chunzhen `['chunzhen', 'qqwry']` 245 | - IPIP `['ipip']` 246 | - Ip2Region `['ip2region', 'i2r']` 247 | - DBIP `['dbip', 'db-ip']` 248 | - IP2Location `['ip2location']` 249 | 250 | #### Windows平台 251 | 252 | ##### 使用geoip数据库 253 | 254 | ``` 255 | set NALI_DB_IP4=geoip 256 | 257 | 或者使用 powershell 258 | 259 | $env:NALI_DB_IP4="geoip" 260 | ``` 261 | 262 | ##### 使用ipip数据库 263 | 264 | ``` 265 | set NALI_DB_IP6=ipip 266 | 267 | 或者使用 powershell 268 | 269 | $env:NALI_DB_IP6="ipip" 270 | ``` 271 | 272 | #### Linux平台 273 | 274 | ##### 使用geoip数据库 275 | 276 | ``` 277 | export NALI_DB_IP4=geoip 278 | ``` 279 | 280 | ##### 使用ipip数据库 281 | 282 | ``` 283 | export NALI_DB_IP4=ipip 284 | ``` 285 | 286 | ### 多语言支持 287 | 288 | 通过修改环境变量 `NALI_LANG` 来指定使用的语言,当使用非中文语言时仅支持GeoIP2这个数据库 289 | 290 | 该参数可设置的值见 GeoIP2 这个数据库的支持列表 291 | 292 | ``` 293 | # NALI_LANG=en nali 1.1.1.1 294 | 1.1.1.1 [Australia] 295 | ``` 296 | 297 | ### 工作目录 298 | 299 | 设置环境变量 `NALI_HOME` 来指定工作目录,配置文件和数据库存放在工作目录下。也可在配置文件中使用绝对路径指定其他数据库路径。 300 | 301 | 设置环境变量 `NALI_CONFIG_HOME` 来指定配置文件目录,`NALI_DB_HOME` 来执行数据库文件目录 302 | 303 | 如果未指定相关环境变量,将使用 XDG 规范,配置文件目录在 `$XDG_CONFIG_HOME/nali`,数据库文件目录在 `$XDG_DATA_HOME/nali` 304 | 305 | ``` 306 | set NALI_HOME=D:\nali 307 | 308 | or 309 | 310 | export NALI_HOME=/var/nali 311 | ``` 312 | 313 | ## 感谢列表 314 | 315 | - [纯真QQIP离线数据库](http://www.cz88.net) 316 | - [qqwry纯真数据库解析](https://github.com/yinheli/qqwry) 317 | - [ZX公网ipv6数据库](https://ip.zxinc.org/ipquery/) 318 | - [Geoip2 city数据库](https://www.maxmind.com/en/geoip2-precision-city-service) 319 | - [geoip2-golang解析器](https://github.com/oschwald/geoip2-golang) 320 | - [CDN provider数据库](https://github.com/SukkaLab/cdn) 321 | - [IPIP数据库](https://www.ipip.net/product/ip.html) 322 | - [IPIP数据库解析](https://github.com/ipipdotnet/ipdb-go) 323 | - [ip2region数据库](https://github.com/lionsoul2014/ip2region) 324 | - [IP2Location DB3 LITE](https://lite.ip2location.com/database/db3-ip-country-region-city) 325 | - [Cobra CLI库](https://github.com/spf13/cobra) 326 | 327 | 感谢 JetBrains 提供开源项目免费License 328 | 329 | 330 | 331 | 332 | 333 | ## 作者 334 | 335 | **Nali** © [zu1k](https://github.com/zu1k), 遵循 [MIT](./LICENSE) 证书.
336 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 |

2 |
Nali
3 |

4 | 5 |

An offline tool for querying IP geographic information and CDN provider.

6 | 7 |

8 | 9 | Github Actions 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 | #### [中文文档](https://github.com/zu1k/nali/blob/master/README.md) 23 | 24 | ## Feature 25 | 26 | - Multi database support 27 | - Chunzhen qqip database 28 | - ZX ipv6 database 29 | - Geoip2 city database 30 | - IPIP free database 31 | - ip2region database 32 | - DB-IP database 33 | - IP2Location DB3 LITE database 34 | - CDN provider query 35 | - Pipeline support 36 | - Interactive query 37 | - Both ipv4 and ipv6 supported 38 | - Multilingual support 39 | - Offline query 40 | - Full platform support 41 | - Color print 42 | 43 | ## Install 44 | 45 | ### Install from source 46 | 47 | Nali Requires Go >= 1.19. You can build it from source: 48 | 49 | ```sh 50 | $ go install github.com/zu1k/nali@latest 51 | ``` 52 | 53 | ### Install pre-build binary 54 | 55 | Pre-built binaries are available here: [release](https://github.com/zu1k/nali/releases) 56 | 57 | Download the binary compatible with your platform, unpack and copy to the directory in path 58 | 59 | ### Arch Linux 60 | 61 | We have published 3 packages in Aur: 62 | 63 | - `nali-go`: release version, compile when installing 64 | - `nali-go-bin`: release version, pre-compiled binary 65 | - `nali-go-git`: the latest master branch version, compile when installing 66 | 67 | ## Usage 68 | 69 | ### Query a simple IP address 70 | 71 | ``` 72 | $ nali 1.2.3.4 73 | 1.2.3.4 [澳大利亚 APNIC Debogon-prefix网络] 74 | ``` 75 | 76 | #### or use `pipe` 77 | 78 | ``` 79 | $ echo IP 6.6.6.6 | nali 80 | IP 6.6.6.6 [美国 亚利桑那州华楚卡堡市美国国防部网络中心] 81 | ``` 82 | 83 | ### Query multiple IP addresses 84 | 85 | ``` 86 | $ nali 1.2.3.4 4.3.2.1 123.23.3.0 87 | 1.2.3.4 [澳大利亚 APNIC Debogon-prefix网络] 88 | 4.3.2.1 [美国 新泽西州纽瓦克市Level3Communications] 89 | 123.23.3.0 [越南 越南邮电集团公司] 90 | ``` 91 | 92 | ### Interactive query 93 | 94 | use `exit` or `quit` to quit 95 | 96 | ``` 97 | $ nali 98 | 123.23.23.23 99 | 123.23.23.23 [越南 越南邮电集团公司] 100 | 1.0.0.1 101 | 1.0.0.1 [美国 APNIC&CloudFlare公共DNS服务器] 102 | 8.8.8.8 103 | 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器] 104 | quit 105 | ``` 106 | 107 | ### Use with `dig` 108 | 109 | ``` 110 | $ dig nali.zu1k.com +short | nali 111 | 104.28.2.115 [美国 CloudFlare公司CDN节点] 112 | 104.28.3.115 [美国 CloudFlare公司CDN节点] 113 | 172.67.135.48 [美国 CloudFlare节点] 114 | ``` 115 | 116 | ### Use with `nslookup` 117 | 118 | ``` 119 | $ nslookup nali.zu1k.com 8.8.8.8 | nali 120 | Server: 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器] 121 | Address: 8.8.8.8 [美国 加利福尼亚州圣克拉拉县山景市谷歌公司DNS服务器]#53 122 | 123 | Non-authoritative answer: 124 | Name: nali.zu1k.com 125 | Address: 104.28.3.115 [美国 CloudFlare公司CDN节点] 126 | Name: nali.zu1k.com 127 | Address: 104.28.2.115 [美国 CloudFlare公司CDN节点] 128 | Name: nali.zu1k.com 129 | Address: 172.67.135.48 [美国 CloudFlare节点] 130 | ``` 131 | 132 | ### Use with any other program 133 | 134 | Because nali can read the contents of the `stdin` pipeline, it can be used with any program. 135 | 136 | ``` 137 | bash abc.sh | nali 138 | ``` 139 | 140 | Nali will insert IP information after IP address. 141 | 142 | ### IPv6 support 143 | 144 | Use like IPv4 145 | 146 | ``` 147 | $ nslookup google.com | nali 148 | Server: 127.0.0.53 [局域网 IP] 149 | Address: 127.0.0.53 [局域网 IP]#53 150 | 151 | Non-authoritative answer: 152 | Name: google.com 153 | Address: 216.58.211.110 [美国 Google全球边缘网络] 154 | Name: google.com 155 | Address: 2a00:1450:400e:809::200e [荷兰Amsterdam Google Inc. 服务器网段] 156 | ``` 157 | 158 | ### Query CDN provider 159 | 160 | ``` 161 | $ nslookup www.gov.cn | nali 162 | Server: 127.0.0.53 [局域网 IP] 163 | Address: 127.0.0.53 [局域网 IP]#53 164 | 165 | Non-authoritative answer: 166 | www.gov.cn canonical name = www.gov.cn.bsgslb.cn [白山云 CDN]. 167 | www.gov.cn.bsgslb.cn [白山云 CDN] canonical name = zgovweb.v.bsgslb.cn [白山云 CDN]. 168 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 169 | Address: 103.104.170.25 [新加坡 ] 170 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 171 | Address: 2001:428:6402:21b::5 [美国Louisiana州Monroe Qwest Communications Company, LLC (CenturyLink)] 172 | Name: zgovweb.v.bsgslb.cn [白山云 CDN] 173 | Address: 2001:428:6402:21b::6 [美国Louisiana州Monroe Qwest Communications Company, LLC (CenturyLink)] 174 | ``` 175 | 176 | ## Interface 177 | 178 | After nali runs for the first time, a configuration file `config.yaml` will be generated in the working directory (default `~/.nali/config.yaml`), the configuration file defines the database information. 179 | 180 | A database is defined as follows: 181 | 182 | ```yaml 183 | - name: geoip 184 | name-alias: 185 | - geolite 186 | - geolite2 187 | format: mmdb 188 | file: GeoLite2-City.mmdb 189 | languages: 190 | - ALL 191 | types: 192 | - IPv4 193 | - IPv6 194 | ``` 195 | 196 | ### Help 197 | 198 | ``` 199 | $ nali --help 200 | Usage: 201 | nali [flags] 202 | nali [command] 203 | 204 | Available Commands: 205 | completion generate the autocompletion script for the specified shell 206 | help Help about any command 207 | update update chunzhen ip database 208 | 209 | Flags: 210 | --gbk Use GBK decoder 211 | -h, --help help for nali 212 | 213 | Use "nali [command] --help" for more information about a command. 214 | ``` 215 | 216 | ### Update database 217 | 218 | Update all databases if available: 219 | 220 | ``` 221 | $ nali update 222 | 2020/07/17 12:53:46 正在下载最新纯真 IP 库... 223 | 2020/07/17 12:54:05 已将最新的纯真 IP 库保存到本地 /root/.nali/qqwry.dat 224 | ``` 225 | 226 | Update specified databases: 227 | 228 | ``` 229 | $ nali update --db qqwry,cdn 230 | 2020/07/17 12:53:46 正在下载最新纯真 IP 库... 231 | 2020/07/17 12:54:05 已将最新的纯真 IP 库保存到本地 /root/.nali/qqwry.dat 232 | ``` 233 | 234 | ### Specify database 235 | 236 | Users can specify which database to use,set environment variables `NALI_DB_IP4`, `NALI_DB_IP6` or both. 237 | 238 | Supported database: 239 | 240 | - Geoip2 `['geoip', 'geoip2']` 241 | - Chunzhen `['chunzhen', 'qqwry']` 242 | - IPIP `['ipip']` 243 | - Ip2Region `['ip2region', 'i2r']` 244 | - DBIP `['dbip', 'db-ip']` 245 | - IP2Location `['ip2location']` 246 | 247 | #### Windows 248 | 249 | ##### Use geoip db 250 | 251 | ``` 252 | set NALI_DB_IP4=geoip 253 | 254 | or use powershell 255 | 256 | $env:NALI_DB_IP4="geoip" 257 | ``` 258 | 259 | ##### Use ipip db 260 | 261 | ``` 262 | set NALI_DB_IP6=ipip 263 | 264 | or use powershell 265 | 266 | $env:NALI_DB_IP6="ipip" 267 | ``` 268 | 269 | #### Linux 270 | 271 | ##### Use geoip db 272 | 273 | ``` 274 | export NALI_DB_IP4=geoip 275 | ``` 276 | 277 | ##### Use ipip db 278 | 279 | ``` 280 | export NALI_DB_IP6=ipip 281 | ``` 282 | 283 | ### Multilingual support 284 | 285 | Specify the language to be used by modifying the environment variable `NALI_LANG`, when using a non-Chinese language only the GeoIP2 database is supported 286 | 287 | The values that can be set for this parameter can be found in the list of supported databases for GeoIP2 288 | 289 | ``` 290 | # NALI_LANG=en nali 1.1.1.1 291 | 1.1.1.1 [Australia] 292 | ``` 293 | 294 | ### Change directory 295 | 296 | Set the environment variable `NALI_HOME` to specify the working directory where the configuration file and database are stored. You can also use absolute paths in the configuration file to specify other database paths. 297 | 298 | Set the environment variable `NALI_CONFIG_HOME` to specify the configuration file directory and `NALI_DB_HOME` to specify the database file directory. 299 | 300 | If no environment variable is specified, the XDG specification will be used, with the configuration file directory in `$XDG_CONFIG_HOME/nali` and the database file directory in `$XDG_DATA_HOME/nali`. 301 | 302 | ``` 303 | set NALI_HOME=D:\nalidb 304 | 305 | or 306 | 307 | export NALI_HOME=/home/nali 308 | ``` 309 | 310 | ## Thanks 311 | 312 | - [纯真QQIP离线数据库](http://www.cz88.net) 313 | - [qqwry纯真数据库解析](https://github.com/yinheli/qqwry) 314 | - [ZX公网ipv6数据库](https://ip.zxinc.org/ipquery/) 315 | - [Geoip2 city数据库](https://www.maxmind.com/en/geoip2-precision-city-service) 316 | - [geoip2-golang解析器](https://github.com/oschwald/geoip2-golang) 317 | - [CDN provider数据库](https://github.com/SukkaLab/cdn) 318 | - [IPIP数据库](https://www.ipip.net/product/ip.html) 319 | - [IPIP数据库解析](https://github.com/ipipdotnet/ipdb-go) 320 | - [ip2region数据库](https://github.com/lionsoul2014/ip2region) 321 | - [IP2Location DB3 LITE](https://lite.ip2location.com/database/db3-ip-country-region-city) *use the IPv6 BIN as it contains both IPv4 & IPv6* 322 | - [Cobra CLI库](https://github.com/spf13/cobra) 323 | 324 | Thanks to JetBrains for the Open Source License 325 | 326 | 327 | 328 | 329 | 330 | ## Author 331 | 332 | **Nali** © [zu1k](https://github.com/zu1k), Released under the [MIT](./LICENSE) License.
333 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= 2 | github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 6 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 8 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 9 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 10 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 11 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 12 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 16 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 17 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 18 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 19 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 20 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 21 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 22 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 23 | github.com/ip2location/ip2location-go/v9 v9.7.0 h1:ipwl67HOWcrw+6GOChkEXcreRQR37NabqBd2ayYa4Q0= 24 | github.com/ip2location/ip2location-go/v9 v9.7.0/go.mod h1:MPLnsKxwQlvd2lBNcQCsLoyzJLDBFizuO67wXXdzoyI= 25 | github.com/ipipdotnet/ipdb-go v1.3.3 h1:GLSAW9ypLUd6EF9QNK2Uhxew9Jzs4XMJ9gOZEFnJm7U= 26 | github.com/ipipdotnet/ipdb-go v1.3.3/go.mod h1:yZ+8puwe3R37a/3qRftXo40nZVQbxYDLqls9o5foexs= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 h1:YeIGErDiB/fhmNsJy0cfjoT8XnRNT9hb19xZ4MvWQDU= 32 | github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= 33 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 34 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 35 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 36 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 37 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 38 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 39 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 40 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 41 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 42 | github.com/nxtrace/NTrace-core v1.3.2 h1:8aU/IQFmPnwbaWGVBIJHwwVIWk+roo+9+lG+U0OFZ+o= 43 | github.com/nxtrace/NTrace-core v1.3.2/go.mod h1:qCVsgSs982jw02BVjTtN8mjSg5OIXW9TaUdISQrMnTw= 44 | github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= 45 | github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= 46 | github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= 47 | github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= 48 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 49 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 51 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 53 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 54 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 55 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= 56 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= 57 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 58 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 59 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda h1:h+YpzUB/bGVJcLqW+d5GghcCmE/A25KbzjXvWJQi/+o= 60 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda/go.mod h1:MSotTrCv1PwoR8QgU1JurEx+lNNbtr25I+m0zbLyAGw= 61 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f h1:PF9WV5j/x6MT+x/sauUHd4objCvJbZb0wdxZkHSdd5A= 62 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f/go.mod h1:6Ff0ADODZ6S3gYepgZ2w7OqFrTqtFcfwDUhmm8jsUhs= 63 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f h1:1cJITU3JUI8qNS5T0BlXwANsVdyoJQHQ4hvOxbunPCw= 64 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f/go.mod h1:LyBTue+RWeyIfN3ZJ4wVxvDuvlGJtDgCLgCb6HCPgps= 65 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 66 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 67 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 68 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 69 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 70 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 71 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 72 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 73 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 74 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 75 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 76 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 77 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 78 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 80 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 81 | github.com/tsosunchia/powclient v0.1.5 h1:hpixFWoPbWSEC0zc9osSltyjtr1+SnhCueZVLkEpyyU= 82 | github.com/tsosunchia/powclient v0.1.5/go.mod h1:yNlzyq+w9llYZV+0q7nrX83ULy4ghq2mCjpTLJFJ2pg= 83 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 84 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 85 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 86 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 87 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= 88 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 89 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 90 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 91 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 92 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 93 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 96 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 97 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 98 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 99 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 100 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 101 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 103 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 104 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 105 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 106 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 107 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 108 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 109 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 111 | lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= 112 | lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 113 | --------------------------------------------------------------------------------