Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/ffresty/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ const (
// HTTPMaxIdleConnsPerHost the max number of idle connections per host
HTTPMaxIdleConnsPerHost = "maxIdleConnsPerHost"

// HTTPDNSServers an optional list of DNS server addresses (host or host:port, port defaults to 53) to use for
Comment thread
EnriqueL8 marked this conversation as resolved.
Outdated
// name resolution. Setting this forces use of Go's built-in DNS resolver rather than the system resolver.
HTTPDNSServers = "dnsServers"

// HTTPConnectionTimeout the connection timeout for new connections
HTTPConnectionTimeout = "connectionTimeout"
// HTTPTLSHandshakeTimeout the TLS handshake connection timeout
Expand Down Expand Up @@ -112,6 +116,7 @@ func InitConfig(conf config.Section) {
conf.AddKnownKey(HTTPMaxIdleConns, defaultHTTPMaxIdleConns)
conf.AddKnownKey(HTTPMaxConnsPerHost, defaultHTTPMaxConnsPerHost)
conf.AddKnownKey(HTTPMaxIdleConnsPerHost, defaultHTTPMaxIdleConnsPerHost)
conf.AddKnownKey(HTTPDNSServers)
conf.AddKnownKey(HTTPConnectionTimeout, defaultHTTPConnectionTimeout)
conf.AddKnownKey(HTTPTLSHandshakeTimeout, defaultHTTPTLSHandshakeTimeout)
conf.AddKnownKey(HTTPExpectContinueTimeout, defaultHTTPExpectContinueTimeout)
Expand Down Expand Up @@ -142,6 +147,7 @@ func GenerateConfig(ctx context.Context, conf config.Section) (*Config, error) {
HTTPMaxIdleConns: conf.GetInt(HTTPMaxIdleConns),
HTTPMaxConnsPerHost: conf.GetInt(HTTPMaxConnsPerHost),
HTTPMaxIdleConnsPerHost: conf.GetInt(HTTPMaxIdleConnsPerHost),
DNSServers: conf.GetStringSlice(HTTPDNSServers),
HTTPConnectionTimeout: fftypes.FFDuration(conf.GetDuration(HTTPConnectionTimeout)),
HTTPTLSHandshakeTimeout: fftypes.FFDuration(conf.GetDuration(HTTPTLSHandshakeTimeout)),
HTTPExpectContinueTimeout: fftypes.FFDuration(conf.GetDuration(HTTPExpectContinueTimeout)),
Expand Down
2 changes: 2 additions & 0 deletions pkg/ffresty/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestWSConfigGeneration(t *testing.T) {
utConf.Set(HTTPTLSHandshakeTimeout, 1)
utConf.Set(HTTPExpectContinueTimeout, 1)
utConf.Set(HTTPPassthroughHeadersEnabled, true)
utConf.Set(HTTPDNSServers, []string{"8.8.8.8", "1.1.1.1:53"})

ctx := context.Background()
config, err := GenerateConfig(ctx, utConf)
Expand All @@ -68,6 +69,7 @@ func TestWSConfigGeneration(t *testing.T) {
assert.Equal(t, fftypes.FFDuration(1000000), config.HTTPConnectionTimeout)
assert.Equal(t, 1, config.HTTPMaxIdleConns)
assert.Equal(t, "custom value", config.HTTPHeaders.GetString("custom-header"))
assert.Equal(t, []string{"8.8.8.8", "1.1.1.1:53"}, config.DNSServers)
}

func TestWSConfigTLSGenerationFail(t *testing.T) {
Expand Down
68 changes: 63 additions & 5 deletions pkg/ffresty/ffresty.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ type HTTPConfig struct {
HTTPPassthroughHeadersEnabled bool `ffstruct:"RESTConfig" json:"httpPassthroughHeadersEnabled,omitempty"`
HTTPHeaders fftypes.JSONObject `ffstruct:"RESTConfig" json:"headers,omitempty"`
HTTPTLSHandshakeTimeout fftypes.FFDuration `ffstruct:"RESTConfig" json:"tlsHandshakeTimeout,omitempty"`
DNSServers []string `ffstruct:"RESTConfig" json:"dnsServers,omitempty"` // optional DNS servers (host or host:port); forces use of Go's built-in resolver
HTTPCustomClient interface{} `json:"-"`
TLSClientConfig *tls.Config `json:"-"` // should be built from separate TLSConfig using fftls utils
Resolver *net.Resolver `json:"-"` // programmatic DNS resolver override; takes precedence over DNSServers
OnCheckRetry func(res *resty.Response, err error) bool `json:"-"` // response could be nil on err
OnBeforeRequest func(req *resty.Request) error `json:"-"` // called before each request, even retry
}
Expand Down Expand Up @@ -209,12 +211,19 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli

if client == nil {

dialer := &net.Dialer{
Timeout: time.Duration(ffrestyConfig.HTTPConnectionTimeout),
KeepAlive: time.Duration(ffrestyConfig.HTTPConnectionTimeout),
}
// An explicit programmatic resolver wins; otherwise build one from any configured DNS servers.
// Either way the system resolver is replaced with Go's built-in resolver.
if resolver := dnsResolver(&ffrestyConfig); resolver != nil {
dialer.Resolver = resolver
}

httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: time.Duration(ffrestyConfig.HTTPConnectionTimeout),
KeepAlive: time.Duration(ffrestyConfig.HTTPConnectionTimeout),
}).DialContext,
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: ffrestyConfig.HTTPMaxIdleConns,
MaxConnsPerHost: ffrestyConfig.HTTPMaxConnsPerHost,
Expand Down Expand Up @@ -376,6 +385,55 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli
return client
}

// dnsResolver derives the resolver to attach to the dialer based on config precedence:
// - an explicitly provided programmatic Resolver always wins
// - otherwise, if DNSServers are configured, a pure-Go resolver that dials those servers
// (in order, failing over to the next on error) is built
// - otherwise nil, leaving Go's default system resolver selection in place
func dnsResolver(ffrestyConfig *Config) *net.Resolver {
if ffrestyConfig.Resolver != nil {
return ffrestyConfig.Resolver
}
return NewDNSResolver(ffrestyConfig.DNSServers, time.Duration(ffrestyConfig.HTTPConnectionTimeout))
Comment thread
onelapahead marked this conversation as resolved.
Outdated
}

// NewDNSResolver builds a pure-Go *net.Resolver that dials the given DNS servers
// (each host or host:port, port defaulting to 53) in order, failing over to the
// next on error. Returns nil when no servers are given (use the system resolver).
// Exported so non-ffresty dialers — e.g. a WebSocket dialer — can honour the same
Comment thread
EnriqueL8 marked this conversation as resolved.
Outdated
// dnsServers config as the HTTP client.
func NewDNSResolver(dnsServers []string, dialTimeout time.Duration) *net.Resolver {
Comment thread
onelapahead marked this conversation as resolved.
Outdated
if len(dnsServers) == 0 {
return nil
}
servers := make([]string, len(dnsServers))
for i, server := range dnsServers {
servers[i] = withDefaultDNSPort(server)
}
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := net.Dialer{Timeout: dialTimeout}
var err error
for _, server := range servers {
var conn net.Conn
if conn, err = d.DialContext(ctx, network, server); err == nil {
return conn, nil
}
}
return nil, err
},
}
}

// withDefaultDNSPort ensures a DNS server address has a port, defaulting to 53.
func withDefaultDNSPort(server string) string {
if _, _, err := net.SplitHostPort(server); err == nil {
return server
}
return net.JoinHostPort(server, "53")
}

func traceBody(v any) string {
switch vt := v.(type) {
case string:
Expand Down
81 changes: 81 additions & 0 deletions pkg/ffresty/ffresty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/ffapi"
"github.com/hyperledger/firefly-common/pkg/fftls"
"github.com/hyperledger/firefly-common/pkg/fftypes"
"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/metric"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -819,3 +820,83 @@ func TestTrace(t *testing.T) {
require.JSONEq(t, `{"some":"data"}`, traceBody(map[string]string{"some": "data"}))
require.Equal(t, `(binary reader)`, traceBody(strings.NewReader("data to stream")))
}

func TestWithDefaultDNSPort(t *testing.T) {
assert.Equal(t, "8.8.8.8:53", withDefaultDNSPort("8.8.8.8"))
assert.Equal(t, "8.8.8.8:5353", withDefaultDNSPort("8.8.8.8:5353"))
assert.Equal(t, "[2001:db8::1]:53", withDefaultDNSPort("2001:db8::1"))
assert.Equal(t, "[2001:db8::1]:5353", withDefaultDNSPort("[2001:db8::1]:5353"))
}

func TestDNSResolverPrecedence(t *testing.T) {
// Nothing configured -> leave Go's default system resolver selection in place
assert.Nil(t, dnsResolver(&Config{}))

// Programmatic resolver always wins
custom := &net.Resolver{}
assert.Same(t, custom, dnsResolver(&Config{HTTPConfig: HTTPConfig{
Resolver: custom,
DNSServers: []string{"8.8.8.8"},
}}))

// DNSServers builds a pure-Go resolver
r := dnsResolver(&Config{HTTPConfig: HTTPConfig{DNSServers: []string{"8.8.8.8"}}})
require.NotNil(t, r)
assert.True(t, r.PreferGo)
assert.NotNil(t, r.Dial)
}

func TestDNSResolverDialFailover(t *testing.T) {
// Stand up a listener acting as the "good" DNS server
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()

accepted := make(chan struct{}, 1)
go func() {
conn, acceptErr := ln.Accept()
if acceptErr == nil {
accepted <- struct{}{}
_ = conn.Close()
}
}()

// First server is unroutable so the dialer must fail over to the live listener
r := dnsResolver(&Config{HTTPConfig: HTTPConfig{
HTTPConnectionTimeout: fftypes.FFDuration(5 * time.Second),
DNSServers: []string{"127.0.0.1:1", ln.Addr().String()},
}})
require.NotNil(t, r)

conn, err := r.Dial(context.Background(), "tcp", "ignored:53")
require.NoError(t, err)
defer conn.Close()
assert.Equal(t, ln.Addr().String(), conn.RemoteAddr().String())

select {
case <-accepted:
case <-time.After(5 * time.Second):
t.Fatal("DNS dial did not reach the configured server")
}
}

func TestDNSResolverDialAllFail(t *testing.T) {
r := dnsResolver(&Config{HTTPConfig: HTTPConfig{
HTTPConnectionTimeout: fftypes.FFDuration(250 * time.Millisecond),
DNSServers: []string{"127.0.0.1:1"},
}})
require.NotNil(t, r)
_, err := r.Dial(context.Background(), "tcp", "ignored:53")
assert.Error(t, err)
}

func TestNewWithConfigDNSServersWired(t *testing.T) {
ctx := context.Background()
c := NewWithConfig(ctx, Config{HTTPConfig: HTTPConfig{
DNSServers: []string{"8.8.8.8"},
}})
require.NotNil(t, c)
transport, ok := c.GetClient().Transport.(*http.Transport)
require.True(t, ok)
assert.NotNil(t, transport.DialContext)
}
Loading
Loading