From 43af87fc49341b5bc26cb51e58992029c45a05cb Mon Sep 17 00:00:00 2001
From: gpotter2 <gabriel@potter.fr>
Date: Wed, 29 Mar 2017 18:17:05 +0200
Subject: [PATCH] Improve routes on windows

---
 scapy/arch/windows/__init__.py | 48 +++++++++++++++++++++++++++++-----
 scapy/consts.py                |  8 ++++++
 scapy/layers/l2.py             |  4 +--
 scapy/route.py                 | 15 +++++++----
 scapy/route6.py                | 20 +++++++++-----
 scapy/utils6.py                |  4 +--
 test/regression.uts            | 11 ++++++++
 7 files changed, 88 insertions(+), 22 deletions(-)

diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py
index a3585e0e..6918f98f 100755
--- a/scapy/arch/windows/__init__.py
+++ b/scapy/arch/windows/__init__.py
@@ -16,7 +16,7 @@ from scapy.error import Scapy_Exception, log_loading, log_runtime, warning
 from scapy.utils import atol, itom, inet_aton, inet_ntoa, PcapReader
 from scapy.base_classes import Gen, Net, SetGen
 from scapy.data import MTU, ETHER_BROADCAST, ETH_P_ARP
-from scapy.consts import LOOPBACK_NAME
+from scapy.consts import LOOPBACK_NAME, getLoopbackInterface
 
 conf.use_pcap = False
 conf.use_dnet = False
@@ -337,6 +337,7 @@ def get_ip_from_name(ifname, v6=False):
                                     ['Description', 'IPAddress']):
         if descr == ifname.strip():
             return ipaddr.split(",", 1)[v6].strip('{}').strip()
+    return None
         
 class NetworkInterface(object):
     """A network interface of your local host"""
@@ -371,6 +372,9 @@ class NetworkInterface(object):
         try:
             if not self.ip:
                 self.ip=get_ip_from_name(data['name'])
+            if not self.ip:
+                # No IP detected
+                self.invalid = True
         except (KeyError, AttributeError, NameError) as e:
             print e
         if not self.ip and self.name == LOOPBACK_NAME:
@@ -413,6 +417,14 @@ class NetworkInterfaceDict(UserDict):
                                 "You probably won't be able to send packets. "
                                 "Deactivating unneeded interfaces and restarting Scapy might help."
                                 "Check your winpcap and powershell installation, and access rights.", True)
