dnscrypt-proxy/dnscrypt-proxy/serversInfo.go

986 lines
29 KiB
Go

package main
import (
crypto_rand "crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/bits"
"math/rand"
"net"
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/VividCortex/ewma"
"github.com/jedisct1/dlog"
clocksmith "github.com/jedisct1/go-clocksmith"
stamps "github.com/jedisct1/go-dnsstamps"
"github.com/miekg/dns"
"golang.org/x/crypto/ed25519"
)
const (
RTTEwmaDecay = 10.0
)
type RegisteredServer struct {
name string
stamp stamps.ServerStamp
description string
}
type ServerBugs struct {
fragmentsBlocked bool
}
type DOHClientCreds struct {
clientCert string
clientKey string
rootCA string
}
type ServerInfo struct {
DOHClientCreds DOHClientCreds
lastActionTS time.Time
rtt ewma.MovingAverage
Name string
HostName string
UDPAddr *net.UDPAddr
TCPAddr *net.TCPAddr
Relay *Relay
URL *url.URL
initialRtt int
Timeout time.Duration
CryptoConstruction CryptoConstruction
ServerPk [32]byte
SharedKey [32]byte
MagicQuery [8]byte
knownBugs ServerBugs
Proto stamps.StampProtoType
useGet bool
odohTargetConfigs []ODoHTargetConfig
}
type LBStrategy interface {
getCandidate(serversCount int) int
getActiveCount(serversCount int) int
}
type LBStrategyP2 struct{}
func (LBStrategyP2) getCandidate(serversCount int) int {
return rand.Intn(Min(serversCount, 2))
}
func (LBStrategyP2) getActiveCount(serversCount int) int {
return Min(serversCount, 2)
}
type LBStrategyPN struct{ n int }
func (s LBStrategyPN) getCandidate(serversCount int) int {
return rand.Intn(Min(serversCount, s.n))
}
func (s LBStrategyPN) getActiveCount(serversCount int) int {
return Min(serversCount, s.n)
}
type LBStrategyPH struct{}
func (LBStrategyPH) getCandidate(serversCount int) int {
return rand.Intn(Max(Min(serversCount, 2), serversCount/2))
}
func (LBStrategyPH) getActiveCount(serversCount int) int {
return Max(Min(serversCount, 2), serversCount/2)
}
type LBStrategyFirst struct{}
func (LBStrategyFirst) getCandidate(int) int {
return 0
}
func (LBStrategyFirst) getActiveCount(int) int {
return 1
}
type LBStrategyRandom struct{}
func (LBStrategyRandom) getCandidate(serversCount int) int {
return rand.Intn(serversCount)
}
func (LBStrategyRandom) getActiveCount(serversCount int) int {
return serversCount
}
var DefaultLBStrategy = LBStrategyP2{}
type DNSCryptRelay struct {
RelayUDPAddr *net.UDPAddr
RelayTCPAddr *net.TCPAddr
}
type ODoHRelay struct {
URL *url.URL
}
type Relay struct {
Proto stamps.StampProtoType
Dnscrypt *DNSCryptRelay
ODoH *ODoHRelay
}
type ServersInfo struct {
sync.RWMutex
inner []*ServerInfo
registeredServers []RegisteredServer
registeredRelays []RegisteredServer
lbStrategy LBStrategy
lbEstimator bool
}
func NewServersInfo() ServersInfo {
return ServersInfo{
lbStrategy: DefaultLBStrategy,
lbEstimator: true,
registeredServers: make([]RegisteredServer, 0),
registeredRelays: make([]RegisteredServer, 0),
}
}
func (serversInfo *ServersInfo) registerServer(name string, stamp stamps.ServerStamp) {
newRegisteredServer := RegisteredServer{name: name, stamp: stamp}
serversInfo.Lock()
defer serversInfo.Unlock()
for i, oldRegisteredServer := range serversInfo.registeredServers {
if oldRegisteredServer.name == name {
serversInfo.registeredServers[i] = newRegisteredServer
return
}
}
serversInfo.registeredServers = append(serversInfo.registeredServers, newRegisteredServer)
}
func (serversInfo *ServersInfo) registerRelay(name string, stamp stamps.ServerStamp) {
newRegisteredServer := RegisteredServer{name: name, stamp: stamp}
serversInfo.Lock()
defer serversInfo.Unlock()
for i, oldRegisteredServer := range serversInfo.registeredRelays {
if oldRegisteredServer.name == name {
serversInfo.registeredRelays[i] = newRegisteredServer
return
}
}
serversInfo.registeredRelays = append(serversInfo.registeredRelays, newRegisteredServer)
}
func (serversInfo *ServersInfo) refreshServer(proxy *Proxy, name string, stamp stamps.ServerStamp) error {
serversInfo.RLock()
isNew := true
for _, oldServer := range serversInfo.inner {
if oldServer.Name == name {
isNew = false
break
}
}
serversInfo.RUnlock()
newServer, err := fetchServerInfo(proxy, name, stamp, isNew)
if err != nil {
return err
}
if name != newServer.Name {
dlog.Fatalf("[%s] != [%s]", name, newServer.Name)
}
newServer.rtt = ewma.NewMovingAverage(RTTEwmaDecay)
newServer.rtt.Set(float64(newServer.initialRtt))
isNew = true
serversInfo.Lock()
for i, oldServer := range serversInfo.inner {
if oldServer.Name == name {
serversInfo.inner[i] = &newServer
isNew = false
break
}
}
serversInfo.Unlock()
if isNew {
serversInfo.Lock()
serversInfo.inner = append(serversInfo.inner, &newServer)
serversInfo.Unlock()
proxy.serversInfo.registerServer(name, stamp)
}
return nil
}
func (serversInfo *ServersInfo) refresh(proxy *Proxy) (int, error) {
dlog.Debug("Refreshing certificates")
serversInfo.RLock()
// Appending registeredServers slice from sources may allocate new memory.
serversCount := len(serversInfo.registeredServers)
registeredServers := make([]RegisteredServer, serversCount)
copy(registeredServers, serversInfo.registeredServers)
serversInfo.RUnlock()
countChannel := make(chan struct{}, proxy.certRefreshConcurrency)
errorChannel := make(chan error, serversCount)
for i := range registeredServers {
countChannel <- struct{}{}
go func(registeredServer *RegisteredServer) {
err := serversInfo.refreshServer(proxy, registeredServer.name, registeredServer.stamp)
if err == nil {
proxy.xTransport.internalResolverReady = true
}
errorChannel <- err
<-countChannel
}(&registeredServers[i])
}
liveServers := 0
var err error
for i := 0; i < serversCount; i++ {
err = <-errorChannel
if err == nil {
liveServers++
}
}
if liveServers > 0 {
err = nil
}
serversInfo.Lock()
sort.SliceStable(serversInfo.inner, func(i, j int) bool {
return serversInfo.inner[i].initialRtt < serversInfo.inner[j].initialRtt
})
inner := serversInfo.inner
innerLen := len(inner)
if innerLen > 1 {
dlog.Notice("Sorted latencies:")
for i := 0; i < innerLen; i++ {
dlog.Noticef("- %5dms %s", inner[i].initialRtt, inner[i].Name)
}
}
if innerLen > 0 {
dlog.Noticef("Server with the lowest initial latency: %s (rtt: %dms)", inner[0].Name, inner[0].initialRtt)
}
serversInfo.Unlock()
return liveServers, err
}
func (serversInfo *ServersInfo) estimatorUpdate(currentActive int) {
// serversInfo.RWMutex is assumed to be Locked
serversCount := len(serversInfo.inner)
activeCount := serversInfo.lbStrategy.getActiveCount(serversCount)
if activeCount == serversCount {
return
}
candidate := rand.Intn(serversCount-activeCount) + activeCount
candidateRtt, currentActiveRtt := serversInfo.inner[candidate].rtt.Value(), serversInfo.inner[currentActive].rtt.Value()
if currentActiveRtt < 0 {
currentActiveRtt = candidateRtt
serversInfo.inner[currentActive].rtt.Set(currentActiveRtt)
return
}
partialSort := false
if candidateRtt < currentActiveRtt {
serversInfo.inner[candidate], serversInfo.inner[currentActive] = serversInfo.inner[currentActive], serversInfo.inner[candidate]
dlog.Debugf(
"New preferred candidate: %s (RTT: %d vs previous: %d)",
serversInfo.inner[currentActive].Name,
int(candidateRtt),
int(currentActiveRtt),
)
partialSort = true
} else if candidateRtt > 0 && candidateRtt >= (serversInfo.inner[0].rtt.Value()+serversInfo.inner[activeCount-1].rtt.Value())/2.0*4.0 {
if time.Since(serversInfo.inner[candidate].lastActionTS) > time.Duration(1*time.Minute) {
serversInfo.inner[candidate].rtt.Add(candidateRtt / 2.0)
dlog.Debugf(
"Giving a new chance to candidate [%s], lowering its RTT from %d to %d (best: %d)",
serversInfo.inner[candidate].Name,
int(candidateRtt),
int(serversInfo.inner[candidate].rtt.Value()),
int(serversInfo.inner[0].rtt.Value()),
)
partialSort = true
}
}
if partialSort {
for i := 1; i < serversCount; i++ {
if serversInfo.inner[i-1].rtt.Value() > serversInfo.inner[i].rtt.Value() {
serversInfo.inner[i-1], serversInfo.inner[i] = serversInfo.inner[i], serversInfo.inner[i-1]
}
}
}
}
func (serversInfo *ServersInfo) getOne() *ServerInfo {
serversInfo.Lock()
serversCount := len(serversInfo.inner)
if serversCount <= 0 {
serversInfo.Unlock()
return nil
}
candidate := serversInfo.lbStrategy.getCandidate(serversCount)
if serversInfo.lbEstimator {
serversInfo.estimatorUpdate(candidate)
}
serverInfo := serversInfo.inner[candidate]
dlog.Debugf("Using candidate [%s] RTT: %d", serverInfo.Name, int(serverInfo.rtt.Value()))
serversInfo.Unlock()
return serverInfo
}
func fetchServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
if stamp.Proto == stamps.StampProtoTypeDNSCrypt {
return fetchDNSCryptServerInfo(proxy, name, stamp, isNew)
} else if stamp.Proto == stamps.StampProtoTypeDoH {
return fetchDoHServerInfo(proxy, name, stamp, isNew)
} else if stamp.Proto == stamps.StampProtoTypeODoHTarget {
return fetchODoHTargetInfo(proxy, name, stamp, isNew)
}
return ServerInfo{}, fmt.Errorf("Unsupported protocol for [%s]: [%s]", name, stamp.Proto.String())
}
func findFarthestRoute(proxy *Proxy, name string, relayStamps []stamps.ServerStamp) *stamps.ServerStamp {
serverIdx := -1
proxy.serversInfo.RLock()
for i, registeredServer := range proxy.serversInfo.registeredServers {
if registeredServer.name == name {
serverIdx = i
break
}
}
if serverIdx < 0 {
proxy.serversInfo.RUnlock()
return nil
}
server := proxy.serversInfo.registeredServers[serverIdx]
proxy.serversInfo.RUnlock()
// Fall back to random relays until the logic is implemented for non-DNSCrypt relays
if server.stamp.Proto == stamps.StampProtoTypeODoHTarget {
candidates := make([]int, 0)
for relayIdx, relayStamp := range relayStamps {
if relayStamp.Proto != stamps.StampProtoTypeODoHRelay {
continue
}
candidates = append(candidates, relayIdx)
}
return &relayStamps[candidates[rand.Intn(len(candidates))]]
} else if server.stamp.Proto != stamps.StampProtoTypeDNSCrypt {
return nil
}
// Anonymized DNSCrypt relays
serverAddrStr, _ := ExtractHostAndPort(server.stamp.ServerAddrStr, 443)
serverAddr := net.ParseIP(serverAddrStr)
if serverAddr == nil {
return nil
}
if len(proxy.serversInfo.registeredRelays) == 0 {
return nil
}
bestRelayIdxs := make([]int, 0)
bestRelaySamePrefixBits := 128
for relayIdx, relayStamp := range relayStamps {
if relayStamp.Proto != stamps.StampProtoTypeDNSCryptRelay {
continue
}
relayAddrStr, _ := ExtractHostAndPort(relayStamp.ServerAddrStr, 443)
relayAddr := net.ParseIP(relayAddrStr)
if relayAddr == nil {
continue
}
relayIsIPv6 := relayAddr.To4() == nil
if relayIsIPv6 != (serverAddr.To4() == nil) {
continue
}
firstByte := 0
if !relayIsIPv6 {
firstByte = 12
}
samePrefixBits := 0
for i := firstByte; i < 16; i++ {
x := serverAddr[i] ^ relayAddr[i]
samePrefixBits += bits.LeadingZeros8(x)
if x != 0 {
break
}
}
if samePrefixBits <= bestRelaySamePrefixBits {
bestRelaySamePrefixBits = samePrefixBits
bestRelayIdxs = append(bestRelayIdxs, relayIdx)
}
}
return &relayStamps[bestRelayIdxs[rand.Intn(len(bestRelayIdxs))]]
}
func relayProtoForServerProto(proto stamps.StampProtoType) (stamps.StampProtoType, error) {
switch proto {
case stamps.StampProtoTypeDNSCrypt:
return stamps.StampProtoTypeDNSCryptRelay, nil
case stamps.StampProtoTypeODoHTarget:
return stamps.StampProtoTypeODoHRelay, nil
default:
return 0, errors.New("protocol cannot be anonymized")
}
}
func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay, error) {
routes := proxy.routes
if routes == nil {
return nil, nil
}
wildcard := false
relayNames, ok := (*routes)[name]
if !ok {
wildcard = true
relayNames, ok = (*routes)["*"]
}
if !ok || len(relayNames) == 0 {
return nil, nil
}
relayProto, err := relayProtoForServerProto(serverProto)
if err != nil {
dlog.Errorf("Server [%v]'s protocol doesn't support anonymization", name)
return nil, nil
}
relayStamps := make([]stamps.ServerStamp, 0)
relayStampToName := make(map[string]string)
for _, relayName := range relayNames {
if relayStamp, err := stamps.NewServerStampFromString(relayName); err == nil {
if relayStamp.Proto == relayProto {
relayStamps = append(relayStamps, relayStamp)
relayStampToName[relayStamp.String()] = relayName
}
} else if relayName == "*" {
proxy.serversInfo.RLock()
for _, registeredServer := range proxy.serversInfo.registeredRelays {
if registeredServer.stamp.Proto == relayProto {
relayStamps = append(relayStamps, registeredServer.stamp)
relayStampToName[registeredServer.stamp.String()] = registeredServer.name
}
}
proxy.serversInfo.RUnlock()
wildcard = true
break
} else {
proxy.serversInfo.RLock()
for _, registeredServer := range proxy.serversInfo.registeredRelays {
if registeredServer.name == relayName && registeredServer.stamp.Proto == relayProto {
relayStamps = append(relayStamps, registeredServer.stamp)
relayStampToName[registeredServer.stamp.String()] = relayName
break
}
}
proxy.serversInfo.RUnlock()
}
}
if len(relayStamps) == 0 {
err := fmt.Errorf("Non-existent relay set for server [%v]", name)
return nil, err
}
var relayCandidateStamp *stamps.ServerStamp
if !wildcard || len(relayStamps) == 1 {
relayCandidateStamp = &relayStamps[rand.Intn(len(relayStamps))]
} else {
relayCandidateStamp = findFarthestRoute(proxy, name, relayStamps)
}
if relayCandidateStamp == nil {
return nil, fmt.Errorf("No valid relay for server [%v]", name)
}
relayName := relayStampToName[relayCandidateStamp.String()]
switch relayCandidateStamp.Proto {
case stamps.StampProtoTypeDNSCrypt, stamps.StampProtoTypeDNSCryptRelay:
relayUDPAddr, err := net.ResolveUDPAddr("udp", relayCandidateStamp.ServerAddrStr)
if err != nil {
return nil, err
}
relayTCPAddr, err := net.ResolveTCPAddr("tcp", relayCandidateStamp.ServerAddrStr)
if err != nil {
return nil, err
}
dlog.Noticef("Anonymizing queries for [%v] via [%v]", name, relayName)
return &Relay{
Proto: stamps.StampProtoTypeDNSCryptRelay,
Dnscrypt: &DNSCryptRelay{RelayUDPAddr: relayUDPAddr, RelayTCPAddr: relayTCPAddr},
}, nil
case stamps.StampProtoTypeODoHRelay:
relayBaseURL, err := url.Parse(
"https://" + url.PathEscape(relayCandidateStamp.ProviderName) + relayCandidateStamp.Path,
)
if err != nil {
return nil, err
}
var relayURLforTarget *url.URL
for _, server := range proxy.registeredServers {
if server.name != name || server.stamp.Proto != stamps.StampProtoTypeODoHTarget {
continue
}
qs := relayBaseURL.Query()
qs.Add("targethost", server.stamp.ProviderName)
qs.Add("targetpath", server.stamp.Path)
tmp := *relayBaseURL
tmp.RawQuery = qs.Encode()
relayURLforTarget = &tmp
break
}
if relayURLforTarget == nil {
return nil, fmt.Errorf("Relay [%v] not found", relayName)
}
if len(relayCandidateStamp.ServerAddrStr) > 0 {
ipOnly, _ := ExtractHostAndPort(relayCandidateStamp.ServerAddrStr, -1)
if ip := ParseIP(ipOnly); ip != nil {
host, _ := ExtractHostAndPort(relayCandidateStamp.ProviderName, -1)
proxy.xTransport.saveCachedIP(host, ip, -1*time.Second)
}
}
dlog.Noticef("Anonymizing queries for [%v] via [%v]", name, relayName)
return &Relay{Proto: stamps.StampProtoTypeODoHRelay, ODoH: &ODoHRelay{
URL: relayURLforTarget,
}}, nil
}
return nil, fmt.Errorf("Invalid relay set for server [%v]", name)
}
func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
if len(stamp.ServerPk) != ed25519.PublicKeySize {
serverPk, err := hex.DecodeString(strings.ReplaceAll(string(stamp.ServerPk), ":", ""))
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
}
knownBugs := ServerBugs{}
for _, buggyServerName := range proxy.serversBlockingFragments {
if buggyServerName == name {
knownBugs.fragmentsBlocked = true
dlog.Infof("Known bug in [%v]: fragmented questions over UDP are blocked", name)
break
}
}
relay, err := route(proxy, name, stamp.Proto)
if err != nil {
return ServerInfo{}, err
}
var dnscryptRelay *DNSCryptRelay
if relay != nil {
dnscryptRelay = relay.Dnscrypt
}
certInfo, rtt, fragmentsBlocked, err := FetchCurrentDNSCryptCert(
proxy,
&name,
proxy.mainProto,
stamp.ServerPk,
stamp.ServerAddrStr,
stamp.ProviderName,
isNew,
dnscryptRelay,
knownBugs,
)
if !knownBugs.fragmentsBlocked && fragmentsBlocked {
dlog.Debugf("[%v] drops fragmented queries", name)
knownBugs.fragmentsBlocked = true
}
if knownBugs.fragmentsBlocked && relay != nil && relay.Dnscrypt != nil {
relay = nil
if proxy.skipAnonIncompatibleResolvers {
dlog.Infof("[%v] couldn't be reached anonymously, it will be ignored", name)
return ServerInfo{}, errors.New("Resolver couldn't be reached anonymously")
}
dlog.Warnf("[%v] couldn't be reached anonymously", name)
}
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: stamps.StampProtoTypeDNSCrypt,
MagicQuery: certInfo.MagicQuery,
ServerPk: certInfo.ServerPk,
SharedKey: certInfo.SharedKey,
CryptoConstruction: certInfo.CryptoConstruction,
Name: name,
Timeout: proxy.timeout,
UDPAddr: remoteUDPAddr,
TCPAddr: remoteTCPAddr,
Relay: relay,
initialRtt: rtt,
knownBugs: knownBugs,
}, nil
}
func dohTestPacket(msgID uint16) []byte {
msg := dns.Msg{}
msg.SetQuestion(".", dns.TypeNS)
msg.Id = msgID
msg.MsgHdr.RecursionDesired = true
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
ext := new(dns.EDNS0_PADDING)
ext.Padding = make([]byte, 16)
_, _ = crypto_rand.Read(ext.Padding)
edns0 := msg.IsEdns0()
edns0.Option = append(edns0.Option, ext)
body, err := msg.Pack()
if err != nil {
dlog.Fatal(err)
}
return body
}
func dohNXTestPacket(msgID uint16) []byte {
msg := dns.Msg{}
qName := make([]byte, 16)
charset := "abcdefghijklmnopqrstuvwxyz"
for i := range qName {
qName[i] = charset[rand.Intn(len(charset))]
}
msg.SetQuestion(string(qName)+".test.dnscrypt.", dns.TypeNS)
msg.Id = msgID
msg.MsgHdr.RecursionDesired = true
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
ext := new(dns.EDNS0_PADDING)
ext.Padding = make([]byte, 16)
_, _ = crypto_rand.Read(ext.Padding)
edns0 := msg.IsEdns0()
edns0.Option = append(edns0.Option, ext)
body, err := msg.Pack()
if err != nil {
dlog.Fatal(err)
}
return body
}
func fetchDoHServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
// If an IP has been provided, use it forever.
// Or else, if the fallback server and the DoH server are operated
// by the same entity, it could provide a unique IPv6 for each client
// in order to fingerprint clients across multiple IP addresses.
if len(stamp.ServerAddrStr) > 0 {
ipOnly, _ := ExtractHostAndPort(stamp.ServerAddrStr, -1)
if ip := ParseIP(ipOnly); ip != nil {
host, _ := ExtractHostAndPort(stamp.ProviderName, -1)
proxy.xTransport.saveCachedIP(host, ip, -1*time.Second)
}
}
url := &url.URL{
Scheme: "https",
Host: stamp.ProviderName,
Path: stamp.Path,
}
body := dohTestPacket(0xcafe)
useGet := false
if _, _, _, _, err := proxy.xTransport.DoHQuery(useGet, url, body, proxy.timeout); err != nil {
useGet = true
if _, _, _, _, err := proxy.xTransport.DoHQuery(useGet, url, body, proxy.timeout); err != nil {
return ServerInfo{}, err
}
dlog.Debugf("Server [%s] doesn't appear to support POST; falling back to GET requests", name)
}
body = dohNXTestPacket(0xcafe)
serverResponse, _, tls, rtt, err := proxy.xTransport.DoHQuery(useGet, url, body, proxy.timeout)
if err != nil {
dlog.Infof("[%s] [%s]: %v", name, url, err)
return ServerInfo{}, err
}
if tls == nil || !tls.HandshakeComplete {
return ServerInfo{}, errors.New("TLS handshake failed")
}
msg := dns.Msg{}
if err := msg.Unpack(serverResponse); err != nil {
dlog.Warnf("[%s]: %v", name, err)
return ServerInfo{}, err
}
if msg.Rcode != dns.RcodeNameError {
dlog.Criticalf("[%s] may be a lying resolver", name)
}
protocol := tls.NegotiatedProtocol
if len(protocol) == 0 {
protocol = "http/1.x"
}
if strings.HasPrefix(protocol, "http/1.") {
dlog.Warnf("[%s] does not support HTTP/2 nor HTTP/3", name)
}
dlog.Infof("[%s] TLS version: %x - Protocol: %v - Cipher suite: %v", name, tls.Version, protocol, tls.CipherSuite)
showCerts := proxy.showCerts
found := false
var wantedHash [32]byte
for _, cert := range tls.PeerCertificates {
h := sha256.Sum256(cert.RawTBSCertificate)
if showCerts {
dlog.Noticef("Advertised cert: [%s] [%x]", cert.Subject, h)
} else {
dlog.Debugf("Advertised cert: [%s] [%x]", cert.Subject, h)
}
for _, hash := range stamp.Hashes {
if len(hash) == len(wantedHash) {
copy(wantedHash[:], hash)
if h == wantedHash {
found = true
break
}
}
}
if found {
break
}
}
if !found && len(stamp.Hashes) > 0 {
dlog.Criticalf("[%s] Certificate hash [%x] not found", name, wantedHash)
return ServerInfo{}, fmt.Errorf("Certificate hash not found")
}
if len(serverResponse) < MinDNSPacketSize || len(serverResponse) > MaxDNSPacketSize ||
serverResponse[0] != 0xca || serverResponse[1] != 0xfe || serverResponse[4] != 0x00 || serverResponse[5] != 0x01 {
dlog.Info("Webserver returned an unexpected response")
return ServerInfo{}, errors.New("Webserver returned an unexpected response")
}
xrtt := int(rtt.Nanoseconds() / 1000000)
if isNew {
dlog.Noticef("[%s] OK (DoH) - rtt: %dms", name, xrtt)
} else {
dlog.Infof("[%s] OK (DoH) - rtt: %dms", name, xrtt)
}
return ServerInfo{
Proto: stamps.StampProtoTypeDoH,
Name: name,
Timeout: proxy.timeout,
URL: url,
HostName: stamp.ProviderName,
initialRtt: xrtt,
useGet: useGet,
}, nil
}
func fetchTargetConfigsFromWellKnown(proxy *Proxy, url *url.URL) ([]ODoHTargetConfig, error) {
bin, statusCode, _, _, err := proxy.xTransport.Get(url, "application/binary", 0)
if err != nil {
return nil, err
}
if statusCode < 200 || statusCode >= 300 {
return nil, fmt.Errorf("HTTP status code was %v", statusCode)
}
return parseODoHTargetConfigs(bin)
}
func _fetchODoHTargetInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
configURL := &url.URL{Scheme: "https", Host: stamp.ProviderName, Path: "/.well-known/odohconfigs"}
odohTargetConfigs, err := fetchTargetConfigsFromWellKnown(proxy, configURL)
if err != nil {
dlog.Debug(configURL)
return ServerInfo{}, fmt.Errorf("[%s] didn't return an ODoH configuration - [%v]", name, err)
} else if len(odohTargetConfigs) == 0 {
dlog.Debug(configURL)
return ServerInfo{}, fmt.Errorf("[%s] has an empty ODoH configuration", name)
}
relay, err := route(proxy, name, stamp.Proto)
if err != nil {
return ServerInfo{}, err
}
if relay == nil {
dlog.Criticalf(
"No relay defined for [%v] - Configuring a relay is required for ODoH servers (see the `[anonymized_dns]` section)",
name,
)
return ServerInfo{}, errors.New("No ODoH relay")
} else {
if relay.ODoH == nil {
dlog.Criticalf("Wrong relay type defined for [%v] - ODoH servers require an ODoH relay", name)
return ServerInfo{}, errors.New("Wrong ODoH relay type")
}
}
dlog.Debugf("Pausing after ODoH configuration retrieval")
delay := time.Duration(rand.Intn(5*1000)) * time.Millisecond
clocksmith.Sleep(time.Duration(delay))
dlog.Debugf("Pausing done")
targetURL := &url.URL{
Scheme: "https",
Host: stamp.ProviderName,
Path: stamp.Path,
}
workingConfigs := make([]ODoHTargetConfig, 0)
rand.Shuffle(len(odohTargetConfigs), func(i, j int) {
odohTargetConfigs[i], odohTargetConfigs[j] = odohTargetConfigs[j], odohTargetConfigs[i]
})
for _, odohTargetConfig := range odohTargetConfigs {
url := relay.ODoH.URL
query := dohTestPacket(0xcafe)
odohQuery, err := odohTargetConfig.encryptQuery(query)
if err != nil {
continue
}
useGet := false
if _, _, _, _, err := proxy.xTransport.ObliviousDoHQuery(useGet, url, odohQuery.odohMessage, proxy.timeout); err != nil {
useGet = true
if _, _, _, _, err := proxy.xTransport.ObliviousDoHQuery(useGet, url, odohQuery.odohMessage, proxy.timeout); err != nil {
continue
}
dlog.Debugf("Server [%s] doesn't appear to support POST; falling back to GET requests", name)
}
query = dohNXTestPacket(0xcafe)
odohQuery, err = odohTargetConfig.encryptQuery(query)
if err != nil {
continue
}
responseBody, responseCode, tls, rtt, err := proxy.xTransport.ObliviousDoHQuery(
useGet,
url,
odohQuery.odohMessage,
proxy.timeout,
)
if err != nil {
continue
}
if responseCode == 401 {
return ServerInfo{}, fmt.Errorf("Configuration changed during a probe")
}
serverResponse, err := odohQuery.decryptResponse(responseBody)
if err != nil {
dlog.Warnf("Unable to decrypt response from [%v]: [%v]", name, err)
continue
}
workingConfigs = append(workingConfigs, odohTargetConfig)
msg := dns.Msg{}
if err := msg.Unpack(serverResponse); err != nil {
dlog.Warnf("[%s]: %v", name, err)
return ServerInfo{}, err
}
if msg.Rcode != dns.RcodeNameError {
dlog.Criticalf("[%s] may be a lying resolver", name)
}
protocol := "http"
tlsVersion := uint16(0)
tlsCipherSuite := uint16(0)
if tls != nil {
protocol = tls.NegotiatedProtocol
if len(protocol) == 0 {
protocol = "http/1.x"
} else {
tlsVersion = tls.Version
tlsCipherSuite = tls.CipherSuite
}
}
if strings.HasPrefix(protocol, "http/1.") {
dlog.Warnf("[%s] does not support HTTP/2", name)
}
dlog.Infof(
"[%s] TLS version: %x - Protocol: %v - Cipher suite: %v",
name,
tlsVersion,
protocol,
tlsCipherSuite,
)
showCerts := proxy.showCerts
found := false
var wantedHash [32]byte
if tls != nil {
for _, cert := range tls.PeerCertificates {
h := sha256.Sum256(cert.RawTBSCertificate)
if showCerts {
dlog.Noticef("Advertised relay cert: [%s] [%x]", cert.Subject, h)
} else {
dlog.Debugf("Advertised relay cert: [%s] [%x]", cert.Subject, h)
}
for _, hash := range stamp.Hashes {
if len(hash) == len(wantedHash) {
copy(wantedHash[:], hash)
if h == wantedHash {
found = true
break
}
}
}
if found {
break
}
}
if !found && len(stamp.Hashes) > 0 {
dlog.Criticalf("[%s] Certificate hash [%x] not found", name, wantedHash)
return ServerInfo{}, fmt.Errorf("Certificate hash not found")
}
}
if len(serverResponse) < MinDNSPacketSize || len(serverResponse) > MaxDNSPacketSize ||
serverResponse[0] != 0xca || serverResponse[1] != 0xfe || serverResponse[4] != 0x00 || serverResponse[5] != 0x01 {
dlog.Info("Webserver returned an unexpected response")
return ServerInfo{}, errors.New("Webserver returned an unexpected response")
}
xrtt := int(rtt.Nanoseconds() / 1000000)
if isNew {
dlog.Noticef("[%s] OK (ODoH) - rtt: %dms", name, xrtt)
} else {
dlog.Infof("[%s] OK (ODoH) - rtt: %dms", name, xrtt)
}
return ServerInfo{
Proto: stamps.StampProtoTypeODoHTarget,
Name: name,
Timeout: proxy.timeout,
URL: targetURL,
HostName: stamp.ProviderName,
initialRtt: xrtt,
useGet: useGet,
Relay: relay,
odohTargetConfigs: workingConfigs,
}, nil
}
return ServerInfo{}, fmt.Errorf("No valid network configuration for [%v]", name)
}
func fetchODoHTargetInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
var err error
var serverInfo ServerInfo
for i := 0; i < 3; i += 1 {
serverInfo, err = _fetchODoHTargetInfo(proxy, name, stamp, isNew)
if err == nil {
break
}
dlog.Infof("Trying to fetch the [%v] configuration again", name)
}
return serverInfo, err
}
func (serverInfo *ServerInfo) noticeFailure(proxy *Proxy) {
proxy.serversInfo.Lock()
serverInfo.rtt.Add(float64(proxy.timeout.Nanoseconds() / 1000000))
proxy.serversInfo.Unlock()
}
func (serverInfo *ServerInfo) noticeBegin(proxy *Proxy) {
proxy.serversInfo.Lock()
serverInfo.lastActionTS = time.Now()
proxy.serversInfo.Unlock()
}
func (serverInfo *ServerInfo) noticeSuccess(proxy *Proxy) {
now := time.Now()
proxy.serversInfo.Lock()
elapsed := now.Sub(serverInfo.lastActionTS)
elapsedMs := elapsed.Nanoseconds() / 1000000
if elapsedMs > 0 && elapsed < proxy.timeout {
serverInfo.rtt.Add(float64(elapsedMs))
}
proxy.serversInfo.Unlock()
}