diff options
| author | Shulhan <ms@kilabit.info> | 2024-03-25 13:42:20 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-03-26 23:07:02 +0700 |
| commit | 71eaafc5119b178be61abf6ae7b8a2fbcdfacc44 (patch) | |
| tree | 04d227d6f7d01bce8a082a98216a6f271145ef7d | |
| parent | aba79f63972515f3849f35641ef7de3f93f812e2 (diff) | |
| download | pakakeh.go-71eaafc5119b178be61abf6ae7b8a2fbcdfacc44.tar.xz | |
lib/dns: implements RFC 9460 for SVCB RR and HTTPS RR
| -rw-r--r-- | lib/dns/dns.go | 2 | ||||
| -rw-r--r-- | lib/dns/message.go | 75 | ||||
| -rw-r--r-- | lib/dns/message_test.go | 50 | ||||
| -rw-r--r-- | lib/dns/rdata_https.go | 48 | ||||
| -rw-r--r-- | lib/dns/rdata_svcb.go | 995 | ||||
| -rw-r--r-- | lib/dns/record_type.go | 14 | ||||
| -rw-r--r-- | lib/dns/resource_record.go | 48 | ||||
| -rw-r--r-- | lib/dns/server.go | 3 | ||||
| -rw-r--r-- | lib/dns/testdata/ParseZone_SVCB_test.txt | 301 | ||||
| -rw-r--r-- | lib/dns/testdata/message/UnpackMessage_SVCB_test.txt | 441 | ||||
| -rw-r--r-- | lib/dns/testdata/zoneParser_next_test.txt | 35 | ||||
| -rw-r--r-- | lib/dns/zone.go | 29 | ||||
| -rw-r--r-- | lib/dns/zone_parser.go | 179 | ||||
| -rw-r--r-- | lib/dns/zone_parser_test.go | 40 | ||||
| -rw-r--r-- | lib/dns/zone_test.go | 80 |
15 files changed, 2334 insertions, 6 deletions
diff --git a/lib/dns/dns.go b/lib/dns/dns.go index 219c5414..0fa8417a 100644 --- a/lib/dns/dns.go +++ b/lib/dns/dns.go @@ -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 ( diff --git a/lib/dns/message.go b/lib/dns/message.go index 318d72fb..d4861496 100644 --- a/lib/dns/message.go +++ b/lib/dns/message.go @@ -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() diff --git a/lib/dns/message_test.go b/lib/dns/message_test.go index 177ca9eb..d97a82d9 100644 --- a/lib/dns/message_test.go +++ b/lib/dns/message_test.go @@ -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)) + } +} diff --git a/lib/dns/rdata_https.go b/lib/dns/rdata_https.go new file mode 100644 index 00000000..dae748e4 --- /dev/null +++ b/lib/dns/rdata_https.go @@ -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 +} diff --git a/lib/dns/rdata_svcb.go b/lib/dns/rdata_svcb.go new file mode 100644 index 00000000..e3ac6d6b --- /dev/null +++ b/lib/dns/rdata_svcb.go @@ -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) +} diff --git a/lib/dns/record_type.go b/lib/dns/record_type.go index e2345bf7..569b0b33 100644 --- a/lib/dns/record_type.go +++ b/lib/dns/record_type.go @@ -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", diff --git a/lib/dns/resource_record.go b/lib/dns/resource_record.go index bcbd95c7..f32ab334 100644 --- a/lib/dns/resource_record.go +++ b/lib/dns/resource_record.go @@ -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" diff --git a/lib/dns/server.go b/lib/dns/server.go index 1f77f23a..e9e1b49a 100644 --- a/lib/dns/server.go +++ b/lib/dns/server.go @@ -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 } diff --git a/lib/dns/testdata/ParseZone_SVCB_test.txt b/lib/dns/testdata/ParseZone_SVCB_test.txt new file mode 100644 index 00000000..270f58e3 --- /dev/null +++ b/lib/dns/testdata/ParseZone_SVCB_test.txt @@ -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" diff --git a/lib/dns/testdata/message/UnpackMessage_SVCB_test.txt b/lib/dns/testdata/message/UnpackMessage_SVCB_test.txt new file mode 100644 index 00000000..e3237f7a --- /dev/null +++ b/lib/dns/testdata/message/UnpackMessage_SVCB_test.txt @@ -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 + } +} diff --git a/lib/dns/testdata/zoneParser_next_test.txt b/lib/dns/testdata/zoneParser_next_test.txt new file mode 100644 index 00000000..9c1b02bb --- /dev/null +++ b/lib/dns/testdata/zoneParser_next_test.txt @@ -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" ' ' diff --git a/lib/dns/zone.go b/lib/dns/zone.go index f2689447..4fbc9700 100644 --- a/lib/dns/zone.go +++ b/lib/dns/zone.go @@ -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 diff --git a/lib/dns/zone_parser.go b/lib/dns/zone_parser.go index e648e43b..dabf392a 100644 --- a/lib/dns/zone_parser.go +++ b/lib/dns/zone_parser.go @@ -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 +} diff --git a/lib/dns/zone_parser_test.go b/lib/dns/zone_parser_test.go index 57b08c9d..ff51fcc2 100644 --- a/lib/dns/zone_parser_test.go +++ b/lib/dns/zone_parser_test.go @@ -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()) + } +} diff --git a/lib/dns/zone_test.go b/lib/dns/zone_test.go index 8d9d6585..d8b80814 100644 --- a/lib/dns/zone_test.go +++ b/lib/dns/zone_test.go @@ -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 |
