From 63ea7215f725c05bc77400b2aa70c833cfd926aa Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Mon, 17 Dec 2018 01:55:58 +0300 Subject: [PATCH] Init --- .codecov.yml | 8 + .gitignore | 2 + .travis.yml | 12 ++ LICENSE | 25 +++ README.md | 33 +++ dnscrypt.go | 547 +++++++++++++++++++++++++++++++++++++++++++++++ dnscrypt_test.go | 128 +++++++++++ go.mod | 13 ++ go.sum | 18 ++ 9 files changed, 786 insertions(+) create mode 100644 .codecov.yml create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dnscrypt.go create mode 100644 dnscrypt_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..f91e5c1 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: 40% + threshold: null + patch: false + changes: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..706fd07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.vscode diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e0e28e6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go +sudo: false + +go: +- 1.11.x +- 1.x + +script: +- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./... + +after_success: +- bash <(curl -s https://codecov.io/bash) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..471f09f --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a48709 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +[![Build Status](https://travis-ci.org/ameshkov/dnscrypt.svg?branch=master)](https://travis-ci.org/ameshkov/dnscrypt) +[![Code Coverage](https://img.shields.io/codecov/c/github/ameshkov/dnscrypt/master.svg)](https://codecov.io/github/ameshkov/dnscrypt?branch=master) + +# DNSCrypt Client + +This is a very simple [DNSCrypt](https://dnscrypt.info/) client library written in Go. + +The idea is to let others use DNSCrypt resolvers in the same manner as we can use regular and DoT resolvers with [miekg's DNS library](https://github.com/miekg/dns). +Unfortunately, I have not found an easy way to use [dnscrypt-proxy](https://github.com/jedisct1/dnscrypt-proxy) as a dependency so here's why this library was created. + +### Usage + +``` + // AdGuard DNS stamp + stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" + + // Initializing the DNSCrypt client + c := dnscrypt.Client{Proto: "udp", Timeout: 10 * time.Second} + + // Fetching and validating the server certificate + serverInfo, rtt, err := client.Dial(stampStr) + + // Create a DNS request + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, + } + + // Get the DNS response + reply, rtt, err := client.Exchange(&req, serverInfo) +``` \ No newline at end of file diff --git a/dnscrypt.go b/dnscrypt.go new file mode 100644 index 0000000..0981120 --- /dev/null +++ b/dnscrypt.go @@ -0,0 +1,547 @@ +package dnscrypt + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "log" + "math/rand" + "net" + "strings" + "time" + + "github.com/jedisct1/go-dnsstamps" + "github.com/jedisct1/xsecretbox" + "github.com/miekg/dns" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/secretbox" +) + +type CryptoConstruction uint16 + +const ( + UndefinedConstruction CryptoConstruction = iota + XSalsa20Poly1305 + XChacha20Poly1305 +) + +var ( + CertMagic = [4]byte{0x44, 0x4e, 0x53, 0x43} + ServerMagic = [8]byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38} + MinDNSPacketSize = 12 + 5 + MaxDNSPacketSize = 4096 + MaxDNSUDPPacketSize = 1252 +) + +const ( + ClientMagicLen = 8 +) + +const ( + NonceSize = xsecretbox.NonceSize + HalfNonceSize = xsecretbox.NonceSize / 2 + TagSize = xsecretbox.TagSize + PublicKeySize = 32 + QueryOverhead = ClientMagicLen + PublicKeySize + HalfNonceSize + TagSize + ResponseOverhead = len(ServerMagic) + NonceSize + TagSize + + // Some servers do not work if padded length is less than 256. Example: Quad9 + MinQuestionSize = 256 +) + +type Client struct { + Proto string // Protocol ("udp" or "tcp") + Timeout time.Duration // Timeout for read/write operations +} + +// DnsCrypt server certificate data +type CertInfo struct { + Serial uint32 + ServerPk [32]byte + SharedKey [32]byte + MagicQuery [ClientMagicLen]byte + CryptoConstruction CryptoConstruction + ForwardSecurity bool +} + +// DNSCrypt server information necessary for decryption/encryption +type ServerInfo struct { + SecretKey [32]byte // Client secret key + PublicKey [32]byte // Client public key + Proto string // Protocol ("udp" or "tcp") + ServerPublicKey ed25519.PublicKey // Server public key + ServerAddress string // Server IP address + ProviderName string // Provider name + + ServerCert *CertInfo // Certificate info (obtained with the first unencrypted DNS request) +} + +// Fetches and validates DNSCrypt certificate from the given server +// Data received during this call is then used for DNS requests encryption/decryption +// stampStr is an sdns:// address which is parsed using go-dnsstamps package +func (c *Client) Dial(stampStr string) (*ServerInfo, time.Duration, error) { + + stamp, err := dnsstamps.NewServerStampFromString(stampStr) + if err != nil { + // Invalid SDNS stamp + return nil, 0, err + } + + if stamp.Proto != dnsstamps.StampProtoTypeDNSCrypt { + return nil, 0, errors.New("stamp is not for a DNSCrypt server") + } + + return c.DialStamp(stamp) +} + +// Fetches and validates DNSCrypt certificate from the given server +// Data received during this call is then used for DNS requests encryption/decryption +func (c *Client) DialStamp(stamp dnsstamps.ServerStamp) (*ServerInfo, time.Duration, error) { + + serverInfo := ServerInfo{} + + // Generate the secret/public pair + if _, err := rand.Read(serverInfo.SecretKey[:]); err != nil { + return nil, 0, err + } + curve25519.ScalarBaseMult(&serverInfo.PublicKey, &serverInfo.SecretKey) + + // Set the provider properties + serverInfo.Proto = c.Proto + serverInfo.ServerPublicKey = stamp.ServerPk + serverInfo.ServerAddress = stamp.ServerAddrStr + serverInfo.ProviderName = stamp.ProviderName + if !strings.HasSuffix(serverInfo.ProviderName, ".") { + serverInfo.ProviderName = serverInfo.ProviderName + "." + } + + // Fetch the certificate and validate it + certInfo, rtt, err := serverInfo.fetchCurrentDNSCryptCert(c.Timeout) + + if err != nil { + return nil, rtt, err + } + + serverInfo.ServerCert = &certInfo + return &serverInfo, rtt, nil +} + +// Performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response. +// This method creates a new network connection for every call so avoid using it for TCP. +// DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method. +func (c *Client) Exchange(m *dns.Msg, s *ServerInfo) (*dns.Msg, time.Duration, error) { + + now := time.Now() + conn, err := net.Dial(c.Proto, s.ServerAddress) + if err != nil { + return nil, 0, err + } + defer conn.Close() + + r, _, err := c.ExchangeConn(m, s, conn) + if err != nil { + return nil, 0, err + } + + rtt := time.Since(now) + return r, rtt, nil +} + +// Performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response. +// DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method +func (c *Client) ExchangeConn(m *dns.Msg, s *ServerInfo, conn net.Conn) (*dns.Msg, time.Duration, error) { + now := time.Now() + + c.adjustPayloadSize(m) + query, err := m.Pack() + if err != nil { + return nil, 0, err + } + + encryptedQuery, clientNonce, err := s.encrypt(query) + if err != nil { + return nil, 0, err + } + + if c.Proto == "tcp" { + encryptedQuery, err = prefixWithSize(encryptedQuery) + if err != nil { + return nil, 0, err + } + } + + conn.SetDeadline(time.Now().Add(c.Timeout)) + conn.Write(encryptedQuery) + encryptedResponse := make([]byte, MaxDNSPacketSize) + + if c.Proto == "tcp" { + encryptedResponse, err = readPrefixed(conn) + if err != nil { + return nil, 0, err + } + } else { + length, err := conn.Read(encryptedResponse) + if err != nil { + return nil, 0, err + } + encryptedResponse = encryptedResponse[:length] + } + + decrypted, err := s.decrypt(encryptedResponse, clientNonce) + if err != nil { + // TODO: we should somehow distinguish this case as we might need to re-dial for the server certificate when it happens + return nil, 0, err + } + + r := dns.Msg{} + err = r.Unpack(decrypted) + if err != nil { + return nil, 0, err + } + + rtt := time.Since(now) + return &r, rtt, nil +} + +// Adjusts the maximum payload size advertised in queries sent to upstream servers +// See https://github.com/jedisct1/dnscrypt-proxy/blob/master/dnscrypt-proxy/plugin_get_set_payload_size.go +// TODO: I don't really understand why it is required :) +func (c *Client) adjustPayloadSize(msg *dns.Msg) { + originalMaxPayloadSize := 512 - ResponseOverhead + edns0 := msg.IsEdns0() + dnssec := false + if edns0 != nil { + originalMaxPayloadSize = min(int(edns0.UDPSize())-ResponseOverhead, originalMaxPayloadSize) + dnssec = edns0.Do() + } + var options *[]dns.EDNS0 + + maxPayloadSize := MaxDNSUDPPacketSize - ResponseOverhead + maxPayloadSize = min(MaxDNSUDPPacketSize-ResponseOverhead, max(originalMaxPayloadSize, maxPayloadSize)) + + if maxPayloadSize > 512 { + var extra2 []dns.RR + for _, extra := range msg.Extra { + if extra.Header().Rrtype != dns.TypeOPT { + extra2 = append(extra2, extra) + } else if xoptions := &extra.(*dns.OPT).Option; len(*xoptions) > 0 && options == nil { + options = xoptions + } + } + msg.Extra = extra2 + msg.SetEdns0(uint16(maxPayloadSize), dnssec) + if options != nil { + for _, extra := range msg.Extra { + if extra.Header().Rrtype == dns.TypeOPT { + extra.(*dns.OPT).Option = *options + break + } + } + } + } +} + +func (s *ServerInfo) fetchCurrentDNSCryptCert(timeout time.Duration) (CertInfo, time.Duration, error) { + if len(s.ServerPublicKey) != ed25519.PublicKeySize { + return CertInfo{}, 0, errors.New("invalid public key length") + } + + query := new(dns.Msg) + query.SetQuestion(s.ProviderName, dns.TypeTXT) + client := dns.Client{Net: s.Proto, UDPSize: uint16(MaxDNSUDPPacketSize), Timeout: timeout} + in, rtt, err := client.Exchange(query, s.ServerAddress) + if err != nil { + return CertInfo{}, 0, err + } + + certInfo := CertInfo{CryptoConstruction: UndefinedConstruction} + for _, answerRr := range in.Answer { + recCertInfo, err := txtToCertInfo(answerRr, s) + + if err != nil { + log.Printf("[%v] %s", s.ProviderName, err) + continue + } + + if recCertInfo.Serial < certInfo.Serial { + log.Printf("[%v] Superseded by a previous certificate", s.ProviderName) + continue + } + + if recCertInfo.Serial == certInfo.Serial { + if recCertInfo.CryptoConstruction > certInfo.CryptoConstruction { + log.Printf("[%v] Upgrading the construction from %v to %v", s.ProviderName, certInfo.CryptoConstruction, recCertInfo.CryptoConstruction) + } else { + log.Printf("[%v] Keeping the previous, preferred crypto construction", s.ProviderName) + continue + } + } + + // Set the cert info + certInfo = recCertInfo + } + + if certInfo.CryptoConstruction == UndefinedConstruction { + return certInfo, 0, errors.New("no useable certificate found") + } + + return certInfo, rtt, nil +} + +func (s *ServerInfo) encrypt(packet []byte) (encrypted []byte, clientNonce []byte, err error) { + nonce, clientNonce := make([]byte, NonceSize), make([]byte, HalfNonceSize) + rand.Read(clientNonce) + copy(nonce, clientNonce) + var publicKey *[PublicKeySize]byte + + sharedKey := &s.ServerCert.SharedKey + publicKey = &s.PublicKey + + minQuestionSize := QueryOverhead + len(packet) + if s.Proto == "udp" { + minQuestionSize = max(MinQuestionSize, minQuestionSize) + } else { + var xpad [1]byte + rand.Read(xpad[:]) + minQuestionSize += int(xpad[0]) + } + paddedLength := min(MaxDNSUDPPacketSize, (max(minQuestionSize, QueryOverhead)+63) & ^63) + + if QueryOverhead+len(packet)+1 > paddedLength { + err = errors.New("question too large; cannot be padded") + return + } + encrypted = append(s.ServerCert.MagicQuery[:], publicKey[:]...) + encrypted = append(encrypted, nonce[:HalfNonceSize]...) + padded := pad(packet, paddedLength-QueryOverhead) + if s.ServerCert.CryptoConstruction == XChacha20Poly1305 { + encrypted = xsecretbox.Seal(encrypted, nonce, padded, sharedKey[:]) + } else { + var xsalsaNonce [24]byte + copy(xsalsaNonce[:], nonce) + encrypted = secretbox.Seal(encrypted, padded, &xsalsaNonce, sharedKey) + } + return +} + +func (s *ServerInfo) decrypt(encrypted []byte, nonce []byte) ([]byte, error) { + + sharedKey := &s.ServerCert.SharedKey + serverMagicLen := len(ServerMagic) + responseHeaderLen := serverMagicLen + NonceSize + if len(encrypted) < responseHeaderLen+TagSize+int(MinDNSPacketSize) || + len(encrypted) > responseHeaderLen+TagSize+int(MaxDNSPacketSize) || + !bytes.Equal(encrypted[:serverMagicLen], ServerMagic[:]) { + return encrypted, errors.New("invalid message size or prefix") + } + serverNonce := encrypted[serverMagicLen:responseHeaderLen] + if !bytes.Equal(nonce[:HalfNonceSize], serverNonce[:HalfNonceSize]) { + return encrypted, errors.New("unexpected nonce") + } + var packet []byte + var err error + if s.ServerCert.CryptoConstruction == XChacha20Poly1305 { + packet, err = xsecretbox.Open(nil, serverNonce, encrypted[responseHeaderLen:], sharedKey[:]) + } else { + var xsalsaServerNonce [24]byte + copy(xsalsaServerNonce[:], serverNonce) + var ok bool + packet, ok = secretbox.Open(nil, encrypted[responseHeaderLen:], &xsalsaServerNonce, sharedKey) + if !ok { + err = errors.New("incorrect tag") + } + } + if err != nil { + return encrypted, err + } + packet, err = unpad(packet) + if err != nil || len(packet) < MinDNSPacketSize { + return encrypted, errors.New("incorrect padding") + } + return packet, nil +} + +func txtToCertInfo(answerRr dns.RR, serverInfo *ServerInfo) (CertInfo, error) { + + now := uint32(time.Now().Unix()) + certInfo := CertInfo{CryptoConstruction: UndefinedConstruction} + + binCert, err := packTxtString(strings.Join(answerRr.(*dns.TXT).Txt, "")) + + // Validate the cert basic params + if err != nil { + return certInfo, errors.New("unable to unpack the certificate") + } + if len(binCert) < 124 { + return certInfo, errors.New("certificate is too short") + } + if !bytes.Equal(binCert[:4], CertMagic[:4]) { + return certInfo, errors.New("invalid cert magic") + } + + switch esVersion := binary.BigEndian.Uint16(binCert[4:6]); esVersion { + case 0x0001: + certInfo.CryptoConstruction = XSalsa20Poly1305 + case 0x0002: + certInfo.CryptoConstruction = XChacha20Poly1305 + default: + return certInfo, errors.New(fmt.Sprintf("unsupported crypto construction: %v", esVersion)) + } + + // Verify the server public key + signature := binCert[8:72] + signed := binCert[72:] + if !ed25519.Verify(serverInfo.ServerPublicKey, signed, signature) { + return certInfo, errors.New("incorrect signature") + } + + certInfo.Serial = binary.BigEndian.Uint32(binCert[112:116]) + + // Validate the certificate date + tsBegin := binary.BigEndian.Uint32(binCert[116:120]) + tsEnd := binary.BigEndian.Uint32(binCert[120:124]) + if tsBegin >= tsEnd { + return certInfo, errors.New(fmt.Sprintf("certificate ends before it starts (%v >= %v)", tsBegin, tsEnd)) + } + if now > tsEnd || now < tsBegin { + return certInfo, errors.New("certificate not valid at the current date") + } + + ttl := tsEnd - tsBegin + if ttl > 86400*7 { + certInfo.ForwardSecurity = false + } else { + certInfo.ForwardSecurity = true + } + + var serverPk [32]byte + copy(serverPk[:], binCert[72:104]) + certInfo.SharedKey = computeSharedKey(certInfo.CryptoConstruction, &serverInfo.SecretKey, &serverPk, &serverInfo.ProviderName) + + copy(certInfo.ServerPk[:], serverPk[:]) + copy(certInfo.MagicQuery[:], binCert[104:112]) + + return certInfo, nil +} + +func computeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[32]byte, serverPk *[32]byte, providerName *string) (sharedKey [32]byte) { + if cryptoConstruction == XChacha20Poly1305 { + var err error + sharedKey, err = xsecretbox.SharedKey(*secretKey, *serverPk) + if err != nil { + log.Printf("[%v] Weak public key", providerName) + } + } else { + box.Precompute(&sharedKey, serverPk, secretKey) + } + return +} + +func isDigit(b byte) bool { return b >= '0' && b <= '9' } + +func dddToByte(s []byte) byte { + return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')) +} + +func packTxtString(s string) ([]byte, error) { + bs := make([]byte, len(s)) + msg := make([]byte, 0) + copy(bs, s) + for i := 0; i < len(bs); i++ { + if bs[i] == '\\' { + i++ + if i == len(bs) { + break + } + if i+2 < len(bs) && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) { + msg = append(msg, dddToByte(bs[i:])) + i += 2 + } else if bs[i] == 't' { + msg = append(msg, '\t') + } else if bs[i] == 'r' { + msg = append(msg, '\r') + } else if bs[i] == 'n' { + msg = append(msg, '\n') + } else { + msg = append(msg, bs[i]) + } + } else { + msg = append(msg, bs[i]) + } + } + return msg, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func pad(packet []byte, minSize int) []byte { + packet = append(packet, 0x80) + for len(packet) < minSize { + packet = append(packet, 0) + } + return packet +} + +func unpad(packet []byte) ([]byte, error) { + for i := len(packet); ; { + if i == 0 { + return nil, errors.New("invalid padding (short packet)") + } + i-- + if packet[i] == 0x80 { + return packet[:i], nil + } else if packet[i] != 0x00 { + return nil, errors.New("invalid padding (delimiter not found)") + } + } +} + +func prefixWithSize(packet []byte) ([]byte, error) { + packetLen := len(packet) + if packetLen > 0xffff { + return packet, errors.New("packet too large") + } + packet = append(append(packet, 0), 0) + copy(packet[2:], packet[:len(packet)-2]) + binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)-2)) + return packet, nil +} + +func readPrefixed(conn net.Conn) ([]byte, error) { + buf := make([]byte, 2+MaxDNSPacketSize) + packetLength, pos := -1, 0 + for { + readnb, err := conn.Read(buf[pos:]) + if err != nil { + return buf, err + } + pos += readnb + if pos >= 2 && packetLength < 0 { + packetLength = int(binary.BigEndian.Uint16(buf[0:2])) + if packetLength > MaxDNSPacketSize-1 { + return buf, errors.New("packet too large") + } + if packetLength < MinDNSPacketSize { + return buf, errors.New("packet too short") + } + } + if packetLength >= 0 && pos >= 2+packetLength { + return buf[2 : 2+packetLength], nil + } + } +} diff --git a/dnscrypt_test.go b/dnscrypt_test.go new file mode 100644 index 0000000..2931e46 --- /dev/null +++ b/dnscrypt_test.go @@ -0,0 +1,128 @@ +package dnscrypt + +import ( + "log" + "net" + "testing" + "time" + + "github.com/jedisct1/go-dnsstamps" + "github.com/miekg/dns" +) + +func TestParseStamp(t *testing.T) { + + // Google DoH + stampStr := "sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA" + stamp, err := dnsstamps.NewServerStampFromString(stampStr) + + if err != nil || stamp.ProviderName == "" { + t.Fatalf("Could not parse stamp %s: %s", stampStr, err) + } + + log.Println(stampStr) + log.Printf("Proto=%s\n", stamp.Proto.String()) + log.Printf("ProviderName=%s\n", stamp.ProviderName) + log.Printf("Path=%s\n", stamp.Path) + log.Println("") + + // AdGuard DNSCrypt + stampStr = "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" + stamp, err = dnsstamps.NewServerStampFromString(stampStr) + + if err != nil || stamp.ProviderName == "" { + t.Fatalf("Could not parse stamp %s: %s", stampStr, err) + } + + log.Println(stampStr) + log.Printf("Proto=%s\n", stamp.Proto.String()) + log.Printf("ProviderName=%s\n", stamp.ProviderName) + log.Printf("Path=%s\n", stamp.Path) + log.Printf("ServerAddrStr=%s\n", stamp.ServerAddrStr) + log.Println("") +} + +func TestDnsCryptResolver(t *testing.T) { + + stamps := []struct { + stampStr string + udp bool + }{ + { + // AdGuard DNS + stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + udp: true, + }, + { + // AdGuard DNS Family + stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ", + udp: true, + }, + { + // Cisco OpenDNS + stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ", + udp: true, + }, + { + // Cisco OpenDNS Family Shield + stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ", + udp: true, + }, + { + // Quad9 (anycast) dnssec/no-log/filter 9.9.9.9 + stampStr: "sdns://AQMAAAAAAAAADDkuOS45Ljk6ODQ0MyBnyEe4yHWM0SAkVUO-dWdG3zTfHYTAC4xHA2jfgh2GPhkyLmRuc2NyeXB0LWNlcnQucXVhZDkubmV0", + udp: true, + }, + { + // https://securedns.eu/ + stampStr: "sdns://AQcAAAAAAAAAEzE0Ni4xODUuMTY3LjQzOjUzNTMgs6WXaRRXWwSJ4Z-unEPmefryjFcYlwAxf3u0likfsJUcMi5kbnNjcnlwdC1jZXJ0LnNlY3VyZWRucy5ldQ", + udp: true, + }, + { + // Yandex DNS + stampStr: "sdns://AQQAAAAAAAAAEDc3Ljg4LjguNzg6MTUzNTMg04TAccn3RmKvKszVe13MlxTUB7atNgHhrtwG1W1JYyciMi5kbnNjcnlwdC1jZXJ0LmJyb3dzZXIueWFuZGV4Lm5ldA", + udp: true, + }, + } + + for _, test := range stamps { + + if test.udp { + checkDnsCryptServer(t, test.stampStr, "udp") + } + checkDnsCryptServer(t, test.stampStr, "tcp") + } +} + +func checkDnsCryptServer(t *testing.T, stampStr string, proto string) { + + client := Client{Proto: proto, Timeout: 10 * time.Second} + serverInfo, rtt, err := client.Dial(stampStr) + if err != nil { + t.Fatalf("Could not establish connection with %s", stampStr) + } + + log.Printf("Established a connection with %s, rtt=%v, proto=%s", serverInfo.ProviderName, rtt, proto) + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, + } + + reply, rtt, err := client.Exchange(&req, serverInfo) + if err != nil { + t.Fatalf("Couldn't talk to upstream %s: %s", serverInfo.ProviderName, err) + } + if len(reply.Answer) != 1 { + t.Fatalf("DNS upstream %s returned reply with wrong number of answers - %d", serverInfo.ProviderName, len(reply.Answer)) + } + if a, ok := reply.Answer[0].(*dns.A); ok { + if !net.IPv4(8, 8, 8, 8).Equal(a.A) { + t.Fatalf("DNS upstream %s returned wrong answer instead of 8.8.8.8: %v", serverInfo.ProviderName, a.A) + } + } else { + t.Fatalf("DNS upstream %s returned wrong answer type instead of A: %v", serverInfo.ProviderName, reply.Answer[0]) + } + log.Printf("Got proper response from %s, rtt=%v, proto=%s", serverInfo.ProviderName, rtt, proto) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12ba2bb --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/ameshkov/dnscrypt + +require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect + github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86 + github.com/jedisct1/xsecretbox v0.0.0-20180508184500-7a679c0bcd9a + github.com/miekg/dns v1.1.1 + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 + golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 // indirect + golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect + golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a402dde --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= +github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= +github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86 h1:Olj4M6T1omUfx7yTTcnhLf4xo6gYMmRHSJIfeA1NZy0= +github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86/go.mod h1:j/ONpSHHmPgDwmFKXg9vhQvIjADe/ft1X4a3TVOmp9g= +github.com/jedisct1/xsecretbox v0.0.0-20180508184500-7a679c0bcd9a h1:2nyBWKszM41RO/gt5ElUXigAFiRgJ9KifHDlWOlw0lc= +github.com/jedisct1/xsecretbox v0.0.0-20180508184500-7a679c0bcd9a/go.mod h1:YlN58h704uRFD0BwsEGTq+7Wx+WG2i7P49bc+HwHyAY= +github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o= +github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ= +golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=