mirror of
https://github.com/golang/net.git
synced 2026-03-31 18:37:08 +09:00
dns/dnsmessage: add https svcb dns types
This change implements the proposal to add new DNS message types HTTPS and
SVCB in the golang.org/x/net/dns/dnsmessage package, as described in
golang/go#43790.
The implementation includes:
- New types TypeHTTPS and TypeSVCB.
- SVCBResource and HTTPSResource structs, with HTTPSResource embedding
SVCBResource.
- SVCParam and SVCParamKey types for handling service parameters.
- pack and unpack methods for the new resource types.
- Integration into the Parser and Builder.
- Comprehensive tests, including for parameter handling logic.
I implemented the SVCB parsing code so that it performs only two
allocations: one for the []SVCParam slice, and one to hold the SVCParam
values. A test was added to demonstrate that.
Fixes golang/go#43790
Change-Id: I60439772fe0e339ae3141bd1dd9829564efe0f2a
GitHub-Last-Rev: 49c2ac0102
GitHub-Pull-Request: golang/net#241
Reviewed-on: https://go-review.googlesource.com/c/net/+/710736
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Vinicius Fortuna <fortuna@google.com>
This commit is contained in:
committed by
t hepudds
parent
63d1a5100f
commit
bb2055dafd
@@ -17,8 +17,21 @@ import (
|
||||
)
|
||||
|
||||
// Message formats
|
||||
//
|
||||
// To add a new Resource Record type:
|
||||
// 1. Create Resource Record types
|
||||
// 1.1. Add a Type constant named "Type<name>"
|
||||
// 1.2. Add the corresponding entry to the typeNames map
|
||||
// 1.3. Add a [ResourceBody] implementation named "<name>Resource"
|
||||
// 2. Implement packing
|
||||
// 2.1. Implement Builder.<name>Resource()
|
||||
// 3. Implement unpacking
|
||||
// 3.1. Add the unpacking code to unpackResourceBody()
|
||||
// 3.2. Implement Parser.<name>Resource()
|
||||
|
||||
// A Type is a type of DNS request and response.
|
||||
// A Type is the type of a DNS Resource Record, as defined in the [IANA registry].
|
||||
//
|
||||
// [IANA registry]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4
|
||||
type Type uint16
|
||||
|
||||
const (
|
||||
@@ -33,6 +46,8 @@ const (
|
||||
TypeAAAA Type = 28
|
||||
TypeSRV Type = 33
|
||||
TypeOPT Type = 41
|
||||
TypeSVCB Type = 64
|
||||
TypeHTTPS Type = 65
|
||||
|
||||
// Question.Type
|
||||
TypeWKS Type = 11
|
||||
@@ -53,6 +68,8 @@ var typeNames = map[Type]string{
|
||||
TypeAAAA: "TypeAAAA",
|
||||
TypeSRV: "TypeSRV",
|
||||
TypeOPT: "TypeOPT",
|
||||
TypeSVCB: "TypeSVCB",
|
||||
TypeHTTPS: "TypeHTTPS",
|
||||
TypeWKS: "TypeWKS",
|
||||
TypeHINFO: "TypeHINFO",
|
||||
TypeMINFO: "TypeMINFO",
|
||||
@@ -273,6 +290,7 @@ var (
|
||||
errTooManyAdditionals = errors.New("too many Additionals to pack (>65535)")
|
||||
errNonCanonicalName = errors.New("name is not in canonical format (it must end with a .)")
|
||||
errStringTooLong = errors.New("character string exceeds maximum length (255)")
|
||||
errParamOutOfOrder = errors.New("parameter out of order")
|
||||
)
|
||||
|
||||
// Internal constants.
|
||||
@@ -2220,6 +2238,16 @@ func unpackResourceBody(msg []byte, off int, hdr ResourceHeader) (ResourceBody,
|
||||
rb, err = unpackSRVResource(msg, off)
|
||||
r = &rb
|
||||
name = "SRV"
|
||||
case TypeSVCB:
|
||||
var rb SVCBResource
|
||||
rb, err = unpackSVCBResource(msg, off, hdr.Length)
|
||||
r = &rb
|
||||
name = "SVCB"
|
||||
case TypeHTTPS:
|
||||
var rb HTTPSResource
|
||||
rb.SVCBResource, err = unpackSVCBResource(msg, off, hdr.Length)
|
||||
r = &rb
|
||||
name = "HTTPS"
|
||||
case TypeOPT:
|
||||
var rb OPTResource
|
||||
rb, err = unpackOPTResource(msg, off, hdr.Length)
|
||||
|
||||
@@ -363,6 +363,49 @@ func TestResourceNotStarted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestSVCBMsg() Message {
|
||||
svcb := &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("svc.example.com."),
|
||||
Params: []SVCParam{{Key: SVCParamALPN, Value: []byte("h2")}},
|
||||
}
|
||||
|
||||
https := &HTTPSResource{
|
||||
SVCBResource{
|
||||
Priority: 2,
|
||||
Target: MustNewName("https.example.com."),
|
||||
Params: []SVCParam{
|
||||
{Key: SVCParamPort, Value: []byte{0x01, 0xbb}},
|
||||
{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return Message{
|
||||
Questions: []Question{},
|
||||
Answers: []Resource{
|
||||
{
|
||||
ResourceHeader{
|
||||
Name: MustNewName("foo.bar.example.com."),
|
||||
Type: TypeSVCB,
|
||||
Class: ClassINET,
|
||||
},
|
||||
svcb,
|
||||
},
|
||||
{
|
||||
ResourceHeader{
|
||||
Name: MustNewName("foo.bar.example.com."),
|
||||
Type: TypeHTTPS,
|
||||
Class: ClassINET,
|
||||
},
|
||||
https,
|
||||
},
|
||||
},
|
||||
Authorities: []Resource{},
|
||||
Additionals: []Resource{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSPackUnpack(t *testing.T) {
|
||||
wants := []Message{
|
||||
{
|
||||
@@ -378,6 +421,7 @@ func TestDNSPackUnpack(t *testing.T) {
|
||||
Additionals: []Resource{},
|
||||
},
|
||||
largeTestMsg(),
|
||||
buildTestSVCBMsg(),
|
||||
}
|
||||
for i, want := range wants {
|
||||
b, err := want.Pack()
|
||||
@@ -390,7 +434,14 @@ func TestDNSPackUnpack(t *testing.T) {
|
||||
t.Fatalf("%d: Message.Unapck() = %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%d: Message.Pack/Unpack() roundtrip: got = %+v, want = %+v", i, &got, &want)
|
||||
t.Errorf("%d: Message.Pack/Unpack() roundtrip: got = %#v, want = %#v", i, &got, &want)
|
||||
if len(got.Answers) > 0 && len(want.Answers) > 0 {
|
||||
if !reflect.DeepEqual(got.Answers[0].Body, want.Answers[0].Body) {
|
||||
t.Errorf("Answer 0 Body mismatch")
|
||||
t.Errorf("got: %#v", got.Answers[0].Body)
|
||||
t.Errorf("want: %#v", want.Answers[0].Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,16 +735,19 @@ func TestBuilderResourceError(t *testing.T) {
|
||||
name string
|
||||
fn func(*Builder) error
|
||||
}{
|
||||
{"CNAMEResource", func(b *Builder) error { return b.CNAMEResource(ResourceHeader{}, CNAMEResource{}) }},
|
||||
{"MXResource", func(b *Builder) error { return b.MXResource(ResourceHeader{}, MXResource{}) }},
|
||||
{"NSResource", func(b *Builder) error { return b.NSResource(ResourceHeader{}, NSResource{}) }},
|
||||
{"PTRResource", func(b *Builder) error { return b.PTRResource(ResourceHeader{}, PTRResource{}) }},
|
||||
{"SOAResource", func(b *Builder) error { return b.SOAResource(ResourceHeader{}, SOAResource{}) }},
|
||||
{"TXTResource", func(b *Builder) error { return b.TXTResource(ResourceHeader{}, TXTResource{}) }},
|
||||
{"SRVResource", func(b *Builder) error { return b.SRVResource(ResourceHeader{}, SRVResource{}) }},
|
||||
// Keep it sorted by resource type name.
|
||||
{"AResource", func(b *Builder) error { return b.AResource(ResourceHeader{}, AResource{}) }},
|
||||
{"AAAAResource", func(b *Builder) error { return b.AAAAResource(ResourceHeader{}, AAAAResource{}) }},
|
||||
{"CNAMEResource", func(b *Builder) error { return b.CNAMEResource(ResourceHeader{}, CNAMEResource{}) }},
|
||||
{"HTTPSResource", func(b *Builder) error { return b.HTTPSResource(ResourceHeader{}, HTTPSResource{}) }},
|
||||
{"MXResource", func(b *Builder) error { return b.MXResource(ResourceHeader{}, MXResource{}) }},
|
||||
{"NSResource", func(b *Builder) error { return b.NSResource(ResourceHeader{}, NSResource{}) }},
|
||||
{"OPTResource", func(b *Builder) error { return b.OPTResource(ResourceHeader{}, OPTResource{}) }},
|
||||
{"PTRResource", func(b *Builder) error { return b.PTRResource(ResourceHeader{}, PTRResource{}) }},
|
||||
{"SOAResource", func(b *Builder) error { return b.SOAResource(ResourceHeader{}, SOAResource{}) }},
|
||||
{"SRVResource", func(b *Builder) error { return b.SRVResource(ResourceHeader{}, SRVResource{}) }},
|
||||
{"SVCBResource", func(b *Builder) error { return b.SVCBResource(ResourceHeader{}, SVCBResource{}) }},
|
||||
{"TXTResource", func(b *Builder) error { return b.TXTResource(ResourceHeader{}, TXTResource{}) }},
|
||||
{"UnknownResource", func(b *Builder) error { return b.UnknownResource(ResourceHeader{}, UnknownResource{}) }},
|
||||
}
|
||||
|
||||
@@ -785,6 +839,14 @@ func TestBuilder(t *testing.T) {
|
||||
if err := b.SRVResource(a.Header, *a.Body.(*SRVResource)); err != nil {
|
||||
t.Fatalf("Builder.SRVResource(%#v) = %v", a, err)
|
||||
}
|
||||
case TypeSVCB:
|
||||
if err := b.SVCBResource(a.Header, *a.Body.(*SVCBResource)); err != nil {
|
||||
t.Fatalf("Builder.SVCBResource(%#v) = %v", a, err)
|
||||
}
|
||||
case TypeHTTPS:
|
||||
if err := b.HTTPSResource(a.Header, *a.Body.(*HTTPSResource)); err != nil {
|
||||
t.Fatalf("Builder.HTTPSResource(%#v) = %v", a, err)
|
||||
}
|
||||
case privateUseType:
|
||||
if err := b.UnknownResource(a.Header, *a.Body.(*UnknownResource)); err != nil {
|
||||
t.Fatalf("Builder.UnknownResource(%#v) = %v", a, err)
|
||||
@@ -1262,6 +1324,14 @@ func benchmarkParsing(tb testing.TB, buf []byte) {
|
||||
if _, err := p.NSResource(); err != nil {
|
||||
tb.Fatal("Parser.NSResource() =", err)
|
||||
}
|
||||
case TypeSVCB:
|
||||
if _, err := p.SVCBResource(); err != nil {
|
||||
tb.Fatal("Parser.SVCBResource() =", err)
|
||||
}
|
||||
case TypeHTTPS:
|
||||
if _, err := p.HTTPSResource(); err != nil {
|
||||
tb.Fatal("Parser.HTTPSResource() =", err)
|
||||
}
|
||||
case TypeOPT:
|
||||
if _, err := p.OPTResource(); err != nil {
|
||||
tb.Fatal("Parser.OPTResource() =", err)
|
||||
|
||||
321
dns/dnsmessage/svcb.go
Normal file
321
dns/dnsmessage/svcb.go
Normal file
@@ -0,0 +1,321 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dnsmessage
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An SVCBResource is an SVCB Resource record.
|
||||
type SVCBResource struct {
|
||||
Priority uint16
|
||||
Target Name
|
||||
Params []SVCParam // Must be in strict increasing order by Key.
|
||||
}
|
||||
|
||||
func (r *SVCBResource) realType() Type {
|
||||
return TypeSVCB
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer.GoString.
|
||||
func (r *SVCBResource) GoString() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("dnsmessage.SVCBResource{")
|
||||
b.WriteString("Priority: " + printUint16(r.Priority) + ", ")
|
||||
b.WriteString("Target: " + r.Target.GoString() + ", ")
|
||||
b.WriteString("Params: []dnsmessage.SVCParam{")
|
||||
if len(r.Params) > 0 {
|
||||
b.WriteString(r.Params[0].GoString())
|
||||
for _, p := range r.Params[1:] {
|
||||
b.WriteString(", " + p.GoString())
|
||||
}
|
||||
}
|
||||
b.WriteString("}}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// An HTTPSResource is an HTTPS Resource record.
|
||||
// It has the same format as the SVCB record.
|
||||
type HTTPSResource struct {
|
||||
// Alias for SVCB resource record.
|
||||
SVCBResource
|
||||
}
|
||||
|
||||
func (r *HTTPSResource) realType() Type {
|
||||
return TypeHTTPS
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer.GoString.
|
||||
func (r *HTTPSResource) GoString() string {
|
||||
return "dnsmessage.HTTPSResource{SVCBResource: " + r.SVCBResource.GoString() + "}"
|
||||
}
|
||||
|
||||
// GetParam returns a parameter value by key.
|
||||
func (r *SVCBResource) GetParam(key SVCParamKey) (value []byte, ok bool) {
|
||||
for i := range r.Params {
|
||||
if r.Params[i].Key == key {
|
||||
return r.Params[i].Value, true
|
||||
}
|
||||
if r.Params[i].Key > key {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SetParam sets a parameter value by key.
|
||||
// The Params list is kept sorted by key.
|
||||
func (r *SVCBResource) SetParam(key SVCParamKey, value []byte) {
|
||||
i := 0
|
||||
for i < len(r.Params) {
|
||||
if r.Params[i].Key >= key {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i < len(r.Params) && r.Params[i].Key == key {
|
||||
r.Params[i].Value = value
|
||||
return
|
||||
}
|
||||
|
||||
r.Params = slices.Insert(r.Params, i, SVCParam{Key: key, Value: value})
|
||||
}
|
||||
|
||||
// DeleteParam deletes a parameter by key.
|
||||
// It returns true if the parameter was present.
|
||||
func (r *SVCBResource) DeleteParam(key SVCParamKey) bool {
|
||||
for i := range r.Params {
|
||||
if r.Params[i].Key == key {
|
||||
r.Params = slices.Delete(r.Params, i, i+1)
|
||||
return true
|
||||
}
|
||||
if r.Params[i].Key > key {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A SVCParam is a service parameter.
|
||||
type SVCParam struct {
|
||||
Key SVCParamKey
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer.GoString.
|
||||
func (p SVCParam) GoString() string {
|
||||
return "dnsmessage.SVCParam{" +
|
||||
"Key: " + p.Key.GoString() + ", " +
|
||||
"Value: []byte{" + printByteSlice(p.Value) + "}}"
|
||||
}
|
||||
|
||||
// A SVCParamKey is a key for a service parameter.
|
||||
type SVCParamKey uint16
|
||||
|
||||
// Values defined at https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml#dns-svcparamkeys.
|
||||
const (
|
||||
SVCParamMandatory SVCParamKey = 0
|
||||
SVCParamALPN SVCParamKey = 1
|
||||
SVCParamNoDefaultALPN SVCParamKey = 2
|
||||
SVCParamPort SVCParamKey = 3
|
||||
SVCParamIPv4Hint SVCParamKey = 4
|
||||
SVCParamECH SVCParamKey = 5
|
||||
SVCParamIPv6Hint SVCParamKey = 6
|
||||
SVCParamDOHPath SVCParamKey = 7
|
||||
SVCParamOHTTP SVCParamKey = 8
|
||||
SVCParamTLSSupportedGroups SVCParamKey = 9
|
||||
)
|
||||
|
||||
var svcParamKeyNames = map[SVCParamKey]string{
|
||||
SVCParamMandatory: "Mandatory",
|
||||
SVCParamALPN: "ALPN",
|
||||
SVCParamNoDefaultALPN: "NoDefaultALPN",
|
||||
SVCParamPort: "Port",
|
||||
SVCParamIPv4Hint: "IPv4Hint",
|
||||
SVCParamECH: "ECH",
|
||||
SVCParamIPv6Hint: "IPv6Hint",
|
||||
SVCParamDOHPath: "DOHPath",
|
||||
SVCParamOHTTP: "OHTTP",
|
||||
SVCParamTLSSupportedGroups: "TLSSupportedGroups",
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.String.
|
||||
func (k SVCParamKey) String() string {
|
||||
if n, ok := svcParamKeyNames[k]; ok {
|
||||
return n
|
||||
}
|
||||
return printUint16(uint16(k))
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer.GoString.
|
||||
func (k SVCParamKey) GoString() string {
|
||||
if n, ok := svcParamKeyNames[k]; ok {
|
||||
return "dnsmessage.SVCParam" + n
|
||||
}
|
||||
return printUint16(uint16(k))
|
||||
}
|
||||
|
||||
func (r *SVCBResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) {
|
||||
oldMsg := msg
|
||||
msg = packUint16(msg, r.Priority)
|
||||
msg, err := r.Target.pack(msg, compression, compressionOff)
|
||||
if err != nil {
|
||||
return oldMsg, &nestedError{"SVCBResource.Target", err}
|
||||
}
|
||||
var previousKey SVCParamKey
|
||||
for i, param := range r.Params {
|
||||
if i > 0 && param.Key <= previousKey {
|
||||
return oldMsg, &nestedError{"SVCBResource.Params", errParamOutOfOrder}
|
||||
}
|
||||
msg = packUint16(msg, uint16(param.Key))
|
||||
msg = packUint16(msg, uint16(len(param.Value)))
|
||||
msg = append(msg, param.Value...)
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func unpackSVCBResource(msg []byte, off int, length uint16) (SVCBResource, error) {
|
||||
// Wire format reference: https://www.rfc-editor.org/rfc/rfc9460.html#section-2.2.
|
||||
r := SVCBResource{}
|
||||
paramsOff := off
|
||||
bodyEnd := off + int(length)
|
||||
|
||||
var err error
|
||||
if r.Priority, paramsOff, err = unpackUint16(msg, paramsOff); err != nil {
|
||||
return SVCBResource{}, &nestedError{"Priority", err}
|
||||
}
|
||||
|
||||
if paramsOff, err = r.Target.unpack(msg, paramsOff); err != nil {
|
||||
return SVCBResource{}, &nestedError{"Target", err}
|
||||
}
|
||||
|
||||
// Two-pass parsing to avoid allocations.
|
||||
// First, count the number of params.
|
||||
n := 0
|
||||
var totalValueLen uint16
|
||||
off = paramsOff
|
||||
var previousKey uint16
|
||||
for off < bodyEnd {
|
||||
var key, len uint16
|
||||
if key, off, err = unpackUint16(msg, off); err != nil {
|
||||
return SVCBResource{}, &nestedError{"Params key", err}
|
||||
}
|
||||
if n > 0 && key <= previousKey {
|
||||
// As per https://www.rfc-editor.org/rfc/rfc9460.html#section-2.2, clients MUST
|
||||
// consider the RR malformed if the SvcParamKeys are not in strictly increasing numeric order
|
||||
return SVCBResource{}, &nestedError{"Params", errParamOutOfOrder}
|
||||
}
|
||||
if len, off, err = unpackUint16(msg, off); err != nil {
|
||||
return SVCBResource{}, &nestedError{"Params value length", err}
|
||||
}
|
||||
if off+int(len) > bodyEnd {
|
||||
return SVCBResource{}, errResourceLen
|
||||
}
|
||||
totalValueLen += len
|
||||
off += int(len)
|
||||
n++
|
||||
}
|
||||
if off != bodyEnd {
|
||||
return SVCBResource{}, errResourceLen
|
||||
}
|
||||
|
||||
// Second, fill in the params.
|
||||
r.Params = make([]SVCParam, n)
|
||||
// valuesBuf is used to hold all param values to reduce allocations.
|
||||
// Each param's Value slice will point into this buffer.
|
||||
valuesBuf := make([]byte, totalValueLen)
|
||||
off = paramsOff
|
||||
for i := 0; i < n; i++ {
|
||||
p := &r.Params[i]
|
||||
var key, len uint16
|
||||
if key, off, err = unpackUint16(msg, off); err != nil {
|
||||
return SVCBResource{}, &nestedError{"param key", err}
|
||||
}
|
||||
p.Key = SVCParamKey(key)
|
||||
if len, off, err = unpackUint16(msg, off); err != nil {
|
||||
return SVCBResource{}, &nestedError{"param length", err}
|
||||
}
|
||||
if copy(valuesBuf, msg[off:off+int(len)]) != int(len) {
|
||||
return SVCBResource{}, &nestedError{"param value", errCalcLen}
|
||||
}
|
||||
p.Value = valuesBuf[:len:len]
|
||||
valuesBuf = valuesBuf[len:]
|
||||
off += int(len)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// genericSVCBResource parses a single Resource Record compatible with SVCB.
|
||||
func (p *Parser) genericSVCBResource(svcbType Type) (SVCBResource, error) {
|
||||
if !p.resHeaderValid || p.resHeaderType != svcbType {
|
||||
return SVCBResource{}, ErrNotStarted
|
||||
}
|
||||
r, err := unpackSVCBResource(p.msg, p.off, p.resHeaderLength)
|
||||
if err != nil {
|
||||
return SVCBResource{}, err
|
||||
}
|
||||
p.off += int(p.resHeaderLength)
|
||||
p.resHeaderValid = false
|
||||
p.index++
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// SVCBResource parses a single SVCBResource.
|
||||
//
|
||||
// One of the XXXHeader methods must have been called before calling this
|
||||
// method.
|
||||
func (p *Parser) SVCBResource() (SVCBResource, error) {
|
||||
return p.genericSVCBResource(TypeSVCB)
|
||||
}
|
||||
|
||||
// HTTPSResource parses a single HTTPSResource.
|
||||
//
|
||||
// One of the XXXHeader methods must have been called before calling this
|
||||
// method.
|
||||
func (p *Parser) HTTPSResource() (HTTPSResource, error) {
|
||||
svcb, err := p.genericSVCBResource(TypeHTTPS)
|
||||
if err != nil {
|
||||
return HTTPSResource{}, err
|
||||
}
|
||||
return HTTPSResource{svcb}, nil
|
||||
}
|
||||
|
||||
// genericSVCBResource is the generic implementation for adding SVCB-like resources.
|
||||
func (b *Builder) genericSVCBResource(h ResourceHeader, r SVCBResource) error {
|
||||
if err := b.checkResourceSection(); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, lenOff, err := h.pack(b.msg, b.compression, b.start)
|
||||
if err != nil {
|
||||
return &nestedError{"ResourceHeader", err}
|
||||
}
|
||||
preLen := len(msg)
|
||||
if msg, err = r.pack(msg, b.compression, b.start); err != nil {
|
||||
return &nestedError{"ResourceBody", err}
|
||||
}
|
||||
if err := h.fixLen(msg, lenOff, preLen); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.incrementSectionCount(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.msg = msg
|
||||
return nil
|
||||
}
|
||||
|
||||
// SVCBResource adds a single SVCBResource.
|
||||
func (b *Builder) SVCBResource(h ResourceHeader, r SVCBResource) error {
|
||||
h.Type = r.realType()
|
||||
return b.genericSVCBResource(h, r)
|
||||
}
|
||||
|
||||
// HTTPSResource adds a single HTTPSResource.
|
||||
func (b *Builder) HTTPSResource(h ResourceHeader, r HTTPSResource) error {
|
||||
h.Type = r.realType()
|
||||
return b.genericSVCBResource(h, r.SVCBResource)
|
||||
}
|
||||
366
dns/dnsmessage/svcb_test.go
Normal file
366
dns/dnsmessage/svcb_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dnsmessage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSVCBParamsRoundTrip(t *testing.T) {
|
||||
testSVCBParam := func(t *testing.T, p *SVCParam) {
|
||||
t.Helper()
|
||||
rr := &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("svc.example.com."),
|
||||
Params: []SVCParam{*p},
|
||||
}
|
||||
buf, err := rr.pack([]byte{}, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("pack() = %v", err)
|
||||
}
|
||||
got, n, err := unpackResourceBody(buf, 0, ResourceHeader{Type: TypeSVCB, Length: uint16(len(buf))})
|
||||
if err != nil {
|
||||
t.Fatalf("unpackResourceBody() = %v", err)
|
||||
}
|
||||
if n != len(buf) {
|
||||
t.Fatalf("unpacked different amount than packed: got = %d, want = %d", n, len(buf))
|
||||
}
|
||||
if !reflect.DeepEqual(got, rr) {
|
||||
t.Fatalf("roundtrip mismatch: got = %#v, want = %#v", got, rr)
|
||||
}
|
||||
}
|
||||
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x03, 0x00, 0x05}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamALPN, Value: []byte{0x02, 'h', '2', 0x02, 'h', '3'}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamNoDefaultALPN, Value: []byte{}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamPort, Value: []byte{0x1f, 0x90}}) // 8080
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1, 198, 51, 100, 2}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamECH, Value: []byte{0x01, 0x02, 0x03, 0x04}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamDOHPath, Value: []byte("/dns-query{?dns}")})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamOHTTP, Value: []byte{0x00, 0x01, 0x02, 0x03}})
|
||||
testSVCBParam(t, &SVCParam{Key: SVCParamTLSSupportedGroups, Value: []byte{0x00, 0x1d, 0x00, 0x17}})
|
||||
}
|
||||
|
||||
func TestSVCBParsingAllocs(t *testing.T) {
|
||||
name := MustNewName("foo.bar.example.com.")
|
||||
msg := Message{
|
||||
Header: Header{Response: true, Authoritative: true},
|
||||
Questions: []Question{{Name: name, Type: TypeA, Class: ClassINET}},
|
||||
Answers: []Resource{{
|
||||
Header: ResourceHeader{Name: name, Type: TypeSVCB, Class: ClassINET, TTL: 300},
|
||||
Body: &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("svc.example.com."),
|
||||
Params: []SVCParam{
|
||||
{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x03, 0x00, 0x05}},
|
||||
{Key: SVCParamALPN, Value: []byte{0x02, 'h', '2', 0x02, 'h', '3'}},
|
||||
{Key: SVCParamPort, Value: []byte{0x1f, 0x90}}, // 8080
|
||||
{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1, 198, 51, 100, 2}},
|
||||
{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
allocs := int(testing.AllocsPerRun(1, func() {
|
||||
var p Parser
|
||||
if _, err := p.Start(buf); err != nil {
|
||||
t.Fatal("Parser.Start(non-nil) =", err)
|
||||
}
|
||||
if err := p.SkipAllQuestions(); err != nil {
|
||||
t.Fatal("Parser.SkipAllQuestions(non-nil) =", err)
|
||||
}
|
||||
if _, err = p.AnswerHeader(); err != nil {
|
||||
t.Fatal("Parser.AnswerHeader(non-nil) =", err)
|
||||
}
|
||||
if _, err = p.SVCBResource(); err != nil {
|
||||
t.Fatal("Parser.SVCBResource(non-nil) =", err)
|
||||
}
|
||||
}))
|
||||
|
||||
// Make sure we have only two allocations: one for the SVCBResource.Params slice, and one
|
||||
// for the SVCParam Values.
|
||||
if allocs != 2 {
|
||||
t.Errorf("allocations during parsing: got = %d, want 2", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSBuildAllocs(t *testing.T) {
|
||||
b := NewBuilder([]byte{}, Header{Response: true, Authoritative: true})
|
||||
b.EnableCompression()
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
t.Fatalf("StartQuestions() = %v", err)
|
||||
}
|
||||
if err := b.Question(Question{Name: MustNewName("foo.bar.example.com."), Type: TypeHTTPS, Class: ClassINET}); err != nil {
|
||||
t.Fatalf("Question() = %v", err)
|
||||
}
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
t.Fatalf("StartAnswers() = %v", err)
|
||||
}
|
||||
|
||||
header := ResourceHeader{Name: MustNewName("foo.bar.example.com."), Type: TypeHTTPS, Class: ClassINET, TTL: 300}
|
||||
resource := HTTPSResource{SVCBResource{Priority: 1, Target: MustNewName("svc.example.com.")}}
|
||||
|
||||
// AllocsPerRun runs the function once to "warm up" before running the measurement.
|
||||
// So technically this function is running twice, on different data, which can potentially
|
||||
// make the measurement inaccurate (e.g. by using the name cache the second time).
|
||||
// So we make sure we don't run in the warm-up phase.
|
||||
warmUp := true
|
||||
allocs := int(testing.AllocsPerRun(1, func() {
|
||||
if warmUp {
|
||||
warmUp = false
|
||||
return
|
||||
}
|
||||
if err := b.HTTPSResource(header, resource); err != nil {
|
||||
t.Fatalf("HTTPSResource() = %v", err)
|
||||
}
|
||||
}))
|
||||
if allocs != 1 {
|
||||
t.Fatalf("unexpected allocations: got = %d, want = 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSVCBParams(t *testing.T) {
|
||||
rr := SVCBResource{Priority: 1, Target: MustNewName("svc.example.com.")}
|
||||
if _, ok := rr.GetParam(SVCParamALPN); ok {
|
||||
t.Fatal("GetParam found non-existent param")
|
||||
}
|
||||
rr.SetParam(SVCParamIPv4Hint, []byte{192, 0, 2, 1})
|
||||
inALPN := []byte{0x02, 'h', '2', 0x02, 'h', '3'}
|
||||
rr.SetParam(SVCParamALPN, inALPN)
|
||||
|
||||
// Check sorting of params
|
||||
packed, err := rr.pack([]byte{}, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal("pack() =", err)
|
||||
}
|
||||
expectedBytes := []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x03, 0x73, 0x76, 0x63, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x00, 0x01, // key 1
|
||||
0x00, 0x06, // length 6
|
||||
0x02, 'h', '2', 0x02, 'h', '3', // value
|
||||
0x00, 0x04, // key 4
|
||||
0x00, 0x04, // length 4
|
||||
192, 0, 2, 1, // value
|
||||
}
|
||||
if !reflect.DeepEqual(packed, expectedBytes) {
|
||||
t.Fatalf("pack() produced unexpected output: want = %v, got = %v", expectedBytes, packed)
|
||||
}
|
||||
|
||||
// Check GetParam and DeleteParam.
|
||||
if outALPN, ok := rr.GetParam(SVCParamALPN); !ok || !bytes.Equal(outALPN, inALPN) {
|
||||
t.Fatal("GetParam failed to retrieve set param")
|
||||
}
|
||||
if !rr.DeleteParam(SVCParamALPN) {
|
||||
t.Fatal("DeleteParam failed to remove existing param")
|
||||
}
|
||||
if _, ok := rr.GetParam(SVCParamALPN); ok {
|
||||
t.Fatal("GetParam found deleted param")
|
||||
}
|
||||
if len(rr.Params) != 1 || rr.Params[0].Key != SVCParamIPv4Hint {
|
||||
t.Fatalf("DeleteParam removed wrong param: got = %#v, want = [%#v]", rr.Params, SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSVCBWireFormat(t *testing.T) {
|
||||
testRecord := func(bytesInput []byte, parsedInput *SVCBResource) {
|
||||
parsedOutput, n, err := unpackResourceBody(bytesInput, 0, ResourceHeader{Type: TypeSVCB, Length: uint16(len(bytesInput))})
|
||||
if err != nil {
|
||||
t.Fatalf("unpackResourceBody() = %v", err)
|
||||
}
|
||||
if n != len(bytesInput) {
|
||||
t.Fatalf("unpacked different amount than packed: got = %d, want = %d", n, len(bytesInput))
|
||||
}
|
||||
if !reflect.DeepEqual(parsedOutput, parsedInput) {
|
||||
t.Fatalf("unpack mismatch: got = %#v, want = %#v", parsedOutput, parsedInput)
|
||||
}
|
||||
|
||||
bytesOutput, err := parsedInput.pack([]byte{}, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("pack() = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(bytesOutput, bytesInput) {
|
||||
t.Fatalf("pack mismatch: got = %#v, want = %#v", bytesOutput, bytesInput)
|
||||
}
|
||||
}
|
||||
// Test examples from https://datatracker.ietf.org/doc/html/rfc9460#name-test-vectors
|
||||
|
||||
// Example D.1. Alias Mode
|
||||
|
||||
// Figure 2: AliasMode
|
||||
// example.com. HTTPS 0 foo.example.com.
|
||||
bytes := []byte{
|
||||
0x00, 0x00, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target: foo.example.com.
|
||||
}
|
||||
parsed := &SVCBResource{
|
||||
Priority: 0,
|
||||
Target: MustNewName("foo.example.com."),
|
||||
Params: []SVCParam{},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Example D.2. Service Mode
|
||||
|
||||
// Figure 3: TargetName Is "."
|
||||
// example.com. SVCB 1 .
|
||||
bytes = []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x00, // target (root label)
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("."),
|
||||
Params: []SVCParam{},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 4: Specifies a Port
|
||||
// example.com. SVCB 16 foo.example.com. port=53
|
||||
bytes = []byte{
|
||||
0x00, 0x10, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x00, 0x03, // key 3
|
||||
0x00, 0x02, // length 2
|
||||
0x00, 0x35, // value
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 16,
|
||||
Target: MustNewName("foo.example.com."),
|
||||
Params: []SVCParam{{Key: SVCParamPort, Value: []byte{0x00, 0x35}}},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 5: A Generic Key and Unquoted Value
|
||||
// example.com. SVCB 1 foo.example.com. key667=hello
|
||||
bytes = []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x02, 0x9b, // key 667
|
||||
0x00, 0x05, // length 5
|
||||
0x68, 0x65, 0x6c, 0x6c, 0x6f, // value
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("foo.example.com."),
|
||||
Params: []SVCParam{{Key: 667, Value: []byte("hello")}},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 6: A Generic Key and Quoted Value with a Decimal Escape
|
||||
// example.com. SVCB 1 foo.example.com. key667="hello\210qoo"
|
||||
bytes = []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x02, 0x9b, // key 667
|
||||
0x00, 0x09, // length 9
|
||||
0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, 0x71, 0x6f, 0x6f, // value
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("foo.example.com."),
|
||||
Params: []SVCParam{{Key: 667, Value: []byte("hello\xd2qoo")}},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 7: Two Quoted IPv6 Hints
|
||||
// example.com. SVCB 1 foo.example.com. (
|
||||
// ipv6hint="2001:db8::1,2001:db8::53:1"
|
||||
// )
|
||||
bytes = []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x00, 0x06, // key 6
|
||||
0x00, 0x20, // length 32
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // first address
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x01, // second address
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("foo.example.com."),
|
||||
Params: []SVCParam{{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x01}}},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 8: An IPv6 Hint Using the Embedded IPv4 Syntax
|
||||
// example.com. SVCB 1 example.com. (
|
||||
// ipv6hint="2001:db8:122:344::192.0.2.33"
|
||||
// )
|
||||
bytes = []byte{
|
||||
0x00, 0x01, // priority
|
||||
0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
|
||||
0x00, 0x06, // key 6
|
||||
0x00, 0x10, // length 16
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x01, 0x22, 0x03, 0x44, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x21, // address
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 1,
|
||||
Target: MustNewName("example.com."),
|
||||
Params: []SVCParam{{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x01, 0x22, 0x03, 0x44, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x21}}},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 9: SvcParamKey Ordering Is Arbitrary in Presentation Format but Sorted in Wire Format
|
||||
// example.com. SVCB 16 foo.example.org. (
|
||||
// alpn=h2,h3-19 mandatory=ipv4hint,alpn
|
||||
// ipv4hint=192.0.2.1
|
||||
// )
|
||||
bytes = []byte{
|
||||
0x00, 0x10, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, 0x72, 0x67, 0x00, // target
|
||||
0x00, 0x00, // key 0
|
||||
0x00, 0x04, // param length 4
|
||||
0x00, 0x01, // value: key 1
|
||||
0x00, 0x04, // value: key 4
|
||||
0x00, 0x01, // key 1
|
||||
0x00, 0x09, // param length 9
|
||||
0x02, // alpn length 2
|
||||
0x68, 0x32, // alpn value
|
||||
0x05, // alpn length 5
|
||||
0x68, 0x33, 0x2d, 0x31, 0x39, // alpn value
|
||||
0x00, 0x04, // key 4
|
||||
0x00, 0x04, // param length 4
|
||||
0xc0, 0x00, 0x02, 0x01, // param value
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 16,
|
||||
Target: MustNewName("foo.example.org."),
|
||||
Params: []SVCParam{
|
||||
{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x04}},
|
||||
{Key: SVCParamALPN, Value: []byte{0x02, 0x68, 0x32, 0x05, 0x68, 0x33, 0x2d, 0x31, 0x39}},
|
||||
{Key: SVCParamIPv4Hint, Value: []byte{0xc0, 0x00, 0x02, 0x01}},
|
||||
},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
|
||||
// Figure 10: An "alpn" Value with an Escaped Comma and an Escaped Backslash in Two Presentation Formats
|
||||
// example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2
|
||||
bytes = []byte{
|
||||
0x00, 0x10, // priority
|
||||
0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, 0x72, 0x67, 0x00, // target
|
||||
0x00, 0x01, // key 1
|
||||
0x00, 0x0c, // param length 12
|
||||
0x08, // alpn length 8
|
||||
0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, // alpn value
|
||||
0x02, // alpn length 2
|
||||
0x68, 0x32, // alpn value
|
||||
}
|
||||
parsed = &SVCBResource{
|
||||
Priority: 16,
|
||||
Target: MustNewName("foo.example.org."),
|
||||
Params: []SVCParam{
|
||||
{Key: SVCParamALPN, Value: []byte{0x08, 0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, 0x02, 0x68, 0x32}},
|
||||
},
|
||||
}
|
||||
testRecord(bytes, parsed)
|
||||
}
|
||||
Reference in New Issue
Block a user