dnscrypt-proxy/dnscrypt-proxy/serversInfo.go

511 lines
15 KiB
Go
Raw Normal View History

package main
import (
crypto_rand "crypto/rand"
2018-01-27 17:48:53 +01:00
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/rand"
"net"
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/VividCortex/ewma"
2018-01-11 11:50:54 +01:00
"github.com/jedisct1/dlog"
2018-04-18 18:58:39 +02:00
stamps "github.com/jedisct1/go-dnsstamps"
"github.com/miekg/dns"
"golang.org/x/crypto/ed25519"
)
const (
RTTEwmaDecay = 10.0
)
type RegisteredServer struct {
name string
2018-04-14 15:03:21 +02:00
stamp stamps.ServerStamp
description string
}
type ServerBugs struct {
fragmentsBlocked bool
}
type DOHClientCreds struct {
clientCert string
clientKey string
}
type ServerInfo struct {
2018-04-14 15:03:21 +02:00
Proto stamps.StampProtoType
MagicQuery [8]byte
ServerPk [32]byte
SharedKey [32]byte
CryptoConstruction CryptoConstruction
Name string
Timeout time.Duration
URL *url.URL
HostName string
UDPAddr *net.UDPAddr
TCPAddr *net.TCPAddr
2019-10-14 01:45:38 +02:00
RelayUDPAddr *net.UDPAddr
RelayTCPAddr *net.TCPAddr
knownBugs ServerBugs
lastActionTS time.Time
rtt ewma.MovingAverage
initialRtt int
useGet bool
DOHClientCreds DOHClientCreds
}
type LBStrategy interface {
getCandidate(serversCount int) int
}
type LBStrategyP2 struct{}
func (LBStrategyP2) getCandidate(serversCount int) int {
return rand.Intn(Min(serversCount, 2))
}
2020-03-20 17:48:54 +01:00
type LBStrategyPN struct{ n int }
func (s LBStrategyPN) getCandidate(serversCount int) int {
return rand.Intn(Min(serversCount, s.n))
}
type LBStrategyPH struct{}
func (LBStrategyPH) getCandidate(serversCount int) int {
return rand.Intn(Max(Min(serversCount, 2), serversCount/2))
}
type LBStrategyFirst struct{}
func (LBStrategyFirst) getCandidate(int) int {
return 0
}
type LBStrategyRandom struct{}
func (LBStrategyRandom) getCandidate(serversCount int) int {
return rand.Intn(serversCount)
}
var DefaultLBStrategy = LBStrategyP2{}
type ServersInfo struct {
sync.RWMutex
inner []*ServerInfo
registeredServers []RegisteredServer
lbStrategy LBStrategy
lbEstimator bool
}
func NewServersInfo() ServersInfo {
return ServersInfo{lbStrategy: DefaultLBStrategy, lbEstimator: true, registeredServers: 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)
}
2018-04-14 15:03:21 +02:00
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
}
}
if isNew {
serversInfo.inner = append(serversInfo.inner, &newServer)
serversInfo.registeredServers = append(serversInfo.registeredServers, RegisteredServer{name: name, stamp: stamp})
}
serversInfo.Unlock()
return nil
}
func (serversInfo *ServersInfo) refresh(proxy *Proxy) (int, error) {
dlog.Debug("Refreshing certificates")
serversInfo.RLock()
registeredServers := serversInfo.registeredServers
serversInfo.RUnlock()
liveServers := 0
var err error
for _, registeredServer := range registeredServers {
if err = serversInfo.refreshServer(proxy, registeredServer.name, registeredServer.stamp); err == nil {
liveServers++
}
}
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() {
// serversInfo.RWMutex is assumed to be Locked
candidate := rand.Intn(len(serversInfo.inner))
if candidate == 0 {
return
}
candidateRtt, currentBestRtt := serversInfo.inner[candidate].rtt.Value(), serversInfo.inner[0].rtt.Value()
if currentBestRtt < 0 {
currentBestRtt = candidateRtt
serversInfo.inner[0].rtt.Set(currentBestRtt)
}
partialSort := false
if candidateRtt < currentBestRtt {
serversInfo.inner[candidate], serversInfo.inner[0] = serversInfo.inner[0], serversInfo.inner[candidate]
partialSort = true
2019-06-03 17:10:38 +02:00
dlog.Debugf("New preferred candidate: %v (rtt: %d vs previous: %d)", serversInfo.inner[0].Name, int(candidateRtt), int(currentBestRtt))
} else if candidateRtt > 0 && candidateRtt >= currentBestRtt*4.0 {
if time.Since(serversInfo.inner[candidate].lastActionTS) > time.Duration(1*time.Minute) {
serversInfo.inner[candidate].rtt.Add(MinF(MaxF(candidateRtt/2.0, currentBestRtt*2.0), candidateRtt))
2019-06-03 17:35:16 +02:00
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(currentBestRtt))
partialSort = true
}
}
if partialSort {
serversCount := len(serversInfo.inner)
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 {
2019-11-05 00:37:46 +01:00
serversInfo.Unlock()
return nil
}
if serversInfo.lbEstimator {
serversInfo.estimatorUpdate()
}
candidate := serversInfo.lbStrategy.getCandidate(serversCount)
serverInfo := serversInfo.inner[candidate]
2019-06-03 17:32:54 +02:00
dlog.Debugf("Using candidate [%s] RTT: %d", (*serverInfo).Name, int((*serverInfo).rtt.Value()))
2019-11-05 00:37:46 +01:00
serversInfo.Unlock()
return serverInfo
}
func fetchServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
2018-04-14 15:03:21 +02:00
if stamp.Proto == stamps.StampProtoTypeDNSCrypt {
return fetchDNSCryptServerInfo(proxy, name, stamp, isNew)
2018-04-14 15:03:21 +02:00
} else if stamp.Proto == stamps.StampProtoTypeDoH {
return fetchDoHServerInfo(proxy, name, stamp, isNew)
}
return ServerInfo{}, errors.New("Unsupported protocol")
}
func route(proxy *Proxy, name string) (*net.UDPAddr, *net.TCPAddr, error) {
routes := proxy.routes
if routes == nil {
return nil, nil, nil
}
relayNames, ok := (*routes)[name]
if !ok {
relayNames, ok = (*routes)["*"]
}
if !ok {
return nil, nil, nil
}
var relayName string
if len(relayNames) > 0 {
candidate := rand.Intn(len(relayNames))
relayName = relayNames[candidate]
}
var relayCandidateStamp *stamps.ServerStamp
if len(relayName) == 0 {
return nil, nil, fmt.Errorf("Route declared for [%v] but an empty relay list", name)
} else if relayStamp, err := stamps.NewServerStampFromString(relayName); err == nil {
relayCandidateStamp = &relayStamp
} else if _, err := net.ResolveUDPAddr("udp", relayName); err == nil {
relayCandidateStamp = &stamps.ServerStamp{
ServerAddrStr: relayName,
Proto: stamps.StampProtoTypeDNSCryptRelay,
}
} else {
for _, registeredServer := range proxy.registeredRelays {
if registeredServer.name == relayName {
relayCandidateStamp = &registeredServer.stamp
break
}
}
for _, registeredServer := range proxy.registeredServers {
if registeredServer.name == relayName {
relayCandidateStamp = &registeredServer.stamp
break
}
}
}
if relayCandidateStamp == nil {
return nil, nil, fmt.Errorf("Undefined relay [%v] for server [%v]", relayName, name)
}
if relayCandidateStamp.Proto == stamps.StampProtoTypeDNSCrypt ||
relayCandidateStamp.Proto == stamps.StampProtoTypeDNSCryptRelay {
relayUDPAddr, err := net.ResolveUDPAddr("udp", relayCandidateStamp.ServerAddrStr)
if err != nil {
return nil, nil, err
}
relayTCPAddr, err := net.ResolveTCPAddr("tcp", relayCandidateStamp.ServerAddrStr)
if err != nil {
return nil, nil, err
}
return relayUDPAddr, relayTCPAddr, nil
}
return nil, nil, fmt.Errorf("Invalid relay [%v] for server [%v]", relayName, name)
}
func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
2018-04-14 15:03:21 +02:00
if len(stamp.ServerPk) != ed25519.PublicKeySize {
serverPk, err := hex.DecodeString(strings.Replace(string(stamp.ServerPk), ":", "", -1))
if err != nil || len(serverPk) != ed25519.PublicKeySize {
2018-04-14 15:03:21 +02:00
dlog.Fatalf("Unsupported public key for [%s]: [%s]", name, stamp.ServerPk)
}
2018-04-14 15:03:21 +02:00
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
}
}
relayUDPAddr, relayTCPAddr, err := route(proxy, name)
if err != nil {
return ServerInfo{}, err
2019-10-14 01:45:38 +02:00
}
certInfo, rtt, fragmentsBlocked, err := FetchCurrentDNSCryptCert(proxy, &name, proxy.mainProto, stamp.ServerPk, stamp.ServerAddrStr, stamp.ProviderName, isNew, relayUDPAddr, relayTCPAddr, knownBugs)
if !knownBugs.fragmentsBlocked && fragmentsBlocked {
dlog.Debugf("[%v] drops fragmented queries", name)
knownBugs.fragmentsBlocked = true
}
if knownBugs.fragmentsBlocked && (relayUDPAddr != nil || relayTCPAddr != nil) {
dlog.Warnf("[%v] is incompatible with anonymization", name)
relayTCPAddr, relayUDPAddr = nil, nil
if proxy.skipAnonIncompatbibleResolvers {
return ServerInfo{}, errors.New("Resolver is incompatible with anonymization")
}
}
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{
2018-04-14 15:03:21 +02:00
Proto: stamps.StampProtoTypeDNSCrypt,
MagicQuery: certInfo.MagicQuery,
ServerPk: certInfo.ServerPk,
SharedKey: certInfo.SharedKey,
CryptoConstruction: certInfo.CryptoConstruction,
Name: name,
Timeout: proxy.timeout,
UDPAddr: remoteUDPAddr,
TCPAddr: remoteTCPAddr,
2019-10-14 01:45:38 +02:00
RelayUDPAddr: relayUDPAddr,
RelayTCPAddr: relayTCPAddr,
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 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.
2018-04-14 15:03:21 +02:00
if len(stamp.ServerAddrStr) > 0 {
ipOnly, _ := ExtractHostAndPort(stamp.ServerAddrStr, -1)
if ip := ParseIP(ipOnly); ip != nil {
proxy.xTransport.saveCachedIP(stamp.ProviderName, ip, -1*time.Second)
}
}
url := &url.URL{
Scheme: "https",
2018-04-14 15:03:21 +02:00
Host: stamp.ProviderName,
Path: stamp.Path,
}
body := dohTestPacket(0xcafe)
dohClientCreds, ok := (*proxy.dohCreds)[name]
if !ok {
dohClientCreds, ok = (*proxy.dohCreds)["*"]
}
if ok {
dlog.Noticef("Enabling TLS authentication for [%s]", name)
proxy.xTransport.tlsClientCreds = dohClientCreds
proxy.xTransport.rebuildTransport()
}
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)
}
serverResponse, tls, rtt, err := proxy.xTransport.DoHQuery(useGet, url, body, proxy.timeout)
if err != nil {
return ServerInfo{}, err
}
2018-01-27 17:48:53 +01:00
if tls == nil || !tls.HandshakeComplete {
return ServerInfo{}, errors.New("TLS handshake failed")
}
protocol := tls.NegotiatedProtocol
if len(protocol) == 0 {
protocol = "h1"
dlog.Warnf("[%s] does not support HTTP/2", name)
}
dlog.Infof("[%s] TLS version: %x - Protocol: %v - Cipher suite: %v", name, tls.Version, protocol, tls.CipherSuite)
showCerts := proxy.showCerts
2018-01-27 17:48:53 +01:00
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)
}
2018-04-14 15:03:21 +02:00
for _, hash := range stamp.Hashes {
if len(hash) == len(wantedHash) {
copy(wantedHash[:], hash)
if h == wantedHash {
found = true
break
}
}
}
if found {
2018-01-27 17:48:53 +01:00
break
}
}
2018-04-14 15:03:21 +02:00
if !found && len(stamp.Hashes) > 0 {
2018-01-27 17:48:53 +01:00
return ServerInfo{}, fmt.Errorf("Certificate hash [%x] not found for [%s]", wantedHash, name)
}
respBody := serverResponse
if err != nil {
return ServerInfo{}, err
}
2018-01-27 16:59:45 +01:00
if len(respBody) < MinDNSPacketSize || len(respBody) > MaxDNSPacketSize ||
respBody[0] != 0xca || respBody[1] != 0xfe || respBody[4] != 0x00 || respBody[5] != 0x01 {
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)
}
2018-03-17 22:47:17 +01:00
return ServerInfo{
2018-04-14 15:03:21 +02:00
Proto: stamps.StampProtoTypeDoH,
Name: name,
Timeout: proxy.timeout,
URL: url,
2018-04-14 15:03:21 +02:00
HostName: stamp.ProviderName,
initialRtt: xrtt,
useGet: useGet,
2018-03-17 22:47:17 +01:00
}, nil
}
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()
2018-01-10 16:01:29 +01:00
}