From f953deb235478b0a69ea41dec4e1a7d94c9001c9 Mon Sep 17 00:00:00 2001 From: Ivan Bakalov Date: Thu, 18 Jan 2024 18:57:06 +0200 Subject: [PATCH] Use alternative DNS resolver for initial setup (#25) Use custom DNS resolver for HTTP requests to the DNSimple API during the initial setup phase to ensure they don't fail if the CoreDNS server itself is used as the system resolver, as it won't be ready at that time. --- CHANGELOG.md | 4 ++++ README.md | 4 +++- dnsimple.go | 22 +++++++++++++++++----- go.mod | 18 ++++++++---------- setup.go | 51 ++++++++++++++++++++++++++++++++++++++++++++------- setup_test.go | 2 +- 6 files changed, 77 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d77783e..03c1181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## main +FEATURES: + +- The plugin now supports the setting of `client_dns_resolver` to override the DNS resolver used for DNSimple API endpoints. This is useful when running CoreDNS as the DNS resolver for the host. Format is `ADDRESS:PORT`. (#25) + ## 1.0.0-rc.1 FEATURES: diff --git a/README.md b/README.md index 1cbc01c..41683d5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ dnsimple ZONE[:REGION ZONE ...] { account_id DNSIMPLE_ACCOUNT_ID base_url STRING custom_dns_resolver ADDRESS + client_dns_resolver ADDRESS fallthrough [ZONES...] identifier STRING max_retries MAX_RETRIES @@ -28,8 +29,9 @@ dnsimple ZONE[:REGION ZONE ...] { - `account_id`: The account ID containing the configured zones. If it's not provided, the environment variable `DNSIMPLE_ACCOUNT_ID` will be used. - `base_url`: The base url for interacting with the DNSimple API. If no value is provided the production environment is used. See [DNSimple Sandbox API](https://support.dnsimple.com/articles/sandbox/) for sandbox environment testing. - `custom_dns_resolver`: Optionally override the DNS resolver to use for resolving ALIAS records. By default, the system resolver is used. See the [ALIAS records](#alias-records) section for more details. +- `client_dns_resolver`: Optionally provide a DNS resolver to use for resolving DNSimple API endpoints. By default, the system resolver is used. This is useful when running CoreDNS as the DNS resolver for the host. Format is `ADDRESS:PORT`. - `fallthrough`: If a query matches the zone(s) but no response message can be generated, the query will be passed to the next plugin in the chain. To restrict passing only for specific zones, list them here; all other zone queries will **not** "fall through". -- `identifer`: Optionally set the CoreDNS instance identifer string. This is used to report the sync status of the zone to the DNSimple application. If no identifier is provided, "default" is used. +- `identifier`: Optionally set the CoreDNS instance identifier string. This is used to report the sync status of the zone to the DNSimple application. If no identifier is provided, "default" is used. - `max_retries`: Maximum retry attempts to fetch zones using the DNSimple API. Must be greater than zero. Defaults to 3. - `refresh`: The interval to refresh zones at. It must be a valid duration, and defaults to `1m`. diff --git a/dnsimple.go b/dnsimple.go index 6cf52e7..fe3828f 100644 --- a/dnsimple.go +++ b/dnsimple.go @@ -83,17 +83,29 @@ func (g *nameGraph) insertName(name string, value string) { type DNSimpleApiCaller func(path string, body []byte) error -func createDNSimpleApiCaller(baseUrl string, accessToken string, userAgent string) DNSimpleApiCaller { +func createDNSimpleAPICaller(options Options, baseURL string, accessToken string, userAgent string) DNSimpleApiCaller { return func(path string, body []byte) error { - url := fmt.Sprintf("%s%s", baseUrl, path) + url := fmt.Sprintf("%s%s", baseURL, path) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } + + client := http.Client{} + if options.clientDNSResolver != "" { + if client.Transport != nil { + client.Transport.(*http.Transport).DialContext = options.customHTTPDialer + } else { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.DialContext = options.customHTTPDialer + client.Transport = transport + } + } + req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken)) req.Header.Set("content-type", "application/json") req.Header.Set("user-agent", userAgent) - res, err := http.DefaultClient.Do(req) + res, err := client.Do(req) if err != nil { return err } @@ -160,14 +172,14 @@ func New(ctx context.Context, client dnsimpleService, keys map[string][]string, } } dnsResolver := net.DefaultResolver - if opts.customDnsResolver != "" { + if opts.customDNSResolver != "" { dnsResolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{ Timeout: time.Second * 10, } - return d.DialContext(ctx, network, opts.customDnsResolver) + return d.DialContext(ctx, network, opts.customDNSResolver) }, } } diff --git a/go.mod b/go.mod index cee0ee1..578fae6 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( github.com/coredns/caddy v1.1.1 github.com/coredns/coredns v1.11.1 github.com/dnsimple/dnsimple-go v1.5.1 - github.com/miekg/dns v1.1.57 + github.com/miekg/dns v1.1.58 github.com/stretchr/testify v1.8.4 + golang.org/x/oauth2 v0.14.0 ) require ( @@ -25,7 +26,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect - github.com/onsi/gomega v1.27.10 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect @@ -34,20 +34,18 @@ require ( github.com/prometheus/procfs v0.10.1 // indirect github.com/quic-go/qtls-go1-20 v0.3.1 // indirect github.com/quic-go/quic-go v0.37.4 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/stretchr/objx v0.5.0 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/oauth2 v0.14.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect - google.golang.org/grpc v1.57.1 // indirect + google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/setup.go b/setup.go index 3699645..83b5a2b 100644 --- a/setup.go +++ b/setup.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "math/rand" + "net" + "net/http" "os" "strconv" "strings" @@ -15,6 +17,7 @@ import ( "github.com/coredns/coredns/plugin/pkg/fall" clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/dnsimple/dnsimple-go/dnsimple" + "golang.org/x/oauth2" ) var log = clog.NewWithPlugin("dnsimple") @@ -27,15 +30,28 @@ type Options struct { accountId string accessToken string apiCaller DNSimpleApiCaller - customDnsResolver string + customDNSResolver string + clientDNSResolver string identifier string maxRetries int refresh time.Duration + customHTTPDialer func(ctx context.Context, network, address string) (net.Conn, error) } // exposed for testing -var newDnsimpleService = func(ctx context.Context, accessToken string, baseUrl string) (dnsimpleService, error) { - client := dnsimple.NewClient(dnsimple.StaticTokenHTTPClient(ctx, accessToken)) +var newDnsimpleService = func(ctx context.Context, options Options, accessToken string, baseUrl string) (dnsimpleService, error) { + httpClient := dnsimple.StaticTokenHTTPClient(ctx, accessToken) + + if options.clientDNSResolver != "" { + if httpClient.Transport.(*oauth2.Transport).Base != nil { + httpClient.Transport.(*oauth2.Transport).Base.(*http.Transport).DialContext = options.customHTTPDialer + } else { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.DialContext = options.customHTTPDialer + httpClient.Transport.(*oauth2.Transport).Base = transport + } + } + client := dnsimple.NewClient(httpClient) client.BaseURL = baseUrl client.SetUserAgent(defaultUserAgent + "/" + PluginVersion) return dnsimpleClient{client}, nil @@ -75,7 +91,6 @@ func setup(c *caddy.Controller) error { keys[zone] = append(keys[zone], region) } - // TODO: set to warn when no zones are defined if len(keys) == 0 { return plugin.Error("dnsimple", c.Errf("no zone(s) specified")) } @@ -104,7 +119,12 @@ func setup(c *caddy.Controller) error { if !c.NextArg() { return plugin.Error("dnsimple", c.ArgErr()) } - opts.customDnsResolver = c.Val() + opts.customDNSResolver = c.Val() + case "client_dns_resolver": + if !c.NextArg() { + return plugin.Error("dnsimple", c.ArgErr()) + } + opts.clientDNSResolver = c.Val() case "fallthrough": fall.SetZonesFromArgs(c.RemainingArgs()) case "identifier": @@ -168,10 +188,27 @@ func setup(c *caddy.Controller) error { opts.refresh = time.Duration(1) * time.Minute } - opts.apiCaller = createDNSimpleApiCaller(baseUrl, accessToken, defaultUserAgent+"/"+PluginVersion) + if opts.clientDNSResolver == "" { + dialer := &net.Dialer{ + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Second * 5, + } + return d.DialContext(ctx, network, opts.clientDNSResolver) + }, + }, + } + opts.customHTTPDialer = func(ctx context.Context, network, address string) (net.Conn, error) { + return dialer.DialContext(ctx, network, address) + } + } + + opts.apiCaller = createDNSimpleAPICaller(opts, baseUrl, accessToken, defaultUserAgent+"/"+PluginVersion) ctx, cancel := context.WithCancel(context.Background()) - client, err := newDnsimpleService(ctx, accessToken, baseUrl) + client, err := newDnsimpleService(ctx, opts, accessToken, baseUrl) if err != nil { cancel() return err diff --git a/setup_test.go b/setup_test.go index 8ea21b2..e451eb0 100644 --- a/setup_test.go +++ b/setup_test.go @@ -10,7 +10,7 @@ import ( func TestSetupDNSimple(t *testing.T) { fakeClient := new(fakeDNSimpleClient) - newDnsimpleService = func(ctx context.Context, accessToken, baseUrl string) (dnsimpleService, error) { + newDnsimpleService = func(ctx context.Context, options Options, accessToken, baseUrl string) (dnsimpleService, error) { return fakeClient, nil }