diff --git a/.appveyor.yml b/.appveyor.yml index 3a744d8528cfbd3e6b1fda65d0ade1e18539b1fe..ffa3c7a8a54c21c928a6ac034ed7be06f8ed32ab 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,7 +29,7 @@ test_script: - 'del test\regression.uts' # Secondary and contrib unit tests - - 'del test\bpf.uts test\linux.uts' # Don't bother with OS dependent regression tests + - 'del test\bpf.uts test\linux.uts test\sendsniff.uts' # Don't bother with OS dependent regression tests - "%PYTHON%\\python -m coverage run --parallel-mode bin\\UTscapy -c test\\configs\\windows.utsc || exit /b 42" # TLS unit tests diff --git a/.travis/test.sh b/.travis/test.sh index 2c27c3e8dbc914761c7b67c4b1cc3e258f95a662..abd6f0cfb6a309e11787bd4e77f1840579718a7b 100644 --- a/.travis/test.sh +++ b/.travis/test.sh @@ -12,6 +12,11 @@ then SCAPY_SUDO="" fi +if [ "$SCAPY_USE_PCAPDNET" = "yes" ] +then + UT_FLAGS+=" -K not_pcapdnet" +fi + # AES-CCM, ChaCha20Poly1305 and X25519 were added to Cryptography v2.0 # but the minimal version mandated by scapy is v1.7 UT_FLAGS+=" -K crypto_advanced" diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 4503abcac26c5526c0cbd2841ba4c59885f78022..12cb665493466e039ddfd97a7fe703ee246a712c 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -31,6 +31,7 @@ from scapy.modules.six import iteritems if conf.route is None: # unused import, only to initialize conf.route import scapy.route +from scapy.supersocket import SuperSocket ################# ## Debug class ## @@ -734,16 +735,25 @@ Examples: @conf.commands.register -def bridge_and_sniff(if1, if2, prn=None, L2socket=None, *args, **kargs): - """Forward traffic between interfaces if1 and if2, sniff and return the -exchanged packets. +def bridge_and_sniff(if1, if2, xfrm12=None, xfrm21=None, prn=None, L2socket=None, + *args, **kargs): + """Forward traffic between interfaces if1 and if2, sniff and return +the exchanged packets. Arguments: - if1, if2: the interfaces to use + if1, if2: the interfaces to use (interface names or opened sockets). + + xfrm12: a function to call when forwarding a packet from if1 to + if2. If it returns True, the packet is forwarded as it. If it + returns False or None, the packet is discarded. If it returns a + packet, this packet is forwarded instead of the original packet + one. + + xfrm21: same as xfrm12 for packets forwarded from if2 to if1. The other arguments are the same than for the function sniff(), - except for opened_socket, offline and iface that are ignored. + except for offline, opened_socket and iface that are ignored. See help(sniff) for more. """ @@ -752,20 +762,45 @@ Arguments: log_runtime.warning("Argument %s cannot be used in " "bridge_and_sniff() -- ignoring it.", arg) del kargs[arg] - if L2socket is None: - L2socket = conf.L2socket - s1 = L2socket(iface=if1) - s2 = L2socket(iface=if2) - peers = {if1: s2, if2: s1} + def _init_socket(iface, count): + if isinstance(iface, SuperSocket): + return iface, "iface%d" % count + else: + return (L2socket or conf.L2socket)(iface=iface), iface + sckt1, if1 = _init_socket(if1, 1) + sckt2, if2 = _init_socket(if2, 2) + peers = {if1: sckt2, if2: sckt1} + xfrms = {} + if xfrm12 is not None: + xfrms[if1] = xfrm12 + if xfrm21 is not None: + xfrms[if2] = xfrm21 def prn_send(pkt): try: sendsock = peers[pkt.sniffed_on] except KeyError: return + if pkt.sniffed_on in xfrms: + try: + newpkt = xfrms[pkt.sniffed_on](pkt) + except: + log_runtime.warning( + 'Exception in transformation function for packet [%s] ' + 'received on %s -- dropping', + pkt.summary(), pkt.sniffed_on, exc_info=True + ) + return + else: + if newpkt is True: + newpkt = pkt.original + elif not newpkt: + return + else: + newpkt = pkt.original try: - sendsock.send(pkt.original) + sendsock.send(newpkt) except: - log_runtime.warning('Cannot forward packet [%s] received from %s', + log_runtime.warning('Cannot forward packet [%s] received on %s', pkt.summary(), pkt.sniffed_on, exc_info=True) if prn is None: prn = prn_send @@ -775,7 +810,8 @@ Arguments: prn_send(pkt) return prn_orig(pkt) - return sniff(opened_socket={s1: if1, s2: if2}, prn=prn, *args, **kargs) + return sniff(opened_socket={sckt1: if1, sckt2: if2}, prn=prn, + *args, **kargs) @conf.commands.register diff --git a/scapy/supersocket.py b/scapy/supersocket.py index 81feec212db0d9e752a6614270630dc5e579d974..9ea7373f9d02235b16a176a12189524ac0d77d30 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -8,14 +8,18 @@ SuperSocket. """ from __future__ import absolute_import -import socket,time +import socket +import subprocess +import struct +import time from scapy.config import conf +from scapy.consts import LINUX, OPENBSD, BSD from scapy.data import * from scapy.error import warning, log_runtime +import scapy.modules.six as six import scapy.packet from scapy.utils import PcapReader, tcpdump -import scapy.modules.six as six class _SuperSocket_metaclass(type): def __repr__(self): @@ -44,12 +48,14 @@ class SuperSocket(six.with_metaclass(_SuperSocket_metaclass)): def close(self): if self.closed: return - self.closed=1 - if self.ins != self.outs: - if self.outs and self.outs.fileno() != -1: - self.outs.close() - if self.ins and self.ins.fileno() != -1: - self.ins.close() + self.closed = True + if hasattr(self, "outs"): + if not hasattr(self, "ins") or self.ins != self.outs: + if self.outs and self.outs.fileno() != -1: + self.outs.close() + if hasattr(self, "ins"): + if self.ins and self.ins.fileno() != -1: + self.ins.close() def sr(self, *args, **kargs): from scapy import sendrecv return sendrecv.sndrcv(self, *args, **kargs) @@ -201,5 +207,81 @@ class L2ListenTcpdump(SuperSocket): return self.ins.recv(x) +class TunTapInterface(SuperSocket): + """A socket to act as the host's peer of a tun / tap interface. + + """ + desc = "Act as the host's peer of a tun / tap interface" + + def __init__(self, iface=None, mode_tun=None, *arg, **karg): + self.iface = conf.iface if iface is None else iface + self.mode_tun = ("tun" in iface) if mode_tun is None else mode_tun + self.closed = True + self.open() + + def __enter__(self): + return self + + def __del__(self): + self.close() + + def __exit__(self, *_): + self.close() + + def open(self): + """Open the TUN or TAP device.""" + if not self.closed: + return + self.outs = self.ins = open( + "/dev/net/tun" if LINUX else ("/dev/%s" % self.iface), "r+b", + ) + if LINUX: + from fcntl import ioctl + # TUNSETIFF = 0x400454ca + # IFF_TUN = 0x0001 + # IFF_TAP = 0x0002 + # IFF_NO_PI = 0x1000 + ioctl(self.ins, 0x400454ca, struct.pack( + "16sH", self.iface, 0x0001 if self.mode_tun else 0x1002, + )) + self.closed = False + + def __call__(self, *arg, **karg): + """Needed when using an instantiated TunTapInterface object for +conf.L2listen, conf.L2socket or conf.L3socket. + + """ + return self + + def recv(self, x=MTU): + if self.mode_tun: + data = os.read(self.ins.fileno(), x + 4) + proto = struct.unpack('!H', data[2:4])[0] + return conf.l3types.get(proto, conf.raw_layer)(data[4:]) + return conf.l2types.get(1, conf.raw_layer)( + os.read(self.ins.fileno(), x) + ) + + def send(self, x): + sx = str(x) + if hasattr(x, "sent_time"): + x.sent_time = time.time() + if self.mode_tun: + try: + proto = conf.l3types[type(x)] + except KeyError: + log_runtime.warning( + "Cannot find layer 3 protocol value to send %s in " + "conf.l3types, using 0", + x.name if hasattr(x, "name") else type(x).__name__ + ) + proto = 0 + sx = struct.pack('!HH', 0, proto) + sx + try: + os.write(self.outs.fileno(), sx) + except socket.error: + log_runtime.error("%s send", self.__class__.__name__, exc_info=True) + + if conf.L3socket is None: conf.L3socket = L3RawSocket diff --git a/test/sendsniff.uts b/test/sendsniff.uts new file mode 100644 index 0000000000000000000000000000000000000000..ba810c48c854245d94cd24bf361b40bf513a7bb3 --- /dev/null +++ b/test/sendsniff.uts @@ -0,0 +1,169 @@ +% send, sniff, sr* tests for Scapy + +~ netaccess + +############ +############ ++ Test bridge_and_sniff() using tap sockets + +~ tap linux + += Create two tap interfaces + +import subprocess +from threading import Thread + +tap0, tap1 = [TunTapInterface("tap%d" % i) for i in range(2)] + +if LINUX: + for i in range(2): + assert subprocess.check_call(["ip", "link", "set", "tap%d" % i, "up"]) == 0 +else: + for i in range(2): + assert subprocess.check_call(["ifconfig", "tap%d" % i, "up"]) == 0 + += Run a sniff thread on the tap1 **interface** +* It will terminate when 5 IP packets from 1.2.3.4 have been sniffed +t_sniff = Thread( + target=sniff, + kwargs={"iface": "tap1", "count": 5, "prn": Packet.summary, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"} +) +t_sniff.start() + += Run a bridge_and_sniff thread between the taps **sockets** +* It will terminate when 5 IP packets from 1.2.3.4 have been forwarded +t_bridge = Thread(target=bridge_and_sniff, args=(tap0, tap1), + kwargs={"store": False, "count": 5, 'prn': Packet.summary, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"}) +t_bridge.start() + += Send five IP packets from 1.2.3.4 to the tap0 **interface** +time.sleep(1) +sendp([Ether(dst=ETHER_BROADCAST) / IP(src="1.2.3.4") / ICMP()], iface="tap0", + count=5) + += Wait for the threads +t_bridge.join() +t_sniff.join() + += Run a sniff thread on the tap1 **interface** +* It will terminate when 5 IP packets from 2.3.4.5 have been sniffed +t_sniff = Thread( + target=sniff, + kwargs={"iface": "tap1", "count": 5, "prn": Packet.summary, + "lfilter": lambda p: IP in p and p[IP].src == "2.3.4.5"} +) +t_sniff.start() + += Run a bridge_and_sniff thread between the taps **sockets** +* It will "NAT" packets from 1.2.3.4 to 2.3.4.5 and will terminate when 5 IP packets have been forwarded +def nat_1_2(pkt): + if IP in pkt and pkt[IP].src == "1.2.3.4": + pkt[IP].src = "2.3.4.5" + del pkt[IP].chksum + return pkt + return False + +t_bridge = Thread(target=bridge_and_sniff, args=(tap0, tap1), + kwargs={"store": False, "count": 5, 'prn': Packet.summary, + "xfrm12": nat_1_2, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"}) +t_bridge.start() + += Send five IP packets from 1.2.3.4 to the tap0 **interface** +time.sleep(1) +sendp([Ether(dst=ETHER_BROADCAST) / IP(src="1.2.3.4") / ICMP()], iface="tap0", + count=5) + += Wait for the threads +t_bridge.join() +t_sniff.join() + += Delete the tap interfaces +del tap0, tap1 + + +############ +############ ++ Test bridge_and_sniff() using tun sockets + +~ tun linux not_pcapdnet + += Create two tun interfaces + +import subprocess +from threading import Thread + +tun0, tun1 = [TunTapInterface("tun%d" % i) for i in range(2)] + +if LINUX: + for i in range(2): + assert subprocess.check_call(["ip", "link", "set", "tun%d" % i, "up"]) == 0 +else: + for i in range(2): + assert subprocess.check_call(["ifconfig", "tun%d" % i, "up"]) == 0 + += Run a sniff thread on the tun1 **interface** +* It will terminate when 5 IP packets from 1.2.3.4 have been sniffed +t_sniff = Thread( + target=sniff, + kwargs={"iface": "tun1", "count": 5, "prn": Packet.summary, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"} +) +t_sniff.start() + += Run a bridge_and_sniff thread between the tuns **sockets** +* It will terminate when 5 IP packets from 1.2.3.4 have been forwarded. +t_bridge = Thread(target=bridge_and_sniff, args=(tun0, tun1), + kwargs={"store": False, "count": 5, 'prn': Packet.summary, + "xfrm12": lambda pkt: pkt, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"}) +t_bridge.start() + += Send five IP packets from 1.2.3.4 to the tun0 **interface** +time.sleep(1) +conf.route.add(net="1.2.3.4/32", dev="tun0") +send(IP(src="1.2.3.4", dst="1.2.3.4") / ICMP(), count=5) +conf.route.delt(net="1.2.3.4/32", dev="tun0") + += Wait for the threads +t_bridge.join() +t_sniff.join() + += Run a sniff thread on the tun1 **interface** +* It will terminate when 5 IP packets from 2.3.4.5 have been sniffed +t_sniff = Thread( + target=sniff, + kwargs={"iface": "tun1", "count": 5, "prn": Packet.summary, + "lfilter": lambda p: IP in p and p[IP].src == "2.3.4.5"} +) +t_sniff.start() + += Run a bridge_and_sniff thread between the tuns **sockets** +* It will "NAT" packets from 1.2.3.4 to 2.3.4.5 and will terminate when 5 IP packets have been forwarded +def nat_1_2(pkt): + if IP in pkt and pkt[IP].src == "1.2.3.4": + pkt[IP].src = "2.3.4.5" + del pkt[IP].chksum + return pkt + return False + +t_bridge = Thread(target=bridge_and_sniff, args=(tun0, tun1), + kwargs={"store": False, "count": 5, 'prn': Packet.summary, + "xfrm12": nat_1_2, + "lfilter": lambda p: IP in p and p[IP].src == "1.2.3.4"}) +t_bridge.start() + += Send five IP packets from 1.2.3.4 to the tun0 **interface** +time.sleep(1) +conf.route.add(net="1.2.3.4/32", dev="tun0") +send(IP(src="1.2.3.4", dst="1.2.3.4") / ICMP(), count=5) +conf.route.delt(net="1.2.3.4/32", dev="tun0") + += Wait for the threads +t_bridge.join() +t_sniff.join() + += Delete the tun interfaces +del tun0, tun1