diff --git a/dnsutils/dns.go b/dnsutils/dns.go index 91eb3be7..d0832d53 100644 --- a/dnsutils/dns.go +++ b/dnsutils/dns.go @@ -557,6 +557,8 @@ func ParseRdata(rdatatype string, rdata []byte, payload []byte, rdata_offset int ret, err = ParsePTR(rdata_offset, payload) case "SOA": ret, err = ParseSOA(rdata_offset, payload) + case "HTTPS", "SVCB": + ret, err = ParseSVCB(rdata) default: ret = "-" err = nil @@ -771,3 +773,215 @@ func ParsePTR(rdata_offset int, payload []byte) (string, error) { } return ptr, err } + +/* +SVCB ++--+--+ +| PRIO| ++--+--+--+ +/ Target / ++--+--+--+ +/ Params / ++--+--+--+ +*/ +func ParseSVCB(rdata []byte) (string, error) { + // priority, root label, no Params + if len(rdata) < 3 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + svcPriority := binary.BigEndian.Uint16(rdata[0:2]) + targetName, offset, err := ParseLabels(2, rdata) + if err != nil { + return "", err + } + if targetName == "" { + targetName = "." + } + ret := fmt.Sprintf("%d %s", svcPriority, targetName) + if len(rdata) == offset { + return ret, nil + } + var svcParam []string + for offset < len(rdata) { + if len(rdata) < offset+4 { + // SVCParam is at least 4 bytes (Key and Length) + return "", ErrDecodeDnsAnswerRdataTooShort + } + paramKey := binary.BigEndian.Uint16(rdata[offset : offset+2]) + offset += 2 + paramLen := binary.BigEndian.Uint16(rdata[offset : offset+2]) + offset += 2 + if len(rdata) < offset+int(paramLen) { + return "", ErrDecodeDnsAnswerRdataTooShort + } + param, err := ParseSVCParam(paramKey, rdata[offset:offset+int(paramLen)]) + if err != nil { + return "", err + } + // Yes, this is ugly but probably good enough + if strings.Contains(param, `\`) { + param = fmt.Sprintf(`"%s"`, param) + } + svcParam = append(svcParam, fmt.Sprintf("%s=%s", SVCParamKeyToString(paramKey), param)) + offset += int(paramLen) + } + return fmt.Sprintf("%s %s", ret, strings.Join(svcParam, " ")), nil +} + +func SVCParamKeyToString(svcParamKey uint16) string { + switch svcParamKey { + case 0: + return "mandatory" + case 1: + return "alpn" + case 2: + return "no-default-alpn" + case 3: + return "port" + case 4: + return "ipv4hint" + case 5: + return "ech" + case 6: + return "ipv6hint" + } + return fmt.Sprintf("key%d", svcParamKey) +} + +func ParseSVCParam(svcParamKey uint16, paramData []byte) (string, error) { + switch svcParamKey { + case 0: + // mandatory + if len(paramData)%2 != 0 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + var mandatory []string + for i := 0; i < len(paramData); i += 2 { + paramKey := binary.BigEndian.Uint16(paramData[i : i+2]) + mandatory = append(mandatory, SVCParamKeyToString(paramKey)) + } + return strings.Join(mandatory, ","), nil + case 1: + // alpn + if len(paramData) == 0 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + var alpns []string + offset := 0 + for { + length := int(paramData[offset]) + offset++ + if len(paramData) < offset+length { + return "", ErrDecodeDnsAnswerRdataTooShort + } + alpns = append(alpns, svcbParamToStr(paramData[offset:offset+length])) + offset += length + if offset == len(paramData) { + break + } + } + return strings.Join(alpns, ","), nil + case 2: + // no-default-alpn + if len(paramData) != 0 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + return "", nil + case 3: + //port + if len(paramData) != 2 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + port := binary.BigEndian.Uint16(paramData) + return fmt.Sprintf("%d", port), nil + case 4: + // ipv4hint + if len(paramData)%4 != 0 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + var addresses []string + for offset := 0; offset < len(paramData); offset += 4 { + address, err := ParseA(paramData[offset : offset+4]) + if err != nil { + return "", nil + } + addresses = append(addresses, address) + } + return strings.Join(addresses, ","), nil + case 5: + // ecs, undefined decoding as of draft-ietf-dnsop-svcb-https-12 + return svcbParamToStr(paramData), nil + case 6: + // ipv6hint + if len(paramData)%16 != 0 { + return "", ErrDecodeDnsAnswerRdataTooShort + } + var addresses []string + for offset := 0; offset < len(paramData); offset += 16 { + address, err := ParseAAAA(paramData[offset : offset+16]) + if err != nil { + return "", nil + } + addresses = append(addresses, address) + } + return strings.Join(addresses, ","), nil + default: + return svcbParamToStr(paramData), nil + } +} + +// These functions and consts have been taken from miekg/dns +const ( + escapedByteSmall = "" + + `\000\001\002\003\004\005\006\007\008\009` + + `\010\011\012\013\014\015\016\017\018\019` + + `\020\021\022\023\024\025\026\027\028\029` + + `\030\031` + escapedByteLarge = `\127\128\129` + + `\130\131\132\133\134\135\136\137\138\139` + + `\140\141\142\143\144\145\146\147\148\149` + + `\150\151\152\153\154\155\156\157\158\159` + + `\160\161\162\163\164\165\166\167\168\169` + + `\170\171\172\173\174\175\176\177\178\179` + + `\180\181\182\183\184\185\186\187\188\189` + + `\190\191\192\193\194\195\196\197\198\199` + + `\200\201\202\203\204\205\206\207\208\209` + + `\210\211\212\213\214\215\216\217\218\219` + + `\220\221\222\223\224\225\226\227\228\229` + + `\230\231\232\233\234\235\236\237\238\239` + + `\240\241\242\243\244\245\246\247\248\249` + + `\250\251\252\253\254\255` +) + +// escapeByte returns the \DDD escaping of b which must +// satisfy b < ' ' || b > '~'. +func escapeByte(b byte) string { + if b < ' ' { + return escapedByteSmall[b*4 : b*4+4] + } + + b -= '~' + 1 + // The cast here is needed as b*4 may overflow byte. + return escapedByteLarge[int(b)*4 : int(b)*4+4] +} + +func svcbParamToStr(s []byte) string { + var str strings.Builder + str.Grow(4 * len(s)) + for _, e := range s { + if ' ' <= e && e <= '~' { + switch e { + case '"', ';', ' ', '\\': + str.WriteByte('\\') + str.WriteByte(e) + default: + str.WriteByte(e) + } + } else { + str.WriteString(escapeByte(e)) + } + } + return str.String() +} + +// END These functions and consts have been taken from miekg/dns diff --git a/dnsutils/dns_test.go b/dnsutils/dns_test.go index 9e031f1a..5524df21 100644 --- a/dnsutils/dns_test.go +++ b/dnsutils/dns_test.go @@ -170,6 +170,55 @@ func TestDecodeAnswer(t *testing.T) { } } +func TestDecodeRdataSVCB_alias(t *testing.T) { + fqdn := TEST_QNAME + + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeSVCB) + + // draft-ietf-dnsop-svcb-https-12 Appendix D.1 + rdata := "0 foo.example.com" + rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + + payload, _ := dm.Pack() + + _, _, offset_rr, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offset_rr, payload) + + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SOA, want %s, got: %s", rdata, answer[0].Rdata) + } +} + +func TestDecodeRdataSVCB_params(t *testing.T) { + fqdn := TEST_QNAME + + vectors := []string{ + "0 foo.example.com", // draft-ietf-dnsop-svcb-https-12 Appendix D.1 + "1 .", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 3 + "16 foo.example.com port=53", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 4 + "1 foo.example.com key667=hello", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 5 + `1 foo.example.com key667="hello\210qoo"`, // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 6 + "1 foo.example.com ipv6hint=2001:db8::1,2001:db8::53:1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 7, modified (single line) + "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1", // draft-ietf-dnsop-svcb-https-12 Appendix D.2, figure 9, modified (sorted) + "16 foo.example.org mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1,192.0.2.2", + } + + for _, rdata := range vectors { + dm := new(dns.Msg) + dm.SetQuestion(fqdn, dns.TypeSVCB) + rr1, _ := dns.NewRR(fmt.Sprintf("%s SVCB %s", fqdn, rdata)) + dm.Answer = append(dm.Answer, rr1) + payload, _ := dm.Pack() + _, _, offset_rr, _ := DecodeQuestion(1, payload) + answer, _, _ := DecodeAnswer(len(dm.Answer), offset_rr, payload) + if answer[0].Rdata != rdata { + t.Errorf("invalid decode for rdata SVCB, want %s, got: %s", rdata, answer[0].Rdata) + } + } +} + func TestDecodeAnswer_QnameMinimized(t *testing.T) { payload := []byte{0x8d, 0xda, 0x81, 0x80, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x05, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x09, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, diff --git a/doc/dnsparser.md b/doc/dnsparser.md index 65cface7..698e2e05 100644 --- a/doc/dnsparser.md +++ b/doc/dnsparser.md @@ -14,8 +14,10 @@ The following Rdatatypes will be decoded, otherwise the `-` value will be used: - TXT - PTR - SOA +- SVCB +- HTTPS Extended DNS is also supported. The following options are decoded: - [Extented DNS Errors](https://www.rfc-editor.org/rfc/rfc8914.html) -- [Client Subnet](https://www.rfc-editor.org/rfc/rfc7871.html) \ No newline at end of file +- [Client Subnet](https://www.rfc-editor.org/rfc/rfc7871.html) diff --git a/go.sum b/go.sum index 9fba1dc6..f5635333 100644 --- a/go.sum +++ b/go.sum @@ -435,8 +435,6 @@ github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgz github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dmachard/go-dnstap-protobuf v0.5.0 h1:JGd96UkFPmztsaOoHxf8cRD4X4bWoN/HKLaMFnjDhys= github.com/dmachard/go-dnstap-protobuf v0.5.0/go.mod h1:l4Qme7Kg8asuB8WwL+egMmXYtBSjN7tqxymWkeyTXqw= -github.com/dmachard/go-framestream v0.3.0 h1:rwiNAvpeGwrXZUKWM/AcmCb/prYjE8J5DOGk+Qbuwpo= -github.com/dmachard/go-framestream v0.3.0/go.mod h1:f0LF2Npbe4plNgzVJX1rUfoIUjTWZIpLLyhuG04RTo4= github.com/dmachard/go-framestream v0.6.0 h1:H2DtbkXNIpM/PSuMN/DA1EDGGO5NhN/OBB2K9SkbA8c= github.com/dmachard/go-framestream v0.6.0/go.mod h1:f0LF2Npbe4plNgzVJX1rUfoIUjTWZIpLLyhuG04RTo4= github.com/dmachard/go-logger v0.3.0 h1:Q7RnOLFCU9V5RSiuFs5cEwsEXTQ4HbvFDjF2H5GPNuQ=