Mirror of BoringSSL (grpc依赖)
https://boringssl.googlesource.com/boringssl
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
11 KiB
389 lines
11 KiB
// Copyright (c) 2021, Google Inc. |
|
// |
|
// Permission to use, copy, modify, and/or distribute this software for any |
|
// purpose with or without fee is hereby granted, provided that the above |
|
// copyright notice and this permission notice appear in all copies. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY |
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION |
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
|
|
package main |
|
|
|
import ( |
|
"errors" |
|
"flag" |
|
"fmt" |
|
"io/ioutil" |
|
"log" |
|
"net" |
|
"os" |
|
"path" |
|
"strings" |
|
|
|
"golang.org/x/crypto/cryptobyte" |
|
"golang.org/x/net/dns/dnsmessage" |
|
) |
|
|
|
const ( |
|
httpsType = 65 // RRTYPE for HTTPS records. |
|
|
|
// SvcParamKey codepoints defined in draft-ietf-dnsop-svcb-https-06. |
|
httpsKeyMandatory = 0 |
|
httpsKeyALPN = 1 |
|
httpsKeyNoDefaultALPN = 2 |
|
httpsKeyPort = 3 |
|
httpsKeyIPV4Hint = 4 |
|
httpsKeyECH = 5 |
|
httpsKeyIPV6Hint = 6 |
|
) |
|
|
|
var ( |
|
name = flag.String("name", "", "The name to look up in DNS. Required.") |
|
server = flag.String("server", "8.8.8.8:53", "Comma-separated host and UDP port that defines the DNS server to query.") |
|
outDir = flag.String("out-dir", "", "The directory where ECHConfigList values will be written. If unspecified, bytes are hexdumped to stdout.") |
|
) |
|
|
|
type httpsRecord struct { |
|
priority uint16 |
|
targetName string |
|
|
|
// SvcParams: |
|
mandatory []uint16 |
|
alpn []string |
|
noDefaultALPN bool |
|
hasPort bool |
|
port uint16 |
|
ipv4hint []net.IP |
|
ech []byte |
|
ipv6hint []net.IP |
|
unknownParams map[uint16][]byte |
|
} |
|
|
|
// String pretty-prints |h| as a multi-line string with bullet points. |
|
func (h httpsRecord) String() string { |
|
var b strings.Builder |
|
fmt.Fprintf(&b, "HTTPS SvcPriority:%d TargetName:%q", h.priority, h.targetName) |
|
|
|
if len(h.mandatory) != 0 { |
|
fmt.Fprintf(&b, "\n * mandatory: %v", h.mandatory) |
|
} |
|
if len(h.alpn) != 0 { |
|
fmt.Fprintf(&b, "\n * alpn: %q", h.alpn) |
|
} |
|
if h.noDefaultALPN { |
|
fmt.Fprint(&b, "\n * no-default-alpn") |
|
} |
|
if h.hasPort { |
|
fmt.Fprintf(&b, "\n * port: %d", h.port) |
|
} |
|
if len(h.ipv4hint) != 0 { |
|
fmt.Fprintf(&b, "\n * ipv4hint:") |
|
for _, address := range h.ipv4hint { |
|
fmt.Fprintf(&b, "\n - %s", address) |
|
} |
|
} |
|
if len(h.ech) != 0 { |
|
fmt.Fprintf(&b, "\n * ech: %x", h.ech) |
|
} |
|
if len(h.ipv6hint) != 0 { |
|
fmt.Fprintf(&b, "\n * ipv6hint:") |
|
for _, address := range h.ipv6hint { |
|
fmt.Fprintf(&b, "\n - %s", address) |
|
} |
|
} |
|
if len(h.unknownParams) != 0 { |
|
fmt.Fprint(&b, "\n * unknown SvcParams:") |
|
for key, value := range h.unknownParams { |
|
fmt.Fprintf(&b, "\n - %d: %x", key, value) |
|
} |
|
} |
|
return b.String() |
|
} |
|
|
|
// dnsQueryForHTTPS queries the DNS server over UDP for any HTTPS records |
|
// associated with |domain|. It scans the response's answers and returns all the |
|
// HTTPS records it finds. It returns an error if any connection steps fail. |
|
func dnsQueryForHTTPS(domain string) ([][]byte, error) { |
|
udpAddr, err := net.ResolveUDPAddr("udp", *server) |
|
if err != nil { |
|
return nil, err |
|
} |
|
conn, err := net.DialUDP("udp", nil, udpAddr) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to dial: %s", err) |
|
} |
|
defer conn.Close() |
|
|
|
// Domain name must be canonical or message packing will fail. |
|
if domain[len(domain)-1] != '.' { |
|
domain += "." |
|
} |
|
dnsName, err := dnsmessage.NewName(domain) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to create DNS name from %q: %s", domain, err) |
|
} |
|
question := dnsmessage.Question{ |
|
Name: dnsName, |
|
Type: httpsType, |
|
Class: dnsmessage.ClassINET, |
|
} |
|
msg := dnsmessage.Message{ |
|
Header: dnsmessage.Header{ |
|
RecursionDesired: true, |
|
}, |
|
Questions: []dnsmessage.Question{question}, |
|
} |
|
packedMsg, err := msg.Pack() |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to pack msg: %s", err) |
|
} |
|
|
|
if _, err = conn.Write(packedMsg); err != nil { |
|
return nil, fmt.Errorf("failed to send the DNS query: %s", err) |
|
} |
|
|
|
for { |
|
response := make([]byte, 512) |
|
n, err := conn.Read(response) |
|
if err != nil { |
|
return nil, fmt.Errorf("failed to read the DNS response: %s", err) |
|
} |
|
response = response[:n] |
|
|
|
var p dnsmessage.Parser |
|
header, err := p.Start(response) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if !header.Response { |
|
return nil, errors.New("received DNS message is not a response") |
|
} |
|
if header.RCode != dnsmessage.RCodeSuccess { |
|
return nil, fmt.Errorf("response from DNS has non-success RCode: %s", header.RCode.String()) |
|
} |
|
if header.ID != 0 { |
|
return nil, errors.New("received a DNS response with the wrong ID") |
|
} |
|
if !header.RecursionAvailable { |
|
return nil, errors.New("server does not support recursion") |
|
} |
|
// Verify that this response answers the question that we asked in the |
|
// query. If the resolver encountered any CNAMEs, it's not guaranteed |
|
// that the response will contain a question with the same QNAME as our |
|
// query. However, RFC 8499 Section 4 indicates that in general use, the |
|
// response's QNAME should match the query, so we will make that |
|
// assumption. |
|
q, err := p.Question() |
|
if err != nil { |
|
return nil, err |
|
} |
|
if q != question { |
|
return nil, fmt.Errorf("response answers the wrong question: %v", q) |
|
} |
|
if q, err = p.Question(); err != dnsmessage.ErrSectionDone { |
|
return nil, fmt.Errorf("response contains an unexpected question: %v", q) |
|
} |
|
|
|
var httpsRecords [][]byte |
|
for { |
|
h, err := p.AnswerHeader() |
|
if err == dnsmessage.ErrSectionDone { |
|
break |
|
} |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
switch h.Type { |
|
case httpsType: |
|
// This should continue to work when golang.org/x/net/dns/dnsmessage |
|
// adds support for HTTPS records. |
|
r, err := p.UnknownResource() |
|
if err != nil { |
|
return nil, err |
|
} |
|
httpsRecords = append(httpsRecords, r.Data) |
|
default: |
|
if _, err := p.UnknownResource(); err != nil { |
|
return nil, err |
|
} |
|
} |
|
} |
|
return httpsRecords, nil |
|
} |
|
} |
|
|
|
// parseHTTPSRecord parses an HTTPS record (draft-ietf-dnsop-svcb-https-06, |
|
// Section 2.2) from |raw|. If there are syntax errors, it returns an error. |
|
func parseHTTPSRecord(raw []byte) (httpsRecord, error) { |
|
reader := cryptobyte.String(raw) |
|
|
|
var priority uint16 |
|
if !reader.ReadUint16(&priority) { |
|
return httpsRecord{}, errors.New("failed to parse HTTPS record priority") |
|
} |
|
|
|
// Read the TargetName. |
|
var dottedDomain string |
|
for { |
|
var label cryptobyte.String |
|
if !reader.ReadUint8LengthPrefixed(&label) { |
|
return httpsRecord{}, errors.New("failed to parse HTTPS record TargetName") |
|
} |
|
if label.Empty() { |
|
break |
|
} |
|
dottedDomain += string(label) + "." |
|
} |
|
|
|
if priority == 0 { |
|
// TODO(dmcardle) Recursively follow AliasForm records. |
|
return httpsRecord{}, fmt.Errorf("received an AliasForm HTTPS record with TargetName=%q", dottedDomain) |
|
} |
|
|
|
record := httpsRecord{ |
|
priority: priority, |
|
targetName: dottedDomain, |
|
unknownParams: make(map[uint16][]byte), |
|
} |
|
|
|
// Read the SvcParams. |
|
var lastSvcParamKey uint16 |
|
for svcParamCount := 0; !reader.Empty(); svcParamCount++ { |
|
var svcParamKey uint16 |
|
var svcParamValue cryptobyte.String |
|
if !reader.ReadUint16(&svcParamKey) || |
|
!reader.ReadUint16LengthPrefixed(&svcParamValue) { |
|
return httpsRecord{}, errors.New("failed to parse HTTPS record SvcParam") |
|
} |
|
if svcParamCount > 0 && svcParamKey <= lastSvcParamKey { |
|
return httpsRecord{}, errors.New("malformed HTTPS record contains out-of-order SvcParamKey") |
|
} |
|
lastSvcParamKey = svcParamKey |
|
|
|
switch svcParamKey { |
|
case httpsKeyMandatory: |
|
if svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed mandatory SvcParamValue") |
|
} |
|
var lastKey uint16 |
|
for !svcParamValue.Empty() { |
|
// |httpsKeyMandatory| may not appear in the mandatory list. |
|
// |httpsKeyMandatory| is zero, so checking against the initial |
|
// value of |lastKey| handles ordering and the invalid code point. |
|
var key uint16 |
|
if !svcParamValue.ReadUint16(&key) || |
|
key <= lastKey { |
|
return httpsRecord{}, errors.New("malformed mandatory SvcParamValue") |
|
} |
|
lastKey = key |
|
record.mandatory = append(record.mandatory, key) |
|
} |
|
case httpsKeyALPN: |
|
if svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed alpn SvcParamValue") |
|
} |
|
for !svcParamValue.Empty() { |
|
var alpn cryptobyte.String |
|
if !svcParamValue.ReadUint8LengthPrefixed(&alpn) || alpn.Empty() { |
|
return httpsRecord{}, errors.New("malformed alpn SvcParamValue") |
|
} |
|
record.alpn = append(record.alpn, string(alpn)) |
|
} |
|
case httpsKeyNoDefaultALPN: |
|
if !svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed no-default-alpn SvcParamValue") |
|
} |
|
record.noDefaultALPN = true |
|
case httpsKeyPort: |
|
if !svcParamValue.ReadUint16(&record.port) || |
|
!svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed port SvcParamValue") |
|
} |
|
record.hasPort = true |
|
case httpsKeyIPV4Hint: |
|
if svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed ipv4hint SvcParamValue") |
|
} |
|
for !svcParamValue.Empty() { |
|
var address []byte |
|
if !svcParamValue.ReadBytes(&address, 4) { |
|
return httpsRecord{}, errors.New("malformed ipv4hint SvcParamValue") |
|
} |
|
record.ipv4hint = append(record.ipv4hint, address) |
|
} |
|
case httpsKeyECH: |
|
if svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed ech SvcParamValue") |
|
} |
|
record.ech = svcParamValue |
|
case httpsKeyIPV6Hint: |
|
if svcParamValue.Empty() { |
|
return httpsRecord{}, errors.New("malformed ipv6hint SvcParamValue") |
|
} |
|
for !svcParamValue.Empty() { |
|
var address []byte |
|
if !svcParamValue.ReadBytes(&address, 16) { |
|
return httpsRecord{}, errors.New("malformed ipv6hint SvcParamValue") |
|
} |
|
record.ipv6hint = append(record.ipv6hint, address) |
|
} |
|
default: |
|
record.unknownParams[svcParamKey] = svcParamValue |
|
} |
|
} |
|
return record, nil |
|
} |
|
|
|
func main() { |
|
flag.Parse() |
|
log.SetFlags(log.Lshortfile | log.LstdFlags) |
|
|
|
if len(*name) == 0 { |
|
flag.Usage() |
|
os.Exit(1) |
|
} |
|
|
|
httpsRecords, err := dnsQueryForHTTPS(*name) |
|
if err != nil { |
|
log.Printf("Error querying %q: %s\n", *name, err) |
|
os.Exit(1) |
|
} |
|
if len(httpsRecords) == 0 { |
|
log.Println("No HTTPS records found in DNS response.") |
|
os.Exit(1) |
|
} |
|
|
|
if len(*outDir) > 0 { |
|
if err = os.Mkdir(*outDir, 0755); err != nil && !os.IsExist(err) { |
|
log.Printf("Failed to create out directory %q: %s\n", *outDir, err) |
|
os.Exit(1) |
|
} |
|
} |
|
|
|
var echConfigListCount int |
|
for _, httpsRecord := range httpsRecords { |
|
record, err := parseHTTPSRecord(httpsRecord) |
|
if err != nil { |
|
log.Printf("Failed to parse HTTPS record: %s", err) |
|
os.Exit(1) |
|
} |
|
fmt.Printf("%s\n", record) |
|
if len(*outDir) == 0 { |
|
continue |
|
} |
|
|
|
outFile := path.Join(*outDir, fmt.Sprintf("ech-config-list-%d", echConfigListCount)) |
|
if err = ioutil.WriteFile(outFile, record.ech, 0644); err != nil { |
|
log.Printf("Failed to write file: %s\n", err) |
|
os.Exit(1) |
|
} |
|
fmt.Printf("Wrote ECHConfigList to %q\n", outFile) |
|
echConfigListCount++ |
|
} |
|
}
|
|
|