├── .dockerignore ├── .github └── workflows │ ├── build-binaries.yaml │ └── ci.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── asnlookup-utils │ ├── convert.go │ └── main.go └── asnlookup │ └── main.go ├── go.mod ├── go.sum ├── hack └── pull_rib.sh ├── pkg ├── binarytrie │ ├── array.go │ ├── array_test.go │ ├── arraymarshaling.go │ ├── arraymarshaling_test.go │ ├── errors.go │ ├── naive.go │ ├── naive_test.go │ ├── optimize.go │ ├── optimize_test.go │ ├── types.go │ └── util.go └── database │ ├── builder.go │ ├── database.go │ ├── errors.go │ └── mrt.go └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | build/ 3 | *.db 4 | Dockerfile 5 | README.md 6 | rib.*.bz2 7 | -------------------------------------------------------------------------------- /.github/workflows/build-binaries.yaml: -------------------------------------------------------------------------------- 1 | name: Build and upload binaries 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | build_and_archive: 8 | name: Build 9 | runs-on: ubuntu-24.04 10 | container: 11 | image: golang:1.24-alpine 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install dependencies 15 | run: apk add --no-cache git make 16 | - name: Build 17 | run: make release-all 18 | - name: Upload binaries 19 | uses: actions/upload-artifact@v4 20 | with: 21 | name: binaries 22 | path: asnlookup-*.tar.gz 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-24.04 11 | container: 12 | image: golang:1.24-alpine 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: apk add --no-cache make 17 | - name: Build 18 | run: make 19 | test: 20 | name: Test 21 | runs-on: ubuntu-24.04 22 | container: 23 | image: golang:1.24-alpine 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install dependencies 27 | run: apk add --no-cache gcc make musl-dev 28 | - name: Test 29 | run: make test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | build/ 3 | *.db 4 | rib.*.bz2 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine3.21 AS builder 2 | 3 | RUN apk add --no-cache bash curl make wget 4 | WORKDIR /go/src/asnlookup 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | COPY . . 8 | RUN make && make install 9 | 10 | FROM alpine:3.21 11 | COPY --from=builder /usr/local/bin/asnlookup /usr/local/bin/asnlookup-utils /usr/local/bin/ 12 | USER nobody 13 | ENV ASNLOOKUP_DB=/default.db 14 | ENTRYPOINT ["/usr/local/bin/asnlookup"] 15 | CMD [] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDDIR ?= build 2 | BINDIR ?= /usr/local/bin 3 | DOCKER_IMAGE ?= banviktor/asnlookup 4 | GOOS ?= $(shell go env GOOS) 5 | GOARCH ?= $(shell go env GOARCH) 6 | DATE = $(shell date -u +%Y%m%d) 7 | VERSION = $(shell git describe --always --dirty) 8 | 9 | .PHONY: build 10 | build: $(BUILDDIR)/asnlookup $(BUILDDIR)/asnlookup-utils 11 | 12 | .PHONY: clean 13 | clean: 14 | rm -f $(BUILDDIR)/* 15 | 16 | .PHONY: deps 17 | deps: 18 | go mod download 19 | 20 | $(BUILDDIR)/asnlookup: deps 21 | CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags '-extldflags "-static" -X github.com/viktb/asnlookup.Version=$(VERSION)' -o $(BUILDDIR)/asnlookup ./cmd/asnlookup 22 | 23 | $(BUILDDIR)/asnlookup-utils: deps 24 | CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags '-extldflags "-static" -X github.com/viktb/asnlookup.Version=$(VERSION)' -o $(BUILDDIR)/asnlookup-utils ./cmd/asnlookup-utils 25 | 26 | .PHONY: release 27 | release: 28 | $(MAKE) clean 29 | $(MAKE) 30 | tar -zcf asnlookup-$(GOOS)-$(GOARCH)-v$(VERSION).tar.gz -C $(BUILDDIR) . 31 | 32 | release-all: 33 | $(MAKE) release GOOS=linux GOARCH=amd64 34 | $(MAKE) release GOOS=linux GOARCH=arm64 35 | $(MAKE) release GOOS=linux GOARCH=386 36 | $(MAKE) release GOOS=darwin GOARCH=amd64 37 | $(MAKE) release GOOS=darwin GOARCH=arm64 38 | 39 | .PHONY: test 40 | test: 41 | go test -race ./... 42 | 43 | .PHONY: install 44 | install: 45 | cp -f $(BUILDDIR)/asnlookup $(BINDIR)/ 46 | cp -f $(BUILDDIR)/asnlookup-utils $(BINDIR)/ 47 | 48 | .PHONY: uninstall 49 | uninstall: 50 | rm -f $(BINDIR)/asnlookup $(BINDIR)/asnlookup-utils 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asnlookup 2 | CLI and Go package for fast, offline ASN lookups. 3 | 4 | A level compressed trie in array representation is used for achieving very fast 5 | lookups with a small memory footprint. The level compression is user-tunable 6 | between space-efficiency and time-efficiency. In LC-trie terms, the tuning 7 | adjusts the fill factor of the redundancy-enabled level compression. 8 | 9 | Due to the array-represented trie and binary marshaling, the inflation of a 10 | pre-converted database can be measured in tens of milliseconds. In other words 11 | the CLI tool can even be used for one-off lookups without any perceivable 12 | startup slowness. 13 | 14 | ``` 15 | time asnlookup --db ~/.asnlookup.db 8.8.8.8 16 | 15169 17 | 18 | real 0m0,027s 19 | user 0m0,025s 20 | sys 0m0,018s 21 | ``` 22 | 23 | ## Installation 24 | 25 | Using prebuilt binaries: 26 | ```shell 27 | curl -fsSL https://github.com/viktb/asnlookup/releases/download/v0.1.2/asnlookup-linux-amd64-v0.1.2.tar.gz | sudo tar -zx -C /usr/local/bin 28 | ``` 29 | 30 | From source: 31 | ```shell 32 | go install github.com/viktb/asnlookup/cmd/asnlookup@latest 33 | go install github.com/viktb/asnlookup/cmd/asnlookup-utils@latest 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### CLI 39 | 40 | 1. Download a fresh RIB dump, e.g. from http://archive.routeviews.org/: 41 | ```shell 42 | ./hack/pull_rib.sh 43 | ``` 44 | 2. Convert it to `asnlookup`'s own format: 45 | ```shell 46 | bzcat rib.*.bz2 | asnlookup-utils convert --input - --output /path/to/my.db 47 | ``` 48 | 3. Use it with `asnlookup`: 49 | ```shell 50 | asnlookup --db /path/to/my.db 8.8.8.8 51 | ``` 52 | or using the `ASNLOOKUP_DB` environment variable: 53 | ```shell 54 | export ASNLOOKUP_DB=/path/to/my.db 55 | asnlookup 8.8.8.8 56 | ``` 57 | 58 | #### Batch lookups 59 | 60 | You may also do batch lookups for IPs provided to standard input using the 61 | `--batch` flag: 62 | ```shell 63 | echo -ne '1.1.1.1\n8.8.8.8\n' | asnlookup --db ~/.asnlookup.db --batch 64 | 13335 65 | 15169 66 | ``` 67 | If you have tons of IPs to check, this will be a lot faster than inflating the 68 | multi-megabyte database each time `asnlookup` is invoked. 69 | 70 | ### Go package 71 | 72 | 1. Build a database 73 | 74 | * Manually: 75 | ```go 76 | builder := database.NewBuilder() 77 | 78 | _, prefix, _ := net.ParseCIDR("8.8.0.0/16") 79 | err := builder.InsertMapping(prefix, 420) 80 | if err != nil { 81 | panic(err) 82 | } 83 | 84 | db, err := builder.Build() 85 | if err != nil { 86 | panic(err) 87 | } 88 | ``` 89 | 90 | * Using an MRT file: 91 | ```go 92 | mrtFile, err := os.OpenFile("/path/to/file.mrt", os.O_RDONLY, 0) 93 | if err != nil { 94 | panic(err) 95 | } 96 | defer mrtFile.Close() 97 | 98 | builder := database.NewBuilder() 99 | if err = builder.ImportMRT(mrtFile); err != nil { 100 | panic(err) 101 | } 102 | 103 | db, err := builder.Build() 104 | if err != nil { 105 | panic(err) 106 | } 107 | ``` 108 | 109 | * Using a marshaled database (see `asnlookup-utils convert`): 110 | ```go 111 | dbFile, err := os.OpenFile("/path/to/file.db", os.O_RDONLY, 0) 112 | if err != nil { 113 | panic(err) 114 | } 115 | defer dbFile.Close() 116 | 117 | db, err := database.NewFromDump(dbFile) 118 | if err != nil { 119 | panic(err) 120 | } 121 | ``` 122 | 123 | 2. Look things up! 124 | ```go 125 | as, err := db.Lookup(net.ParseIP("8.8.8.8")) 126 | if err != nil { 127 | panic(err) 128 | } 129 | fmt.Println(as.Number) 130 | ``` 131 | -------------------------------------------------------------------------------- /cmd/asnlookup-utils/convert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/viktb/asnlookup/pkg/database" 9 | ) 10 | 11 | func executeConvertCommand(args []string) { 12 | convertCmd := flag.NewFlagSet("convert", flag.ExitOnError) 13 | 14 | inputFile := convertCmd.String("input", "", "input MRT `file` (required)") 15 | inShort := convertCmd.String("i", "", "input MRT `file` (shorthand)") 16 | inAlt := convertCmd.String("in", "", "input MRT `file` (shorthand)") 17 | 18 | outputFile := convertCmd.String("output", "", "output destination `file` (required)") 19 | outShort := convertCmd.String("o", "", "output destination `file` (shorthand)") 20 | outAlt := convertCmd.String("out", "", "output destination `file` (shorthand)") 21 | 22 | optimization := convertCmd.Int("optimization", 5, "set optimization `level` (1 - smallest, 8 - fastest)") 23 | 24 | if err := convertCmd.Parse(args); err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | // Handle flag aliases - use the first non-empty value 29 | inputPath := firstNonEmpty(*inputFile, *inShort, *inAlt) 30 | outputPath := firstNonEmpty(*outputFile, *outShort, *outAlt) 31 | 32 | // Check required flags 33 | if inputPath == "" { 34 | log.Fatal("Required flag --input not provided") 35 | } 36 | if outputPath == "" { 37 | log.Fatal("Required flag --output not provided") 38 | } 39 | 40 | // Execute the convert action 41 | err := doConvert(inputPath, outputPath, *optimization) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | 47 | func firstNonEmpty(values ...string) string { 48 | for _, v := range values { 49 | if v != "" { 50 | return v 51 | } 52 | } 53 | return "" 54 | } 55 | 56 | func doConvert(inputPath, outputPath string, optimization int) error { 57 | var err error 58 | 59 | if optimization < 1 || optimization > 8 { 60 | log.Fatalf("Optimization level must be between 1 and 8") 61 | } 62 | 63 | // Initialize input. 64 | inputFile := os.Stdin 65 | if inputPath != "-" { 66 | inputFile, err = os.OpenFile(inputPath, os.O_RDONLY, 0) 67 | if err != nil { 68 | log.Fatalf("Failed to open input file: %v", err) 69 | } 70 | defer inputFile.Close() 71 | } 72 | 73 | // Initialize output. 74 | outputFile := os.Stdout 75 | if outputPath != "-" { 76 | outputFile, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 77 | if err != nil { 78 | log.Fatalf("Failed to create output file: %v", err) 79 | } 80 | defer outputFile.Close() 81 | } 82 | 83 | // Build database. 84 | builder := database.NewBuilder() 85 | err = builder.ImportMRT(inputFile) 86 | if err != nil { 87 | log.Fatalf("Failed to import: %v", err) 88 | } 89 | builder.SetFillFactor(optimizationLevelToFillFactor(optimization)) 90 | db, err := builder.Build() 91 | if err != nil { 92 | log.Fatalf("Failed to build database: %v", err) 93 | } 94 | 95 | // Dump optimized database. 96 | data, err := db.MarshalBinary() 97 | if err != nil { 98 | log.Fatalf("Failed to marshal database: %v", err) 99 | } 100 | _, err = outputFile.Write(data) 101 | if err != nil { 102 | log.Fatalf("Failed to write database to file: %v", err) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func optimizationLevelToFillFactor(level int) float32 { 109 | return float32(9-level) * 0.125 110 | } 111 | -------------------------------------------------------------------------------- /cmd/asnlookup-utils/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/viktb/asnlookup" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) < 2 { 12 | printUsage() 13 | os.Exit(1) 14 | } 15 | 16 | command := os.Args[1] 17 | args := os.Args[2:] 18 | 19 | switch command { 20 | case "convert": 21 | executeConvertCommand(args) 22 | case "version": 23 | printVersion() 24 | case "help": 25 | printUsage() 26 | default: 27 | fmt.Printf("Unknown command: %s\n", command) 28 | printUsage() 29 | os.Exit(1) 30 | } 31 | } 32 | 33 | func printUsage() { 34 | fmt.Print(`asnlookup-utils - utilities for asnlookup 35 | 36 | USAGE: 37 | asnlookup-utils command [command flags] [arguments...] 38 | 39 | COMMANDS: 40 | convert converts an MRT file to an asnlookup database 41 | version prints the version 42 | help prints this help message 43 | `) 44 | } 45 | 46 | func printVersion() { 47 | fmt.Println(fmt.Sprintf("asnlookup-utils %s", asnlookup.Version)) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/asnlookup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | 11 | "github.com/viktb/asnlookup" 12 | "github.com/viktb/asnlookup/pkg/database" 13 | ) 14 | 15 | const ( 16 | databaseFilenameEnvVar = "ASNLOOKUP_DB" 17 | ) 18 | 19 | func main() { 20 | // Initialize flags. 21 | dbFilename := flag.String( 22 | "db", 23 | os.Getenv(databaseFilenameEnvVar), 24 | fmt.Sprintf("database file to use (env: %s)", databaseFilenameEnvVar), 25 | ) 26 | batch := flag.Bool( 27 | "batch", 28 | false, 29 | "process IPs from stdin", 30 | ) 31 | version := flag.Bool( 32 | "version", 33 | false, 34 | "print version information and exit", 35 | ) 36 | flag.Usage = func() { 37 | fmt.Printf("Usage: asnlookup [OPTION]... [IP]\n") 38 | flag.PrintDefaults() 39 | } 40 | 41 | // Check provided arguments. 42 | flag.Parse() 43 | if *version { 44 | fmt.Printf("asnlookup %s\n", asnlookup.Version) 45 | os.Exit(0) 46 | } 47 | if !*batch && flag.NArg() != 1 { 48 | fmt.Println("Missing argument: IP") 49 | flag.Usage() 50 | os.Exit(1) 51 | } 52 | if *dbFilename == "" { 53 | fmt.Println("Missing required option: db") 54 | flag.Usage() 55 | os.Exit(1) 56 | } 57 | 58 | // Inflate database. 59 | dbFile, err := os.OpenFile(*dbFilename, os.O_RDONLY, 0) 60 | if err != nil { 61 | fmt.Println("Failed to open database file:", err) 62 | os.Exit(1) 63 | } 64 | defer dbFile.Close() 65 | db, err := database.NewFromDump(dbFile) 66 | if err != nil { 67 | fmt.Println("Failed to parse database file:", err) 68 | os.Exit(1) 69 | } 70 | 71 | // Do the lookup(s). 72 | if *batch { 73 | r := bufio.NewScanner(os.Stdin) 74 | for r.Scan() { 75 | ip := net.ParseIP(r.Text()) 76 | lookup(db, &ip) 77 | } 78 | } else { 79 | ip := net.ParseIP(flag.Arg(0)) 80 | lookup(db, &ip) 81 | } 82 | } 83 | 84 | func lookup(db *database.Database, ip *net.IP) { 85 | as, err := db.Lookup(*ip) 86 | if errors.Is(err, database.ErrASNotFound) { 87 | fmt.Println("not found") 88 | return 89 | } 90 | if err != nil { 91 | fmt.Println("error:", err) 92 | return 93 | } 94 | fmt.Println(as.Number) 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/viktb/asnlookup 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/stretchr/testify v1.10.0 7 | github.com/viktb/go-mrt v0.0.0-20230515165434-0ce2ad0d8984 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 14 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | github.com/viktb/go-mrt v0.0.0-20230515165434-0ce2ad0d8984 h1:ZDmOmWDu2vOiHOEbM8IHqSpJ9x3RFeMCOUvufN+W/4g= 16 | github.com/viktb/go-mrt v0.0.0-20230515165434-0ce2ad0d8984/go.mod h1:XPLL/opj8vE3zTxhacRA5oN/EP89qqcD7QusT0VtSm8= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /hack/pull_rib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | base_url="http://archive.routeviews.org/bgpdata" 5 | filename=$(curl -fsSL "${base_url}/$(date +%Y.%m)/RIBS/" | grep -oE 'rib\.[0-9]{8}\.[0-9]{4}\.bz2' | tail -n 1) 6 | curl -fSLO "${base_url}/$(date +%Y.%m)/RIBS/${filename}" 7 | -------------------------------------------------------------------------------- /pkg/binarytrie/array.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // ArrayTrie represents a trie in a space-efficient array format. 9 | type ArrayTrie struct { 10 | nodes []arrayTrieNode 11 | skippedBits map[int]uint32 12 | } 13 | 14 | type arrayTrieNode struct { 15 | branchingFactor uint8 16 | skipValue uint8 17 | childrenOffset uint32 18 | value uint32 19 | } 20 | 21 | // Insert implements Trie. 22 | func (t *ArrayTrie) Insert(*net.IPNet, uint32) error { 23 | return ErrTrieImmutable 24 | } 25 | 26 | // Lookup implements Trie. 27 | func (t *ArrayTrie) Lookup(ip net.IP) (value uint32, err error) { 28 | var bitPosition, index, nextIndex int 29 | var skippedBits uint32 30 | 31 | for { 32 | if t.nodes[index].value != 0 { 33 | value = t.nodes[index].value 34 | } 35 | if t.nodes[index].isLeaf() { 36 | break 37 | } 38 | 39 | skippedBits = extractBits(ip, bitPosition, int(t.nodes[index].skipValue)) 40 | bitPosition += int(t.nodes[index].skipValue) 41 | nextIndex = index + int(t.nodes[index].childrenOffset) + int(extractBits(ip, bitPosition, int(t.nodes[index].branchingFactor))) 42 | bitPosition += int(t.nodes[index].branchingFactor) 43 | 44 | if t.nodes[index].skipValue > 0 { 45 | if expected, ok := t.skippedBits[nextIndex]; !ok || expected != skippedBits { 46 | break 47 | } 48 | } 49 | index = nextIndex 50 | } 51 | if value == 0 { 52 | return 0, ErrValueNotFound 53 | } 54 | return 55 | } 56 | 57 | // NewArrayTrie returns an empty ArrayTrie. 58 | // 59 | // This is rarely useful, as an ArrayTrie does not support inserts. 60 | func NewArrayTrie() *ArrayTrie { 61 | return &ArrayTrie{ 62 | nodes: make([]arrayTrieNode, 0), 63 | skippedBits: make(map[int]uint32), 64 | } 65 | } 66 | 67 | // NewArrayTrieFromNaiveTrie creates an ArrayTrie from a NaiveTrie. 68 | func NewArrayTrieFromNaiveTrie(nt *NaiveTrie) *ArrayTrie { 69 | at := NewArrayTrie() 70 | at.nodes = make([]arrayTrieNode, 0, nt.allocatedSize()) 71 | 72 | nodeQueue := []*naiveTrieNode{nt.root} 73 | for len(nodeQueue) > 0 { 74 | batchIndex := len(at.nodes) 75 | batchSize := len(nodeQueue) 76 | for _, nNode := range nodeQueue { 77 | if nNode == nil { 78 | at.nodes = append(at.nodes, arrayTrieNode{}) 79 | continue 80 | } 81 | 82 | i := len(at.nodes) 83 | at.nodes = append(at.nodes, arrayTrieNode{ 84 | branchingFactor: nNode.branchingFactor, 85 | skipValue: nNode.skipValue, 86 | value: nNode.value, 87 | }) 88 | if nNode.parent != nil && nNode.parent.skipValue > 0 { 89 | at.skippedBits[i] = nNode.skippedBits 90 | } 91 | if !nNode.isLeaf() { 92 | at.nodes[i].childrenOffset = uint32(batchIndex - i + len(nodeQueue)) 93 | nodeQueue = append(nodeQueue, nNode.children...) 94 | } 95 | } 96 | nodeQueue = nodeQueue[batchSize:] 97 | } 98 | 99 | return at 100 | } 101 | 102 | func (n *arrayTrieNode) isLeaf() bool { 103 | return n.branchingFactor == 0 104 | } 105 | 106 | // String implements fmt.Stringer. 107 | func (t *ArrayTrie) String() string { 108 | str := "#\tBF\tSV\tCO\tValue\n" 109 | for i, n := range t.nodes { 110 | suffix := "" 111 | if bits, ok := t.skippedBits[i]; ok { 112 | suffix = fmt.Sprintf(" (skipped: %0*b)", n.skipValue, bits) 113 | } 114 | str += fmt.Sprintf("%d\t%d\t%d\t%d\t%d%s\n", i, n.branchingFactor, n.skipValue, n.childrenOffset, n.value, suffix) 115 | } 116 | str += fmt.Sprintf("%v\n", t.skippedBits) 117 | return str 118 | } 119 | -------------------------------------------------------------------------------- /pkg/binarytrie/array_test.go: -------------------------------------------------------------------------------- 1 | package binarytrie_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEmptyArrayTrieLookup(t *testing.T) { 8 | trie, testCases := newEmptyNaiveTrie() 9 | testLookup(t, trie.ToArrayTrie(), testCases) 10 | } 11 | 12 | func TestTrivialArrayTrieLookup(t *testing.T) { 13 | trie, testCases := newTrivialNaiveTrie(t) 14 | testLookup(t, trie.ToArrayTrie(), testCases) 15 | } 16 | 17 | func TestPopulatedArrayTrieLookup(t *testing.T) { 18 | trie, testCases := newPopulatedNaiveTrie(t) 19 | testLookup(t, trie.ToArrayTrie(), testCases) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/binarytrie/arraymarshaling.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | const ( 8 | arrayTrieMarshalHeader = "github.com/banviktor/asnlookup/pkg/binarytrie\x00ArrayTrie\x00" 9 | arrayTrieMarshalVersion = uint8(1) 10 | ) 11 | 12 | // MarshalBinary implements encoding.BinaryMarshaler. 13 | func (t *ArrayTrie) MarshalBinary() (data []byte, err error) { 14 | var i int 15 | data = make([]byte, len(arrayTrieMarshalHeader)+1+8+len(t.nodes)*10+8+len(t.skippedBits)*12) 16 | 17 | // Write header. 18 | copy(data, arrayTrieMarshalHeader) 19 | i += len(arrayTrieMarshalHeader) 20 | data[i] = arrayTrieMarshalVersion 21 | i += 1 22 | 23 | // Write nodes. 24 | binary.LittleEndian.PutUint64(data[i:i+8], uint64(len(t.nodes))) 25 | i += 8 26 | for _, n := range t.nodes { 27 | bN, err := n.MarshalBinary() 28 | if err != nil { 29 | return nil, err 30 | } 31 | copy(data[i:i+10], bN) 32 | i += 10 33 | } 34 | 35 | // Write skipped bits information. 36 | binary.LittleEndian.PutUint64(data[i:i+8], uint64(len(t.skippedBits))) 37 | i += 8 38 | for k, v := range t.skippedBits { 39 | binary.LittleEndian.PutUint64(data[i:i+8], uint64(k)) 40 | i += 8 41 | binary.LittleEndian.PutUint32(data[i:i+4], v) 42 | i += 4 43 | } 44 | 45 | return 46 | } 47 | 48 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 49 | func (t *ArrayTrie) UnmarshalBinary(data []byte) error { 50 | var i int 51 | 52 | // Check header. 53 | if string(data[:len(arrayTrieMarshalHeader)]) != arrayTrieMarshalHeader { 54 | return ErrInvalidFormat 55 | } 56 | i += len(arrayTrieMarshalHeader) 57 | if data[i] != arrayTrieMarshalVersion { 58 | return ErrInvalidFormat 59 | } 60 | i += 1 61 | 62 | // Populate nodes. 63 | nodeCount := binary.LittleEndian.Uint64(data[i : i+8]) 64 | i += 8 65 | t.nodes = make([]arrayTrieNode, nodeCount) 66 | for j := uint64(0); j < nodeCount; j++ { 67 | if err := t.nodes[j].UnmarshalBinary(data[i : i+10]); err != nil { 68 | return err 69 | } 70 | i += 10 71 | } 72 | 73 | // Populate skipped bits information. 74 | skippedBitCount := binary.LittleEndian.Uint64(data[i : i+8]) 75 | i += 8 76 | t.skippedBits = make(map[int]uint32, skippedBitCount) 77 | for j := uint64(0); j < skippedBitCount; j++ { 78 | k := int(binary.LittleEndian.Uint64(data[i : i+8])) 79 | i += 8 80 | v := binary.LittleEndian.Uint32(data[i : i+4]) 81 | i += 4 82 | t.skippedBits[k] = v 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // MarshalBinary implements encoding.BinaryMarshaler. 89 | func (n *arrayTrieNode) MarshalBinary() (data []byte, err error) { 90 | data = make([]byte, 10) 91 | data[0] = n.branchingFactor 92 | data[1] = n.skipValue 93 | binary.LittleEndian.PutUint32(data[2:6], n.childrenOffset) 94 | binary.LittleEndian.PutUint32(data[6:10], n.value) 95 | return 96 | } 97 | 98 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 99 | func (n *arrayTrieNode) UnmarshalBinary(data []byte) error { 100 | n.branchingFactor = data[0] 101 | n.skipValue = data[1] 102 | n.childrenOffset = binary.LittleEndian.Uint32(data[2:6]) 103 | n.value = binary.LittleEndian.Uint32(data[6:10]) 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/binarytrie/arraymarshaling_test.go: -------------------------------------------------------------------------------- 1 | package binarytrie_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | . "github.com/viktb/asnlookup/pkg/binarytrie" 9 | ) 10 | 11 | func TestEmptyMarshaledArrayTrieLookup(t *testing.T) { 12 | trie, testCases := newEmptyNaiveTrie() 13 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 14 | arrayTrie := trie.ToArrayTrie() 15 | 16 | buf, err := arrayTrie.MarshalBinary() 17 | assert.NoError(t, err, "MarshalBinary should not error") 18 | 19 | newTrie := &ArrayTrie{} 20 | err = newTrie.UnmarshalBinary(buf) 21 | assert.NoError(t, err, "UnmarshalBinary should not error") 22 | 23 | testLookup(t, newTrie, testCases) 24 | } 25 | 26 | func TestTrivialMarshaledArrayTrieLookup(t *testing.T) { 27 | trie, testCases := newTrivialNaiveTrie(t) 28 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 29 | arrayTrie := trie.ToArrayTrie() 30 | 31 | buf, err := arrayTrie.MarshalBinary() 32 | assert.NoError(t, err, "MarshalBinary should not error") 33 | 34 | newTrie := &ArrayTrie{} 35 | err = newTrie.UnmarshalBinary(buf) 36 | assert.NoError(t, err, "UnmarshalBinary should not error") 37 | 38 | testLookup(t, newTrie, testCases) 39 | } 40 | 41 | func TestPopulatedMarshaledArrayTrieLookup(t *testing.T) { 42 | trie, testCases := newPopulatedNaiveTrie(t) 43 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 44 | arrayTrie := trie.ToArrayTrie() 45 | 46 | buf, err := arrayTrie.MarshalBinary() 47 | assert.NoError(t, err, "MarshalBinary should not error") 48 | 49 | newTrie := &ArrayTrie{} 50 | err = newTrie.UnmarshalBinary(buf) 51 | assert.NoError(t, err, "UnmarshalBinary should not error") 52 | 53 | testLookup(t, newTrie, testCases) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/binarytrie/errors.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrInvalidIPAddress IP address is invalid. 9 | ErrInvalidIPAddress = errors.New("invalid IP address") 10 | // ErrTrieImmutable Trie is immutable. 11 | ErrTrieImmutable = errors.New("trie is immutable") 12 | // ErrValueNotFound value was not found. 13 | ErrValueNotFound = errors.New("value not found") 14 | // ErrInvalidFormat invalid marshaled input. 15 | ErrInvalidFormat = errors.New("invalid format") 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/binarytrie/naive.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type NaiveTrie struct { 8 | root *naiveTrieNode 9 | mutable bool 10 | } 11 | 12 | type naiveTrieNode struct { 13 | skipValue uint8 14 | skippedBits uint32 15 | branchingFactor uint8 16 | parent *naiveTrieNode 17 | children []*naiveTrieNode 18 | value uint32 19 | } 20 | 21 | // NewNaiveTrie creates an empty NaiveTrie. 22 | func NewNaiveTrie() *NaiveTrie { 23 | return &NaiveTrie{ 24 | root: &naiveTrieNode{}, 25 | mutable: true, 26 | } 27 | } 28 | 29 | // Insert implements Trie. 30 | func (t *NaiveTrie) Insert(ipNet *net.IPNet, value uint32) error { 31 | if !t.mutable { 32 | return ErrTrieImmutable 33 | } 34 | 35 | prefix, prefixSize, err := parseIpNet(ipNet) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | currentNode := t.root 41 | bitPosition := 0 42 | for { 43 | if currentNode.branchingFactor == 0 { 44 | currentNode.branchingFactor = 1 45 | currentNode.children = make([]*naiveTrieNode, 2) 46 | } 47 | 48 | bit := extractBits(prefix, bitPosition, 1) 49 | bitPosition++ 50 | if currentNode.children[bit] == nil { 51 | currentNode.children[bit] = &naiveTrieNode{parent: currentNode} 52 | } 53 | currentNode = currentNode.children[bit] 54 | 55 | if bitPosition >= prefixSize { 56 | break 57 | } 58 | } 59 | currentNode.value = value 60 | return nil 61 | } 62 | 63 | // Lookup implements Trie. 64 | func (t *NaiveTrie) Lookup(ip net.IP) (value uint32, err error) { 65 | ip = ip.To16() 66 | if ip == nil { 67 | return 0, ErrInvalidIPAddress 68 | } 69 | 70 | currentNode := t.root 71 | bitPosition := 0 72 | for { 73 | if currentNode.value != 0 { 74 | value = currentNode.value 75 | } 76 | if currentNode.isLeaf() { 77 | break 78 | } 79 | 80 | skippedBits := extractBits(ip, bitPosition, int(currentNode.skipValue)) 81 | bitPosition += int(currentNode.skipValue) 82 | prefix := extractBits(ip, bitPosition, int(currentNode.branchingFactor)) 83 | bitPosition += int(currentNode.branchingFactor) 84 | 85 | nextNode := currentNode.children[prefix] 86 | if nextNode == nil || nextNode.skippedBits != skippedBits { 87 | break 88 | } 89 | currentNode = nextNode 90 | } 91 | if value == 0 { 92 | return 0, ErrValueNotFound 93 | } 94 | return 95 | } 96 | 97 | // ToArrayTrie creates an identical ArrayTrie. 98 | func (t *NaiveTrie) ToArrayTrie() *ArrayTrie { 99 | return NewArrayTrieFromNaiveTrie(t) 100 | } 101 | 102 | func (t *NaiveTrie) allocatedSize() int { 103 | return t.root.allocatedSize() 104 | } 105 | 106 | func (n *naiveTrieNode) isLeaf() bool { 107 | return n.branchingFactor == 0 108 | } 109 | 110 | func (n *naiveTrieNode) allocatedSize() int { 111 | count := 1 112 | for _, child := range n.children { 113 | if child != nil { 114 | count += child.allocatedSize() 115 | } else { 116 | count++ 117 | } 118 | } 119 | return count 120 | } 121 | -------------------------------------------------------------------------------- /pkg/binarytrie/naive_test.go: -------------------------------------------------------------------------------- 1 | package binarytrie_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | . "github.com/viktb/asnlookup/pkg/binarytrie" 11 | ) 12 | 13 | type testCase struct { 14 | ip string 15 | asn uint32 16 | err error 17 | } 18 | 19 | func TestEmptyNaiveTrieLookup(t *testing.T) { 20 | trie, testCases := newEmptyNaiveTrie() 21 | testLookup(t, trie, testCases) 22 | } 23 | 24 | func TestTrivialNaiveTrieLookup(t *testing.T) { 25 | trie, testCases := newTrivialNaiveTrie(t) 26 | testLookup(t, trie, testCases) 27 | } 28 | 29 | func TestPopulatedNaiveTrieLookup(t *testing.T) { 30 | trie, testCases := newPopulatedNaiveTrie(t) 31 | testLookup(t, trie, testCases) 32 | } 33 | 34 | func newEmptyNaiveTrie() (*NaiveTrie, []testCase) { 35 | trie := NewNaiveTrie() 36 | 37 | testCases := []testCase{ 38 | {"0.0.0.0", 0, ErrValueNotFound}, 39 | {"255.255.255.255", 0, ErrValueNotFound}, 40 | } 41 | 42 | return trie, testCases 43 | } 44 | 45 | func newTrivialNaiveTrie(t *testing.T) (*NaiveTrie, []testCase) { 46 | trie := NewNaiveTrie() 47 | _, ipNet, _ := net.ParseCIDR("0.0.0.0/0") 48 | require.NoError(t, trie.Insert(ipNet, 42)) 49 | 50 | testCases := []testCase{ 51 | {"0.0.0.0", 42, nil}, 52 | {"255.255.255.255", 42, nil}, 53 | } 54 | 55 | return trie, testCases 56 | } 57 | 58 | func newPopulatedNaiveTrie(t *testing.T) (*NaiveTrie, []testCase) { 59 | trie := NewNaiveTrie() 60 | testData := []struct { 61 | net string 62 | asn uint32 63 | }{ 64 | {"192.168.1.0/24", 999}, 65 | {"0.0.0.0/2", 200}, 66 | {"128.0.0.0/2", 210}, 67 | {"160.0.0.0/3", 2101}, 68 | {"160.0.0.0/3", 2101}, // duplicate entry on purpose 69 | {"192.0.0.0/3", 211}, 70 | {"224.0.0.0/3", 211}, 71 | } 72 | for _, td := range testData { 73 | _, ipNet, _ := net.ParseCIDR(td.net) 74 | require.NoError(t, trie.Insert(ipNet, td.asn)) 75 | } 76 | 77 | testCases := []testCase{ 78 | {"0.0.0.0", 200, nil}, 79 | {"32.128.128.128", 200, nil}, 80 | {"63.255.255.255", 200, nil}, 81 | {"64.0.0.0", 0, ErrValueNotFound}, 82 | {"96.128.128.128", 0, ErrValueNotFound}, 83 | {"127.255.255.255", 0, ErrValueNotFound}, 84 | {"128.0.0.0", 210, nil}, 85 | {"159.255.255.255", 210, nil}, 86 | {"160.128.128.128", 2101, nil}, 87 | {"191.255.255.255", 2101, nil}, 88 | {"192.0.0.0", 211, nil}, 89 | {"192.168.0.255", 211, nil}, 90 | {"192.168.1.0", 999, nil}, 91 | {"192.168.1.128", 999, nil}, 92 | {"192.168.1.255", 999, nil}, 93 | {"192.168.2.0", 211, nil}, 94 | {"224.128.128.128", 211, nil}, 95 | {"255.255.255.255", 211, nil}, 96 | } 97 | 98 | return trie, testCases 99 | } 100 | 101 | func testLookup(t *testing.T, trie Trie, testCases []testCase) { 102 | for _, tc := range testCases { 103 | asn, err := trie.Lookup(net.ParseIP(tc.ip)) 104 | if tc.err != nil && assert.Error(t, err, "%s should have error", tc.ip) { 105 | assert.Equal(t, tc.err, err) 106 | } 107 | assert.Equal(t, int(tc.asn), int(asn), "%s expected AS%d, actual: AS%d", tc.ip, tc.asn, asn) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/binarytrie/optimize.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | // Optimize performs level compression and path compression on the trie. 4 | // 5 | // This operation makes the trie immutable. 6 | func (t *NaiveTrie) Optimize(fillFactor float32) error { 7 | if !t.mutable { 8 | return ErrTrieImmutable 9 | } 10 | 11 | t.root.propagateValues(0) 12 | t.root.removeRedundancies() 13 | t.root.compressLevels(fillFactor) 14 | t.root.compressPaths(nil, 0, 0) 15 | t.mutable = false 16 | return nil 17 | } 18 | 19 | func (n *naiveTrieNode) propagateValues(value uint32) { 20 | for i, child := range n.children { 21 | if child == nil { 22 | n.children[i] = &naiveTrieNode{parent: n} 23 | } 24 | } 25 | if n.value == 0 { 26 | n.value = value 27 | } 28 | 29 | // Preorder traversal. 30 | for _, child := range n.children { 31 | child.propagateValues(n.value) 32 | } 33 | } 34 | 35 | func (n *naiveTrieNode) removeRedundancies() { 36 | // Postorder traversal. 37 | for _, child := range n.children { 38 | child.removeRedundancies() 39 | } 40 | 41 | if n.isLeaf() { 42 | return 43 | } 44 | if n.children[0].isLeaf() && n.children[1].isLeaf() && n.children[0].value == n.children[1].value { 45 | n.value = n.children[0].value 46 | n.branchingFactor = 0 47 | n.children = nil 48 | } 49 | } 50 | 51 | func (n *naiveTrieNode) compressPaths(firstNode *naiveTrieNode, depth uint8, prefix uint32) { 52 | var nextNode *naiveTrieNode 53 | var nextNodeIndex int 54 | for i, child := range n.children { 55 | if child.isLeaf() { 56 | if child.value == n.value { 57 | // Ignore trivial leaves introduced by normalization. 58 | continue 59 | } 60 | // Any other leaf breaks the path. 61 | nextNode = nil 62 | break 63 | } 64 | if nextNode != nil { 65 | // The path ends if there is more than 1 nontrivial child. 66 | nextNode = nil 67 | break 68 | } 69 | nextNode = child 70 | nextNodeIndex = i 71 | } 72 | 73 | if firstNode != nil && (nextNode == nil || depth+n.branchingFactor > 32) { 74 | // The path ends. 75 | firstNode.skipValue = depth 76 | firstNode.children = n.children 77 | firstNode.branchingFactor = n.branchingFactor 78 | for _, child := range n.children { 79 | child.skippedBits = prefix 80 | child.parent = firstNode 81 | } 82 | } else if firstNode != nil { 83 | // The path continues. 84 | nextNode.compressPaths(firstNode, depth+n.branchingFactor, (prefix<= 31 || float32(realNodeCount)/float32(len(nextNodes)) < fillFactor { 126 | break 127 | } 128 | depth++ 129 | nodes = nextNodes 130 | } 131 | 132 | if depth > 1 { 133 | n.branchingFactor = depth 134 | n.children = nodes 135 | } 136 | 137 | for _, child := range n.children { 138 | child.compressLevels(fillFactor) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /pkg/binarytrie/optimize_test.go: -------------------------------------------------------------------------------- 1 | package binarytrie_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEmptyOptimizedNaiveTrieLookup(t *testing.T) { 11 | trie, testCases := newEmptyNaiveTrie() 12 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 13 | testLookup(t, trie, testCases) 14 | } 15 | 16 | func TestTrivialOptimizedNaiveTrieLookup(t *testing.T) { 17 | trie, testCases := newTrivialNaiveTrie(t) 18 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 19 | testLookup(t, trie, testCases) 20 | } 21 | 22 | func TestPopulatedOptimizedNaiveTrieLookup(t *testing.T) { 23 | trie, testCases := newPopulatedNaiveTrie(t) 24 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 25 | testLookup(t, trie, testCases) 26 | } 27 | 28 | func TestEmptyOptimizedArrayTrieLookup(t *testing.T) { 29 | trie, testCases := newEmptyNaiveTrie() 30 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 31 | arrayTrie := trie.ToArrayTrie() 32 | 33 | fmt.Println(arrayTrie.String()) 34 | testLookup(t, arrayTrie, testCases) 35 | } 36 | 37 | func TestTrivialOptimizedArrayTrieLookup(t *testing.T) { 38 | trie, testCases := newTrivialNaiveTrie(t) 39 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 40 | arrayTrie := trie.ToArrayTrie() 41 | 42 | fmt.Println(arrayTrie.String()) 43 | testLookup(t, arrayTrie, testCases) 44 | } 45 | 46 | func TestPopulatedOptimizedArrayTrieLookup(t *testing.T) { 47 | trie, testCases := newPopulatedNaiveTrie(t) 48 | assert.NoError(t, trie.Optimize(0.5), "Optimize should not error") 49 | arrayTrie := trie.ToArrayTrie() 50 | 51 | fmt.Println(arrayTrie.String()) 52 | testLookup(t, arrayTrie, testCases) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/binarytrie/types.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type Trie interface { 8 | // Insert inserts an IP network - value mapping into the trie. 9 | Insert(*net.IPNet, uint32) error 10 | // Lookup returns a value for the given IP address. 11 | Lookup(ip net.IP) (uint32, error) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/binarytrie/util.go: -------------------------------------------------------------------------------- 1 | package binarytrie 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func parseIpNet(ipNet *net.IPNet) (net.IP, int, error) { 8 | ip := ipNet.IP.To16() 9 | if ip == nil { 10 | return nil, 0, ErrInvalidIPAddress 11 | } 12 | subnetSize, bits := ipNet.Mask.Size() 13 | if bits == net.IPv4len*8 { 14 | subnetSize += (net.IPv6len - net.IPv4len) * 8 15 | } 16 | return ip, subnetSize, nil 17 | } 18 | 19 | func extractBits(ip net.IP, position, length int) uint32 { 20 | if length < 1 || length > 32 || position < 0 || position+length-1 >= len(ip)*8 { 21 | return 0 22 | } 23 | 24 | lastBit := position + length - 1 25 | firstByte := position / 8 26 | lastByte := lastBit / 8 27 | 28 | // Extract the right bytes. 29 | rightShift := 7 - lastBit%8 30 | bits := uint32(ip[lastByte]) >> rightShift 31 | for i := 1; firstByte <= lastByte-i; i++ { 32 | bits |= uint32(ip[lastByte-i]) << (8*i - rightShift) 33 | } 34 | 35 | // Mask unnecessary bits. 36 | bits &= uint32(0xFFFFFFFF) >> (32 - length) 37 | 38 | return bits 39 | } 40 | -------------------------------------------------------------------------------- /pkg/database/builder.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | 8 | "github.com/viktb/go-mrt" 9 | 10 | "github.com/viktb/asnlookup/pkg/binarytrie" 11 | ) 12 | 13 | // Builder is a helper for constructing a Database. 14 | type Builder struct { 15 | prototype *binarytrie.NaiveTrie 16 | fillFactor float32 17 | } 18 | 19 | // NewBuilder creates a Builder. 20 | func NewBuilder() *Builder { 21 | return &Builder{ 22 | prototype: binarytrie.NewNaiveTrie(), 23 | fillFactor: 0.5, 24 | } 25 | } 26 | 27 | // InsertMapping stores an IP prefix - AutonomousSystem mapping. 28 | func (b *Builder) InsertMapping(ipNet *net.IPNet, asn uint32) error { 29 | err := b.prototype.Insert(ipNet, asn) 30 | if err != nil { 31 | return err 32 | } 33 | return nil 34 | } 35 | 36 | // ImportMRT imports records from an MRT stream. 37 | func (b *Builder) ImportMRT(input io.Reader) error { 38 | r := mrt.NewReader(input) 39 | 40 | for { 41 | record, err := r.Next() 42 | if err == io.EOF { 43 | break 44 | } 45 | if err != nil { 46 | return fmt.Errorf("failed to parse MRT record: %v", err) 47 | } 48 | 49 | rib, ok := record.(*mrt.TableDumpV2RIB) 50 | if !ok || isNullMask(rib.Prefix.Mask) { 51 | continue 52 | } 53 | 54 | prefix, asn, err := mrtRIBToMapping(rib) 55 | if err != nil { 56 | continue 57 | } 58 | 59 | err = b.InsertMapping(prefix, asn) 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // SetFillFactor sets the fill factor parameter for the optimization phase. 69 | func (b *Builder) SetFillFactor(fillFactor float32) { 70 | b.fillFactor = fillFactor 71 | } 72 | 73 | // Build builds the Database instance. 74 | func (b *Builder) Build() (*Database, error) { 75 | err := b.prototype.Optimize(b.fillFactor) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return &Database{ 80 | mappings: b.prototype.ToArrayTrie(), 81 | }, nil 82 | } 83 | 84 | func isNullMask(mask net.IPMask) bool { 85 | for _, b := range mask { 86 | if b != 0 { 87 | return false 88 | } 89 | } 90 | return true 91 | } 92 | -------------------------------------------------------------------------------- /pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net" 8 | 9 | "github.com/viktb/asnlookup/pkg/binarytrie" 10 | ) 11 | 12 | // AutonomousSystem represents an Autonomous System on the Internet. 13 | type AutonomousSystem struct { 14 | // Number (aka ASN) is the unique identifier for an Autonomous System. 15 | Number uint32 16 | } 17 | 18 | // Database stores mappings between IP addresses and Autonomous Systems. 19 | type Database struct { 20 | mappings *binarytrie.ArrayTrie 21 | } 22 | 23 | // NewFromDump creates a Database from a binary dump. 24 | func NewFromDump(r io.Reader) (*Database, error) { 25 | d := &Database{ 26 | mappings: binarytrie.NewArrayTrie(), 27 | } 28 | data, err := io.ReadAll(r) 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to read: %v", err) 31 | } 32 | 33 | if err = d.UnmarshalBinary(data); err != nil { 34 | return nil, fmt.Errorf("failed to restore dump: %v", err) 35 | } 36 | return d, nil 37 | } 38 | 39 | // Lookup returns the AutonomousSystem for a given net.IP. 40 | func (d *Database) Lookup(ip net.IP) (AutonomousSystem, error) { 41 | asn, err := d.mappings.Lookup(ip) 42 | if errors.Is(err, binarytrie.ErrValueNotFound) { 43 | return AutonomousSystem{}, ErrASNotFound 44 | } else if err != nil { 45 | return AutonomousSystem{}, fmt.Errorf("lookup failed: %w", err) 46 | } 47 | 48 | return AutonomousSystem{ 49 | Number: asn, 50 | }, nil 51 | } 52 | 53 | // MarshalBinary implements encoding.BinaryMarshaler. 54 | func (d *Database) MarshalBinary() ([]byte, error) { 55 | return d.mappings.MarshalBinary() 56 | } 57 | 58 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 59 | func (d *Database) UnmarshalBinary(data []byte) error { 60 | return d.mappings.UnmarshalBinary(data) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/database/errors.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrASNotFound is returned when an autonomous system for the specified IP was not found. 9 | ErrASNotFound = errors.New("autonomous system not found") 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/database/mrt.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "net" 7 | 8 | "github.com/viktb/go-mrt" 9 | ) 10 | 11 | func mrtRIBToMapping(rib *mrt.TableDumpV2RIB) (*net.IPNet, uint32, error) { 12 | for _, entry := range rib.RIBEntries { 13 | var asPath mrt.BGPPathAttributeASPath 14 | for _, attr := range entry.BGPAttributes { 15 | path, ok := attr.Value.(mrt.BGPPathAttributeASPath) 16 | if !ok { 17 | continue 18 | } 19 | asPath = path 20 | break 21 | } 22 | if len(asPath) == 0 { 23 | continue 24 | } 25 | 26 | var segment *mrt.BGPASPathSegment 27 | for _, seg := range asPath { 28 | if seg.Type != mrt.BGPASPathSegmentTypeASSequence || len(seg.Value) == 0 { 29 | continue 30 | } 31 | segment = seg 32 | break 33 | } 34 | if segment == nil { 35 | continue 36 | } 37 | 38 | asn, ok := mrtASToUint32(segment.Value[len(segment.Value)-1]) 39 | if !ok { 40 | continue 41 | } 42 | 43 | return rib.Prefix, asn, nil 44 | } 45 | 46 | return nil, 0, errors.New("RIB record does not contain a valid AS path") 47 | } 48 | 49 | func mrtASToUint32(b mrt.AS) (uint32, bool) { 50 | switch len(b) { 51 | case 2: 52 | return uint32(binary.BigEndian.Uint16(b)), true 53 | case 4: 54 | return binary.BigEndian.Uint32(b), true 55 | } 56 | return 0, false 57 | } 58 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package asnlookup 2 | 3 | // Version denotes the version of asnlookup. 4 | // 5 | // This variable is set at compile time using the -ldflags flag. 6 | var Version = "unknown" 7 | --------------------------------------------------------------------------------