diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index b5938bf4..204d7d45 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -43,6 +43,7 @@ type Config struct { SourceRequireDNSSEC bool `toml:"require_dnssec"` SourceRequireNoLog bool `toml:"require_nolog"` SourceRequireNoFilter bool `toml:"require_nofilter"` + SourceDNSCrypt bool `toml:"dnscrypt_servers"` SourceDoH bool `toml:"doh_servers"` SourceIPv4 bool `toml:"ipv4_servers"` SourceIPv6 bool `toml:"ipv6_servers"` @@ -71,6 +72,7 @@ func newConfig() Config { SourceRequireNoFilter: true, SourceIPv4: true, SourceIPv6: false, + SourceDNSCrypt: true, SourceDoH: true, MaxClients: 250, FallbackResolver: DefaultFallbackResolver, @@ -285,6 +287,7 @@ func ConfigLoad(proxy *Proxy, svcFlag *string) error { config.SourceRequireNoLog = false config.SourceIPv4 = true config.SourceIPv6 = true + config.SourceDNSCrypt = true config.SourceDoH = true } @@ -435,7 +438,8 @@ func (config *Config) loadSource(proxy *Proxy, requiredProps ServerInformalPrope continue } } - if !(config.SourceDoH && registeredServer.stamp.proto == StampProtoTypeDoH) { + if !((config.SourceDNSCrypt && registeredServer.stamp.proto == StampProtoTypeDNSCrypt) || + (config.SourceDoH && registeredServer.stamp.proto == StampProtoTypeDoH)) { continue } dlog.Debugf("Adding [%s] to the set of wanted resolvers", registeredServer.name) diff --git a/dnscrypt-proxy/dnscrypt_certs.go b/dnscrypt-proxy/dnscrypt_certs.go new file mode 100644 index 00000000..34109b3c --- /dev/null +++ b/dnscrypt-proxy/dnscrypt_certs.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "encoding/binary" + "errors" + "strings" + "time" + + "github.com/jedisct1/dlog" + "github.com/jedisct1/xsecretbox" + "github.com/miekg/dns" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/nacl/box" +) + +type CertInfo struct { + ServerPk [32]byte + SharedKey [32]byte + MagicQuery [ClientMagicLen]byte + CryptoConstruction CryptoConstruction + ForwardSecurity bool +} + +func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk ed25519.PublicKey, serverAddress string, providerName string, isNew bool) (CertInfo, int, error) { + if len(pk) != ed25519.PublicKeySize { + return CertInfo{}, 0, errors.New("Invalid public key length") + } + if !strings.HasSuffix(providerName, ".") { + providerName = providerName + "." + } + if serverName == nil { + serverName = &providerName + } + query := new(dns.Msg) + query.SetQuestion(providerName, dns.TypeTXT) + client := dns.Client{Net: proto, UDPSize: uint16(MaxDNSUDPPacketSize)} + in, rtt, err := client.Exchange(query, serverAddress) + if err != nil { + dlog.Noticef("[%s] TIMEOUT", *serverName) + return CertInfo{}, 0, err + } + now := uint32(time.Now().Unix()) + certInfo := CertInfo{CryptoConstruction: UndefinedConstruction} + highestSerial := uint32(0) + for _, answerRr := range in.Answer { + binCert, err := packTxtString(strings.Join(answerRr.(*dns.TXT).Txt, "")) + if err != nil { + dlog.Warnf("[%v] Unable to unpack the certificate", providerName) + continue + } + if len(binCert) < 124 { + dlog.Warnf("[%v] Certificate too short", providerName) + continue + } + if !bytes.Equal(binCert[:4], CertMagic[:4]) { + dlog.Warnf("[%v] Invalid cert magic", providerName) + continue + } + cryptoConstruction := CryptoConstruction(0) + switch esVersion := binary.BigEndian.Uint16(binCert[4:6]); esVersion { + case 0x0001: + cryptoConstruction = XSalsa20Poly1305 + case 0x0002: + cryptoConstruction = XChacha20Poly1305 + default: + dlog.Noticef("[%v] Unsupported crypto construction", providerName) + continue + } + signature := binCert[8:72] + signed := binCert[72:] + if !ed25519.Verify(pk, signed, signature) { + dlog.Warnf("[%v] Incorrect signature", providerName) + continue + } + serial := binary.BigEndian.Uint32(binCert[112:116]) + tsBegin := binary.BigEndian.Uint32(binCert[116:120]) + tsEnd := binary.BigEndian.Uint32(binCert[120:124]) + if tsBegin >= tsEnd { + dlog.Warnf("[%v] certificate ends before it starts (%v >= %v)", providerName, tsBegin, tsEnd) + continue + } + ttl := tsEnd - tsBegin + if ttl > 86400*7 { + dlog.Infof("[%v] the key validity period for this server is excessively long (%d days), significantly reducing reliability and forward security.", providerName, ttl/86400) + daysLeft := (tsEnd - now) / 86400 + if daysLeft < 1 { + dlog.Criticalf("[%v] certificate will expire today -- Switch to a different resolver as soon as possible", providerName) + } else if daysLeft <= 7 { + dlog.Warnf("[%v] certificate is about to expire -- if you don't manage this server, tell the server operator about it", providerName) + } else if daysLeft <= 30 { + dlog.Infof("[%v] certificate will expire in %d days", providerName, daysLeft) + } + certInfo.ForwardSecurity = false + } else { + certInfo.ForwardSecurity = true + } + if !proxy.certIgnoreTimestamp { + if now > tsEnd || now < tsBegin { + dlog.Debugf("[%v] Certificate not valid at the current date", providerName) + continue + } + } + if serial < highestSerial { + dlog.Debugf("[%v] Superseded by a previous certificate", providerName) + continue + } + if serial == highestSerial { + if cryptoConstruction < certInfo.CryptoConstruction { + dlog.Debugf("[%v] Keeping the previous, preferred crypto construction", providerName) + continue + } else { + dlog.Debugf("[%v] Upgrading the construction from %v to %v", providerName, certInfo.CryptoConstruction, cryptoConstruction) + } + } + if cryptoConstruction != XChacha20Poly1305 && cryptoConstruction != XSalsa20Poly1305 { + dlog.Noticef("[%v] Cryptographic construction %v not supported", providerName, cryptoConstruction) + continue + } + var serverPk [32]byte + copy(serverPk[:], binCert[72:104]) + var sharedKey [32]byte + if cryptoConstruction == XChacha20Poly1305 { + sharedKey, err = xsecretbox.SharedKey(proxy.proxySecretKey, serverPk) + if err != nil { + dlog.Criticalf("[%v] Weak public key", providerName) + continue + } + } else { + box.Precompute(&sharedKey, &serverPk, &proxy.proxySecretKey) + } + certInfo.SharedKey = sharedKey + highestSerial = serial + certInfo.CryptoConstruction = cryptoConstruction + copy(certInfo.ServerPk[:], serverPk[:]) + copy(certInfo.MagicQuery[:], binCert[104:112]) + if isNew { + dlog.Noticef("[%s] OK (crypto v%d) - rtt: %dms", *serverName, cryptoConstruction, rtt.Nanoseconds()/1000000) + } else { + dlog.Infof("[%s] OK (crypto v%d) - rtt: %dms", *serverName, cryptoConstruction, rtt.Nanoseconds()/1000000) + } + } + if certInfo.CryptoConstruction == UndefinedConstruction { + return certInfo, 0, errors.New("No useable certificate found") + } + return certInfo, int(rtt.Nanoseconds() / 1000000), nil +} + +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 +} diff --git a/dnscrypt-proxy/proxy.go b/dnscrypt-proxy/proxy.go index 42de53ae..2be3e4a6 100644 --- a/dnscrypt-proxy/proxy.go +++ b/dnscrypt-proxy/proxy.go @@ -271,7 +271,22 @@ func (proxy *Proxy) processIncomingQuery(serverInfo *ServerInfo, clientProto str } if len(response) == 0 { var ttl *uint32 - if serverInfo.Proto == StampProtoTypeDoH { + if serverInfo.Proto == StampProtoTypeDNSCrypt { + encryptedQuery, clientNonce, err := proxy.Encrypt(serverInfo, query, serverProto) + if err != nil { + return + } + serverInfo.noticeBegin(proxy) + if serverProto == "udp" { + response, err = proxy.exchangeWithUDPServer(serverInfo, encryptedQuery, clientNonce) + } else { + response, err = proxy.exchangeWithTCPServer(serverInfo, encryptedQuery, clientNonce) + } + if err != nil { + serverInfo.noticeFailure(proxy) + return + } + } else if serverInfo.Proto == StampProtoTypeDoH { tid := TransactionID(query) SetTransactionID(query, 0) serverInfo.noticeBegin(proxy) diff --git a/dnscrypt-proxy/serversInfo.go b/dnscrypt-proxy/serversInfo.go index 5187b71c..a760345d 100644 --- a/dnscrypt-proxy/serversInfo.go +++ b/dnscrypt-proxy/serversInfo.go @@ -2,6 +2,7 @@ package main import ( "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -16,6 +17,7 @@ import ( "github.com/VividCortex/ewma" "github.com/jedisct1/dlog" + "golang.org/x/crypto/ed25519" ) const ( @@ -205,12 +207,49 @@ func (serversInfo *ServersInfo) getOne() *ServerInfo { } func (serversInfo *ServersInfo) fetchServerInfo(proxy *Proxy, name string, stamp ServerStamp, isNew bool) (ServerInfo, error) { - if stamp.proto == StampProtoTypeDoH { + if stamp.proto == StampProtoTypeDNSCrypt { + return serversInfo.fetchDNSCryptServerInfo(proxy, name, stamp, isNew) + } else if stamp.proto == StampProtoTypeDoH { return serversInfo.fetchDoHServerInfo(proxy, name, stamp, isNew) } return ServerInfo{}, errors.New("Unsupported protocol") } +func (serversInfo *ServersInfo) fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp ServerStamp, isNew bool) (ServerInfo, error) { + if len(stamp.serverPk) != ed25519.PublicKeySize { + serverPk, err := hex.DecodeString(strings.Replace(string(stamp.serverPk), ":", "", -1)) + if err != nil || len(serverPk) != ed25519.PublicKeySize { + dlog.Fatalf("Unsupported public key for [%s]: [%s]", name, stamp.serverPk) + } + dlog.Warnf("Public key [%s] shouldn't be hex-encoded any more", string(stamp.serverPk)) + stamp.serverPk = serverPk + } + certInfo, rtt, err := FetchCurrentDNSCryptCert(proxy, &name, proxy.mainProto, stamp.serverPk, stamp.serverAddrStr, stamp.providerName, isNew) + if err != nil { + return ServerInfo{}, err + } + remoteUDPAddr, err := net.ResolveUDPAddr("udp", stamp.serverAddrStr) + if err != nil { + return ServerInfo{}, err + } + remoteTCPAddr, err := net.ResolveTCPAddr("tcp", stamp.serverAddrStr) + if err != nil { + return ServerInfo{}, err + } + return ServerInfo{ + Proto: StampProtoTypeDNSCrypt, + MagicQuery: certInfo.MagicQuery, + ServerPk: certInfo.ServerPk, + SharedKey: certInfo.SharedKey, + CryptoConstruction: certInfo.CryptoConstruction, + Name: name, + Timeout: proxy.timeout, + UDPAddr: remoteUDPAddr, + TCPAddr: remoteTCPAddr, + initialRtt: rtt, + }, nil +} + func (serversInfo *ServersInfo) fetchDoHServerInfo(proxy *Proxy, name string, stamp ServerStamp, isNew bool) (ServerInfo, error) { if len(stamp.serverAddrStr) > 0 { addrStr := stamp.serverAddrStr diff --git a/dnscrypt-proxy/sources.go b/dnscrypt-proxy/sources.go index eecff85f..5a3ccbad 100644 --- a/dnscrypt-proxy/sources.go +++ b/dnscrypt-proxy/sources.go @@ -1,6 +1,7 @@ package main import ( + "encoding/csv" "errors" "fmt" "io" @@ -208,13 +209,58 @@ func NewSource(xTransport *XTransport, urls []string, minisignKeyStr string, cac } func (source *Source) Parse(prefix string) ([]RegisteredServer, error) { - if source.format == SourceFormatV2 { + if source.format == SourceFormatV1 { + return source.parseV1(prefix) + } else if source.format == SourceFormatV2 { return source.parseV2(prefix) } dlog.Fatal("Unexpected source format") return []RegisteredServer{}, nil } +func (source *Source) parseV1(prefix string) ([]RegisteredServer, error) { + var registeredServers []RegisteredServer + + csvReader := csv.NewReader(strings.NewReader(source.in)) + records, err := csvReader.ReadAll() + if err != nil { + return registeredServers, nil + } + for lineNo, record := range records { + if len(record) == 0 { + continue + } + if len(record) < 14 { + return registeredServers, fmt.Errorf("Parse error at line %d", 1+lineNo) + } + if lineNo == 0 { + continue + } + name := prefix + record[0] + description := record[2] + serverAddrStr := record[10] + providerName := record[11] + serverPkStr := record[12] + props := ServerInformalProperties(0) + if strings.EqualFold(record[7], "yes") { + props |= ServerInformalPropertyDNSSEC + } + if strings.EqualFold(record[8], "yes") { + props |= ServerInformalPropertyNoLog + } + stamp, err := NewDNSCryptServerStampFromLegacy(serverAddrStr, serverPkStr, providerName, props) + if err != nil { + return registeredServers, err + } + registeredServer := RegisteredServer{ + name: name, stamp: stamp, description: description, + } + dlog.Debugf("Registered [%s] with stamp [%s]", name, stamp.String()) + registeredServers = append(registeredServers, registeredServer) + } + return registeredServers, nil +} + func (source *Source) parseV2(prefix string) ([]RegisteredServer, error) { var registeredServers []RegisteredServer in := string(source.in) diff --git a/dnscrypt-proxy/stamps.go b/dnscrypt-proxy/stamps.go index 63807d5b..c7388ad6 100644 --- a/dnscrypt-proxy/stamps.go +++ b/dnscrypt-proxy/stamps.go @@ -3,6 +3,7 @@ package main import ( "encoding/base64" "encoding/binary" + "encoding/hex" "errors" "fmt" "net" @@ -10,19 +11,23 @@ import ( "strings" "github.com/jedisct1/dlog" + "golang.org/x/crypto/ed25519" ) type StampProtoType uint8 const ( - StampProtoTypePlain = StampProtoType(0x00) - StampProtoTypeDoH = StampProtoType(0x02) + StampProtoTypePlain = StampProtoType(0x00) + StampProtoTypeDNSCrypt = StampProtoType(0x01) + StampProtoTypeDoH = StampProtoType(0x02) ) func (stampProtoType *StampProtoType) String() string { switch *stampProtoType { case StampProtoTypePlain: return "Plain" + case StampProtoTypeDNSCrypt: + return "DNSCrypt" case StampProtoTypeDoH: return "DoH" default: @@ -40,6 +45,23 @@ type ServerStamp struct { proto StampProtoType } +func NewDNSCryptServerStampFromLegacy(serverAddrStr string, serverPkStr string, providerName string, props ServerInformalProperties) (ServerStamp, error) { + if net.ParseIP(serverAddrStr) != nil { + serverAddrStr = fmt.Sprintf("%s:%d", serverAddrStr, DefaultPort) + } + serverPk, err := hex.DecodeString(strings.Replace(serverPkStr, ":", "", -1)) + if err != nil || len(serverPk) != ed25519.PublicKeySize { + return ServerStamp{}, fmt.Errorf("Unsupported public key: [%s]", serverPkStr) + } + return ServerStamp{ + serverAddrStr: serverAddrStr, + serverPk: serverPk, + providerName: providerName, + props: props, + proto: StampProtoTypeDNSCrypt, + }, nil +} + func NewServerStampFromString(stampStr string) (ServerStamp, error) { if !strings.HasPrefix(stampStr, "sdns://") && !strings.HasPrefix(stampStr, "dnsc://") { return ServerStamp{}, errors.New("Stamps are expected to start with sdns://") @@ -51,12 +73,58 @@ func NewServerStampFromString(stampStr string) (ServerStamp, error) { if len(bin) < 1 { return ServerStamp{}, errors.New("Stamp is too short") } - if bin[0] == uint8(StampProtoTypeDoH) { + if bin[0] == uint8(StampProtoTypeDNSCrypt) { + return newDNSCryptServerStamp(bin) + } else if bin[0] == uint8(StampProtoTypeDoH) { return newDoHServerStamp(bin) } return ServerStamp{}, errors.New("Unsupported stamp version or protocol") } +// id(u8)=0x01 props addrLen(1) serverAddr pkStrlen(1) pkStr providerNameLen(1) providerName + +func newDNSCryptServerStamp(bin []byte) (ServerStamp, error) { + stamp := ServerStamp{proto: StampProtoTypeDNSCrypt} + if len(bin) < 66 { + return stamp, errors.New("Stamp is too short") + } + stamp.props = ServerInformalProperties(binary.LittleEndian.Uint64(bin[1:9])) + binLen := len(bin) + pos := 9 + + len := int(bin[pos]) + if 1+len >= binLen-pos { + return stamp, errors.New("Invalid stamp") + } + pos++ + stamp.serverAddrStr = string(bin[pos : pos+len]) + pos += len + if net.ParseIP(strings.TrimRight(strings.TrimLeft(stamp.serverAddrStr, "["), "]")) != nil { + stamp.serverAddrStr = fmt.Sprintf("%s:%d", stamp.serverAddrStr, DefaultPort) + } + + len = int(bin[pos]) + if 1+len >= binLen-pos { + return stamp, errors.New("Invalid stamp") + } + pos++ + stamp.serverPk = bin[pos : pos+len] + pos += len + + len = int(bin[pos]) + if len >= binLen-pos { + return stamp, errors.New("Invalid stamp") + } + pos++ + stamp.providerName = string(bin[pos : pos+len]) + pos += len + + if pos != binLen { + return stamp, errors.New("Invalid stamp (garbage after end)") + } + return stamp, nil +} + // id(u8)=0x02 props addrLen(1) serverAddr hashLen(1) hash providerNameLen(1) providerName pathLen(1) path func newDoHServerStamp(bin []byte) (ServerStamp, error) { @@ -120,13 +188,38 @@ func newDoHServerStamp(bin []byte) (ServerStamp, error) { } func (stamp *ServerStamp) String() string { - if stamp.proto == StampProtoTypeDoH { + if stamp.proto == StampProtoTypeDNSCrypt { + return stamp.dnsCryptString() + } else if stamp.proto == StampProtoTypeDoH { return stamp.dohString() } dlog.Fatal("Unsupported protocol") return "" } +func (stamp *ServerStamp) dnsCryptString() string { + bin := make([]uint8, 9) + bin[0] = uint8(StampProtoTypeDNSCrypt) + binary.LittleEndian.PutUint64(bin[1:9], uint64(stamp.props)) + + serverAddrStr := stamp.serverAddrStr + if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(DefaultPort)) { + serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(DefaultPort))] + } + bin = append(bin, uint8(len(serverAddrStr))) + bin = append(bin, []uint8(serverAddrStr)...) + + bin = append(bin, uint8(len(stamp.serverPk))) + bin = append(bin, stamp.serverPk...) + + bin = append(bin, uint8(len(stamp.providerName))) + bin = append(bin, []uint8(stamp.providerName)...) + + str := base64.RawURLEncoding.EncodeToString(bin) + + return "sdns://" + str +} + func (stamp *ServerStamp) dohString() string { bin := make([]uint8, 9) bin[0] = uint8(StampProtoTypeDoH)