Repository: secdev/scapy

Issue:
Using `sendp` inside `bridge_and_sniff` stops bridging
```python
def mitm(pkt):
  if one_moment_in_time:
    sendp(Ether()/IP(dst="1.2.3.4",ttl=(1,4)), iface="eth2")
  return pkt

bridge_and_sniff("eth2", "eth3", xfrm12=mitm, xfrm21=mitm, prn=lambda x:x.summary())
```

I have been using `bridge_and_sniff` as shown above and noticed that – in this case – frames were no longer forwarded from `eth2` to `eth3` (they still were from `eth3` to `eth2`, though) after `sendp` was used.

*Edit:* Just in case it matters, this was done on a 32 bit Linux using two Ethernet interfaces

I assume this is a bug, or maybe I am doing it wrong?


Discussion:

Resolving patch:
diff --git a/scapy/arch/linux.py b/scapy/arch/linux.py
--- a/scapy/arch/linux.py
+++ b/scapy/arch/linux.py
@@ -20,9 +20,10 @@
 import sys
 import time
 
+import subprocess
 
 from scapy.compat import *
-from scapy.consts import LOOPBACK_NAME, IS_64BITS
+from scapy.consts import LOOPBACK_NAME, IS_64BITS, LINUX
 import scapy.utils
 import scapy.utils6
 from scapy.packet import Packet, Padding
@@ -640,3 +641,65 @@ def send(self, x):
 conf.L3socket = L3PacketSocket
 conf.L2socket = L2Socket
 conf.L2listen = L2ListenSocket