+        else:
+            # Loading state: remove invalid interfaces
+            self.remove_invalid_ifaces()
+            # Replace interface for getLoopbackInterface()
+            try:
+                scapy.consts.LOOPBACK_INTERFACE = self.dev_from_name(LOOPBACK_NAME)
+            except:
+                pass
 
     def dev_from_name(self, name):
         """Return the first pcap device name for a given Windows
@@ -435,8 +447,19 @@ class NetworkInterfaceDict(UserDict):
         for devname, iface in self.items():
             if iface.win_index == str(if_index):
                 return iface
+        if str(if_index) == "1":
+            # Local loopback
+            loopback = getLoopbackInterface()
+            if loopback != LOOPBACK_NAME:
+                return loopback
         raise ValueError("Unknown network interface index %r" % if_index)
 
+    def remove_invalid_ifaces(self):
+        """Remove all invalid interfaces"""
+        for devname, iface in self.items():
+            if iface.is_invalid():
+                self.data.pop(devname)
+
     def show(self, resolve_mac=True):
         """Print list of available network interfaces in human readable form"""
         print "%s  %s  %s  %s" % ("INDEX".ljust(5), "IFACE".ljust(35), "IP".ljust(15), "MAC")
@@ -635,14 +658,11 @@ def read_routes6():
                 cset = ['::1']
             else:
                 devaddrs = filter(lambda x: x[2] == iface, lifaddr)
-                cset = scapy.utils6.construct_source_candidate_set(d, dp, devaddrs, LOOPBACK_NAME)
+                cset = scapy.utils6.construct_source_candidate_set(d, dp, devaddrs, getLoopbackInterface())
             # APPEND (DESTINATION, NETMASK, NEXT HOP, IFACE, CANDIDATS)
             routes.append((d, dp, nh, iface, cset))
     return routes
 
-
-
-
 if conf.interactive_shell != 'ipython' and conf.interactive:
     try:
         __IPYTHON__
@@ -680,12 +700,16 @@ def get_working_if():
         return min(read_routes(), key=lambda x: x[1])[3]
     except ValueError:
         # no route
-        return LOOPBACK_NAME
+        return getLoopbackInterface()
 
 conf.iface = get_working_if()
 
 def route_add_loopback(routes=None, ipv6=False, iflist=None):
     """Add a route to 127.0.0.1 and ::1 to simplify unit tests on Windows"""
+    if not WINDOWS:
+        warning("Not available")
+        return
+    warning("This will completly mess up the routes. Testing purpose only !")
     # Add only if some adpaters already exist
     if ipv6:
         if len(conf.route6.routes) == 0:
@@ -699,10 +723,22 @@ def route_add_loopback(routes=None, ipv6=False, iflist=None):
     data['win_index'] = -1
     data['guid'] = "{0XX00000-X000-0X0X-X00X-00XXXX000XXX}"
     data['invalid'] = True
+    data['mac'] = '00:00:00:00:00:00'
     adapter = NetworkInterface(data)
     if iflist:
         iflist.append(unicode("\\Device\\NPF_" + adapter.guid))
         return
+    # Remove all LOOPBACK_NAME routes
+    for route in list(conf.route.routes):
+        iface = route[3]
+        if iface.name == LOOPBACK_NAME:
+            conf.route.routes.remove(route)
+    # Remove LOOPBACK_NAME interface
+    for devname, iface in IFACES.items():
+        if iface.name == LOOPBACK_NAME:
+            IFACES.pop(devname)
+    # Inject interface
+    IFACES[data['guid']] = adapter
     # Build the packed network addresses
     loop_net = struct.unpack("!I", socket.inet_aton("127.0.0.0"))[0]
     loop_mask = struct.unpack("!I", socket.inet_aton("255.0.0.0"))[0]
diff --git a/scapy/consts.py b/scapy/consts.py
index cb498333..2e7f2f1a 100644
--- a/scapy/consts.py
+++ b/scapy/consts.py
@@ -59,6 +59,8 @@ WINDOWS = platform.startswith("win32")
 BSD = DARWIN or FREEBSD or OPENBSD or NETBSD
 # See https://docs.python.org/3/library/platform.html#cross-platform
 IS_64BITS = maxsize > 2**32
+# Windows only
+LOOPBACK_INTERFACE = None
 
 if WINDOWS:
     try:
@@ -72,5 +74,11 @@ else:
     uname = os.uname()
     LOOPBACK_NAME = "lo" if LINUX else "lo0"
 
+def getLoopbackInterface():
+    if LOOPBACK_INTERFACE and WINDOWS:
+        return LOOPBACK_INTERFACE
+    else:
+        return LOOPBACK_NAME
+
 def parent_function():
     return inspect.getouterframes(inspect.currentframe())
diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py
index 9b3292d0..127514d0 100644
--- a/scapy/layers/l2.py
+++ b/scapy/layers/l2.py
@@ -18,7 +18,7 @@ from scapy.plist import SndRcvList
 from scapy.fields import *
 from scapy.sendrecv import srp, srp1, srpflood
 from scapy.arch import get_if_hwaddr
-from scapy.consts import LOOPBACK_NAME
+from scapy.consts import LOOPBACK_NAME, getLoopbackInterface
 from scapy.utils import inet_ntoa, inet_aton
 from scapy.error import warning
 if conf.route is None:
@@ -63,7 +63,7 @@ def getmacbyip(ip, chainCC=0):
     if (tmp[0] & 0xf0) == 0xe0: # mcast @
         return "01:00:5e:%.2x:%.2x:%.2x" % (tmp[1]&0x7f,tmp[2],tmp[3])
     iff,a,gw = conf.route.route(ip)
-    if ( (iff == LOOPBACK_NAME) or (ip == conf.route.get_if_bcast(iff)) ):
+    if ( (iff == getLoopbackInterface()) or (ip == conf.route.get_if_bcast(iff)) ):
         return "ff:ff:ff:ff:ff:ff"
     if gw != "0.0.0.0":
         ip = gw
diff --git a/scapy/route.py b/scapy/route.py
index 017ac7b7..cb216175 100644
--- a/scapy/route.py
+++ b/scapy/route.py
@@ -8,7 +8,7 @@ Routing and handling of network interfaces.
 """
 
 import socket
