blob: 03b2f8738ee78e0731a995cadd22f26d7030c94e [file] [log] [blame]
Dan McArdle995574c2021-06-09 15:39:37 -04001// Copyright (c) 2021, Google Inc.
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15package main
16
17import (
18 "errors"
19 "flag"
20 "fmt"
21 "io/ioutil"
22 "log"
23 "net"
24 "os"
25 "path"
26 "strings"
27
28 "golang.org/x/crypto/cryptobyte"
29 "golang.org/x/net/dns/dnsmessage"
30)
31
32const (
33 httpsType = 65 // RRTYPE for HTTPS records.
34
35 // SvcParamKey codepoints defined in draft-ietf-dnsop-svcb-https-06.
36 httpsKeyMandatory = 0
37 httpsKeyALPN = 1
38 httpsKeyNoDefaultALPN = 2
39 httpsKeyPort = 3
40 httpsKeyIPV4Hint = 4
41 httpsKeyECH = 5
42 httpsKeyIPV6Hint = 6
43)
44
45var (
46 name = flag.String("name", "", "The name to look up in DNS. Required.")
47 server = flag.String("server", "8.8.8.8:53", "Comma-separated host and UDP port that defines the DNS server to query.")
48 outDir = flag.String("out-dir", "", "The directory where ECHConfigList values will be written. If unspecified, bytes are hexdumped to stdout.")
49)
50
51type httpsRecord struct {
52 priority uint16
53 targetName string
54
55 // SvcParams:
56 mandatory []uint16
57 alpn []string
58 noDefaultALPN bool
59 hasPort bool
60 port uint16
61 ipv4hint []net.IP
62 ech []byte
63 ipv6hint []net.IP
64 unknownParams map[uint16][]byte
65}
66
67// String pretty-prints |h| as a multi-line string with bullet points.
68func (h httpsRecord) String() string {
69 var b strings.Builder
70 fmt.Fprintf(&b, "HTTPS SvcPriority:%d TargetName:%q", h.priority, h.targetName)
71
72 if len(h.mandatory) != 0 {
73 fmt.Fprintf(&b, "\n * mandatory: %v", h.mandatory)
74 }
75 if len(h.alpn) != 0 {
76 fmt.Fprintf(&b, "\n * alpn: %q", h.alpn)
77 }
78 if h.noDefaultALPN {
79 fmt.Fprint(&b, "\n * no-default-alpn")
80 }
81 if h.hasPort {
82 fmt.Fprintf(&b, "\n * port: %d", h.port)
83 }
84 if len(h.ipv4hint) != 0 {
85 fmt.Fprintf(&b, "\n * ipv4hint:")
86 for _, address := range h.ipv4hint {
87 fmt.Fprintf(&b, "\n - %s", address)
88 }
89 }
90 if len(h.ech) != 0 {
91 fmt.Fprintf(&b, "\n * ech: %x", h.ech)
92 }
93 if len(h.ipv6hint) != 0 {
94 fmt.Fprintf(&b, "\n * ipv6hint:")
95 for _, address := range h.ipv6hint {
96 fmt.Fprintf(&b, "\n - %s", address)
97 }
98 }
99 if len(h.unknownParams) != 0 {
100 fmt.Fprint(&b, "\n * unknown SvcParams:")
101 for key, value := range h.unknownParams {
102 fmt.Fprintf(&b, "\n - %d: %x", key, value)
103 }
104 }
105 return b.String()
106}
107
108// dnsQueryForHTTPS queries the DNS server over UDP for any HTTPS records
109// associated with |domain|. It scans the response's answers and returns all the
110// HTTPS records it finds. It returns an error if any connection steps fail.
111func dnsQueryForHTTPS(domain string) ([][]byte, error) {
112 udpAddr, err := net.ResolveUDPAddr("udp", *server)
113 if err != nil {
114 return nil, err
115 }
116 conn, err := net.DialUDP("udp", nil, udpAddr)
117 if err != nil {
118 return nil, fmt.Errorf("failed to dial: %s", err)
119 }
120 defer conn.Close()
121
122 // Domain name must be canonical or message packing will fail.
123 if domain[len(domain)-1] != '.' {
124 domain += "."
125 }
126 dnsName, err := dnsmessage.NewName(domain)
127 if err != nil {
128 return nil, fmt.Errorf("failed to create DNS name from %q: %s", domain, err)
129 }
130 question := dnsmessage.Question{
131 Name: dnsName,
132 Type: httpsType,
133 Class: dnsmessage.ClassINET,
134 }
135 msg := dnsmessage.Message{
136 Header: dnsmessage.Header{
137 RecursionDesired: true,
138 },
139 Questions: []dnsmessage.Question{question},
140 }
141 packedMsg, err := msg.Pack()
142 if err != nil {
143 return nil, fmt.Errorf("failed to pack msg: %s", err)
144 }
145
146 if _, err = conn.Write(packedMsg); err != nil {
147 return nil, fmt.Errorf("failed to send the DNS query: %s", err)
148 }
149
150 for {
151 response := make([]byte, 512)
152 n, err := conn.Read(response)
153 if err != nil {
154 return nil, fmt.Errorf("failed to read the DNS response: %s", err)
155 }
156 response = response[:n]
157
158 var p dnsmessage.Parser
159 header, err := p.Start(response)
160 if err != nil {
161 return nil, err
162 }
163 if !header.Response {
164 return nil, errors.New("received DNS message is not a response")
165 }
166 if header.RCode != dnsmessage.RCodeSuccess {
167 return nil, fmt.Errorf("response from DNS has non-success RCode: %s", header.RCode.String())
168 }
169 if header.ID != 0 {
170 return nil, errors.New("received a DNS response with the wrong ID")
171 }
172 if !header.RecursionAvailable {
173 return nil, errors.New("server does not support recursion")
174 }
175 // Verify that this response answers the question that we asked in the
176 // query. If the resolver encountered any CNAMEs, it's not guaranteed
177 // that the response will contain a question with the same QNAME as our
178 // query. However, RFC8499 Section 4 indicates that in general use, the
179 // response's QNAME should match the query, so we will make that
180 // assumption.
181 q, err := p.Question()
182 if err != nil {
183 return nil, err
184 }
185 if q != question {
186 return nil, fmt.Errorf("response answers the wrong question: %v", q)
187 }
188 if q, err = p.Question(); err != dnsmessage.ErrSectionDone {
189 return nil, fmt.Errorf("response contains an unexpected question: %v", q)
190 }
191
192 var httpsRecords [][]byte
193 for {
194 h, err := p.AnswerHeader()
195 if err == dnsmessage.ErrSectionDone {
196 break
197 }
198 if err != nil {
199 return nil, err
200 }
201
202 switch h.Type {
203 case httpsType:
204 // This should continue to work when golang.org/x/net/dns/dnsmessage
205 // adds support for HTTPS records.
206 r, err := p.UnknownResource()
207 if err != nil {
208 return nil, err
209 }
210 httpsRecords = append(httpsRecords, r.Data)
211 default:
212 if _, err := p.UnknownResource(); err != nil {
213 return nil, err
214 }
215 }
216 }
217 return httpsRecords, nil
218 }
219}
220
221// parseHTTPSRecord parses an HTTPS record (draft-ietf-dnsop-svcb-https-06,
222// Section 2.2) from |raw|. If there are syntax errors, it returns an error.
223func parseHTTPSRecord(raw []byte) (httpsRecord, error) {
224 reader := cryptobyte.String(raw)
225
226 var priority uint16
227 if !reader.ReadUint16(&priority) {
228 return httpsRecord{}, errors.New("failed to parse HTTPS record priority")
229 }
230
231 // Read the TargetName.
232 var dottedDomain string
233 for {
234 var label cryptobyte.String
235 if !reader.ReadUint8LengthPrefixed(&label) {
236 return httpsRecord{}, errors.New("failed to parse HTTPS record TargetName")
237 }
238 if label.Empty() {
239 break
240 }
241 dottedDomain += string(label) + "."
242 }
243
244 if priority == 0 {
245 // TODO(dmcardle) Recursively follow AliasForm records.
246 return httpsRecord{}, fmt.Errorf("received an AliasForm HTTPS record with TargetName=%q", dottedDomain)
247 }
248
249 record := httpsRecord{
250 priority: priority,
251 targetName: dottedDomain,
252 unknownParams: make(map[uint16][]byte),
253 }
254
255 // Read the SvcParams.
256 var lastSvcParamKey uint16
257 for svcParamCount := 0; !reader.Empty(); svcParamCount++ {
258 var svcParamKey uint16
259 var svcParamValue cryptobyte.String
260 if !reader.ReadUint16(&svcParamKey) ||
261 !reader.ReadUint16LengthPrefixed(&svcParamValue) {
262 return httpsRecord{}, errors.New("failed to parse HTTPS record SvcParam")
263 }
264 if svcParamCount > 0 && svcParamKey <= lastSvcParamKey {
265 return httpsRecord{}, errors.New("malformed HTTPS record contains out-of-order SvcParamKey")
266 }
267 lastSvcParamKey = svcParamKey
268
269 switch svcParamKey {
270 case httpsKeyMandatory:
271 if svcParamValue.Empty() {
272 return httpsRecord{}, errors.New("malformed mandatory SvcParamValue")
273 }
274 var lastKey uint16
275 for !svcParamValue.Empty() {
276 // |httpsKeyMandatory| may not appear in the mandatory list.
277 // |httpsKeyMandatory| is zero, so checking against the initial
278 // value of |lastKey| handles ordering and the invalid code point.
279 var key uint16
280 if !svcParamValue.ReadUint16(&key) ||
281 key <= lastKey {
282 return httpsRecord{}, errors.New("malformed mandatory SvcParamValue")
283 }
284 lastKey = key
285 record.mandatory = append(record.mandatory, key)
286 }
287 case httpsKeyALPN:
288 if svcParamValue.Empty() {
289 return httpsRecord{}, errors.New("malformed alpn SvcParamValue")
290 }
291 for !svcParamValue.Empty() {
292 var alpn cryptobyte.String
293 if !svcParamValue.ReadUint8LengthPrefixed(&alpn) || alpn.Empty() {
294 return httpsRecord{}, errors.New("malformed alpn SvcParamValue")
295 }
296 record.alpn = append(record.alpn, string(alpn))
297 }
298 case httpsKeyNoDefaultALPN:
299 if !svcParamValue.Empty() {
300 return httpsRecord{}, errors.New("malformed no-default-alpn SvcParamValue")
301 }
302 record.noDefaultALPN = true
303 case httpsKeyPort:
304 if !svcParamValue.ReadUint16(&record.port) ||
305 !svcParamValue.Empty() {
306 return httpsRecord{}, errors.New("malformed port SvcParamValue")
307 }
308 record.hasPort = true
309 case httpsKeyIPV4Hint:
310 if svcParamValue.Empty() {
311 return httpsRecord{}, errors.New("malformed ipv4hint SvcParamValue")
312 }
313 for !svcParamValue.Empty() {
314 var address []byte
315 if !svcParamValue.ReadBytes(&address, 4) {
316 return httpsRecord{}, errors.New("malformed ipv4hint SvcParamValue")
317 }
318 record.ipv4hint = append(record.ipv4hint, address)
319 }
320 case httpsKeyECH:
321 if svcParamValue.Empty() {
322 return httpsRecord{}, errors.New("malformed ech SvcParamValue")
323 }
324 record.ech = svcParamValue
325 case httpsKeyIPV6Hint:
326 if svcParamValue.Empty() {
327 return httpsRecord{}, errors.New("malformed ipv6hint SvcParamValue")
328 }
329 for !svcParamValue.Empty() {
330 var address []byte
331 if !svcParamValue.ReadBytes(&address, 16) {
332 return httpsRecord{}, errors.New("malformed ipv6hint SvcParamValue")
333 }
334 record.ipv6hint = append(record.ipv6hint, address)
335 }
336 default:
337 record.unknownParams[svcParamKey] = svcParamValue
338 }
339 }
340 return record, nil
341}
342
343func main() {
344 flag.Parse()
345 log.SetFlags(log.Lshortfile | log.LstdFlags)
346
347 if len(*name) == 0 {
348 flag.Usage()
349 os.Exit(1)
350 }
351
352 httpsRecords, err := dnsQueryForHTTPS(*name)
353 if err != nil {
354 log.Printf("Error querying %q: %s\n", *name, err)
355 os.Exit(1)
356 }
357 if len(httpsRecords) == 0 {
358 log.Println("No HTTPS records found in DNS response.")
359 os.Exit(1)
360 }
361
362 if len(*outDir) > 0 {
363 if err = os.Mkdir(*outDir, 0755); err != nil && !os.IsExist(err) {
364 log.Printf("Failed to create out directory %q: %s\n", *outDir, err)
365 os.Exit(1)
366 }
367 }
368
369 var echConfigListCount int
370 for _, httpsRecord := range httpsRecords {
371 record, err := parseHTTPSRecord(httpsRecord)
372 if err != nil {
373 log.Printf("Failed to parse HTTPS record: %s", err)
374 os.Exit(1)
375 }
376 fmt.Printf("%s\n", record)
377 if len(*outDir) == 0 {
378 continue
379 }
380
381 outFile := path.Join(*outDir, fmt.Sprintf("ech-config-list-%d", echConfigListCount))
382 if err = ioutil.WriteFile(outFile, record.ech, 0644); err != nil {
383 log.Printf("Failed to write file: %s\n", err)
384 os.Exit(1)
385 }
386 fmt.Printf("Wrote ECHConfigList to %q\n", outFile)
387 echConfigListCount++
388 }
389}