Let's start with a 15 minutes ugly PoC hack before going to bed
Who said the DNSCrypt protocol was "complex"?
This commit is contained in:
parent
9653ab395c
commit
b076e01f7a
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue