1## This file is part of Scapy
2## See http://www.secdev.org/projects/scapy for more informations
3## Copyright (C) Philippe Biondi <phil@secdev.org>
4## This program is published under a GPLv2 license
5
6"""
7Common customizations for all Unix-like operating systems other than Linux
8"""
9
10import sys,os,struct,socket,time
11from fcntl import ioctl
12import socket
13
14from scapy.error import warning, log_interactive
15import scapy.config
16import scapy.utils
17from scapy.utils6 import in6_getscope, construct_source_candidate_set
18from scapy.utils6 import in6_isvalid, in6_ismlladdr, in6_ismnladdr
19from scapy.consts import FREEBSD, NETBSD, OPENBSD, SOLARIS, LOOPBACK_NAME
20from scapy.arch import get_if_addr
21from scapy.config import conf
22
23
24##################
25## Routes stuff ##
26##################
27
28def _guess_iface_name(netif):
29    """
30    We attempt to guess the name of interfaces that are truncated from the
31    output of ifconfig -l.
32    If there is only one possible candidate matching the interface name then we
33    return it.
34    If there are none or more, then we return None.
35    """
36    with os.popen('%s -l' % conf.prog.ifconfig) as fdesc:
37        ifaces = fdesc.readline().strip().split(' ')
38    matches = [iface for iface in ifaces if iface.startswith(netif)]
39    if len(matches) == 1:
40        return matches[0]
41    return None
42
43
44def read_routes():
45    if SOLARIS:
46        f=os.popen("netstat -rvn") # -f inet
47    elif FREEBSD:
48        f=os.popen("netstat -rnW") # -W to handle long interface names
49    else:
50        f=os.popen("netstat -rn") # -f inet
51    ok = 0
52    mtu_present = False
53    prio_present = False
54    routes = []
55    pending_if = []
56    for l in f.readlines():
57        if not l:
58            break
59        l = l.strip()
60        if l.find("----") >= 0: # a separation line
61            continue
62        if not ok:
63            if l.find("Destination") >= 0:
64                ok = 1
65                mtu_present = "Mtu" in l
66                prio_present = "Prio" in l
67                refs_present = "Refs" in l
68            continue
69        if not l:
70            break
71        if SOLARIS:
72            lspl = l.split()
73            if len(lspl) == 10:
74                dest,mask,gw,netif,mxfrg,rtt,ref,flg = lspl[:8]
75            else: # missing interface
76                dest,mask,gw,mxfrg,rtt,ref,flg = lspl[:7]
77                netif=None
78        else:
79            rt = l.split()
80            dest,gw,flg = rt[:3]
81            netif = rt[4 + mtu_present + prio_present + refs_present]
82        if flg.find("Lc") >= 0:
83            continue
84        if dest == "default":
85            dest = 0
86            netmask = 0
87        else:
88            if SOLARIS:
89                netmask = scapy.utils.atol(mask)
90            elif "/" in dest:
91                dest,netmask = dest.split("/")
92                netmask = scapy.utils.itom(int(netmask))
93            else:
94                netmask = scapy.utils.itom((dest.count(".") + 1) * 8)
95            dest += ".0"*(3-dest.count("."))
96            dest = scapy.utils.atol(dest)
97        # XXX: TODO: add metrics for unix.py (use -e option on netstat)
98        metric = 1
99        if not "G" in flg:
100            gw = '0.0.0.0'
101        if netif is not None:
102            try:
103                ifaddr = get_if_addr(netif)
104                routes.append((dest,netmask, gw, netif, ifaddr, metric))
105            except OSError as exc:
106                if exc.message == 'Device not configured':
107                    # This means the interface name is probably truncated by
108                    # netstat -nr. We attempt to guess it's name and if not we
109                    # ignore it.
110                    guessed_netif = _guess_iface_name(netif)
111                    if guessed_netif is not None:
112                        ifaddr = get_if_addr(guessed_netif)
113                        routes.append((dest, netmask, gw, guessed_netif, ifaddr, metric))
114                    else:
115                        warning("Could not guess partial interface name: %s", netif)
116                else:
117                    raise
118        else:
119            pending_if.append((dest,netmask,gw))
120    f.close()
121
122    # On Solaris, netstat does not provide output interfaces for some routes
123    # We need to parse completely the routing table to route their gw and
124    # know their output interface
125    for dest,netmask,gw in pending_if:
126        gw_l = scapy.utils.atol(gw)
127        max_rtmask,gw_if,gw_if_addr, = 0,None,None
128        for rtdst,rtmask,_,rtif,rtaddr in routes[:]:
129            if gw_l & rtmask == rtdst:
130                if rtmask >= max_rtmask:
131                    max_rtmask = rtmask
132                    gw_if = rtif
133                    gw_if_addr = rtaddr
134        # XXX: TODO add metrics
135        metric = 1
136        if gw_if:
137            routes.append((dest,netmask, gw, gw_if, gw_if_addr, metric))
138        else:
139            warning("Did not find output interface to reach gateway %s", gw)
140
141    return routes
142
143############
144### IPv6 ###
145############
146
147def _in6_getifaddr(ifname):
148    """
149    Returns a list of IPv6 addresses configured on the interface ifname.
150    """
151
152    # Get the output of ifconfig
153    try:
154        f = os.popen("%s %s" % (conf.prog.ifconfig, ifname))
155    except OSError as msg:
156        log_interactive.warning("Failed to execute ifconfig.")
157        return []
158
159    # Iterate over lines and extract IPv6 addresses
160    ret = []
161    for line in f:
162        if "inet6" in line:
163            addr = line.rstrip().split(None, 2)[1] # The second element is the IPv6 address
164        else:
165            continue
166        if '%' in line: # Remove the interface identifier if present
167            addr = addr.split("%", 1)[0]
168
169        # Check if it is a valid IPv6 address
170        try:
171            socket.inet_pton(socket.AF_INET6, addr)
172        except:
173            continue
174
175        # Get the scope and keep the address
176        scope = in6_getscope(addr)
177        ret.append((addr, scope, ifname))
178
179    return ret
180
181def in6_getifaddr():
182    """
183    Returns a list of 3-tuples of the form (addr, scope, iface) where
184    'addr' is the address of scope 'scope' associated to the interface
185    'iface'.
186
187    This is the list of all addresses of all interfaces available on
188    the system.
189    """
190
191    # List all network interfaces
192    if OPENBSD:
193        try:
194            f = os.popen("%s" % conf.prog.ifconfig)
195        except OSError as msg:
196            log_interactive.warning("Failed to execute ifconfig.")
197            return []
198
199        # Get the list of network interfaces
200        splitted_line = []
201        for l in f:
202            if "flags" in l:
203                iface = l.split()[0].rstrip(':')
204                splitted_line.append(iface)
205
206    else: # FreeBSD, NetBSD or Darwin
207        try:
208            f = os.popen("%s -l" % conf.prog.ifconfig)
209        except OSError as msg:
210            log_interactive.warning("Failed to execute ifconfig.")
211            return []
212
213        # Get the list of network interfaces
214        splitted_line = f.readline().rstrip().split()
215
216    ret = []
217    for i in splitted_line:
218        ret += _in6_getifaddr(i)
219    return ret
220
221
222def read_routes6():
223    """Return a list of IPv6 routes than can be used by Scapy."""
224
225    # Call netstat to retrieve IPv6 routes
226    fd_netstat = os.popen("netstat -rn -f inet6")
227
228    # List interfaces IPv6 addresses
229    lifaddr = in6_getifaddr()
230    if not lifaddr:
231        return []
232
233    # Routes header information
234    got_header = False
235    mtu_present = False
236    prio_present = False
237
238    # Parse the routes
239    routes = []
240    for line in fd_netstat.readlines():
241
242        # Parse the routes header and try to identify extra columns
243        if not got_header:
244            if "Destination" == line[:11]:
245                got_header = True
246                mtu_present = "Mtu" in line
247                prio_present = "Prio" in line
248            continue
249
250        # Parse a route entry according to the operating system
251        splitted_line = line.split()
252        if OPENBSD or NETBSD:
253            index = 5 + mtu_present + prio_present
254            if len(splitted_line) < index:
255                warning("Not enough columns in route entry !")
256                continue
257            destination, next_hop, flags = splitted_line[:3]
258            dev = splitted_line[index]
259        else:
260            # FREEBSD or DARWIN
261            if len(splitted_line) < 4:
262                warning("Not enough columns in route entry !")
263                continue
264            destination, next_hop, flags, dev = splitted_line[:4]
265
266        # XXX: TODO: add metrics for unix.py (use -e option on netstat)
267        metric = 1
268
269        # Check flags
270        if not "U" in flags:  # usable route
271            continue
272        if "R" in flags:  # Host or net unreachable
273            continue
274        if "m" in flags:  # multicast address
275            # Note: multicast routing is handled in Route6.route()
276            continue
277
278        # Replace link with the default route in next_hop
279        if "link" in next_hop:
280            next_hop = "::"
281
282        # Default prefix length
283        destination_plen = 128
284
285        # Extract network interface from the zone id
286        if '%' in destination:
287            destination, dev = destination.split('%')
288            if '/' in dev:
289                # Example: fe80::%lo0/64 ; dev = "lo0/64"
290                dev, destination_plen = dev.split('/')
291        if '%' in next_hop:
292            next_hop, dev = next_hop.split('%')
293
294        # Ensure that the next hop is a valid IPv6 address
295        if not in6_isvalid(next_hop):
296            # Note: the 'Gateway' column might contain a MAC address
297            next_hop = "::"
298
299        # Modify parsed routing entries
300        # Note: these rules are OS specific and may evolve over time
301        if destination == "default":
302            destination, destination_plen = "::", 0
303        elif '/' in destination:
304            # Example: fe80::/10
305            destination, destination_plen = destination.split('/')
306        if '/' in dev:
307            # Example: ff02::%lo0/32 ; dev = "lo0/32"
308            dev, destination_plen = dev.split('/')
309
310        # Check route entries parameters consistency
311        if not in6_isvalid(destination):
312            warning("Invalid destination IPv6 address in route entry !")
313            continue
314        try:
315            destination_plen = int(destination_plen)
316        except:
317            warning("Invalid IPv6 prefix length in route entry !")
318            continue
319        if in6_ismlladdr(destination) or in6_ismnladdr(destination):
320            # Note: multicast routing is handled in Route6.route()
321            continue
322
323        if LOOPBACK_NAME in dev:
324            # Handle ::1 separately
325            cset = ["::1"]
326            next_hop = "::"
327        else:
328            # Get possible IPv6 source addresses
329            devaddrs = (x for x in lifaddr if x[2] == dev)
330            cset = construct_source_candidate_set(destination, destination_plen, devaddrs)
331
332        if len(cset):
333            routes.append((destination, destination_plen, next_hop, dev, cset, metric))
334
335    fd_netstat.close()
336    return routes
337