-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdns.go
More file actions
176 lines (151 loc) · 4.65 KB
/
dns.go
File metadata and controls
176 lines (151 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package main
import (
"context"
"fmt"
"net"
"net/url"
"time"
"github.com/sirupsen/logrus"
)
// DNSHealthChecker validates DNS resolution with custom timeouts and retry logic.
// It's designed to verify that DNS is working before attempting HTTP requests.
type DNSHealthChecker struct {
resolver *net.Resolver
timeout time.Duration
}
// NewDNSHealthChecker creates a DNS health checker with a custom resolver.
// The resolver uses a pure Go implementation with the specified timeout for DNS queries.
//
// Parameters:
// - timeout: Maximum time to wait for a DNS query to complete
//
// Example:
//
// checker := NewDNSHealthChecker(5 * time.Second)
// err := checker.CheckDNS(ctx, "https://link-ip.nextdns.io/id/token")
func NewDNSHealthChecker(timeout time.Duration) *DNSHealthChecker {
// Create a custom resolver with PreferGo=true for pure Go DNS resolution
// This gives us more control over timeouts and behavior
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: timeout,
}
return d.DialContext(ctx, network, address)
},
}
return &DNSHealthChecker{
resolver: resolver,
timeout: timeout,
}
}
// CheckDNS verifies that DNS resolution works for the given endpoint URL.
// It extracts the hostname from the URL and attempts to resolve it.
//
// Returns nil if DNS resolution succeeds, error otherwise.
func (dhc *DNSHealthChecker) CheckDNS(ctx context.Context, endpoint string) error {
// Extract hostname from endpoint URL
hostname, err := dhc.extractHostname(endpoint)
if err != nil {
return fmt.Errorf("invalid endpoint URL: %w", err)
}
// Attempt DNS resolution
_, err = dhc.resolveDNS(ctx, hostname)
if err != nil {
return fmt.Errorf("DNS resolution failed for %s: %w", hostname, err)
}
return nil
}
// WaitForDNS blocks until DNS resolution succeeds for the given endpoint,
// using exponential backoff for retries. It will keep retrying until either
// DNS works or the context is cancelled.
//
// Parameters:
// - ctx: Context for cancellation
// - endpoint: The URL endpoint to check (hostname will be extracted)
// - backoff: Exponential backoff strategy for retry delays
//
// Returns:
// - nil if DNS eventually succeeds
// - error if context is cancelled before DNS succeeds
//
// Example:
//
// backoff := NewExponentialBackoff(1*time.Second, 60*time.Second, 2.0)
// err := checker.WaitForDNS(ctx, endpoint, backoff)
func (dhc *DNSHealthChecker) WaitForDNS(ctx context.Context, endpoint string, backoff *ExponentialBackoff) error {
attempt := 0
for {
attempt++
err := dhc.CheckDNS(ctx, endpoint)
if err == nil {
// DNS check succeeded!
if attempt > 1 {
logger.WithFields(logrus.Fields{
"attempts": attempt,
}).Info("DNS is now ready")
}
return nil
}
// Check if context was cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Calculate next retry delay
delay := backoff.Next()
logger.WithFields(logrus.Fields{
"attempt": attempt,
"error": err.Error(),
"next_retry": delay.String(),
}).Warn("DNS check failed, retrying")
// Wait for delay or context cancellation
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
// Continue to next attempt
}
}
}
// extractHostname extracts the hostname from a URL string.
// Returns error if the URL is invalid or has no host.
func (dhc *DNSHealthChecker) extractHostname(endpoint string) (string, error) {
if endpoint == "" {
return "", fmt.Errorf("endpoint is empty")
}
parsedURL, err := url.Parse(endpoint)
if err != nil {
return "", fmt.Errorf("failed to parse URL: %w", err)
}
if parsedURL.Scheme == "" {
return "", fmt.Errorf("URL has no scheme")
}
if parsedURL.Host == "" {
return "", fmt.Errorf("URL has no host")
}
// Remove port if present (e.g., "example.com:8080" -> "example.com")
hostname := parsedURL.Hostname()
if hostname == "" {
return "", fmt.Errorf("could not extract hostname from URL")
}
return hostname, nil
}
// resolveDNS attempts to resolve a hostname to IP addresses using the custom resolver.
// Returns the list of IP addresses if successful, error otherwise.
func (dhc *DNSHealthChecker) resolveDNS(ctx context.Context, hostname string) ([]string, error) {
// Create a context with timeout for the DNS query
queryCtx, cancel := context.WithTimeout(ctx, dhc.timeout)
defer cancel()
// Use LookupHost to resolve the hostname
addrs, err := dhc.resolver.LookupHost(queryCtx, hostname)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no addresses found for hostname %s", hostname)
}
return addrs, nil
}