├── AUTHORS ├── delegation ├── testdata │ ├── find │ │ ├── exchange │ │ │ ├── 192.0.2.1 │ │ │ │ └── IN │ │ │ │ │ └── NS │ │ │ │ │ ├── nxdomain.example.net │ │ │ │ │ ├── reserr.example.net │ │ │ │ │ ├── 2.0.192.in-addr.arpa │ │ │ │ │ ├── baddelegation.example.net │ │ │ │ │ ├── autoreverse.example.net │ │ │ │ │ ├── autoreverse.a.b.c.example.net │ │ │ │ │ └── wrongdelegation.example.net │ │ │ ├── 2001_db8__c_1 │ │ │ │ └── IN │ │ │ │ │ └── PTR │ │ │ │ │ └── 81.2.0.192.in-addr.arpa │ │ │ ├── 192.0.2.30 │ │ │ │ └── IN │ │ │ │ │ └── AAAA │ │ │ │ │ └── cubyh.wrongprobe.example.net │ │ │ ├── 192.0.2.2 │ │ │ │ └── IN │ │ │ │ │ └── NS │ │ │ │ │ ├── lame.example.net │ │ │ │ │ ├── noprobe.example.net │ │ │ │ │ ├── wrongprobe.example.net │ │ │ │ │ └── 0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa │ │ │ ├── 192.0.2.11 │ │ │ │ └── IN │ │ │ │ │ └── AAAA │ │ │ │ │ └── cubyh.autoreverse.example.net │ │ │ ├── 2001_db8__94da_d800_781 │ │ │ │ └── IN │ │ │ │ │ └── AAAA │ │ │ │ │ └── cubyh.autoreverse.a.b.c.example.net │ │ │ └── 2001_db8__a_1 │ │ │ │ └── IN │ │ │ │ └── PTR │ │ │ │ └── d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa │ │ └── lookup │ │ │ └── IN │ │ │ ├── A │ │ │ ├── a.ns.example.net │ │ │ ├── b.ns.example.net │ │ │ ├── ns.noprobe.example.net │ │ │ └── ns.wrongprobe.example.net │ │ │ ├── AAAA │ │ │ ├── a.ns.example.net │ │ │ ├── ns.arpa.example.net │ │ │ └── ns.ip6.example.net │ │ │ └── NS │ │ │ ├── 192.in-addr.arpa │ │ │ ├── 8.b.d.0.1.0.0.2.ip6.arpa │ │ │ ├── example.net │ │ │ └── example.org │ └── authority │ │ └── lookup │ │ └── IN │ │ ├── AAAA │ │ └── ns1.autoreverse.example.net │ │ └── A │ │ ├── ns1.autoreverse.example.net │ │ └── ns2.autoreverse.example.net ├── structs.go ├── authority_test.go ├── find_test.go └── authority.go ├── mock ├── resolver │ ├── testdata │ │ ├── lookup │ │ │ └── IN │ │ │ │ ├── A │ │ │ │ ├── example.mock │ │ │ │ └── www.apple.com │ │ │ │ └── AAAA │ │ │ │ └── www.apple.com │ │ └── exchange │ │ │ └── 192.0.2.254 │ │ │ └── CH │ │ │ └── MX │ │ │ └── a.ns.example.net │ ├── doc.go │ ├── resolver_test.go │ ├── file.go │ └── resolver.go ├── doc.go ├── dns │ ├── server.go │ ├── exchange.go │ └── axfr.go ├── netaddr.go ├── iowriter.go └── responsewriter.go ├── testdata ├── discover │ ├── lookup │ │ └── IN │ │ │ ├── A │ │ │ ├── a.ns.example.net │ │ │ ├── b.ns.example.net │ │ │ └── ns.noprobe.example.net │ │ │ ├── AAAA │ │ │ ├── a.ns.example.net │ │ │ ├── ns.arpa.example.net │ │ │ └── ns.ip6.example.net │ │ │ └── NS │ │ │ ├── 192.in-addr.arpa │ │ │ ├── 8.b.d.0.1.0.0.2.ip6.arpa │ │ │ ├── example.net │ │ │ └── example.org │ └── exchange │ │ ├── 2001_db8__c_1 │ │ └── IN │ │ │ └── PTR │ │ │ └── 17.2.0.192.in-addr.arpa │ │ ├── 192.0.2.1 │ │ └── IN │ │ │ └── NS │ │ │ ├── 2.0.192.in-addr.arpa │ │ │ ├── 4.0.192.in-addr.arpa │ │ │ └── autoreverse.example.net │ │ ├── 192.0.2.11 │ │ └── IN │ │ │ └── AAAA │ │ │ └── cubyh.autoreverse.example.net │ │ ├── 192.0.2.2 │ │ └── IN │ │ │ └── NS │ │ │ ├── noprobe.example.net │ │ │ └── 0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa │ │ └── 2001_db8__a_1 │ │ └── IN │ │ └── PTR │ │ ├── b.9.9.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa │ │ ├── b.a.b.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa │ │ └── d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa └── loadzones │ ├── example.com.zone │ ├── bad.example.zone │ ├── 8.b.d.0.1.0.0.2.ip6.arpa.zone │ └── example.net.zone ├── manpage.go ├── dnsutil ├── doc.go ├── consts.go ├── chomp.go ├── chomp_test.go ├── synthesize_test.go ├── indomain_test.go ├── shorten_test.go ├── delegation.go ├── indomain.go ├── ip.go ├── shorten.go ├── isequal.go ├── ip_test.go ├── tostring_test.go ├── synthesize.go ├── deduce.go ├── tostring.go ├── delegation_test.go ├── isequal_test.go ├── deduce_test.go ├── invert_test.go ├── invert.go └── pretty_test.go ├── version.go ├── osutil ├── constrain_windows.go ├── signal_windows.go ├── signal_unix.go ├── constrain_test.go └── constrain_unix.go ├── .gitignore ├── database ├── getter_test.go ├── doc.go ├── getter.go ├── tree_test.go └── tree.go ├── generate_usage.sh ├── resolver ├── doc.go ├── log_test.go ├── resolver.go ├── resolver_test.go ├── log.go ├── exchange.go └── interface.go ├── go.mod ├── .github └── workflows │ ├── go.yml │ ├── codecov.yml │ └── codeql-analysis.yml ├── config_test.go ├── generate_version.sh ├── autoreverse_test.go ├── log ├── doc.go ├── log_test.go └── log.go ├── ChangeLog.md ├── request_test.go ├── LICENSE ├── passthru.go ├── go.sum ├── server_test.go ├── run_test.go ├── usage_test.go ├── acceptfunc.go ├── locals.go ├── locals_test.go ├── main.go ├── passthru_test.go ├── QUICKSTART.md ├── authorities_test.go ├── stats_test.go ├── Makefile ├── run.go ├── stats.go ├── server.go ├── authorities.go ├── chaos_test.go ├── autoreverse.go ├── config.go └── discover.go /AUTHORS: -------------------------------------------------------------------------------- 1 | Mark Delany 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/nxdomain.example.net: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/resolver/testdata/lookup/IN/A/example.mock: -------------------------------------------------------------------------------- 1 | A:example.mock. IN A 127.0.0.1 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/A/a.ns.example.net: -------------------------------------------------------------------------------- 1 | A:a.ns.example.net. IN A 192.0.2.1 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/A/b.ns.example.net: -------------------------------------------------------------------------------- 1 | A:b.ns.example.net. IN A 192.0.2.2 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/A/a.ns.example.net: -------------------------------------------------------------------------------- 1 | A:a.ns.example.net. IN A 192.0.2.1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/A/b.ns.example.net: -------------------------------------------------------------------------------- 1 | A:b.ns.example.net. IN A 192.0.2.2 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/AAAA/a.ns.example.net: -------------------------------------------------------------------------------- 1 | A:a.ns.example.net. IN AAAA 2001:db8::a:1 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/NS/192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | A:192.in-addr.arpa. IN NS a.ns.example.net. 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/AAAA/a.ns.example.net: -------------------------------------------------------------------------------- 1 | A:a.ns.example.net. IN AAAA 2001:db8::a:1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/NS/192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | A:192.in-addr.arpa. IN NS a.ns.example.net. 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/A/ns.noprobe.example.net: -------------------------------------------------------------------------------- 1 | A:ns.wrongprobe.example.net. IN A 192.0.2.31 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/AAAA/ns.arpa.example.net: -------------------------------------------------------------------------------- 1 | A:ns.arpa.example.net. IN AAAA 2001:db8::c:1 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/AAAA/ns.ip6.example.net: -------------------------------------------------------------------------------- 1 | A:ns.ip6.example.net. IN AAAA 2001:db8::a:1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/A/ns.noprobe.example.net: -------------------------------------------------------------------------------- 1 | A:ns.wrongprobe.example.net. IN A 192.0.2.31 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/AAAA/ns.arpa.example.net: -------------------------------------------------------------------------------- 1 | A:ns.arpa.example.net. IN AAAA 2001:db8::c:1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/AAAA/ns.ip6.example.net: -------------------------------------------------------------------------------- 1 | A:ns.ip6.example.net. IN AAAA 2001:db8::a:1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/A/ns.wrongprobe.example.net: -------------------------------------------------------------------------------- 1 | A:ns.wrongprobe.example.net. IN A 192.0.2.30 2 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/NS/8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | A:8.b.d.0.1.0.0.2.ip6.arpa IN NS b.ns.example.net. 2 | -------------------------------------------------------------------------------- /delegation/testdata/authority/lookup/IN/AAAA/ns1.autoreverse.example.net: -------------------------------------------------------------------------------- 1 | A:ns1.autoreverse.example.net IN AAAA ::1 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/NS/8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | A:8.b.d.0.1.0.0.2.ip6.arpa IN NS b.ns.example.net. 2 | -------------------------------------------------------------------------------- /mock/resolver/testdata/lookup/IN/A/www.apple.com: -------------------------------------------------------------------------------- 1 | A:www.apple.com. IN A 17.0.0.1 2 | A:www.apple.com. IN A 17.0.0.2 3 | -------------------------------------------------------------------------------- /delegation/testdata/authority/lookup/IN/A/ns1.autoreverse.example.net: -------------------------------------------------------------------------------- 1 | A:ns1.autoreverse.example.net IN A 192.0.2.23 2 | -------------------------------------------------------------------------------- /delegation/testdata/authority/lookup/IN/A/ns2.autoreverse.example.net: -------------------------------------------------------------------------------- 1 | A:ns2.autoreverse.example.net IN A 192.0.2.73 2 | -------------------------------------------------------------------------------- /manpage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed autoreverse.8 8 | var Manpage []byte 9 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/NS/example.net: -------------------------------------------------------------------------------- 1 | A:example.net. IN NS a.ns.example.net. 2 | A:example.net. IN NS b.ns.example.net. 3 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/2001_db8__c_1/IN/PTR/81.2.0.192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | A:81.2.0.192.in-addr.arpa. IN PTR ubyhi.example.org. 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/NS/example.net: -------------------------------------------------------------------------------- 1 | A:example.net. IN NS a.ns.example.net. 2 | A:example.net. IN NS b.ns.example.net. 3 | -------------------------------------------------------------------------------- /testdata/discover/exchange/2001_db8__c_1/IN/PTR/17.2.0.192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | A:17.2.0.192.in-addr.arpa. IN PTR ocpmi.autoreverse.example.net. 2 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.30/IN/AAAA/cubyh.wrongprobe.example.net: -------------------------------------------------------------------------------- 1 | A:cubyh.wrongprobe.example.net. IN AAAA 2001:db8:94da::bad:1 2 | -------------------------------------------------------------------------------- /mock/resolver/testdata/lookup/IN/AAAA/www.apple.com: -------------------------------------------------------------------------------- 1 | A:www.apple.com. IN AAAA 2620:149:ae0::80 2 | A:www.apple.com. IN AAAA 2620:149:ae0::443 3 | -------------------------------------------------------------------------------- /testdata/loadzones/example.com.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | @ IN SOA internal.example.com. hostmaster.example.com. 1639200485 3 3 1209600 480 3 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.2/IN/NS/lame.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A lame delegation 3 | ;; 4 | N:lame.example.net. 300 IN NS ns.lame.example.net. 5 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.1/IN/NS/2.0.192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A valid delegation 3 | ;; 4 | N:2.0.192.in-addr.arpa. 300 IN NS ns.arpa.example.net. 5 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.1/IN/NS/4.0.192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A valid delegation 3 | ;; 4 | N:4.0.192.in-addr.arpa. 300 IN NS ns.arpa.example.net. 5 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/reserr.example.net: -------------------------------------------------------------------------------- 1 | ;; This is an error file to trigger and error return from the mock resolver 2 | RCODE:SERVFAIL 3 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.11/IN/AAAA/cubyh.autoreverse.example.net: -------------------------------------------------------------------------------- 1 | ;; Probe response 2 | A:cubyh.autoreverse.example.net. IN AAAA 2001:db8::94da:d800:7a07 3 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/2.0.192.in-addr.arpa: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A valid delegation 3 | ;; 4 | N:2.0.192.in-addr.arpa. 300 IN NS ns.arpa.example.net. 5 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.2/IN/NS/noprobe.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A good reverse delegation 3 | ;; 4 | N:noprobe.example.net. 300 IN NS ns.noprobe.example.net. 5 | -------------------------------------------------------------------------------- /testdata/discover/lookup/IN/NS/example.org: -------------------------------------------------------------------------------- 1 | ;; Name servers without IP addresses 2 | A:example.org. IN NS a.ns.example.org. 3 | A:example.org. IN NS b.ns.example.org. 4 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.11/IN/AAAA/cubyh.autoreverse.example.net: -------------------------------------------------------------------------------- 1 | ;; Probe response 2 | A:cubyh.autoreverse.example.net. IN AAAA 2001:db8::94da:d800:7a07 3 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.2/IN/NS/noprobe.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A good reverse delegation 3 | ;; 4 | N:noprobe.example.net. 300 IN NS ns.noprobe.example.net. 5 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.2/IN/NS/wrongprobe.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A good delegation 3 | ;; 4 | N:wrongprobe.example.net. 300 IN NS ns.wrongprobe.example.net. 5 | -------------------------------------------------------------------------------- /delegation/testdata/find/lookup/IN/NS/example.org: -------------------------------------------------------------------------------- 1 | ;; Name servers without IP addresses 2 | A:example.org. IN NS a.ns.example.org. 3 | A:example.org. IN NS b.ns.example.org. 4 | -------------------------------------------------------------------------------- /mock/resolver/doc.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | /* 4 | 5 | This package cannot go in the mock directory as it creates a circular import dependency 6 | with mock.IOWriter. 7 | 8 | */ 9 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/baddelegation.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A bad delegation as it has no NS entry 3 | ;; 4 | E:ns.baddelegation.example.net. 300 IN A 192.0.2.11 5 | -------------------------------------------------------------------------------- /dnsutil/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dnsutil provides a motley collection functions which manipulated and render 3 | structs created by the "net" and "github.com/miekg/dns". 4 | */ 5 | package dnsutil 6 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/2001_db8__94da_d800_781/IN/AAAA/cubyh.autoreverse.a.b.c.example.net: -------------------------------------------------------------------------------- 1 | ;; Probe response 2 | A:cubyh.autoreverse.a.b.c.example.net. IN AAAA 2001:db8::94da:d800:7a07 3 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.2/IN/NS/0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A good reverse delegation 3 | ;; 4 | N:0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. 300 IN NS ns.ip6.example.net. 5 | -------------------------------------------------------------------------------- /mock/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package mock contains mock implementations used across multiple packages within 3 | autoreverse. Mocks unique to a particular package stay within their own package. 4 | */ 5 | package mock 6 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.2/IN/NS/0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A good reverse delegation 3 | ;; 4 | N:0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. 300 IN NS ns.ip6.example.net. 5 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | // Version is auto-generated from ChangeLog.md 5 | Version = "v1.4.0" 6 | // ReleaseDate is also auto-generated from ChangeLog.md 7 | ReleaseDate = "2023-02-14" 8 | ) 9 | -------------------------------------------------------------------------------- /testdata/discover/exchange/2001_db8__a_1/IN/PTR/b.9.9.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | A:b.9.9.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR bleea.example.org. 2 | -------------------------------------------------------------------------------- /testdata/discover/exchange/192.0.2.1/IN/NS/autoreverse.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A valid delegation 3 | ;; 4 | N:autoreverse.example.net. 300 IN NS ns.autoreverse.example.net. 5 | E:ns.autoreverse.example.net. 300 IN A 192.0.2.11 6 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/autoreverse.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; A valid delegation 3 | ;; 4 | N:autoreverse.example.net. 300 IN NS ns.autoreverse.example.net. 5 | E:ns.autoreverse.example.net. 300 IN A 192.0.2.11 6 | -------------------------------------------------------------------------------- /testdata/discover/exchange/2001_db8__a_1/IN/PTR/b.a.b.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | A:b.a.b.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR kyrwx.autoreverse.example.net. 2 | -------------------------------------------------------------------------------- /testdata/discover/exchange/2001_db8__a_1/IN/PTR/d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | ;; A good reverse probe 2 | A:d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR yhizz.example.org. 3 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/2001_db8__a_1/IN/PTR/d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa: -------------------------------------------------------------------------------- 1 | ;; A good reverse probe 2 | A:d.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR yhizz.example.org. 3 | -------------------------------------------------------------------------------- /osutil/constrain_windows.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | func Constrain(userName, groupName, chrootDir string) error { 4 | return nil 5 | } 6 | 7 | func ConstraintReport(chroot string) string { 8 | return "uid=windows gid=windows cwd=" + chroot 9 | } 10 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/autoreverse.a.b.c.example.net: -------------------------------------------------------------------------------- 1 | N:autoreverse.a.b.c.example.net. 300 IN NS ns.autoreverse.a.b.c.example.net. 2 | E:ns.autoreverse.a.b.c.example.net. 300 IN AAAA 2001:db8::94da:d800:781 3 | E:ns.autoreverse.a.b.c.example.net. 300 IN A 192.0.2.191 4 | -------------------------------------------------------------------------------- /testdata/loadzones/bad.example.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN example.net. 2 | $TTL 60 3 | subzone IN SOAx internal.example.net. hostmaster.example.net. 1636570941 16384 2048 1209600 480 4 | 5 | pluton IN A 192.0.2.123 6 | IN AAAA 2001:db8::123 7 | 8 | images IN A 192.0.2.124 9 | IN AAAA 2001:db8::124 10 | IN AAAA 2001:db8::125 11 | -------------------------------------------------------------------------------- /delegation/testdata/find/exchange/192.0.2.1/IN/NS/wrongdelegation.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; This is the wrong delegation because the returned details don't match the qName. Note the qName 3 | ;; on the NS 4 | ;; 5 | N:wrongrelative.example.net. 300 IN NS ns.wrongdelegation.example.net. 6 | E:ns.wrongdelegation.example.net. 300 IN A 192.0.2.12 7 | -------------------------------------------------------------------------------- /testdata/loadzones/8.b.d.0.1.0.0.2.ip6.arpa.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN 8.b.d.0.1.0.0.2.ip6.arpa. 2 | $TTL 120 3 | 4 | @ IN SOA internal.example.net. hostmaster.example.net. 1636863624 3 3 1209600 480 5 | 6 | 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR images.example.com. 7 | 2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR pluton.example.com. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /database/getter_test.go: -------------------------------------------------------------------------------- 1 | package database_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/markdingo/autoreverse/database" 7 | ) 8 | 9 | func TestGetter(t *testing.T) { 10 | getter := database.NewGetter() 11 | db1 := getter.Current() 12 | getter.Replace(database.NewDatabase()) 13 | db2 := getter.Current() 14 | if db1 == db2 { 15 | t.Error("Current() returned old", db1, db2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /generate_usage.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Create USAGE.md markdown from the --help output supplied on stdin 4 | 5 | cat <<'EOF' 6 | # autoreverse usage 7 | 8 | The following documentation is auto-generated with `autoreverse -h` from @latest. It may 9 | not reflect the most recent changes to @master. 10 | 11 | 12 | ``` 13 | EOF 14 | 15 | sed -e 's/[[:space:]]*$//' 16 | 17 | cat <<'EOF' 18 | ``` 19 | EOF 20 | -------------------------------------------------------------------------------- /dnsutil/consts.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | const ( 4 | V4Suffix = ".in-addr.arpa." // The leading '.' is important here as some callers 5 | V6Suffix = ".ip6.arpa." // rely on strings.HasSuffix() to label match. 6 | 7 | TCPNetwork = "tcp" // Yeah, yea, a bit silly, but case is important 8 | UDPNetwork = "udp" // so having consts here avoids pernickety errors 9 | 10 | MaxUDPSize uint16 = 1232 // Generally suggested as universally safe in edns0 11 | ) 12 | -------------------------------------------------------------------------------- /dnsutil/chomp.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | // ChompCanonicalName makes a name canonical but loses the trailing dot. For logging and 8 | // mock processing, where zones names are often converted to file names, the trailing dot 9 | // is more of a hindrance than a help, so this helps. 10 | func ChompCanonicalName(n string) string { 11 | n = dns.CanonicalName(n) 12 | if len(n) > 0 && n[len(n)-1] == '.' { 13 | n = n[:len(n)-1] 14 | } 15 | 16 | return n 17 | } 18 | -------------------------------------------------------------------------------- /dnsutil/chomp_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestChompCanonicalName(t *testing.T) { 8 | r := ChompCanonicalName("a.b.c") 9 | if r != "a.b.c" { 10 | t.Error("Chomp is modifying when it shouldn't", r) 11 | } 12 | r = ChompCanonicalName("a.b.c.") 13 | if r != "a.b.c" { 14 | t.Error("Chomp is not chomping", r) 15 | } 16 | r = ChompCanonicalName("a.b.c..") // Only chomps one dot 17 | if r != "a.b.c." { 18 | t.Error("Chomp is not chomping", r) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resolver/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package resolver defines an interface and provides a concrete implementation of a 3 | Frankestein DNS resolver service which is an amalgam of the standard go net package 4 | resolver functions and the github.com/miekg/dns package. 5 | 6 | The sole reason this package exists is to present resolving as an interface which can be 7 | mocked for testing purposes. It only covers functions used by autoreverse which reach out 8 | to the network. All other functions are accessed directly. 9 | */ 10 | package resolver 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/markdingo/autoreverse 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/dchest/siphash v1.2.3 9 | github.com/markdingo/miekgrrl v1.0.0 10 | github.com/markdingo/rrl v1.0.0 11 | github.com/miekg/dns v1.1.57 12 | github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace 13 | ) 14 | 15 | require ( 16 | golang.org/x/mod v0.12.0 // indirect 17 | golang.org/x/net v0.38.0 // indirect 18 | golang.org/x/sys v0.31.0 // indirect 19 | golang.org/x/tools v0.13.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /osutil/signal_windows.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | ) 7 | 8 | func SignalNotify(c chan os.Signal) { 9 | signal.Notify(c, os.Interrupt) 10 | } 11 | 12 | func IsSignalUSR1(s os.Signal) bool { 13 | return false 14 | } 15 | 16 | func IsSignalUSR2(s os.Signal) bool { 17 | return false 18 | } 19 | 20 | func IsSignalTERM(s os.Signal) bool { 21 | return false 22 | } 23 | 24 | func IsSignalINT(s os.Signal) bool { 25 | return s == os.Interrupt 26 | } 27 | 28 | func IsSignalHUP(s os.Signal) bool { 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | name: Build and Test 8 | strategy: 9 | matrix: 10 | go: [ 1.23.x ] 11 | runs-on: 12 | - ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ${{ matrix.go }} 18 | 19 | - name: Check out code 20 | uses: actions/checkout@v3 21 | 22 | - name: Build 23 | run: make all 24 | 25 | - name: Test 26 | run: make tests 27 | -------------------------------------------------------------------------------- /testdata/loadzones/example.net.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN example.net. 2 | @ IN SOA internal.example.net. hostmaster.example.net. 1636863624 3 3 1209600 480 3 | IN NS ns1 4 | 5 | pluton IN A 192.0.2.123 6 | IN AAAA 2001:db8::123 7 | 8 | images IN A 192.0.2.124 9 | IN AAAA 2001:db8::124 10 | IN AAAA 2001:db8::125 11 | 12 | frodo IN MX 10 palace ;; ignored 13 | 14 | google IN CNAME dns.google. ;; Thanks Mr Google - hope u don't mind 15 | ;; Should return two A RRs and two AAAA RRs 16 | ;; 17 | ;; From a testing perspective, this zone should results in 9 PTRs 18 | ;; 19 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/markdingo/autoreverse/log" 8 | "github.com/markdingo/autoreverse/mock" 9 | ) 10 | 11 | // Why not? 12 | func TestVersion(t *testing.T) { 13 | out := &mock.IOWriter{} 14 | log.SetOut(out) 15 | cfg := newConfig() 16 | cfg.printVersion() 17 | got := out.String() 18 | if !strings.Contains(got, "Program:") || 19 | !strings.Contains(got, "Project:") || 20 | !strings.Contains(got, "Inspiration:") || 21 | !strings.Contains(got, programName) || 22 | !strings.Contains(got, cfg.projectURL) { 23 | t.Error("Unexpected version output", got) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dnsutil/synthesize_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestSynthesize(t *testing.T) { 9 | ip4 := net.ParseIP("192.0.2.199") 10 | ip6 := net.ParseIP("2001:db8::27") 11 | ptr4 := SynthesizePTR("4.example.net.", "autoreverse.example.net", ip4) 12 | ptr6 := SynthesizePTR("6.example.net.", "autoreverse.example.net", ip6) 13 | exp := "192-0-2-199.autoreverse.example.net" 14 | got := ptr4.Ptr 15 | if exp != got { 16 | t.Error("Synth PTR4 not", exp, got) 17 | } 18 | 19 | exp = "2001-db8--27.autoreverse.example.net" 20 | got = ptr6.Ptr 21 | if exp != got { 22 | t.Error("Synth PTR6 not", exp, got) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /delegation/structs.go: -------------------------------------------------------------------------------- 1 | package delegation 2 | 3 | // All exported structs are defined here. 4 | 5 | import ( 6 | "github.com/miekg/dns" 7 | 8 | "github.com/markdingo/autoreverse/resolver" 9 | ) 10 | 11 | // Finder is the container used to manage FindAndProbe requests. As you can see, it merely 12 | // contains a resolver which may or may not offer some efficiencies to the find process. 13 | type Finder struct { 14 | resolver resolver.Resolver 15 | } 16 | 17 | // Results are return by Finder.Find() 18 | type Results struct { 19 | ProbeSuccess bool // If probe responded as desired 20 | Respondent dns.RR // Name server which answered the probe 21 | Parent *Authority 22 | Target *Authority 23 | } 24 | -------------------------------------------------------------------------------- /mock/dns/server.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | // StartServer is a clone of the real code to start up a miekg DNS server. 8 | func StartServer(net, serverAddr string, h dns.Handler) *dns.Server { 9 | srv := &dns.Server{Net: net, Addr: serverAddr, Handler: h} 10 | hasStarted := make(chan struct{}) 11 | srv.NotifyStartedFunc = func() { 12 | hasStarted <- struct{}{} 13 | } 14 | 15 | go func() { 16 | err := srv.ListenAndServe() 17 | defer close(hasStarted) 18 | if err != nil { // Shutdown or real error? 19 | panic("Setup of Server failed:" + err.Error()) 20 | } 21 | }() 22 | 23 | <-hasStarted // Wait for server, one way of the other 24 | 25 | return srv 26 | } 27 | -------------------------------------------------------------------------------- /mock/netaddr.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | // netAddr is a mock replacement for the net.Addr interface. 4 | type netAddr struct { 5 | networkStr, stringStr string 6 | } 7 | 8 | // Network helps meet the net.Addr interface. 9 | func (t *netAddr) Network() string { 10 | return t.networkStr 11 | } 12 | 13 | // String helps meet the net.Addr interface. 14 | func (t *netAddr) String() string { 15 | return t.stringStr 16 | } 17 | 18 | // NewNetAddr creates a mock net.Addr with return values for Network() and String() 19 | func NewNetAddr(networkStr, stringStr string) *netAddr { 20 | t := &netAddr{networkStr: networkStr, stringStr: stringStr} 21 | if len(t.networkStr) == 0 { 22 | t.networkStr = "udp" 23 | } 24 | 25 | return t 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | codecov: 12 | name: codecov 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: [ 1.23.x ] 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Generate Coverage Report 26 | run: go test ./... -coverprofile=coverage.txt -covermode=atomic 27 | 28 | - name: Upload coverage report 29 | uses: codecov/codecov-action@v3 30 | with: 31 | file: ./coverage.txt 32 | -------------------------------------------------------------------------------- /dnsutil/indomain_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInDomain(t *testing.T) { 8 | testCases := []struct { 9 | sub, parent string 10 | expect bool 11 | }{ 12 | {"a.example.net", "example.net", true}, 13 | {"example.net", "example.net", true}, 14 | {"example.net", ".example.net", true}, 15 | {"a.example.net", ".example.net.", true}, 16 | {"a.example.org", ".example.net.", false}, 17 | {"short.example.org", "notshort.example.org.", false}, 18 | {"short.example.org", "ort.example.org.", false}, 19 | {"root", ".", true}, 20 | } 21 | 22 | for ix, tc := range testCases { 23 | if InDomain(tc.sub, tc.parent) != tc.expect { 24 | t.Error(ix, "Wrong", tc.sub, tc.parent, tc.expect) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dnsutil/shorten_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestShorten(t *testing.T) { 9 | testCases := []struct{ in, out string }{ 10 | {"", ""}, 11 | {"This should remain unchanged", ""}, 12 | {"An embedded i/o timeout is a", "Timeout"}, 13 | {"An embedded connection refused is a", "Connection refused"}, 14 | } 15 | 16 | e := ShortenLookupError(nil) 17 | if e != nil { 18 | t.Error("shorten created an error out of thin air!", e) 19 | } 20 | 21 | for ix, tc := range testCases { 22 | e = fmt.Errorf(tc.in) 23 | e = ShortenLookupError(e) 24 | exp := tc.out 25 | if len(exp) == 0 { 26 | exp = tc.in 27 | } 28 | got := e.Error() 29 | if exp != got { 30 | t.Error(ix, "Expected", exp, "Got", got) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generate_version.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Extract version and release date from ChangeLog.md 4 | 5 | cl=$1 6 | if [ -z "$cl" ]; then 7 | echo Error: Need the changelog file as parameter one >&2 8 | exit 1 9 | fi 10 | 11 | # Looking for '### version -- date' 12 | # $1 $2 $3 $4 13 | 14 | recent=`grep '^### v' $cl | head -1` 15 | if [ $? -ne 0 ]; then 16 | echo Error: changelog $cl does not contain a version heading >&2 17 | exit 1 18 | fi 19 | 20 | set -- $recent 21 | version=$2 22 | date=$4 23 | 24 | printf 'package main\n\nconst (\n' 25 | printf '\t// Version is auto-generated from ChangeLog.md\n' 26 | printf '\tVersion = "%s"\n' "${version}" 27 | printf '\t// ReleaseDate is also auto-generated from ChangeLog.md\n' 28 | printf '\tReleaseDate = "%s"\n' "${date}" 29 | printf ')\n' 30 | -------------------------------------------------------------------------------- /mock/resolver/testdata/exchange/192.0.2.254/CH/MX/a.ns.example.net: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Answer Section 3 | ;; 4 | A:a.ns.example.net. 300 IN MX 5 alt2.aspmx.l.google.com. 5 | A:a.ns.example.net. 300 IN MX 1 aspmx.l.google.com. 6 | A:a.ns.example.net. 300 IN MX 10 alt3.aspmx.l.google.com. 7 | A:a.ns.example.net. 300 IN MX 10 alt4.aspmx.l.google.com. 8 | A:a.ns.example.net. 300 IN MX 15 urkaz4ggycbffdtljwk7cpwwhv7bhlyh6o5mpx4r6vcnjfxt2sba.mx-verification.google.com. 9 | A:a.ns.example.net. 300 IN MX 5 alt1.aspmx.l.google.com. 10 | ;; 11 | ;; Authority Section 12 | ;; 13 | N:a.ns.example.net. 86400 IN NS ns-1580.awsdns-05.co.uk. 14 | N:a.ns.example.net. 86400 IN NS ns-321.awsdns-40.com. 15 | N:a.ns.example.net. 86400 IN NS ns-764.awsdns-31.net. 16 | N:a.ns.example.net. 86400 IN NS ns-1050.awsdns-03.org. 17 | ;; 18 | E:a.ns.example.net. 60 IN TXT "This is just a filler for testing purposes" 19 | -------------------------------------------------------------------------------- /resolver/log_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/markdingo/autoreverse/log" 9 | "github.com/markdingo/autoreverse/mock" 10 | ) 11 | 12 | func TestLogFunctions(t *testing.T) { 13 | var iow mock.IOWriter 14 | log.SetOut(&iow) 15 | log.SetLevel(log.DebugLevel) 16 | r := NewResolver() 17 | 18 | iow.Reset() 19 | r.LookupIPAddr(context.Background(), "www.apple.com") 20 | ll := iow.String() 21 | exp := " Dbg:res:IP#www.apple.com#" 22 | if !strings.HasPrefix(ll, exp) { 23 | t.Error("IPAddr is wrong exp: >>" + exp + "<< got >>" + ll + "<<") 24 | } 25 | 26 | iow.Reset() 27 | r.LookupNS(context.Background(), "apple.com") 28 | ll = iow.String() 29 | exp = " Dbg:res:NS#apple.com#" 30 | if !strings.HasPrefix(ll, exp) { 31 | t.Error("NS is wrong exp: >>" + exp + "<< got >>" + ll + "<<") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dnsutil/delegation.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | // ValidDelegation returns true if the message contains a standard-conforming delegation 8 | // response. 9 | // 10 | // Strictly, a valid delegation is one which is !Authoritative, has zero Answer RRs, has 11 | // at least one Ns RR and optional contains glue in Extra. 12 | func ValidDelegation(response *dns.Msg) bool { 13 | if response.MsgHdr.Rcode != dns.RcodeSuccess { 14 | return false 15 | } 16 | 17 | if len(response.Answer) > 0 { 18 | return false 19 | } 20 | 21 | if len(response.Ns) == 0 { 22 | return false 23 | } 24 | 25 | for _, rr := range response.Ns { 26 | if rr.Header().Rrtype != dns.TypeNS || rr.Header().Class != dns.ClassINET { 27 | return false 28 | } 29 | if _, ok := rr.(*dns.NS); !ok { // Belts and braces 30 | return false 31 | } 32 | } 33 | 34 | return true 35 | } 36 | -------------------------------------------------------------------------------- /autoreverse_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAutoReverseAddA(t *testing.T) { 8 | ar := newAutoReverse(nil, nil) 9 | 10 | d1 := &authority{forward: true} 11 | d1.Domain = "1.example.net." 12 | d2 := &authority{forward: true} 13 | d2.Domain = "2.example.com." 14 | d3 := &authority{forward: true} 15 | d3.Domain = "3.example.org." 16 | 17 | if !ar.addAuthority(d1) { 18 | t.Error("Add of", d1, "should have succeeded") 19 | } 20 | if !ar.addAuthority(d2) { 21 | t.Error("Add of", d2, "should have succeeded") 22 | } 23 | if !ar.addAuthority(d3) { 24 | t.Error("Add of", d3, "should have succeeded") 25 | } 26 | 27 | if ar.addAuthority(d1) { 28 | t.Error("Add of", d1, "should have failed") 29 | } 30 | if ar.addAuthority(d2) { 31 | t.Error("Add of", d2, "should have failed") 32 | } 33 | if ar.addAuthority(d3) { 34 | t.Error("Add of", d3, "should have failed") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dnsutil/indomain.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // InDomain returns true if the purported sub-domain is in-domain of the parent 10 | // domain. This function assumes two relatively well-formed domain names but makes sure 11 | // they are both Canonical before comparisons are made. In the interest of being "helpful" 12 | // the parent domain may or may not have a leading "." as that is common for a lot of 13 | // domain storage in this program. 14 | func InDomain(sub, parent string) bool { 15 | if len(parent) == 0 || parent == "." { // Root? 16 | return true 17 | } 18 | 19 | parent = dns.CanonicalName(parent) 20 | if parent[0] == '.' { 21 | parent = parent[1:] 22 | } 23 | sub = dns.CanonicalName(sub) 24 | if len(sub) < len(parent) { 25 | return false 26 | } 27 | if sub == parent { 28 | return true 29 | } 30 | 31 | return strings.HasSuffix(sub, "."+parent) 32 | } 33 | -------------------------------------------------------------------------------- /database/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package database provides a hierarchical DNS lookup mechanism. LookupRR() requires a 3 | class, type and FQDN and returns a set of RRs or an NXDOMAIN indication. 4 | 5 | Once the database has been handed to a Getter() only Lookup() calls can be made as there 6 | is no internal concurrency protection. 7 | 8 | Expected usage is: 9 | 10 | db := database.NewDatabase(config) 11 | for { 12 | db.AddRR(dns.RR) 13 | } 14 | 15 | fmt.Println("Size", db.Count()) 16 | for { 17 | rrset, nxDomain := db.LookupRR(...) 18 | } 19 | 20 | database.Getter exists to assist with switching databases atomically. 21 | 22 | For compatibility purpose, the older ptr database interfaces are also supported in 23 | compat.go. These are Add() to add a PTR and Lookup() to look up PTRs given just an ip 24 | address. These functions will presumably disappear once autoreverse is fully migrated over 25 | to the newer interfaces. 26 | */ 27 | package database 28 | -------------------------------------------------------------------------------- /dnsutil/ip.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // IPToReverseQName converts an IP address into the reverse string normally looked up in 10 | // the reverse path. It includes the reverse suffix, is fully qualified and is ready for 11 | // querying. 12 | // 13 | // An empty string is returned if the IP address cannot be parsed. 14 | // 15 | // This is not intended to be a high-speed function. 16 | func IPToReverseQName(ip net.IP) string { 17 | if ip == nil { // Emulate net.ParseIP and be nice. 18 | return "" 19 | } 20 | if ip4 := ip.To4(); ip4 != nil { 21 | return fmt.Sprintf("%d.%d.%d.%d%s", ip4[3], ip4[2], ip4[1], ip4[0], V4Suffix) 22 | } 23 | 24 | ip6 := ip.To16() 25 | if ip6 == nil { 26 | return "" 27 | } 28 | 29 | joiner := make([]string, 0, 16) 30 | for ix := 15; ix >= 0; ix-- { 31 | joiner = append(joiner, fmt.Sprintf("%x", ip6[ix]&0xf)) 32 | joiner = append(joiner, fmt.Sprintf("%x", ip6[ix]&0xf0>>4)) 33 | } 34 | 35 | return strings.Join(joiner, ".") + V6Suffix 36 | } 37 | -------------------------------------------------------------------------------- /dnsutil/shorten.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // shortenedError is a wrapped error so the caller doesn't lose the original error 8 | // context, if that is of interest to them. 9 | type shortenedError struct { 10 | msg string 11 | err error 12 | } 13 | 14 | func (t *shortenedError) Error() string { 15 | return t.msg 16 | } 17 | 18 | func (t *shortenedError) Unwrap() error { 19 | return t.err 20 | } 21 | 22 | // ShortenLookupError turns a long unwieldy error return from net.Resolver into a succinct 23 | // error in the common cases which don't require the extensive error normally returned. 24 | func ShortenLookupError(err error) error { 25 | if err == nil { 26 | return err 27 | } 28 | m := err.Error() // Shorten up the error if we can 29 | switch { 30 | case strings.Contains(m, "i/o timeout"): 31 | err = &shortenedError{msg: "Timeout", err: err} 32 | case strings.Contains(m, "connection refused"): 33 | err = &shortenedError{msg: "Connection refused", err: err} 34 | } 35 | 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /dnsutil/isequal.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // RRIsEqual returns true if the RRs are "effectively" identical. That means they are 10 | // identical excepting for TTL. Miekg does not offer an IsEqual() public function that 11 | // compares the non-header part of an RR so we use the Stringer function and compare them 12 | // as a string. A bit of a hack as we have to remove the header part of the string to 13 | // eliminate the TTL, but it works, albeit slowly. 14 | func RRIsEqual(a, b dns.RR) bool { 15 | ah := a.Header() 16 | bh := b.Header() 17 | 18 | // Do the easy stuff first 19 | if ah.Class != bh.Class || 20 | ah.Rrtype != bh.Rrtype || 21 | dns.CanonicalName(ah.Name) != dns.CanonicalName(bh.Name) { 22 | return false 23 | } 24 | 25 | // Looking equal so far, how about the payload part? 26 | 27 | ahl := len(ah.String()) 28 | bhl := len(bh.String()) 29 | as := a.String()[ahl:] 30 | bs := b.String()[bhl:] 31 | 32 | return strings.ToLower(as) == strings.ToLower(bs) 33 | } 34 | -------------------------------------------------------------------------------- /dnsutil/ip_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/markdingo/autoreverse/dnsutil" 8 | ) 9 | 10 | func TestIPToReverse(t *testing.T) { 11 | testCases := []struct{ ipStr, expect string }{ 12 | {"1.2.3.4", "4.3.2.1.in-addr.arpa."}, 13 | {"2001:db8:a:b:c::1", "1.0.0.0.0.0.0.0.0.0.0.0.c.0.0.0.b.0.0.0.a.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."}, 14 | {"x", ""}, 15 | {"", ""}, // Empty string 16 | {"0.0.0.0", "0.0.0.0.in-addr.arpa."}, 17 | {"::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."}, 18 | } 19 | 20 | for ix, tc := range testCases { 21 | var ip net.IP 22 | if len(tc.ipStr) > 0 { 23 | ip = net.ParseIP(tc.ipStr) 24 | } 25 | got := dnsutil.IPToReverseQName(ip) 26 | if got != tc.expect { 27 | t.Error(ix, "Input:", tc.ipStr, "Got", got, "Expected", tc.expect) 28 | } 29 | } 30 | } 31 | 32 | func TestIPToReverseBogus(t *testing.T) { 33 | ip := net.IP(make([]byte, 0)) 34 | if ip == nil { 35 | t.Fatal("Setup error") 36 | } 37 | got := dnsutil.IPToReverseQName(ip) 38 | if got != "" { 39 | t.Error("Expected '' from bogus IP, not", got) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dnsutil/tostring_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestClassToString(t *testing.T) { 10 | s := ClassToString(dns.ClassCHAOS) 11 | if s != "CH" { 12 | t.Error("Expected 'CH', not", s) 13 | } 14 | s = ClassToString(15000) 15 | if s != "C-15000" { 16 | t.Error("Expected C-15000, not", s) 17 | } 18 | } 19 | 20 | func TestTypeToString(t *testing.T) { 21 | s := TypeToString(dns.TypeTXT) 22 | if s != "TXT" { 23 | t.Error("Expected 'TXT', not", s) 24 | } 25 | s = TypeToString(15000) 26 | if s != "T-15000" { 27 | t.Error("Expected T-15000, not", s) 28 | } 29 | } 30 | 31 | func TestRcodeToString(t *testing.T) { 32 | s := RcodeToString(dns.RcodeRefused) 33 | if s != "REFUSED" { 34 | t.Error("Expected 'REFUSED', not", s) 35 | } 36 | s = RcodeToString(15000) 37 | if s != "r-15000" { 38 | t.Error("Expected r-15000, not", s) 39 | } 40 | } 41 | 42 | func TestOpcodeToString(t *testing.T) { 43 | s := OpcodeToString(dns.OpcodeIQuery) 44 | if s != "IQUERY" { 45 | t.Error("Expected 'IQUERY', not", s) 46 | } 47 | s = OpcodeToString(15000) 48 | if s != "o-15000" { 49 | t.Error("Expected o-15000, not", s) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /log/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package log provides global output control across the whole application. Logging comes in 3 | four levels: Silent, Major, Minor and Debug which each level more detailed than the 4 | previous. It's up to the application to decided which output belong with which 5 | level. Levels are inclusive, so, e.g., if MinorLevel is set that implies MajorLevel 6 | logging. 7 | 8 | In general an application should have *all* output go via the logging interface once it 9 | has completed successful parsing on the command line. One exception might be 10 | start-up/shut-down messages, just in case logging is not working or has been redirected to 11 | a null consumer. 12 | 13 | The Print and Printf interface are similar to the fmt versions with a few subtle 14 | differences due to the need to prefix lines. The main difference is that if the resulting 15 | string contains multiple lines they are all printed with the prefix for the logging 16 | level. The second different is that a trailing newline is not needed and excess ones are 17 | trimmed. 18 | 19 | Specialist logging functions external to this package should still use log.Out() to access 20 | the current io.Writer for the purposes of capturing output for tests. 21 | */ 22 | package log 23 | -------------------------------------------------------------------------------- /mock/iowriter.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | // IOWriter is a mock replacement for any place that accepts an io.Writer. Only used by 4 | // test programs, it appends each write to a []byte slice and makes it available via the 5 | // String() function. In the case of autoreverse, it's most often used to replace the log 6 | // package output to capture logging activity and compare it against expected. 7 | type IOWriter struct { 8 | line []byte 9 | } 10 | 11 | // Reset clears the byte slice such that String() will now return an empty string. 12 | func (t *IOWriter) Reset() { 13 | t.line = make([]byte, 0) 14 | } 15 | 16 | // Write helps meet the io.Writer interface. Is appends the bytes to the internal byte slice. 17 | func (t *IOWriter) Write(b []byte) (int, error) { 18 | t.line = append(t.line, b...) 19 | 20 | return len(b), nil 21 | } 22 | 23 | // String returns the complete byte slice as a string. The byte slice is not changed by 24 | // this function call. If you want the slice to be reset, called the Reset() function. 25 | func (t *IOWriter) String() string { 26 | return string(t.line) 27 | } 28 | 29 | // Len is a helper function which returns the size of the byte slice. 30 | func (t *IOWriter) Len() int { 31 | return len(t.line) 32 | } 33 | -------------------------------------------------------------------------------- /osutil/signal_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package osutil 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // SignalNotify asks OS to send all the main Unix signals to the supplied channel. 13 | func SignalNotify(c chan os.Signal) { 14 | signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2) 15 | } 16 | 17 | // IsSignalUSR1 returns true if the supplied signal is SIGUSR1. A noop on Windows. 18 | func IsSignalUSR1(s os.Signal) bool { 19 | return s == syscall.SIGUSR1 20 | } 21 | 22 | // IsSignalUSR2 returns true if the supplied signal is SIGUSR2. A noop on Windows. 23 | func IsSignalUSR2(s os.Signal) bool { 24 | return s == syscall.SIGUSR2 25 | } 26 | 27 | // IsSignalTERM returns true if the supplied signal is SIGTERM. A noop on Windows. 28 | func IsSignalTERM(s os.Signal) bool { 29 | return s == syscall.SIGTERM 30 | } 31 | 32 | // IsSignalINT returns true if the supplied signal is SIGINT. A noop on Windows. 33 | func IsSignalINT(s os.Signal) bool { 34 | return s == os.Interrupt 35 | } 36 | 37 | // IsSignalHUP returns true if the supplied signal is SIGHUP. A noop on Windows. 38 | func IsSignalHUP(s os.Signal) bool { 39 | return s == syscall.SIGHUP 40 | } 41 | -------------------------------------------------------------------------------- /dnsutil/synthesize.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | const ( 11 | obeyRFC = true 12 | ) 13 | 14 | // SynthesizePTR converts an IP address into a synthetic PTR. Mainly it just string 15 | // substitutes ":" and "." in ip addresses to "-" which is an acceptable 16 | // character. 17 | // 18 | // According to rfc1035 Ptr has to hold a which is constrainted to 19 | // "let-dig-hyp", but I'll bet if the Ptr data contained "." and ":" (which would allow an 20 | // *exact* representation of the query address) that almost nothing would care. If you 21 | // want to toy with that idea, set obeyRFC to false. 22 | // 23 | // The suffix parameter is assumed to be canonical. 24 | func SynthesizePTR(qname, suffix string, ip net.IP) *dns.PTR { 25 | ptr := new(dns.PTR) 26 | ptr.Hdr.Name = qname 27 | ptr.Hdr.Class = dns.ClassINET 28 | ptr.Hdr.Rrtype = dns.TypePTR 29 | // ptr.Hdr.Ttl = 60 // Set by caller 30 | 31 | s := ip.String() 32 | if obeyRFC { 33 | s = strings.ReplaceAll(s, ":", "-") 34 | s = strings.ReplaceAll(s, ".", "-") 35 | } 36 | 37 | if len(suffix) > 0 { // The normal use-case 38 | s += "." + suffix 39 | } 40 | 41 | ptr.Ptr = s 42 | 43 | return ptr 44 | } 45 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ### v1.4.0 - 2023-02-14 2 | * Added RRL support with https://github.com/markdingo/rrl 3 | # autoreverse Change Log 4 | * Replace dns.MsgAcceptFunc to ensure rfc7873#5.4 queries are accepted 5 | ### v1.3.0 - 2022-02-10 6 | * Move cmd code to top level so "go install" just works 7 | * Remove compact logging - it's confusing and doesn't save much 8 | * Add missing qType=PTR test to synth checks 9 | * Don't bother calling synth functions for qName == authority 10 | * Adapt stats reporting to new query logic 11 | ### v1.2.0 -- 2022-01-27 12 | * Refactor query dispatch logic to take advantage of the tree database 13 | * Reimplement database as a tree to better distinguish NXDomain vs NOError 14 | * Have ConstraintReport() report the chroot path 15 | * Wrap all returned errors via fmt.Errorf() with original error 16 | * More sophisticated treatment of cookie timestamps 17 | ### v1.1.0 -- 2021-12-23 18 | * Add rfc7873, rfc9018 DNS Cookie support 19 | * Add a trigger reason to the zone reload log message 20 | * Use dnsutil.*ToString instead of dns.*ToString to render unmapped values 21 | * Fix most concerns expressed by https://goreportcard.com/ 22 | ### v1.0.1 -- 2021-12-14 23 | * Rename ":" test files to "_" to help github and zip 24 | ### v1.0.0 -- 2021-12-13 25 | * Initial public release. 26 | -------------------------------------------------------------------------------- /dnsutil/deduce.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | // DeducePtr converts a dns.A/AAAA RR into a dns.PTR or returns the supplied RR if it's 8 | // already a recognizable/convertable PTR. If the wrong type of RR is supplied a nil value 9 | // is returned. The returned "key" value is effectively the IP address expressed as a 10 | // string regardless of the RR type. It can be used by callers who want to reference the 11 | // PTR via the original or extracted IP address. 12 | func DeducePtr(rr dns.RR) (ptr *dns.PTR, key string) { 13 | switch rrt := rr.(type) { 14 | case *dns.A: 15 | ptr = &dns.PTR{} 16 | ptr.Hdr.Name = IPToReverseQName(rrt.A) 17 | ptr.Hdr.Rrtype = dns.TypePTR 18 | ptr.Hdr.Class = rrt.Hdr.Class 19 | ptr.Hdr.Ttl = rrt.Hdr.Ttl 20 | ptr.Ptr = rrt.Hdr.Name 21 | key = rrt.A.String() 22 | 23 | case *dns.AAAA: 24 | ptr = &dns.PTR{} 25 | ptr.Hdr.Name = IPToReverseQName(rrt.AAAA) 26 | ptr.Hdr.Rrtype = dns.TypePTR 27 | ptr.Hdr.Class = rrt.Hdr.Class 28 | ptr.Hdr.Ttl = rrt.Hdr.Ttl 29 | ptr.Ptr = rrt.Hdr.Name 30 | key = rrt.AAAA.String() 31 | 32 | case *dns.PTR: 33 | ip, truncated, err := InvertPtrToIP(rrt.Hdr.Name) // See if it's well-formed first 34 | if err == nil && !truncated { 35 | ptr = rrt 36 | key = ip.String() 37 | } 38 | } 39 | 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /dnsutil/tostring.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // ClassToString converts an miekg class to a string, but if the resulting string is empty 10 | // it's replaced with the numeric value. 11 | func ClassToString(c dns.Class) (s string) { 12 | s = dns.ClassToString[uint16(c)] 13 | if len(s) == 0 { 14 | s = fmt.Sprintf("C-%d", c) 15 | } 16 | 17 | return 18 | } 19 | 20 | // TypeToString converts an miekg type to a string, but if the resulting string is empty 21 | // it's replaced with the numeric value. 22 | func TypeToString(t uint16) (s string) { 23 | s = dns.TypeToString[t] 24 | if len(s) == 0 { 25 | s = fmt.Sprintf("T-%d", t) 26 | } 27 | 28 | return 29 | } 30 | 31 | // RcodeToString converts an miekg rcode to a string, but if the resulting string is empty 32 | // it's replaced with the numeric value. 33 | func RcodeToString(r int) (s string) { 34 | s = dns.RcodeToString[r] 35 | if len(s) == 0 { 36 | s = fmt.Sprintf("r-%d", r) 37 | } 38 | 39 | return 40 | } 41 | 42 | // OpcodeToString converts an miekg opcode to a string, but if the resulting string is 43 | // empty it's replaced with the numeric value. 44 | func OpcodeToString(o int) (s string) { 45 | s = dns.OpcodeToString[o] 46 | if len(s) == 0 { 47 | s = fmt.Sprintf("o-%d", o) 48 | } 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /osutil/constrain_test.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // This function is virtually impossible to test within the Go test framework as a single 10 | // successful test means no others can possibly run as we've thrown away all our 11 | // rights. All we can do is test a few of the error paths and you'll have to have faith 12 | // that the successful code paths have been manually tested... 13 | func TestConstrain(t *testing.T) { 14 | if os.Getuid() != 0 { 15 | t.Log("Warning: Cannot test osutil.Constrain() as we're not running as root") 16 | } 17 | err := Constrain("bogusUser", "", "") 18 | if err == nil { 19 | t.Error("Expected Error Return with bogusUser") 20 | } else { 21 | if !strings.Contains(err.Error(), "unknown user") { 22 | t.Error("Did not get unknown user in ", err) 23 | } 24 | } 25 | 26 | err = Constrain("", "bogusGroup", "") 27 | if err == nil { 28 | t.Error("Expected Error Return with bogusGroup") 29 | } else { 30 | if !strings.Contains(err.Error(), "unknown group") { 31 | t.Error("Did not get unknown group in ", err) 32 | } 33 | } 34 | } 35 | 36 | // This is a pretty lame test 37 | func TestReport(t *testing.T) { 38 | rep := ConstraintReport("/tmp") 39 | if !strings.Contains(rep, "uid=") { 40 | t.Error("ConstraintReport is really bruk", rep) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/markdingo/rrl" 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | "github.com/markdingo/autoreverse/log" 11 | "github.com/markdingo/autoreverse/mock" 12 | ) 13 | 14 | // Meet the net.Addr interface so we can mock into the request struct 15 | type mockNetAddr struct { 16 | net string 17 | str string 18 | } 19 | 20 | func (t *mockNetAddr) Network() string { 21 | return t.net 22 | } 23 | 24 | func (t *mockNetAddr) String() string { 25 | return t.str 26 | } 27 | 28 | func TestRequestLog(t *testing.T) { 29 | out := &mock.IOWriter{} 30 | log.SetOut(out) 31 | 32 | req := &request{} 33 | req.network = dnsutil.TCPNetwork 34 | req.compressed = true 35 | req.truncated = true 36 | req.response = new(dns.Msg) 37 | req.question = dns.Question{} 38 | req.src = &mockNetAddr{} 39 | req.log() 40 | 41 | got := out.String() 42 | exp := "ru=ne q=None/ s= id=0 h=Tzt sz=0/0 C=0/0/0\n" 43 | if exp != got { 44 | t.Error("Log wrong. Exp", exp, "Got", got) 45 | } 46 | 47 | out = &mock.IOWriter{} 48 | log.SetOut(out) 49 | req.rrlAction = rrl.Drop 50 | req.log() 51 | 52 | got = out.String() 53 | exp = "ru=ne/D q=None/ s= id=0 h=Tzt sz=0/0 C=0/0/0\n" 54 | if exp != got { 55 | t.Error("Log wrong. Exp", exp, "Got", got) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, 2022, 2023 Mark Delany 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /dnsutil/delegation_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestValidDelegation(t *testing.T) { 10 | m := new(dns.Msg) 11 | res := ValidDelegation(m) 12 | if res { 13 | t.Error("Did not expect an empty message to be a valid delegation") 14 | } 15 | 16 | m.MsgHdr.Rcode = dns.RcodeSuccess 17 | ns, err := dns.NewRR("example.net. IN NS a.ns.example.net") 18 | if err != nil { 19 | t.Fatal("Setup error", err) 20 | } 21 | m.Ns = append(m.Ns, ns) 22 | 23 | res = ValidDelegation(m) 24 | if !res { 25 | t.Error("Did not expect good message to fail", m) 26 | } 27 | 28 | mgood := *m // Save this good one as we re-use it as a template 29 | m.MsgHdr.Rcode = dns.RcodeNameError 30 | if ValidDelegation(m) { 31 | t.Error("Bad rcode should not be valid") 32 | } 33 | 34 | *m = mgood 35 | m.Answer = append(m.Answer, new(dns.A)) 36 | if ValidDelegation(m) { 37 | t.Error("Ans > 0 should fail") 38 | } 39 | 40 | *m = mgood 41 | nsBad, err := dns.NewRR("example.net. IN NS a.ns.example.net") 42 | if err != nil { 43 | t.Fatal("Setup error", err) 44 | } 45 | nsBad.Header().Class = dns.ClassCHAOS 46 | m.Ns = append(m.Ns, nsBad) 47 | if ValidDelegation(m) { 48 | t.Error("Wrong Class in Ns should fail") 49 | } 50 | 51 | *m = mgood 52 | a, _ := dns.NewRR("example.net. IN A 127.0.0.1") 53 | a.Header().Rrtype = dns.TypeNS 54 | m.Ns = append(m.Ns, a) 55 | if ValidDelegation(m) { 56 | t.Error("Bogus go type case should have been detected") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /passthru.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/miekg/dns" 7 | 8 | "github.com/markdingo/autoreverse/dnsutil" 9 | "github.com/markdingo/autoreverse/resolver" 10 | ) 11 | 12 | // passthru proxies the query thru to the passthru server and sends the reply - if any - 13 | // directly back to our querying client. The query and response are more or less 14 | // transparently exchanged. No retry attempts are made not is there any transition to a 15 | // TCP query if the response is truncated. 16 | // 17 | // At this stage, each exchange involves a new socket setup via SingleExchange, if 18 | // passthru happens to become a popular feature that proxies a lot of traffic then it'll 19 | // probably be worth holding on to a socket across passthru requests. That will require 20 | // an extension to the resolver interface. 21 | func (t *server) passthru(wtr dns.ResponseWriter, req *request) { 22 | req.addNote("passthru") 23 | req.stats.gen.passthruOut++ 24 | response, _, err := t.resolver.SingleExchange(context.Background(), 25 | resolver.NewExchangeConfig(), req.query, t.cfg.passthru, "") 26 | if err != nil { 27 | req.logError = dnsutil.ShortenLookupError(err) 28 | return 29 | } 30 | 31 | req.response = response // Save for logging 32 | req.logQName = req.qName 33 | req.stats.gen.passthruIn++ 34 | 35 | req.msgSize = req.response.Len() // Stats for reporting 36 | req.compressed = req.response.Compress 37 | req.truncated = req.response.MsgHdr.Truncated 38 | 39 | err = wtr.WriteMsg(response) 40 | if err != nil { 41 | req.logError = dnsutil.ShortenLookupError(err) 42 | return 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /mock/resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/miekg/dns" 8 | 9 | real "github.com/markdingo/autoreverse/resolver" 10 | ) 11 | 12 | func TestMockResolver(t *testing.T) { 13 | r := NewResolver("./testdata") 14 | ips, err := r.LookupIPAddr(context.Background(), "example.mock") 15 | if err != nil { 16 | t.Fatal("Setup error with example.mock", err.Error()) 17 | } 18 | 19 | if len(ips) != 1 { 20 | t.Error("Expected one address, not", len(ips)) 21 | } 22 | if ips[0].String() != "127.0.0.1" { 23 | t.Error("Expected 127.0.0.1, not", ips[0].String()) 24 | } 25 | 26 | ips, err = r.LookupIPAddr(context.Background(), "www.apple.com") 27 | if err != nil { 28 | t.Fatal("Setup error with www.apple.com", err.Error()) 29 | } 30 | if len(ips) != 4 { 31 | t.Error("Expected 4 addresses for www.apple.com, not", len(ips)) 32 | } 33 | 34 | in := new(dns.Msg) 35 | in.Question = append(in.Question, dns.Question{Name: "a.ns.example.net", 36 | Qclass: dns.ClassCHAOS, Qtype: dns.TypeMX}) 37 | out, _, err := r.SingleExchange(context.Background(), real.NewExchangeConfig(), 38 | in, "192.0.2.254", "192.0.2.254") 39 | if err != nil { 40 | t.Error("Setup error for r.SingleExchange", err.Error()) 41 | } else if out == nil { 42 | t.Fatal("No out with no error from SingleExchange") 43 | } 44 | 45 | if out.MsgHdr.Rcode != dns.RcodeSuccess { 46 | t.Error("Expected Success, not", out.MsgHdr.Rcode) 47 | } 48 | if len(out.Answer) != 6 || len(out.Ns) != 4 || len(out.Extra) != 1 { 49 | t.Error("Wrong RR Count. Want 6, 4, 1. Got", 50 | len(out.Answer), len(out.Ns), len(out.Extra)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= 2 | github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= 3 | github.com/markdingo/miekgrrl v1.0.0 h1:1hMVAgSktU4YThDqy/7nFIpXHZTnq5OYpoAgsilT+jc= 4 | github.com/markdingo/miekgrrl v1.0.0/go.mod h1:Vmd2fGiT7CvZeHhYG2Vsnu/ZO/SprykTGljWcrpo0AI= 5 | github.com/markdingo/rrl v1.0.0 h1:hnlbqn8XGbk4T91QhCy56d7i/xdEctWJ7poC+lbjwO0= 6 | github.com/markdingo/rrl v1.0.0/go.mod h1:vkP0Re1+y7ZPI/oOfcG4aiXGqv2OShhznYFy9YDJbFU= 7 | github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= 8 | github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= 9 | github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= 10 | github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 11 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 12 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 13 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 14 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 15 | golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= 16 | golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 17 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 18 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 19 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 20 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 21 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/markdingo/autoreverse/log" 9 | "github.com/markdingo/autoreverse/mock" 10 | ) 11 | 12 | func TestStartServersGood(t *testing.T) { 13 | out := &mock.IOWriter{} 14 | log.SetOut(out) 15 | log.SetLevel(log.MajorLevel) 16 | ar := newAutoReverse(nil, nil) 17 | ar.cfg.listen = []string{"127.0.0.1:2056", "127.0.0.1:2057", "[::1]:2058", "[::1]:2059"} 18 | ar.startServers() 19 | ar.stopServers() 20 | exp := `Listen on: udp 127.0.0.1:2056 21 | Listen on: udp 127.0.0.1:2057 22 | Listen on: udp [::1]:2058 23 | Listen on: udp [::1]:2059 24 | Listen on: tcp 127.0.0.1:2056 25 | Listen on: tcp 127.0.0.1:2057 26 | Listen on: tcp [::1]:2058 27 | Listen on: tcp [::1]:2059 28 | ` 29 | got := out.String() 30 | 31 | // The server start up order is effectively random as each listen interface is run 32 | // as a separate go-routine so sort the log lines to eliminate order issues when 33 | // comparing against expected. 34 | 35 | gar := strings.Split(got, "\n") 36 | ear := strings.Split(exp, "\n") 37 | sort.Strings(gar) 38 | sort.Strings(ear) 39 | nGot := strings.Join(gar, "\n") 40 | nExp := strings.Join(ear, "\n") 41 | 42 | if nGot != nExp { 43 | t.Error("Log mismatch. Exp", nExp, "\nGot", nGot) 44 | } 45 | } 46 | 47 | // Exercise the error path of starting a server 48 | func TestStartServersBad(t *testing.T) { 49 | out := &mock.IOWriter{} 50 | log.SetOut(out) 51 | log.SetLevel(log.MajorLevel) 52 | ar := newAutoReverse(nil, nil) 53 | srv := newServer(ar.cfg, ar.dbGetter, ar.resolver, nil, "udp", "127.0.0.1:xx") 54 | err := ar.startServer(srv) 55 | if err == nil { 56 | t.Error("Expected server to fail due to bogus port number") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /database/getter.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Getter supports atomically switching databases on the fly - this occurs most often due 8 | // to expired zone reloads. All database access for each request should go via 9 | // Getter.Current() and go-routines should not hold on to the returned values of Getter() 10 | // for longer than a single set of related accesses (such as an SOA query which might 11 | // access SOA, NS and address RRs). 12 | // 13 | // The Getter exists because the database is read-only once populated and rather than 14 | // having update capabilities they are simply replaced. Getter makes that easier. 15 | type Getter struct { 16 | mu sync.RWMutex 17 | db *Database 18 | } 19 | 20 | // NewGetter creates a Getter with valid databases. This ensures Getter.Current() always 21 | // returns valid pointers to database structs. After a Getter is created, all access 22 | // functions are mutex protected to ensure concurrent access is ok. 23 | func NewGetter() *Getter { 24 | return &Getter{db: NewDatabase()} 25 | } 26 | 27 | // Replace the current database. The old database will eventually garbage collect 28 | // out of existence once the go-routines re-get via Current(). Replace can be called with 29 | // a nil replacement pointer, in which case Replace() does nothing. 30 | // 31 | // The replacement occurs under the protection of a mutex making it concurrency safe. 32 | func (t *Getter) Replace(newDB *Database) { 33 | if newDB == nil { 34 | return 35 | } 36 | t.mu.Lock() 37 | defer t.mu.Unlock() 38 | t.db = newDB 39 | } 40 | 41 | // Current returns the current database pointers under mutex protection. 42 | func (t *Getter) Current() *Database { 43 | t.mu.RLock() 44 | defer t.mu.RUnlock() 45 | 46 | return t.db 47 | } 48 | -------------------------------------------------------------------------------- /dnsutil/isequal_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestIsEqual(t *testing.T) { 10 | testCases := []struct{ rr1, rr2, reason string }{ 11 | {"a.example.Net. 91 IN A 192.0.2.123", 12 | "a.example.Net. 91 IN A 192.0.2.123", 13 | ""}, 14 | 15 | {"a.example.Net. 9991 IN A 192.0.2.123", 16 | "a.example.Net. 91 IN A 192.0.2.123", 17 | ""}, 18 | 19 | {"b.example.Net. 9991 IN A 192.0.2.123", 20 | "a.example.Net. 91 IN A 192.0.2.123", 21 | "qName"}, 22 | 23 | {"a.example.Net. 9991 IN A 192.0.2.123", 24 | "a.example.Net. 91 IN A 192.0.2.124", 25 | "trailing byte"}, 26 | 27 | {"a.example.Net. 9991 IN AAAA ::1", 28 | "a.example.Net. 91 IN A 192.0.2.124", 29 | "Type"}, 30 | {"a.example.Net. 9991 IN AAAA ::1", 31 | "a.example.Net. 91 IN AAAA ::1", 32 | ""}, 33 | {"a.example.Net. 9991 IN AAAA 2001:db8::1", 34 | "a.example.Net. 91 IN AAAA 2001:db8::2", 35 | "trailing byte"}, 36 | {"a.example.Net. 9991 IN AAAA 2001:db8::1", 37 | "a.example.Net. 91 IN AAAA 1001:db8::1", 38 | "leading byte"}, 39 | 40 | {"a.example.Org. 9991 IN PTR This.Is.A.ptr", 41 | "a.example.Org. 9991 IN PTR This.is.a.PTR", 42 | ""}, 43 | {"a.example.Org. 9991 IN PTR This.Is.A.Ptr", 44 | "A.EXAMPLE.Org. 9991 IN PTR This.Is.b.Ptr", 45 | "Ptr text"}, 46 | } 47 | 48 | for ix, tc := range testCases { 49 | rr1, err := dns.NewRR(tc.rr1) 50 | if err != nil { 51 | t.Fatal(ix, "Setup failed", err) 52 | } 53 | rr2, err := dns.NewRR(tc.rr2) 54 | if err != nil { 55 | t.Fatal(ix, "Setup failed", err) 56 | } 57 | 58 | if RRIsEqual(rr1, rr2) { 59 | if len(tc.reason) > 0 { 60 | t.Error(ix, "Expected difference", tc.reason, rr1, rr2) 61 | } 62 | } else { 63 | if len(tc.reason) == 0 { 64 | t.Error(ix, "Expected Equal", rr1, rr2) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | "github.com/markdingo/autoreverse/log" 9 | ) 10 | 11 | type resolver struct { 12 | netResolver net.Resolver 13 | 14 | // Currently these timeout and retry values cannot be changed from the defaults. 15 | // Let's see if there is ever any real need to change them prior to adding an 16 | // adjustment capability. 17 | singleExchangeTimeout, fullExchangeTimeout time.Duration 18 | 19 | queryTries int 20 | } 21 | 22 | // NewResolver creates a fully formed resolver which is ready to use. 23 | func NewResolver() *resolver { 24 | t := &resolver{ 25 | singleExchangeTimeout: defaultSingleExchangeTimeout, 26 | fullExchangeTimeout: defaultfFullExchangeTimeout, 27 | queryTries: defaultQueryTries, 28 | } 29 | 30 | return t 31 | } 32 | 33 | func (t *resolver) LookupNS(ctx context.Context, name string) ([]string, error) { 34 | ctxWithTO, cancel := context.WithDeadline(ctx, time.Now().Add(t.singleExchangeTimeout)) 35 | defer cancel() 36 | nsSet, err := t.netResolver.LookupNS(ctxWithTO, name) 37 | if log.IfDebug() { 38 | LogNS(name, nsSet, "", err) 39 | } 40 | 41 | if err != nil { 42 | return []string{}, err 43 | } 44 | 45 | nss := make([]string, 0, len(nsSet)) 46 | for _, n := range nsSet { 47 | nss = append(nss, n.Host) 48 | } 49 | 50 | return nss, nil 51 | } 52 | 53 | func (t *resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IP, error) { 54 | ctxWithTO, cancel := context.WithDeadline(ctx, time.Now().Add(t.singleExchangeTimeout)) 55 | defer cancel() 56 | addrs, err := t.netResolver.LookupIPAddr(ctxWithTO, host) 57 | if log.IfDebug() { 58 | LogIP(host, addrs, "", err) 59 | } 60 | if err != nil { 61 | return []net.IP{}, err 62 | } 63 | 64 | ips := make([]net.IP, 0, len(addrs)) 65 | for _, a := range addrs { 66 | ips = append(ips, a.IP) 67 | } 68 | 69 | return ips, nil 70 | } 71 | -------------------------------------------------------------------------------- /mock/dns/exchange.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // ExchangeResponse is set by the caller to inform the ExchangeServer what the next 11 | // response should contain. 12 | type ExchangeResponse struct { 13 | Ignore bool 14 | Truncated bool 15 | Rcode int 16 | Ns []dns.RR 17 | Answer []dns.RR 18 | Extra []dns.RR 19 | 20 | QueryCount int // Times mockDNSHandler served this mockExchangeResponse 21 | } 22 | 23 | // ExchangeServer is a mock replacement for a miekg dns.Handler. Used only for tests. It's 24 | // a dumb server which does nothing more than copies the ExchangeResponse values into the 25 | // reply message. It never checks the input or anything like that. 26 | type ExchangeServer struct { 27 | mu sync.Mutex 28 | resp *ExchangeResponse 29 | } 30 | 31 | // SetResponse sets a new response for the next query 32 | func (t *ExchangeServer) SetResponse(r *ExchangeResponse) { 33 | t.mu.Lock() 34 | defer t.mu.Unlock() 35 | t.resp = r 36 | } 37 | 38 | // GetResponse returns the current response as set 39 | func (t *ExchangeServer) GetResponse() *ExchangeResponse { 40 | t.mu.Lock() 41 | defer t.mu.Unlock() 42 | return t.resp 43 | } 44 | 45 | // ServeDNS meets the interface definition for dns.Handler 46 | func (t *ExchangeServer) ServeDNS(wtr dns.ResponseWriter, q *dns.Msg) { 47 | resp := t.GetResponse() 48 | if resp == nil { 49 | panic("resp == nil in mock exchange server") 50 | } 51 | resp.QueryCount++ 52 | if resp.Ignore { 53 | return 54 | } 55 | 56 | m := new(dns.Msg) 57 | m.SetRcode(q, resp.Rcode) 58 | if resp.Truncated { 59 | m.MsgHdr.Truncated = true 60 | } else if resp.Rcode == dns.RcodeSuccess { // Only populate if rcode is good 61 | m.Ns = resp.Ns 62 | m.Answer = resp.Answer 63 | m.Extra = resp.Extra 64 | } 65 | 66 | err := wtr.WriteMsg(m) 67 | if err != nil { 68 | fmt.Println("Alert: WriteMsg error:", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/markdingo/autoreverse/log" 9 | "github.com/markdingo/autoreverse/mock" 10 | ) 11 | 12 | func TestResolver(t *testing.T) { 13 | out := &mock.IOWriter{} 14 | log.SetOut(out) 15 | log.SetLevel(log.DebugLevel) // Turns on logging in resolver 16 | 17 | res := NewResolver() 18 | 19 | nss, err := res.LookupNS(context.Background(), "apple.com.") 20 | if err != nil { 21 | t.Fatal("apple.com no longer exists?", err) 22 | } 23 | nsc := 0 24 | ipc := 0 25 | for _, ns := range nss { 26 | if strings.Contains(ns, "apple") { 27 | nsc++ 28 | ips, err := res.LookupIPAddr(context.Background(), ns) 29 | if err != nil { 30 | t.Error("IP lookup failed for", ns, err) 31 | continue 32 | } 33 | for _, ip := range ips { 34 | s := ip.String() 35 | if strings.Contains(s, "17.") || strings.Contains(s, "2620:149") { 36 | ipc++ 37 | } 38 | } 39 | } 40 | } 41 | if nsc == 0 { 42 | t.Error("Apple.com has no in-domain NSes?", nss) 43 | } 44 | 45 | if ipc == 0 { 46 | t.Error("No apple.com name servers are served in-house?") 47 | } 48 | 49 | _, err = res.LookupNS(context.Background(), "broken name") 50 | if err == nil { 51 | t.Fatal("expected error return with borken name") 52 | } 53 | 54 | got := out.String() 55 | if !strings.Contains(got, "Dbg:res:NS#apple.com") { 56 | t.Error("Expected log to contain apple.com", got) 57 | } 58 | if !strings.Contains(got, "no such host") { 59 | t.Error("Expected log to contain no such host for broken name", got) 60 | } 61 | } 62 | 63 | func TestResolverBadHost(t *testing.T) { 64 | out := &mock.IOWriter{} 65 | log.SetOut(out) 66 | res := NewResolver() 67 | 68 | // We just need to make sure it's an error return 69 | _, err := res.LookupIPAddr(context.Background(), "Bad Host Name") 70 | if err == nil { 71 | t.Fatal("Expected an error return with a bad host name") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /mock/responsewriter.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | var ( 10 | local = NewNetAddr("udp", "127.0.0.1:53") 11 | remote = NewNetAddr("udp", "127.0.0.2:4056") 12 | ) 13 | 14 | // ResponseWriter is a mock replacement for the miekg dns.ResponseWriter. It's used for 15 | // tests only. It contains a response message that is arbitrarily returned. 16 | type ResponseWriter struct { 17 | m *dns.Msg // Saved by writeMsg 18 | } 19 | 20 | // Reset clears the response message 21 | func (t *ResponseWriter) Reset() { 22 | t.m = nil 23 | } 24 | 25 | // Get returns the last response, if any then clears the response 26 | func (t *ResponseWriter) Get() *dns.Msg { 27 | m := t.m 28 | t.m = nil 29 | return m 30 | } 31 | 32 | // LocalAddr helps meet the dns.ResponseWriter interface 33 | func (t *ResponseWriter) LocalAddr() (a net.Addr) { 34 | return local 35 | } 36 | 37 | // RemoteAddr helps meet the dns.ResponseWriter interface 38 | func (t *ResponseWriter) RemoteAddr() (r net.Addr) { 39 | return remote 40 | } 41 | 42 | // WriteMsg helps meet the dns.ResponseWriter interface 43 | func (t *ResponseWriter) WriteMsg(m *dns.Msg) (e error) { 44 | t.m = m 45 | 46 | return 47 | } 48 | 49 | // Write helps meet the dns.ResponseWriter interface 50 | func (t *ResponseWriter) Write(b []byte) (l int, e error) { 51 | // l = len(b) 52 | // return 53 | 54 | panic("Don't expect Write() to be called") 55 | } 56 | 57 | // Close helps meet the dns.ResponseWriter interface. It is a no-op. 58 | func (t *ResponseWriter) Close() (e error) { 59 | return 60 | } 61 | 62 | // TsigStatus helps meet the dns.ResponseWriter interface. It is a no-op. 63 | func (t *ResponseWriter) TsigStatus() (e error) { 64 | return 65 | } 66 | 67 | // TsigTimersOnly helps meet the dns.ResponseWriter interface. It is a no-op. 68 | func (t *ResponseWriter) TsigTimersOnly(bool) { 69 | return 70 | } 71 | 72 | // Hijack helps meet the dns.ResponseWriter interface. It is a no-op. 73 | func (t *ResponseWriter) Hijack() { 74 | } 75 | -------------------------------------------------------------------------------- /run_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "syscall" 7 | "testing" 8 | "time" 9 | 10 | "github.com/markdingo/autoreverse/database" 11 | "github.com/markdingo/autoreverse/log" 12 | "github.com/markdingo/autoreverse/mock" 13 | "github.com/markdingo/autoreverse/resolver" 14 | ) 15 | 16 | func TestRun(t *testing.T) { 17 | testCases := []string{ 18 | "Local Forward Zone", 19 | "IN/SOA", 20 | "Zone Authority", 21 | "Load Zones Of Authority", 22 | "Load Chaos", 23 | programName, 24 | "Ready", 25 | "Stats: Uptime", 26 | "Stats: Total q=0", 27 | "Stats: A Ptr q=0", 28 | "Stats: AAAA Ptr q=0", 29 | "Stats: A Forward q=0", 30 | "Stats: AAAA Forward q=0", 31 | "Signal", 32 | "log-queries=true", 33 | "log-queries=false", 34 | "PTR-deduce reload", 35 | "LoadAllZones Database Entries", 36 | "initiates shutdown", 37 | "All Listen servers stopped", 38 | } 39 | 40 | out := &mock.IOWriter{} 41 | log.SetOut(out) 42 | log.SetLevel(log.MinorLevel) 43 | 44 | cfg := newConfig() 45 | cfg.TTLAsSecs = 60 46 | cfg.chaosFlag = true 47 | cfg.reportInterval = time.Second * 3 48 | ar := newAutoReverse(cfg, nil) 49 | ar.generateLocalForward("example.net.") 50 | srv := newServer(cfg, database.NewGetter(), resolver.NewResolver(), nil, "UDP", "[::1]:3053") 51 | ar.servers = append(ar.servers, srv) 52 | ar.startServers() 53 | go ar.Run() 54 | time.Sleep(time.Second * 4) // Give stats report time to trigger 55 | 56 | // Send all non-terminating signals and toggle USR2 (--log-queries toggle) 57 | 58 | for _, sig := range []os.Signal{syscall.SIGUSR1, syscall.SIGHUP, syscall.SIGUSR2, syscall.SIGUSR2} { 59 | ar.sig <- sig 60 | time.Sleep(time.Millisecond * 100) 61 | } 62 | 63 | // Send shutdown and wait for co-routine channel to close 64 | ar.sig <- syscall.SIGTERM 65 | <-ar.Done() 66 | time.Sleep(time.Second) 67 | got := out.String() 68 | for _, s := range testCases { 69 | if !strings.Contains(got, s) { 70 | t.Error("Does not contain", s) 71 | t.Error(got) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resolver/log.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | "github.com/markdingo/autoreverse/log" 11 | ) 12 | 13 | // LogNS logs results from LookupNS. Exported for mock resolver. Caller should test for 14 | // log.IfDebug() prior to calling. 15 | func LogNS(name string, ns []*net.NS, note string, err error) { 16 | var s [5]string 17 | s[0] = "res:NS" 18 | s[1] = name 19 | if err != nil { 20 | s[3] = err.Error() 21 | } else { 22 | var ar []string 23 | for _, n := range ns { 24 | ar = append(ar, n.Host) 25 | } 26 | s[2] = strings.Join(ar, ",") 27 | } 28 | s[4] = note 29 | log.Debug(strings.Join(s[:], "#")) 30 | } 31 | 32 | // LogIP logs results from LookupIPAddr. Exported for mock resolver. Caller should test 33 | // for log.IfDebug() prior to calling. 34 | func LogIP(host string, addrs []net.IPAddr, note string, err error) { 35 | var s [5]string 36 | s[0] = "res:IP" 37 | s[1] = host 38 | if err != nil { 39 | s[3] = err.Error() 40 | } else { 41 | var ar []string 42 | for _, a := range addrs { 43 | ar = append(ar, a.IP.String()) 44 | } 45 | s[2] = strings.Join(ar, ",") 46 | } 47 | s[4] = note 48 | log.Debug(strings.Join(s[:], "#")) 49 | } 50 | 51 | // LogExchangeQ logs the question given to miekg.Exchange(). Exported for mock 52 | // resolver. Caller should test for log.IfDebug() prior to calling. 53 | func LogExchangeQ(net, logName, server string, q dns.Question) { 54 | log.Debugf("miekg Q:%s:%s/%s q=%s", 55 | net, logName, server, dnsutil.PrettyQuestion(q)) 56 | } 57 | 58 | // LogExchangeA logs the answer returned by miekg.Exchange(). See above. 59 | func LogExchangeA(server string, question dns.Question, r *dns.Msg, err error) { 60 | if err == nil { 61 | log.Debug("miekg A:", dnsutil.PrettyMsg1(r)) 62 | } else { 63 | log.Debugf("miekg E:%s/%s/%s %s", 64 | server, dnsutil.ChompCanonicalName(question.Name), 65 | dnsutil.TypeToString(question.Qtype), 66 | dnsutil.ShortenLookupError(err).Error()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /usage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/markdingo/autoreverse/log" 8 | "github.com/markdingo/autoreverse/mock" 9 | ) 10 | 11 | func TestUsage(t *testing.T) { 12 | out := &mock.IOWriter{} 13 | log.SetOut(out) 14 | cfg := newConfig() 15 | ar := newAutoReverse(cfg, nil) 16 | 17 | testCases := []struct { 18 | options string 19 | expect string 20 | result parseResult 21 | }{ 22 | {"", "", parseContinue}, 23 | {"-h", "SYNOPSIS", parseStop}, 24 | {"--help", "SYNOPSIS", parseStop}, 25 | {"-v", "Program:", parseStop}, 26 | {"--version", "Program:", parseStop}, 27 | {"--manpage", ".Sh NAME", parseStop}, 28 | {"goop", "goop", parseFailed}, 29 | {"-X", "unknown shorthand flag", parseFailed}, 30 | {"--forward o.example.net --forward t.example.net", "Duplicate option", parseFailed}, 31 | {"--listen 127.0.0.1 --listen ::1", "", parseContinue}, // This duplicate is ok 32 | {"--forward a.v --reverse 127.0.0.1/24" + 33 | " --listen ::1 --listen 127.0.0.1" + 34 | " --PTR-deduce http://url --PTR-deduce axfr://url" + 35 | " --passthru a-server --synthesize=true --CHAOS=true" + 36 | " --NSID myname --TTL 45m" + 37 | " --user u --group g --chroot /root" + 38 | " --log-major --log-minor --log-debug=true" + 39 | " --log-queries=false --report 4h", "", parseContinue}, // Every legit option 40 | } 41 | 42 | for ix, tc := range testCases { 43 | var opts []string 44 | if len(tc.options) > 0 { 45 | opts = strings.Split(tc.options, " ") 46 | } 47 | args := []string{programName} 48 | args = append(args, opts...) 49 | out.Reset() 50 | res := ar.parseOptions(args) 51 | if res != tc.result { 52 | t.Error(ix, "Results mismatch. Want", tc.result, "got", res) 53 | } 54 | got := out.String() 55 | if len(tc.expect) == 0 && len(got) != 0 { 56 | t.Error(ix, "Did not expect any output, but got", len(got), got) 57 | } 58 | if len(tc.expect) > 0 { 59 | if !strings.Contains(got, tc.expect) { 60 | t.Error(ix, "Output does not contain", tc.expect, "got", got) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dnsutil/deduce_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | 8 | "github.com/markdingo/autoreverse/dnsutil" 9 | ) 10 | 11 | func TestDeducePtr(t *testing.T) { 12 | 13 | testCases := []struct { 14 | input string 15 | expectPtr string // If len() == 0 then don't expect a ptr back 16 | expectKey string 17 | }{ 18 | {"a1. 123 IN A 192.0.2.250", "250.2.0.192.in-addr.arpa. 123\tIN\tPTR\ta1.", "192.0.2.250"}, 19 | { 20 | "a224. 60 IN AAAA ::1", 21 | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.\t60\tIN\tPTR\ta224.", 22 | "::1", 23 | }, 24 | { 25 | "a3. 124 IN AAAA 2001:db8::1", 26 | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.\t124\tIN\tPTR\ta3.", 27 | "2001:db8::1", 28 | }, 29 | { 30 | "a4. 125 NS a.ns.a4.", // Should not return a PTR 31 | "", 32 | "", 33 | }, 34 | 35 | { 36 | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.dodgy 73 IN PTR v100.example.net.", 37 | "", 38 | "", 39 | }, 40 | { 41 | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.\t124\tIN\tPTR\ta3.", 42 | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.\t124\tIN\tPTR\ta3.", 43 | "2001:db8::1", 44 | }, 45 | { 46 | "11.2.0.192.in-addr.arpa.\t124\tIN\tPTR\ta3.", 47 | "11.2.0.192.in-addr.arpa.\t124\tIN\tPTR\ta3.", 48 | "192.0.2.11", 49 | }, 50 | } 51 | 52 | for ix, tc := range testCases { 53 | addRR, err := dns.NewRR(tc.input) 54 | if err != nil { 55 | t.Fatal(ix, "Setup failed", err) 56 | } 57 | ptr, key := dnsutil.DeducePtr(addRR) 58 | if ptr == nil { 59 | if len(tc.expectPtr) > 0 { 60 | t.Error(ix, "PTR not return when expected from", tc.input, addRR) 61 | } 62 | continue // Expected 63 | } 64 | 65 | got := ptr.String() 66 | if got != tc.expectPtr { 67 | t.Error(ix, "PTR mismatch: Input", tc.input, "Got", got, "Expected", tc.expectPtr) 68 | } 69 | if key != tc.expectKey { 70 | t.Error(ix, "Key mismatch: Input", tc.input, "Got", key, "Expected", tc.expectKey) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /acceptfunc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | /* 8 | 9 | This is a clone of the miekg.defaultMsgAcceptFunc with the test for qdcount != 1 10 | removed. This is to support queries for Server Cookies getting thru to our handler - see 11 | RFC 7873 Section 5.4. Review this function periodically to ensure it stays in sync with 12 | the miekg default. Last reviewed 25Nov2022 with the last commit of the miekg original 13 | being 3b8982c on Oct 15, 2021. 14 | 15 | */ 16 | 17 | // DefaultMsgAcceptFunc checks the request and will reject if: 18 | // 19 | // * isn't a request (don't respond in that case) 20 | // 21 | // * opcode isn't OpcodeQuery or OpcodeNotify 22 | // 23 | // * Zero bit isn't zero 24 | // 25 | // * does not have exactly 1 question in the question section 26 | // 27 | // * has more than 1 RR in the Answer section 28 | // 29 | // * has more than 0 RRs in the Authority section 30 | // 31 | // * has more than 2 RRs in the Additional section 32 | // 33 | 34 | const ( 35 | // Header.Bits 36 | _QR = 1 << 15 // query/response (response=1) 37 | ) 38 | 39 | func (t *server) customMsgAcceptFunc(dh dns.Header) dns.MsgAcceptAction { 40 | if isResponse := dh.Bits&_QR != 0; isResponse { 41 | t.addAcceptError() 42 | return dns.MsgIgnore 43 | } 44 | 45 | // Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs. 46 | opcode := int(dh.Bits>>11) & 0xF 47 | if opcode != dns.OpcodeQuery && opcode != dns.OpcodeNotify { 48 | t.addAcceptError() 49 | return dns.MsgRejectNotImplemented 50 | } 51 | 52 | ////////////////////////////////////////////////////////////////////// 53 | // if dh.Qdcount != 1 { 54 | // return MsgReject 55 | // } 56 | ////////////////////////////////////////////////////////////////////// 57 | 58 | // NOTIFY requests can have a SOA in the ANSWER section. See RFC 1996 Section 3.7 and 3.11. 59 | if dh.Ancount > 1 { 60 | t.addAcceptError() 61 | return dns.MsgReject 62 | } 63 | // IXFR request could have one SOA RR in the NS section. See RFC 1995, section 3. 64 | if dh.Nscount > 1 { 65 | t.addAcceptError() 66 | return dns.MsgReject 67 | } 68 | if dh.Arcount > 2 { 69 | t.addAcceptError() 70 | return dns.MsgReject 71 | } 72 | return dns.MsgAccept 73 | } 74 | -------------------------------------------------------------------------------- /locals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | ) 11 | 12 | // Synthesize the --local-forward zone which includes making an SOA. 13 | func (t *autoReverse) generateLocalForward(forward string) { 14 | auth := &authority{forward: true} 15 | auth.Source = "--local-forward" 16 | auth.Domain = dns.CanonicalName(forward) 17 | auth.synthesizeSOA(auth.Domain, t.cfg.TTLAsSecs) 18 | logAuth(auth, "Local Forward") 19 | t.forwardAuthority = auth 20 | t.addAuthority(auth) 21 | } 22 | 23 | // Synthesize zones for --local-reverse zones. This includes making an SOA and copying the 24 | // forward NS details which may or may not be present and may or may not be right... 25 | // generateLocalReverses relies on the forward zone already being set. 26 | func (t *autoReverse) generateLocalReverses() error { 27 | for _, ipNet := range t.localReverses { 28 | auth := &authority{cidr: ipNet} 29 | auth.Source = "--local-reverse" 30 | 31 | // The reverse zone is needed to match the PTR queries. In a slightly 32 | // hacky way, we generate a full PTR qName - because that function already 33 | // exists - and trim off the excess tokens based on the prefix length. 34 | 35 | domain := dnsutil.IPToReverseQName(ipNet.IP) // full PTR qName 36 | ones, bits := ipNet.Mask.Size() 37 | var remove int 38 | if bits == 32 { // ipv4 39 | remove = 4 - ones/8 // Octets to remove 40 | } else { 41 | remove = 32 - ones/4 // Nibbles to remove 42 | } 43 | tokens := strings.Split(domain, ".") 44 | if len(tokens) <= remove { 45 | return fmt.Errorf("Internal error, local %s (%d) < %d tokens", 46 | domain, len(tokens), remove) 47 | } 48 | domain = strings.Join(tokens[remove:], ".") 49 | auth.Domain = domain 50 | 51 | // Now we have a domain the NS qNames can be mutated 52 | for _, ns := range t.forwardAuthority.NS { 53 | rr := dns.Copy(ns) // Take a copy as we modify 54 | rr.Header().Name = auth.Domain 55 | auth.NS = append(auth.NS, rr) 56 | 57 | } 58 | auth.synthesizeSOA(t.forward, t.cfg.TTLAsSecs) 59 | if !t.addAuthority(auth) { 60 | return fmt.Errorf("--local-reverse %s is duplicated", auth.Domain) 61 | } 62 | logAuth(auth, "Local Reverse") 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /resolver/exchange.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | 11 | "github.com/markdingo/autoreverse/dnsutil" 12 | "github.com/markdingo/autoreverse/log" 13 | ) 14 | 15 | func (t *resolver) SingleExchange(ctx context.Context, c ExchangeConfig, q *dns.Msg, 16 | server, logName string) (r *dns.Msg, rtt time.Duration, err error) { 17 | if len(q.Question) != 1 { 18 | err = fmt.Errorf("SingleExchange Message contains %d Question(s), expect one", 19 | len(q.Question)) 20 | return 21 | } 22 | 23 | question := q.Question[0] 24 | client := &dns.Client{Timeout: t.singleExchangeTimeout} 25 | client.Net = c.Net() 26 | client.UDPSize = c.UDPSize() 27 | _, _, e := net.SplitHostPort(server) // Coerce a service onto the name if 28 | if e != nil { // it hasn't got one 29 | server = net.JoinHostPort(server, "domain") 30 | } 31 | 32 | if log.IfDebug() { 33 | LogExchangeQ(client.Net, logName, server, question) 34 | } 35 | 36 | r, rtt, err = client.ExchangeContext(ctx, q, server) 37 | 38 | if log.IfDebug() { 39 | LogExchangeA(server, question, r, err) 40 | } 41 | 42 | return 43 | } 44 | 45 | func (t *resolver) FullExchange(ctx context.Context, c ExchangeConfig, question dns.Question, 46 | server, logName string) (r *dns.Msg, rtt time.Duration, err error) { 47 | query := new(dns.Msg) 48 | query.Id = dns.Id() 49 | query.RecursionDesired = false // Just to make it clear this is purposefully false 50 | query.SetEdns0(c.UDPSize(), false) 51 | query.Question = append(query.Question, question) 52 | 53 | // Set an overall timeout for the full exchange which includes all retries and 54 | // possible TCP tries. I'm not entirely sure this is honoured by miekg, but the 55 | // individual timeouts set by t.SingleExchange() protect us from an unbounded 56 | // stall, which is good enough. 57 | ctxWithTO, cancel := context.WithDeadline(ctx, time.Now().Add(t.fullExchangeTimeout)) 58 | defer cancel() 59 | 60 | for tries := 0; tries < t.queryTries; tries++ { 61 | c.setNet(dnsutil.UDPNetwork) 62 | r, rtt, err = t.SingleExchange(ctxWithTO, c, query, server, logName) 63 | if err != nil { 64 | continue 65 | } 66 | 67 | // If truncated, try again with TCP 68 | if r.MsgHdr.Rcode == dns.RcodeSuccess && r.MsgHdr.Truncated { 69 | c.setNet(dnsutil.TCPNetwork) 70 | r, rtt, err = t.SingleExchange(ctxWithTO, c, query, server, logName) 71 | if err != nil { 72 | continue 73 | } 74 | } 75 | 76 | return 77 | } 78 | 79 | return // No valid response from any nameserver 80 | } 81 | -------------------------------------------------------------------------------- /locals_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/markdingo/autoreverse/log" 8 | "github.com/markdingo/autoreverse/mock" 9 | ) 10 | 11 | func TestGenerateLocalForward(t *testing.T) { 12 | ar := newAutoReverse(nil, nil) 13 | ar.generateLocalForward("example.net.") 14 | if ar.authorities.len() != 1 { 15 | t.Error("GLF should have added authority", ar.authorities.len()) 16 | } 17 | 18 | auth := ar.authorities.slice[0] 19 | if auth.Domain != "example.net." { 20 | t.Error("Auth was net set", auth) 21 | } 22 | } 23 | 24 | func TestGenerateLocalReverse(t *testing.T) { 25 | out := &mock.IOWriter{} 26 | log.SetOut(out) 27 | log.SetLevel(log.MajorLevel) 28 | 29 | ar := newAutoReverse(nil, nil) 30 | 31 | // Set up a forward as locals assumes there's already one present. Include an NS 32 | // to ensure it's copied to the locals 33 | ar.generateLocalForward("example.net.") 34 | ar.forwardAuthority.NS = append(ar.forwardAuthority.NS, 35 | newRR("example.net. IN NS a.ns.example.net.")) 36 | 37 | _, ipNet, err := net.ParseCIDR("2001:db8::/20") 38 | if err != nil { 39 | t.Fatal("Setup error", err) 40 | } 41 | ar.localReverses = append(ar.localReverses, ipNet) 42 | _, ipNet, err = net.ParseCIDR("192.0.2.0/16") 43 | if err != nil { 44 | t.Fatal("Setup error", err) 45 | } 46 | ar.localReverses = append(ar.localReverses, ipNet) 47 | err = ar.generateLocalReverses() 48 | if err != nil { 49 | t.Error("Unexpected v4 error", ipNet, err) 50 | } 51 | 52 | if ar.authorities.len() != 3 { // Forward + two reverses 53 | t.Error("GLR should have added authority", ar.authorities.len()) 54 | } 55 | 56 | auth := ar.authorities.slice[1] 57 | exp := "0.1.0.0.2.ip6.arpa." 58 | if auth.Domain != exp { 59 | t.Error("Wrong v6 reverse. Exp", exp, "Got", auth.Domain) 60 | } 61 | if len(auth.NS) != 1 { 62 | t.Error("Forward NS was not copied across") 63 | } else if auth.NS[0].Header().Name != exp { 64 | t.Error("Reverse NS was not transmogrified", auth.NS[0].Header().Name) 65 | } 66 | 67 | auth = ar.authorities.slice[2] 68 | exp = "0.192.in-addr.arpa." 69 | if auth.Domain != exp { 70 | t.Error("Wrong v4 reverse. Exp", exp, "Got", auth.Domain) 71 | } 72 | if len(auth.NS) != 1 { 73 | t.Error("Forward NS was not copied across") 74 | } else if auth.NS[0].Header().Name != exp { 75 | t.Error("Reverse NS was not transmogrified", auth.NS[0].Header().Name) 76 | } 77 | 78 | // Calling a second time exercises the duplicate tests code - in a lazy way 79 | err = ar.generateLocalReverses() 80 | if err == nil { 81 | t.Error("Expected a 'duplicates' error") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 0 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/markdingo/autoreverse/log" 10 | ) 11 | 12 | func reportError(severity string, err error, messages ...string) { 13 | msg := severity 14 | if len(messages) > 0 { 15 | msg += ": " + strings.Join(messages, " ") 16 | } 17 | if err != nil { 18 | msg += ": " + err.Error() 19 | } 20 | fmt.Fprintln(log.Out(), msg) 21 | } 22 | 23 | func fatal(err error, messages ...string) { 24 | reportError("Fatal", err, messages...) 25 | os.Exit(1) 26 | } 27 | 28 | func warning(err error, messages ...string) { 29 | reportError("Warning", err, messages...) 30 | } 31 | 32 | ////////////////////////////////////////////////////////////////////// 33 | 34 | func main() { 35 | ar := newAutoReverse(nil, nil) 36 | switch ar.parseOptions(os.Args) { 37 | case parseStop: 38 | return 39 | case parseFailed: 40 | os.Exit(1) 41 | case parseContinue: 42 | } 43 | 44 | // Transfer logging options to the log package 45 | 46 | if ar.cfg.logMajorFlag { 47 | log.SetLevel(log.MajorLevel) 48 | } 49 | if ar.cfg.logMinorFlag { 50 | log.SetLevel(log.MinorLevel) 51 | } 52 | if ar.cfg.logDebugFlag { 53 | log.SetLevel(log.DebugLevel) 54 | } 55 | 56 | fmt.Fprintln(log.Out(), 57 | programName, Version, "Starting with Log Level:", log.Level()) 58 | 59 | // Validate everything that is likely a typo or usage error 60 | err := ar.ValidateCommandLineOptions() 61 | if err != nil { 62 | fatal(err) 63 | } 64 | 65 | // RRL is conditionally activated if any --rrl-*psec options have been set 66 | if ar.activateRRL() { 67 | fmt.Fprintln(log.Out(), "RRL Active", ar.cfg.rrlConfig.String()) 68 | } 69 | 70 | // Zone of Authority phase 71 | if len(ar.cfg.localForward) > 0 { 72 | ar.generateLocalForward(ar.cfg.localForward) // Synthesize local forward zone 73 | } 74 | 75 | ar.startServers() // Only returns if listens succeed 76 | 77 | err = ar.discover() // Discover all delegated zones 78 | if err != nil { 79 | fatal(err) 80 | } 81 | 82 | if len(ar.localReverses) > 0 { // Generate locals once forward is assured 83 | err = ar.generateLocalReverses() 84 | if err != nil { 85 | fatal(err) 86 | } 87 | } 88 | 89 | // Zones of Authority are set - ensure correct search order. This should rarely if 90 | // ever matter, but it's possible that one authority might legitimately be a 91 | // superset of another. The sort ensures that more specific zone comes first. 92 | ar.authorities.sort() 93 | 94 | ar.Constrain() // setuid/setgid/chroot 95 | 96 | if !ar.loadAllZones(ar.cfg.PTRZones, "Initial load") { 97 | fatal(nil, "Cannot continue due to failed -PTRZone load") 98 | } 99 | 100 | ar.Run() 101 | 102 | ar.statsReport(false) // Final stats - depending on log level 103 | 104 | fmt.Fprintln(log.Out(), programName, Version, "Exiting after", 105 | time.Now().Sub(ar.startTime).Round(time.Second)) 106 | } 107 | -------------------------------------------------------------------------------- /mock/dns/axfr.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | ) 11 | 12 | // AXFRResponse is what is set with the AxfrServer to define what the response will be for 13 | // its AXFR request. 14 | type AXFRResponse struct { 15 | Rcode int 16 | } 17 | 18 | // AxfrServer is a mock server designed for a single DNS axfr request, a dumb server which 19 | // loads the zone from a file and sends it back. It checks as little as possible to do the 20 | // job. 21 | type AxfrServer struct { 22 | Path string 23 | mu sync.Mutex 24 | resp *AXFRResponse 25 | } 26 | 27 | // SetResponse sets a new response for the axfr query 28 | func (t *AxfrServer) SetResponse(r *AXFRResponse) { 29 | t.mu.Lock() 30 | defer t.mu.Unlock() 31 | t.resp = r 32 | } 33 | 34 | // GetResponse returns the current response as set 35 | func (t *AxfrServer) GetResponse() *AXFRResponse { 36 | t.mu.Lock() 37 | defer t.mu.Unlock() 38 | return t.resp 39 | } 40 | 41 | // ServeDNS meets the interface definition for dns.Handler 42 | func (t *AxfrServer) ServeDNS(wtr dns.ResponseWriter, q *dns.Msg) { 43 | resp := t.GetResponse() 44 | if resp == nil { 45 | panic("resp == nil in mock axfr server") 46 | } 47 | 48 | if len(q.Question) != 1 { 49 | r := new(dns.Msg) 50 | r.SetRcodeFormatError(q) 51 | r.SetRcode(q, dns.RcodeFormatError) 52 | wtr.WriteMsg(r) 53 | return 54 | } 55 | question := q.Question[0] 56 | if question.Qclass != dns.ClassINET || question.Qtype != dns.TypeAXFR { 57 | r := new(dns.Msg) 58 | r.SetRcode(q, dns.RcodeFormatError) 59 | wtr.WriteMsg(r) 60 | return 61 | } 62 | 63 | // Is a custome Rcode requested? If so, just reply with that. 64 | if resp.Rcode != -1 { 65 | r := new(dns.Msg) 66 | r.SetRcode(q, resp.Rcode) 67 | wtr.WriteMsg(r) 68 | return 69 | } 70 | 71 | // Ok, slurp up the zone from disk 72 | qName := dnsutil.ChompCanonicalName(question.Name) 73 | file := t.Path + qName + ".zone" 74 | f, err := os.Open(file) 75 | if err != nil { 76 | r := new(dns.Msg) 77 | r.SetRcode(q, dns.RcodeNameError) 78 | wtr.WriteMsg(r) 79 | return 80 | } 81 | defer f.Close() 82 | parser := dns.NewZoneParser(f, "", file) 83 | parser.SetIncludeAllowed(true) 84 | parser.SetDefaultTTL(60) // ZoneParser needs this in case $TTL is absent 85 | 86 | ch := make(chan *dns.Envelope) 87 | tr := new(dns.Transfer) 88 | var wg sync.WaitGroup 89 | wg.Add(1) 90 | go func() { 91 | tr.Out(wtr, q, ch) 92 | wg.Done() 93 | }() 94 | 95 | var soa dns.RR 96 | for rr, ok := parser.Next(); ok; rr, ok = parser.Next() { 97 | ch <- &dns.Envelope{RR: []dns.RR{rr}} 98 | if soa == nil { 99 | soa = rr 100 | } 101 | } 102 | 103 | if soa == nil { 104 | panic("Set up error: No SOA in " + file) 105 | } 106 | ch <- &dns.Envelope{RR: []dns.RR{soa}} 107 | 108 | wg.Wait() // wait until everything is written out 109 | } 110 | -------------------------------------------------------------------------------- /passthru_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/markdingo/autoreverse/database" 8 | "github.com/markdingo/autoreverse/dnsutil" 9 | "github.com/markdingo/autoreverse/log" 10 | "github.com/markdingo/autoreverse/mock" 11 | "github.com/markdingo/autoreverse/resolver" 12 | 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | const ( 17 | ptServer = "127.0.0.1:21053" 18 | ) 19 | 20 | func TestPassthru(t *testing.T) { 21 | out := &mock.IOWriter{} 22 | log.SetOut(out) 23 | log.SetLevel(log.SilentLevel) 24 | 25 | wtr := &mock.ResponseWriter{} 26 | 27 | res := resolver.NewResolver() 28 | cfg := &config{logQueriesFlag: true, passthru: ptServer} 29 | server := newServer(cfg, database.NewGetter(), res, nil, "", "") 30 | 31 | // First query without anything listening - this will show in the logs 32 | query := setQuestion(dns.ClassINET, dns.TypeNS, "ns.example.net.") 33 | server.ServeDNS(wtr, query) 34 | resp := wtr.Get() 35 | if resp != nil { 36 | t.Fatal("Did not expect a response from passthru") 37 | } 38 | 39 | addr, err := net.ResolveUDPAddr("udp", ptServer) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | // Open socket here so we know it's opened before starting reply go-routine. 44 | sock, err := net.ListenUDP("udp", addr) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | go passReply(sock) // Ignores first message and replies to second msg 49 | server.ServeDNS(wtr, query) 50 | resp = wtr.Get() 51 | if resp != nil { 52 | t.Error("Did not expect response to first msg from passReply", resp) 53 | } 54 | 55 | server.ServeDNS(wtr, query) 56 | resp = wtr.Get() 57 | if resp == nil { 58 | t.Error("Expected a response to second msg from passReply") 59 | } else if resp.Rcode != dns.RcodeSuccess { 60 | t.Error("Expected RcodeSuccess, not", dnsutil.RcodeToString(resp.Rcode)) 61 | } else { 62 | if len(resp.Answer) != 1 && resp.Answer[0].Header().Rrtype != dns.TypeNS { 63 | t.Error("Wrong response", len(resp.Answer), resp.Answer[0].Header().Rrtype) 64 | } 65 | } 66 | 67 | // Check error logging 68 | exp := `ru=ne q=NS/ns.example.net. s=127.0.0.2:4056 id=0 h=U sz=0/1232 C=0/0/0 passthru:Connection refused 69 | ru=ne q=NS/ns.example.net. s=127.0.0.2:4056 id=0 h=U sz=0/1232 C=0/0/0 passthru:Timeout 70 | ru=ok q=NS/ns.example.net. s=127.0.0.2:4056 id=1 h=U sz=76/1232 C=1/0/0 passthru 71 | ` 72 | got := out.String() 73 | if exp != got { 74 | t.Error("Passthru log mismatch got:\n", got, "\nexp:\n", exp) 75 | } 76 | } 77 | 78 | func passReply(conn *net.UDPConn) { 79 | defer conn.Close() 80 | b := make([]byte, 512) 81 | var addr *net.UDPAddr 82 | var err error 83 | _, _, err = conn.ReadFromUDP(b) 84 | if err != nil { 85 | return 86 | } 87 | 88 | _, addr, err = conn.ReadFromUDP(b) 89 | if err != nil { 90 | return 91 | } 92 | m := new(dns.Msg) 93 | err = m.Unpack(b) 94 | if err != nil { 95 | return 96 | } 97 | 98 | m.Answer = append(m.Answer, newRR("ns.example.net. IN NS a.ns.example.com.")) 99 | out, err := m.Pack() 100 | if err != nil { 101 | return 102 | } 103 | _, err = conn.WriteToUDP(out, addr) 104 | if err != nil { 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | ## autoreverse Quick Start Guide 2 | 3 | If you just want to get `autoreverse` running in a vanilla environment with no special 4 | features, the steps are: 5 | 6 | 1. Create a forward delegation in your DNS 7 | 2. Request a reverse delegation from your ISP or address provider 8 | 3. Run `autoreverse` 9 | 10 | #### 1. Create a forward delegation in your DNS 11 | 12 | `autoreverse` needs a name space to append to synthetic PTR responses which it also uses 13 | to respond to the forward queries on the synthetic names. The convention is to create a 14 | sub-domain, or delegation called "autoreverse" with the following snippet added to your 15 | zone file: 16 | 17 | ```sh 18 | $ORIGIN yourdomain. 19 | ;; Snippet starts 20 | autoreverse IN NS autoreverse ;; --forward name 21 | IN AAAA 2001:db8:aa:bb::53 ;; --listen address 22 | IN A 192.0.2.53 ;; --listen address 23 | ;; Snippet ends 24 | ``` 25 | 26 | (Obviously the **AAAA** and **A** addresses need tweaking to match the listen addresses 27 | you've allocated for `autoreverse`.) 28 | 29 | 30 | #### 2. Request a reverse delegation from your ISP or address provider 31 | 32 | Normally you make a request to your ISP or address provider to arrange for the reverse 33 | delegation to your `autoreverse` instance. Your provider will want to know that 34 | `autoreverse.yourdomain` is the name server to add to the delegation. Unfortunately, some 35 | providers insist that your reverse name server responds to queries *before* they'll act 36 | on the delegation request. In this case read to the end of this guide, then revisit this 37 | section. But the short answer is to run `autoreverse` with `--local-forward` while the ISP 38 | verifies responsiveness. 39 | 40 | #### 3. Run autoreverse 41 | 42 | You need three pieces of information to run `autoreverse`: listen addresses; the forward 43 | domain name created in the first step and the CIDR of your reverse assignment. Given those, 44 | you start `autoreverse` with: 45 | 46 | ```sh 47 | # autoreverse --listen ipv4-addr --listen ipv6-addr --forward autoreverse.yourdomain --reverse reverse-CIDR 48 | ``` 49 | 50 | That's it. You're done. If your delgations in the global DNS are correct, `autoreverse` 51 | will find all the information it needs and start serving queries. To test this, perhaps 52 | try a reverse query with `dig -x ip` where "ip" is in range of the `reverse-CIDR`? 53 | 54 | --- 55 | 56 | #### That pesky special case 57 | 58 | You may need to deal with the special case mentioned in [Step 2](#2. Request a reverse 59 | delegation from your ISP or address provider) which is where your provider insists on 60 | `autoreverse` running first before they will delegate. Since `autoreverse` requires the 61 | `--forward` and `--reverse` delegations be in place before it starts, this requirement by 62 | your provider creates a "chicken-and-egg" dilemma. 63 | 64 | If this is your situation, simply invoke `autoreverse` with `--local-reverse` in place of 65 | `--reverse` and `autoreverse` will run well enough for your provider to test the 66 | delegation and proceed with their delegation process. 67 | 68 | Once the delegation has completed, revert the `--local-reverse` back to `--reverse` and 69 | `autoreverse` should pull in the "Zone of Authority" details from your ISP's delegation. 70 | 71 | --- 72 | -------------------------------------------------------------------------------- /authorities_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestAuthoritiesSort(t *testing.T) { 9 | input := []string{ 10 | "0.192.in-addr.arpa.", 11 | "0.8.b.d.0.1.0.0.2.ip6.arpa.", 12 | "192.in-addr.arpa.", 13 | "191.in-addr.arpa.", 14 | "193.in-addr.arpa.", 15 | "2.0.192.in-addr.arpa.", 16 | "8.b.d.0.1.0.0.2.ip6.arpa.", 17 | "example.com.", 18 | "f.0.8.b.d.0.1.0.0.2.ip6.arpa.", 19 | "aspecific.example.com.", 20 | "a.b.c.d.e.f.labels.", // More labels should come earlier 21 | "aabbccddeeff.labels.", // than merely long labels 22 | "a.bbbbbbb.c.", // Same number of labels 23 | "bbbbbb.aa.c.", // means a string compare 24 | } 25 | 26 | expected := []string{ 27 | "f.0.8.b.d.0.1.0.0.2.ip6.arpa.", // This is most labels first 28 | "0.8.b.d.0.1.0.0.2.ip6.arpa.", // followed by string comparison 29 | "8.b.d.0.1.0.0.2.ip6.arpa.", // for same label count 30 | "a.b.c.d.e.f.labels.", 31 | "2.0.192.in-addr.arpa.", 32 | "0.192.in-addr.arpa.", 33 | "bbbbbb.aa.c.", 34 | "aspecific.example.com.", 35 | "a.bbbbbbb.c.", 36 | "193.in-addr.arpa.", 37 | "192.in-addr.arpa.", 38 | "191.in-addr.arpa.", 39 | "example.com.", 40 | "aabbccddeeff.labels.", 41 | } 42 | 43 | var auths authorities 44 | for _, d := range input { 45 | a := &authority{} 46 | a.Domain = d 47 | _, a.cidr, _ = net.ParseCIDR("192.0.2.0/24") // Just to make append() happy 48 | auths.append(a) 49 | } 50 | b4 := auths.len() 51 | auths.sort() 52 | af := auths.len() 53 | if b4 != af { 54 | t.Fatal("Slice lengths differ", b4, af) 55 | } 56 | 57 | for ix := 0; ix < auths.len(); ix++ { 58 | if auths.slice[ix].Domain != expected[ix] { 59 | t.Error(ix, "Mismatch", auths.slice[ix].Domain, expected[ix]) 60 | } 61 | } 62 | } 63 | 64 | func TestAuthoritiesFindInDomain(t *testing.T) { 65 | var auths authorities 66 | for _, d := range []string{"a.example.net.", "b.a.example.net.", "c.b.a.example.net."} { 67 | a := &authority{forward: true} 68 | a.Domain = d 69 | auths.append(a) 70 | } 71 | 72 | for _, d := range []string{"0.8.b.d.0.1.0.0.2.ip6.arpa.", "1.0.0.2.ip6.arpa."} { 73 | a := &authority{} 74 | a.Domain = d 75 | a.cidr = &net.IPNet{} // Filler to make append() happy 76 | auths.append(a) 77 | } 78 | 79 | auths.sort() 80 | 81 | type testCase struct { 82 | qName string 83 | expect string 84 | } 85 | 86 | testCases := []testCase{ 87 | {"8.b.d.0.2.0.0.2.ip6.arpa.", ""}, 88 | {"0.8.b.d.0.1.0.0.2.ip6.arpa.", "0.8.b.d.0.1.0.0.2.ip6.arpa."}, 89 | {"1.2.3.0.8.b.d.0.1.0.0.2.ip6.arpa.", "0.8.b.d.0.1.0.0.2.ip6.arpa."}, 90 | {"1.2.3.40.8.b.d.0.1.0.0.2.ip6.arpa.", "1.0.0.2.ip6.arpa."}, 91 | {"1.0.0.2.ip6.arpa.", "1.0.0.2.ip6.arpa."}, 92 | {"a.example.net.", "a.example.net."}, 93 | {"b.a.example.net.", "b.a.example.net."}, 94 | {"c.b.a.example.net.", "c.b.a.example.net."}, 95 | {"d.c.b.a.example.net.", "c.b.a.example.net."}, 96 | } 97 | 98 | for ix, tc := range testCases { 99 | a := auths.findInDomain(tc.qName) 100 | if a == nil { 101 | if len(tc.expect) > 0 { 102 | t.Error(ix, "No auth returned for", tc.qName) 103 | } 104 | continue 105 | } 106 | if a.Domain != tc.expect { 107 | t.Error(ix, "Wrong Domain returned", a.Domain) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStatsQuery(t *testing.T) { 8 | var qs1, qs2 qTypeStats 9 | qs1.good = 1 10 | qs2.good = 3 11 | qs1.add(&qs2) 12 | if qs1.good != 4 { 13 | t.Error("qTypeStats.add flawed", qs1, qs2) 14 | } 15 | qs3 := qTypeStats{1, 2, 12, 4, 5} 16 | qs1.add(&qs3) 17 | if qs1.queries != 1 || qs1.good != 6 || qs1.answers != 12 || 18 | qs1.truncated != 4 || qs1.invertError != 5 { 19 | t.Errorf("qTypeStats.add flawed %+v %+v\n", qs1, qs3) 20 | } 21 | 22 | got := qs1.String() 23 | exp := "q=1 good=6(12) trunc=4 invErr=5" 24 | if got != exp { 25 | t.Error("qTypeStats.String \nExp:", exp, "\nGot:", got) 26 | } 27 | } 28 | 29 | func TestStatsGeneral(t *testing.T) { 30 | gs := generalStats{} 31 | setGeneralStats(&gs) 32 | gs.add(&gs) // Should double all counters 33 | if gs.queries != 1*2 || 34 | gs.badRequest != 2*2 || 35 | gs.chaos != 11*2 || 36 | gs.nsid != 12*2 || 37 | gs.cookie != 21*2 || 38 | gs.cookieOnly != 22*2 || 39 | gs.wrongCookie != 23*2 || 40 | gs.malformedCookie != 24*2 || 41 | gs.passthruOut != 31*2 || 42 | gs.passthruIn != 32*2 || 43 | gs.chaosRefused != 41*2 || 44 | gs.noAuthority != 42*2 || 45 | gs.wrongClass != 43*2 || 46 | gs.authZoneANY != 51*2 || 47 | gs.authZoneSOA != 52*2 || 48 | gs.authZoneNS != 53*2 || 49 | gs.truncatedV6 != 61*2 || 50 | gs.truncatedV4 != 62*2 || 51 | gs.dbDone != 71*2 || 52 | gs.dbNoError != 72*2 || 53 | gs.dbNXDomain != 73*2 || 54 | gs.dbFormErr != 74*2 || 55 | gs.synthForward != 81*2 || 56 | gs.synthReverse != 82*2 || 57 | gs.noSynth != 83*2 || 58 | gs.synthDone != 91*2 || 59 | gs.synthNoError != 92*2 || 60 | gs.synthNXDomain != 93*2 || 61 | gs.synthFormErr != 94*2 { 62 | t.Errorf("generalStats.Add flawed %+v\n", gs) 63 | } 64 | } 65 | 66 | func TestStatsServer(t *testing.T) { 67 | ss1 := serverStats{} 68 | ss2 := serverStats{} 69 | ss2.gen.passthruIn = 1 // Pick some random fields to populate 70 | ss2.gen.wrongCookie = 2 // All of these values should transfer to ss1 71 | ss2.gen.noAuthority = 3 // when added and thus show up uniquely in the 72 | ss2.APtr.good = 4 // String() output 73 | ss2.AAAAPtr.good = 5 74 | ss2.AForward.good = 6 75 | ss2.AAAAForward.good = 7 76 | ss1.add(&ss2) 77 | exp := "Gen: q=0/0/0/0 C=0/0/2/0 gen=0/1/0/3/0 auth=0/0/0 tc=0/0 db=0/0/0/0 synth=0/0/0 sr=0/0/0/0 APtr: q=0 good=4(0) trunc=0 invErr=0 AAAAPtr: q=0 good=5(0) trunc=0 invErr=0 AForward: q=0 good=6(0) trunc=0 invErr=0 AAAAForward: q=0 good=7(0) trunc=0 invErr=0" 78 | got := ss1.String() 79 | if exp != got { 80 | t.Error("serverStats wrong. \nExp:", exp, "\nGot", got) 81 | } 82 | } 83 | 84 | func setGeneralStats(gs *generalStats) { 85 | gs.queries = 1 86 | gs.badRequest = 2 87 | 88 | gs.chaos = 11 89 | gs.nsid = 12 90 | 91 | gs.cookie = 21 92 | gs.cookieOnly = 22 93 | gs.wrongCookie = 23 94 | gs.malformedCookie = 24 95 | 96 | gs.passthruOut = 31 97 | gs.passthruIn = 32 98 | 99 | gs.chaosRefused = 41 100 | gs.noAuthority = 42 101 | gs.wrongClass = 43 102 | 103 | gs.authZoneANY = 51 104 | gs.authZoneSOA = 52 105 | gs.authZoneNS = 53 106 | 107 | gs.truncatedV6 = 61 108 | gs.truncatedV4 = 62 109 | 110 | gs.dbDone = 71 111 | gs.dbNoError = 72 112 | gs.dbNXDomain = 73 113 | gs.dbFormErr = 74 114 | 115 | gs.synthForward = 81 116 | gs.synthReverse = 82 117 | gs.noSynth = 83 118 | 119 | gs.synthDone = 91 120 | gs.synthNoError = 92 121 | gs.synthNXDomain = 93 122 | gs.synthFormErr = 94 123 | } 124 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/markdingo/autoreverse/mock" 7 | ) 8 | 9 | func TestLevels(t *testing.T) { 10 | var w mock.IOWriter 11 | SetOut(&w) 12 | if Out() != &w { 13 | t.Fatal("SetOut or Out failed") 14 | } 15 | 16 | SetLevel(SilentLevel) 17 | if Level() != SilentLevel { 18 | t.Error("Set Silent failed") 19 | } 20 | if IfMajor() { 21 | t.Error("Silent should not be major") 22 | } 23 | if IfMinor() { 24 | t.Error("Silent should not be minor") 25 | } 26 | if IfDebug() { 27 | t.Error("Silent should not be debug") 28 | } 29 | if MajorLevel.String() != "Major" { 30 | t.Error("Wrong Major string", MajorLevel.String()) 31 | } 32 | if MinorLevel.String() != "Minor" { 33 | t.Error("Wrong Minor string", MinorLevel.String()) 34 | } 35 | if DebugLevel.String() != "Debug" { 36 | t.Error("Wrong Debug string", DebugLevel.String()) 37 | } 38 | if SilentLevel.String() != "Silent" { 39 | t.Error("Wrong Silent string", SilentLevel.String()) 40 | } 41 | 42 | Major("Should not log") 43 | Minor("Should not log") 44 | Debug("Should not log") 45 | Majorf("Should not log") 46 | Minorf("Should not log") 47 | Debugf("Should not log") 48 | if w.Len() > 0 { 49 | t.Error("Silent still logged", w.String()) 50 | } 51 | 52 | w.Reset() 53 | SetLevel(MajorLevel) // Should accept minor + major but not debug 54 | Major("a") 55 | Minor("b") 56 | Debug("c") 57 | 58 | Majorf("d") 59 | Minorf("e") 60 | Debugf("f") 61 | 62 | exp := "a\nd\n" 63 | if w.String() != exp { 64 | t.Error("Major Levels not working. Got:", w.String(), "Exp:", exp, "<<") 65 | } 66 | 67 | w.Reset() 68 | SetLevel(MinorLevel) // Should accept minor + major but not debug 69 | Major("a") 70 | Minor("b") 71 | Debug("c") 72 | Majorf("d") 73 | Minorf("e") 74 | Debugf("f") 75 | exp = "a\n" + minorPrefix + "b\n" + "d\n" + minorPrefix + "e\n" 76 | if w.String() != exp { 77 | t.Error("Minor Levels not working. Got:", w.String(), "Exp:", exp) 78 | } 79 | } 80 | 81 | func TestFormat(t *testing.T) { 82 | var w mock.IOWriter 83 | SetOut(&w) 84 | SetLevel(MinorLevel) 85 | // Need to trick the compiler so it doesn't warn about %d 86 | f := "%" 87 | f += "d a " 88 | Major(f, 5) // Should not format 89 | Majorf("%d b", 5) // Should format 90 | exp := "%d a 5\n5 b\n" 91 | if exp != w.String() { 92 | t.Error("F and non-F not working", len(w.String()), len(exp), w.String(), exp) 93 | } 94 | } 95 | 96 | func TestMultiLine(t *testing.T) { 97 | var w mock.IOWriter 98 | SetOut(&w) 99 | SetLevel(MinorLevel) 100 | 101 | w.Reset() 102 | Major("a") 103 | exp := "a\n" 104 | if exp != w.String() { 105 | t.Error("Multiline with no trailing NL failed", exp, w.String()) 106 | } 107 | w.Reset() 108 | Major("a\n") // Should produce the same result 109 | if exp != w.String() { 110 | t.Error("Multiline with no trailing NL failed", exp, w.String()) 111 | } 112 | 113 | w.Reset() 114 | Major("a\nb") 115 | exp = "a\nb\n" 116 | if exp != w.String() { 117 | t.Error("Multiline with no trailing NL failed", exp, w.String()) 118 | } 119 | 120 | w.Reset() 121 | Major("a\nb\n\n\n") // Should produce the same results 122 | if exp != w.String() { 123 | t.Error("Multiline with many trailing NLs failed", exp, w.String()) 124 | } 125 | 126 | // Check that prefix gets added correctly 127 | w.Reset() 128 | SetLevel(DebugLevel) 129 | Debug("a\nb") 130 | exp = debugPrefix + "a\n" + debugPrefix + "b\n" 131 | if exp != w.String() { 132 | t.Error("Multiline with no trailing NL failed", exp, w.String()) 133 | } 134 | 135 | w.Reset() 136 | Debug("a\nb\n\n\n") // Should produce the same results 137 | if exp != w.String() { 138 | t.Error("Multiline with many trailing NLs failed", exp, w.String()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This Makefile mostly exists for developers, but it's also of use if 'go 3 | # build' or 'go install' don't do what you want; namely installing the 4 | # executable *and* the manpage in traditional Unix locations. 5 | ################################################################################ 6 | 7 | BINDIST=/usr/local/sbin 8 | MANDIST=/usr/share/man/man8 9 | MANSRC=autoreverse.8 10 | ARCMD=autoreverse 11 | 12 | all: version.go $(ARCMD) USAGE.md 13 | @echo All targets built. "Consider 'make help' for other targets". 14 | 15 | $(ARCMD): *.go */*.go Makefile $(MANSRC) 16 | go build 17 | 18 | .PHONY: help 19 | help: 20 | @echo 21 | @echo Make targets for "'autoreverse'": 22 | @echo " Local targets: 'all', 'vet', 'fmt', 'clean' and 'install'" 23 | @echo 24 | @echo " Cross-platform targets:" 25 | @echo " 'freebsd/amd64' - OPNSense on Intel" 26 | @echo " 'linux/mips' - Mikrotik Router Boards" 27 | @echo " 'linux/mips64' - Ubiquiti Edge Router series" 28 | @echo " 'linux/armv71' - 32-bit Raspberry Pi3/Pi-hole, ASUS RT-AX55" 29 | @echo " 'linux/armv8' - 64-bit Raspberry Pi4/Pi-hole" 30 | @echo " 'windows/amd64' - Windows 64bit on Intel/AMD" 31 | @echo " 'windows/386' - Windows 32bit" 32 | @echo 33 | 34 | .PHONY: vet 35 | vet: 36 | go vet ./... 37 | mandoc -Tlint $(MANSRC); exit 0 38 | 39 | .PHONY: clean 40 | clean: 41 | @rm -f $(ARCMD) $(ARCMD).exe 42 | @echo Directory cleaned 43 | @echo "Warning: Never run 'go clean' as that erases the manpage (for obscure reasons)" 44 | 45 | .PHONY: install 46 | install: $(ARCMD) 47 | install -d -o 0 -g 0 -m a=rx $(BINDIST) # Ensure destination directory 48 | install -p -o 0 -g 0 -m a=rx $(ARCMD) $(BINDIST) 49 | @echo $(ARCMD) installed in $(BINDIST) 50 | install -d -o 0 -g 0 -m a=rx $(MANDIST) # Ensure destination directory 51 | install -p -m a=rx $(MANSRC) $(MANDIST) 52 | @echo $(MANSRC) installed in $(MANDIST) 53 | 54 | .PHONY: fmt 55 | fmt: 56 | gofmt -s -w . 57 | 58 | .PHONY: test tests 59 | test tests: 60 | go test ./... 61 | go vet ./... 62 | 63 | version.go: generate_version.sh ChangeLog.md Makefile go.mod 64 | sh generate_version.sh ChangeLog.md >$@ 65 | 66 | USAGE.md: $(ARCMD) usage.go generate_usage.sh Makefile 67 | ./$(ARCMD) -h | sh generate_usage.sh >$@ 68 | 69 | # Cross-compile targets 70 | 71 | .PHONY: freebsd/amd64 72 | freebsd/amd64: clean 73 | @echo 'Building for FreeBSD/amd64 targets (maybe OPNSense Routers)' 74 | @GOOS=freebsd GOARCH=amd64 go build 75 | @file $(ARCMD) 76 | 77 | .PHONY: freebsd/arm64 78 | freebsd/arm64: clean 79 | @echo 'Building for FreeBSD/arm64 targets (maybe OPNSense Routers)' 80 | @GOOS=freebsd GOARCH=arm64 go build 81 | @file $(ARCMD) 82 | 83 | .PHONY: linux/mips 84 | linux/mips: clean 85 | @echo 'Building for Linux/mips targets (maybe Mikrotik Router Boards)' 86 | @GOOS=linux GOARCH=mips go build 87 | @file $(ARCMD) 88 | 89 | .PHONY: linux/mips64 90 | linux/mips64: clean 91 | @echo 'Building for Linux/mips64 targets (Ubiquiti er3, er6)' 92 | @GOOS=linux GOARCH=mips64 go build 93 | @file $(ARCMD) 94 | 95 | .PHONY: linux/armv71 96 | linux/armv71: clean 97 | @echo 'Building for 32-bit Linux/armv71 (ASUS RT-AX55)' 98 | @GOOS=linux GOARCH=arm go build 99 | @file $(ARCMD) 100 | 101 | .PHONY: linux/armv8 102 | linux/armv8: clean 103 | @echo 'Building for 64-bit Linux/armv8 (pi4)' 104 | @GOOS=linux GOARCH=arm go build 105 | @file $(ARCMD) 106 | 107 | .PHONY: windows/amd64 108 | windows/amd64: clean 109 | @echo Building for amd64 Windows 110 | @GOOS=windows GOARCH=amd64 go build 111 | @file $(ARCMD).exe 112 | 113 | .PHONY: windows/386 114 | windows/386: clean 115 | @echo Building for 386 Windows 116 | @GOOS=windows GOARCH=386 go build 117 | @file $(ARCMD).exe 118 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/markdingo/autoreverse/log" 9 | "github.com/markdingo/autoreverse/osutil" 10 | ) 11 | 12 | // Run the server loop checking for signals and stats reports events 13 | func (t *autoReverse) Run() { 14 | t.startTime = time.Now() 15 | t.statsTime = t.startTime 16 | for _, srv := range t.servers { 17 | srv.setMutables(t.forward, nil, t.authorities) 18 | } 19 | 20 | var signal os.Signal 21 | osutil.SignalNotify(t.sig) // Register interest in signals 22 | 23 | for _, a := range t.authorities.slice { 24 | log.Major("Zone Authority: ", a.Domain) 25 | } 26 | 27 | pzs := t.cfg.PTRZones // Transfer ownership to watcher (even if there are none) 28 | t.cfg.PTRZones = []*PTRZone{} // and make sure it sticks! 29 | go t.watchForZoneReloads(pzs, reloadInterval) 30 | 31 | fmt.Fprintln(log.Out(), programName, Version, "Ready") 32 | 33 | // Conditionally create the periodic report channel. Fortunately select purposely 34 | // doesn't mind a nil channel, which is very convenient. 35 | var reportChannel <-chan time.Time 36 | if t.cfg.reportInterval > 0 { 37 | reportTicker := time.NewTicker(t.cfg.reportInterval) 38 | reportChannel = reportTicker.C 39 | defer reportTicker.Stop() 40 | } 41 | 42 | // Wait for any of: a signal, a reporting channel ticker or a reload ticker. 43 | 44 | stopFlag := false 45 | for !stopFlag { 46 | select { 47 | case <-reportChannel: 48 | t.statsReport(true) 49 | 50 | case signal = <-t.sig: 51 | switch { 52 | case osutil.IsSignalTERM(signal), osutil.IsSignalINT(signal): 53 | stopFlag = true 54 | 55 | case osutil.IsSignalUSR1(signal): // USR1 produces a status report 56 | t.statsReport(false) 57 | 58 | case osutil.IsSignalUSR2(signal): // USR1 toggles --log-queries 59 | t.cfg.logQueriesFlag = !t.cfg.logQueriesFlag // Not race-safe, but oh well. 60 | log.Majorf("--log-queries=%t", t.cfg.logQueriesFlag) 61 | 62 | case osutil.IsSignalHUP(signal): 63 | log.Major("SIGHUP --PTR-deduce reload initiated") 64 | t.forceReload <- struct{}{} 65 | 66 | default: 67 | log.Majorf("Signal '%s' reserved for future use", signal) 68 | } 69 | } 70 | } 71 | 72 | log.Majorf("Signal '%s' initiates shutdown", signal) 73 | close(t.done) // Tell companion go-routines 74 | t.stopServers() // Tell servers and wait until they exit 75 | log.Minor("All Listen servers stopped") 76 | } 77 | 78 | var zeroStats serverStats 79 | 80 | // Writes summary stats to Stdout 81 | func (t *autoReverse) statsReport(resetCounters bool) { 82 | var totals serverStats 83 | for _, srv := range t.servers { 84 | srv.statsMu.Lock() // Take writer lock in case resetCounters is true 85 | totals.add(&srv.stats) 86 | if resetCounters { 87 | srv.stats = zeroStats 88 | } 89 | srv.statsMu.Unlock() 90 | } 91 | 92 | now := time.Now() 93 | upDuration := now.Sub(t.startTime) 94 | statsDuration := now.Sub(t.statsTime) 95 | if resetCounters { 96 | t.statsTime = now 97 | } 98 | upDuration = upDuration.Round(time.Second) 99 | statsDuration = statsDuration.Round(time.Second) 100 | 101 | // Include version with output so log parsers can adapt to changes that will inevitably 102 | // occur from release to release. 103 | 104 | log.Major("Stats: Uptime ", upDuration, " Interval: ", statsDuration, " ", Version) 105 | log.Major("Stats: Total ", totals.gen.String()) 106 | log.Major("Stats: A Ptr ", totals.APtr.String()) 107 | log.Major("Stats: AAAA Ptr ", totals.AAAAPtr.String()) 108 | log.Major("Stats: A Forward ", totals.AForward.String()) 109 | log.Major("Stats: AAAA Forward ", totals.AAAAForward.String()) 110 | 111 | if t.rrlHandler != nil { 112 | rrlStats := t.rrlHandler.GetStats(resetCounters) 113 | log.Major("Stats: RRL ", rrlStats.String()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // qTypeStats is for high activity qTypes: A, AAAA, in-addr.arpa & ip6.arps PTR. 8 | type qTypeStats struct { 9 | queries int // Type specific query count 10 | good int // Good replies sent back to client 11 | answers int // Total RRs sent in all good replies 12 | 13 | truncated int // Returns from InvertPtr* 14 | invertError int 15 | } 16 | 17 | func (t *qTypeStats) add(from *qTypeStats) { 18 | t.queries += from.queries 19 | t.good += from.good 20 | t.answers += from.answers 21 | t.truncated += from.truncated 22 | t.invertError += from.invertError 23 | } 24 | 25 | func (t *qTypeStats) String() string { 26 | return fmt.Sprintf("q=%d good=%d(%d) trunc=%d invErr=%d", 27 | t.queries, t.good, t.answers, t.truncated, t.invertError) 28 | } 29 | 30 | type generalStats struct { 31 | queries int // Total queries 32 | badRequest int // No Question, wrong op-code 33 | 34 | chaos int 35 | nsid int 36 | 37 | cookie int 38 | cookieOnly int 39 | wrongCookie int // Server cookie mismatch 40 | malformedCookie int 41 | 42 | passthruOut int 43 | passthruIn int 44 | 45 | chaosRefused int // Refused counters 46 | noAuthority int 47 | wrongClass int 48 | 49 | authZoneANY int // Authority Zone Counters 50 | authZoneSOA int 51 | authZoneNS int 52 | 53 | truncatedV6 int 54 | truncatedV4 int 55 | 56 | dbDone int 57 | dbNoError int 58 | dbNXDomain int 59 | dbFormErr int 60 | 61 | synthForward int 62 | synthReverse int 63 | noSynth int 64 | 65 | synthDone int 66 | synthNoError int 67 | synthNXDomain int 68 | synthFormErr int 69 | } 70 | 71 | func (t *generalStats) add(from *generalStats) { 72 | t.queries += from.queries 73 | t.badRequest += from.badRequest 74 | t.chaos += from.chaos 75 | t.nsid += from.nsid 76 | t.cookie += from.cookie 77 | t.cookieOnly += from.cookieOnly 78 | t.wrongCookie += from.wrongCookie 79 | t.malformedCookie += from.malformedCookie 80 | t.passthruOut += from.passthruOut 81 | t.passthruIn += from.passthruIn 82 | t.chaosRefused += from.chaosRefused 83 | t.noAuthority += from.noAuthority 84 | t.wrongClass += from.wrongClass 85 | t.authZoneANY += from.authZoneANY 86 | t.authZoneSOA += from.authZoneSOA 87 | t.authZoneNS += from.authZoneNS 88 | t.truncatedV6 += from.truncatedV6 89 | t.truncatedV4 += from.truncatedV4 90 | t.dbDone += from.dbDone 91 | t.dbNoError += from.dbNoError 92 | t.dbNXDomain += from.dbNXDomain 93 | t.dbFormErr += from.dbFormErr 94 | t.synthForward += from.synthForward 95 | t.synthReverse += from.synthReverse 96 | t.noSynth += from.noSynth 97 | t.synthDone += from.synthDone 98 | t.synthNoError += from.synthNoError 99 | t.synthNXDomain += from.synthNXDomain 100 | t.synthFormErr += from.synthFormErr 101 | } 102 | 103 | func (t *generalStats) String() string { 104 | return fmt.Sprintf("q=%d/%d/%d/%d C=%d/%d/%d/%d gen=%d/%d/%d/%d/%d auth=%d/%d/%d tc=%d/%d db=%d/%d/%d/%d synth=%d/%d/%d sr=%d/%d/%d/%d", 105 | t.queries, t.badRequest, t.chaos, t.nsid, 106 | t.cookie, t.cookieOnly, t.wrongCookie, t.malformedCookie, 107 | t.passthruOut, t.passthruIn, t.chaosRefused, t.noAuthority, t.wrongClass, 108 | t.authZoneANY, t.authZoneSOA, t.authZoneNS, 109 | t.truncatedV6, t.truncatedV4, 110 | t.dbDone, t.dbNoError, t.dbNXDomain, t.dbFormErr, 111 | t.synthForward, t.synthReverse, t.noSynth, 112 | t.synthDone, t.synthNoError, t.synthNXDomain, t.synthFormErr) 113 | } 114 | 115 | type serverStats struct { 116 | gen generalStats 117 | APtr qTypeStats 118 | AAAAPtr qTypeStats 119 | AForward qTypeStats 120 | AAAAForward qTypeStats 121 | } 122 | 123 | func (t *serverStats) add(from *serverStats) { 124 | t.gen.add(&from.gen) 125 | t.APtr.add(&from.APtr) 126 | t.AAAAPtr.add(&from.AAAAPtr) 127 | t.AForward.add(&from.AForward) 128 | t.AAAAForward.add(&from.AAAAForward) 129 | } 130 | 131 | func (t *serverStats) String() string { 132 | return "Gen: " + t.gen.String() + 133 | " APtr: " + t.APtr.String() + 134 | " AAAAPtr: " + t.AAAAPtr.String() + 135 | " AForward: " + t.AForward.String() + 136 | " AAAAForward: " + t.AAAAForward.String() 137 | } 138 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/markdingo/rrl" 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/database" 10 | "github.com/markdingo/autoreverse/delegation" 11 | "github.com/markdingo/autoreverse/dnsutil" 12 | "github.com/markdingo/autoreverse/resolver" 13 | ) 14 | 15 | // mutables are set by the main go-routine during discovery and are read by the query 16 | // processing code, thus they need mutex protection. The rule is that mutables must only 17 | // ever be accessed by setMutable() and getMutable(). 18 | type mutables struct { 19 | ptrSuffix string // String to append to synthesized PTR names 20 | probe delegation.Probe // Current probe if any 21 | authorities // Forward + all reverse zones of authority 22 | } 23 | 24 | // Set mutables under protection of a mutex. This is the only way they should be set. 25 | func (t *server) setMutables(ps string, pr delegation.Probe, auths authorities) { 26 | t.mutablesMu.Lock() 27 | t.ptrSuffix = ps 28 | t.authorities = auths 29 | t.probe = pr 30 | t.mutablesMu.Unlock() 31 | } 32 | 33 | // Get a copy of mutables under protection of a mutex. 34 | func (t *server) getMutables() mutables { 35 | t.mutablesMu.RLock() 36 | var ret mutables 37 | ret.ptrSuffix = t.ptrSuffix 38 | ret.probe = t.probe 39 | ret.authorities = t.authorities 40 | t.mutablesMu.RUnlock() 41 | 42 | return ret 43 | } 44 | 45 | // server is created for each listen address. 46 | type server struct { 47 | cfg *config 48 | resolver resolver.Resolver 49 | dbGetter *database.Getter 50 | rrlHandler *rrl.RRL // May be nil if not configured 51 | 52 | network string // Listen details 53 | address string 54 | 55 | miekg *dns.Server 56 | 57 | mutablesMu sync.RWMutex 58 | mutables // Only ever access this via the mutables accessor functions 59 | 60 | statsMu sync.RWMutex 61 | stats serverStats 62 | 63 | cookieSecrets [2]uint64 64 | } 65 | 66 | func newServer(cfg *config, dbGetter *database.Getter, r resolver.Resolver, rrlHandler *rrl.RRL, network, address string) *server { 67 | t := &server{ 68 | cfg: cfg, 69 | resolver: r, 70 | dbGetter: dbGetter, 71 | rrlHandler: rrlHandler, 72 | network: network, 73 | address: address, 74 | } 75 | 76 | if len(t.network) == 0 { 77 | t.network = dnsutil.UDPNetwork 78 | } 79 | 80 | t.miekg = &dns.Server{Net: t.network, Addr: t.address, ReusePort: true, Handler: t} 81 | 82 | // The miekg.defaultMsgAcceptFunc rejects Server Cookie queries (RFC7873#5.4) as 83 | // qdcount==0, so that function has been replaced with our own function with is 84 | // mostly a clone with the original qdcount != 1 commented out. We also take the 85 | // opportunity to gather stats on rejections as that wasn't previously possible. 86 | 87 | t.miekg.MsgAcceptFunc = func(dh dns.Header) dns.MsgAcceptAction { 88 | return t.customMsgAcceptFunc(dh) 89 | } 90 | 91 | return t 92 | } 93 | 94 | // Start starts accepting DNS queries by calling dns.ListenAndServe(). It waits until the 95 | // service has actually started prior to returning to the caller by way of NotifyStartFunc. 96 | // 97 | // Returns error if the server fails to start or nil. 98 | func (t *autoReverse) startServer(srv *server) error { 99 | t.wg.Add(1) 100 | 101 | hasStarted := make(chan error) // Make sure listener has started before returning 102 | srv.miekg.NotifyStartedFunc = func() { 103 | hasStarted <- nil 104 | } 105 | 106 | go func() { 107 | err := srv.miekg.ListenAndServe() 108 | t.wg.Done() 109 | if err != nil { 110 | hasStarted <- err 111 | } 112 | close(hasStarted) 113 | }() 114 | 115 | return <-hasStarted // Closed by t.miekg.NotifyStartedFunc 116 | 117 | } 118 | 119 | func (t *server) stop() { 120 | t.miekg.Shutdown() 121 | } 122 | 123 | func (t *server) addStats(from *serverStats) { 124 | t.statsMu.Lock() 125 | t.stats.add(from) 126 | t.statsMu.Unlock() 127 | } 128 | 129 | // Called from acceptFunc from within miekg when a query fails prior to our ServerDNS() 130 | func (t *server) addAcceptError() { 131 | t.statsMu.Lock() 132 | t.stats.gen.badRequest++ 133 | t.statsMu.Unlock() 134 | } 135 | -------------------------------------------------------------------------------- /mock/resolver/file.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/miekg/dns" 11 | 12 | "github.com/markdingo/autoreverse/log" 13 | ) 14 | 15 | func (t *mockResolver) loadLookupFile(qClass, qType, qName string) (r dns.Msg, fname string) { 16 | fname = path.Join(t.dir, "lookup", strings.ToUpper(qClass), strings.ToUpper(qType), qName) 17 | return t.loadFile(fname), fname 18 | } 19 | 20 | func (t *mockResolver) loadExchangeFile(net, addr, qClass, qType, qName string) (r dns.Msg, 21 | fname string) { 22 | 23 | // addr is ipv4:service or [ipv6]:service - we want just the ip address to form 24 | // the path to the response RRs. Do a cheap&nasty extraction of the IP address. 25 | 26 | sx := 0 27 | var ex int 28 | if addr[0] == '[' { 29 | sx = 1 30 | ex = strings.Index(addr, "]") 31 | } else { 32 | ex = len(addr) 33 | } 34 | if ex == -1 { 35 | panic("Bogus IP Address:" + addr) 36 | } 37 | addr = addr[sx:ex] 38 | 39 | fname = path.Join(t.dir, "exchange", addr, strings.ToUpper(qClass), strings.ToUpper(qType), qName) 40 | return t.loadFile(fname), fname 41 | } 42 | 43 | // Attempt to open a mock file. If it doesn't exist, return REFUSED. If it does exist and 44 | // is empty return NXDOMAIN. If it's not empty parse as a series of dns.NewRR() lines with 45 | // a prefix indicating which section the RR belongs in: 46 | // 47 | // A:Answer 48 | // N:NS 49 | // E:Extra 50 | // RCODE:miekg rcode string - must be uppercase - see miekg/msg.go lines 139 onwards. 51 | // ;; Comment 52 | // Blank lines ignored 53 | // No spaces between the ":" separator 54 | // 55 | // If you set RCODE: then normally there should be no RRs in the message as no caller 56 | // will look at them. 57 | // 58 | // If rCode is anything but NOERROR, the returned message has no reliable content. 59 | 60 | func init() { 61 | path := os.Getenv("AUTOREVERSE_TRACE") 62 | if len(path) > 0 { 63 | var err error 64 | tracer, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | } 70 | 71 | var tracer *os.File 72 | 73 | // It turns out that github and zip don't like filenames with colons, so we substitute 74 | // with "_". This is all just test data so it has no impact on running code. 75 | func (t *mockResolver) loadFile(fname string) (r dns.Msg) { 76 | fname = strings.ReplaceAll(fname, ":", "_") 77 | log.Debug("mock:Resolver:Open:", fname) 78 | file, err := os.Open(fname) 79 | if tracer != nil { 80 | _, e2 := fmt.Fprintf(tracer, "%s:%t\n", fname, err == nil) 81 | if e2 != nil { 82 | panic(e2) 83 | } 84 | tracer.Sync() // Because we never get a chance to close it 85 | } 86 | 87 | if err != nil { // Assume no exist 88 | r.MsgHdr.Rcode = dns.RcodeRefused 89 | return 90 | } 91 | defer file.Close() 92 | rcode := -1 // Means not set 93 | 94 | scanner := bufio.NewScanner(file) 95 | ln := 0 96 | for scanner.Scan() { 97 | line := scanner.Text() 98 | line = strings.TrimSuffix(line, "\n") 99 | ln++ 100 | if len(line) == 0 { 101 | continue 102 | } 103 | if strings.HasPrefix(line, ";;") { 104 | continue 105 | } 106 | ar := strings.SplitN(line, ":", 2) 107 | if len(ar) != 2 { // Malformed is a setup error 108 | panic("Malformed loadfile " + fname) 109 | } 110 | 111 | if ar[0] == "RCODE" { 112 | rcode = dns.StringToRcode[ar[1]] 113 | log.Debugf("Mock:File:Rcode %d from '%s'\n", rcode, ar[1]) 114 | continue 115 | } 116 | 117 | rr, err := dns.NewRR(ar[1]) 118 | if err != nil { 119 | panic(err) // Parse failure is a setup error 120 | } 121 | 122 | switch ar[0] { 123 | case "A": 124 | r.Answer = append(r.Answer, rr) 125 | case "N": 126 | r.Ns = append(r.Ns, rr) 127 | case "E": 128 | r.Extra = append(r.Extra, rr) 129 | 130 | default: 131 | panic("filemock bad Section: " + ar[0]) 132 | } 133 | } 134 | 135 | if rcode == -1 { 136 | if len(r.Answer) == 0 && len(r.Ns) == 0 && len(r.Extra) == 0 { 137 | rcode = dns.RcodeNameError // NXDOMAIN 138 | } 139 | } 140 | if rcode == -1 { 141 | rcode = dns.RcodeSuccess 142 | } 143 | r.MsgHdr.Rcode = rcode 144 | 145 | return 146 | } 147 | -------------------------------------------------------------------------------- /osutil/constrain_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package osutil 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/user" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | const ( 16 | me = "osutil.Constrain: " 17 | ) 18 | 19 | // Constrain reduces the abilities of the process by changing to a nominated uid/gid which 20 | // presumably has less power and chroots to a directory that presumably has very little in 21 | // it or below it. 22 | // 23 | // The order of operations is important. The symbolic user and group names are converted 24 | // to uid and gid first while we have access to /etc/passwd (or the moral equivalent) then 25 | // chroot is performed while we presumably have the power to access that directly. After 26 | // that we eliminate supplementary groups as part of setting the group while we have a 27 | // powerful uid and then we finally issue setuid that should make this whole sequence 28 | // irreversible. 29 | // 30 | // Each step is optional if the corresponding parameter is an empty string. 31 | // 32 | // An error is returned if the constrains could not be applied. 33 | // 34 | // Arguable we should also consider setsid and closing all un-needed file descriptors, but 35 | // this is a reasonable start for this application. It is also the case that apparently 36 | // everyone re-writes this function and most get it wrong, so I may have too... 37 | // 38 | // This function is limited on Linux and a noop on Windows. 39 | func Constrain(userName, groupName, chrootDir string) error { 40 | 41 | // Step 1: Convert symbolic names to ids 42 | 43 | uid := -1 44 | gid := -1 45 | if len(userName) > 0 { 46 | u, err := user.Lookup(userName) 47 | if err != nil { 48 | return fmt.Errorf(me+"User name lookup failed: %w", err) 49 | } 50 | uid, err = strconv.Atoi(u.Uid) 51 | if err != nil { 52 | return fmt.Errorf(me+"Could not convert UID %s to an int: %w", u.Uid, err) 53 | } 54 | } 55 | 56 | if len(groupName) > 0 { 57 | g, err := user.LookupGroup(groupName) 58 | if err != nil { 59 | return fmt.Errorf(me+"Group name lookup failed: %w", err) 60 | } 61 | gid, err = strconv.Atoi(g.Gid) 62 | if err != nil { 63 | return fmt.Errorf(me+"Could not convert GID %s to an int: %w", g.Gid, err) 64 | } 65 | } 66 | 67 | // Step 2: chdir/chroot. Must be root to do this, but let Chroot() do the checking. 68 | 69 | if len(chrootDir) > 0 { 70 | err := os.Chdir(chrootDir) 71 | if err != nil { 72 | return fmt.Errorf(me+"Could not cd to %s: %w", chrootDir, err) 73 | } 74 | 75 | err = syscall.Chroot(chrootDir) 76 | if err != nil { 77 | return fmt.Errorf(me+"Could not chroot to %s: %w", chrootDir, err) 78 | } 79 | 80 | err = os.Chdir("/") 81 | if err != nil { 82 | return fmt.Errorf(me+"Could not cd to /: %w", err) 83 | } 84 | } 85 | 86 | // Step 3: setgid. This includes removing all supplementary groups. 87 | 88 | if gid != -1 { 89 | err := syscall.Setgroups([]int{}) 90 | if err != nil { 91 | return fmt.Errorf(me+"Could not clear group list: %w", err) 92 | } 93 | err = syscall.Setgid(gid) 94 | if err != nil { 95 | return fmt.Errorf(me+"Could not setgid to %d/%s: %w", gid, groupName, err) 96 | } 97 | } 98 | 99 | // The final piece of the puzzle. Step 4: setuid 100 | 101 | if uid != -1 { 102 | err := syscall.Setuid(uid) 103 | if err != nil { 104 | return fmt.Errorf(me+"Could not setuid to %d/%s: %w", uid, userName, err) 105 | } 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // ConstraintReport returns a printable string showing the uid/gid/cwd of the 112 | // process. Normally called after Constrain() to confirm that the process has reduced 113 | // privileges. 114 | func ConstraintReport(chroot string) string { 115 | uid := os.Getuid() 116 | gid := os.Getgid() 117 | var cwdPath, cwdType string 118 | if len(chroot) > 0 { 119 | cwdPath = chroot 120 | cwdType = "chroot" 121 | } else { 122 | cwdPath, _ = os.Getwd() 123 | cwdType = "cwd" 124 | } 125 | gList, _ := os.Getgroups() 126 | gStr := make([]string, 0, len(gList)) 127 | for _, g := range gList { 128 | gStr = append(gStr, fmt.Sprintf("%d", g)) 129 | } 130 | 131 | return fmt.Sprintf("uid=%d gid=%d (%s) %s=%s", 132 | uid, gid, strings.Join(gStr, ","), cwdType, cwdPath) 133 | } 134 | -------------------------------------------------------------------------------- /authorities.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "sort" 6 | "strings" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | 11 | "github.com/markdingo/autoreverse/delegation" 12 | "github.com/markdingo/autoreverse/dnsutil" 13 | ) 14 | 15 | type authority struct { 16 | delegation.Authority 17 | forward bool // Whether a forward or reverse authority 18 | cidr *net.IPNet 19 | } 20 | 21 | func newAuthority(da *delegation.Authority, forward bool) *authority { 22 | auth := &authority{} 23 | auth.Authority = *da 24 | auth.forward = forward 25 | 26 | return auth 27 | } 28 | 29 | var soaTime = time.Now() // Set here so tests can over-ride 30 | 31 | func (t *authority) synthesizeSOA(mboxDomain string, TTLAsSecs uint32) { 32 | t.SOA.Hdr.Name = t.Domain 33 | t.SOA.Hdr.Class = dns.ClassINET 34 | t.SOA.Hdr.Rrtype = dns.TypeSOA 35 | t.SOA.Hdr.Ttl = TTLAsSecs 36 | if len(t.NS) > 0 { // Zero is possible for locals 37 | t.SOA.Ns = t.NS[0].(*dns.NS).Ns 38 | } else { 39 | t.SOA.Ns = t.Domain 40 | } 41 | 42 | t.SOA.Mbox = "hostmaster." + mboxDomain // Why not? 43 | t.SOA.Serial = uint32(soaTime.Unix()) 44 | 45 | t.SOA.Refresh = 110040 // None of these timers really have much meaning 46 | t.SOA.Retry = 110080 // but we have to populate them with something so give them 47 | t.SOA.Expire = 28 // signature values which make "von Fastrand" proud. 48 | t.SOA.Minttl = 9030 // Hit me up if you recognize all of these numbers. 49 | } 50 | 51 | // authorities contains the Zones Of Authority which are primarily used to determine 52 | // whether queries are in-domain or not. Once populated, sort() should be called to ensure 53 | // findInDomain() functions properly. 54 | type authorities struct { 55 | slice []*authority 56 | } 57 | 58 | // Only append if unique. Return true if appended. 59 | func (t *authorities) append(add *authority) bool { 60 | if !add.forward && add.cidr == nil { 61 | panic("Attempt to add reverse authority with no CIDR") 62 | } 63 | for _, auth := range t.slice { 64 | if add.Domain == auth.Domain { 65 | return false 66 | } 67 | } 68 | t.slice = append(t.slice, add) 69 | 70 | return true 71 | } 72 | 73 | func (t *authorities) len() int { 74 | return len(t.slice) 75 | } 76 | 77 | // sort arranges the slice of authorities to be in most-specific-first order to ensure 78 | // that findInDomain() returns the most specific zone. 79 | // 80 | // Label count is the primary sort key, with less labels coming earler. If the label 81 | // counts are equal there can't possibly be an overlap so it doesn't matter which order 82 | // they come in, but this function uses the alphabetical FQDN as the secondary sort key 83 | // which produces stable results and a visually convenient order for external viewers. 84 | func (t *authorities) sort() { 85 | sort.Slice(t.slice, 86 | func(i, j int) bool { 87 | di := t.slice[i].Domain 88 | dj := t.slice[j].Domain 89 | ilc := strings.Count(di, ".") 90 | jlc := strings.Count(dj, ".") 91 | if ilc != jlc { // If label counts differ, 92 | return ilc > jlc // the smaller count wins 93 | } 94 | 95 | return di > dj 96 | }, 97 | ) 98 | } 99 | 100 | // findInDomain returns the matching authority for the qName or nil. 101 | // 102 | // The search is serial as it's a suffix match rather than an exact match. Possibly could 103 | // have some fancy suffix tree to mimic the DNS hierarchy, but in most cases the number of 104 | // authorities is likely to be now more than 2 or 3, so a serial search probably beats a 105 | // fancy tree search any way. 106 | // 107 | // Authorities are assumed to have already been sorted by sortAuthorities which ensures 108 | // this function will return the longest prefix/most-specific match. 109 | func (t *authorities) findInDomain(qName string) *authority { 110 | for _, auth := range t.slice { 111 | if dnsutil.InDomain(qName, auth.Domain) { 112 | return auth 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // findIPInDomain finds the matching reverse authority which contains the 120 | // IP. Return nil if not found. 121 | func (t *authorities) findIPInDomain(ip net.IP) *authority { 122 | for _, auth := range t.slice { 123 | if !auth.forward && auth.cidr.Contains(ip) { 124 | return auth 125 | } 126 | } 127 | 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /chaos_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/markdingo/autoreverse/database" 7 | "github.com/markdingo/autoreverse/dnsutil" 8 | "github.com/markdingo/autoreverse/log" 9 | "github.com/markdingo/autoreverse/mock" 10 | "github.com/markdingo/autoreverse/resolver" 11 | 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | func TestDNSChaos(t *testing.T) { 16 | cfg := &config{logQueriesFlag: true, projectURL: "projectURL", 17 | nsid: "nsid1", TTLAsSecs: 60, chaosFlag: true} 18 | expect := commonCHAOSPrefix + " " + cfg.projectURL 19 | out := &mock.IOWriter{} 20 | log.SetOut(out) 21 | log.SetLevel(log.MajorLevel) 22 | 23 | wtr := &mock.ResponseWriter{} 24 | res := resolver.NewResolver() 25 | ar := newAutoReverse(cfg, res) 26 | newDB := database.NewDatabase() 27 | ar.loadFromChaos(newDB) 28 | ar.dbGetter.Replace(newDB) 29 | server := newServer(cfg, ar.dbGetter, res, nil, "", "") 30 | 31 | // First try with wrong type 32 | query := setQuestion(dns.ClassCHAOS, dns.TypeNS, "version.bind.") 33 | server.ServeDNS(wtr, query) 34 | resp := wtr.Get() 35 | if resp == nil { 36 | t.Fatal("Setup error - No response to chaos query") 37 | } 38 | if resp.Rcode != dns.RcodeRefused { 39 | t.Error("Expected RcodeRefused, not", dns.RcodeToString[wtr.Get().Rcode]) 40 | } 41 | 42 | // Check error logging 43 | exp := "ru=REFUSED q=NS/version.bind. s=127.0.0.2:4056 id=1 h=U sz=41/1232 C=0/0/1\n" 44 | got := out.String() 45 | if exp != got { 46 | t.Error("Error log mismatch. \n Got:", got, "Exp:", exp) 47 | } 48 | 49 | // Check not chaos flag set 50 | 51 | cfg.chaosFlag = false 52 | out.Reset() 53 | query = setQuestion(dns.ClassCHAOS, dns.TypeTXT, "version.bind.") 54 | query.Id = 2 55 | server.ServeDNS(wtr, query) 56 | resp = wtr.Get() 57 | if resp == nil { 58 | t.Fatal("Setup error - No response to chaos query") 59 | } 60 | if resp.Rcode != dns.RcodeRefused { 61 | t.Error("Expected RcodeRefused, not", dnsutil.RcodeToString(resp.Rcode)) 62 | } 63 | 64 | // Check error logging 65 | exp = "ru=REFUSED q=TXT/version.bind. s=127.0.0.2:4056 id=2 h=U sz=41/1232 C=0/0/1 not in-domain\n" 66 | got = out.String() 67 | if exp != got { 68 | t.Error("Error log mismatch \n Got:", got, "Exp:", exp) 69 | } 70 | 71 | // Now with flag set 72 | 73 | out.Reset() 74 | cfg.chaosFlag = true 75 | 76 | testCases := []struct{ in, out string }{ 77 | {"version.bind.", expect}, 78 | {"version.server.", expect}, 79 | {"authors.bind.", expect}, 80 | {"hostname.bind.", "nsid1"}, 81 | {"id.server.", "nsid1"}, 82 | {"nope", ""}, 83 | } 84 | 85 | for ix, tc := range testCases { 86 | query = setQuestion(dns.ClassCHAOS, dns.TypeTXT, tc.in) 87 | query.Id = uint16(3 + ix) 88 | server.ServeDNS(wtr, query) 89 | resp = wtr.Get() 90 | if resp == nil { 91 | t.Fatal(ix, "Setup error - No response to chaos query") 92 | } 93 | if resp.Rcode != dns.RcodeSuccess { 94 | if len(tc.out) > 0 { // Expect an error if no response expected 95 | t.Error(ix, 96 | "Expected RcodeSuccess, not", 97 | dnsutil.RcodeToString(resp.Rcode)) 98 | } 99 | continue 100 | } 101 | 102 | if len(resp.Answer) != 1 { 103 | t.Error(ix, "Wrong number of answers", len(resp.Answer)) 104 | continue 105 | } 106 | ans := resp.Answer[0] 107 | if txt, ok := ans.(*dns.TXT); ok { 108 | if len(txt.Txt) != 1 { 109 | t.Error(ix, "Wrong TXT count", len(txt.Txt)) 110 | continue 111 | } 112 | if txt.Txt[0] != tc.out { 113 | t.Error(ix, "Response not as expected: got", txt.Txt[0], "exp", tc.out) 114 | } 115 | } else { 116 | t.Error(ix, "Did not get a TXT answer", ans) 117 | } 118 | } 119 | 120 | // Check logging to confirm responses - good enough 121 | exp = `ru=ok q=TXT/version.bind. s=127.0.0.2:4056 id=3 h=U sz=106/1232 C=1/0/1 122 | ru=ok q=TXT/version.server. s=127.0.0.2:4056 id=4 h=U sz=110/1232 C=1/0/1 123 | ru=ok q=TXT/authors.bind. s=127.0.0.2:4056 id=5 h=U sz=106/1232 C=1/0/1 124 | ru=ok q=TXT/hostname.bind. s=127.0.0.2:4056 id=6 h=U sz=73/1232 C=1/0/1 125 | ru=ok q=TXT/id.server. s=127.0.0.2:4056 id=7 h=U sz=65/1232 C=1/0/1 126 | ru=REFUSED q=TXT/nope. s=127.0.0.2:4056 id=8 h=U sz=33/1232 C=0/0/1 127 | ` 128 | got = out.String() 129 | if exp != got { 130 | t.Error("Log mismatch\ngot >>"+got+"<<\nexp >>"+exp+"<<", len(got), len(exp)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /dnsutil/invert_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInvertToIPv4(t *testing.T) { 8 | testCases := []struct { 9 | input, expect string 10 | truncated bool 11 | }{ 12 | {"1.2.3.4", "4.3.2.1", false}, 13 | {"255.255.255.255", "255.255.255.255", false}, 14 | 15 | {"1.168.192", "192.168.1.0", true}, 16 | {"168.192", "192.168.0.0", true}, 17 | {"192", "192.0.0.0", true}, 18 | {"", "", false}, 19 | 20 | {"255.255.255.255.255", "", false}, 21 | {"255.255.255", "255.255.255.0", true}, 22 | {"001.2.3.4", "", false}, 23 | {"a.b.c.d.e", "", false}, 24 | {"11.120.0.205", "205.0.120.11", false}, 25 | {"11.120.0.300", "", false}, 26 | {"11.120..200", "", false}, 27 | } 28 | 29 | for ix, tc := range testCases { 30 | ip, truncated, err := InvertPtrToIPv4(tc.input) 31 | if err != nil { 32 | if len(tc.expect) == 0 { 33 | continue 34 | } 35 | t.Error(ix, "Unexpected error with", tc.input, err) 36 | continue 37 | } 38 | if truncated != tc.truncated { 39 | t.Error(ix, "Truncated flag is not", tc.truncated) 40 | } 41 | if len(tc.expect) == 0 { // Expect error? 42 | t.Error(ix, "Expected error, got none with", tc.input, "and", ip.String()) 43 | continue 44 | } 45 | if ip.String() != tc.expect { 46 | t.Error(ix, "Mismatch. Expected:", tc.expect, "got", ip.String()) 47 | } 48 | } 49 | } 50 | 51 | func TestInvertToIPv6(t *testing.T) { 52 | testCases := []struct { 53 | input, expect string 54 | truncated bool 55 | }{ 56 | {"1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", "::1", false}, 57 | {"3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.8.0.a.0.0.0.3.0.3.0.4.2", 58 | "2403:300:a08:f000::3", false}, 59 | {"7.d.0.5.2.d.a.c.5.c.b.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 60 | "fd2d:e363:95de:fe30:881:7bc5:cad2:50d7", false}, 61 | // 3. Mixed case hex 62 | {"7.D.0.5.2.d.a.C.5.c.B.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 63 | "fd2d:e363:95de:fe30:881:7bc5:cad2:50d7", false}, 64 | // 4. Empty nibble '..' 65 | {"7.d.0.5.2.d.a.c.5..b.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", "", false}, 66 | // 5. Invalid hex 'g' 67 | {"7.d.0.5.2.d.a.c.5.g.b.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", "", false}, 68 | // 6. Truncated 69 | {"1.7.d.0.5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 70 | "fd2d:e363:95de:fe30:881:75ca:d250:d710", true}, 71 | {"7.d.0.5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 72 | "fd2d:e363:95de:fe30:881:75ca:d250:d700", true}, 73 | {"d.0.5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 74 | "fd2d:e363:95de:fe30:881:75ca:d250:d000", true}, 75 | {"d.0.5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 76 | "fd2d:e363:95de:fe30:881:75ca:d250:d000", true}, 77 | {"0.5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", 78 | "fd2d:e363:95de:fe30:881:75ca:d250:0", true}, 79 | // 11. Empty nibble 80 | {".5.2.d.a.c.5.7.1.8.8.0.0.3.e.f.e.d.5.9.3.6.3.e.d.2.d.f", "", false}, 81 | // 12. nibble greater than 1 byte 82 | {"001.2.3.4", "", false}, 83 | {"11.120.0.205", "", false}, 84 | } 85 | 86 | for ix, tc := range testCases { 87 | ip, truncated, err := InvertPtrToIPv6(tc.input) 88 | if err != nil { 89 | if len(tc.expect) == 0 { 90 | continue 91 | } 92 | t.Error(ix, "Unexpected error with", tc.input, err) 93 | continue 94 | } 95 | if truncated != tc.truncated { 96 | t.Error(ix, "Truncated flag is not", tc.truncated) 97 | } 98 | if len(tc.expect) == 0 { // Expect error? 99 | t.Error(ix, "Expected error, got none with", tc.input, "and", ip.String()) 100 | continue 101 | } 102 | if ip.String() != tc.expect { 103 | t.Error(ix, "Mismatch. Input:", tc.input, "got", ip.String()) 104 | } 105 | } 106 | } 107 | 108 | func TestConvertDecimalOctet(t *testing.T) { 109 | testCases := []struct { 110 | input string 111 | expect int 112 | }{ 113 | {"", -1}, 114 | {"z", -1}, 115 | {".255.", -1}, 116 | {"zabc", -1}, 117 | {"123", 123}, 118 | {"0", 0}, 119 | {"255", 255}, 120 | {"256", -1}, 121 | {"25x", -1}, 122 | {"a25", -1}, 123 | {"2a5", -1}, 124 | {"001", -1}, 125 | } 126 | 127 | for ix, tc := range testCases { 128 | ret := convertDecimalOctet(tc.input) 129 | if ret != tc.expect { 130 | t.Error(ix, "Input:", tc.input, "Expected:", tc.expect, "Got:", ret) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /dnsutil/invert.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // InvertPtrToIP extracts and inverts the purported IP address from a reverse qName. Like 10 | // any name in the DNS, a reverse qName does not *have* to represent an IP address, but 11 | // this code ignores all else. Return an error if an IP address cannot be extracted. The 12 | // return bool is true if the IP address is as valid as far as it goes, but is truncated. 13 | func InvertPtrToIP(qName string) (net.IP, bool, error) { 14 | if strings.HasSuffix(qName, V4Suffix) { 15 | return InvertPtrToIPv4(strings.TrimSuffix(qName, V4Suffix)) 16 | } 17 | if strings.HasSuffix(qName, V6Suffix) { 18 | return InvertPtrToIPv6(strings.TrimSuffix(qName, V6Suffix)) 19 | } 20 | 21 | return nil, false, fmt.Errorf("Unknown reverse suffix '%s'", qName) 22 | } 23 | 24 | // InvertPtrToIPv4 takes the first part of the reverse qName from the ipv4 zone and 25 | // converts it back into an ipv4 Address, if possible. As a reminder, a dig -x 192.168.1.2 26 | // results in a qName of 2.1.168.192.in-addr.arpa. The suffix is removed by the caller 27 | // leaving just 2.1.168.192. There are of course no guarantees that this string is in 28 | // reversed IP address format as a rogue query can come in directly with anything in 29 | // qName, thus all the checking and potential error return if the string doesn't parse. 30 | // 31 | // The returned bool is true if the IP address is valid as far as it goes, but is 32 | // truncated, e.g. 1.168.192.in-addr.arpa. The reason for converting truncated IPs is so 33 | // that the caller can distinguish between a malformed address and a truncated one as the 34 | // former results in an NXDomain and the latter results in a NoError. 35 | func InvertPtrToIPv4(qName string) (net.IP, bool, error) { 36 | if len(qName) == 0 { 37 | return nil, false, fmt.Errorf("Empty reverse ipv4 address qName") 38 | } 39 | var octets [4]byte 40 | reverse := strings.SplitN(qName, ".", 4) 41 | ix := 4 - len(reverse) 42 | for _, octet := range reverse { 43 | v := convertDecimalOctet(octet) 44 | if v == -1 { 45 | return nil, false, fmt.Errorf("Malformed reverse ipv4 address '%s'", qName) 46 | } 47 | octets[ix] = byte(v) 48 | ix++ 49 | } 50 | ip := net.IPv4(octets[3], octets[2], octets[1], octets[0]) 51 | 52 | return ip, len(reverse) < 4, nil 53 | } 54 | 55 | // InvertPtrToIPv6 takes the first part of the reverse query name, and converts it back 56 | // into an ipv6 address, if possible. Expected input looks something like: 57 | // 3.f.6.d.4.d.3.b.c.4.3.0.1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa less the 58 | // "ip6.arpa" suffix. 59 | // 60 | // The returned bool is true if the IP address is valid as far as it goes, but is 61 | // truncated, e.g. 0.8.e.f.ip6.arpa returns an ipv6 address of fe80::0 with 62 | // truncated=true. See the discussion of InvertPtrToIPv4. 63 | func InvertPtrToIPv6(qName string) (net.IP, bool, error) { 64 | if len(qName) == 0 { 65 | return nil, false, fmt.Errorf("Empty reverse ipv6 address qName") 66 | } 67 | var hex [32]byte 68 | reverse := strings.SplitN(qName, ".", 32) 69 | ix := 32 - len(reverse) 70 | for _, hStr := range reverse { 71 | if len(hStr) != 1 { 72 | return nil, false, fmt.Errorf("Malformed reverse ipv6 address '%s'", qName) 73 | } 74 | h := hStr[0] 75 | switch { 76 | case h >= '0' && h <= '9': 77 | hex[ix] = h - '0' 78 | case h >= 'a' && h <= 'f': 79 | hex[ix] = h - 'a' + 10 80 | case h >= 'A' && h <= 'F': 81 | hex[ix] = h - 'A' + 10 82 | default: 83 | return nil, false, fmt.Errorf("Malformed reverse ipv6 address '%s'", qName) 84 | } 85 | ix++ 86 | } 87 | 88 | ip := make(net.IP, net.IPv6len) // Create an allocated net.IP 89 | ix = 15 90 | for rx := 0; rx < 32; rx += 2 { 91 | ip[ix] = hex[rx+1]<<4 + hex[rx] 92 | ix-- 93 | } 94 | 95 | return ip, len(reverse) < 32, nil 96 | } 97 | 98 | // convertDecimalOctet strictly converts an ipv4 decimal octet to an int. Return -1 if 99 | // conversion fails. Rules: no leading zeroes, numeric range 0-255, lenfth 1-3 bytes and 100 | // no non-digit characters. 101 | func convertDecimalOctet(s string) (ret int) { 102 | if len(s) == 0 || len(s) > 3 { 103 | return -1 104 | } 105 | if s[0] == '0' && len(s) > 1 { // Don't allow leading digits 106 | return -1 107 | } 108 | 109 | for _, c := range s { 110 | if c < '0' || c > '9' { 111 | return -1 112 | } 113 | c -= '0' 114 | ret *= 10 115 | ret += int(c) 116 | } 117 | if ret > 255 { 118 | return -1 119 | } 120 | 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /mock/resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | 11 | "github.com/markdingo/autoreverse/dnsutil" 12 | "github.com/markdingo/autoreverse/log" 13 | "github.com/markdingo/autoreverse/resolver" 14 | ) 15 | 16 | // mockResolver implements the resolver.Resolver interface by converting queries to 17 | // file names and loading responses from those files. The convention is, if the file 18 | // doesn't exist, the response is REFUSED or an error - depending on the interface. If the 19 | // file exists, each line in the file is parsed with dns.NewRR. 20 | // 21 | // If the number of RRs parsed is zero, then rcode is NOERROR for functions which return 22 | // an rcode. 23 | // 24 | // The filename convention for Lookup functions is: $dir/lookup/$Class/$Type/$qname and 25 | // for Exchange is $dir/exchange/$net/$IP/$Class/$Type/$qname. 26 | type mockResolver struct { 27 | dir string 28 | } 29 | 30 | // NewResolver creates a mock resolver which uses the supplied directory as the location 31 | // of mock files to parse to produce dns lookup responses. 32 | func NewResolver(dir string) *mockResolver { 33 | return &mockResolver{dir: dir} 34 | } 35 | 36 | func (t *mockResolver) LookupNS(ctx context.Context, name string) (ns []string, err error) { 37 | name = dnsutil.ChompCanonicalName(name) 38 | msg, path := t.loadLookupFile("IN", "NS", name) 39 | rCode := msg.MsgHdr.Rcode 40 | nsSet := make([]*net.NS, 0) // For logging purposes only 41 | if rCode == dns.RcodeSuccess { 42 | for _, rr := range msg.Answer { // Convert msg Answer RRs to strings 43 | if rrt, ok := rr.(*dns.NS); ok { 44 | ns = append(ns, rrt.Ns) 45 | nsSet = append(nsSet, &net.NS{Host: rrt.Ns}) 46 | } 47 | } 48 | } else { 49 | err = fmt.Errorf("host not found") 50 | } 51 | 52 | resolver.LogNS(name, nsSet, path, err) 53 | 54 | return 55 | } 56 | 57 | func (t *mockResolver) LookupIPAddr(ctx context.Context, host string) (ips []net.IP, err error) { 58 | host = dnsutil.ChompCanonicalName(host) 59 | aMsg, aPath := t.loadLookupFile("IN", "A", host) 60 | aaaaMsg, aaaaPath := t.loadLookupFile("IN", "AAAA", host) 61 | aCode := aMsg.MsgHdr.Rcode 62 | aaaaCode := aaaaMsg.MsgHdr.Rcode 63 | 64 | addrs := make([]net.IPAddr, 0) // For logging purposes only 65 | 66 | // Convert msg answers to the returned slice of net.IPs 67 | if aCode == dns.RcodeSuccess { 68 | for _, rr := range aMsg.Answer { 69 | if rrt, ok := rr.(*dns.A); ok { 70 | ips = append(ips, rrt.A) 71 | addrs = append(addrs, net.IPAddr{IP: rrt.A}) 72 | } 73 | } 74 | } 75 | 76 | if aaaaCode == dns.RcodeSuccess { 77 | for _, rr := range aaaaMsg.Answer { 78 | if rrt, ok := rr.(*dns.AAAA); ok { 79 | ips = append(ips, rrt.AAAA) 80 | addrs = append(addrs, net.IPAddr{IP: rrt.AAAA}) 81 | } 82 | } 83 | } 84 | if len(addrs) == 0 { // A proxy for aCode and aaaaCode 85 | err = fmt.Errorf("no such host") 86 | } 87 | resolver.LogIP(host, addrs, aPath+","+aaaaPath, err) 88 | 89 | return 90 | 91 | } 92 | 93 | func (t *mockResolver) SingleExchange(ctx context.Context, c resolver.ExchangeConfig, q *dns.Msg, 94 | server, logName string) (out *dns.Msg, rtt time.Duration, err error) { 95 | if len(q.Question) != 1 { 96 | err = fmt.Errorf("SingleExchange Message contains %d Question(s), expect one", 97 | len(q.Question)) 98 | return 99 | } 100 | 101 | question := q.Question[0] 102 | net := c.Net() 103 | if len(net) == 0 { // Set to specific value to ensure correct path generation 104 | net = "udp" 105 | } 106 | 107 | if log.IfDebug() { 108 | resolver.LogExchangeQ(net, logName, server, question) 109 | } 110 | r, _ := t.loadExchangeFile(net, server, 111 | dns.ClassToString[question.Qclass], dns.TypeToString[question.Qtype], 112 | dnsutil.ChompCanonicalName(question.Name)) 113 | rcode := r.MsgHdr.Rcode 114 | r.SetRcode(q, rcode) 115 | if rcode == dns.RcodeServerFailure { // Return an error 116 | err = fmt.Errorf("Server Failed") 117 | } 118 | if log.IfDebug() { 119 | resolver.LogExchangeA(server, question, &r, err) 120 | } 121 | 122 | out = &r 123 | 124 | return 125 | } 126 | 127 | // Only need to do a single exchange here as the file system is a tad more stable than the 128 | // DNS and can hold more than 512 bytes per file - hopefully! 129 | func (t *mockResolver) FullExchange(ctx context.Context, c resolver.ExchangeConfig, q dns.Question, 130 | server, logName string) (r *dns.Msg, rtt time.Duration, err error) { 131 | query := new(dns.Msg) 132 | query.Question = append(query.Question, q) 133 | return t.SingleExchange(ctx, c, query, server, logName) 134 | } 135 | -------------------------------------------------------------------------------- /resolver/interface.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | 10 | "github.com/markdingo/autoreverse/dnsutil" 11 | ) 12 | 13 | const ( 14 | defaultSingleExchangeTimeout = 4 * time.Second // Also applies to Lookup* functions 15 | defaultfFullExchangeTimeout = 3 * defaultSingleExchangeTimeout 16 | defaultQueryTries = 2 // Total number of exchange attempts 17 | ) 18 | 19 | // ExchangeConfig expresses settings which were previously passed to miekg via a 20 | // Client struct. Only the ones relevant to autoreverse have been transferred across to 21 | // ExchangeConfig. It's defined as an interface rather than a struct to enforce the use 22 | // of NewExchangeConfig with sets default. 23 | type ExchangeConfig interface { 24 | Net() string 25 | UDPSize() uint16 26 | setNet(s string) 27 | } 28 | 29 | type exchangeConfig struct { 30 | net string // Not sure whether this is needed or the right place 31 | udpSize uint16 32 | } 33 | 34 | func (t *exchangeConfig) Net() string { return t.net } 35 | func (t *exchangeConfig) UDPSize() uint16 { return t.udpSize } 36 | func (t *exchangeConfig) setNet(s string) { t.net = s } 37 | 38 | // NewExchangeConfig creates a default configuration struct required by SingleExchange() 39 | // and FullExchange() to pass in parameters needed by miekg. 40 | func NewExchangeConfig() *exchangeConfig { 41 | return &exchangeConfig{net: dnsutil.UDPNetwork, udpSize: dnsutil.MaxUDPSize} 42 | } 43 | 44 | // Resolver represents the Frankenstein interface which supports *all* of the resolver 45 | // functions used by autoreverse which reach out to the internet. All non-networking 46 | // functions are still called directly by the application. 47 | // 48 | // Based on the claim that both net.Resolver and miekg.Client are concurrency safe, then 49 | // implementations of this interface must also ensure concurrency safety. 50 | // 51 | // Arguably having the caller pass a context is bogus since all callers in this 52 | // application pass in context.Background() so the Resolver functions could just refer to 53 | // that directly. Nonetheless, context is the future, so we've left that complexity in 54 | // place. 55 | type Resolver interface { 56 | 57 | // LookupNS is similar to net.Resolver.LookupNS. 58 | // 59 | // LookupNS derives a WithDeadline context from the supplied context so there is 60 | // no need for the caller to worry about timeouts. 61 | LookupNS(context.Context, string) ([]string, error) 62 | 63 | // LookipIPAddr is similar to net.Resolver.LookupAddr. 64 | // 65 | // LookupIPAddr derives a WithDeadline context from the supplied context so there 66 | // is no need for the caller to worry about timeouts. 67 | LookupIPAddr(context.Context, string) ([]net.IP, error) 68 | 69 | // SingleExchange is a shim for the github.com/miekg/dns ExchangeContext function 70 | // which makes a single exchange attempt with the server; no retries, no fallback 71 | // to TCP. See FullExchange() for that capability. 72 | // 73 | // ExchangeConfig defines parameters which were originally passed thru to miekg 74 | // via the the miekg.Client struct but we want to reduce complexity where possible 75 | // to simplify alternative implementations of Resolver. 76 | // 77 | // SingleExchange sets the dns.Client.Timeout to singleExchangeTimeout so the 78 | // caller doesn't have to worry about timeouts via context, or whatever. 79 | // 80 | // The dns.Msg must be fully formed with all flags and Id set as needed by the 81 | // caller. 82 | // 83 | // logName is normally the domainName of server and is only used for logging 84 | // purposes to help identify the server (which is normally an ip address in the 85 | // autoreverse context). 86 | SingleExchange(ctx context.Context, c ExchangeConfig, q *dns.Msg, 87 | server, logName string) (r *dns.Msg, rtt time.Duration, err error) 88 | 89 | // FullExchange is a wrapper around SingleExchange which handles timeouts and 90 | // truncation. It also creates a fully-formed dns.Msg for SingleExchange. 91 | // 92 | // FullExchange derives a WithDeadline context from the supplied context to manage 93 | // timeouts so the caller doesn't have to do that themselves. This timeout applies 94 | // across the whole of FullExchange processing including retries and truncation 95 | // processing. The SingleExchange timeouts still apply across calls to it thus 96 | // there are in effect two timeouts active for exchanges initiated via 97 | // FullExchange. 98 | FullExchange(ctx context.Context, c ExchangeConfig, q dns.Question, 99 | server, logName string) (r *dns.Msg, rtt time.Duration, err error) 100 | } 101 | -------------------------------------------------------------------------------- /database/tree_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestAddRR(t *testing.T) { 10 | db := NewDatabase() 11 | tf := db.AddRR(newRR("a.b.c. IN A 1.2.3.4")) 12 | if !tf { 13 | t.Error("Expected Add to work") 14 | } 15 | tf = db.AddRR(newRR("a.b.c. IN A 1.2.3.4")) 16 | if tf { 17 | t.Error("Expected Second Add to fail") 18 | } 19 | c := db.Count() 20 | if c != 1 { 21 | t.Error("Count should be one, not", c) 22 | } 23 | db.AddRR(newRR("a.b.c. IN A 1.2.3.5")) 24 | db.AddRR(newRR("a.b.c. IN AAAA ::1")) 25 | db.AddRR(newRR("a.b.c.d.e.f. IN AAAA ::1")) 26 | db.AddRR(newRR("a.w.x.b.c.d.e.f. IN AAAA ::1")) 27 | db.AddRR(newRR("3.f.6.d.4.d.3.b.c.4.3.0.1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa. IN PTR a.b.c.")) 28 | c = db.Count() 29 | if c != 6 { 30 | t.Error("Count should be six, not", c) 31 | } 32 | } 33 | 34 | func TestLookup(t *testing.T) { 35 | type testCase struct { 36 | qClass uint16 37 | qType uint16 38 | qName string 39 | arCount int 40 | nx bool 41 | } 42 | testCases := []testCase{ 43 | {dns.ClassINET, dns.TypeA, ".", 0, true}, // No class 44 | {dns.ClassHESIOD, dns.TypeTXT, "bind.version.", 0, true}, // No class 45 | {dns.ClassINET, dns.TypeA, "b.c.", 0, false}, // a.b.c. exists 46 | {dns.ClassCHAOS, dns.TypeTXT, "bind.version.", 1, false}, 47 | {dns.ClassCHAOS, dns.TypeA, "bind.version.", 0, false}, 48 | {dns.ClassINET, dns.TypeA, "a.b.c.d.e.f.", 0, false}, 49 | {dns.ClassINET, dns.TypeAAAA, "a.b.c.d.e.f.", 1, false}, 50 | {dns.ClassINET, dns.TypeAAAA, "w.a.b.c.d.e.f.", 0, true}, // Too deep 51 | 52 | {dns.ClassINET, dns.TypeAAAA, "a.b.c.d.e.f.", 1, false}, 53 | {dns.ClassINET, dns.TypeAAAA, "a.b.", 0, true}, // No TLD of b. 54 | {dns.ClassINET, dns.TypePTR, "0.168.192.in-addr.arpa.", 0, false}, 55 | {dns.ClassINET, dns.TypePTR, "2.0.168.192.in-addr.arpa.", 0, true}, 56 | {dns.ClassINET, dns.TypePTR, 57 | "3.f.6.d.4.d.3.b.c.4.3.0.1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.", 58 | 1, false}, 59 | {dns.ClassINET, dns.TypePTR, 60 | "f.6.d.4.d.3.b.c.4.3.0.1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.", 61 | 0, false}, 62 | {dns.ClassINET, dns.TypePTR, 63 | "1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.", 64 | 0, false}, 65 | } 66 | 67 | db := NewDatabase() 68 | ar, nx := db.LookupRR(dns.ClassINET, dns.TypeA, "a.") 69 | if len(ar) > 0 { 70 | t.Error("Lookup of empty DB returned RRset", ar) 71 | } 72 | if !nx { 73 | t.Error("Lookup of empty DB returned NoError") 74 | } 75 | db.AddRR(newRR("a.b.c. IN A 1.2.3.4")) 76 | db.AddRR(newRR("a.b.c. IN A 1.2.3.5")) // 2 A RRs 77 | db.AddRR(newRR("a.b.c. IN AAAA ::1")) // and 1 AAAA RR and zero MX RRs 78 | db.AddRR(newRR("a.b.c.d.e.f. IN AAAA ::1")) 79 | db.AddRR(newRR("a.w.x.b.c.d.e.f. IN AAAA ::1")) 80 | db.AddRR(newRR("bind.version. CH TXT '10.1'")) 81 | db.AddRR(newRR("3.f.6.d.4.d.3.b.c.4.3.0.1.3.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa. IN PTR a.b.c.")) 82 | db.AddRR(newRR("1.0.168.192.in-addr.arpa. IN PTR w.x.y.")) 83 | for ix, tc := range testCases { 84 | ar, nx = db.LookupRR(tc.qClass, tc.qType, tc.qName) 85 | if len(ar) != tc.arCount { 86 | t.Error(ix, "Wrong rrset count", len(ar), tc.arCount) 87 | } 88 | if nx != tc.nx { 89 | t.Error(ix, "Wrong NXDomain of", nx) 90 | } 91 | } 92 | } 93 | 94 | func TestImmutable(t *testing.T) { 95 | db := NewDatabase() 96 | rr1 := newRR("a.b.c. IN A 1.2.3.4") 97 | db.AddRR(rr1) 98 | rr1.Header().Ttl = 53 99 | ans, _ := db.LookupRR(dns.ClassINET, dns.TypeA, "a.b.c.") 100 | for _, a := range ans { 101 | if a.Header().Ttl == 53 { 102 | t.Error("Was able to modify in-DB copy of RR", ans) 103 | } 104 | } 105 | 106 | // Second and subsequent RRs take a different code path in Add() so check that 107 | // path as well. Also test for modification on returned RRs. 108 | rr1 = newRR("a.b.c. IN A 1.2.3.5") 109 | db.AddRR(rr1) 110 | rr1.Header().Ttl = 53 111 | ans, _ = db.LookupRR(dns.ClassINET, dns.TypeA, "a.b.c.") 112 | for _, a := range ans { 113 | if a.Header().Ttl == 53 { 114 | t.Error("Was able to modify in-DB copy of RR", ans) 115 | } 116 | a.Header().Ttl = 55 117 | } 118 | 119 | ans, _ = db.LookupRR(dns.ClassINET, dns.TypeA, "a.b.c.") 120 | for _, a := range ans { 121 | if a.Header().Ttl == 55 { 122 | t.Error("Was able to modify returned copy of RR", ans) 123 | } 124 | } 125 | } 126 | 127 | // Allow newRR in function calls by dealing with errors locally 128 | func newRR(s string) dns.RR { 129 | rr, err := dns.NewRR(s) 130 | if err != nil { 131 | panic("newRR Setup error with: " + s) 132 | } 133 | 134 | return rr 135 | } 136 | -------------------------------------------------------------------------------- /dnsutil/pretty_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestPretty(t *testing.T) { 10 | m := new(dns.Msg) 11 | s := PrettyMsg1(m) 12 | exp := "0 f= NOERROR Q=0- Ans=0- Ns=0- Extra=0-" // Completely empty 13 | if s != exp { 14 | t.Error("PrettyMsg1 empty msg got", s, "not", exp) 15 | } 16 | 17 | rr1, _ := dns.NewRR("x.example. IN AAAA ::1") 18 | rr2, _ := dns.NewRR("y.example. IN MX 10 a.b.") 19 | m.Ns = append(m.Ns, rr1) 20 | m.Answer = append(m.Answer, rr1) 21 | m.Answer = append(m.Answer, rr2) 22 | m.Extra = append(m.Extra, rr1) 23 | m.MsgHdr.Id = 4321 24 | m.MsgHdr.Response = true 25 | m.MsgHdr.Authoritative = true 26 | m.MsgHdr.Truncated = true 27 | 28 | s = PrettyMsg1(m) 29 | exp = "4321 f=qr+aa+tc NOERROR Q=0- Ans=2-AAAA,MX Ns=1-AAAA Extra=1-AAAA" 30 | if s != exp { 31 | t.Error("PrettyMsg1 full msg got", s, "not", exp) 32 | } 33 | 34 | // Question 35 | 36 | m.SetQuestion("example.org", dns.TypeSOA) 37 | s = PrettyQuestion(m.Question[0]) 38 | exp = "IN/SOA example.org" 39 | if s != exp { 40 | t.Error("PrettyQuestion wrong", s, "not", exp) 41 | } 42 | 43 | // NS 44 | 45 | rr, _ := dns.NewRR("example.org IN NS a.ns.example.org") 46 | s = PrettyNS(rr.(*dns.NS), false) 47 | exp = "IN/NS 3600 a.ns.example.org." 48 | if s != exp { 49 | t.Error("PrettyNS 1", s) 50 | } 51 | 52 | s = PrettyNS(rr.(*dns.NS), true) 53 | exp = "example.org. IN/NS 3600 a.ns.example.org." 54 | if s != exp { 55 | t.Error("PrettyNS 2", s) 56 | } 57 | s = PrettyRR(rr, true) 58 | if s != exp { 59 | t.Error("PrettyNS 3", s) 60 | } 61 | 62 | // ShortNSSet 63 | 64 | rr1, _ = dns.NewRR("example.org IN NS b.ns.example.org") 65 | s = PrettyShortNSSet([]dns.RR{rr, rr1}) 66 | exp = "a.ns.example.org., b.ns.example.org." 67 | if s != exp { 68 | t.Error("PrettyNSSet", s) 69 | } 70 | 71 | // SOA 72 | 73 | rr, _ = dns.NewRR("example.net IN SOA internal. hostmaster. 1 2 3 4 5") 74 | s = PrettySOA(rr.(*dns.SOA), false) 75 | exp = "IN/SOA 3600 internal. hostmaster. 1 2 3 4 5" 76 | if s != exp { 77 | t.Error("PrettySOA 1", s) 78 | } 79 | s = PrettySOA(rr.(*dns.SOA), true) 80 | exp = "example.net. IN/SOA 3600 internal. hostmaster. 1 2 3 4 5" 81 | if s != exp { 82 | t.Error("PrettySOA 2", s) 83 | } 84 | s = PrettyRR(rr, true) 85 | if s != exp { 86 | t.Error("PrettySOA 3", s) 87 | } 88 | 89 | // AAAA 90 | 91 | rr1, _ = dns.NewRR("f1.example.net. IN AAAA ::1") 92 | s = PrettyAAAA(rr1.(*dns.AAAA), false) 93 | exp = "IN/AAAA 3600 ::1" 94 | if s != exp { 95 | t.Error("PrettyAAAA 1", s) 96 | } 97 | s = PrettyAAAA(rr1.(*dns.AAAA), true) 98 | exp = "f1.example.net. IN/AAAA 3600 ::1" 99 | if s != exp { 100 | t.Error("PrettyAAAA 2", s) 101 | } 102 | s = PrettyRR(rr1, true) 103 | if s != exp { 104 | t.Error("PrettyAAAA 3", s) 105 | } 106 | 107 | // A 108 | 109 | rr2, _ = dns.NewRR("f1.example.net. IN A 127.0.0.1") 110 | s = PrettyA(rr2.(*dns.A), false) 111 | exp = "IN/A 3600 127.0.0.1" 112 | if s != exp { 113 | t.Error("PrettyA 1", s) 114 | } 115 | s = PrettyA(rr2.(*dns.A), true) 116 | exp = "f1.example.net. IN/A 3600 127.0.0.1" 117 | if s != exp { 118 | t.Error("PrettyA 2", s) 119 | } 120 | s = PrettyRR(rr2, true) 121 | if s != exp { 122 | t.Error("PrettyA 3", s) 123 | } 124 | 125 | // PTR 126 | 127 | rr3, _ := dns.NewRR("0.0.192.in-addr.arpa. IN PTR f1.example.net") 128 | s = PrettyPTR(rr3.(*dns.PTR), false) 129 | exp = "IN/PTR 3600 f1.example.net." 130 | if s != exp { 131 | t.Error("PrettyPTR 1", s) 132 | } 133 | s = PrettyPTR(rr3.(*dns.PTR), true) 134 | exp = "0.0.192.in-addr.arpa. IN/PTR 3600 f1.example.net." 135 | if s != exp { 136 | t.Error("PrettyPTR 2", s) 137 | } 138 | s = PrettyRR(rr3, true) 139 | if s != exp { 140 | t.Error("PrettyPTR 3", s) 141 | } 142 | 143 | // RRSet 144 | s = PrettyRRSet([]dns.RR{rr1, rr2, rr3}, false) 145 | exp = "IN/AAAA 3600 ::1, IN/A 3600 127.0.0.1, IN/PTR 3600 f1.example.net." 146 | if s != exp { 147 | t.Error("PrettyRRSet 1", s) 148 | } 149 | 150 | // Addr 151 | rr, _ = dns.NewRR("oct.example.net IN A 127.0.0.1") 152 | s = PrettyAddr(rr, false) 153 | exp = "127.0.0.1" 154 | if s != exp { 155 | t.Error("PrettyAddr 1", s) 156 | } 157 | s = PrettyAddr(rr, true) 158 | exp = "oct.example.net/127.0.0.1" 159 | if s != exp { 160 | t.Error("PrettyAddr 1", s) 161 | } 162 | 163 | rr, _ = dns.NewRR("hex.example.net IN AAAA ::1") 164 | s = PrettyAddr(rr, false) 165 | exp = "::1" 166 | if s != exp { 167 | t.Error("PrettyAddr 2", s) 168 | } 169 | s = PrettyAddr(rr, true) 170 | exp = "hex.example.net/::1" 171 | if s != exp { 172 | t.Error("PrettyAddr 2", s) 173 | } 174 | 175 | rr, _ = dns.NewRR("hex.example.net IN MX 10 mailer.") 176 | s = PrettyAddr(rr, true) 177 | exp = "hex.example.net/?PrettyAddr?" 178 | if s != exp { 179 | t.Error("PrettyAddr 3", s) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /autoreverse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "net" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/markdingo/rrl" 11 | 12 | "github.com/markdingo/autoreverse/database" 13 | "github.com/markdingo/autoreverse/dnsutil" 14 | "github.com/markdingo/autoreverse/log" 15 | "github.com/markdingo/autoreverse/osutil" 16 | "github.com/markdingo/autoreverse/resolver" 17 | ) 18 | 19 | // The autoReverse container exists so that most of the "main" functionality can be 20 | // delegated to support functions and help keep the flow of main() nice and clean. 21 | type autoReverse struct { 22 | cfg *config 23 | 24 | done chan struct{} // All collaborative go-routines should monitor - see Done() 25 | forceReload chan struct{} // Tell watcher to forcefully reload 26 | sig chan os.Signal 27 | 28 | resolver resolver.Resolver 29 | dbGetter *database.Getter 30 | rrlHandler *rrl.RRL 31 | 32 | wg sync.WaitGroup // For all servers started 33 | servers []*server 34 | 35 | startTime time.Time 36 | statsTime time.Time // Last time stats were reset 37 | forward string // Canonical forward domain name 38 | forwardAuthority *authority // Could be either delegated or local 39 | 40 | delegatedReverses []*net.IPNet 41 | localReverses []*net.IPNet 42 | 43 | authorities // Contains all authorities, including forward 44 | } 45 | 46 | func newAutoReverse(cfg *config, r resolver.Resolver) *autoReverse { 47 | t := &autoReverse{ 48 | cfg: cfg, 49 | done: make(chan struct{}), 50 | forceReload: make(chan struct{}), 51 | sig: make(chan os.Signal), 52 | resolver: r, 53 | dbGetter: database.NewGetter(), 54 | } 55 | if t.cfg == nil { 56 | t.cfg = newConfig() 57 | } 58 | if t.resolver == nil { 59 | t.resolver = resolver.NewResolver() 60 | } 61 | 62 | return t 63 | } 64 | 65 | // Done is the go idiomatic way to tell collaborative go-routines to exit. All such 66 | // go-routines should include a "case <-autoreverse.Done(): return" in their select loop. 67 | func (t *autoReverse) Done() <-chan struct{} { 68 | return t.done 69 | } 70 | 71 | // Return true if added. Return false if duplicate. 72 | func (t *autoReverse) addAuthority(add *authority) bool { 73 | return t.authorities.append(add) 74 | } 75 | 76 | // Spin up the rrlHandler and return true if the config has meaningful rate limits set. If 77 | // the config is effectively a no-op, do not create the rrlHandler. 78 | func (t *autoReverse) activateRRL() bool { 79 | if t.cfg.rrlConfig.IsActive() { 80 | t.rrlHandler = rrl.NewRRL(t.cfg.rrlConfig) 81 | return true 82 | } 83 | 84 | return false 85 | } 86 | 87 | // Open Listen sockets and start servers. Does not return until all servers have started 88 | // or an error is detected. 89 | // 90 | // The server secrets for cookie generation are set here. Note that strictly the secret 91 | // should be configurable so that anycast DNS servers can all generate the same cookie, 92 | // but it's extremely unlikely that autoreverse will be used in that scenario, so for now, 93 | // we just use a cryptographically strong random value. 94 | func (t *autoReverse) startServers() { 95 | var cookieSecrets [2]uint64 96 | b := make([]byte, 16) // Effectively two uint64s 97 | rand.Read(b) // as needed by siphash-2-4 98 | for ix := 0; ix < 16; ix = ix + 2 { 99 | cookieSecrets[0] <<= 8 100 | cookieSecrets[1] <<= 8 101 | cookieSecrets[0] |= uint64(b[ix]) 102 | cookieSecrets[1] |= uint64(b[ix+1]) 103 | } 104 | 105 | for _, network := range []string{dnsutil.UDPNetwork, dnsutil.TCPNetwork} { 106 | for _, addr := range t.cfg.listen { 107 | srv := newServer(t.cfg, t.dbGetter, t.resolver, t.rrlHandler, network, addr) 108 | srv.cookieSecrets = cookieSecrets // All servers get the same secret 109 | err := t.startServer(srv) 110 | if err != nil { 111 | fatal(err) 112 | } else { 113 | t.servers = append(t.servers, srv) 114 | log.Major("Listen on: ", srv.network, " ", srv.address) 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Stop all servers and only return when they have all exited 121 | func (t *autoReverse) stopServers() { 122 | for _, srv := range t.servers { 123 | srv.stop() 124 | } 125 | t.wg.Wait() // Wait for them all to shutdown completely 126 | } 127 | 128 | // Constrain process via setuid, setgid and choot 129 | // 130 | // Security Note: Prior to go1.16.2 or thereabouts, osutil.Constrain() did not work 131 | // properly on Linux due to syscall.Setsid()/syscall.Setgid() not being correctly applied 132 | // to all threads in a process. 133 | func (t *autoReverse) Constrain() { 134 | if len(t.cfg.user) > 0 || len(t.cfg.group) > 0 || len(t.cfg.chroot) > 0 { 135 | err := osutil.Constrain(t.cfg.user, t.cfg.group, t.cfg.chroot) 136 | if err != nil { 137 | fatal(err) 138 | } 139 | log.Major("Process Constraint: ", osutil.ConstraintReport(t.cfg.chroot)) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /delegation/authority_test.go: -------------------------------------------------------------------------------- 1 | package delegation 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/log" 10 | "github.com/markdingo/autoreverse/mock" 11 | "github.com/markdingo/autoreverse/mock/resolver" 12 | ) 13 | 14 | func TestAuthorityLame(t *testing.T) { 15 | a := &Authority{} 16 | if !a.IsCompletelyLame() { 17 | t.Error("An empty Authority should be completely lame") 18 | } 19 | // Create not in-domain and in-domain name servers 20 | ns1, _ := dns.NewRR("autoreverse.example.net. IN NS ns1.autoreverse.example.net") 21 | ns2, _ := dns.NewRR("autoreverse.example.net. IN NS ns2.autoreverse.example.net") 22 | a.NS = append(a.NS, ns1) 23 | a.NS = append(a.NS, ns2) 24 | if !a.IsCompletelyLame() { 25 | t.Error("No addresses should still be lame") 26 | } 27 | // ns3 is not a name server 28 | a1, _ := dns.NewRR("ns3.autoreverse.example.net IN A 192.0.2.1") 29 | a.A = append(a.A, a1) 30 | if !a.IsCompletelyLame() { 31 | t.Error("No Address matches NS yet, should be lame still") 32 | } 33 | 34 | a1, _ = dns.NewRR("ns1.autoreverse.example.net IN A 192.0.2.253") // Match ns1 35 | a.A = append(a.A, a1) 36 | if a.IsCompletelyLame() { 37 | t.Error("A RR should mean no longer lame") 38 | } 39 | 40 | a.A = []dns.RR{} // Reset 41 | a2, _ := dns.NewRR("ns2.autoreverse.example.net IN AAAA 2001:db8:7::1") // Match ns2 42 | a.AAAA = append(a.AAAA, a2) 43 | if a.IsCompletelyLame() { 44 | t.Error("AAAA RR should mean no longer lame") 45 | } 46 | } 47 | 48 | func TestAuthorityPopulate(t *testing.T) { 49 | m := new(dns.Msg) 50 | a := &Authority{} 51 | a.populateFromDelegation(m) // Empty message should be a no-op 52 | 53 | if len(a.Source) > 0 || len(a.Domain) > 0 || len(a.SOA.Header().Name) > 0 || 54 | len(a.NS) > 0 || len(a.A) > 0 || len(a.AAAA) > 0 { 55 | t.Error("Authority changed with a noop msg", a, a.SOA.String()) 56 | } 57 | 58 | ns1, _ := dns.NewRR("autoreverse.example.net. IN NS ns1.autoreverse.example.net") 59 | ns2, _ := dns.NewRR("autoreverse.example.net. IN NS ns2.autoreverse.example.net") 60 | a1, _ := dns.NewRR("ns1.autoreverse.example.net IN A 192.0.2.1") 61 | a2, _ := dns.NewRR("ns2.autoreverse.example.net IN AAAA 2001:db8:7::1") 62 | a3, _ := dns.NewRR("ns3.autoreverse.example.net IN A 192.0.2.1") 63 | m.Ns = append(m.Ns, ns1) 64 | m.Ns = append(m.Ns, ns2) 65 | m.Extra = append(m.Extra, a2) 66 | m.Extra = append(m.Extra, a1) 67 | m.Extra = append(m.Extra, a3) 68 | 69 | a.populateFromDelegation(m) 70 | if len(a.Domain) == 0 { 71 | t.Error("populateFrom didn't set Domain", a) 72 | } 73 | 74 | if a.IsCompletelyLame() { 75 | t.Error("populateFrom didn't make the Authority non-lame", a) 76 | } 77 | } 78 | 79 | func TestNewRRs(t *testing.T) { 80 | m := new(dns.Msg) 81 | a := &Authority{Source: "TestNewRRs"} 82 | 83 | ns1 := newNS("autoreverse.example.net.", "ns1.autoreverse.example.net.") 84 | ns2 := newNS("autoreverse.example.net.", "ns2.autoreverse.example.net.") 85 | a1 := newA("ns1.autoreverse.example.net.", net.ParseIP("192.0.2.1")) 86 | a2 := newAAAA("ns2.autoreverse.example.net.", net.ParseIP("2001:db8:7::1")) 87 | a3 := newA("ns3.autoreverse.example.net.", net.ParseIP("192.0.2.2")) 88 | m.Ns = append(m.Ns, ns1) 89 | m.Ns = append(m.Ns, ns2) 90 | m.Extra = append(m.Extra, a1) 91 | m.Extra = append(m.Extra, a2) 92 | m.Extra = append(m.Extra, a3) 93 | 94 | a.populateFromDelegation(m) 95 | if len(a.Domain) == 0 { 96 | t.Error("populateFrom didn't set Domain", a) 97 | } 98 | 99 | if a.IsCompletelyLame() { 100 | t.Error("populateFrom with New* RRs, didn't make the Authority non-lame", a) 101 | } 102 | } 103 | 104 | func TestResolveMissing(t *testing.T) { 105 | out := &mock.IOWriter{} 106 | log.SetOut(out) 107 | log.SetLevel(log.MinorLevel) 108 | res := resolver.NewResolver("./testdata/authority") 109 | a := &Authority{Source: "TestResolveMissing", Domain: "autoreverse.example.net"} 110 | 111 | ns1 := newNS("autoreverse.example.net.", "ns1.autoreverse.example.net.") 112 | ns2 := newNS("autoreverse.example.net.", "ns2.autoreverse.example.net.") 113 | ns3 := newNS("autoreverse.example.net.", "ns3.example.org.") 114 | 115 | a.NS = append(a.NS, ns1) 116 | a.NS = append(a.NS, ns2) 117 | a.NS = append(a.NS, ns3) 118 | a.resolveMissingNSAddresses(res) 119 | if a.IsCompletelyLame() { 120 | got := out.String() 121 | t.Error("Name servers were not resolved", a, got) 122 | } 123 | 124 | if len(a.AAAA) != 1 || len(a.A) != 2 { 125 | t.Error("Not lame, but not right address count", len(a.AAAA), len(a.A)) 126 | } 127 | 128 | // Second time should be a no-op 129 | 130 | a.resolveMissingNSAddresses(res) 131 | if a.IsCompletelyLame() { 132 | got := out.String() 133 | t.Error("Name servers were not resolved", a, got) 134 | } 135 | 136 | if len(a.AAAA) != 1 || len(a.A) != 2 { 137 | t.Error("Not lame, but not right address count", len(a.AAAA), len(a.A)) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /delegation/find_test.go: -------------------------------------------------------------------------------- 1 | package delegation 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/markdingo/autoreverse/log" 10 | "github.com/markdingo/autoreverse/mock" 11 | "github.com/markdingo/autoreverse/mock/resolver" 12 | ) 13 | 14 | // A complete bogus domain that should elicit an error return 15 | func TestFindBogus(t *testing.T) { 16 | res := resolver.NewResolver("./testdata/find") // Mock resolver 17 | finder := NewFinder(res) 18 | pr := NewForwardProbe("autoreverse.doesnot.exist.") 19 | _, err := finder.FindAndProbe(pr) 20 | if err == nil { 21 | t.Error("Expected an error return with non-existent TLD") 22 | } 23 | } 24 | 25 | type tfapCase struct { 26 | reverse string 27 | name, parent, contains string 28 | target, success bool 29 | } 30 | 31 | // Run various probes thru finder.FindAndProbe and check all responses. The lookup data 32 | // for the mock resolver has been crafted to trigger each of these errors. The 33 | // FindAndProbe function is careful to generate a unique error message for each different 34 | // condition so there can be no ambiguity (tho we also use the go coverage tool to confirm 35 | // which conditions have been exercised). 36 | func TestFindAndProbe(t *testing.T) { 37 | testCases := []tfapCase{ 38 | {"", "autoreverse.example.net.", "example.net.", "", 39 | true, true}, 40 | {"", "autoreverse.a.b.c.example.net.", "example.net.", "", 41 | true, true}, // Label gaps 42 | {"", "noprobe.example.net.", "example.net.", "No Probe response", 43 | true, false}, 44 | {"", "wrongprobe.example.net.", "example.net.", "Wrong Probe response", 45 | true, false}, 46 | {"", "lame.example.net.", "example.net.", "100% lame", 47 | true, false}, 48 | {"", "noautoreverse.example.net.", "example.net.", "no delegation", 49 | false, false}, 50 | {"", "lameparents.example.org.", "example.org.", "not resolve parent", 51 | false, false}, 52 | {"", "nxdomain.example.net.", "example.net.", "NXDomain from parent", 53 | false, false}, 54 | {"", "odd.example.net.", "example.net.", "Odd", 55 | false, false}, 56 | {"", "baddelegation.example.net.", "example.net.", "Invalid Delegation", 57 | false, false}, 58 | {"", "reserr.example.net.", "example.net.", "Resolver error from parent", 59 | false, false}, 60 | {"", "wrongdelegation.example.net.", "example.net.", "Alert:Wrong delegation", 61 | false, false}, 62 | 63 | {"192.0.2.0/24", "2.0.192.in-addr.arpa.", "192.in-addr.arpa.", "", 64 | true, true}, 65 | {"2001:db8::/64", "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", 66 | "8.b.d.0.1.0.0.2.ip6.arpa.", "", true, true}, 67 | } 68 | 69 | for ix, tc := range testCases { 70 | t.Run(tc.name, func(t *testing.T) { 71 | testFindOne(t, ix, tc) 72 | }) 73 | } 74 | } 75 | 76 | func testFindOne(t *testing.T, ix int, tc tfapCase) { 77 | rand.Seed(0) 78 | out := &mock.IOWriter{} 79 | log.SetOut(out) 80 | log.SetLevel(log.DebugLevel) 81 | res := resolver.NewResolver("./testdata/find") // Mock resolver 82 | finder := NewFinder(res) 83 | 84 | var pr Probe 85 | if len(tc.reverse) > 0 { 86 | _, ipNet, err := net.ParseCIDR(tc.reverse) 87 | if err != nil { 88 | t.Fatal("Setup", err) 89 | } 90 | pr = NewReverseProbe("example.org.", ipNet) 91 | } else { 92 | pr = NewForwardProbe(tc.name) 93 | } 94 | R, err := finder.FindAndProbe(pr) 95 | if err != nil { 96 | t.Error(ix, tc.name, "Unexpected error from FindAndProbe:", err) 97 | t.Log(out.String()) 98 | return 99 | } 100 | 101 | if R.ProbeSuccess != tc.success { 102 | t.Error(ix, tc.name, "Probe success mismatch. Want", 103 | tc.success, "got", R.ProbeSuccess) 104 | t.Log(out.String()) 105 | out.Reset() 106 | } 107 | 108 | if len(tc.parent) > 0 { // Is parent expected? 109 | if R.Parent == nil { 110 | t.Error(ix, tc.name, "Expected parent", tc.parent) 111 | t.Log(out.String()) 112 | out.Reset() 113 | } else if tc.parent != R.Parent.Domain { 114 | t.Error(ix, tc.name, "Parent mismatch. Want", 115 | tc.parent, "got", R.Parent.Domain) 116 | t.Log(out.String()) 117 | out.Reset() 118 | } 119 | } else if R.Parent != nil { 120 | t.Error(ix, tc.name, "Did not expect a parent response", R.Parent.Domain) 121 | t.Log(out.String()) 122 | out.Reset() 123 | } 124 | 125 | if tc.target { 126 | if R.Target == nil { 127 | t.Error(ix, tc.name, "Expected Target return") 128 | t.Log(out.String()) 129 | out.Reset() 130 | } else if R.Target.Domain != tc.name { 131 | t.Error(ix, tc.name, "Target Domain mismatch. Want", 132 | tc.name, "got", R.Target.Domain) 133 | t.Log(out.String()) 134 | out.Reset() 135 | } 136 | } else { 137 | if R.Target != nil { 138 | t.Error(ix, tc.name, "Did not expect target", R.Target.Domain) 139 | } 140 | } 141 | 142 | if len(tc.contains) > 0 { 143 | got := out.String() 144 | if !strings.Contains(got, tc.contains) { 145 | t.Error(ix, tc.name, "Got error, but wrong one. Want", tc.contains, 146 | "got", got) 147 | out.Reset() 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /database/tree.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | ) 11 | 12 | // If RR is a.b.c. IN A 1.2.3.4, then the reference to the RR is: 13 | // 14 | // rrSet := database.cm[IN].children[c].children[b].children[a].tm[A] 15 | 16 | type classMap map[uint16]*node 17 | type typeMap map[uint16][]dns.RR 18 | 19 | type node struct { 20 | tm typeMap // Both of these maps are created on-demand so that the 21 | children labelMap // presence of a map implies at least one map entry. 22 | } 23 | 24 | type labelMap map[string]*node 25 | 26 | // Database is constructed with NewDatabase() - using a default construction will result 27 | // in a panic due to unconstructed maps. 28 | type Database struct { 29 | cm classMap 30 | count int // RRs added 31 | } 32 | 33 | // NewDatabase *must* be used to construct a new database 34 | func NewDatabase() *Database { 35 | return &Database{cm: make(classMap)} 36 | } 37 | 38 | // AddRR into the map. Return true if it was added. Return false it's a duplicate or an 39 | // impossible RR (which should never be the case). 40 | func (t *Database) AddRR(rr dns.RR) bool { 41 | qClass := rr.Header().Class 42 | qType := rr.Header().Rrtype 43 | qName := dnsutil.ChompCanonicalName(rr.Header().Name) 44 | labels := strings.Split(qName, ".") 45 | if len(labels) == 0 { 46 | return false 47 | } 48 | parent := t.cm[qClass] // Get or create root node for this class 49 | if parent == nil { 50 | parent = &node{} 51 | t.cm[qClass] = parent 52 | } 53 | 54 | for ix := len(labels) - 1; ix >= 0; ix-- { // Iterate down the labels 55 | if parent.children == nil { 56 | parent.children = make(labelMap) 57 | } 58 | child := parent.children[labels[ix]] 59 | if child == nil { 60 | child = &node{} 61 | parent.children[labels[ix]] = child 62 | } 63 | parent = child 64 | } 65 | 66 | // "parent" points to the bottom of the qName tree which is not necessarily the 67 | // bottom of the database tree. 68 | 69 | tm := parent.tm // Get or create the typeMap for this node 70 | if tm == nil { 71 | tm = make(typeMap) 72 | parent.tm = tm 73 | } 74 | 75 | rrset, ok := tm[qType] 76 | if !ok { // No RRs for this type so it's an easy add 77 | tm[qType] = []dns.RR{dns.Copy(rr)} 78 | t.count++ 79 | return true 80 | } 81 | 82 | // Compare existing RRs to avoid duplication 83 | 84 | for _, eRR := range rrset { 85 | if dnsutil.RRIsEqual(eRR, rr) { 86 | return false 87 | } 88 | } 89 | 90 | // dns.RR is effectively a pointer, so make a copy of the RR when placing it in 91 | // the database so callers cannot use their rr pointer to modify our database 92 | // copy. Just a bit of paranoia here. 93 | 94 | tm[qType] = append(rrset, dns.Copy(rr)) 95 | t.count++ 96 | 97 | return true 98 | } 99 | 100 | // LookupRR returns an array of copies of matching RRs. Copies are important as we know 101 | // caller are likely to modify the results, particularly TTL. nxDomain is true if there is 102 | // no node for the qName. Note a node is only every created when there is something to add 103 | // into it so the presence of a node implies RRs or children. 104 | func (t *Database) LookupRR(qClass, qType uint16, qName string) (ans []dns.RR, nxDomain bool) { 105 | nxDomain = true 106 | qName = dnsutil.ChompCanonicalName(qName) 107 | labels := strings.Split(qName, ".") 108 | if len(labels) == 0 { 109 | nxDomain = len(t.cm) > 0 // Should never occur in practice 110 | return 111 | } 112 | 113 | parent := t.cm[qClass] // Iterate from the root of the desired class 114 | if parent == nil { 115 | return 116 | } 117 | for ix := len(labels) - 1; ix >= 0; ix-- { 118 | if parent.children == nil { 119 | return 120 | } 121 | child := parent.children[labels[ix]] 122 | if child == nil { 123 | return 124 | } 125 | parent = child 126 | } 127 | 128 | // "parent" points to the bottom of the qName tree 129 | 130 | nxDomain = false // Because either tm entries or children will always be present 131 | 132 | tm := parent.tm // Look up type map for this node 133 | if tm == nil { 134 | return 135 | } 136 | 137 | rrset, ok := tm[qType] 138 | if !ok { 139 | return 140 | } 141 | 142 | for _, rr := range rrset { 143 | ans = append(ans, dns.Copy(rr)) 144 | } 145 | 146 | return 147 | } 148 | 149 | // Count returns the total count of all RRs in the database. 150 | func (t *Database) Count() int { 151 | return t.count 152 | } 153 | 154 | // Dump is a test/debug function only. 155 | func (t *Database) Dump() { 156 | fmt.Println("Database Dump", t.count) 157 | for ct, parent := range t.cm { 158 | t.dumpChildren(dns.ClassToString[ct]+" ", "", parent) 159 | } 160 | } 161 | 162 | func (t *Database) dumpChildren(prefix, qName string, parent *node) { 163 | thisPrefix := prefix 164 | nextPrefix := strings.Repeat(" ", len(thisPrefix)) 165 | for _, rrset := range parent.tm { 166 | fmt.Print(thisPrefix, " ", dnsutil.PrettyRRSet(rrset, true)) 167 | thisPrefix = nextPrefix 168 | fmt.Println() 169 | } 170 | for label, child := range parent.children { 171 | t.dumpChildren(prefix+" ", label+"."+qName, child) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | "time" 7 | 8 | "github.com/markdingo/rrl" 9 | "github.com/miekg/dns" 10 | 11 | "github.com/markdingo/autoreverse/dnsutil" 12 | "github.com/markdingo/autoreverse/log" 13 | "github.com/markdingo/autoreverse/resolver" 14 | ) 15 | 16 | const ( 17 | programName = "autoreverse" 18 | 19 | // Kinda subtle, but uppercase HTTPS implies BuildInfo was empty which in turn 20 | // implies a go1.16 compilation. That knowledge my be of some use to someone... 21 | defaultProjectURL = "HTTPS://github.com/markdingo/autoreverse" 22 | 23 | defaultService = "domain" 24 | defaultListen = ":" + defaultService 25 | 26 | reloadInterval = time.Minute * 10 // How often zone reloads are checked 27 | defaultReportInterval = time.Hour 28 | ) 29 | 30 | var ( 31 | defaultTTL = uint32(time.Hour.Seconds()) // One hour for synthetic PTRs 32 | ) 33 | 34 | type loadScheme int 35 | 36 | const ( 37 | fileScheme loadScheme = iota 38 | httpScheme 39 | axfrScheme 40 | ) 41 | 42 | // PTRZone manages the loading and reloading of PTR-deduce URLs. 43 | type PTRZone struct { 44 | resolver resolver.Resolver // Convenience copy of system-wide resolver 45 | url string // From command line option 46 | host, port, path, domain string // Extracted from url.Parse() 47 | scheme loadScheme 48 | 49 | soa dns.SOA // Results of parsing 50 | dtm time.Time // Last modified or last loaded 51 | loadTime time.Time 52 | lines, added, oob int 53 | } 54 | 55 | // rrlConfigStrings separates out the RRL options from all the rest for easy management 56 | // and identification. 57 | type rrlConfigStrings struct { 58 | window string // "--rrl-window" 59 | slipRatio string // "--rrl-slip-ratio" 60 | maxTableSize string // "--rrl-max-table-size" 61 | 62 | ipv4PrefixLength string // "--rrl-ipv4-CIDR" 63 | ipv6PrefixLength string // "--rrl-ipv6-CIDR" 64 | 65 | responsesInterval string // "--rrl-responses-psec" 66 | nodataInterval string // "--rrl-nodata-psec" 67 | nxdomainsInterval string // "--rrl-nxdomains-psec" 68 | referralsInterval string // "--rrl-referrals-psec" 69 | errorsInterval string // "--rrl-errors-psec" 70 | requestsInterval string // "--rrl-requests-psec" 71 | } 72 | 73 | // config defines the global configuration settings used by autoreverse. These setting 74 | // apply across the whole program and all servers. Once set it should never be changed as 75 | // it is shared amongst go-routines without an lock protections. 76 | type config struct { 77 | projectURL string 78 | passthru string // backend server to pass thru queries 79 | 80 | chaosFlag bool 81 | 82 | logMajorFlag bool // Major events and on-going information such as periodic stats 83 | logMinorFlag bool // Details associated with Major event 84 | logDebugFlag bool // Developer flag 85 | logQueriesFlag bool // Each DNS Query exchanged 86 | 87 | synthesizeFlag bool 88 | 89 | TTL time.Duration // TTLs for synthetic RRs 90 | TTLAsSecs uint32 // Converted and rounded from TTL 91 | maxAnswers int // Maximum number of PTRs to place in Answers response 92 | reportInterval time.Duration // Statistics reporting interval. Zero means never. 93 | 94 | nsid string // Respond to EDNS NSID request with this string 95 | nsidAsHex string // Encoding version 96 | nsidOpt dns.OPT // Ready to send version 97 | 98 | user, group, chroot string // Privilege constraints 99 | 100 | delegatedForward string // Forward zone to discover delegation 101 | localForward string // Forward zone with empty delegation 102 | delegatedReverse []string // Reverse CIDR to discover delegation 103 | localReverse []string // Local reverses with empty delegation 104 | 105 | PTRDeduceURLs []string // Load zones from these URLs 106 | 107 | listen []string // All addresses to listen on 108 | 109 | PTRZones []*PTRZone // Populated from PTRDeduceURLs 110 | 111 | rrlOptions rrlConfigStrings // Set by flags package 112 | rrlOptionSet bool // True if at least one rrl option was set 113 | rrlDryRun bool // "--rrl-dryrun" 114 | rrlConfig *rrl.Config // Populated if RRL is active 115 | } 116 | 117 | func newConfig() *config { 118 | t := &config{projectURL: defaultProjectURL} 119 | info, ok := debug.ReadBuildInfo() 120 | if ok { 121 | t.projectURL = info.Main.Path // Override with embedded if present 122 | } 123 | 124 | t.rrlConfig = rrl.NewConfig() // This default config is a no-op 125 | 126 | return t 127 | } 128 | 129 | func (t *config) generateNSIDOpt() { 130 | // Prepopulate our NSID opt 131 | t.nsidOpt.Hdr.Name = "." 132 | t.nsidOpt.Hdr.Rrtype = dns.TypeOPT 133 | t.nsidOpt.Hdr.Ttl = 0 // extended RCODE and flags 134 | t.nsidOpt.SetUDPSize(dnsutil.MaxUDPSize) 135 | e := new(dns.EDNS0_NSID) 136 | e.Code = dns.EDNS0NSID 137 | e.Nsid = t.nsidAsHex 138 | t.nsidOpt.Option = append(t.nsidOpt.Option, e) 139 | } 140 | 141 | func (t *config) printVersion() { 142 | fmt.Fprintf(log.Out(), "Program: %s %s (%s)\n", 143 | programName, Version, ReleaseDate) 144 | fmt.Fprintf(log.Out(), "Project: %s\n", t.projectURL) 145 | fmt.Fprintf(log.Out(), "Inspiration: %s\n", 146 | "https://datatracker.ietf.org/doc/html/rfc8501#section-2.5") 147 | } 148 | -------------------------------------------------------------------------------- /delegation/authority.go: -------------------------------------------------------------------------------- 1 | package delegation 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/miekg/dns" 8 | 9 | "github.com/markdingo/autoreverse/dnsutil" 10 | "github.com/markdingo/autoreverse/log" 11 | "github.com/markdingo/autoreverse/resolver" 12 | ) 13 | 14 | // Authority contains the delegated and synthetic SOA details so our auth server can 15 | // respond to SOA DNS requests. An Authority is considered valid if there is at least one 16 | // resolved address for the name servers. If some name servers happen to be "lame", that 17 | // doesn't invalidate the authority - tho of course it reduces their availability and 18 | // effectiveness. 19 | type Authority struct { 20 | Source string // Printable and possibly unique identifier 21 | Domain string // AKA Zone of authority - matched by DNS serving 22 | SOA dns.SOA 23 | NS []dns.RR 24 | AAAA []dns.RR 25 | A []dns.RR 26 | } 27 | 28 | // Transfer the delegation material from the parent name server response into the 29 | // Authority; this includes the domain name extracted from the qName of the NS entry. 30 | // This is important to note as it may well be a different domain from that targeted, if, 31 | // e.g., the parent has given a different answer than what we were expecting. It's the 32 | // responsibility of the caller to make a final check that this domain name suits their 33 | // needs. Having said that, typically the Domain should match in the forward case or is 34 | // reasonable in the reverse case. 35 | // 36 | // Another important note is that the message should be a delegation message which means 37 | // that the NS addresses are in Extra, not Answer. 38 | func (t *Authority) populateFromDelegation(m *dns.Msg) { 39 | t.NS = m.Ns 40 | 41 | // If we have at least one NS RR, make its qName the authority domain 42 | if len(t.NS) > 0 { 43 | if ns, ok := t.NS[0].(*dns.NS); ok { 44 | t.Domain = ns.Hdr.Name 45 | } 46 | } 47 | 48 | // Copy relevant glue 49 | for _, addr := range m.Extra { 50 | if AAAA, ok := addr.(*dns.AAAA); ok { 51 | t.AAAA = append(t.AAAA, AAAA) 52 | } 53 | if A, ok := addr.(*dns.A); ok { 54 | t.A = append(t.A, A) 55 | } 56 | } 57 | } 58 | 59 | // Find addresses of all name servers which do not already have at least one address in 60 | // the Authority. This is typically non-glue names tho it can also occur for in-domain 61 | // names which happen to be CNAMEs! In any event, rather than try and discriminate between 62 | // names which should have come back as glue, we simply query for all outstanding names 63 | // and let a real resolver work it out for us. 64 | func (t *Authority) resolveMissingNSAddresses(res resolver.Resolver) { 65 | aMap := make(map[string]struct{}) // Track already-resolved names 66 | for _, rr := range t.AAAA { 67 | aMap[rr.Header().Name] = struct{}{} 68 | } 69 | for _, rr := range t.A { 70 | aMap[rr.Header().Name] = struct{}{} 71 | } 72 | 73 | for _, rr := range t.NS { 74 | if rrt, ok := rr.(*dns.NS); ok { 75 | name := rrt.Ns 76 | if _, ok := aMap[name]; ok { // If already resolved, skip 77 | continue 78 | } 79 | addrs, err := res.LookupIPAddr(context.Background(), name) 80 | if err != nil { 81 | log.Minorf("Cannot resolve NS address of %s for %s:%s", 82 | name, t.Domain, dnsutil.ShortenLookupError(err).Error()) 83 | continue 84 | } 85 | for _, ip := range addrs { 86 | if ip.To4() != nil { 87 | t.A = append(t.A, newA(name, ip)) 88 | } else { 89 | t.AAAA = append(t.AAAA, newAAAA(name, ip)) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | // IsCompletelyLame returns true of none of the name servers have any address records. It 97 | // is also considered completely lame if there are no name servers to begin with! 98 | func (t *Authority) IsCompletelyLame() bool { 99 | if len(t.NS) == 0 { 100 | return true 101 | } 102 | 103 | nMap := make(map[string]struct{}) // Linear search is probably ok too 104 | for _, rr := range t.NS { 105 | if rrt, ok := rr.(*dns.NS); ok { 106 | nMap[rrt.Ns] = struct{}{} 107 | } 108 | } 109 | 110 | for _, rr := range t.AAAA { 111 | if _, ok := nMap[rr.Header().Name]; ok { 112 | return false 113 | } 114 | } 115 | 116 | for _, rr := range t.A { 117 | if _, ok := nMap[rr.Header().Name]; ok { 118 | return false 119 | } 120 | } 121 | 122 | return true 123 | } 124 | 125 | // newNS converts LookupNS results into dns.RRs 126 | func newNS(qName, nsName string) *dns.NS { 127 | rr := new(dns.NS) 128 | rr.Hdr.Name = qName 129 | rr.Hdr.Rrtype = dns.TypeNS 130 | rr.Hdr.Class = dns.ClassINET 131 | rr.Hdr.Ttl = 59 // Should be populated by caller, but this is a sentinal safety net 132 | rr.Ns = nsName 133 | 134 | return rr 135 | } 136 | 137 | // newAAAA converts LookupIPAddr results into dns.RRs 138 | func newAAAA(qName string, ip net.IP) *dns.AAAA { 139 | rr := new(dns.AAAA) 140 | rr.Hdr.Name = qName 141 | rr.Hdr.Rrtype = dns.TypeAAAA 142 | rr.Hdr.Class = dns.ClassINET 143 | rr.Hdr.Ttl = 59 // Should be populated by caller, but this is a sentinal safety net 144 | rr.AAAA = ip 145 | 146 | return rr 147 | } 148 | 149 | // newA converts LookupIPAddr results into dns.RRs 150 | func newA(qName string, ip net.IP) *dns.A { 151 | rr := new(dns.A) 152 | rr.Hdr.Name = qName 153 | rr.Hdr.Rrtype = dns.TypeA 154 | rr.Hdr.Class = dns.ClassINET 155 | rr.Hdr.Ttl = 59 // Should be populated by caller, but this is a sentinal safety net 156 | rr.A = ip 157 | 158 | return rr 159 | } 160 | -------------------------------------------------------------------------------- /discover.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/markdingo/autoreverse/delegation" 8 | "github.com/markdingo/autoreverse/dnsutil" 9 | "github.com/markdingo/autoreverse/log" 10 | ) 11 | 12 | // Ask the Delegation Finder to discover the forward and reverse zones by probing. 13 | func (t *autoReverse) discover() error { 14 | finder := delegation.NewFinder(t.resolver) 15 | 16 | if len(t.cfg.delegatedForward) > 0 { // Only discover delegate forwards, not locals 17 | err := t.discoverForward(finder, t.cfg.delegatedForward) 18 | if err != nil { 19 | return err 20 | } 21 | } 22 | 23 | err := t.discoverAllReverses(finder) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // discoverForward searches the forward DNS for the authoritative parents of our domain. 32 | // 33 | // Start with the forward discovery as it's highly likely that the reverse discovery will 34 | // refer to the forward and be queried from resolvers by the probe process. The end result 35 | // of a successful forward probe is the addition of an Authority to t.authorities which 36 | // will be added into the server mutables as part of the reverse probe. 37 | func (t *autoReverse) discoverForward(finder *delegation.Finder, domain string) error { 38 | pr := delegation.NewForwardProbe(domain) 39 | for _, srv := range t.servers { 40 | srv.setMutables(t.forward, pr, t.authorities) 41 | } 42 | q := pr.Question() 43 | log.Major("Forward: Find ", domain, " with ", dnsutil.PrettyQuestion(q)) 44 | 45 | fr, err := finder.FindAndProbe(pr) 46 | if err != nil { 47 | return fmt.Errorf("Forward:%w", err) 48 | } 49 | 50 | log.Minor("Forward: Parent found: ", fr.Parent.Domain, 51 | " Name Servers: ", dnsutil.PrettyShortNSSet(fr.Parent.NS)) 52 | 53 | if fr.Target == nil { 54 | return fmt.Errorf("Forward:Parent %s has no delegation for %s", 55 | fr.Parent.Domain, domain) 56 | } 57 | 58 | log.Minor("Forward: Target found: ", fr.Target.Domain, 59 | " Name Servers: ", dnsutil.PrettyShortNSSet(fr.Target.NS)) 60 | 61 | if !fr.ProbeSuccess { 62 | return fmt.Errorf("Forward: Probe failed to self-identify %s", fr.Target.Domain) 63 | } 64 | 65 | auth := newAuthority(fr.Target, true) 66 | auth.Source = domain 67 | auth.synthesizeSOA(fr.Parent.Domain, t.cfg.TTLAsSecs) 68 | logAuth(auth, "Forward") 69 | 70 | t.forwardAuthority = auth 71 | t.addAuthority(auth) 72 | 73 | return nil 74 | } 75 | 76 | // discoverAllReverses searches the reverse DNS for the authoritative parents of all our 77 | // reverse domains. 78 | func (t *autoReverse) discoverAllReverses(finder *delegation.Finder) error { 79 | // Set the reverse probe in the mutables so the dns server responds and also set 80 | // the current authorities to *only* the forward zone. We don't want earlier 81 | // Authorities from reverse discoveries to perturb later discoveries so reverses 82 | // are never added to mutables while in discovery mode - they are all added by 83 | // Run() post-discovery. 84 | var fwdOnly authorities 85 | if t.forwardAuthority != nil { // This must always be true I think 86 | fwdOnly.append(t.forwardAuthority) 87 | } 88 | for _, srv := range t.servers { 89 | srv.setMutables(t.forward, nil, fwdOnly) 90 | } 91 | 92 | for _, ipNet := range t.delegatedReverses { 93 | err := t.discoverReverse(finder, t.forward, ipNet) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // Discover one reverse zone by walking then probing. 103 | func (t *autoReverse) discoverReverse(finder *delegation.Finder, forward string, ipNet *net.IPNet) error { 104 | pr := delegation.NewReverseProbe(forward, ipNet) // Create the Probe 105 | for _, srv := range t.servers { 106 | mutables := srv.getMutables() // Replace or set probe in server mutables 107 | srv.setMutables(mutables.ptrSuffix, pr, mutables.authorities) 108 | } 109 | q := pr.Question() 110 | domain := ipNet.String() 111 | log.Major("Reverse: Find ", domain, " with ", dnsutil.PrettyQuestion(q)) 112 | 113 | fr, err := finder.FindAndProbe(pr) 114 | if err != nil { 115 | return fmt.Errorf("Reverse:%w", err) 116 | } 117 | 118 | log.Minor("Reverse: Parent found: ", fr.Parent.Domain, 119 | " Name Servers: ", dnsutil.PrettyShortNSSet(fr.Parent.NS)) 120 | 121 | if fr.Target == nil { 122 | return fmt.Errorf("Reverse:Parent %s has no delegation for %s", 123 | fr.Parent.Domain, domain) 124 | } 125 | 126 | log.Minor("Reverse: Target found: ", fr.Target.Domain, 127 | " Name Servers: ", dnsutil.PrettyShortNSSet(fr.Target.NS)) 128 | 129 | if !fr.ProbeSuccess { 130 | return fmt.Errorf("Reverse: Probe failed to self-identify %s", fr.Target.Domain) 131 | } 132 | 133 | auth := newAuthority(fr.Target, false) 134 | auth.cidr = ipNet 135 | auth.Source = ipNet.String() 136 | auth.synthesizeSOA(forward, t.cfg.TTLAsSecs) 137 | 138 | if !t.addAuthority(auth) { 139 | return fmt.Errorf("-reverse %s is duplicated", auth.Domain) 140 | } 141 | 142 | logAuth(auth, "Reverse") 143 | 144 | return nil 145 | } 146 | 147 | // Print auth details to log - should be called after SOA synthetic 148 | func logAuth(auth *authority, name string) { 149 | log.Major(name, " Zone of Authority ", auth.Domain) 150 | log.Minor(dnsutil.PrettySOA(&auth.SOA, false)) 151 | log.Minor(dnsutil.PrettyRRSet(auth.NS, false)) 152 | if len(auth.A) > 0 { 153 | log.Minor(dnsutil.PrettyRRSet(auth.A, true)) 154 | } 155 | if len(auth.AAAA) > 0 { 156 | log.Minor(dnsutil.PrettyRRSet(auth.AAAA, true)) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type logLevel int 11 | 12 | // Define the valid logLevels which can be set with SetLevel() 13 | const ( 14 | SilentLevel logLevel = iota 15 | MajorLevel 16 | MinorLevel 17 | DebugLevel 18 | ) 19 | 20 | var ( 21 | majorPrefix = "" // Prepended to each output. These values may be configurable 22 | minorPrefix = " " // at some point in the future, but there is no current need 23 | debugPrefix = " Dbg:" // for that yet. 24 | 25 | out io.Writer 26 | level logLevel 27 | ) 28 | 29 | func init() { 30 | out = os.Stdout 31 | } 32 | 33 | func (t logLevel) String() string { 34 | switch t { 35 | case MajorLevel: 36 | return "Major" 37 | case MinorLevel: 38 | return "Minor" 39 | case DebugLevel: 40 | return "Debug" 41 | } 42 | 43 | return "Silent" 44 | } 45 | 46 | // SetOut changes the output of logging to the supplied io.Writer. The default is 47 | // os.Stdout. The supplied io.Writer must never be nil. 48 | func SetOut(w io.Writer) { 49 | if w == nil { 50 | panic("log.SetOut() called with a nil io.Writer") 51 | } 52 | out = w 53 | } 54 | 55 | // Out returns the current io.Writer for specialist logger functions which are not 56 | // controlled by log levels. The return value will never be nil. 57 | func Out() io.Writer { 58 | return out 59 | } 60 | 61 | // SetLevel sets the current logging level. Ignored if previously set by ENV variable. 62 | func SetLevel(l logLevel) { 63 | level = l 64 | } 65 | 66 | // Level returns current level 67 | func Level() logLevel { 68 | return level 69 | } 70 | 71 | // IfMajor returns true if Major logging is enabled. Applications have access to these If* 72 | // functions in cases where evaluation of the log arguments is expensive and the caller 73 | // wishes to minimize that cost. 74 | func IfMajor() bool { 75 | return level >= MajorLevel 76 | } 77 | 78 | // IfMinor returns true if Minor or Major logging is enabled. 79 | func IfMinor() bool { 80 | return level >= MinorLevel 81 | } 82 | 83 | // IfDebug returns true if Debug, Minor or Major logging is enabled. 84 | func IfDebug() bool { 85 | return level >= DebugLevel 86 | } 87 | 88 | // Majorf provides an approximate fmt.Printf equivalent interface to logging. Output is 89 | // only generated if the level is >= Major. A newline is always added to the end of the 90 | // output so the caller should not have that in there string. All output is prefixed with 91 | // the current major prefix which may be an empty string. 92 | func Majorf(format string, a ...interface{}) (n int, err error) { 93 | if level >= MajorLevel { 94 | s := fmt.Sprintf(format, a...) 95 | return prefixAndPrintLines(s, majorPrefix) 96 | } 97 | 98 | return 0, nil 99 | } 100 | 101 | // Major provides a fmt.Print like interface to logging. Output is only generated if the 102 | // level is >= Major. Major uses fmt.Sprint to generate the output line thus it inherits 103 | // the feature whereby spaces are added between operands when neither is a string. 104 | func Major(a ...interface{}) (n int, err error) { 105 | if level >= MajorLevel { 106 | s := fmt.Sprint(a...) 107 | return prefixAndPrintLines(s, majorPrefix) 108 | } 109 | 110 | return 0, nil 111 | } 112 | 113 | // Minorf provides a fmt.Printf equivalent interface to logging. Output is only generated 114 | // if the level is >= Minor. 115 | func Minorf(format string, a ...interface{}) (n int, err error) { 116 | if level >= MinorLevel { 117 | s := fmt.Sprintf(format, a...) 118 | return prefixAndPrintLines(s, minorPrefix) 119 | } 120 | 121 | return 0, nil 122 | } 123 | 124 | // Minor provides a fmt.Print like interface to logging. Output is only generated if the 125 | // level is >= Minor. Minor uses fmt.Sprint to generate the output line thus it inherits 126 | // the feature whereby spaces are added between operands when neither is a string. 127 | func Minor(a ...interface{}) (n int, err error) { 128 | if level >= MinorLevel { 129 | s := fmt.Sprint(a...) 130 | return prefixAndPrintLines(s, minorPrefix) 131 | } 132 | 133 | return 0, nil 134 | } 135 | 136 | // Debugf provides a fmt.Printf equivalent interface to logging. Output is only generated 137 | // if the level is >= Debug. 138 | func Debugf(format string, a ...interface{}) (n int, err error) { 139 | if level >= DebugLevel { 140 | s := fmt.Sprintf(format, a...) 141 | return prefixAndPrintLines(s, debugPrefix) 142 | } 143 | 144 | return 0, nil 145 | } 146 | 147 | // Debug provides a fmt.Print like interface to logging. Output is only generated if the 148 | // level is >= Debug. Debug uses fmt.Sprint to generate the output line thus it inherits 149 | // the feature whereby spaces are added between operands when neither is a string. 150 | func Debug(a ...interface{}) (n int, err error) { 151 | if level >= DebugLevel { 152 | s := fmt.Sprint(a...) 153 | return prefixAndPrintLines(s, debugPrefix) 154 | } 155 | 156 | return 0, nil 157 | } 158 | 159 | // prefixAndPrintLines is the common handler which takes potentially multiple lines and 160 | // sends them to the out stream prefixed with the supplied prefix. 161 | func prefixAndPrintLines(lines, prefix string) (int, error) { 162 | if strings.Index(lines, "\n") == 0 { // Expect this to be the common case 163 | return fmt.Fprint(out, prefix, lines, "\n") 164 | } 165 | 166 | ar := strings.Split(lines, "\n") 167 | 168 | for len(ar) > 0 && len(ar[len(ar)-1]) == 0 { // Chomp trailing empty lines 169 | ar = ar[:len(ar)-1] 170 | } 171 | 172 | s := strings.Join(ar, "\n"+prefix) // Line1 \nprefix Line2 \nprefix Line3 173 | 174 | return fmt.Fprint(out, prefix, s, "\n") 175 | } 176 | --------------------------------------------------------------------------------