Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsing for SVCB and HTTPS #347

Merged
merged 2 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
# vendor/

# Python
__pycache__/
__pycache__/

go-dnscollector
214 changes: 214 additions & 0 deletions dnsutils/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
49 changes: 49 additions & 0 deletions dnsutils/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion doc/dnsparser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- [Client Subnet](https://www.rfc-editor.org/rfc/rfc7871.html)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading