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:
Vinicius Fortuna
2025-10-14 22:24:07 +00:00
committed by t hepudds
parent 63d1a5100f
commit bb2055dafd
4 changed files with 794 additions and 9 deletions

View File

@@ -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)

View File

@@ -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
View 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
View 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)
}