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