+
+
+class ScapyInvalidPlatformException(Scapy_Exception):
+    pass
+
+
+class VEthPair(object):
+    """
+    encapsulates a virtual Ethernet interface pair
+    """
+
+    def __init__(self, iface_name, peer_name):
+
+        if not LINUX:
+            # ToDo: do we need a kernel version check here?
+            raise ScapyInvalidPlatformException('virtual Ethernet interface pair only available on Linux')
+
+        self.ifaces = [iface_name, peer_name]
+
+    def iface(self):
+        return self.ifaces[0]
+
+    def peer(self):
+        return self.ifaces[1]
+
+    def setup(self):
+        """
+        create veth pair links
+        :raises subprocess.CalledProcessError if operation fails
+        """
+        subprocess.check_call(['ip', 'link', 'add', self.ifaces[0], 'type', 'veth', 'peer', 'name', self.ifaces[1]])
+
+    def destroy(self):
+        """
+        remove veth pair links
+        :raises subprocess.CalledProcessError if operation failes
+        """
+        subprocess.check_call(['ip', 'link', 'del', self.ifaces[0]])
+
+    def up(self):
+        """
+        set veth pair links up
+        :raises subprocess.CalledProcessError if operation failes
+        """
+        for idx in [0, 1]:
+            subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "up"])
+
+    def down(self):
+        """
+        set veth pair links down
+        :raises subprocess.CalledProcessError if operation failes
+        """
+        for idx in [0, 1]:
+            subprocess.check_call(["ip", "link", "set", self.ifaces[idx], "down"])
+
+    def __enter__(self):
+        self.setup()
+        self.up()
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.destroy()
diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py
--- a/scapy/sendrecv.py
+++ b/scapy/sendrecv.py
@@ -640,7 +640,7 @@ def srp1flood(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kar
 @conf.commands.register
 def sniff(count=0, store=True, offline=None, prn=None, lfilter=None,
           L2socket=None, timeout=None, opened_socket=None,
-          stop_filter=None, iface=None, *arg, **karg):
+          stop_filter=None, iface=None, started_callback=None, *arg, **karg):
     """
     Sniff packets and return a list of packets.
 
@@ -666,6 +666,8 @@ def sniff(count=0, store=True, offline=None, prn=None, lfilter=None,
         iface: interface or list of interfaces (default: None for sniffing
                on all interfaces).
         monitor: use monitor mode. May not be available on all OS
+        started_callback: called as soon as the sniffer starts sniffing
+                          (default: None).
 
     The iface, offline and opened_socket parameters can be either an
     element, a list of elements, or a dict object mapping an element to a
@@ -755,6 +757,8 @@ def _select(sockets):
                     return []
                 raise
     try:
+        if started_callback:
+            started_callback()
         while sniff_sockets:
             if timeout is not None:
                 remain = stoptime - time.time()
@@ -767,6 +771,11 @@ def _select(sockets):
                 except read_allowed_exceptions:
                     continue
                 if p is None:
+                    try:
+                        if s.promisc:
+                            continue
+                    except AttributeError:
+                        pass
                     del sniff_sockets[s]
                     break
                 if lfilter and not lfilter(p):


Test patch:
diff --git a/test/linux.uts b/test/linux.uts
--- a/test/linux.uts
+++ b/test/linux.uts
@@ -193,3 +193,123 @@ with patch("scapy.layers.l2.get_if_hwaddr") as mgih:
 
 conf.route6.routes = bck_conf_route6_routes
 conf.iface = bck_conf_iface
+
+= veth interface error handling
+~ linux needs_root veth
+
+try:
+    veth = VEthPair('this_IF_name_is_to_long_and_will_cause_an_error', 'veth_scapy_1')
+    veth.setup()
+    assert False
+except subprocess.CalledProcessError:
+    pass
+except Exception:
+    assert False
+
+= veth interface usage - ctx manager
+~ linux needs_root veth
+
+from threading import Condition
+
+cond_started = Condition()
+
+def _sniffer_started():
+
+    global cond_started
+    cond_started.acquire()
+    cond_started.notify()
+    cond_started.release()
+
+cond_started.acquire()
+
+try:
+    with VEthPair('veth_scapy_0', 'veth_scapy_1') as veth:
+        if_list = get_if_list()
+        assert ('veth_scapy_0' in if_list)
+        assert ('veth_scapy_1' in if_list)
+        frm_count = 0
+        def _sniffer():
+            sniffed = sniff(iface='veth_scapy_1',
+                            store=True,
+                            count=2,
+                            lfilter=lambda p: Ether in p and p[Ether].type == 0xbeef,
+                            started_callback=_sniffer_started)
+            global frm_count
+            frm_count = 2
+        t_sniffer = Thread(target=_sniffer)
+        t_sniffer.start()
+        cond_started.wait()
+        sendp(Ether(type=0xbeef)/Raw(b'0123456789'),
+              iface='veth_scapy_0',
+              count=2)
+        t_sniffer.join(1)
+        assert(frm_count == 2)
+
+    if_list = get_if_list()
+    assert ('veth_scapy_0' not in if_list)
+    assert ('veth_scapy_1' not in if_list)
+except subprocess.CalledProcessError:
+    assert False
+except Exception:
+    assert False
+
+= veth interface usage - manual interface handling
+~ linux needs_root veth
+
+from threading import Condition
+
+cond_started = Condition()
+
+def _sniffer_started():
+
+    global cond_started
+    cond_started.acquire()
+    cond_started.notify()
+    cond_started.release()
+
+cond_started.acquire()
+
+
+veth = VEthPair('veth_scapy_0', 'veth_scapy_1')
+try:
+    veth.setup()
+    veth.up()
+except subprocess.CalledProcessError:
+    assert False
+except Exception:
+    assert False
+
+if_list = get_if_list()
+assert ('veth_scapy_0' in if_list)
+assert ('veth_scapy_1' in if_list)
+
+frm_count = 0
+def _sniffer():
+    sniffed = sniff(iface='veth_scapy_1',
+                    store=True,
+                    count=2,
+                    lfilter=lambda p: Ether in p and p[Ether].type == 0xbeef,
+                    started_callback=_sniffer_started)
+    global frm_count
+    frm_count = 2
+
+t_sniffer = Thread(target=_sniffer)
+t_sniffer.start()
+cond_started.wait()
+sendp(Ether(type=0xbeef)/Raw(b'0123456789'),
+      iface='veth_scapy_0',
+      count=2)
+t_sniffer.join(1)
+assert(frm_count == 2)
+
+try:
+    veth.down()
+    veth.destroy()
+
+    if_list = get_if_list()
+    assert ('veth_scapy_0' not in if_list)
+    assert ('veth_scapy_1' not in if_list)
+except subprocess.CalledProcessError:
+    assert False
+except Exception:
+    assert False
diff --git a/test/sendsniff.uts b/test/sendsniff.uts
--- a/test/sendsniff.uts
+++ b/test/sendsniff.uts
@@ -167,3 +167,45 @@ t_sniff.join()
 
 = Delete the tun interfaces
 del tun0, tun1
+
+############
+############
++ Test bridge_and_sniff() using veth pairs
+~ linux needs_root veth
+
+= Ensure bridge_and_sniff does not close sockets if data is send within xfrm on ingress interface
+
+with VEthPair('a_0', 'a_1') as veth_0:
+    with VEthPair('b_0', 'b_1') as veth_1:
+        xfrm_count = {
+            'a_0':0,
+            'b_0': 0
+        }
+        def xfrm_x(pkt):
+            pkt_tx = pkt.copy()
+            ether_lyr = pkt_tx[Ether]
+            ether_lyr.type = 0x1234  # we send to peer interface - avoid loop
+            # send on receiving interface - triggers return None on recv() in L2Socket
+            sendp(pkt_tx, iface=pkt.sniffed_on)
+            global xfrm_count
+            xfrm_count[pkt.sniffed_on] = xfrm_count[pkt.sniffed_on] + 1
+            return True
+        t_bridge = Thread(target=bridge_and_sniff,
+                          args=('a_0', 'b_0'),
+                          kwargs={
+                              'xfrm12': xfrm_x,
+                              'xfrm21': xfrm_x,
+                              'store': False,
+                              'count': 4,
+                              'lfilter': lambda p: Ether in p and p[Ether].type == 0xbeef})
+        t_bridge.start()
+        time.sleep(1)
+        # send frames in both directions
+        for if_name in ['a_1', 'b_1', 'a_1', 'b_1']:
+            sendp([Ether(type=0xbeef) /
+                   Raw(b'On a scale from one to ten what is your favourite colour of the alphabet?')],
+                  iface=if_name)
+        t_bridge.join(1)
+        # now test of the socket used in bridge_and_sniff() was alive all the time
+        assert (xfrm_count['a_0'] == 2)
+        assert (xfrm_count['b_0'] == 2)