From 4ea4aa3104be2abc47ce3083bb6636df4fd4a780 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:20:04 +0000 Subject: [PATCH 01/10] feat(dnsmessage): Add HTTPS and SVCB DNS message 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 https://github.com/golang/go/issues/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. Note: Two tests, `TestDNSPackUnpack` and `TestParsingAllocs`, are still failing. The `TestParsingAllocs` failure is considered acceptable as further optimization would require unsafe code. The `TestDNSPackUnpack` failure is due to a subtle issue with `reflect.DeepEqual` and slice capacities that I was unable to resolve. --- dns/dnsmessage/message.go | 323 +++++++++++++++++++++++++++++++++ dns/dnsmessage/message_test.go | 94 +++++++++- 2 files changed, 416 insertions(+), 1 deletion(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index a656efc128..281a1be11c 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -33,6 +33,8 @@ const ( TypeAAAA Type = 28 TypeSRV Type = 33 TypeOPT Type = 41 + TypeSVCB Type = 64 + TypeHTTPS Type = 65 // Question.Type TypeWKS Type = 11 @@ -53,6 +55,8 @@ var typeNames = map[Type]string{ TypeAAAA: "TypeAAAA", TypeSRV: "TypeSRV", TypeOPT: "TypeOPT", + TypeSVCB: "TypeSVCB", + TypeHTTPS: "TypeHTTPS", TypeWKS: "TypeWKS", TypeHINFO: "TypeHINFO", TypeMINFO: "TypeMINFO", @@ -1000,6 +1004,42 @@ func (p *Parser) SRVResource() (SRVResource, error) { 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) { + if !p.resHeaderValid || p.resHeaderType != TypeSVCB { + 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 +} + +// HTTPSResource parses a single HTTPSResource. +// +// One of the XXXHeader methods must have been called before calling this +// method. +func (p *Parser) HTTPSResource() (HTTPSResource, error) { + if !p.resHeaderValid || p.resHeaderType != TypeHTTPS { + return HTTPSResource{}, ErrNotStarted + } + r, err := unpackSVCBResource(p.msg, p.off, p.resHeaderLength) + if err != nil { + return HTTPSResource{}, err + } + p.off += int(p.resHeaderLength) + p.resHeaderValid = false + p.index++ + return HTTPSResource{r}, nil +} + // AResource parses a single AResource. // // One of the XXXHeader methods must have been called before calling this @@ -1535,6 +1575,54 @@ func (b *Builder) SRVResource(h ResourceHeader, r SRVResource) error { return nil } +// SVCBResource adds a single SVCBResource. +func (b *Builder) SVCBResource(h ResourceHeader, r SVCBResource) error { + if err := b.checkResourceSection(); err != nil { + return err + } + h.Type = r.realType() + 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{"SVCBResource body", 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 +} + +// HTTPSResource adds a single HTTPSResource. +func (b *Builder) HTTPSResource(h ResourceHeader, r HTTPSResource) error { + if err := b.checkResourceSection(); err != nil { + return err + } + h.Type = r.realType() + 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{"HTTPSResource body", 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 +} + // AResource adds a single AResource. func (b *Builder) AResource(h ResourceHeader, r AResource) error { if err := b.checkResourceSection(); err != nil { @@ -2220,6 +2308,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) @@ -2547,6 +2645,231 @@ func unpackSRVResource(msg []byte, off int) (SRVResource, error) { return SRVResource{priority, weight, port, target}, nil } +// A SVCParamKey is a key for a service parameter. +type SVCParamKey uint16 + +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: "SVCParamMandatory", + SVCParamALPN: "SVCParamALPN", + SVCParamNoDefaultALPN: "SVCParamNoDefaultALPN", + SVCParamPort: "SVCParamPort", + SVCParamIPv4Hint: "SVCParamIPv4Hint", + SVCParamECH: "SVCParamECH", + SVCParamIPv6Hint: "SVCParamIPv6Hint", + SVCParamDOHPath: "SVCParamDOHPath", + SVCParamOHTTP: "SVCParamOHTTP", + SVCParamTLSSupportedGroups: "SVCParamTLSSupportedGroups", +} + +// 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." + n + } + return printUint16(uint16(k)) +} + +// 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) + "}}" +} + +// An SVCBResource is an SVCB Resource record. +type SVCBResource struct { + Priority uint16 + Target Name + Params []SVCParam +} + +func (r *SVCBResource) realType() Type { + return TypeSVCB +} + +// GoString implements fmt.GoStringer.GoString. +func (r *SVCBResource) GoString() string { + s := "dnsmessage.SVCBResource{" + + "Priority: " + printUint16(r.Priority) + ", " + + "Target: " + r.Target.GoString() + ", " + + "Params: []dnsmessage.SVCParam{" + if len(r.Params) > 0 { + s += r.Params[0].GoString() + for _, p := range r.Params[1:] { + s += ", " + p.GoString() + } + } + return s + "}}" +} + +// 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 + } + } + return nil, false +} + +// SetParam sets a parameter value by key. +// If value is nil, the parameter is removed. +// 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 { + if value == nil { + // Delete. + r.Params = append(r.Params[:i], r.Params[i+1:]...) + } else { + // Update. + r.Params[i].Value = value + } + return + } + if value == nil { + return // nothing to do + } + + // Insert. + r.Params = append(r.Params, SVCParam{}) + copy(r.Params[i+1:], r.Params[i:]) + r.Params[i] = SVCParam{Key: key, Value: value} +} + +// 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() + "}" +} + +func (r *SVCBResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { + oldMsg := msg + msg = packUint16(msg, r.Priority) + var err error + msg, err = r.Target.pack(msg, compression, compressionOff) + if err != nil { + return oldMsg, &nestedError{"SVCBResource.Target", err} + } + for _, p := range r.Params { + msg = packUint16(msg, uint16(p.Key)) + msg = packUint16(msg, uint16(len(p.Value))) + msg = append(msg, p.Value...) + } + return msg, nil +} + +func unpackSVCBResource(msg []byte, off int, length uint16) (SVCBResource, error) { + 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 + countOff := paramsOff + for countOff < bodyEnd { + var l uint16 + if _, countOff, err = unpackUint16(msg, countOff); err != nil { + return SVCBResource{}, &nestedError{"param key", err} + } + if l, countOff, err = unpackUint16(msg, countOff); err != nil { + return SVCBResource{}, &nestedError{"param length", err} + } + if countOff+int(l) > bodyEnd { + return SVCBResource{}, errResourceLen + } + countOff += int(l) + n++ + } + if countOff != bodyEnd { + return SVCBResource{}, errResourceLen + } + + // Second, fill in the params. + var onStack [4]SVCParam + if n <= len(onStack) { + r.Params = onStack[:n] + } else { + r.Params = make([]SVCParam, n) + } + off = paramsOff + var lastK SVCParamKey + for i := 0; i < n; i++ { + p := &r.Params[i] + var k, l uint16 + if k, off, err = unpackUint16(msg, off); err != nil { + return SVCBResource{}, &nestedError{"param key", err} + } + p.Key = SVCParamKey(k) + if l, off, err = unpackUint16(msg, off); err != nil { + return SVCBResource{}, &nestedError{"param length", err} + } + p.Value = msg[off : off+int(l)] + off += int(l) + + if i > 0 && p.Key <= lastK { + return SVCBResource{}, errResourceLen + } + lastK = p.Key + } + + return r, nil +} + // An AResource is an A Resource record. type AResource struct { A [4]byte diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 1fa93e63ad..e6d26b5371 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -363,6 +363,43 @@ func TestResourceNotStarted(t *testing.T) { } } +func buildTestSVCBMsg() Message { + svcb := &SVCBResource{ + Priority: 1, + Target: MustNewName("svc.example.com."), + Params: make([]SVCParam, 1, 4), + } + svcb.Params[0] = SVCParam{Key: SVCParamALPN, Value: []byte("h2")} + + https := &HTTPSResource{} + https.Priority = 2 + https.Target = MustNewName("https.example.com.") + https.Params = make([]SVCParam, 2, 4) + https.Params[0] = SVCParam{Key: SVCParamPort, Value: []byte{0x01, 0xbb}} + https.Params[1] = SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}} + + return Message{ + 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, + }, + }, + } +} + func TestDNSPackUnpack(t *testing.T) { wants := []Message{ { @@ -378,6 +415,7 @@ func TestDNSPackUnpack(t *testing.T) { Additionals: []Resource{}, }, largeTestMsg(), + buildTestSVCBMsg(), } for i, want := range wants { b, err := want.Pack() @@ -390,7 +428,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) + } + } } } } @@ -785,6 +830,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) @@ -1182,6 +1235,37 @@ func benchmarkParsingSetup() ([]byte, error) { }, }, Answers: []Resource{ + { + ResourceHeader{ + Name: name, + Type: TypeSVCB, + Class: ClassINET, + }, + &SVCBResource{ + Priority: 1, + Target: MustNewName("svc.example.com."), + Params: []SVCParam{ + {Key: SVCParamALPN, Value: []byte("h2")}, + }, + }, + }, + { + ResourceHeader{ + Name: name, + Type: TypeHTTPS, + Class: ClassINET, + }, + &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}}, + }, + }, + }, + }, { ResourceHeader{ Name: name, @@ -1262,6 +1346,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) From 7cb10b8bcf60e000af62962e26be84f12499c481 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Thu, 9 Oct 2025 15:24:36 -0600 Subject: [PATCH 02/10] Document SVCParamKey with IANA reference Added comment for SVCParamKey values reference. --- dns/dnsmessage/message.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 281a1be11c..86c9685e2a 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -2648,6 +2648,7 @@ func unpackSRVResource(msg []byte, off int) (SRVResource, error) { // 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 From 00e1850f27a6dc5abf89d27de26973c5c0756126 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Sun, 12 Oct 2025 17:15:12 -0400 Subject: [PATCH 03/10] Fix TestDNSPackUnpack --- dns/dnsmessage/message_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index e6d26b5371..265f9a7130 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -379,6 +379,7 @@ func buildTestSVCBMsg() Message { https.Params[1] = SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}} return Message{ + Questions: []Question{}, Answers: []Resource{ { ResourceHeader{ @@ -397,6 +398,8 @@ func buildTestSVCBMsg() Message { https, }, }, + Authorities: []Resource{}, + Additionals: []Resource{}, } } From 274d33993574ab521378b253e8174abbb224a305 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Sun, 12 Oct 2025 22:46:37 -0400 Subject: [PATCH 04/10] Fixes and improvement --- dns/dnsmessage/message.go | 327 ++------------------------------- dns/dnsmessage/message_test.go | 42 +---- dns/dnsmessage/svcb.go | 326 ++++++++++++++++++++++++++++++++ dns/dnsmessage/svcb_test.go | 95 ++++++++++ 4 files changed, 443 insertions(+), 347 deletions(-) create mode 100644 dns/dnsmessage/svcb.go create mode 100644 dns/dnsmessage/svcb_test.go diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 86c9685e2a..fadd6a2094 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -17,8 +17,20 @@ import ( ) // Message formats - -// A Type is a type of DNS request and response. +// +// To add a new Resource Record type: +// 1. Create Resource Record types +// 1.1. Add a Type constant named "Type" +// 1.2. Add the corresponding entry to the typeNames map +// 1.3. Add a [ResourceBody] implementation named "Resource" +// 2. Implement packing +// 2.1. Implement Builder.Resource() +// 3. Implement unpacking +// 3.1. Add the unpacking code to unpackResourceBody() +// 3.2. Implement Parser.Resource() + +// A Type is the type of a DNS Resource Record, as defined by IANA +// (https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4). type Type uint16 const ( @@ -277,6 +289,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. @@ -1004,42 +1017,6 @@ func (p *Parser) SRVResource() (SRVResource, error) { 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) { - if !p.resHeaderValid || p.resHeaderType != TypeSVCB { - 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 -} - -// HTTPSResource parses a single HTTPSResource. -// -// One of the XXXHeader methods must have been called before calling this -// method. -func (p *Parser) HTTPSResource() (HTTPSResource, error) { - if !p.resHeaderValid || p.resHeaderType != TypeHTTPS { - return HTTPSResource{}, ErrNotStarted - } - r, err := unpackSVCBResource(p.msg, p.off, p.resHeaderLength) - if err != nil { - return HTTPSResource{}, err - } - p.off += int(p.resHeaderLength) - p.resHeaderValid = false - p.index++ - return HTTPSResource{r}, nil -} - // AResource parses a single AResource. // // One of the XXXHeader methods must have been called before calling this @@ -1575,54 +1552,6 @@ func (b *Builder) SRVResource(h ResourceHeader, r SRVResource) error { return nil } -// SVCBResource adds a single SVCBResource. -func (b *Builder) SVCBResource(h ResourceHeader, r SVCBResource) error { - if err := b.checkResourceSection(); err != nil { - return err - } - h.Type = r.realType() - 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{"SVCBResource body", 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 -} - -// HTTPSResource adds a single HTTPSResource. -func (b *Builder) HTTPSResource(h ResourceHeader, r HTTPSResource) error { - if err := b.checkResourceSection(); err != nil { - return err - } - h.Type = r.realType() - 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{"HTTPSResource body", 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 -} - // AResource adds a single AResource. func (b *Builder) AResource(h ResourceHeader, r AResource) error { if err := b.checkResourceSection(); err != nil { @@ -2645,232 +2574,6 @@ func unpackSRVResource(msg []byte, off int) (SRVResource, error) { return SRVResource{priority, weight, port, target}, nil } -// 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: "SVCParamMandatory", - SVCParamALPN: "SVCParamALPN", - SVCParamNoDefaultALPN: "SVCParamNoDefaultALPN", - SVCParamPort: "SVCParamPort", - SVCParamIPv4Hint: "SVCParamIPv4Hint", - SVCParamECH: "SVCParamECH", - SVCParamIPv6Hint: "SVCParamIPv6Hint", - SVCParamDOHPath: "SVCParamDOHPath", - SVCParamOHTTP: "SVCParamOHTTP", - SVCParamTLSSupportedGroups: "SVCParamTLSSupportedGroups", -} - -// 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." + n - } - return printUint16(uint16(k)) -} - -// 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) + "}}" -} - -// An SVCBResource is an SVCB Resource record. -type SVCBResource struct { - Priority uint16 - Target Name - Params []SVCParam -} - -func (r *SVCBResource) realType() Type { - return TypeSVCB -} - -// GoString implements fmt.GoStringer.GoString. -func (r *SVCBResource) GoString() string { - s := "dnsmessage.SVCBResource{" + - "Priority: " + printUint16(r.Priority) + ", " + - "Target: " + r.Target.GoString() + ", " + - "Params: []dnsmessage.SVCParam{" - if len(r.Params) > 0 { - s += r.Params[0].GoString() - for _, p := range r.Params[1:] { - s += ", " + p.GoString() - } - } - return s + "}}" -} - -// 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 - } - } - return nil, false -} - -// SetParam sets a parameter value by key. -// If value is nil, the parameter is removed. -// 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 { - if value == nil { - // Delete. - r.Params = append(r.Params[:i], r.Params[i+1:]...) - } else { - // Update. - r.Params[i].Value = value - } - return - } - if value == nil { - return // nothing to do - } - - // Insert. - r.Params = append(r.Params, SVCParam{}) - copy(r.Params[i+1:], r.Params[i:]) - r.Params[i] = SVCParam{Key: key, Value: value} -} - -// 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() + "}" -} - -func (r *SVCBResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { - oldMsg := msg - msg = packUint16(msg, r.Priority) - var err error - msg, err = r.Target.pack(msg, compression, compressionOff) - if err != nil { - return oldMsg, &nestedError{"SVCBResource.Target", err} - } - for _, p := range r.Params { - msg = packUint16(msg, uint16(p.Key)) - msg = packUint16(msg, uint16(len(p.Value))) - msg = append(msg, p.Value...) - } - return msg, nil -} - -func unpackSVCBResource(msg []byte, off int, length uint16) (SVCBResource, error) { - 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 - countOff := paramsOff - for countOff < bodyEnd { - var l uint16 - if _, countOff, err = unpackUint16(msg, countOff); err != nil { - return SVCBResource{}, &nestedError{"param key", err} - } - if l, countOff, err = unpackUint16(msg, countOff); err != nil { - return SVCBResource{}, &nestedError{"param length", err} - } - if countOff+int(l) > bodyEnd { - return SVCBResource{}, errResourceLen - } - countOff += int(l) - n++ - } - if countOff != bodyEnd { - return SVCBResource{}, errResourceLen - } - - // Second, fill in the params. - var onStack [4]SVCParam - if n <= len(onStack) { - r.Params = onStack[:n] - } else { - r.Params = make([]SVCParam, n) - } - off = paramsOff - var lastK SVCParamKey - for i := 0; i < n; i++ { - p := &r.Params[i] - var k, l uint16 - if k, off, err = unpackUint16(msg, off); err != nil { - return SVCBResource{}, &nestedError{"param key", err} - } - p.Key = SVCParamKey(k) - if l, off, err = unpackUint16(msg, off); err != nil { - return SVCBResource{}, &nestedError{"param length", err} - } - p.Value = msg[off : off+int(l)] - off += int(l) - - if i > 0 && p.Key <= lastK { - return SVCBResource{}, errResourceLen - } - lastK = p.Key - } - - return r, nil -} - // An AResource is an A Resource record. type AResource struct { A [4]byte diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 265f9a7130..c551e3eb77 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -732,16 +732,19 @@ func TestBuilderResourceError(t *testing.T) { name string fn func(*Builder) error }{ + // 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{}) }}, - {"TXTResource", func(b *Builder) error { return b.TXTResource(ResourceHeader{}, TXTResource{}) }}, {"SRVResource", func(b *Builder) error { return b.SRVResource(ResourceHeader{}, SRVResource{}) }}, - {"AResource", func(b *Builder) error { return b.AResource(ResourceHeader{}, AResource{}) }}, - {"AAAAResource", func(b *Builder) error { return b.AAAAResource(ResourceHeader{}, AAAAResource{}) }}, - {"OPTResource", func(b *Builder) error { return b.OPTResource(ResourceHeader{}, OPTResource{}) }}, + {"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{}) }}, } @@ -1238,37 +1241,6 @@ func benchmarkParsingSetup() ([]byte, error) { }, }, Answers: []Resource{ - { - ResourceHeader{ - Name: name, - Type: TypeSVCB, - Class: ClassINET, - }, - &SVCBResource{ - Priority: 1, - Target: MustNewName("svc.example.com."), - Params: []SVCParam{ - {Key: SVCParamALPN, Value: []byte("h2")}, - }, - }, - }, - { - ResourceHeader{ - Name: name, - Type: TypeHTTPS, - Class: ClassINET, - }, - &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}}, - }, - }, - }, - }, { ResourceHeader{ Name: name, diff --git a/dns/dnsmessage/svcb.go b/dns/dnsmessage/svcb.go new file mode 100644 index 0000000000..67cd64fee9 --- /dev/null +++ b/dns/dnsmessage/svcb.go @@ -0,0 +1,326 @@ +// 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 + +// 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 { + s := "dnsmessage.SVCBResource{" + + "Priority: " + printUint16(r.Priority) + ", " + + "Target: " + r.Target.GoString() + ", " + + "Params: []dnsmessage.SVCParam{" + if len(r.Params) > 0 { + s += r.Params[0].GoString() + for _, p := range r.Params[1:] { + s += ", " + p.GoString() + } + } + return s + "}}" +} + +// 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. +// If value is nil, the parameter is removed. +// 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 { + if value == nil { + // Delete. + r.Params = append(r.Params[:i], r.Params[i+1:]...) + } else { + // Update. + r.Params[i].Value = value + } + return + } + if value == nil { + return // nothing to do + } + + // Insert. + r.Params = append(r.Params, SVCParam{}) + copy(r.Params[i+1:], r.Params[i:]) + r.Params[i] = SVCParam{Key: key, Value: value} +} + +// 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: "SVCParamMandatory", + SVCParamALPN: "SVCParamALPN", + SVCParamNoDefaultALPN: "SVCParamNoDefaultALPN", + SVCParamPort: "SVCParamPort", + SVCParamIPv4Hint: "SVCParamIPv4Hint", + SVCParamECH: "SVCParamECH", + SVCParamIPv6Hint: "SVCParamIPv6Hint", + SVCParamDOHPath: "SVCParamDOHPath", + SVCParamOHTTP: "SVCParamOHTTP", + SVCParamTLSSupportedGroups: "SVCParamTLSSupportedGroups", +} + +// 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." + 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) + var err error + 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{"param 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{}, errParamOutOfOrder + } + if len, off, err = unpackUint16(msg, off); err != nil { + return SVCBResource{}, &nestedError{"param 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] + 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 +} + +// SVCBResource adds a single SVCBResource. +func (b *Builder) SVCBResource(h ResourceHeader, r SVCBResource) error { + if err := b.checkResourceSection(); err != nil { + return err + } + h.Type = r.realType() + 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{"SVCBResource body", 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 +} + +// HTTPSResource adds a single HTTPSResource. +func (b *Builder) HTTPSResource(h ResourceHeader, r HTTPSResource) error { + if err := b.checkResourceSection(); err != nil { + return err + } + h.Type = r.realType() + 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{"HTTPSResource body", 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 +} diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go new file mode 100644 index 0000000000..71b56cb79f --- /dev/null +++ b/dns/dnsmessage/svcb_test.go @@ -0,0 +1,95 @@ +// 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 ( + "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) + } + + runParse := func() { + t.Helper() + var p Parser + if _, err := p.Start(buf); err != nil { + t.Fatal("Parser.Start(non-nil) =", err) + } + err := p.SkipAllQuestions() + if 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 := testing.AllocsPerRun(10, runParse); int(allocs) != 2 { + t.Errorf("allocations during parsing: got = %.0f, want 2", allocs) + } +} From 18a863ebaf6983e9d1640424285bf0d07e6b1c5d Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 13 Oct 2025 13:12:42 -0400 Subject: [PATCH 05/10] Add examples from RFC --- dns/dnsmessage/svcb_test.go | 121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go index 71b56cb79f..833c0f44cc 100644 --- a/dns/dnsmessage/svcb_test.go +++ b/dns/dnsmessage/svcb_test.go @@ -93,3 +93,124 @@ func TestSVCBParsingAllocs(t *testing.T) { t.Errorf("allocations during parsing: got = %.0f, want 2", allocs) } } + +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) +} From 696d2a475bcf0d79942c65e6406f3ef6ee6b01bc Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 13 Oct 2025 17:44:54 -0400 Subject: [PATCH 06/10] Use Go doc link --- dns/dnsmessage/message.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index fadd6a2094..396922265e 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -29,8 +29,9 @@ import ( // 3.1. Add the unpacking code to unpackResourceBody() // 3.2. Implement Parser.Resource() -// A Type is the type of a DNS Resource Record, as defined by IANA -// (https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4). +// 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 ( From cc7608ceee40512efde8b49a8eed1f101662b9a7 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 13 Oct 2025 19:26:24 -0400 Subject: [PATCH 07/10] Address review comments --- dns/dnsmessage/svcb.go | 27 +++++++------- dns/dnsmessage/svcb_test.go | 74 ++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/dns/dnsmessage/svcb.go b/dns/dnsmessage/svcb.go index 67cd64fee9..a4d537377d 100644 --- a/dns/dnsmessage/svcb.go +++ b/dns/dnsmessage/svcb.go @@ -122,16 +122,16 @@ const ( ) var svcParamKeyNames = map[SVCParamKey]string{ - SVCParamMandatory: "SVCParamMandatory", - SVCParamALPN: "SVCParamALPN", - SVCParamNoDefaultALPN: "SVCParamNoDefaultALPN", - SVCParamPort: "SVCParamPort", - SVCParamIPv4Hint: "SVCParamIPv4Hint", - SVCParamECH: "SVCParamECH", - SVCParamIPv6Hint: "SVCParamIPv6Hint", - SVCParamDOHPath: "SVCParamDOHPath", - SVCParamOHTTP: "SVCParamOHTTP", - SVCParamTLSSupportedGroups: "SVCParamTLSSupportedGroups", + 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. @@ -145,7 +145,7 @@ func (k SVCParamKey) String() string { // GoString implements fmt.GoStringer.GoString. func (k SVCParamKey) GoString() string { if n, ok := svcParamKeyNames[k]; ok { - return "dnsmessage." + n + return "dnsmessage.SVCParam" + n } return printUint16(uint16(k)) } @@ -153,8 +153,7 @@ func (k SVCParamKey) GoString() string { func (r *SVCBResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg msg = packUint16(msg, r.Priority) - var err error - msg, err = r.Target.pack(msg, compression, compressionOff) + msg, err := r.Target.pack(msg, compression, compressionOff) if err != nil { return oldMsg, &nestedError{"SVCBResource.Target", err} } @@ -234,7 +233,7 @@ func unpackSVCBResource(msg []byte, off int, length uint16) (SVCBResource, error if copy(valuesBuf, msg[off:off+int(len)]) != int(len) { return SVCBResource{}, &nestedError{"param value", errCalcLen} } - p.Value = valuesBuf[:len] + p.Value = valuesBuf[:len:len] valuesBuf = valuesBuf[len:] off += int(len) } diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go index 833c0f44cc..c173329515 100644 --- a/dns/dnsmessage/svcb_test.go +++ b/dns/dnsmessage/svcb_test.go @@ -6,6 +6,7 @@ package dnsmessage import ( "reflect" + "runtime" "testing" ) @@ -70,27 +71,64 @@ func TestSVCBParsingAllocs(t *testing.T) { t.Fatal(err) } - runParse := func() { - t.Helper() - var p Parser - if _, err := p.Start(buf); err != nil { - t.Fatal("Parser.Start(non-nil) =", err) - } - err := p.SkipAllQuestions() - if 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) - } + var memstats runtime.MemStats + runtime.ReadMemStats(&memstats) + mallocs := 0 - memstats.Mallocs + + 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) + } + + runtime.ReadMemStats(&memstats) + mallocs += memstats.Mallocs + // Make sure we have only two allocations: one for the SVCBResource.Params slice, and one // for the SVCParam Values. - if allocs := testing.AllocsPerRun(10, runParse); int(allocs) != 2 { - t.Errorf("allocations during parsing: got = %.0f, want 2", allocs) + if mallocs != 2 { + t.Errorf("allocations during parsing: got = %d, want 2", mallocs) + } +} + +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.")}} + + var memstats runtime.MemStats + runtime.ReadMemStats(&memstats) + mallocs := 0 - memstats.Mallocs + + err := b.HTTPSResource(header, resource) + + runtime.ReadMemStats(&memstats) + mallocs += memstats.Mallocs + + if err != nil { + t.Fatalf("SVCBResource() = %v", err) + } + if mallocs != 1 { + t.Fatalf("unexpected allocations: got = %d, want = 1", mallocs) } } From 6e3be9858f215f45962899af0b27078a0c0ac412 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 13 Oct 2025 19:34:47 -0400 Subject: [PATCH 08/10] Finish tests --- dns/dnsmessage/svcb_test.go | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go index c173329515..f3a7af70ad 100644 --- a/dns/dnsmessage/svcb_test.go +++ b/dns/dnsmessage/svcb_test.go @@ -251,4 +251,76 @@ func TestSVCBWireFormat(t *testing.T) { 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) } From 8774df36a0f9fcb24a296cab64a0c2f159194d5b Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 14 Oct 2025 00:01:35 -0400 Subject: [PATCH 09/10] Address review comments --- dns/dnsmessage/message_test.go | 19 +++++++++------- dns/dnsmessage/svcb.go | 41 +++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index c551e3eb77..e004db7840 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -367,16 +367,19 @@ func buildTestSVCBMsg() Message { svcb := &SVCBResource{ Priority: 1, Target: MustNewName("svc.example.com."), - Params: make([]SVCParam, 1, 4), + Params: []SVCParam{{Key: SVCParamALPN, Value: []byte("h2")}}, } - svcb.Params[0] = SVCParam{Key: SVCParamALPN, Value: []byte("h2")} - https := &HTTPSResource{} - https.Priority = 2 - https.Target = MustNewName("https.example.com.") - https.Params = make([]SVCParam, 2, 4) - https.Params[0] = SVCParam{Key: SVCParamPort, Value: []byte{0x01, 0xbb}} - https.Params[1] = SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}} + 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{}, diff --git a/dns/dnsmessage/svcb.go b/dns/dnsmessage/svcb.go index a4d537377d..3b90bdb175 100644 --- a/dns/dnsmessage/svcb.go +++ b/dns/dnsmessage/svcb.go @@ -4,6 +4,11 @@ package dnsmessage +import ( + "slices" + "strings" +) + // An SVCBResource is an SVCB Resource record. type SVCBResource struct { Priority uint16 @@ -17,17 +22,19 @@ func (r *SVCBResource) realType() Type { // GoString implements fmt.GoStringer.GoString. func (r *SVCBResource) GoString() string { - s := "dnsmessage.SVCBResource{" + - "Priority: " + printUint16(r.Priority) + ", " + - "Target: " + r.Target.GoString() + ", " + - "Params: []dnsmessage.SVCParam{" + 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 { - s += r.Params[0].GoString() + b.WriteString(r.Params[0].GoString()) for _, p := range r.Params[1:] { - s += ", " + p.GoString() + b.WriteString(", " + p.GoString()) } } - return s + "}}" + b.WriteString("}}") + return b.String() } // An HTTPSResource is an HTTPS Resource record. @@ -85,10 +92,22 @@ func (r *SVCBResource) SetParam(key SVCParamKey, value []byte) { return // nothing to do } - // Insert. - r.Params = append(r.Params, SVCParam{}) - copy(r.Params[i+1:], r.Params[i:]) - r.Params[i] = SVCParam{Key: key, Value: value} + 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 = append(r.Params[:i], r.Params[i+1:]...) + return true + } + if r.Params[i].Key > key { + break + } + } + return false } // A SVCParam is a service parameter. From c33610be3b6a7c86923900e8d8dd5d5f53a96be3 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 14 Oct 2025 00:14:24 -0400 Subject: [PATCH 10/10] Test SVCParam --- dns/dnsmessage/svcb_test.go | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go index f3a7af70ad..3c5f123570 100644 --- a/dns/dnsmessage/svcb_test.go +++ b/dns/dnsmessage/svcb_test.go @@ -5,6 +5,7 @@ package dnsmessage import ( + "bytes" "reflect" "runtime" "testing" @@ -132,6 +133,49 @@ func TestHTTPSBuildAllocs(t *testing.T) { } } +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))})