-from scapy.consts import LOOPBACK_NAME
+from scapy.consts import LOOPBACK_NAME, getLoopbackInterface
 from scapy.utils import atol, ltoa, itom
 from scapy.config import conf
 from scapy.error import Scapy_Exception, warning
@@ -32,7 +32,7 @@ class Route:
         self.routes = read_routes()
 
     def __repr__(self):
-        rtlst = [("Network", "Netmask", "Gateway", "Iface", "Output IP")]
+        rtlst = []
         
         for net,msk,gw,iface,addr in self.routes:
 	    rtlst.append((ltoa(net),
@@ -40,6 +40,11 @@ class Route:
                       gw,
                       (iface.name if not isinstance(iface, basestring) else iface),
                       addr))
+
+        # Sort correctly
+        rtlst.sort(key=lambda x: x[0])
+        # Append tag
+        rtlst = [("Network", "Netmask", "Gateway", "Iface", "Output IP")] + rtlst
         
         colwidth = map(lambda x: max(map(lambda y: len(y), x)), apply(zip, rtlst))
         fmt = "  ".join(map(lambda x: "%%-%ds"%x, colwidth))
@@ -154,13 +159,13 @@ class Route:
                 continue
             aa = atol(a)
             if aa == dst:
-                pathes.append((0xffffffffL,(LOOPBACK_NAME,a,"0.0.0.0")))
+                pathes.append((0xffffffffL,(getLoopbackInterface(),a,"0.0.0.0")))
             if (dst & m) == (d & m):
                 pathes.append((m,(i,a,gw)))
         if not pathes:
             if verbose:
                 warning("No route found (no default route?)")
-            return LOOPBACK_NAME,"0.0.0.0","0.0.0.0" #XXX linux specific!
+            return getLoopbackInterface(),"0.0.0.0","0.0.0.0"
         # Choose the more specific route (greatest netmask).
         # XXX: we don't care about metrics
         pathes.sort()
@@ -185,6 +190,6 @@ conf.route=Route()
 
 #XXX use "with"
 _betteriface = conf.route.route("0.0.0.0", verbose=0)[0]
-if _betteriface != LOOPBACK_NAME:
+if ((_betteriface.name != LOOPBACK_NAME) if WINDOWS else (_betteriface != LOOPBACK_NAME)):
     conf.iface = _betteriface
 del(_betteriface)
diff --git a/scapy/route6.py b/scapy/route6.py
index 8a7cb1ad..7c88d6d5 100644
--- a/scapy/route6.py
+++ b/scapy/route6.py
@@ -22,6 +22,7 @@ from scapy.utils6 import *
 from scapy.arch import *
 from scapy.pton_ntop import *
 from scapy.error import warning, log_loading
+from scapy.consts import getLoopbackInterface
 
 
 class Route6:
@@ -46,14 +47,19 @@ class Route6:
             log_loading.info("No IPv6 support in kernel")
 
     def __repr__(self):
