327 lines
9.1 KiB
Go
327 lines
9.1 KiB
Go
// 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)
|
|
}
|