~shulhan/pakakeh.go

71eaafc5119b178be61abf6ae7b8a2fbcdfacc44 — Shulhan 6 months ago aba79f6
lib/dns: implements RFC 9460 for SVCB RR and HTTPS RR
M lib/dns/dns.go => lib/dns/dns.go +2 -0
@@ 12,6 12,8 @@
//   - RFC2782 A DNS RR for specifying the location of services (DNS SRV)
//   - RFC6891 Extension Mechanisms for DNS (EDNS(0))
//   - RFC8484 DNS Queries over HTTPS (DoH)
//   - RFC9460 Service Binding and Parameter Specification via the DNS (SVCB
//     and HTTPS Resource Records)
package dns

import (

M lib/dns/message.go => lib/dns/message.go +75 -0
@@ 487,6 487,10 @@ func (msg *Message) packRData(rr *ResourceRecord) {
		msg.packAAAA(rr)
	case RecordTypeOPT:
		msg.packOPT(rr)
	case RecordTypeSVCB:
		msg.packSVCB(rr)
	case RecordTypeHTTPS:
		msg.packHTTPS(rr)
	}
}



@@ 705,6 709,77 @@ func (msg *Message) packOPT(rr *ResourceRecord) {
	libbytes.WriteUint16(msg.packet, off, n)
}

func (msg *Message) packSVCB(rr *ResourceRecord) {
	var (
		svcb *RDataSVCB
		ok   bool
	)

	svcb, ok = rr.Value.(*RDataSVCB)
	if !ok {
		return
	}

	// Reserve two octets for rdlength.
	var off = uint(len(msg.packet))
	msg.packet = libbytes.AppendUint16(msg.packet, 0)

	var n = svcb.pack(msg)

	// Write rdlength.
	libbytes.WriteUint16(msg.packet, off, uint16(n))
}

func (msg *Message) packHTTPS(rr *ResourceRecord) {
	var (
		rrhttps *RDataHTTPS
		ok      bool
	)

	rrhttps, ok = rr.Value.(*RDataHTTPS)
	if !ok {
		return
	}

	// Reserve two octets for rdlength.
	var off = uint(len(msg.packet))
	msg.packet = libbytes.AppendUint16(msg.packet, 0)

	// Priority.
	msg.packet = libbytes.AppendUint16(msg.packet, 0)

	var n = msg.packDomainName([]byte(rrhttps.TargetName), false)

	// In HTTPS (AliasMode), Params is ignored.

	// Write rdlength.
	libbytes.WriteUint16(msg.packet, off, uint16(n))
}

func (msg *Message) packIPv4(addr string) {
	var ip = net.ParseIP(addr)
	if ip == nil {
		msg.packet = append(msg.packet, []byte{0, 0, 0, 0}...)
	} else {
		var ipv4 = ip.To4()
		if ipv4 == nil {
			msg.packet = append(msg.packet, []byte{0, 0, 0, 0}...)
		} else {
			msg.packet = append(msg.packet, ipv4...)
		}
	}
}

func (msg *Message) packIPv6(addr string) {
	var ip = net.ParseIP(addr)

	if ip == nil {
		msg.packet = append(msg.packet, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}...)
	} else {
		msg.packet = append(msg.packet, ip...)
	}
}

// Reset the message fields.
func (msg *Message) Reset() {
	msg.Header.Reset()

M lib/dns/message_test.go => lib/dns/message_test.go +50 -0
@@ 6,6 6,7 @@ package dns

import (
	"bytes"
	"encoding/json"
	"testing"

	libbytes "git.sr.ht/~shulhan/pakakeh.go/lib/bytes"


@@ 2079,3 2080,52 @@ func TestMessageUnpack(t *testing.T) {
		}
	}
}

func TestUnpackMessage_SVCB(t *testing.T) {
	var (
		logp  = `TestUnpackMessage_SVCB`
		tdata *test.Data
		err   error
	)

	tdata, err = test.LoadData(`testdata/message/UnpackMessage_SVCB_test.txt`)
	if err != nil {
		t.Fatal(logp, err)
	}

	var listCase = []string{
		`AliasMode`,
		`ServiceMode`,
		`ServiceMode:port`,
		`ServiceMode:keyGeneric667`,
		`ServiceMode:keyGenericQuoted`,
		`ServiceMode:TwoQuotedIpv6Hint`,
		`ServiceMode:Ipv6hintEmbedIpv4`,
		`ServiceMode:WithMandatoryKey`,
		`ServiceMode:AlpnWithEscapedComma`,
	}

	var (
		name    string
		msgjson []byte
	)
	for _, name = range listCase {
		var msg Message

		msg.packet, err = libbytes.ParseHexDump(tdata.Input[name], true)
		if err != nil {
			t.Fatal(logp, err)
		}

		err = msg.Unpack()
		if err != nil {
			t.Fatal(logp, err)
		}

		msgjson, err = json.MarshalIndent(&msg, ``, `  `)
		if err != nil {
			t.Fatal(logp, err)
		}
		test.Assert(t, name, string(tdata.Output[name]), string(msgjson))
	}
}

A lib/dns/rdata_https.go => lib/dns/rdata_https.go +48 -0
@@ 0,0 1,48 @@
// Copyright 2024, Shulhan <ms@kilabit.info>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dns

import (
	"bytes"
	"fmt"
	"io"
)

// RDataHTTPS the resource record for type 65 [HTTPS RR].
//
// [HTTPS RR]: https://datatracker.ietf.org/doc/html/rfc9460
type RDataHTTPS struct {
	RDataSVCB
}

// WriteTo write the SVCB record as zone format to out.
func (https *RDataHTTPS) WriteTo(out io.Writer) (_ int64, err error) {
	var buf bytes.Buffer

	fmt.Fprintf(&buf, `HTTPS %d %s`, https.Priority, https.TargetName)

	var (
		keys = https.keys()

		keyid int
	)
	for _, keyid = range keys {
		buf.WriteByte(' ')

		if keyid == svcbKeyIDNoDefaultALPN {
			buf.WriteString(svcbKeyNameNoDefaultALPN)
			continue
		}

		https.writeParam(&buf, keyid)
	}
	buf.WriteByte('\n')

	var n int

	n, err = out.Write(buf.Bytes())

	return int64(n), err
}

A lib/dns/rdata_svcb.go => lib/dns/rdata_svcb.go +995 -0
@@ 0,0 1,995 @@
// Copyright 2024, Shulhan <ms@kilabit.info>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dns

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"math"
	"net"
	"sort"
	"strconv"
	"strings"

	libbytes "git.sr.ht/~shulhan/pakakeh.go/lib/bytes"
)

// List of known parameter names for SVCB.
const (
	svcbKeyNameMandatory     = `mandatory`
	svcbKeyNameALPN          = `alpn`
	svcbKeyNameNoDefaultALPN = `no-default-alpn`
	svcbKeyNamePort          = `port`
	svcbKeyNameIpv4hint      = `ipv4hint`
	svcbKeyNameEch           = `ech`
	svcbKeyNameIpv6hint      = `ipv6hint`
)

const (
	svcbKeyIDMandatory     int = 0
	svcbKeyIDALPN          int = 1
	svcbKeyIDNoDefaultALPN int = 2
	svcbKeyIDPort          int = 3
	svcbKeyIDIpv4hint      int = 4
	svcbKeyIDEch           int = 5
	svcbKeyIDIpv6hint      int = 6
)

