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