mirror of
https://github.com/SamTherapy/dnscrypt.git
synced 2024-12-21 16:50:42 +00:00
Init
This commit is contained in:
commit
63ea7215f7
9 changed files with 786 additions and 0 deletions
8
.codecov.yml
Normal file
8
.codecov.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 40%
|
||||
threshold: null
|
||||
patch: false
|
||||
changes: false
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
.vscode
|
12
.travis.yml
Normal file
12
.travis.yml
Normal file
|
@ -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)
|
25
LICENSE
Normal file
25
LICENSE
Normal file
|
@ -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 <http://unlicense.org/>
|
||||
|
33
README.md
Normal file
33
README.md
Normal file
|
@ -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)
|
||||
```
|
547
dnscrypt.go
Normal file
547
dnscrypt.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
128
dnscrypt_test.go
Normal file
128
dnscrypt_test.go
Normal file
|
@ -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)
|
||||
}
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -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
|
||||
)
|
18
go.sum
Normal file
18
go.sum
Normal file
|
@ -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=
|
Loading…
Reference in a new issue