Repository: secdev/scapy

Issue:
DNSRR parser don't set rdlen
<!--
Things to consider

1.  Please check that you are using the latest scapy version, e.g. installed via:

    `pip install --upgrade git+git://github.com/secdev/scapy`

2.  If you are here to ask a question - please check previous issues and online resources, and consider using gitter instead: <https://gitter.im/secdev/scapy>

3.  Please understand that **this is not a forum** but an issue tracker. The following article explains why you should limit questions asked on Github issues: <https://medium.com/@methane/why-you-must-not-ask-questions-on-github-issues-51d741d83fde>
-->

#### Brief description

<!-- describe the main issue in one sentence -->
<!-- if possible, describe what components / protocols could be affected by the issue (e.g. wrpcap() + IPv6, it is likely this also affects XXX) -->
Parsing a DNS Packet don't set rdlen

#### Environment

-   Scapy version: `2da3800b87702178a0f60598aebdd7335ce5603d`
-   Python version: `3.8.5`
-   Operating System: `Ubuntu 20.04.2 LTS`

<!-- if needed - further information to get a picture of your setup (e.g. a sketch of your network setup) -->

#### How to reproduce

<!-- step-by-step explanation or a short script, may reference section 'Related resources' -->

```
p = DNS(DNS(qd=[DNSQR()],an=[DNSRR()]).build())
print(p.an[0].rdlen)
```

#### Actual result

```
None
```

<!-- dump results that outline the issue, please format your code -->

#### Expected result

```
4
```

<!-- describe the expected result and outline the difference to the actual one, could also be a screen shot (e.g. wireshark) -->

#### Related resources

<!-- traces / sample pcaps (stripped to the relevant frames), related standards, RFCs or other resources -->



Discussion:
It is a expected side effect of the way in which Scapy manages the DNS compression.

DNS message are decompressed, but Scapy does not know how to compress back. Setting `rdlen` to None ensure that Scapy will recompute the length if you modify the packet (i.e. calling `raw()` on an unmodified packet will return the compressed version that is stored in an internal cache).

I had a look at the code and there is indeed a bug: `rdlen` is always set to None. I will submit a PR that keep the value is no decompression was involved, and if the RR type does not deal with DNS names.
This wouldn't solve my problem. I want to extract the unmodified rdata and rdlength of a given DNS Packet. Compressed rdata is the relevant part.

Resolving patch:
diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py
--- a/scapy/layers/dns.py
+++ b/scapy/layers/dns.py
@@ -128,7 +128,7 @@ def dns_get_str(s, pointer=0, pkt=None, _fullpacket=False):
     if bytes_left is None:
         bytes_left = s[pointer:]
     # name, end_index, remaining
-    return name, pointer, bytes_left
+    return name, pointer, bytes_left, len(processed_pointers) != 0
 
 
 def dns_encode(x, check_built=False):
@@ -161,7 +161,7 @@ def DNSgetstr(*args, **kwargs):
         "DNSgetstr is deprecated. Use dns_get_str instead.",
         DeprecationWarning
     )
-    return dns_get_str(*args, **kwargs)
+    return dns_get_str(*args, **kwargs)[:-1]
 
 
 def dns_compress(pkt):
@@ -265,6 +265,8 @@ class DNSStrField(StrLenField):
     It will also handle DNS decompression.
     (may be StrLenField if a length_from is passed),
     """
+    __slots__ = ["compressed"]
+
     def h2i(self, pkt, x):
         if not x:
             return b"."
@@ -281,7 +283,7 @@ def getfield(self, pkt, s):
         if self.length_from:
             remain, s = super(DNSStrField, self).getfield(pkt, s)
         # Decode the compressed DNS message
-        decoded, _, left = dns_get_str(s, 0, pkt)
+        decoded, _, left, self.compressed = dns_get_str(s, 0, pkt)
         # returns (remaining, decoded)
         return left + remain, decoded
 
@@ -337,8 +339,15 @@ def decodeRR(self, name, s, p):
         p += 10
         cls = DNSRR_DISPATCHER.get(typ, DNSRR)
         rr = cls(b"\x00" + ret + s[p:p + rdlen], _orig_s=s, _orig_p=p)
-        # Will have changed because of decompression
-        rr.rdlen = None
+
+        # Reset rdlen if DNS compression was used
+        for fname in rr.fieldtype.keys():
+            rdata_obj = rr.fieldtype[fname]
+            if fname == "rdata" and isinstance(rdata_obj, MultipleTypeField):
+                rdata_obj = rdata_obj._find_fld_pkt_val(rr, rr.type)[0]
+            if isinstance(rdata_obj, DNSStrField) and rdata_obj.compressed:
+                del rr.rdlen
+                break
         rr.rrname = name
 
         p += rdlen
@@ -356,7 +365,7 @@ def getfield(self, pkt, s):
             return s, b""
         while c:
             c -= 1
-            name, p, _ = dns_get_str(s, p, _fullpacket=True)
+            name, p, _, _ = dns_get_str(s, p, _fullpacket=True)
             rr, p = self.decodeRR(name, s, p)
             if ret is None:
                 ret = rr


Test patch:
diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts
--- a/test/scapy/layers/dns.uts
+++ b/test/scapy/layers/dns.uts
@@ -222,3 +222,15 @@ assert dns_encode(dns_encode(b"*")) == b'\x03\x01*\x00'
 = DNS - simple request
 
 assert raw(DNS()) == b'\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01'
+
+= DNS - preserve rdlen when rdata is not a compressed DNS string
+
+# RR type A
+dnsrr1 = Raw(b'\x01a\x01b\x01c\x00\x00\x01\x10\x00\x00\x00\x00\x01\x00\x04\x01\x02\x03\x04')
+# RR type NS & plain rdata
+dnsrr2 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x06\x01x\x01y\x01z')
+# RR type NS & compressed rdata
+dnsrr3 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x07\x04test\xc0\x0e')
+
+d = DNS(raw(DNS(ancount=3, an=dnsrr1/dnsrr2/dnsrr3)))
+assert d.an[0].rdlen == 4 and d.an[1].rdlen == 6 and d.an[2].rdlen is None