-        rtlst = [('Destination', 'Next Hop', "iface", "src candidates")]
+        rtlst = []
 
         for net,msk,gw,iface,cset in self.routes:
             rtlst.append(('%s/%i'% (net,msk), gw, (iface if isinstance(iface, basestring) else iface.name), ", ".join(cset)))
 
-        colwidth = map(lambda x: max(map(lambda y: len(y), x)), apply(zip, rtlst))
-        fmt = "  ".join(map(lambda x: "%%-%ds"%x, colwidth))
-        rt = "\n".join(map(lambda x: fmt % x, rtlst))
+        # Sort correctly
+        rtlst.sort(key=lambda x: x[0])
+        # Append tag
+        rtlst = [('Destination', 'Next Hop', "iface", "src candidates")] + rtlst
+        
+        colwidth = [max([len(y) for y in x]) for x in zip(*rtlst)]
+        fmt = "  ".join(["%%-%ds"%x for x in colwidth])
+        rt = "\n".join([fmt % x for x in rtlst])
 
         return rt
 
@@ -77,7 +83,7 @@ class Route6:
             # replace that unique address by the list of all addresses
             lifaddr = in6_getifaddr()
             devaddrs = filter(lambda x: x[2] == dev, lifaddr)
-            ifaddr = construct_source_candidate_set(prefix, plen, devaddrs, LOOPBACK_NAME)
+            ifaddr = construct_source_candidate_set(prefix, plen, devaddrs, getLoopbackInterface())
 
         return (prefix, plen, gw, dev, ifaddr)
 
@@ -220,7 +226,7 @@ class Route6:
 
         if not pathes:
             warning("No route found for IPv6 destination %s (no default route?)" % dst)
-            return (LOOPBACK_NAME, "::", "::") # XXX Linux specific
+            return (getLoopbackInterface(), "::", "::")
 
         # Sort with longest prefix first
         pathes.sort(reverse=True)
@@ -237,7 +243,7 @@ class Route6:
 
         if res == []:
             warning("Found a route for IPv6 destination '%s', but no possible source address." % dst)
-            return (LOOPBACK_NAME, "::", "::") # XXX Linux specific
+            return (getLoopbackInterface(), "::", "::")
 
         # Symptom  : 2 routes with same weight (our weight is plen)
         # Solution :
diff --git a/scapy/utils6.py b/scapy/utils6.py
index cb78d2cf..99292911 100644
--- a/scapy/utils6.py
+++ b/scapy/utils6.py
@@ -21,7 +21,7 @@ from scapy.volatile import RandMAC
 from scapy.error import warning
 
 
-def construct_source_candidate_set(addr, plen, laddr, loname):
+def construct_source_candidate_set(addr, plen, laddr, loiface):
     """
     Given all addresses assigned to a specific interface ('laddr' parameter),
     this function returns the "candidate set" associated with 'addr/plen'.
@@ -57,7 +57,7 @@ def construct_source_candidate_set(addr, plen, laddr, loname):
         cset = filter(lambda x: x[1] == IPV6_ADDR_SITELOCAL, laddr)
     elif in6_ismaddr(addr):
         if in6_ismnladdr(addr):
-            cset = [('::1', 16, loname)]
+            cset = [('::1', 16, loiface)]
         elif in6_ismgladdr(addr):
             cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr)
         elif in6_ismlladdr(addr):
diff --git a/test/regression.uts b/test/regression.uts
index 80de536c..0f17307e 100644
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -11,6 +11,8 @@
 * Dump the current configuration
 conf
 
+IP().src
+
 = List layers
 ~ conf command
 ls()
@@ -92,6 +94,15 @@ else:
 conf.route6.ifchange(LOOPBACK_NAME, "::1/128")
 True
 
+= UTscapy route check
+* Check that UTscapy has correctly replaced the routes. Many tests won't work otherwise
+
+if WINDOWS:
+    route_add_loopback()
+
+IP().src
+assert _ == "127.0.0.1"
+
 ############
 ############
 + Main.py tests
-- 
GitLab