// 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" ) // 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 { b := []byte("dnsmessage.SVCBResource{" + "Priority: " + printUint16(r.Priority) + ", " + "Target: " + r.Target.GoString() + ", " + "Params: []dnsmessage.SVCParam{") if len(r.Params) > 0 { b = append(b, r.Params[0].GoString()...) for _, p := range r.Params[1:] { b = append(b, ", "+p.GoString()...) } } b = append(b, "}}"...) return string(b) } // 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, _ map[string]uint16, _ int) ([]byte, error) { oldMsg := msg msg = packUint16(msg, r.Priority) // https://datatracker.ietf.org/doc/html/rfc3597#section-4 prohibits name // compression for RR types that are not "well-known". // https://datatracker.ietf.org/doc/html/rfc9460#section-2.2 explicitly states that // compression of the Target is prohibited, following RFC 3597. msg, err := r.Target.pack(msg, nil, 0) 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} } if len(param.Value) > (1<<16)-1 { return oldMsg, &nestedError{"SVCBResource.Params", errTooLongSVCBValue} } 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) }