diff --git a/ChangeLog b/ChangeLog index 16edc1a2..83934ad6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,10 @@ * `generate-domains-blacklist.py` has been renamed to `generate-domains-blocklist.py`, and the configuration files have been renamed as well. + - `dnscrypt-proxy -resolve` has been completely revamped, and now requires +the configuration file to be accessible. It will send a query to an IP address +of the `dnscrypt-proxy` server by default. Sending queries to arbitrary +servers is also supported with the `-resolve name,address` syntax. - Server lists can't be older than a week any more, even if directory permissions are incorrect and cache files cannot be written. - macOS/arm64 is now officially supported. diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index 2ef3beb8..06b7be39 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -277,6 +277,7 @@ type CaptivePortalsConfig struct { } type ConfigFlags struct { + Resolve *string List *bool ListAll *bool JSONOutput *bool @@ -314,6 +315,16 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { if err != nil { return err } + + if flags.Resolve != nil && len(*flags.Resolve) > 0 { + addr := "127.0.0.1:53" + if len(config.ListenAddresses) > 0 { + addr = config.ListenAddresses[0] + } + Resolve(addr, *flags.Resolve, len(config.ServerNames) == 1) + os.Exit(0) + } + if err := cdFileDir(foundConfigFile); err != nil { return err } diff --git a/dnscrypt-proxy/main.go b/dnscrypt-proxy/main.go index 8d1e2b5a..42ec964a 100644 --- a/dnscrypt-proxy/main.go +++ b/dnscrypt-proxy/main.go @@ -41,8 +41,8 @@ func main() { svcFlag := flag.String("service", "", fmt.Sprintf("Control the system service: %q", service.ControlAction)) version := flag.Bool("version", false, "print current proxy version") - resolve := flag.String("resolve", "", "resolve a name using system libraries") flags := ConfigFlags{} + flags.Resolve = flag.String("resolve", "", "resolve a DNS name (string can be or ,)") flags.List = flag.Bool("list", false, "print the list of available resolvers for the enabled filters") flags.ListAll = flag.Bool("list-all", false, "print the complete list of available resolvers, ignoring filters") flags.JSONOutput = flag.Bool("json", false, "output list as JSON") @@ -58,10 +58,6 @@ func main() { fmt.Println(AppVersion) os.Exit(0) } - if resolve != nil && len(*resolve) > 0 { - Resolve(*resolve) - os.Exit(0) - } app := &App{ flags: &flags, diff --git a/dnscrypt-proxy/resolve.go b/dnscrypt-proxy/resolve.go index 7ed495bf..4ba0e985 100644 --- a/dnscrypt-proxy/resolve.go +++ b/dnscrypt-proxy/resolve.go @@ -1,58 +1,336 @@ package main import ( + "errors" "fmt" "net" + "os" "strings" + "time" + + "github.com/miekg/dns" ) -const myResolverHost string = "resolver.dnscrypt.info" +const myResolverHost string = "resolver.dnscrypt.info." +const nonexistentName string = "nonexistent-zone.dnscrypt-test." -func Resolve(name string) { - fmt.Printf("Resolving [%s]\n\n", name) - - fmt.Printf("Canonical name: ") - cname, err := net.LookupCNAME(name) - if err != nil { - fmt.Println("-") - } else { - fmt.Println(cname) +func resolveQuery(server string, qName string, qType uint16) (*dns.Msg, error) { + client := new(dns.Client) + client.ReadTimeout = 10 * time.Second + msg := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionDesired: true, + Opcode: dns.OpcodeQuery, + }, + Question: make([]dns.Question, 1), } - - fmt.Printf("IP addresses: ") - addrs, err := net.LookupHost(name) - if err != nil { - fmt.Println("-") - } else { - fmt.Println(strings.Join(addrs, ", ")) + options := &dns.OPT{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: dns.TypeOPT, + }, } - - fmt.Printf("TXT records: ") - txt, err := net.LookupTXT(name) - if err != nil { - fmt.Println("-") - } else { - fmt.Println(strings.Join(txt, " ")) - } - - mxs, _ := net.LookupMX(name) - if len(mxs) > 0 { - fmt.Printf("Mail servers: %d mail servers found\n", len(mxs)) - } - - ns, _ := net.LookupNS(name) - if len(ns) > 0 { - fmt.Printf("Name servers: %d name servers found\n", len(ns)) - } - - resIP, err := net.LookupHost(myResolverHost) - if err == nil && len(resIP) > 0 { - fmt.Printf("Resolver IP: %s", resIP[0]) - rev, err := net.LookupAddr(resIP[0]) - if err == nil && len(rev) > 0 { - fmt.Printf(" (%s)", rev[0]) + msg.Extra = append(msg.Extra, options) + options.SetDo() + options.SetUDPSize(uint16(MaxDNSPacketSize)) + msg.Question[0] = dns.Question{Name: qName, Qtype: qType, Qclass: dns.ClassINET} + msg.Id = dns.Id() + for i := 0; i < 2; i++ { + response, rtt, err := client.Exchange(msg, server) + if neterr, ok := err.(net.Error); ok && neterr.Timeout() { + continue } - fmt.Println("") + _ = rtt + if err != nil { + return nil, err + } + return response, nil + } + return nil, errors.New("Timeout") +} + +func Resolve(server string, name string, singleResolver bool) { + parts := strings.SplitN(name, ",", 2) + if len(parts) == 2 { + name, server = parts[0], parts[1] + singleResolver = true + } + + host, port := ExtractHostAndPort(server, 53) + if host == "0.0.0.0" { + host = "127.0.0.1" + } else if host == "[::]" { + host = "[::1]" + } + server = fmt.Sprintf("%s:%d", host, port) + + fmt.Printf("Resolving [%s] using %s port %d\n\n", name, host, port) + name = dns.Fqdn(name) + + cname := name + + for { + response, err := resolveQuery(server, myResolverHost, dns.TypeA) + if err != nil { + fmt.Printf("Unable to resolve: [%s]\n", err) + os.Exit(1) + } + fmt.Printf("Resolver : ") + res := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Class != dns.ClassINET { + continue + } + var ip string + if answer.Header().Rrtype == dns.TypeA { + ip = answer.(*dns.A).A.String() + } else if answer.Header().Rrtype == dns.TypeAAAA { + ip = answer.(*dns.AAAA).AAAA.String() + } + if rev, err := dns.ReverseAddr(ip); err == nil { + response, err = resolveQuery(server, rev, dns.TypePTR) + if err != nil { + break + } + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypePTR || answer.Header().Class != dns.ClassINET { + continue + } + ip = ip + " (" + answer.(*dns.PTR).Ptr + ")" + break + } + } + res = append(res, ip) + } + if len(res) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(res, ", ")) + } + break + } + + if singleResolver { + for { + fmt.Printf("Lying : ") + response, err := resolveQuery(server, nonexistentName, dns.TypeA) + if err != nil { + break + } + if response.Rcode == dns.RcodeSuccess { + fmt.Println("yes. That resolver returns wrong responses") + } else if response.Rcode == dns.RcodeNameError { + fmt.Println("no") + } else { + fmt.Printf("unknown - query returned %s\n", dns.RcodeToString[response.Rcode]) + } + + if response.Rcode == dns.RcodeNameError { + fmt.Printf("DNSSEC : ") + if response.AuthenticatedData { + fmt.Println("yes, the resolver supports DNSSEC") + } else { + fmt.Println("no, the resolver doesn't support DNSSEC") + fmt.Println(response) + } + } + break + } + } else { + fmt.Println("Multiple resolvers have been configured; this is just one one of them.") + } + + fmt.Println("") + +cname: + for { + fmt.Printf("Canonical name: ") + for i := 0; i < 100; i++ { + response, err := resolveQuery(server, cname, dns.TypeCNAME) + if err != nil { + break cname + } + found := false + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeCNAME || answer.Header().Class != dns.ClassINET { + continue + } + cname = answer.(*dns.CNAME).Target + found = true + break + } + if !found { + break + } + } + fmt.Println(cname) + break + } + + fmt.Println("") + + for { + fmt.Printf("IPv4 addresses: ") + response, err := resolveQuery(server, cname, dns.TypeA) + if err != nil { + break + } + ipv4 := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeA || answer.Header().Class != dns.ClassINET { + continue + } + ipv4 = append(ipv4, answer.(*dns.A).A.String()) + } + if len(ipv4) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(ipv4, ", ")) + } + break + } + + for { + fmt.Printf("IPv6 addresses: ") + response, err := resolveQuery(server, cname, dns.TypeAAAA) + if err != nil { + break + } + ipv6 := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeAAAA || answer.Header().Class != dns.ClassINET { + continue + } + ipv6 = append(ipv6, answer.(*dns.AAAA).AAAA.String()) + } + if len(ipv6) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(ipv6, ", ")) + } + break + } + + fmt.Println("") + + for { + fmt.Printf("Name servers : ") + response, err := resolveQuery(server, cname, dns.TypeNS) + if err != nil { + break + } + nss := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeNS || answer.Header().Class != dns.ClassINET { + continue + } + nss = append(nss, answer.(*dns.NS).Ns) + } + if len(nss) == 0 { + fmt.Println("No name servers found") + } else { + fmt.Println(strings.Join(nss, ", ")) + } + + fmt.Printf("DNSSEC signed : ") + if response.AuthenticatedData { + fmt.Println("yes") + } else { + fmt.Println("no") + } + break + } + + for { + fmt.Printf("Mail servers : ") + response, err := resolveQuery(server, cname, dns.TypeMX) + if err != nil { + break + } + mxs := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeMX || answer.Header().Class != dns.ClassINET { + continue + } + mxs = append(mxs, answer.(*dns.MX).Mx) + } + if len(mxs) == 0 { + fmt.Println("no mail servers found") + } else if len(mxs) > 1 { + fmt.Printf("%d mail servers found\n", len(mxs)) + } else { + fmt.Println("1 mail servers found") + } + break + } + + fmt.Println("") + + for { + fmt.Printf("HTTPS alias : ") + response, err := resolveQuery(server, cname, dns.TypeHTTPS) + if err != nil { + break + } + aliases := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeHTTPS || answer.Header().Class != dns.ClassINET { + continue + } + https := answer.(*dns.HTTPS) + if https.Priority != 0 || len(https.Target) < 2 { + continue + } + aliases = append(aliases, https.Target) + } + if len(aliases) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(aliases, ", ")) + } + + fmt.Printf("HTTPS info : ") + info := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeHTTPS || answer.Header().Class != dns.ClassINET { + continue + } + https := answer.(*dns.HTTPS) + if https.Priority == 0 || len(https.Target) > 1 { + continue + } + for _, value := range https.Value { + info = append(info, fmt.Sprintf("[%s]=[%s]", value.Key(), value.String())) + } + } + if len(info) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(info, ", ")) + } + break + } + + fmt.Println("") + + for { + fmt.Printf("TXT records : ") + response, err := resolveQuery(server, cname, dns.TypeTXT) + if err != nil { + break + } + txt := make([]string, 0) + for _, answer := range response.Answer { + if answer.Header().Rrtype != dns.TypeTXT || answer.Header().Class != dns.ClassINET { + continue + } + txt = append(txt, strings.Join(answer.(*dns.TXT).Txt, " ")) + } + if len(txt) == 0 { + fmt.Println("-") + } else { + fmt.Println(strings.Join(txt, ", ")) + } + break } - fmt.Println("") }