// RDataSVCB the resource record for type 64 [SVCB RR].
// Format of SVCB RDATA,
//
//	+-------------+
//	| SvcPriority | 2-octets.
//	+-------------+
//	/ TargetName  / A <domain-name>.
//	/             /
//	+-------------+
//	/ SvcParams   / A <character-string>.
//	/             /
//	+-------------+
//
// SVCB RR has two modes: AliasMode and ServiceMode.
// SvcPriority with value 0 indicates SVCB RR as AliasMode.
// SvcParams SHALL be used only for ServiceMode.
//
// The SvcParams contains the SVCB parameter key and value.
// Format of SvcParams,
//
//	+-------------------+
//	| SvcParamKey       | ; 2-octets.
//	+-------------------+
//	| SvcParamKeyLength | ; 2-octets, indicates the length of SvcParamValue.
//	+-------------------+
//	/ SvcParamValue     / ; Dynamic value based on the key.
//	/                   /
//	+-------------------+
//
// The RDATA considered malformed if:
//
//   - RDATA end at SvcParamKeyLength with non-zero value.
//   - SvcParamKey are not in increasing numeric order, for example: 1, 3, 2.
//   - Contains duplicate SvcParamKey.
//   - Contains invalid SvcParamValue format.
//
// Currently, there are six known keys,
//
//   - mandatory (0): define list of keys that must be exists on TargetName.
//     Each value is stored as 2-octets of its numeric ID.
//   - alpn (1): define list of Application-Layer Protocol Negotiation
//     (ALPN) supported by TargetName.
//     Each alpn is stored as combination of 2-octets length and its value.
//   - no-default-alpn (2): indicates that no default ALPN exists on
//     TargetName.
//     This key does not have value.
//   - port (3): define TCP or UDP port of TargetName.
//     The port value is encoded in 2-octets.
//   - ipv4hint (4): contains list of IPv4 addresses of TargetName.
//     Each IPv4 address is encoded in 4-octets.
//   - ech (5): Reserved.
//   - ipv6hint (6): contains list of IPv6 addresses of TargetName.
//     Each IPv6 address is encoded in 8-octets.
//
// A generic key can be defined in zone file by prefixing the number with
// string "key".
// For example,
//
//	key123="hello"
//
// will be encoded in RDATA as 123 (2-octets), followed by 5 (length of
// value, 2-octets), and followed by "hello" (5-octets).
//
// # Example
//
// The domain "example.com" provides a service "foo.example.org" with
// priority 16 and with two mandatory parameters: "alpn" and "ipv4hint".
//
//	example.com.   SVCB   16 foo.example.org. (
//	                           alpn=h2,h3-19 mandatory=ipv4hint,alpn
//	                           ipv4hint=192.0.2.1
//	                         )
//
// The above zone record when encoded to RDATA (displayed in decimal for
// readability),
//
//	+----+-----------------+
//	| 16 / foo.example.org /
//	+----+-----------------+
//	; SvcPriority=16               (2 octets)
//	; TargetName="foo.example.org" (domain-name, max 255 octects)
//	+---+---+---+---+
//	| 0 | 4 | 1 | 4 |
//	+---+---+---+---+
//	; SvcParamKey=0 (mandatory)  (2 octets)
//	; length=4                   (2 octets)
//	; value[0]: 1 (alpn)         (2 octets)
//	; value[1]: 4 (ipv4hint)     (2 octets)
//	+---+---+---+----+---+-------+
//	| 1 | 9 | 2 | h2 | 5 | h3-19 |
//	+---+---+---+----+---+-------+
//	; SvcParamKey=1 (alpn)              (2 octets)
//	; length=9                          (2 octets)
//	; value[0]: length=2, value="h2"    (1 + 2 octets)
//	; value[1]: length=5, value="h3-19" (1 + 5 octets)
//	+---+---+-----------+
//	| 4 | 4 | 192.0.2.1 |
//	+---+---+-----------+
//	; SvcParamKey=4 (ipv4hint)  (2 octets)
//	; length=4                  (2 octets)
//	; value="192.0.2.1"         (4 octets)
//
// [SVCB RR]: https://datatracker.ietf.org/doc/html/rfc9460
type RDataSVCB struct {
	// Params contains service parameters indexed by key's ID.
	Params map[int][]string

	TargetName string
	Priority   uint16
}

// AddParam add parameter to service binding.
// It will return an error if key already exist or contains invalid value.
func (svcb *RDataSVCB) AddParam(key string, listValue []string) (err error) {
	var logp = `AddParam`

	var keyid = svcbKeyID(key)
	if keyid < 0 {
		return fmt.Errorf(`%s: unknown key %q`, logp, key)
	}

	var isExist bool

	_, isExist = svcb.Params[keyid]
	if isExist {
		return fmt.Errorf(`%s: duplicate key %q`, logp, key)
	}

	switch keyid {
	case svcbKeyIDMandatory:
		var (
			listKeyID = map[int]struct{}{}
			name      string
			gotid     int
		)
		for _, name = range listValue {
			gotid = svcbKeyID(name)
			if gotid < 0 {
				return fmt.Errorf(`%s: invalid mandatory key %q`, logp, name)
			}
			_, isExist = listKeyID[gotid]
			if isExist {
				return fmt.Errorf(`%s: duplicate mandatory key %q`, logp, name)
			}
			listKeyID[gotid] = struct{}{}
		}
		svcb.Params[keyid] = listValue

	case svcbKeyIDALPN:
		var name string
		for _, name = range listValue {
			if len(name) > math.MaxUint8 {
				return fmt.Errorf(`%s: ALPN value must not exceed %d: %q`, logp, math.MaxUint8, name)
			}
		}
		svcb.Params[keyid] = listValue

	case svcbKeyIDNoDefaultALPN:
		if len(listValue) != 0 {
			return fmt.Errorf(`%s: key no-default-alpn must not have values`, logp)
		}
		svcb.Params[keyid] = listValue

	case svcbKeyIDPort:
		if len(listValue) == 0 {
			return fmt.Errorf(`%s: missing port value`, logp)
		}
		if len(listValue) > 1 {
			return fmt.Errorf(`%s: multiple port values %q`, logp, listValue)
		}

		var port int64

		port, err = strconv.ParseInt(listValue[0], 10, 16)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}
		if port < 0 || port > math.MaxUint16 {
			return fmt.Errorf(`%s: invalid port value %q`, logp, listValue[0])
		}
		svcb.Params[keyid] = listValue

	case svcbKeyIDIpv4hint, svcbKeyIDIpv6hint:
		if len(listValue) == 0 {
			return fmt.Errorf(`%s: missing %q value`, logp, key)
		}
		var (
			val string
			ip  net.IP
		)
		for _, val = range listValue {
			ip = net.ParseIP(val)
			if ip == nil {
				return fmt.Errorf(`%s: invalid IP %q`, logp, val)
			}
		}
		svcb.Params[keyid] = listValue

	case svcbKeyIDEch:
		// NO-OP.

	default:
		svcb.Params[keyid] = listValue
	}

	return nil
}

// WriteTo write the SVCB record as zone format to out.
func (svcb *RDataSVCB) WriteTo(out io.Writer) (_ int64, err error) {
	var buf bytes.Buffer

	fmt.Fprintf(&buf, `SVCB %d %s`, svcb.Priority, svcb.TargetName)

	var (
		keys = svcb.keys()

		keyid int
	)
	for _, keyid = range keys {
		buf.WriteByte(' ')

		if keyid == svcbKeyIDNoDefaultALPN {
			buf.WriteString(svcbKeyNameNoDefaultALPN)
			continue
		}

		svcb.writeParam(&buf, keyid)
	}
	buf.WriteByte('\n')

	var n int

	n, err = out.Write(buf.Bytes())

	return int64(n), err
}

func (svcb *RDataSVCB) getParamKey(zp *zoneParser) (_ []byte, err error) {
	var logp = `getParamKey`

	for {
		err = zp.next()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return nil, fmt.Errorf(`%s: %w`, logp, err)
		}
		if len(zp.token) != 0 {
			break
		}
	}
	return zp.token, nil
}

func (svcb *RDataSVCB) getParamValue(zp *zoneParser) (val []byte, err error) {
	var (
		logp = `getParamValue`

		lenToken int
		isQuoted bool
	)

	for {
		err = zp.next()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return nil, fmt.Errorf(`%s: %w`, logp, err)
		}

		val = append(val, zp.token...)

		if isQuoted {
			lenToken = len(zp.token)
			if lenToken != 0 && zp.token[lenToken-1] == '"' {
				if lenToken >= 2 && zp.token[lenToken-2] == '\\' {
					// Double-quote is escaped.
					continue
				}
				break
			}
			continue
		}
		if zp.token[0] == '"' {
			isQuoted = true
			continue
		}
		break
	}

	if isQuoted {
		val = val[1 : len(val)-1]
	}

	return val, nil
}

// keys return the list of sorted parameter key.
func (svcb *RDataSVCB) keys() (listKey []int) {
	var key int
	for key = range svcb.Params {
		listKey = append(listKey, key)
	}
	sort.Ints(listKey)
	return listKey
}

