• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// 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, RFC 8499 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}
390