Repository: secdev/scapy

Issue:
Wrong UDP checksum when using IPv4 Source Routing options
#### Brief description

The UDP checksum is computed on a pseudo-header that does not appear to take IPv4 header options into account. I think this is a bug in `inet.in4_chksum()`, because the IP layer checksums are correct.

#### Environment

-   Scapy version: current master HEAD (e412d2f2877dbebfb8d88d90626406960dc7a559)
-   Python version: 3.7.3
-   Operating System: reproduced on Linux and macOS

#### How to reproduce

```python
# LSRR routers=[192.168.1.1]
opt = IPOption(copy_flag=1, optclass=0, option=3, length=7, value=b'\x04\xC0\xA8\x01\x01')
ip_1 = IP(dst="127.0.0.1") / UDP(sport=0, dport=5555) / b"payload"
ip_2 = IP(dst="127.0.0.1", options=opt) / UDP(sport=0, dport=5555) / b"payload"
```

Then `show2()` the IP packets. The UDP checksum on `ip_2` is computed incorrectly -- specifically, it is the same as for `ip_1`. 

#### Actual result

```python
>>> ip_1.show2()
###[ IP ]### 
  version   = 4
  ihl       = 5
  tos       = 0x0
  len       = 35
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = udp
  chksum    = 0x7cc7
  src       = 127.0.0.1
  dst       = 127.0.0.1
  \options   \
###[ UDP ]### 
     sport     = 0
     dport     = personal_agent
     len       = 15
     chksum    = 0x2eeb
###[ Raw ]### 
        load      = 'payload'
```

```python
>>> ip_2.show2()
###[ IP ]### 
  version   = 4
  ihl       = 7
  tos       = 0x0
  len       = 43
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = udp
  chksum    = 0x49f6
  src       = 127.0.0.1
  dst       = 127.0.0.1
  \options   \
   |###[ IP Option Loose Source and Record Route ]### 
   |  copy_flag = 1
   |  optclass  = control
   |  option    = loose_source_route
   |  length    = 7
   |  pointer   = 4
   |  routers   = [192.168.1.1]
   |###[ IP Option End of Options List ]### 
   |  copy_flag = 0
   |  optclass  = control
   |  option    = end_of_list
###[ UDP ]### 
     sport     = 0
     dport     = personal_agent
     len       = 15
     chksum    = 0x2eeb
###[ Raw ]### 
        load      = 'payload'
```

#### Expected result

The UDP checksums for the two packets should be different.


Discussion:
Could you share a reference for this behavior? RFC768 seems to confirm our implementation. Thanks.
Other implementations appear to use a different value for the `dst_address` field of the UDP pseudo header when an IP packet arrives with a source-routing option. At least, `tcpdump` reports a checksum mismatch and the Linux kernel drops incoming UDP packets before they reach a bound socket in userspace.

tcpdump output of a successful and rejected packet:
```
05:43:00.560565 00:50:56:36:1e:3d (oui Unknown) > 00:50:56:3f:94:51 (oui Unknown), ethertype IPv4 (0x0800), length 60: (tos 0x0, ttl 64, id 1, offset 0, flags [none], proto UDP (17), length 35)
    10.1.1.100.0 > 10.1.1.101.rplay: [udp sum ok] UDP, length 7
	0x0000:  0050 563f 9451 0050 5636 1e3d 0800 4500
	0x0010:  0023 0001 0000 4011 63ff 0a01 0164 0a01
	0x0020:  0165 0000 15b3 000f 1623 7061 796c 6f61
	0x0030:  6400 0000 0000 0000 0000 0000

05:43:04.386947 00:50:56:36:1e:3d (oui Unknown) > 00:50:56:3f:94:51 (oui Unknown), ethertype IPv4 (0x0800), length 60: (tos 0x0, ttl 64, id 1, offset 0, flags [none], proto UDP (17), length 43, options (LSRR 192.168.1.1,EOL))
    10.1.1.100.0 > 10.1.1.101.rplay: [bad udp cksum 0x1623 -> 0x5fdf!] UDP, length 7
	0x0000:  0050 563f 9451 0050 5636 1e3d 0800 4700
	0x0010:  002b 0001 0000 4011 312e 0a01 0164 0a01
	0x0020:  0165 8307 04c0 a801 0100 0000 15b3 000f
	0x0030:  1623 7061 796c 6f61 6400 0000
```

