Repository: secdev/scapy

Issue:
Packet Send function doesn't respect routing for private subnets in multi-interface environments
#### Brief description
Packet Send function doesn't respect routing for private subnets in multi-interface environments.  
If there be multiple interfaces (physical/virtual) packets always are sent via default gateway (`conf.iface`) despite from routing results so a packet can be sent via incorrect interface.  

#### Environment

-   Scapy version: `2.4.3rc1.dev91`
-   Python version: `3.6.7`
-   Operating System: `Ubuntu 18.04.2 LTS`

#### How to reproduce
If there be 2 or more interfaces on the same system and someone want to send packet to a reachable subnet via routing table and not default gateway problem occures.
for example in my system I have a physical WLAN interface and I use tun device for VPN connection. so my routing table is this:
```bash
$ ip r
default dev tun0 scope link 
default via 192.168.1.1 dev wlp3s0 proto dhcp metric 600 
159.69.**.*** via 192.168.1.1 dev wlp3s0 src 192.168.1.100 
169.254.0.0/16 dev wlp3s0 scope link metric 1000 
192.168.1.0/24 dev wlp3s0 proto kernel scope link src 192.168.1.100 metric 600 
192.168.8.0/24 dev tun0 scope link 
``` 
and so scapy's route is the same: 
```python
>>> conf.route                                                                                                                                                                                             
Network        Netmask          Gateway      Iface   Output IP      Metric
0.0.0.0        0.0.0.0          0.0.0.0      tun0    192.168.8.202  0
127.0.0.0      255.0.0.0        0.0.0.0      lo      127.0.0.1      1
159.69.**.***  255.255.255.255  192.168.1.1  wlp3s0  192.168.1.100  0
169.254.0.0    255.255.0.0      0.0.0.0      wlp3s0  192.168.1.100  1000
192.168.1.0    255.255.255.0    0.0.0.0      wlp3s0  192.168.1.100  600
192.168.8.0    255.255.255.0    0.0.0.0      tun0    192.168.8.202  0
```
and scapy's default interface is `tun0` as it should be:
```python
>>> conf.iface                                                                                                                                                                                             
'tun0'
```
Now sending any packet to private networks which are reachable via routing table fails (e.g. 192.168.1.103): 
```python
>>> sr1(IP(dst="192.168.1.103")/ICMP())
```

#### Actual result

```python
>>> sr(IP(dst="192.168.1.103")/ICMP())                                                                                                                                                                       
Begin emission:
Finished sending 1 packets.
...........^C
Received 11 packets, got 0 answers, remaining 1 packets
(<Results: TCP:0 UDP:0 ICMP:0 Other:0>,
 <Unanswered: TCP:0 UDP:0 ICMP:1 Other:0>)
>>>  
```

#### Expected result
```python
>>> sr1(IP(dst="192.168.1.103")/ICMP(), iface='wlp3s0').show()                                                                                                                                             
Begin emission:
Finished sending 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
###[ IP ]### 
  version= 4
  ihl= 5
  tos= 0x0
  len= 28
  id= 9943
  flags= 
  frag= 0
  ttl= 64
  proto= icmp
  chksum= 0xcfee
  src= 192.168.1.103
  dst= 192.168.1.100
  \options\
###[ ICMP ]### 
     type= echo-reply
     code= 0
     chksum= 0xffff
     id= 0x0
     seq= 0x0
###[ Padding ]### 
        load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```
#### Preventing The Bug
Setting the `iface` parameter of `sr1` prevents the bug:
```python
>>> a = IP(dst="192.168.1.103")/ICMP()                                                                                                                                                                     
>>> sr1(a, iface=a.route()[0])                                                                                                                                                                                   
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x0 len=28 id=2178 flags= frag=0 ttl=64 proto=icmp chksum=0xee43 src=192.168.1.103 dst=192.168.1.100 |<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>
>>>  


```

#### A Quick Fix
It seems `L3PacketSocket` (`arch/linux.py`) doesn't respect to routing while it routes before sending packet.   

a quick fix would be: 

