├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── Taskfile.yml ├── axfr.go ├── go.mod ├── go.sum ├── gopher.png ├── hosts.go ├── lookup.go ├── main.go ├── options.go ├── output.go ├── ranger.go ├── rlimit.go ├── rlimit_unix.go └── zone.go /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | axfr2hosts 9 | dist/ 10 | vendor/ 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | ### Intellij ### 19 | # User-specific stuff: 20 | .idea/ 21 | 22 | # CMake 23 | cmake-build-debug/ 24 | 25 | ## File-based project format: 26 | *.iws 27 | 28 | # IntelliJ 29 | /out/ 30 | 31 | ### VisualStudioCode ### 32 | .vscode/ 33 | 34 | ### Tokens ### 35 | credentials.json 36 | token.json 37 | 38 | ### Shell wrappers ### 39 | *.sh 40 | 41 | ## Usual macOS cruft ### 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - cyclop 6 | - depguard 7 | - dupl 8 | - exhaustruct 9 | - forbidigo 10 | - funlen 11 | - gochecknoglobals 12 | - gocognit 13 | - lll 14 | - mnd 15 | - varnamelen 16 | - wrapcheck 17 | exclusions: 18 | generated: lax 19 | presets: 20 | - comments 21 | - common-false-positives 22 | - legacy 23 | - std-error-handling 24 | paths: 25 | - third_party$ 26 | - builtin$ 27 | - examples$ 28 | formatters: 29 | enable: 30 | - gci 31 | - gofmt 32 | - gofumpt 33 | - goimports 34 | exclusions: 35 | generated: lax 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - flags: 6 | - -trimpath 7 | env: 8 | - CGO_ENABLED=0 9 | ldflags: | 10 | -s -w -extldflags '-static' 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | - freebsd 16 | goarch: 17 | - amd64 18 | - arm 19 | - arm64 20 | goarm: 21 | - 6 22 | - 7 23 | ignore: 24 | - goos: windows 25 | goarch: arm64 26 | - goos: windows 27 | goarch: arm 28 | universal_binaries: 29 | - replace: true 30 | changelog: 31 | sort: asc 32 | archives: 33 | - name_template: >- 34 | {{ .ProjectName }}_ 35 | {{- title .Os }}_ 36 | {{- if eq .Arch "amd64" }}x86_64 37 | {{- else if eq .Arch "386" }}i386 38 | {{- else }}{{ .Arch }}{{ end }} 39 | {{- if .Arm }}v{{ .Arm }}{{ end }} 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | files: 44 | - README.md 45 | - LICENSE 46 | - src: dist/CHANGELOG.md 47 | dst: "" 48 | strip_parent: true 49 | checksum: 50 | name_template: "checksums.txt" 51 | snapshot: 52 | name_template: "{{ .Tag }}-next" 53 | nfpms: 54 | - package_name: axfr2hosts 55 | vendor: Dinko Korunic 56 | homepage: https://github.com/dkorunic/axfr2hosts 57 | maintainer: Dinko Korunic 58 | description: Fetches one or more DNS zones via AXFR and dumps in Unix hosts format for local use 59 | license: MIT 60 | formats: 61 | - apk 62 | - deb 63 | - rpm 64 | - termux.deb 65 | - archlinux 66 | bindir: /usr/bin 67 | section: net 68 | priority: optional 69 | deb: 70 | lintian_overrides: 71 | - statically-linked-binary 72 | - changelog-file-missing-in-native-package 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 Dinko Korunic 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axfr2hosts 2 | 3 | [![GitHub license](https://img.shields.io/github/license/dkorunic/axfr2hosts)](https://github.com/dkorunic/axfr2hosts/blob/master/LICENSE) 4 | [![GitHub release](https://img.shields.io/github/release/dkorunic/axfr2hosts)](https://github.com/dkorunic/axfr2hosts/releases/latest) 5 | [![codebeat badge](https://codebeat.co/badges/b535ef48-ba10-413e-81f0-dcb5a17e01c4)](https://codebeat.co/projects/github-com-dkorunic-axfr2hosts-main) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/dkorunic/axfr2hosts)](https://goreportcard.com/report/github.com/dkorunic/axfr2hosts) 7 | 8 | ![](gopher.png) 9 | 10 | ## About 11 | 12 | axfr2hosts is a tool meant to do a [DNS zone transfer](https://en.wikipedia.org/wiki/DNS_zone_transfer) in a form of AXFR transaction of one or more zones towards a single DNS server and convert received A, AAAA and CNAME records from a DNS responses into a [hosts file]() for a local use, for instance when DNS servers are [unreachable](https://blog.cloudflare.com/october-2021-facebook-outage/) and/or down. 13 | 14 | By default hosts entries will be sorted its IP as a key and under each entry individual FQDNs will be sorted alphabetically. 15 | 16 | If needed, axfr2hosts can also read and parse local RFC 1035 zones (for instance BIND 9 zone files) and process A and CNAME records into a hosts file as described above so that a zone transfer is not needed. 17 | 18 | ## Requirements 19 | 20 | Either of: 21 | 22 | - Ability to do a full zone transfer (AXFR), usually permitted with `allow-transfer` in [BIND 9](https://www.isc.org/bind/) or with `allow-axfr-ips` in [PowerDNS](https://www.powerdns.com/), 23 | - Permissions to read RFC 1035 zone files locally. 24 | 25 | ## Installation 26 | 27 | There are two ways of installing axfr2hosts: 28 | 29 | ### Manual 30 | 31 | Download your preferred flavor from [the releases](https://github.com/dkorunic/axfr2hosts/releases) page and install manually, typically to `/usr/local/bin/axfr2hosts`. 32 | 33 | ### Using go get 34 | 35 | ```shell 36 | go install github.com/dkorunic/axfr2hosts@latest 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```shell 42 | Usage: ./axfr2hosts [options] zone [zone2 [zone3 ...]] [@server[:port]] 43 | -cidr_list string 44 | Use only targets from CIDR whitelist (comma separated list) 45 | -cpu_profile string 46 | CPU profile output file 47 | -greedy_cname 48 | Resolve out-of-zone CNAME targets (default true) 49 | -ignore_star 50 | Ignore wildcard records (default true) 51 | -max_retries uint 52 | Maximum DNS zone transfer attempts (default 3) 53 | -max_transfers uint 54 | Maximum parallel zone transfers (default 10) 55 | -mem_profile string 56 | memory profile output file 57 | -resolver_address string 58 | DNS resolver (DNS recursor) IP address 59 | -resolver_timeout duration 60 | DNS queries timeout (should be 2-10s) (default 10s) 61 | -strip_domain 62 | Strip domain name from FQDN hosts entries 63 | -strip_unstrip 64 | Keep both FQDN names and domain-stripped names 65 | -verbose 66 | Enable more verbosity 67 | 1) If server was not specified, zones will be parsed as RFC 1035 zone files on a local filesystem, 68 | 2) We also permit zone=domain argument format to infer a domain name for zone files. 69 | 70 | For more information visit project home: https://github.com/dkorunic/axfr2hosts 71 | ``` 72 | 73 | At minimum, a single zone and a single server are needed for any meaningful action. 74 | 75 | Typical use case would be: 76 | 77 | ```shell 78 | axfr2hosts dkorunic.net pkorunic.net @172.64.33.146 79 | ``` 80 | 81 | ### CNAME handling 82 | 83 | However the tool by default follows CNAMEs even if they are out-of-zone and resolves to one or more IP addresses if possible and lists all of them. That behaviour can be changed with `-greedy_cname=false` flag. 84 | 85 | ### Wildcard handling 86 | 87 | Also, by default tool lists wildcard (DNS labels containing `*`) like they are ordinary labels and that can be changed with `-ignore_star=true` flag, which simply skips over those records. 88 | 89 | ### Filter results by CIDR 90 | 91 | Finally if there is a need to list only a subset of records matching one or more CIDR ranges, `-cidr_list` flag can be used. 92 | 93 | ### Many zones transfer 94 | 95 | If there is a lot of zones that need to be fetched at once, tool works well with `xargs`. Individual zone errors will be displayed and such zones will be skipped over: 96 | 97 | ```shell 98 | xargs axfr2hosts @nameserver < list 99 | ``` 100 | 101 | Maximum of concurrent zone transfers is limited by `-max_transfers` flag and defaults to `10`, aligned with BIND 9 default (`transfers-out` in BIND 9 `named.conf`). 102 | 103 | ### Strip domain name 104 | 105 | It is also possible to output hosts file with domain names stripped by using `-strip_domain=true` flag. It is also possible to keep both domain-stripped labels and FQDNs at the same time by using `-strip_unstrip=true` flag. When using many domains at once, either of these options do not make much sense. 106 | 107 | ### Process local zone files 108 | 109 | It is also possible to directly process RFC 1035 zone files on a local filesystem when a nameserver is not been specified. We would typically recommend specifying a domain name manually by suffixing the zone file with `=` and domain name as shown below, as one inferred from a zone can possibly be invalid (due to lack of top-level `$ORIGIN` and/or all records being non-FQDN and/or being suffixed with `@` macro): 110 | 111 | ```shell 112 | axfr2hosts dkorunic.net.zone=dkorunic.net 113 | ``` 114 | 115 | ### DNS error code responses 116 | 117 | In case you are wondering what `dns: bad xfr rcode: 9` means, here is a list of DNS response codes: 118 | 119 | | Response Code | Return Message | Explanation | 120 | | :------------ | :------------- | :------------------- | 121 | | 0 | NOERROR | No error | 122 | | 1 | FORMERR | Format error | 123 | | 2 | SERVFAIL | Server failure | 124 | | 3 | NXDOMAIN | Name does not exist | 125 | | 4 | NOTIMP | Not implemented | 126 | | 5 | REFUSED | Refused | 127 | | 6 | YXDOMAIN | Name exists | 128 | | 7 | YRRSET | RRset exists | 129 | | 8 | NXRRSET | RRset does not exist | 130 | | 9 | NOTAUTH | Not authoritative | 131 | | 10 | NOTZONE | Name not in zone | 132 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | vars: 4 | TARGET: axfr2hosts 5 | GIT_LAST_TAG: 6 | sh: git describe --abbrev=0 --tags 2>/dev/null || echo latest 7 | GIT_HEAD_COMMIT: 8 | sh: git rev-parse --short HEAD 2>/dev/null || echo unknown 9 | GIT_TAG_COMMIT: 10 | sh: git rev-parse --short {{.GIT_LAST_TAG}} 2>/dev/null || echo unknown 11 | GIT_MODIFIED1: 12 | sh: git diff {{.GIT_HEAD_COMMIT}} {{.GIT_TAG_COMMIT}} --quiet 2>/dev/null || echo .dev 13 | GIT_MODIFIED2: 14 | sh: git diff --quiet 2>/dev/null || echo .dirty 15 | GIT_MODIFIED: 16 | sh: echo "{{.GIT_MODIFIED1}}{{.GIT_MODIFIED2}}" 17 | BUILD_DATE: 18 | sh: date -u '+%Y-%m-%dT%H:%M:%SZ' 19 | 20 | env: 21 | CGO_ENABLED: 0 22 | 23 | tasks: 24 | default: 25 | cmds: 26 | - task: update 27 | - task: build 28 | 29 | update: 30 | cmds: 31 | - go get -u 32 | - go mod tidy 33 | 34 | update-major: 35 | cmds: 36 | - gomajor list 37 | 38 | fmt: 39 | cmds: 40 | - gci write . 41 | - gofumpt -l -w . 42 | - betteralign -apply ./... 43 | 44 | build: 45 | cmds: 46 | - task: fmt 47 | - go build -trimpath -pgo=auto -ldflags="-s -w -extldflags '-static' -X main.GitTag={{.GIT_LAST_TAG}} -X main.GitCommit={{.GIT_HEAD_COMMIT}} -X main.GitDirty={{.GIT_MODIFIED}} -X main.BuildTime={{.BUILD_DATE}}" -o {{.TARGET}} 48 | 49 | build-debug: 50 | env: 51 | CGO_ENABLED: 1 52 | cmds: 53 | - task: update 54 | - task: fmt 55 | - go build -ldflags="-X main.GitTag={{.GIT_LAST_TAG}} -X main.GitCommit={{.GIT_HEAD_COMMIT}} -X main.GitDirty={{.GIT_MODIFIED}} -X main.BuildTime={{.BUILD_DATE}}" -race -o {{.TARGET}} 56 | 57 | lint: 58 | cmds: 59 | - task: fmt 60 | - golangci-lint run --timeout 5m 61 | 62 | release: 63 | cmds: 64 | - goreleaser release --clean -p 4 65 | -------------------------------------------------------------------------------- /axfr.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | "os" 28 | "strings" 29 | "time" 30 | 31 | "github.com/avast/retry-go/v4" 32 | "github.com/miekg/dns" 33 | ) 34 | 35 | const ( 36 | dialTimeout = 30 * time.Second 37 | readTimeout = 30 * time.Second 38 | writeTimeout = 30 * time.Second 39 | ) 40 | 41 | // zoneTransfer prepares and executes AXFR towards a specific DNS server, returning DNS RR slice. 42 | func zoneTransfer(zone, server string) []dns.RR { 43 | ctx := context.Background() 44 | 45 | // make sure zone always ends with dot 46 | if !strings.HasSuffix(zone, endingDot) { 47 | zone = strings.Join([]string{zone, endingDot}, "") 48 | } 49 | 50 | // prepare AXFR 51 | tr := new(dns.Transfer) 52 | m := new(dns.Msg) 53 | m.SetAxfr(zone) 54 | 55 | // set timeouts 56 | tr.DialTimeout = dialTimeout 57 | tr.ReadTimeout = readTimeout 58 | tr.WriteTimeout = writeTimeout 59 | 60 | var records []dns.RR 61 | 62 | var c chan *dns.Envelope 63 | 64 | // execute AXFR with automatic retrying 65 | err := retry.Do( 66 | func() error { 67 | var err error 68 | 69 | c, err = tr.In(m, server) 70 | if err != nil { 71 | return fmt.Errorf("error performing zone transfer: %w", err) 72 | } 73 | 74 | return nil 75 | }, 76 | retry.Attempts(*maxRetries), 77 | retry.Context(ctx), 78 | ) 79 | if err != nil { 80 | fmt.Fprintf(os.Stderr, "Error: AXFR failure for zone %q / server %q, will skip over: %v\n", zone, server, err) 81 | 82 | return records 83 | } 84 | 85 | // parse messages and fetch RRs if any 86 | for msg := range c { 87 | if msg.Error != nil { 88 | fmt.Fprintf(os.Stderr, "Error: AXFR payload problem for zone %q / server %q, but will try to continue: %v\n", 89 | zone, server, msg.Error) 90 | 91 | continue 92 | } 93 | 94 | records = append(records, msg.RR...) 95 | } 96 | 97 | return records 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dkorunic/axfr2hosts 2 | 3 | go 1.23.6 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/KimMachineGun/automemlimit v0.7.2 9 | github.com/avast/retry-go/v4 v4.6.1 10 | github.com/miekg/dns v1.1.66 11 | github.com/monoidic/cidranger/v2 v2.0.1 12 | go.uber.org/automaxprocs v1.6.0 13 | golang.org/x/sync v0.14.0 14 | ) 15 | 16 | require ( 17 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 18 | github.com/sirupsen/logrus v1.9.3 // indirect 19 | golang.org/x/mod v0.24.0 // indirect 20 | golang.org/x/net v0.40.0 // indirect 21 | golang.org/x/sys v0.33.0 // indirect 22 | golang.org/x/tools v0.33.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KimMachineGun/automemlimit v0.7.2 h1:DyfHI7zLWmZPn2Wqdy2AgTiUvrGPmnYWgwhHXtAegX4= 2 | github.com/KimMachineGun/automemlimit v0.7.2/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 3 | github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 4 | github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= 11 | github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= 12 | github.com/monoidic/cidranger/v2 v2.0.1 h1:PKDDfmSx9ng5RWE/aime6yW2agbrRvxXtRAGmTNcITg= 13 | github.com/monoidic/cidranger/v2 v2.0.1/go.mod h1:74n9wRQl+CqA4v0cR2rifIdr8kn02s4rHchraYVwYm4= 14 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 15 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 19 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 20 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 21 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 27 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 28 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 29 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 30 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 31 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 32 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 33 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 34 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 36 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 37 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 38 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkorunic/axfr2hosts/91892e363f42714236f95c2d13fd8ddba45483e0/gopher.png -------------------------------------------------------------------------------- /hosts.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "net/netip" 26 | "strings" 27 | ) 28 | 29 | // HostEntry contains label, addr and ipAddr for sending through a channel. 30 | type HostEntry struct { 31 | ipAddr netip.Addr 32 | label string 33 | } 34 | 35 | // HostMap contains map of addresses and labels. 36 | type HostMap map[netip.Addr]map[string]struct{} 37 | 38 | // processHost cleans FQDN and optionally shortens it, calling low-level writeHostEntries and returning updated hosts 39 | // map and keys slice. 40 | func processHost(label, zone string, ipAddr netip.Addr, hosts chan<- HostEntry) { 41 | label = strings.TrimSuffix(label, endingDot) 42 | label = strings.ToLower(label) 43 | 44 | // strip domain if needed 45 | if *stripDomain || *stripUnstrip { 46 | labelStripped := strings.TrimSuffix(label, strings.Join([]string{endingDot, zone}, "")) 47 | if labelStripped != "" { 48 | hosts <- HostEntry{label: labelStripped, ipAddr: ipAddr} 49 | 50 | if !*stripUnstrip { 51 | return 52 | } 53 | } 54 | } 55 | 56 | hosts <- HostEntry{label: label, ipAddr: ipAddr} 57 | } 58 | 59 | // writeHostEntries updates hosts map with a new label-IP pair, returning updated hosts map and keys slice. 60 | func writeHostEntries(hosts <-chan HostEntry, keys *[]netip.Addr, entries HostMap) { 61 | for x := range hosts { 62 | label, ipAddr := x.label, x.ipAddr 63 | if _, ok := entries[ipAddr]; ok { 64 | entries[ipAddr][label] = struct{}{} 65 | } else { 66 | *keys = append(*keys, ipAddr) 67 | entries[ipAddr] = make(map[string]struct{}, subMapSize) 68 | entries[ipAddr][label] = struct{}{} 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lookup.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2024 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "context" 26 | "errors" 27 | "net" 28 | "strconv" 29 | "strings" 30 | 31 | "github.com/miekg/dns" 32 | "golang.org/x/sync/singleflight" 33 | ) 34 | 35 | var lookupGroup singleflight.Group 36 | 37 | // lookupFunc is a function that returns a closure function to perform DNS lookups based on the type of the DNS record. 38 | // 39 | // It takes a context.Context, a string, a uint16, and a net.Resolver as parameters, and returns a closure function that 40 | // returns an interface and an error. 41 | func lookupFunc(ctx context.Context, s string, t uint16, r *net.Resolver) func() (any, error) { 42 | switch t { 43 | case dns.TypeCNAME: 44 | return func() (any, error) { 45 | ctx, cancel := context.WithTimeout(ctx, *resolverTimeout) 46 | defer cancel() 47 | 48 | rr, err := r.LookupCNAME(ctx, s) 49 | 50 | return []string{rr}, err 51 | } 52 | case dns.TypeA, dns.TypeAAAA: 53 | return func() (any, error) { 54 | ctx, cancel := context.WithTimeout(ctx, *resolverTimeout) 55 | defer cancel() 56 | 57 | return r.LookupHost(ctx, s) 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // lookup performs a lookup operation using the provided context, string, type, and resolver. 65 | // 66 | // It returns a slice of strings and an error. 67 | func lookup(ctx context.Context, s string, t uint16, r *net.Resolver) ([]string, error) { 68 | key := strings.Join([]string{strconv.FormatUint(uint64(t), 10), s}, "") 69 | ch := lookupGroup.DoChan(key, lookupFunc(ctx, s, t, r)) 70 | 71 | var err error 72 | 73 | select { 74 | case <-ctx.Done(): 75 | err = ctx.Err() 76 | if errors.Is(err, context.DeadlineExceeded) { 77 | lookupGroup.Forget(key) 78 | 79 | return nil, err 80 | } 81 | case res := <-ch: 82 | rrs, ok := res.Val.([]string) 83 | if ok { 84 | return rrs, res.Err 85 | } 86 | 87 | return []string{}, res.Err 88 | } 89 | 90 | return []string{}, nil 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "net/netip" 27 | "os" 28 | "runtime" 29 | "runtime/pprof" 30 | "sync" 31 | 32 | "github.com/KimMachineGun/automemlimit/memlimit" 33 | "go.uber.org/automaxprocs/maxprocs" 34 | ) 35 | 36 | const ( 37 | mapSize = 4096 38 | subMapSize = 8 39 | hostChanSize = 64 40 | maxMemRatio = 0.9 41 | ) 42 | 43 | func main() { 44 | _, _ = memlimit.SetGoMemLimitWithOpts( 45 | memlimit.WithRatio(maxMemRatio), 46 | memlimit.WithProvider( 47 | memlimit.ApplyFallback( 48 | memlimit.FromCgroup, 49 | memlimit.FromSystem, 50 | ), 51 | ), 52 | ) 53 | 54 | undo, _ := maxprocs.Set() 55 | defer undo() 56 | 57 | zones, server, cidrList := parseFlags() 58 | 59 | // enable CPU profiling dump on exit 60 | if *cpuProfile != "" { 61 | f, err := os.Create(*cpuProfile) 62 | if err != nil { 63 | fmt.Fprintf(os.Stderr, "Error creating CPU profile: %v\n", err) 64 | } 65 | defer f.Close() 66 | 67 | if err := pprof.StartCPUProfile(f); err != nil { 68 | fmt.Fprintf(os.Stderr, "Error starting CPU profile: %v\n", err) 69 | } 70 | defer pprof.StopCPUProfile() 71 | } 72 | 73 | // enable memory profile dump on exit 74 | if *memProfile != "" { 75 | f, err := os.Create(*memProfile) 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "Error trying to create memory profile: %v\n", err) 78 | } 79 | defer f.Close() 80 | 81 | defer func() { 82 | runtime.GC() 83 | 84 | if err := pprof.WriteHeapProfile(f); err != nil { 85 | fmt.Fprintf(os.Stderr, "Error writing memory profile: %v\n", err) 86 | } 87 | }() 88 | } 89 | 90 | _ = setNofile() 91 | 92 | ranger, doCIDR := rangerInit(cidrList) 93 | hostChan := make(chan HostEntry, hostChanSize) 94 | 95 | entries := make(HostMap, mapSize) 96 | keys := make([]netip.Addr, 0, mapSize) 97 | 98 | var wgMon, wgWrk sync.WaitGroup 99 | 100 | wgMon.Add(1) 101 | 102 | // host map/key slice managing monitor routine 103 | go func() { 104 | defer wgMon.Done() 105 | 106 | writeHostEntries(hostChan, &keys, entries) 107 | }() 108 | 109 | // limit total AXFRs in progress 110 | semAXFR := make(chan struct{}, *maxTransfers) 111 | 112 | // routines for processing local and remote zones 113 | for _, zone := range zones { 114 | if server == "" { 115 | // there is no remote server, so assume zones are local Bind9 files 116 | wgWrk.Add(1) 117 | 118 | go func() { 119 | defer wgWrk.Done() 120 | 121 | processLocalZone(zone, doCIDR, ranger, hostChan) 122 | }() 123 | } else { 124 | // otherwise assume remote AXFR-able zones 125 | wgWrk.Add(1) 126 | semAXFR <- struct{}{} 127 | 128 | go func() { 129 | defer wgWrk.Done() 130 | 131 | processRemoteZone(zone, server, doCIDR, ranger, hostChan) 132 | <-semAXFR 133 | }() 134 | } 135 | } 136 | 137 | wgWrk.Wait() 138 | close(hostChan) 139 | wgMon.Wait() 140 | 141 | displayHostEntries(keys, entries) 142 | } 143 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "flag" 26 | "fmt" 27 | "net" 28 | "os" 29 | "strings" 30 | "time" 31 | ) 32 | 33 | const ( 34 | endingDot = "." 35 | dnsPort = "53" 36 | dnsPrefix = "@" 37 | cidrSeparator = "," 38 | portSeparator = ":" 39 | projectHome = "https://github.com/dkorunic/axfr2hosts" 40 | maxTransfersDefault = 10 41 | maxRetriesDefault = 3 42 | defaultResolverTimeout = 10 * time.Second 43 | ) 44 | 45 | var ( 46 | greedyCNAME = flag.Bool("greedy_cname", true, "Resolve out-of-zone CNAME targets") 47 | ignoreStar = flag.Bool("ignore_star", true, "Ignore wildcard records") 48 | cidrString = flag.String("cidr_list", "", "Use only targets from CIDR whitelist (comma separated list)") 49 | stripDomain = flag.Bool("strip_domain", false, "Strip domain name from FQDN hosts entries") 50 | stripUnstrip = flag.Bool("strip_unstrip", false, "Keep both FQDN names and domain-stripped names") 51 | verbose = flag.Bool("verbose", false, "Enable more verbosity") 52 | maxTransfers = flag.Uint("max_transfers", maxTransfersDefault, "Maximum parallel zone transfers") 53 | maxRetries = flag.Uint("max_retries", maxRetriesDefault, "Maximum DNS zone transfer attempts") 54 | cpuProfile = flag.String("cpu_profile", "", "CPU profile output file") 55 | memProfile = flag.String("mem_profile", "", "memory profile output file") 56 | resolverAddress = flag.String("resolver_address", "", "DNS resolver (DNS recursor) IP address") 57 | resolverTimeout = flag.Duration("resolver_timeout", defaultResolverTimeout, "DNS queries timeout (should be 2-10s)") 58 | ) 59 | 60 | func parseFlags() ([]string, string, []string) { 61 | flag.Usage = func() { 62 | fmt.Fprintf(os.Stderr, "Usage: %v [options] zone [zone2 [zone3 ...]] [@server[:port]]\n", os.Args[0]) 63 | flag.PrintDefaults() 64 | fmt.Fprintf(os.Stderr, "1) If server was not specified, zones will be parsed as RFC 1035 zone files on a local filesystem,\n") 65 | fmt.Fprintf(os.Stderr, "2) We also permit zone=domain argument format to infer a domain name for zone files.\n") 66 | fmt.Fprintf(os.Stderr, "\nFor more information visit project home: %v\n", projectHome) 67 | os.Exit(0) 68 | } 69 | 70 | flag.Parse() 71 | 72 | var server string 73 | 74 | zones := make([]string, 0, len(flag.Args())) 75 | zoneMap := make(map[string]struct{}, len(flag.Args())) 76 | 77 | if len(flag.Args()) == 0 { 78 | fmt.Fprintf(os.Stderr, "Error: no arguments were given\n") 79 | flag.Usage() 80 | } 81 | 82 | for _, arg := range flag.Args() { 83 | // nameserver starts with '@' 84 | if strings.HasPrefix(arg, dnsPrefix) { 85 | server = strings.TrimPrefix(arg, dnsPrefix) 86 | 87 | // make sure server is in server:port format 88 | if !strings.Contains(server, portSeparator) { 89 | server = net.JoinHostPort(server, dnsPort) 90 | } 91 | 92 | continue 93 | } 94 | 95 | // otherwise it is a zone name; make sure to strip ending dot 96 | arg = strings.TrimSuffix(arg, endingDot) 97 | 98 | // add zone only if unique 99 | if _, ok := zoneMap[arg]; !ok { 100 | zones = append(zones, arg) 101 | zoneMap[arg] = struct{}{} 102 | } 103 | } 104 | 105 | // check if zones are empty 106 | if len(zones) == 0 { 107 | fmt.Fprintf(os.Stderr, "Error: no zones to transfer or parse\n") 108 | flag.Usage() 109 | } 110 | 111 | // split if non-empty 112 | var cidrList []string 113 | if len(*cidrString) > 0 { 114 | cidrList = strings.Split(*cidrString, cidrSeparator) 115 | } 116 | 117 | // check if resolverIP is in server:port format 118 | if *resolverAddress != "" && !strings.Contains(*resolverAddress, portSeparator) { 119 | *resolverAddress = net.JoinHostPort(*resolverAddress, dnsPort) 120 | } 121 | 122 | return zones, server, cidrList 123 | } 124 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "net/netip" 27 | "sort" 28 | "strings" 29 | "time" 30 | ) 31 | 32 | // displayHostEntries does a final Unix hosts file output with a list of unique IPs and labels. 33 | func displayHostEntries(keysAddr []netip.Addr, results HostMap) { 34 | var ( 35 | x, last int 36 | sb strings.Builder 37 | ipAddr netip.Addr 38 | ) 39 | 40 | keysHost := make([]string, 0, subMapSize) 41 | 42 | t := time.Now().Format(time.RFC1123) 43 | fmt.Printf("# axfr2hosts generated list at %v\n", t) 44 | 45 | // sorting by IP 46 | sort.Slice(keysAddr, func(i, j int) bool { 47 | return keysAddr[i].Compare(keysAddr[j]) < 0 48 | }) 49 | 50 | for i := range keysAddr { 51 | ipAddr = keysAddr[i] 52 | labelMap := results[ipAddr] 53 | 54 | sb.Reset() 55 | sb.WriteString(ipAddr.String()) 56 | sb.WriteString("\t") 57 | 58 | last = len(labelMap) 59 | 60 | keysHost = keysHost[:0] 61 | 62 | for k := range labelMap { 63 | keysHost = append(keysHost, k) 64 | } 65 | 66 | // sorting by hostname 67 | sort.Strings(keysHost) 68 | 69 | x = 0 70 | 71 | for _, k := range keysHost { 72 | sb.WriteString(k) 73 | 74 | x++ 75 | 76 | if x != last { 77 | sb.WriteString(" ") 78 | } 79 | } 80 | 81 | sb.WriteString("\n") 82 | 83 | fmt.Print(sb.String()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ranger.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "net/netip" 27 | "os" 28 | 29 | "github.com/monoidic/cidranger/v2" 30 | ) 31 | 32 | // rangerInit initializes and loads CIDR Ranger and sets doCIDR flag to true if list of networks is non-empty. 33 | func rangerInit(cidrList []string) (cidranger.Ranger[struct{}], bool) { 34 | var ( 35 | ranger cidranger.Ranger[struct{}] 36 | doCIDR bool 37 | ) 38 | 39 | // prepare CIDR matching 40 | if len(cidrList) > 0 { 41 | doCIDR = true 42 | ranger = cidranger.NewPCTrieRanger[struct{}]() 43 | 44 | // parse and insert individual networks 45 | for _, s := range cidrList { 46 | n, err := netip.ParsePrefix(s) 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, "Error: problem parsing CIDR: %v\n", err) 49 | 50 | continue 51 | } 52 | 53 | _ = ranger.Insert(n, struct{}{}) 54 | } 55 | } 56 | 57 | return ranger, doCIDR 58 | } 59 | -------------------------------------------------------------------------------- /rlimit.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2023 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build !unix 23 | 24 | package main 25 | 26 | func setNofile() error { 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /rlimit_unix.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2023 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | //go:build unix 23 | 24 | package main 25 | 26 | import ( 27 | "runtime" 28 | "syscall" 29 | ) 30 | 31 | const ( 32 | darwinMagic = 24576 33 | defaultNoFile = 100000 34 | ) 35 | 36 | // setNofile sets the maximum number of open files to the maximum allowed value. 37 | // 38 | // For darwin (macOS), the maximum allowed value is 24576 as per the 39 | // documentation for setrlimit(2). 40 | // 41 | // For other platforms, the maximum allowed value is 100000. 42 | // 43 | // The setNofile function returns an error if the syscall.Setrlimit call fails. 44 | func setNofile() error { 45 | if runtime.GOOS == "darwin" { 46 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &syscall.Rlimit{ 47 | Cur: darwinMagic, 48 | Max: darwinMagic, 49 | }) 50 | } 51 | 52 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &syscall.Rlimit{ 53 | Cur: defaultNoFile, 54 | Max: defaultNoFile, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /zone.go: -------------------------------------------------------------------------------- 1 | // @license 2 | // Copyright (C) 2021 Dinko Korunic 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "context" 26 | "flag" 27 | "fmt" 28 | "net" 29 | "net/netip" 30 | "os" 31 | "strings" 32 | "sync" 33 | 34 | "github.com/miekg/dns" 35 | "github.com/monoidic/cidranger/v2" 36 | ) 37 | 38 | const ( 39 | wildcard = "*" 40 | fileZoneSeparator = "=" 41 | ) 42 | 43 | // processRemoteZone is calling zoneTransfer() for AXFR and processRecords() for handling each valid RR. 44 | func processRemoteZone(zone, server string, doCIDR bool, ranger cidranger.Ranger[struct{}], hosts chan<- HostEntry) { 45 | if *verbose { 46 | fmt.Fprintf(os.Stderr, "Info: doing AXFR for zone %q / server %q\n", zone, server) 47 | } 48 | 49 | zoneRecords := zoneTransfer(zone, server) 50 | processRecords(zone, doCIDR, ranger, hosts, zoneRecords) 51 | } 52 | 53 | // processLocalZone is calling zoneParser() for local zone parse and processRecords() for handling valid RR. 54 | func processLocalZone(zone string, doCIDR bool, ranger cidranger.Ranger[struct{}], hosts chan<- HostEntry) { 55 | var domain string 56 | 57 | if strings.Contains(zone, fileZoneSeparator) { 58 | t := strings.Split(zone, fileZoneSeparator) 59 | 60 | if len(t) == 2 { 61 | zone = t[0] // filename 62 | domain = t[1] // domain 63 | 64 | // make sure domain always ends with dot 65 | if !strings.HasSuffix(domain, endingDot) { 66 | domain = strings.Join([]string{domain, endingDot}, "") 67 | } 68 | } else { 69 | fmt.Fprintf(os.Stderr, "Error: invalid file=domain option format: %q\n", zone) 70 | flag.Usage() 71 | } 72 | } 73 | 74 | if *verbose { 75 | fmt.Fprintf(os.Stderr, "Info: loading and parsing zone %q / domain %q\n", zone, domain) 76 | } 77 | 78 | zoneRecords := zoneParser(zone, domain) 79 | if len(zoneRecords) == 0 && domain == "" { 80 | fmt.Fprintf(os.Stderr, "Error: no detected records in %q file. Try next time with \"%v=domain\"\n", 81 | zone, zone) 82 | } 83 | 84 | processRecords(zone, doCIDR, ranger, hosts, zoneRecords) 85 | } 86 | 87 | // processRecords is processing each RR and calling processHost() for each valid RR. 88 | func processRecords(zone string, doCIDR bool, ranger cidranger.Ranger[struct{}], hosts chan<- HostEntry, 89 | zoneRecords []dns.RR, 90 | ) { 91 | var wg sync.WaitGroup 92 | 93 | var r net.Resolver 94 | 95 | if *resolverAddress != "" { 96 | // custom DNS resolver 97 | r = net.Resolver{ 98 | PreferGo: true, 99 | Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { 100 | d := net.Dialer{} 101 | 102 | return d.DialContext(ctx, network, *resolverAddress) 103 | }, 104 | } 105 | } else { 106 | r = net.Resolver{PreferGo: true} 107 | } 108 | 109 | // process each RR 110 | for _, rr := range zoneRecords { 111 | switch t := rr.(type) { 112 | case *dns.A: 113 | wg.Add(1) 114 | 115 | go func() { 116 | defer wg.Done() 117 | 118 | // ignore wildcards if ignoreStar is used 119 | if *ignoreStar && strings.Contains(t.Hdr.Name, wildcard) { 120 | return 121 | } 122 | 123 | ipAddr, ok := unmapAddrFromSlice(t.A) 124 | if !ok { 125 | return 126 | } 127 | 128 | // if CIDR matching is true, check if IP is whitelisted 129 | if doCIDR && ranger != nil { 130 | if c, _ := ranger.Contains(ipAddr); !c { 131 | return 132 | } 133 | } 134 | 135 | processHost(t.Hdr.Name, zone, ipAddr, hosts) 136 | }() 137 | case *dns.AAAA: 138 | wg.Add(1) 139 | 140 | go func() { 141 | defer wg.Done() 142 | 143 | // ignore wildcards if ignoreStar is used 144 | if *ignoreStar && strings.Contains(t.Hdr.Name, wildcard) { 145 | return 146 | } 147 | 148 | ipAddr6, ok := unmapAddrFromSlice(t.AAAA) 149 | if !ok { 150 | return 151 | } 152 | 153 | // if CIDR matching is true, check if IP is whitelisted 154 | if doCIDR && ranger != nil { 155 | if c, _ := ranger.Contains(ipAddr6); !c { 156 | return 157 | } 158 | } 159 | 160 | processHost(t.Hdr.Name, zone, ipAddr6, hosts) 161 | }() 162 | case *dns.CNAME: 163 | wg.Add(1) 164 | 165 | go func() { 166 | defer wg.Done() 167 | 168 | ctx := context.Background() 169 | 170 | // ignore out-of-zone targets if not using greedyCNAME 171 | if !*greedyCNAME { 172 | cnames, err := lookup(ctx, t.Hdr.Name, dns.TypeCNAME, &r) 173 | if err != nil { 174 | return 175 | } 176 | 177 | if len(cnames) > 0 && !strings.HasSuffix(cnames[0], zone) { 178 | return 179 | } 180 | } 181 | 182 | addrs, err := lookup(ctx, t.Hdr.Name, dns.TypeA, &r) 183 | if err != nil { 184 | return 185 | } 186 | 187 | // loop through resolved array 188 | for _, a := range addrs { 189 | ipAddr, err := unmapParseAddr(a) 190 | if err != nil { 191 | continue 192 | } 193 | 194 | // if CIDR matching is true, check if IP is whitelisted 195 | if doCIDR && ranger != nil { 196 | if c, _ := ranger.Contains(ipAddr); !c { 197 | continue 198 | } 199 | } 200 | 201 | processHost(t.Hdr.Name, zone, ipAddr, hosts) 202 | } 203 | }() 204 | // every other RR type is skipped over 205 | default: 206 | } 207 | } 208 | 209 | wg.Wait() 210 | } 211 | 212 | // zoneParser is parsing loading zones into memory and parsing them, returning slice of RRs. 213 | func zoneParser(zone, domain string) []dns.RR { 214 | var records []dns.RR 215 | 216 | // read a whole zone into memory 217 | z, err := os.ReadFile(zone) 218 | if err != nil { 219 | fmt.Fprintf(os.Stderr, "Error: problem reading zone file: %q: %v\n", zone, err) 220 | 221 | return records 222 | } 223 | 224 | // initialize RFC 1035 zone parser 225 | zp := dns.NewZoneParser(strings.NewReader(string(z)), domain, "") 226 | 227 | // fetch all RRs 228 | for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { 229 | if err := zp.Err(); err != nil { 230 | fmt.Fprintf(os.Stderr, "Error: problem parsing zone %q but will try to continue: %v\n", zone, err) 231 | 232 | continue 233 | } 234 | 235 | records = append(records, rr) 236 | } 237 | 238 | return records 239 | } 240 | 241 | // unmapAddrFromSlice parses 4 or 16-byte slice as IPv4 or IPv6 address and removes any IPv4-mapped IPv6 prefix. 242 | func unmapAddrFromSlice(slice []byte) (netip.Addr, bool) { 243 | ipAddr, ok := netip.AddrFromSlice(slice) 244 | if !ok { 245 | return ipAddr, false 246 | } 247 | 248 | return ipAddr.Unmap(), true 249 | } 250 | 251 | // unmapParseAddr parses string as an IP address, returning result and removes any IPv4-mapped IPv6 prefix. 252 | func unmapParseAddr(s string) (netip.Addr, error) { 253 | ipAddr, err := netip.ParseAddr(s) 254 | if err != nil { 255 | return ipAddr, err 256 | } 257 | 258 | return ipAddr.Unmap(), nil 259 | } 260 | --------------------------------------------------------------------------------