func (svcb *RDataSVCB) pack(msg *Message) (n int) {
	n = len(msg.packet)

	msg.packet = libbytes.AppendUint16(msg.packet, svcb.Priority)

	_ = msg.packDomainName([]byte(svcb.TargetName), false)

	var (
		sortedKeys = svcb.keys()

		listValue []string
		keyid     int
	)
	for _, keyid = range sortedKeys {
		listValue = svcb.Params[keyid]

		switch keyid {
		case svcbKeyIDMandatory:
			svcb.packMandatory(msg, listValue)

		case svcbKeyIDALPN:
			svcb.packALPN(msg, listValue)

		case svcbKeyIDNoDefaultALPN:
			msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDNoDefaultALPN))

		case svcbKeyIDPort:
			svcb.packPort(msg, listValue)

		case svcbKeyIDIpv4hint:
			svcb.packIpv4hint(msg, listValue)

		case svcbKeyIDEch:
			// NO-OP.

		case svcbKeyIDIpv6hint:
			svcb.packIpv6hint(msg, listValue)

		default:
			svcb.packGenericValue(keyid, msg, listValue)
		}
	}

	n = len(msg.packet) - n
	return n
}

func (svcb *RDataSVCB) packMandatory(msg *Message, listValue []string) {
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDMandatory))
	var total = 2 * len(listValue)
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(total))

	var (
		listKeyID = make([]int, 0, len(listValue))
		keyName   string
		keyid     int
	)
	for _, keyName = range listValue {
		keyid = svcbKeyID(keyName)
		listKeyID = append(listKeyID, keyid)
	}
	sort.Ints(listKeyID)
	for _, keyid = range listKeyID {
		msg.packet = libbytes.AppendUint16(msg.packet, uint16(keyid))
	}
}

func (svcb *RDataSVCB) packALPN(msg *Message, listValue []string) {
	var (
		val   string
		total int
	)
	for _, val = range listValue {
		total += 1 + len(val)
	}

	msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDALPN))
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(total))

	for _, val = range listValue {
		msg.packet = append(msg.packet, byte(len(val)))
		msg.packet = append(msg.packet, []byte(val)...)
	}
}

func (svcb *RDataSVCB) packPort(msg *Message, listValue []string) {
	var (
		port int64
		err  error
	)

	port, err = strconv.ParseInt(listValue[0], 10, 16)
	if err != nil {
		return
	}

	msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDPort))
	msg.packet = libbytes.AppendUint16(msg.packet, 2)
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(port))
}

func (svcb *RDataSVCB) packIpv4hint(msg *Message, listValue []string) {
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDIpv4hint))

	var total = 4 * len(listValue)
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(total))

	var val string

	for _, val = range listValue {
		msg.packIPv4(val)
	}
}

func (svcb *RDataSVCB) packIpv6hint(msg *Message, listValue []string) {
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(svcbKeyIDIpv6hint))

	var total = 16 * len(listValue)
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(total))

	var val string

	for _, val = range listValue {
		msg.packIPv6(val)
	}
}

func (svcb *RDataSVCB) packGenericValue(keyid int, msg *Message, listValue []string) {
	var val = strings.Join(listValue, `,`)

	msg.packet = libbytes.AppendUint16(msg.packet, uint16(keyid))
	msg.packet = libbytes.AppendUint16(msg.packet, uint16(len(val)))
	msg.packet = append(msg.packet, []byte(val)...)
}

// parseParams parse parameters from zone file.
//
//	SvcParam      = SvcParamKey [ "=" SvcParamValue ]
//	SvcParamKey   = 1*63(ASCII_LETTER / ASCII_DIGIT / "-")
//	SvcParamValue = STRING
//	WSP           = " " / "\t"
//	ASCII_LETTER  = ; a-z
//	ASCII_DIGIT   = ; 0-9
func (svcb *RDataSVCB) parseParams(zp *zoneParser) (err error) {
	var (
		logp = `parseParams`

		tok []byte
	)

	zp.parser.AddDelimiters([]byte{'='})
	defer zp.parser.RemoveDelimiters([]byte{'='})

	for {
		tok, err = svcb.getParamKey(zp)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}
		if len(tok) == 0 {
			break
		}

		var key = strings.ToLower(string(tok))
		if key == svcbKeyNameNoDefaultALPN {
			if zp.delim == '=' {
				return fmt.Errorf(`%s: key %q must not have value`, logp, key)
			}
			err = svcb.AddParam(key, nil)
			if err != nil {
				return fmt.Errorf(`%s: %w`, logp, err)
			}
			continue
		}

		tok, err = svcb.getParamValue(zp)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}
		if len(tok) == 0 {
			return fmt.Errorf(`%s: missing value for key %q`, logp, key)
		}

		tok, err = zp.decodeString(tok)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}

		var listValue []string

		listValue, err = svcbSplitRawValue(tok)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}

		err = svcb.AddParam(key, listValue)
		if err != nil {
			return fmt.Errorf(`%s: %w`, logp, err)
		}
	}

	return nil
}

func (svcb *RDataSVCB) unpack(packet []byte) (err error) {
	svcb.Priority = libbytes.ReadUint16(packet, 0)
	packet = packet[2:]

	var x uint

	svcb.TargetName, x, err = unpackDomainName(packet, 0)
	if err != nil {
		return err
	}
	packet = packet[x:]

	svcb.unpackParams(packet)

	return nil
}

func (svcb *RDataSVCB) unpackParams(packet []byte) (err error) {
	var keyid uint16

	for len(packet) > 0 {
		keyid = libbytes.ReadUint16(packet, 0)
		packet = packet[2:]

		switch int(keyid) {
		case svcbKeyIDMandatory:
			packet, err = svcb.unpackParamMandatory(packet)

		case svcbKeyIDALPN:
			packet, err = svcb.unpackParamALPN(packet)

		case svcbKeyIDNoDefaultALPN:
			svcb.Params[int(keyid)] = nil

		case svcbKeyIDPort:
			packet, err = svcb.unpackParamPort(packet)

		case svcbKeyIDIpv4hint:
			packet, err = svcb.unpackParamIpv4hint(packet)

		case svcbKeyIDEch:
			// NO-OP.

		case svcbKeyIDIpv6hint:
			packet, err = svcb.unpackParamIpv6hint(packet)

		default:
			packet, err = svcb.unpackParamGeneric(packet, int(keyid))
		}
		if err != nil {
			return err
		}
	}
	return nil
}

func (svcb *RDataSVCB) unpackParamMandatory(packet []byte) ([]byte, error) {
	if len(packet) < 2 {
		return packet, errors.New(`missing mandatory key value`)
	}

	var size = libbytes.ReadUint16(packet, 0)
	if size <= 0 {
		return packet, fmt.Errorf(`invalid mandatory length %d`, size)
	}
	packet = packet[2:]

	var (
		n = int(size) / 2

		listValue []string
	)
	for n > 0 {
		if len(packet) == 0 {
			return packet, fmt.Errorf(`missing mandatory value on index %d`, len(listValue))
		}

		var keyid = libbytes.ReadUint16(packet, 0)
		packet = packet[2:]

		var keyName = svcbKeyName(int(keyid))
		listValue = append(listValue, keyName)
		n--
	}
	svcb.Params[svcbKeyIDMandatory] = listValue

	return packet, nil
}

func (svcb *RDataSVCB) unpackParamALPN(packet []byte) ([]byte, error) {
	var logp = `unpackParamALPN`

	if len(packet) < 2 {
		return packet, fmt.Errorf(`%s: missing length and value`, logp)
	}

	var total = int(libbytes.ReadUint16(packet, 0))
	if total <= 0 {
		return packet, fmt.Errorf(`%s: invalid length %d`, logp, total)
	}
	packet = packet[2:]

	var listValue []string

	for total > 0 {
		if len(packet) == 0 {
			return packet, fmt.Errorf(`%s: missing value on index %d`, logp, len(listValue))
		}

		var n = int(packet[0])
		packet = packet[1:]
		total -= 1

		if len(packet) < int(total) {
			return packet, fmt.Errorf(`%s: mismatch value length, want %d got %d`, logp, n, len(packet))
		}

		var keyName = string(packet[:n])
		packet = packet[n:]

		listValue = append(listValue, keyName)
		total -= n
	}

	svcb.Params[svcbKeyIDALPN] = listValue

	return packet, nil
}

