| // 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) |
| } |
| } |