```patch
--- /usr/local/lib/python3.6/dist-packages/scapy/sendrecv.py    2019-04-05 03:47:15.816344394 +0430
+++ /tmp/sendrecv.py    2019-04-05 15:05:30.242518994 +0430
@@ -475,8 +475,14 @@
 @conf.commands.register
 def sr1(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs):
     """Send packets at layer 3 and return only the first answer"""
+    if iface is None:
+        iff, a, gw = x.route()
+        if iff is None:
+            iff = conf.iface
+    else:
+        iff = iface
     s = conf.L3socket(promisc=promisc, filter=filter,
-                      nofilter=nofilter, iface=iface)
+                      nofilter=nofilter, iface=iff)
     ans, _ = sndrcv(s, x, *args, **kargs)
     s.close()
     if len(ans) > 0:

```

But it seems the problem should be fixed in `L2Socket` class in `arch/linux.py`


Discussion:
@p-l- could you have a look, it believe that it is related to #1935
> @p-l- could you have a look, it believe that it is related to #1935

Hi, The MAC address problem was another problem existed for virtual devices which is fixed in the merge you mentioned.


I checked codes again today and found the problem is in promiscuous mode setup of `L2Socket` in `arch/linux.py`

In the following code:
```python
    def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None,
                 nofilter=0, monitor=None):
        self.iface = conf.iface if iface is None else iface
        self.type = type
        self.promisc = conf.sniff_promisc if promisc is None else promisc
        ...
        if self.promisc:
            set_promisc(self.ins, self.iface)
```
`L2Socket` class `__init__` method (`arch/linux.py:455`) sets the promisc to `conf.iface` without respecting routing table results
And routing table can say packet needs to go out via another iface later in `L3PacketSocket.send` method (`arch/linux.py:522`):
```python
    def send(self, x):
        iff, a, gw = x.route()
        if iff is None:
            iff = conf.iface
        sdto = (iff, self.type)
        ...
```

so reply will come back from the route calculated interface while L2Socket sniffs on default gateway which makes it unable to receive reply packet

I Think the best fix for this can be moving the entire routing process in L3Socket init function or reseting the promisc before send function  

Now I think my quick fix (_I updated my fix so it can still respect iface selection in sr1_) is the best choice until fixing L2Socket class itself 
Thanks for reporting this issue. I did not have time to have a close look at it earlier.

I think that the current behavior of Scapy sockets is correct, and allows flexible usages. The sockets should not care about routing but only send and receive on the specified interface.

Instead, as you are suggesting, commands dealing with sending layer 3 packets should try their best to benefit from Scapy routing table. I will provide a PR that implements a fix for such functions.



Resolving patch:
diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py
--- a/scapy/sendrecv.py
+++ b/scapy/sendrecv.py
@@ -462,9 +462,25 @@ def sr(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs):
     return result
 
 
+def _interface_selection(iface, packet):
+    """
+    Select the network interface according to the layer 3 destination
+    """
+
+    if iface is None:
+        try:
+            iff = packet.route()[0]
+        except AttributeError:
+            iff = None
+        return iff or conf.iface
+
+    return iface
+
+
 @conf.commands.register
 def sr1(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs):
     """Send packets at layer 3 and return only the first answer"""
+    iface = _interface_selection(iface, x)
     s = conf.L3socket(promisc=promisc, filter=filter,
                       nofilter=nofilter, iface=iface)
     ans, _ = sndrcv(s, x, *args, **kargs)


Test patch:
diff --git a/test/linux.uts b/test/linux.uts
--- a/test/linux.uts
+++ b/test/linux.uts
@@ -366,3 +366,15 @@ saved_conf_prog_tcpdump = conf.prog.tcpdump
 conf.prog.tcpdump = "does_not_exist"
 assert _check_tcpdump() == False
 conf.prog.tcpdump = saved_conf_prog_tcpdump
+
+= Test _interface_selection
+~ netaccess linux needs_root
+
+import os
+from scapy.sendrecv import _interface_selection
+assert _interface_selection(None, IP(dst="8.8.8.8")/UDP()) == conf.iface
+exit_status = os.system("ip link add name scapy0 type dummy")
+exit_status = os.system("ip addr add 192.0.2.1/24 dev scapy0")
+exit_status = os.system("ip link set scapy0 up")
+assert _interface_selection(None, IP(dst="192.0.2.42")/UDP()) == "scapy0"
+exit_status = os.system("ip link del name dev scapy0")