func (svcb *RDataSVCB) unpackParamPort(packet []byte) ([]byte, error) {
	var logp = `unpackParamPort`

	if len(packet) < 4 {
		return packet, fmt.Errorf(`%s: missing value`, logp)
	}

	var u16 = libbytes.ReadUint16(packet, 0)
	if u16 <= 0 {
		return packet, fmt.Errorf(`%s: invalid length %d`, logp, u16)
	}
	packet = packet[2:]

	u16 = libbytes.ReadUint16(packet, 0)
	if u16 <= 0 {
		return packet, fmt.Errorf(`%s: invalid port %d`, logp, u16)
	}
	packet = packet[2:]

	var portv string

	portv = strconv.FormatUint(uint64(u16), 10)
	svcb.Params[svcbKeyIDPort] = []string{portv}

	return packet, nil
}

func (svcb *RDataSVCB) unpackParamIpv4hint(packet []byte) ([]byte, error) {
	var logp = `unpackParamIpv4hint`

	if len(packet) < 2 {
		return packet, fmt.Errorf(`%s: missing value`, logp)
	}

	var size = int(libbytes.ReadUint16(packet, 0))
	if size <= 0 {
		return nil, fmt.Errorf(`%s: invalid length %d`, logp, size)
	}
	packet = packet[2:]

	var (
		n         = size / 4
		listValue []string
	)
	for n > 0 {
		if len(packet) < 4 {
			return packet, fmt.Errorf(`%s: missing value on index %d`, logp, len(listValue))
		}
		var ip = net.IP(packet[0:4])
		packet = packet[4:]
		listValue = append(listValue, ip.String())
	}

	svcb.Params[svcbKeyIDIpv4hint] = listValue
	return packet, nil
}

func (svcb *RDataSVCB) unpackParamIpv6hint(packet []byte) ([]byte, error) {
	var logp = `unpackParamIpv6hint`

	if len(packet) < 2 {
		return packet, fmt.Errorf(`%s: missing value`, logp)
	}

	var size = int(libbytes.ReadUint16(packet, 0))
	if size <= 0 {
		return nil, fmt.Errorf(`%s: invalid length %d`, logp, size)
	}
	packet = packet[2:]

	var (
		n         = size / 16
		listValue []string
	)
	for n > 0 {
		if len(packet) < 16 {
			return packet, fmt.Errorf(`%s: missing value on index %d`, logp, len(listValue))
		}
		var ip = net.IP(packet[:16])
		packet = packet[16:]
		listValue = append(listValue, ip.String())
		n--
	}

	svcb.Params[svcbKeyIDIpv6hint] = listValue

	return packet, nil
}

func (svcb *RDataSVCB) unpackParamGeneric(packet []byte, keyid int) ([]byte, error) {
	var logp = `unpackParamGeneric`

	if len(packet) < 2 {
		return nil, fmt.Errorf(`%s: missing parameter value`, logp)
	}

	var size = int(libbytes.ReadUint16(packet, 0))
	if size <= 0 {
		return packet, fmt.Errorf(`%s: invalid length %d`, logp, size)
	}
	packet = packet[2:]

	if len(packet) < size {
		return packet, fmt.Errorf(`%s: mismatch value length, want %d got %d`,
			logp, size, len(packet))
	}

	var val = string(packet[:size])
	packet = packet[size:]

	svcb.Params[keyid] = []string{val}

	return packet, nil
}

// validate the mandatory parameter.
// Each key in mandatory value should only defined once.
func (svcb *RDataSVCB) validate() (err error) {
	var (
		listValue []string
		ok        bool
	)
	listValue, ok = svcb.Params[svcbKeyIDMandatory]
	if !ok {
		return nil
	}

	var (
		key   string
		keyid int
	)
	for _, key = range listValue {
		keyid = svcbKeyID(key)
		if keyid < 0 {
			return fmt.Errorf(`invalid key %q`, key)
		}
		if keyid == svcbKeyIDMandatory {
			return errors.New(`mandatory key must not be included in the "mandatory" value`)
		}
		_, ok = svcb.Params[keyid]
		if !ok {
			return fmt.Errorf(`missing mandatory key %q`, key)
		}
	}
	return nil
}

func (svcb *RDataSVCB) writeParam(out io.Writer, keyid int) {
	var (
		listValue = svcb.Params[keyid]

		sb        strings.Builder
		val       string
		x         int
		isEscaped bool
		isQuoted  bool
	)
	for x, val = range listValue {
		if x > 0 {
			sb.WriteByte(',')
		}
		val, isEscaped = svcbEncodeValue(val)
		if isEscaped {
			isQuoted = true
		}
		sb.WriteString(val)
	}

	var keyName = svcbKeyName(keyid)
	if isQuoted {
		fmt.Fprintf(out, `%s="%s"`, keyName, sb.String())
	} else {
		fmt.Fprintf(out, `%s=%s`, keyName, sb.String())
	}
}

