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