This is a reland of 160a8891ae
with go.mod
and go.sum fixed. This updates golang.org/x/crypto, adds the latest
golang.org/x/net as a direct dependency (it was previously an indirect
dependency via x/crypto), and cleans up stale entries from go.sum with
go mod tidy.
Original change's description:
> Add util/fetch_ech_config_list.go
>
> I wrote this tool to make it easier to test the ECH client against
> real-world servers with the bssl client tool. I found that manually
> extracting an ECHConfigList from a raw HTTPS record is unnecessarily
> painful.
>
> The tool queries DNS over UDP for HTTPS records. If it finds any HTTPS
> records in the response, it attempts to extract an ECHConfigList from
> the "ech" SvcParam. It can write each extracted ECHConfigList to a file
> in a given directory. Once the ECH client implementation lands, the bssl
> client tool should have a new flag that that takes the path to an
> ECHConfigList file.
>
> I am using golang.org/x/net/dns/dnsmessage to parse the DNS response. I
> recently added the |UnknownResource| type to this library to enable
> callers (like us) to extract the bytes of otherwise-unsupported records
> (like HTTPS). I updated the dependency with `go get -u golang.org/x/net`.
>
> Although the bssl client tool knows how to resolve the address of its
> "-connect" parameter, it is difficult to query HTTPS records in a
> platform-agnostic way. If we decide the bssl client should directly
> query HTTPS rather than leaning on fetch_ech_config_list.go, we should
> look into libresolv. Specifically, the |res_query| function enables the
> caller to query arbitrary record types. This may open its own can of
> cross-platform worms; macOS and Linux typically ship with different
> implementations and it is not available on Windows. For more info, see
> `man 3 resolver`.
>
> Bug: 275
> Change-Id: I705591658921f60a958164a18b68ffb697c2ea4b
> Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/44104
> Reviewed-by: David Benjamin <davidben@google.com>
Bug: 275
Change-Id: I9571e96c7a2ad7e239d86a353929a4e556d71287
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/48106
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
grpc-202302
parent
9a5abe05cc
commit
995574c225
3 changed files with 406 additions and 9 deletions
@ -1,8 +1,13 @@ |
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= |
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= |
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= |
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= |
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= |
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= |
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= |
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= |
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
|
@ -0,0 +1,389 @@ |
||||
// 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, RFC8499 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++ |
||||
} |
||||
} |
Loading…
Reference in new issue