// svcbEncodeValue encode the parameter value.
// A comma ',', backslash '\', or double quote '"' will be escaped using
// backslash.
// Non-printable character will be encoded as escaped octal, "\XXX", where
// XXX is the octal value of character.
func svcbEncodeValue(in string) (out string, escaped bool) {
	var (
		rawin = []byte(in)

		sb strings.Builder
		c  byte
	)
	for _, c = range rawin {
		switch {
		case c == ',', c == '\\', c == '"':
			sb.WriteString(`\\\`)
			sb.WriteByte(c)
			escaped = true
			continue

		case c == '!',
			c >= 0x23 && c <= 0x27,
			c >= 0x2A && c <= 0x3A,
			c >= 0x3C && c <= 0x5B,
			c >= 0x5D && c <= 0x7E:
			sb.WriteByte(c)

		default:
			// Write byte as escaped decimal "\XXX".
			sb.WriteString(`\` + strconv.FormatUint(uint64(c), 10))
			escaped = true
		}

	}
	return sb.String(), escaped
}

// svcbSplitRawValue split raw SVCB parameter value by comma ','.
// A comma can be escaped using backslash '\'.
// A backslash also can be escaped using backslash.
// Other than that, no escaped sequence are allowed.
func svcbSplitRawValue(raw []byte) (listValue []string, err error) {
	var (
		val   []byte
		x     int
		isEsc bool
	)
	for ; x < len(raw); x++ {
		if isEsc {
			switch raw[x] {
			case '\\':
				val = append(val, '\\')
			case ',':
				val = append(val, ',')
			default:
				return nil, fmt.Errorf(`invalid escaped character %q`, raw[x])
			}
			isEsc = false
			continue
		}
		if raw[x] == '\\' {
			isEsc = true
			continue
		}
		if raw[x] == ',' {
			listValue = append(listValue, string(val))
			val = nil
			continue
		}
		val = append(val, raw[x])
	}
	if len(val) != 0 {
		listValue = append(listValue, string(val))
	}
	return listValue, nil
}

// svcbKeyID return the key ID based on string value.
// It will return -1 if key is invalid.
func svcbKeyID(key string) int {
	switch key {
	case svcbKeyNameMandatory:
		return svcbKeyIDMandatory
	case svcbKeyNameALPN:
		return svcbKeyIDALPN
	case svcbKeyNameNoDefaultALPN:
		return svcbKeyIDNoDefaultALPN
	case svcbKeyNamePort:
		return svcbKeyIDPort
	case svcbKeyNameIpv4hint:
		return svcbKeyIDIpv4hint
	case svcbKeyNameEch:
		return svcbKeyIDEch
	case svcbKeyNameIpv6hint:
		return svcbKeyIDIpv6hint
	}
	if !strings.HasPrefix(key, `key`) {
		return -1
	}

	key = strings.TrimPrefix(key, `key`)

	var (
		keyid int64
		err   error
	)

	keyid, err = strconv.ParseInt(key, 10, 16)
	if err != nil {
		return -1
	}
	if keyid < 0 || keyid > math.MaxUint16 {
		return -1
	}
	return int(keyid)
}

func svcbKeyName(keyid int) string {
	switch keyid {
	case svcbKeyIDMandatory:
		return svcbKeyNameMandatory
	case svcbKeyIDALPN:
		return svcbKeyNameALPN
	case svcbKeyIDNoDefaultALPN:
		return svcbKeyNameNoDefaultALPN
	case svcbKeyIDPort:
		return svcbKeyNamePort
	case svcbKeyIDIpv4hint:
		return svcbKeyNameIpv4hint
	case svcbKeyIDEch:
		return svcbKeyNameEch
	case svcbKeyIDIpv6hint:
		return svcbKeyNameIpv6hint
	}
	return fmt.Sprintf(`key%d`, keyid)
}

M lib/dns/record_type.go => lib/dns/record_type.go +11 -3
@@ 29,9 29,13 @@ const (
	RecordTypeMX                      // 15 - Mail exchange
	RecordTypeTXT                     // 16 - Text strings

	RecordTypeAAAA  RecordType = 28  // IPv6 address
	RecordTypeSRV   RecordType = 33  // A SRV RR for locating service.
	RecordTypeOPT   RecordType = 41  // An OPT pseudo-RR (sometimes called a meta-RR)
	RecordTypeAAAA RecordType = 28 // IPv6 address
	RecordTypeSRV  RecordType = 33 // A SRV RR for locating service.
	RecordTypeOPT  RecordType = 41 // An OPT pseudo-RR (sometimes called a meta-RR)

	RecordTypeSVCB  RecordType = 64 // RFC 9460.
	RecordTypeHTTPS RecordType = 65 // RFC 9460.

	RecordTypeAXFR  RecordType = 252 // A request for a transfer of an entire zone
	RecordTypeMAILB RecordType = 253 // A request for mailbox-related records (MB, MG or MR)
	RecordTypeMAILA RecordType = 254 // A request for mail agent RRs (Obsolete - see MX)


@@ 47,6 51,7 @@ var RecordTypes = map[string]RecordType{
	"AXFR":  RecordTypeAXFR,
	"CNAME": RecordTypeCNAME,
	"HINFO": RecordTypeHINFO,
	`HTTPS`: RecordTypeHTTPS,
	"MAILA": RecordTypeMAILA,
	"MAILB": RecordTypeMAILB,
	"MB":    RecordTypeMB,


@@ 61,6 66,7 @@ var RecordTypes = map[string]RecordType{
	"OPT":   RecordTypeOPT,
	"PTR":   RecordTypePTR,
	"SOA":   RecordTypeSOA,
	`SVCB`:  RecordTypeSVCB,
	"SRV":   RecordTypeSRV,
	"TXT":   RecordTypeTXT,
	"WKS":   RecordTypeWKS,


@@ 75,6 81,7 @@ var RecordTypeNames = map[RecordType]string{
	RecordTypeAXFR:  "AXFR",
	RecordTypeCNAME: "CNAME",
	RecordTypeHINFO: "HINFO",
	RecordTypeHTTPS: `HTTPS`,
	RecordTypeMAILA: "MAILA",
	RecordTypeMAILB: "MAILB",
	RecordTypeMB:    "MB",


@@ 89,6 96,7 @@ var RecordTypeNames = map[RecordType]string{
	RecordTypeOPT:   "OPT",
	RecordTypePTR:   "PTR",
	RecordTypeSOA:   "SOA",
	RecordTypeSVCB:  `SVCB`,
	RecordTypeSRV:   "SRV",
	RecordTypeTXT:   "TXT",
	RecordTypeWKS:   "WKS",

M lib/dns/resource_record.go => lib/dns/resource_record.go +48 -0
@@ 400,6 400,12 @@ func (rr *ResourceRecord) unpackRData(packet []byte, startIdx uint) (err error) 
	case RecordTypeOPT:
		return rr.unpackOPT(packet, startIdx)

	case RecordTypeSVCB:
		return rr.unpackSVCB(packet, startIdx)

	case RecordTypeHTTPS:
		return rr.unpackHTTPS(packet, startIdx)

	default:
		log.Printf("= Unknown query type: %d\n", rr.Type)
	}


@@ 541,6 547,48 @@ func (rr *ResourceRecord) unpackOPT(packet []byte, x uint) error {
	return nil
}

func (rr *ResourceRecord) unpackSVCB(packet []byte, x uint) (err error) {
	var (
		logp = `unpackSVCB`
		svcb = &RDataSVCB{
			Params: map[int][]string{},
		}
	)

	packet = packet[x:]

	err = svcb.unpack(packet)
	if err != nil {
		return fmt.Errorf(`%s: %w`, logp, err)
	}

	rr.Value = svcb

	return nil
}

func (rr *ResourceRecord) unpackHTTPS(packet []byte, x uint) (err error) {
	var (
		logp  = `unpackHTTPS`
		https = &RDataHTTPS{
			RDataSVCB: RDataSVCB{
				Params: map[int][]string{},
			},
		}
	)

	packet = packet[x:]

	err = https.RDataSVCB.unpack(packet)
	if err != nil {
		return fmt.Errorf(`%s: %w`, logp, err)
	}

	rr.Value = https

	return nil
}

func (rr *ResourceRecord) unpackSOA(packet []byte, startIdx uint) (err error) {
	var (
		logp  = "unpackSOA"

M lib/dns/server.go => lib/dns/server.go +2 -1
@@ 560,7 560,8 @@ func (srv *Server) isImplemented(msg *Message) bool {
	}
	switch msg.Question.Type {
	case RecordTypeAAAA, RecordTypeSRV, RecordTypeOPT, RecordTypeAXFR,
		RecordTypeMAILB, RecordTypeMAILA:
		RecordTypeMAILB, RecordTypeMAILA,
		RecordTypeSVCB, RecordTypeHTTPS:
		return true
	}


A lib/dns/testdata/ParseZone_SVCB_test.txt => lib/dns/testdata/ParseZone_SVCB_test.txt +301 -0
@@ 0,0 1,301 @@
vi: set tw=0:

Test data for parsing SVCB and HTTPS record from zone file, based on
RFC 9460, Appendix D.

>>> AliasMode
example.com.   HTTPS   0 foo.example.com.

<<< AliasMode
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN HTTPS 0 foo.example.com.

<<< AliasMode:message_0.hex
{Name:example.com. Type:HTTPS}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 41 00 01 c0 0c 00 | ..A..... |   0   0  65   0   1 192  12   0 |24
0x00000020| 41 00 01 00 00 00 3c 00 | A.....<. |  65   0   1   0   0   0  60   0 |32
0x00000028| 11 00 00 03 66 6f 6f 07 | ....foo. |  17   0   0   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 63 6f 6d 00             | com.     |  99 111 109   0                 |56

>>> ServiceMode
example.com.   SVCB   1 .

<<< ServiceMode
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 1 .

<<< ServiceMode:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 03 00 01 00             | ....     |   3   0   1   0                 |40

>>> ServiceMode:port
example.com.   SVCB   16 foo.example.com. port=53

<<< ServiceMode:port
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 16 foo.example.com. port=53

<<< ServiceMode:port:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 19 00 10 03 66 6f 6f 07 | ....foo. |  25   0  16   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 63 6f 6d 00 00 03 00 02 | com..... |  99 111 109   0   0   3   0   2 |56
0x00000040| 00 35                   | .5       |   0  53                         |64

>>> ServiceMode:keyGeneric667
example.com.   SVCB   1 foo.example.com. key667=hello

<<< ServiceMode:keyGeneric667
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 1 foo.example.com. key667=hello

<<< ServiceMode:keyGeneric667:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 1c 00 01 03 66 6f 6f 07 | ....foo. |  28   0   1   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 63 6f 6d 00 02 9b 00 05 | com..... |  99 111 109   0   2 155   0   5 |56
0x00000040| 68 65 6c 6c 6f          | hello    | 104 101 108 108 111             |64

>>> ServiceMode:keyGenericQuoted
example.com.   SVCB   1 foo.example.com. key667="hello\210qoo"

<<< ServiceMode:keyGenericQuoted
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 1 foo.example.com. key667="hello\210qoo"

<<< ServiceMode:keyGenericQuoted:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 20 00 01 03 66 6f 6f 07 | ....foo. |  32   0   1   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 63 6f 6d 00 02 9b 00 09 | com..... |  99 111 109   0   2 155   0   9 |56
0x00000040| 68 65 6c 6c 6f d2 71 6f | hello.qo | 104 101 108 108 111 210 113 111 |64
0x00000048| 6f                      | o        | 111                             |72

>>> ServiceMode:TwoQuotedIpv6Hint
example.com.   SVCB   1 foo.example.com. (
                      ipv6hint="2001:db8::1,2001:db8::53:1"
                      )

<<< ServiceMode:TwoQuotedIpv6Hint
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 1 foo.example.com. ipv6hint=2001:db8::1,2001:db8::53:1

<<< ServiceMode:TwoQuotedIpv6Hint:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 37 00 01 03 66 6f 6f 07 | 7...foo. |  55   0   1   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 63 6f 6d 00 00 06 00 20 | com..... |  99 111 109   0   0   6   0  32 |56
0x00000040| 20 01 0d b8 00 00 00 00 | ........ |  32   1  13 184   0   0   0   0 |64
0x00000048| 00 00 00 00 00 00 00 01 | ........ |   0   0   0   0   0   0   0   1 |72
0x00000050| 20 01 0d b8 00 00 00 00 | ........ |  32   1  13 184   0   0   0   0 |80
0x00000058| 00 00 00 00 00 53 00 01 | .....S.. |   0   0   0   0   0  83   0   1 |88

>>> ServiceMode:Ipv6hintEmbedIpv4
example.com.   SVCB   1 example.com. (
                        ipv6hint="2001:db8:122:344::192.0.2.33"
                        )

<<< ServiceMode:Ipv6hintEmbedIpv4
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 1 example.com. ipv6hint=2001:db8:122:344::192.0.2.33

<<< ServiceMode:Ipv6hintEmbedIpv4:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 23 00 01 07 65 78 61 6d | #...exam |  35   0   1   7 101 120  97 109 |40
0x00000030| 70 6c 65 03 63 6f 6d 00 | ple.com. | 112 108 101   3  99 111 109   0 |48
0x00000038| 00 06 00 10 20 01 0d b8 | ........ |   0   6   0  16  32   1  13 184 |56
0x00000040| 01 22 03 44 00 00 00 00 | .".D.... |   1  34   3  68   0   0   0   0 |64
0x00000048| c0 00 02 21             | ...!     | 192   0   2  33                 |72

>>> ServiceMode:WithMandatoryKey
example.com.   SVCB   16 foo.example.org. (
                      alpn=h2,h3-19 mandatory=ipv4hint,alpn
                      ipv4hint=192.0.2.1
                      )

<<< ServiceMode:WithMandatoryKey
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 16 foo.example.org. mandatory=ipv4hint,alpn alpn=h2,h3-19 ipv4hint=192.0.2.1

<<< ServiceMode:WithMandatoryKey:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 30 00 10 03 66 6f 6f 07 | 0...foo. |  48   0  16   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 6f 72 67 00 00 00 00 04 | org..... | 111 114 103   0   0   0   0   4 |56
0x00000040| 00 01 00 04 00 01 00 09 | ........ |   0   1   0   4   0   1   0   9 |64
0x00000048| 02 68 32 05 68 33 2d 31 | .h2.h3-1 |   2 104  50   5 104  51  45  49 |72
0x00000050| 39 00 04 00 04 c0 00 02 | 9....... |  57   0   4   0   4 192   0   2 |80
0x00000058| 01                      | .        |   1                             |88

>>> ServiceMode:AlpnWithEscapedComma
example.com.   SVCB   16 foo.example.org. alpn="f\\\\oo\\,bar,h2"

<<< ServiceMode:AlpnWithEscapedComma
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 16 foo.example.org. alpn="f\\\\oo\\\,bar,h2"

<<< ServiceMode:AlpnWithEscapedComma:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 23 00 10 03 66 6f 6f 07 | #...foo. |  35   0  16   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 6f 72 67 00 00 01 00 0c | org..... | 111 114 103   0   0   1   0  12 |56
0x00000040| 08 66 5c 6f 6f 2c 62 61 | .f\oo,ba |   8 102  92 111 111  44  98  97 |64
0x00000048| 72 02 68 32             | r.h2     | 114   2 104  50                 |72

>>> ServiceMode:AlpnWithEscapedBackslash
example.com.   SVCB   16 foo.example.org. alpn=f\\\092oo\092,bar,h2

<<< ServiceMode:AlpnWithEscapedBackslash
$ORIGIN example.com.
@ SOA example.com. root 1691222000 86400 3600 0 60
@ 60 IN SVCB 16 foo.example.org. alpn="f\\\\oo\\\,bar,h2"

<<< ServiceMode:AlpnWithEscapedBackslash:message_0.hex
{Name:example.com. Type:SVCB}
          |  0  1  2  3  4  5  6  7 | 01234567 |   0   1   2   3   4   5   6   7 |
          |  8  9  A  B  C  D  E  F | 89ABCDEF |   8   9   A   B   C   D   E   F |
0x00000000| 00 00 84 00 00 01 00 01 | ........ |   0   0 132   0   0   1   0   1 |0
0x00000008| 00 00 00 00 07 65 78 61 | .....exa |   0   0   0   0   7 101 120  97 |8
0x00000010| 6d 70 6c 65 03 63 6f 6d | mple.com | 109 112 108 101   3  99 111 109 |16
0x00000018| 00 00 40 00 01 c0 0c 00 | ..@..... |   0   0  64   0   1 192  12   0 |24
0x00000020| 40 00 01 00 00 00 3c 00 | @.....<. |  64   0   1   0   0   0  60   0 |32
0x00000028| 23 00 10 03 66 6f 6f 07 | #...foo. |  35   0  16   3 102 111 111   7 |40
0x00000030| 65 78 61 6d 70 6c 65 03 | example. | 101 120  97 109 112 108 101   3 |48
0x00000038| 6f 72 67 00 00 01 00 0c | org..... | 111 114 103   0   0   1   0  12 |56
0x00000040| 08 66 5c 6f 6f 2c 62 61 | .f\oo,ba |   8 102  92 111 111  44  98  97 |64
0x00000048| 72 02 68 32             | r.h2     | 114   2 104  50                 |72

>>> FailureMode:DuplicateKey
example.com.   SVCB   1 foo.example.com. (
                          key123=abc key123=def
                          )

<<< FailureMode:DuplicateKey:error
ParseZone: parse: parseRR: line 2: parseSVCB: parseParams: AddParam: duplicate key "key123"

>>> FailureMode:KeyMandatoryNoValue
example.com.   SVCB   1 foo.example.com. mandatory

<<< FailureMode:KeyMandatoryNoValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: missing value for key "mandatory"

>>> FailureMode:KeyAlpnNoValue
example.com.   SVCB   1 foo.example.com. alpn

<<< FailureMode:KeyAlpnNoValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: missing value for key "alpn"

>>> FailureMode:KeyPortNoValue
example.com.   SVCB   1 foo.example.com. port

<<< FailureMode:KeyPortNoValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: missing value for key "port"

>>> FailureMode:KeyIpv4hintNoValue
example.com.   SVCB   1 foo.example.com. ipv4hint

<<< FailureMode:KeyIpv4hintNoValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: missing value for key "ipv4hint"

>>> FailureMode:KeyIpv6hintNoValue
example.com.   SVCB   1 foo.example.com. ipv6hint

<<< FailureMode:KeyIpv6hintNoValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: missing value for key "ipv6hint"

>>> FailureMode:KeyNodefaultalpnWithValue
example.com.   SVCB   1 foo.example.com. no-default-alpn=abc

<<< FailureMode:KeyNodefaultalpnWithValue:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: key "no-default-alpn" must not have value

>>> FailureMode:MissingMandatoryKey
example.com.   SVCB   1 foo.example.com. mandatory=key123

<<< FailureMode:MissingMandatoryKey:error
ParseZone: parse: parseRR: line 1: parseSVCB: missing mandatory key "key123"

>>> FailureMode:RecursiveMandatoryKey
example.com.   SVCB   1 foo.example.com. mandatory=mandatory

<<< FailureMode:RecursiveMandatoryKey:error
ParseZone: parse: parseRR: line 1: parseSVCB: mandatory key must not be included in the "mandatory" value

>>> FailureMode:DuplicateMandatoryKey
example.com.   SVCB   1 foo.example.com. (
                         mandatory=key123,key123 key123=abc
                         )

<<< FailureMode:DuplicateMandatoryKey:error
ParseZone: parse: parseRR: line 1: parseSVCB: parseParams: AddParam: duplicate mandatory key "key123"

A lib/dns/testdata/message/UnpackMessage_SVCB_test.txt => lib/dns/testdata/message/UnpackMessage_SVCB_test.txt +441 -0
@@ 0,0 1,441 @@
Test data for parsing SVCB record from bytes.
The test input taken from output of parsing SVCB record from zone file.

>>> AliasMode
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4100 01c0 0c00
0000020 4100 0100 0000 3c00 1100 0003 666f 6f07
0000030 6578 616d 706c 6503 636f 6d00

<<< AliasMode
{
  "Answer": [
    {
      "Value": {
        "Params": {},
        "TargetName": "foo.example.com",
        "Priority": 0
      },
      "Name": "example.com",
      "Type": 65,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 65,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 0300 0100

<<< ServiceMode
{
  "Answer": [
    {
      "Value": {
        "Params": {},
        "TargetName": "",
        "Priority": 1
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:port
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 1900 1003 666f 6f07
0000030 6578 616d 706c 6503 636f 6d00 0003 0002
0000040 0035

<<< ServiceMode:port
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "3": [
            "53"
          ]
        },
        "TargetName": "foo.example.com",
        "Priority": 16
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:keyGeneric667
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 1c00 0103 666f 6f07
0000030 6578 616d 706c 6503 636f 6d00 029b 0005
0000040 6865 6c6c 6f

<<< ServiceMode:keyGeneric667
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "667": [
            "hello"
          ]
        },
        "TargetName": "foo.example.com",
        "Priority": 1
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:keyGenericQuoted
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 2000 0103 666f 6f07
0000030 6578 616d 706c 6503 636f 6d00 029b 0009
0000040 6865 6c6c 6fd2 716f 6f

<<< ServiceMode:keyGenericQuoted
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "667": [
            "hello\ufffdqoo"
          ]
        },
        "TargetName": "foo.example.com",
        "Priority": 1
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:TwoQuotedIpv6Hint
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 3700 0103 666f 6f07
0000030 6578 616d 706c 6503 636f 6d00 0006 0020
0000040 2001 0db8 0000 0000 0000 0000 0000 0001
0000050 2001 0db8 0000 0000 0000 0000 0053 0001

<<< ServiceMode:TwoQuotedIpv6Hint
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "6": [
            "2001:db8::1",
            "2001:db8::53:1"
          ]
        },
        "TargetName": "foo.example.com",
        "Priority": 1
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:Ipv6hintEmbedIpv4
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 2300 0107 6578 616d
0000030 706c 6503 636f 6d00 0006 0010 2001 0db8
0000040 0122 0344 0000 0000 c000 0221

<<< ServiceMode:Ipv6hintEmbedIpv4
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "6": [
            "2001:db8:122:344::c000:221"
          ]
        },
        "TargetName": "example.com",
        "Priority": 1
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:WithMandatoryKey
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 3000 1003 666f 6f07
0000030 6578 616d 706c 6503 6f72 6700 0000 0004
0000040 0001 0004 0001 0009 0268 3205 6833 2d31
0000050 3900 0400 04c0 0002 01

<<< ServiceMode:WithMandatoryKey
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "0": [
            "alpn",
            "ipv4hint"
          ],
          "1": [
            "h2",
            "h3-19"
          ]
        },
        "TargetName": "foo.example.org",
        "Priority": 16
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

>>> ServiceMode:AlpnWithEscapedComma
0000000 0000 8400 0001 0001 0000 0000 0765 7861
0000010 6d70 6c65 0363 6f6d 0000 4000 01c0 0c00
0000020 4000 0100 0000 3c00 2300 1003 666f 6f07
0000030 6578 616d 706c 6503 6f72 6700 0001 000c
0000040 0866 5c6f 6f2c 6261 7202 6832          

<<< ServiceMode:AlpnWithEscapedComma
{
  "Answer": [
    {
      "Value": {
        "Params": {
          "1": [
            "f\\oo,bar",
            "h2"
          ]
        },
        "TargetName": "foo.example.org",
        "Priority": 16
      },
      "Name": "example.com",
      "Type": 64,
      "Class": 1,
      "TTL": 60
    }
  ],
  "Authority": null,
  "Additional": null,
  "Question": {
    "Name": "example.com",
    "Type": 64,
    "Class": 1
  },
  "Header": {
    "ID": 0,
    "IsQuery": false,
    "Op": 0,
    "IsAA": true,
    "IsTC": false,
    "IsRD": false,
    "IsRA": false,
    "RCode": 0,
    "QDCount": 1,
    "ANCount": 1,
    "NSCount": 0,
    "ARCount": 0
  }
}

A lib/dns/testdata/zoneParser_next_test.txt => lib/dns/testdata/zoneParser_next_test.txt +35 -0
@@ 0,0 1,35 @@

>>> comments
a b ; c d
e; f g
;h i
;j k

<<< comments
"a" ' '
"b" ' '
"" '\n'
"e" '\n'
"" '\n'
"" '\x00'


>>> multiline
a b c=d e="f g" (
 h=i j="k l"
) m n
( o p )

<<< multiline
"a" ' '
"b" ' '
"c=d" ' '
"e=\"f" ' '
"g\"" ' '
"h=i" ' '
"j=\"k" ' '
"l\"" '\n'
"m" ' '
"n" '\n'
"o" ' '
"p" ' '

M lib/dns/zone.go => lib/dns/zone.go +29 -0
@@ 292,6 292,7 @@ func (zone *Zone) Save() (err error) {

func (zone *Zone) saveListRR(out io.Writer, dname string, listRR []*ResourceRecord) (total int, err error) {
	var (
		logp         = `saveListRR`
		suffixOrigin = "." + zone.Origin

		hinfo *RDataHINFO


@@ 407,6 408,34 @@ func (zone *Zone) saveListRR(out io.Writer, dname string, listRR []*ResourceReco
				"%s %d %s SRV %d %d %d %s\n",
				dname, rr.TTL, RecordClassName[rr.Class],
				srv.Priority, srv.Weight, srv.Port, v)

		case RecordTypeSVCB:
			var svcb *RDataSVCB

			svcb, ok = rr.Value.(*RDataSVCB)
			if !ok {
				return total, fmt.Errorf(`%s: expecting %T, got %T`, logp, svcb, rr.Value)
			}
			n, _ = fmt.Fprintf(out, `%s %d IN `, dname, rr.TTL)
			total += n

			var n64 int64
			n64, _ = svcb.WriteTo(out)
			n = int(n64)

		case RecordTypeHTTPS:
			var https *RDataHTTPS

			https, ok = rr.Value.(*RDataHTTPS)
			if !ok {
				return total, fmt.Errorf(`%s: expecting %T, got %T`, logp, https, rr.Value)
			}
			n, err = fmt.Fprintf(out, `%s %d IN `, dname, rr.TTL)
			total += n

			var n64 int64
			n64, _ = https.WriteTo(out)
			n = int(n64)
		}
		if err != nil {
			return total, err

M lib/dns/zone_parser.go => lib/dns/zone_parser.go +177 -2
@@ 7,6 7,8 @@ package dns
import (
	"bytes"
	"fmt"
	"io"
	"math"
	"strconv"
	"strings"
	"time"


@@ 26,10 28,15 @@ const (
)

type zoneParser struct {
	err    error
	zone   *Zone
	parser *libbytes.Parser
	lastRR *ResourceRecord
	lineno int

	token       []byte
	lineno      int
	delim       byte
	isMultiline bool
}

func newZoneParser(data []byte, zone *Zone) (zp *zoneParser) {


@@ 52,7 59,12 @@ func (m *zoneParser) Reset(data []byte, zone *Zone) {

	data = bytes.TrimSpace(data)
	data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
	m.parser.Reset(data, []byte{' ', '\t', '\n', ';'})
	m.parser.Reset(data, []byte{' ', '\t', '(', ')', '\n', ';'})

	m.err = nil
	m.token = nil
	m.delim = ' '
	m.isMultiline = false
}

// The format of these files is a sequence of entries.  Entries are


@@ 479,6 491,8 @@ func (m *zoneParser) parseRRClassOrType(rr *ResourceRecord, stok string, flag in
}

func (m *zoneParser) parseRRData(rr *ResourceRecord, tok []byte, c byte) (err error) {
	var logp = `parseRRData`

	switch rr.Type {
	case RecordTypeA, RecordTypeAAAA:
		rr.Value = string(tok)


@@ 515,6 529,15 @@ func (m *zoneParser) parseRRData(rr *ResourceRecord, tok []byte, c byte) (err er

	case RecordTypeSRV:
		err = m.parseSRV(rr, tok)

	case RecordTypeSVCB:
		err = m.parseSVCB(rr, tok)

	case RecordTypeHTTPS:
		err = m.parseHTTPS(rr, tok)

	default:
		err = fmt.Errorf(`%s: unknown record type %d`, logp, rr.Type)
	}

	return err


@@ 680,6 703,86 @@ func (m *zoneParser) parseHInfo(rr *ResourceRecord, tok []byte) (err error) {
	return nil
}

func (m *zoneParser) parseSVCB(rr *ResourceRecord, tok []byte) (err error) {
	var (
		logp = `parseSVCB`
		stok = string(tok)

		priority int64
	)

	priority, err = strconv.ParseInt(stok, 10, 16)
	if err != nil {
		return fmt.Errorf(`%s: invalid SvcPriority %q: %w`, logp, stok, err)
	}
	if priority > math.MaxUint16 {
		return fmt.Errorf(`%s: overflow SvcPriority %d`, logp, priority)
	}

	err = m.next()
	if err != nil {
		return fmt.Errorf(`%s: missing TargetName`, logp)
	}

	var svcb = &RDataSVCB{
		Priority:   uint16(priority),
		TargetName: m.generateDomainName(m.token),
		Params:     map[int][]string{},
	}

	err = svcb.parseParams(m)
	if err != nil {
		return fmt.Errorf(`%s: %w`, logp, err)
	}

	err = svcb.validate()
	if err != nil {
		return fmt.Errorf(`%s: %w`, logp, err)
	}

	rr.Value = svcb

	return nil
}

func (m *zoneParser) parseHTTPS(rr *ResourceRecord, tok []byte) (err error) {
	var (
		logp = `parseHTTPS`
		stok = string(tok)

		priority int64
	)

	priority, err = strconv.ParseInt(stok, 10, 64)
	if err != nil {
		return fmt.Errorf(`%s: invalid SvcPriority %q: %w`, logp, stok, err)
	}
	if priority != 0 {
		return fmt.Errorf(`%s: expecting 0, got %q`, logp, stok)
	}

	err = m.next()
	if err != nil {
		return fmt.Errorf(`%s: missing TargetName`, logp)
	}

	var https = &RDataHTTPS{
		RDataSVCB: RDataSVCB{
			TargetName: m.generateDomainName(m.token),
			Params:     map[int][]string{},
		},
	}

	err = https.RDataSVCB.parseParams(m)
	if err != nil {
		return fmt.Errorf(`%s: %w`, logp, err)
	}

	rr.Value = https

	return nil
}

func (m *zoneParser) parseMInfo(rr *ResourceRecord, tok []byte) (err error) {
	var (
		logp    = `parseMInfo`


@@ 1056,3 1159,75 @@ func (m *zoneParser) pack() {
		}
	}
}

// next get next token and delimiter.
// The end of reading single record indicated by [zoneParser.delim] set to
// LF ("\n").
// A multiline record always return ' ' even if token is end with LF.
func (m *zoneParser) next() (err error) {
	if m.delim == 0 {
		// Calling next when we reached EOF always return false.
		m.token = nil
		return io.EOF
	}
	if m.delim == ';' {
		// Skip until new line.
		m.delim = m.parser.SkipLine()
		if m.delim == 0 {
			return io.EOF
		}
	}

	for {
		m.token, m.delim = m.parser.ReadNoSpace()
		switch m.delim {
		case ';':
			m.delim = m.parser.SkipLine()
			if m.isMultiline {
				if m.delim == '\n' {
					m.delim = ' '
				}
				return nil
			}
			if len(m.token) == 0 {
				return nil
			}

		case '(':
			if m.isMultiline {
				return fmt.Errorf(`line %d: multiple '('`, m.lineno)
			}
			m.isMultiline = true

		case ')':
			if !m.isMultiline {
				return fmt.Errorf(`line %d: unexpected ')'`, m.lineno)
			}
			m.isMultiline = false

		case '\n':
			m.lineno++

			if !m.isMultiline {
				// Delimiter '\n' mark the end of the
				// record for non-multiline, so we return
				// immediately here.
				return nil
			}

		case 0:
			if len(m.token) == 0 {
				return io.EOF
			}

			// Token is not empty, so we return true first.
			// The next call will return false with empty token.
			return nil
		}
		if len(m.token) != 0 {
			break
		}
		// Read the next token.
	}
	return nil
}

M lib/dns/zone_parser_test.go => lib/dns/zone_parser_test.go +40 -0
@@ 1,6 1,8 @@
package dns

import (
	"bytes"
	"fmt"
	"testing"

	"git.sr.ht/~shulhan/pakakeh.go/lib/test"


@@ 49,3 51,41 @@ func TestZoneParserDecodeString(t *testing.T) {
		test.Assert(t, string(c.in), c.exp, string(got))
	}
}

func TestZoneParser_next(t *testing.T) {
	var (
		logp = `TestZoneParser_next`

		tdata *test.Data
		err   error
	)

	tdata, err = test.LoadData(`testdata/zoneParser_next_test.txt`)
	if err != nil {
		t.Fatal(logp, err)
	}

	var listCase = []string{
		`comments`,
		`multiline`,
	}
	var (
		tag  string
		buf  bytes.Buffer
		zone Zone
		zp   zoneParser
	)
	for _, tag = range listCase {
		buf.Reset()
		zp.Reset(tdata.Input[tag], &zone)
		for {
			err = zp.next()
			if err != nil {
				t.Logf(`err:%s`, err)
				break
			}
			fmt.Fprintf(&buf, "%q %q\n", zp.token, zp.delim)
		}
		test.Assert(t, tag, string(tdata.Output[tag]), buf.String())
	}
}

M lib/dns/zone_test.go => lib/dns/zone_test.go +80 -0
@@ 84,6 84,86 @@ func TestParseZone(t *testing.T) {
	}
}

func TestParseZone_SVCB(t *testing.T) {
	var (
		logp = `TestParseZone_SVCB`

		tdata *test.Data
		err   error
	)

	tdata, err = test.LoadData(`testdata/ParseZone_SVCB_test.txt`)
	if err != nil {
		t.Fatal(logp, err)
	}

	var listCase = []string{
		`AliasMode`,
		`ServiceMode`,
		`ServiceMode:port`,
		`ServiceMode:keyGeneric667`,
		`ServiceMode:keyGenericQuoted`,
		`ServiceMode:TwoQuotedIpv6Hint`,
		`ServiceMode:Ipv6hintEmbedIpv4`,
		`ServiceMode:WithMandatoryKey`,
		`ServiceMode:AlpnWithEscapedComma`,
		`ServiceMode:AlpnWithEscapedBackslash`,
		`FailureMode:DuplicateKey`,
		`FailureMode:KeyMandatoryNoValue`,
		`FailureMode:KeyAlpnNoValue`,
		`FailureMode:KeyPortNoValue`,
		`FailureMode:KeyIpv4hintNoValue`,
		`FailureMode:KeyIpv6hintNoValue`,
		`FailureMode:KeyNodefaultalpnWithValue`,
		`FailureMode:MissingMandatoryKey`,
		`FailureMode:RecursiveMandatoryKey`,
		`FailureMode:DuplicateMandatoryKey`,
	}

	var (
		origin        = `example.com`
		ttl    uint32 = 60

		name   string
		stream []byte
		zone   *Zone
		out    bytes.Buffer

		tag string
		msg *Message
		x   int
	)

	for _, name = range listCase {
		stream = tdata.Input[name]
		if len(stream) == 0 {
			t.Fatalf(`%s: %s: empty input`, logp, name)
		}

		zone, err = ParseZone(stream, origin, ttl)
		if err != nil {
			tag = name + `:error`
			test.Assert(t, tag, string(tdata.Output[tag]), err.Error())
			continue
		}

		out.Reset()

		_, _ = zone.WriteTo(&out)
		stream = tdata.Output[name]
		test.Assert(t, name, string(stream), out.String())

		for x, msg = range zone.messages {
			out.Reset()
			libbytes.DumpPrettyTable(&out, msg.Question.String(), msg.packet)

			tag = fmt.Sprintf(`%s:message_%d.hex`, name, x)
			stream = tdata.Output[tag]
			test.Assert(t, tag, string(stream), out.String())
		}
	}
}

func TestZoneParseDirectiveOrigin(t *testing.T) {
	type testCase struct {
		desc   string