blob: 74fcccdac51ec89d15c58d89845deb0e48715967 [file] [log] [blame] [edit]
// 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"
"math"
"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)
}
func TestSVCBPackLongValue(t *testing.T) {
b := NewBuilder(nil, Header{})
b.StartQuestions()
b.StartAnswers()
res := SVCBResource{
Target: MustNewName("example.com."),
Params: []SVCParam{
{
Key: SVCParamMandatory,
Value: make([]byte, math.MaxUint16+1),
},
},
}
err := b.SVCBResource(ResourceHeader{Name: MustNewName("example.com.")}, res)
if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
t.Fatalf(`b.SVCBResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
}
err = b.HTTPSResource(ResourceHeader{Name: MustNewName("example.com.")}, HTTPSResource{res})
if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
t.Fatalf(`b.HTTPSResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
}
}