mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Some users mentioned that when there is an Internet outage, ctrld fails to recover, crashing or locks up the router. When requests start failing, this results in the clients emitting more queries, creating a resource spiral of death that can brick the device entirely. To guard against this case, this commit implement an upstream monitor approach: - Marking upstream as down after 100 consecutive failed queries. - Start a goroutine to check when the upstream is back again. - When upstream is down, answer all queries with SERVFAIL. - The checking process uses backoff retry to reduce high requests rate. - As long as the query succeeded, marking the upstream as alive then start operate normally.
119 lines
2.9 KiB
Go
119 lines
2.9 KiB
Go
package ctrld
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
const (
|
|
dohMacHeader = "x-cd-mac"
|
|
dohIPHeader = "x-cd-ip"
|
|
dohHostHeader = "x-cd-host"
|
|
headerApplicationDNS = "application/dns-message"
|
|
)
|
|
|
|
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
|
r := &dohResolver{
|
|
endpoint: uc.u,
|
|
isDoH3: uc.Type == ResolverTypeDOH3,
|
|
http3RoundTripper: uc.http3RoundTripper,
|
|
sendClientInfo: uc.UpstreamSendClientInfo(),
|
|
uc: uc,
|
|
}
|
|
return r
|
|
}
|
|
|
|
type dohResolver struct {
|
|
uc *UpstreamConfig
|
|
endpoint *url.URL
|
|
isDoH3 bool
|
|
http3RoundTripper http.RoundTripper
|
|
sendClientInfo bool
|
|
}
|
|
|
|
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
|
data, err := msg.Pack()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
enc := base64.RawURLEncoding.EncodeToString(data)
|
|
query := r.endpoint.Query()
|
|
query.Add("dns", enc)
|
|
|
|
endpoint := *r.endpoint
|
|
endpoint.RawQuery = query.Encode()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create request: %w", err)
|
|
}
|
|
addHeader(ctx, req, r.sendClientInfo)
|
|
dnsTyp := uint16(0)
|
|
if len(msg.Question) > 0 {
|
|
dnsTyp = msg.Question[0].Qtype
|
|
}
|
|
c := http.Client{Transport: r.uc.dohTransport(dnsTyp)}
|
|
if r.isDoH3 {
|
|
transport := r.uc.doh3Transport(dnsTyp)
|
|
if transport == nil {
|
|
return nil, errors.New("DoH3 is not supported")
|
|
}
|
|
c.Transport = transport
|
|
}
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
if r.isDoH3 {
|
|
if closer, ok := c.Transport.(io.Closer); ok {
|
|
closer.Close()
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("could not perform request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not read message from response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("wrong response from DOH server, got: %s, status: %d", string(buf), resp.StatusCode)
|
|
}
|
|
|
|
answer := new(dns.Msg)
|
|
if err := answer.Unpack(buf); err != nil {
|
|
return nil, fmt.Errorf("answer.Unpack: %w", err)
|
|
}
|
|
return answer, nil
|
|
}
|
|
|
|
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
|
req.Header.Set("Content-Type", headerApplicationDNS)
|
|
req.Header.Set("Accept", headerApplicationDNS)
|
|
printed := false
|
|
if sendClientInfo {
|
|
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
|
printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
|
|
if ci.Mac != "" {
|
|
req.Header.Set(dohMacHeader, ci.Mac)
|
|
}
|
|
if ci.IP != "" {
|
|
req.Header.Set(dohIPHeader, ci.IP)
|
|
}
|
|
if ci.Hostname != "" {
|
|
req.Header.Set(dohHostHeader, ci.Hostname)
|
|
}
|
|
}
|
|
}
|
|
if printed {
|
|
Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header")
|
|
}
|
|
}
|