diff --git a/certs.go b/certs.go new file mode 100644 index 00000000..2840a57d --- /dev/null +++ b/certs.go @@ -0,0 +1,137 @@ +package main + +import ( + "encoding/binary" + "errors" + "log" + "reflect" + "strings" + "time" + + "github.com/jedisct1/xsecretbox" + "github.com/miekg/dns" + "golang.org/x/crypto/ed25519" +) + +type CertInfo struct { + ServerPk [32]byte + SharedKey [32]byte + MagicQuery [8]byte + CryptoConstruction CryptoConstruction +} + +func FetchCurrentCert(proxy *Proxy, pk ed25519.PublicKey, serverAddress string, providerName string) (CertInfo, error) { + if len(pk) != ed25519.PublicKeySize { + return CertInfo{}, errors.New("Invalid public key length") + } + if strings.HasSuffix(providerName, ".") == false { + providerName = providerName + "." + } + query := new(dns.Msg) + query.SetQuestion(providerName, dns.TypeTXT) + client := dns.Client{Net: "tcp", UDPSize: 1252} + in, _, err := client.Exchange(query, serverAddress) + if err != nil { + log.Fatal(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 { + return certInfo, err + } + if len(binCert) < 124 { + return certInfo, errors.New("Certificate too short") + } + if reflect.DeepEqual(binCert[:4], CertMagic[:4]) == false { + return certInfo, errors.New("Invalid cert magic") + } + cryptoConstruction := CryptoConstruction(0) + switch esVersion := binary.BigEndian.Uint16(binCert[4:6]); esVersion { + case 0x0001: + cryptoConstruction = XSalsa20Poly1305 + case 0x0002: + cryptoConstruction = XChacha20Poly1305 + default: + return certInfo, errors.New("Unsupported crypto construction") + } + signature := binCert[8:72] + signed := binCert[72:] + if ed25519.Verify(pk, signed, signature) == false { + log.Fatal("Incorrect signature") + } + serial := binary.BigEndian.Uint32(binCert[112:116]) + tsBegin := binary.BigEndian.Uint32(binCert[116:120]) + tsEnd := binary.BigEndian.Uint32(binCert[120:124]) + if now > tsEnd || now < tsBegin { + log.Print("Certificate not valid at the current date") + continue + } + if serial < highestSerial { + log.Print("Superseded by a previous certificate") + continue + } + if serial == highestSerial && cryptoConstruction < certInfo.CryptoConstruction { + log.Print("Keeping the previous, preferred crypto construction") + continue + } + if cryptoConstruction != XChacha20Poly1305 { + log.Printf("Cryptographic construction %v not supported\n", cryptoConstruction) + continue + } + var serverPk [32]byte + copy(serverPk[:], binCert[72:104]) + sharedKey, err := xsecretbox.SharedKey(proxy.proxySecretKey, serverPk) + if err != nil { + log.Print("Weak public key") + continue + } + certInfo.SharedKey = sharedKey + highestSerial = serial + certInfo.CryptoConstruction = cryptoConstruction + copy(certInfo.ServerPk[:], serverPk[:]) + copy(certInfo.MagicQuery[:], binCert[104:112]) + log.Printf("Valid cert found: %x\n", certInfo.ServerPk) + } + if certInfo.CryptoConstruction == UndefinedConstruction { + return certInfo, errors.New("No useable certificate found") + } + return certInfo, 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/common.go b/common.go new file mode 100644 index 00000000..a7782b7e --- /dev/null +++ b/common.go @@ -0,0 +1,57 @@ +package main + +import ( + "errors" + "time" +) + +type CryptoConstruction uint16 + +const ( + UndefinedConstruction CryptoConstruction = iota + XSalsa20Poly1305 + XChacha20Poly1305 +) + +type ServerParams struct { + CertInfo CertInfo +} + +var ( + CertMagic = [4]byte{0x44, 0x4e, 0x53, 0x43} + ServerMagic = [8]byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38} + MinDNSPacketSize = uint(12) + MaxDNSPacketSize = uint(4096) + InitialMinQuestionSize = uint(128) + TimeoutMin = 1 * time.Second + TimeoutMax = 5 * time.Second +) + +func HasTCFlag(packet []byte) bool { + return packet[2]&2 == 2 +} + +func Pad(packet []byte, minSize uint) []byte { + packet = append(packet, 0x80) + for uint(len(packet)) < minSize { + packet = append(packet, 0) + } + return packet +} + +func Unpad(packet []byte) ([]byte, error) { + i := len(packet) + for { + if i == 0 { + return nil, errors.New("Invalid padding (short packet)") + } + i-- + if packet[i] == 0x80 { + break + } + if packet[i] != 0x00 { + return nil, errors.New("Invalid padding (delimiter not found)") + } + } + return packet[:i], nil +} diff --git a/dnscrypt-proxy.go b/dnscrypt-proxy.go new file mode 100644 index 00000000..ce69b27b --- /dev/null +++ b/dnscrypt-proxy.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "net" + "strings" + "time" + + "github.com/jedisct1/xsecretbox" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/ed25519" +) + +type Proxy struct { + proxyPublicKey [32]byte + proxySecretKey [32]byte + minQuestionSize uint + serversInfo []ServerInfo +} + +func (proxy *Proxy) fetchServerInfo(serverAddrStr string, serverPkStr string, providerName string) { + serverPublicKey, err := hex.DecodeString(strings.Replace(serverPkStr, ":", "", -1)) + if err != nil || len(serverPublicKey) != ed25519.PublicKeySize { + log.Fatal("Invalid public key") + } + certInfo, err := FetchCurrentCert(proxy, serverPublicKey, serverAddrStr, providerName) + if err != nil { + log.Fatal(err) + } + remoteUDPAddr, err := net.ResolveUDPAddr("udp", serverAddrStr) + if err != nil { + log.Fatal(err) + } + remoteTCPAddr, err := net.ResolveTCPAddr("tcp", serverAddrStr) + if err != nil { + log.Fatal(err) + } + serverInfo := ServerInfo{ + MagicQuery: certInfo.MagicQuery, + ServerPk: certInfo.ServerPk, + SharedKey: certInfo.SharedKey, + CryptoConstruction: certInfo.CryptoConstruction, + Timeout: TimeoutMin, + UDPAddr: remoteUDPAddr, + TCPAddr: remoteTCPAddr, + } + proxy.serversInfo = append(proxy.serversInfo, serverInfo) +} + +func NewProxy(listenAddrStr string, serverAddrStr string, serverPkStr string, providerName string) Proxy { + proxy := Proxy{minQuestionSize: InitialMinQuestionSize} + if _, err := rand.Read(proxy.proxySecretKey[:]); err != nil { + log.Fatal(err) + } + curve25519.ScalarBaseMult(&proxy.proxyPublicKey, &proxy.proxySecretKey) + proxy.fetchServerInfo(serverAddrStr, serverPkStr, providerName) + clientPc, err := net.ListenPacket("udp", listenAddrStr) + if err != nil { + log.Fatal(err) + } + defer clientPc.Close() + fmt.Printf("Now listening to %v [UDP]\n", listenAddrStr) + for { + buffer := make([]byte, MaxDNSPacketSize) + length, clientAddr, err := clientPc.ReadFrom(buffer) + if err != nil { + break + } + packet := buffer[:length] + go func() { + proxy.processIncomingQuery(&proxy.serversInfo[0], packet, clientAddr, clientPc) + }() + } + return proxy +} + +type ServerInfo struct { + MagicQuery [8]byte + ServerPk [32]byte + SharedKey [32]byte + CryptoConstruction CryptoConstruction + Timeout time.Duration + UDPAddr *net.UDPAddr + TCPAddr *net.TCPAddr +} + +func (proxy *Proxy) processIncomingQuery(serverInfo *ServerInfo, packet []byte, clientAddr net.Addr, clientPc net.PacketConn) { + packet = Pad(packet, proxy.minQuestionSize) + nonce := make([]byte, xsecretbox.NonceSize) + rand.Read(nonce[0 : xsecretbox.NonceSize/2]) + encrypted := serverInfo.MagicQuery[:] + encrypted = append(encrypted, proxy.proxyPublicKey[:]...) + encrypted = append(encrypted, nonce[:xsecretbox.NonceSize/2]...) + encrypted = xsecretbox.Seal(encrypted, nonce, packet, serverInfo.SharedKey[:]) + pc, err := net.DialUDP("udp", nil, serverInfo.UDPAddr) + defer pc.Close() + if err != nil { + return + } + pc.SetDeadline(time.Now().Add(serverInfo.Timeout)) + pc.Write(encrypted) + buffer := make([]byte, MaxDNSPacketSize) + length, err := pc.Read(buffer) + if err != nil { + return + } + buffer = buffer[:length] + serverMagicLen := len(ServerMagic) + responseHeaderLen := serverMagicLen + xsecretbox.NonceSize + if len(buffer) < responseHeaderLen+xsecretbox.TagSize || + !bytes.Equal(buffer[:serverMagicLen], ServerMagic[:]) { + return + } + serverNonce := buffer[serverMagicLen:responseHeaderLen] + if !bytes.Equal(nonce[:xsecretbox.NonceSize/2], serverNonce[:xsecretbox.NonceSize/2]) { + return + } + decrypted, err := xsecretbox.Open(nil, serverNonce, buffer[responseHeaderLen:], serverInfo.SharedKey[:]) + if err != nil { + return + } + decrypted, err = Unpad(decrypted) + if err != nil || uint(len(decrypted)) < MinDNSPacketSize { + return + } + if HasTCFlag(decrypted) { + if MaxDNSPacketSize-proxy.minQuestionSize < proxy.minQuestionSize { + proxy.minQuestionSize = MaxDNSPacketSize + } else { + proxy.minQuestionSize *= 2 + } + } + clientPc.WriteTo(decrypted, clientAddr) +} + +func main() { + log.SetFlags(0) + _ = NewProxy("127.0.0.1:5399", "212.47.228.136:443", "E801:B84E:A606:BFB0:BAC0:CE43:445B:B15E:BA64:B02F:A3C4:AA31:AE10:636A:0790:324D", "2.dnscrypt-cert.fr.dnscrypt.org") +} diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 00000000..9ea41d06 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,9 @@ +package: . +import: +- package: github.com/jedisct1/xsecretbox +- package: github.com/miekg/dns + version: ^1.0.1 +- package: golang.org/x/crypto + subpackages: + - curve25519 + - ed25519