blob: 0f1a685855c8c7a235ba105c10defe8d2c3335fc [file] [log] [blame]
Abhay Kumara2ae5992025-11-10 14:02:24 +00001// Copyright 2015 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package netutil
16
17import (
18 "context"
19 "errors"
20 "fmt"
21 "net"
22 "net/url"
23 "reflect"
24 "sort"
25 "time"
26
27 "go.uber.org/zap"
28
29 "go.etcd.io/etcd/client/pkg/v3/types"
30)
31
32// indirection for testing
33var resolveTCPAddr = resolveTCPAddrDefault
34
35const retryInterval = time.Second
36
37// taken from go's ResolveTCP code but uses configurable ctx
38func resolveTCPAddrDefault(ctx context.Context, addr string) (*net.TCPAddr, error) {
39 host, port, serr := net.SplitHostPort(addr)
40 if serr != nil {
41 return nil, serr
42 }
43 portnum, perr := net.DefaultResolver.LookupPort(ctx, "tcp", port)
44 if perr != nil {
45 return nil, perr
46 }
47
48 var ips []net.IPAddr
49 if ip := net.ParseIP(host); ip != nil {
50 ips = []net.IPAddr{{IP: ip}}
51 } else {
52 // Try as a DNS name.
53 ipss, err := net.DefaultResolver.LookupIPAddr(ctx, host)
54 if err != nil {
55 return nil, err
56 }
57 ips = ipss
58 }
59 // randomize?
60 ip := ips[0]
61 return &net.TCPAddr{IP: ip.IP, Port: portnum, Zone: ip.Zone}, nil
62}
63
64// resolveTCPAddrs is a convenience wrapper for net.ResolveTCPAddr.
65// resolveTCPAddrs return a new set of url.URLs, in which all DNS hostnames
66// are resolved.
67func resolveTCPAddrs(ctx context.Context, lg *zap.Logger, urls [][]url.URL) ([][]url.URL, error) {
68 newurls := make([][]url.URL, 0)
69 for _, us := range urls {
70 nus := make([]url.URL, len(us))
71 for i, u := range us {
72 nu, err := url.Parse(u.String())
73 if err != nil {
74 return nil, fmt.Errorf("failed to parse %q (%w)", u.String(), err)
75 }
76 nus[i] = *nu
77 }
78 for i, u := range nus {
79 h, err := resolveURL(ctx, lg, u)
80 if err != nil {
81 return nil, fmt.Errorf("failed to resolve %q (%w)", u.String(), err)
82 }
83 if h != "" {
84 nus[i].Host = h
85 }
86 }
87 newurls = append(newurls, nus)
88 }
89 return newurls, nil
90}
91
92func resolveURL(ctx context.Context, lg *zap.Logger, u url.URL) (string, error) {
93 if u.Scheme == "unix" || u.Scheme == "unixs" {
94 // unix sockets don't resolve over TCP
95 return "", nil
96 }
97 host, _, err := net.SplitHostPort(u.Host)
98 if err != nil {
99 lg.Warn(
100 "failed to parse URL Host while resolving URL",
101 zap.String("url", u.String()),
102 zap.String("host", u.Host),
103 zap.Error(err),
104 )
105 return "", err
106 }
107 if host == "localhost" {
108 return "", nil
109 }
110 for ctx.Err() == nil {
111 tcpAddr, err := resolveTCPAddr(ctx, u.Host)
112 if err == nil {
113 lg.Info(
114 "resolved URL Host",
115 zap.String("url", u.String()),
116 zap.String("host", u.Host),
117 zap.String("resolved-addr", tcpAddr.String()),
118 )
119 return tcpAddr.String(), nil
120 }
121
122 lg.Warn(
123 "failed to resolve URL Host",
124 zap.String("url", u.String()),
125 zap.String("host", u.Host),
126 zap.Duration("retry-interval", retryInterval),
127 zap.Error(err),
128 )
129
130 select {
131 case <-ctx.Done():
132 lg.Warn(
133 "failed to resolve URL Host; returning",
134 zap.String("url", u.String()),
135 zap.String("host", u.Host),
136 zap.Duration("retry-interval", retryInterval),
137 zap.Error(err),
138 )
139 return "", err
140 case <-time.After(retryInterval):
141 }
142 }
143 return "", ctx.Err()
144}
145
146// urlsEqual checks equality of url.URLS between two arrays.
147// This check pass even if an URL is in hostname and opposite is in IP address.
148func urlsEqual(ctx context.Context, lg *zap.Logger, a []url.URL, b []url.URL) (bool, error) {
149 if len(a) != len(b) {
150 return false, fmt.Errorf("len(%q) != len(%q)", urlsToStrings(a), urlsToStrings(b))
151 }
152
153 sort.Sort(types.URLs(a))
154 sort.Sort(types.URLs(b))
155 var needResolve bool
156 for i := range a {
157 if !reflect.DeepEqual(a[i], b[i]) {
158 needResolve = true
159 break
160 }
161 }
162 if !needResolve {
163 return true, nil
164 }
165
166 // If URLs are not equal, try to resolve it and compare again.
167 urls, err := resolveTCPAddrs(ctx, lg, [][]url.URL{a, b})
168 if err != nil {
169 return false, err
170 }
171 a, b = urls[0], urls[1]
172 sort.Sort(types.URLs(a))
173 sort.Sort(types.URLs(b))
174 for i := range a {
175 if !reflect.DeepEqual(a[i], b[i]) {
176 return false, fmt.Errorf("resolved urls: %q != %q", a[i].String(), b[i].String())
177 }
178 }
179 return true, nil
180}
181
182// URLStringsEqual returns "true" if given URLs are valid
183// and resolved to same IP addresses. Otherwise, return "false"
184// and error, if any.
185func URLStringsEqual(ctx context.Context, lg *zap.Logger, a []string, b []string) (bool, error) {
186 if len(a) != len(b) {
187 return false, fmt.Errorf("len(%q) != len(%q)", a, b)
188 }
189 urlsA, err := stringsToURLs(a)
190 if err != nil {
191 return false, err
192 }
193 urlsB, err := stringsToURLs(b)
194 if err != nil {
195 return false, err
196 }
197 return urlsEqual(ctx, lg, urlsA, urlsB)
198}
199
200func urlsToStrings(us []url.URL) []string {
201 rs := make([]string, len(us))
202 for i := range us {
203 rs[i] = us[i].String()
204 }
205 return rs
206}
207
208func stringsToURLs(us []string) ([]url.URL, error) {
209 urls := make([]url.URL, 0, len(us))
210 for _, str := range us {
211 u, err := url.Parse(str)
212 if err != nil {
213 return nil, fmt.Errorf("failed to parse string to URL: %q", str)
214 }
215 urls = append(urls, *u)
216 }
217 return urls, nil
218}
219
220func IsNetworkTimeoutError(err error) bool {
221 var nerr net.Error
222 return errors.As(err, &nerr) && nerr.Timeout()
223}