In `scapy`, I can reproduce the 0x1623 checksum by using the IP header's destination in the UDP pseudo-header:
```
>>> hex(checksum(b"".join([
  b"\x0a\x01\x01\x64",                 # src_address 10.1.1.100
  b"\x0a\x01\x01\x65",                 # dst_address 10.1.1.101
  b"\x00\x11",                         # (zeros, protocol)
  b"\x00\x0f",                         # length
  b"\x00\x00",                         # src_port
  b"\x15\xb3",                         # dst_port
  b"\x00\x0f",                         # length
  b"\x00\x00",                         # checksum placeholder
  b"\x70\x61\x79\x6c\x6f\x61\x64\x00", # data
])))
'0x1623'
```

and can reproduce the 0x5fdf value expected by `tcpdump` by changing dst_address:
```
>>> hex(checksum(b"".join([
  b"\x0a\x01\x01\x64", 
  b"\xc0\xa8\x01\x01",    # dst_address 192.168.1.1
  b"\x00\x11",
  b"\x00\x0f",
  b"\x00\x00",
  b"\x15\xb3",
  b"\x00\x0f",
  b"\x00\x00",
  b"\x70\x61\x79\x6c\x6f\x61\x64\x00",
])))
'0x5fdf'
```
Thanks a lot for the detailed follow up! Everything makes more sense now. The issue seems valid and is not caused by a random IPv4 option but by a Source Route Option.

I did not find any reference that explain how to compute the L4 checksum when a Source Route Option is present but the [Linux Kernel source code was helpful](https://elixir.bootlin.com/linux/latest/source/net/ipv4/udp.c#L1147): the last IPv4 address must be used as it is the final destination. This is similar to the computation performed with IPv6 at https://github.com/secdev/scapy/blob/master/scapy/layers/inet6.py#L591

I will provide a fix.

Resolving patch:
diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py
--- a/scapy/layers/inet.py
+++ b/scapy/layers/inet.py
@@ -653,6 +653,20 @@ def in4_chksum(proto, u, p):
         ln = max(u.len - 4 * ihl, 0)
     else:
         ln = len(p)
+
+    # Filter out IPOption_LSRR and IPOption_SSRR
+    sr_options = [opt for opt in u.options if isinstance(opt, IPOption_LSRR) or
+                  isinstance(opt, IPOption_SSRR)]
+    len_sr_options = len(sr_options)
+    if len_sr_options == 1 and len(sr_options[0].routers):
+        # The checksum must be computed using the final
+        # destination address
+        u.dst = sr_options[0].routers[-1]
+    elif len_sr_options > 1:
+        message = "Found %d Source Routing Options! "
+        message += "Falling back to IP.dst for checksum computation."
+        warning(message, len_sr_options)
+
     psdhdr = struct.pack("!4s4sHH",
                          inet_pton(socket.AF_INET, u.src),
                          inet_pton(socket.AF_INET, u.dst),


Test patch:
diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts
--- a/test/scapy/layers/inet.uts
+++ b/test/scapy/layers/inet.uts
@@ -56,7 +56,7 @@ p = IP(src="9.10.11.12", dst="13.14.15.16", options=[IPOption_NOP(),IPOption_LSR
 p
 r = raw(p)
 r
-assert(r == b'K\x00\x00@\x00\x01\x00\x00@\x06\xf3\x83\t\n\x0b\x0c\r\x0e\x0f\x10\x01\x83\x0b\x04\x01\x02\x03\x04\x05\x06\x07\x08\x82\x0b\x00\x00\x00\x00\x00\x00XYZ\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00_K\x00\x00')
+assert(r == b'K\x00\x00@\x00\x01\x00\x00@\x06\xf3\x83\t\n\x0b\x0c\r\x0e\x0f\x10\x01\x83\x0b\x04\x01\x02\x03\x04\x05\x06\x07\x08\x82\x0b\x00\x00\x00\x00\x00\x00XYZ\x00\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00o[\x00\x00')
 q = IP(r)
 q
 assert(q[IPOption_LSRR].get_current_router() == "1.2.3.4")
@@ -584,4 +584,11 @@ def test_IPID_count():
 
 test_IPID_count()
 
+= IPv4 - Checksum computation with source routing
 
+no_sr = IP(raw(IP(dst="8.8.8.8")/UDP()/DNS()))
+sr = IP(raw(IP(options=[IPOption_SSRR(routers=["1.1.1.1", "8.8.8.8"])])/UDP()/DNS()))
+assert no_sr[UDP].chksum == sr[UDP].chksum
+
+sr = IP(raw(IP(options=[IPOption_LSRR(routers=["1.1.1.1"]), IPOption_SSRR(routers=["8.8.8.8"])])/UDP()/DNS()))
+assert no_sr[UDP].chksum != sr[UDP].chksum