blob: 4840516a7f88e5c6a344715e4bafd1d254f19a19 [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 (
"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)
}