Add max upstream connections dns-proxy option

Allows defining a limit to the number of connections that can be
established with the upstream DNS host.

If left unset, there may be situations where connections fail to
establish, which causes the Transport to create an influx of connections
causing upstream to throttle our requests and triggering a runaway
effect resulting in high CPU usage. See https://github.com/cloudflare/cloudflared/issues/91
This commit is contained in:
dvejmz 2021-01-09 17:25:48 +00:00
parent a34604cfc7
commit 43a1e317f3
5 changed files with 40 additions and 14 deletions

View File

@ -11,8 +11,9 @@ const (
// ResolverServiceType is used to identify what kind of overwatch service this is // ResolverServiceType is used to identify what kind of overwatch service this is
ResolverServiceType = "resolver" ResolverServiceType = "resolver"
LogFieldResolverAddress = "resolverAddress" LogFieldResolverAddress = "resolverAddress"
LogFieldResolverPort = "resolverPort" LogFieldResolverPort = "resolverPort"
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
) )
// ResolverService is used to wrap the tunneldns package's DNS over HTTP // ResolverService is used to wrap the tunneldns package's DNS over HTTP
@ -57,7 +58,7 @@ func (s *ResolverService) Shutdown() {
func (s *ResolverService) Run() error { func (s *ResolverService) Run() error {
// create a listener // create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(), l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.log) s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
if err != nil { if err != nil {
return err return err
} }
@ -74,6 +75,7 @@ func (s *ResolverService) Run() error {
resolverLog := s.log.With(). resolverLog := s.log.With().
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()). Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()). Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
Logger() Logger()
resolverLog.Info().Msg("Starting resolver") resolverLog.Info().Msg("Starting resolver")

View File

@ -30,6 +30,7 @@ type DNSResolver struct {
Port uint16 `json:"port,omitempty"` Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"` Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"` Bootstraps []string `json:"bootstraps,omitempty"`
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
} }
// Root is the base options to configure the service // Root is the base options to configure the service
@ -59,6 +60,7 @@ func (r *DNSResolver) Hash() string {
io.WriteString(h, strings.Join(r.Bootstraps, ",")) io.WriteString(h, strings.Join(r.Bootstraps, ","))
io.WriteString(h, strings.Join(r.Upstreams, ",")) io.WriteString(h, strings.Join(r.Upstreams, ","))
io.WriteString(h, fmt.Sprintf("%d", r.Port)) io.WriteString(h, fmt.Sprintf("%d", r.Port))
io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
io.WriteString(h, fmt.Sprintf("%v", r.Enabled)) io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
@ -99,3 +101,11 @@ func (r *DNSResolver) BootstrapsOrDefault() []string {
} }
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"} return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
} }
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if 0
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
if r.MaxUpstreamConnections >= 0 {
return r.MaxUpstreamConnections
}
return 0
}

View File

@ -13,7 +13,11 @@ func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{},
if port <= 0 || port > 65535 { if port <= 0 || port > 65535 {
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.") return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
} }
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), log) maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
if maxUpstreamConnections < 0 {
return errors.New("'proxy-dns-max-upstream-conns' must be 0 or higher")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
if err != nil { if err != nil {
close(dnsReadySignal) close(dnsReadySignal)
listener.Stop() listener.Stop()

View File

@ -23,19 +23,20 @@ const (
// UpstreamHTTPS is the upstream implementation for DNS over HTTPS service // UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
type UpstreamHTTPS struct { type UpstreamHTTPS struct {
client *http.Client client *http.Client
endpoint *url.URL endpoint *url.URL
bootstraps []string bootstraps []string
log *zerolog.Logger maxConnections int
log *zerolog.Logger
} }
// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint // NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
func NewUpstreamHTTPS(endpoint string, bootstraps []string, log *zerolog.Logger) (Upstream, error) { func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
u, err := url.Parse(endpoint) u, err := url.Parse(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &UpstreamHTTPS{client: configureClient(u.Hostname()), endpoint: u, bootstraps: bootstraps, log: log}, nil return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
} }
// Exchange provides an implementation for the Upstream interface // Exchange provides an implementation for the Upstream interface
@ -122,17 +123,18 @@ func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname()) return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
} }
return b, configureClient(b.Hostname()), nil return b, configureClient(b.Hostname(), 0), nil
} }
// configureClient will configure a HTTPS client for upstream DoH requests // configureClient will configure a HTTPS client for upstream DoH requests
func configureClient(hostname string) *http.Client { func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
// Update TLS and HTTP client configuration // Update TLS and HTTP client configuration
tlsConfig := &tls.Config{ServerName: hostname} tlsConfig := &tls.Config{ServerName: hostname}
transport := &http.Transport{ transport := &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
DisableCompression: true, DisableCompression: true,
MaxIdleConns: 1, MaxIdleConns: 1,
MaxConnsPerHost: maxUpstreamConnections,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
} }
_ = http2.ConfigureTransport(transport) _ = http2.ConfigureTransport(transport)

View File

@ -68,6 +68,12 @@ func Command(hidden bool) *cli.Command {
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"), Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"}, EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
}, },
&cli.IntFlag{
Name: "max-upstream-conns",
Usage: "Maximum concurrent connections to upstream, unlimited by default",
Value: 0,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
},
}, },
ArgsUsage: " ", // can't be the empty string or we get the default output ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: hidden, Hidden: hidden,
@ -90,8 +96,10 @@ func Run(c *cli.Context) error {
uint16(c.Uint("port")), uint16(c.Uint("port")),
c.StringSlice("upstream"), c.StringSlice("upstream"),
c.StringSlice("bootstrap"), c.StringSlice("bootstrap"),
c.Int("max-upstream-conns"),
log, log,
) )
if err != nil { if err != nil {
log.Err(err).Msg("Failed to create the listeners") log.Err(err).Msg("Failed to create the listeners")
return err return err
@ -173,12 +181,12 @@ func (l *Listener) Stop() error {
} }
// CreateListener configures the server and bound sockets // CreateListener configures the server and bound sockets
func CreateListener(address string, port uint16, upstreams []string, bootstraps []string, log *zerolog.Logger) (*Listener, error) { func CreateListener(address string, port uint16, upstreams []string, bootstraps []string, maxUpstreamConnections int, log *zerolog.Logger) (*Listener, error) {
// Build the list of upstreams // Build the list of upstreams
upstreamList := make([]Upstream, 0) upstreamList := make([]Upstream, 0)
for _, url := range upstreams { for _, url := range upstreams {
log.Info().Str(LogFieldURL, url).Msg("Adding DNS upstream") log.Info().Str(LogFieldURL, url).Msg("Adding DNS upstream")
upstream, err := NewUpstreamHTTPS(url, bootstraps, log) upstream, err := NewUpstreamHTTPS(url, bootstraps, maxUpstreamConnections, log)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create HTTPS upstream") return nil, errors.Wrap(err, "failed to create HTTPS upstream")
} }