1#!/usr/bin/python
2#
3# Copyright 2014 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Base module for multinetwork tests."""
18
19import errno
20import fcntl
21import os
22import posix
23import random
24import re
25from socket import *  # pylint: disable=wildcard-import
26import struct
27
28from scapy import all as scapy
29
30import csocket
31import cstruct
32import iproute
33import net_test
34
35
36IFF_TUN = 1
37IFF_TAP = 2
38IFF_NO_PI = 0x1000
39TUNSETIFF = 0x400454ca
40
41SO_BINDTODEVICE = 25
42
43# Setsockopt values.
44IP_UNICAST_IF = 50
45IPV6_MULTICAST_IF = 17
46IPV6_UNICAST_IF = 76
47
48# Cmsg values.
49IP_TTL = 2
50IP_PKTINFO = 8
51IPV6_2292PKTOPTIONS = 6
52IPV6_FLOWINFO = 11
53IPV6_PKTINFO = 50
54IPV6_HOPLIMIT = 52  # Different from IPV6_UNICAST_HOPS, this is cmsg only.
55
56# Data structures.
57# These aren't constants, they're classes. So, pylint: disable=invalid-name
58InPktinfo = cstruct.Struct("in_pktinfo", "@i4s4s", "ifindex spec_dst addr")
59In6Pktinfo = cstruct.Struct("in6_pktinfo", "@16si", "addr ifindex")
60
61
62def HaveUidRouting():
63  """Checks whether the kernel supports UID routing."""
64  # Create a rule with the UID range selector. If the kernel doesn't understand
65  # the selector, it will create a rule with no selectors.
66  try:
67    iproute.IPRoute().UidRangeRule(6, True, 1000, 2000, 100, 10000)
68  except IOError:
69    return False
70
71  # Dump all the rules. If we find a rule using the UID range selector, then the
72  # kernel supports UID range routing.
73  rules = iproute.IPRoute().DumpRules(6)
74  result = any("FRA_UID_START" in attrs for rule, attrs in rules)
75
76  # Delete the rule.
77  iproute.IPRoute().UidRangeRule(6, False, 1000, 2000, 100, 10000)
78  return result
79
80AUTOCONF_TABLE_SYSCTL = "/proc/sys/net/ipv6/conf/default/accept_ra_rt_table"
81
82HAVE_AUTOCONF_TABLE = os.path.isfile(AUTOCONF_TABLE_SYSCTL)
83HAVE_UID_ROUTING = HaveUidRouting()
84
85
86class UnexpectedPacketError(AssertionError):
87  pass
88
89
90def MakePktInfo(version, addr, ifindex):
91  family = {4: AF_INET, 6: AF_INET6}[version]
92  if not addr:
93    addr = {4: "0.0.0.0", 6: "::"}[version]
94  if addr:
95    addr = inet_pton(family, addr)
96  if version == 6:
97    return In6Pktinfo((addr, ifindex)).Pack()
98  else:
99    return InPktinfo((ifindex, addr, "\x00" * 4)).Pack()
100
101
102class MultiNetworkBaseTest(net_test.NetworkTest):
103  """Base class for all multinetwork tests.
104
105  This class does not contain any test code, but contains code to set up and
106  tear a multi-network environment using multiple tun interfaces. The
107  environment is designed to be similar to a real Android device in terms of
108  rules and routes, and supports IPv4 and IPv6.
109
110  Tests wishing to use this environment should inherit from this class and
111  ensure that any setupClass, tearDownClass, setUp, and tearDown methods they
112  implement also call the superclass versions.
113  """
114
115  # Must be between 1 and 256, since we put them in MAC addresses and IIDs.
116  NETIDS = [100, 150, 200, 250]
117
118  # Stores sysctl values to write back when the test completes.
119  saved_sysctls = {}
120
121  # Wether to output setup commands.
122  DEBUG = False
123
124  # The size of our UID ranges.
125  UID_RANGE_SIZE = 1000
126
127  # Rule priorities.
128  PRIORITY_UID = 100
129  PRIORITY_OIF = 200
130  PRIORITY_FWMARK = 300
131  PRIORITY_IIF = 400
132  PRIORITY_DEFAULT = 999
133  PRIORITY_UNREACHABLE = 1000
134
135  # For convenience.
136  IPV4_ADDR = net_test.IPV4_ADDR
137  IPV6_ADDR = net_test.IPV6_ADDR
138  IPV4_PING = net_test.IPV4_PING
139  IPV6_PING = net_test.IPV6_PING
140
141  @classmethod
142  def UidRangeForNetid(cls, netid):
143    return (
144        cls.UID_RANGE_SIZE * netid,
145        cls.UID_RANGE_SIZE * (netid + 1) - 1
146    )
147
148  @classmethod
149  def UidForNetid(cls, netid):
150    return random.randint(*cls.UidRangeForNetid(netid))
151
152  @classmethod
153  def _TableForNetid(cls, netid):
154    if cls.AUTOCONF_TABLE_OFFSET and netid in cls.ifindices:
155      return cls.ifindices[netid] + (-cls.AUTOCONF_TABLE_OFFSET)
156    else:
157      return netid
158
159  @staticmethod
160  def GetInterfaceName(netid):
161    return "nettest%d" % netid
162
163  @staticmethod
164  def RouterMacAddress(netid):
165    return "02:00:00:00:%02x:00" % netid
166
167  @staticmethod
168  def MyMacAddress(netid):
169    return "02:00:00:00:%02x:01" % netid
170
171  @staticmethod
172  def _RouterAddress(netid, version):
173    if version == 6:
174      return "fe80::%02x00" % netid
175    elif version == 4:
176      return "10.0.%d.1" % netid
177    else:
178      raise ValueError("Don't support IPv%s" % version)
179
180  @classmethod
181  def _MyIPv4Address(cls, netid):
182    return "10.0.%d.2" % netid
183
184  @classmethod
185  def _MyIPv6Address(cls, netid):
186    return net_test.GetLinkAddress(cls.GetInterfaceName(netid), False)
187
188  @classmethod
189  def MyAddress(cls, version, netid):
190    return {4: cls._MyIPv4Address(netid),
191            5: "::ffff:" + cls._MyIPv4Address(netid),
192            6: cls._MyIPv6Address(netid)}[version]
193
194  @classmethod
195  def MyLinkLocalAddress(cls, netid):
196    return net_test.GetLinkAddress(cls.GetInterfaceName(netid), True)
197
198  @staticmethod
199  def IPv6Prefix(netid):
200    return "2001:db8:%02x::" % netid
201
202  @staticmethod
203  def GetRandomDestination(prefix):
204    if "." in prefix:
205      return prefix + "%d.%d" % (random.randint(0, 31), random.randint(0, 255))
206    else:
207      return prefix + "%x:%x" % (random.randint(0, 65535),
208                                 random.randint(0, 65535))
209
210  def GetProtocolFamily(self, version):
211    return {4: AF_INET, 6: AF_INET6}[version]
212
213  @classmethod
214  def CreateTunInterface(cls, netid):
215    iface = cls.GetInterfaceName(netid)
216    f = open("/dev/net/tun", "r+b")
217    ifr = struct.pack("16sH", iface, IFF_TAP | IFF_NO_PI)
218    ifr += "\x00" * (40 - len(ifr))
219    fcntl.ioctl(f, TUNSETIFF, ifr)
220    # Give ourselves a predictable MAC address.
221    net_test.SetInterfaceHWAddr(iface, cls.MyMacAddress(netid))
222    # Disable DAD so we don't have to wait for it.
223    cls.SetSysctl("/proc/sys/net/ipv6/conf/%s/accept_dad" % iface, 0)
224    # Set accept_ra to 2, because that's what we use.
225    cls.SetSysctl("/proc/sys/net/ipv6/conf/%s/accept_ra" % iface, 2)
226    net_test.SetInterfaceUp(iface)
227    net_test.SetNonBlocking(f)
228    return f
229
230  @classmethod
231  def SendRA(cls, netid, retranstimer=None, reachabletime=0):
232    validity = 300                 # seconds
233    macaddr = cls.RouterMacAddress(netid)
234    lladdr = cls._RouterAddress(netid, 6)
235
236    if retranstimer is None:
237      # If no retrans timer was specified, pick one that's as long as the
238      # router lifetime. This ensures that no spurious ND retransmits
239      # will interfere with test expectations.
240      retranstimer = validity
241
242    # We don't want any routes in the main table. If the kernel doesn't support
243    # putting RA routes into per-interface tables, configure routing manually.
244    routerlifetime = validity if HAVE_AUTOCONF_TABLE else 0
245
246    ra = (scapy.Ether(src=macaddr, dst="33:33:00:00:00:01") /
247          scapy.IPv6(src=lladdr, hlim=255) /
248          scapy.ICMPv6ND_RA(reachabletime=reachabletime,
249                            retranstimer=retranstimer,
250                            routerlifetime=routerlifetime) /
251          scapy.ICMPv6NDOptSrcLLAddr(lladdr=macaddr) /
252          scapy.ICMPv6NDOptPrefixInfo(prefix=cls.IPv6Prefix(netid),
253                                      prefixlen=64,
254                                      L=1, A=1,
255                                      validlifetime=validity,
256                                      preferredlifetime=validity))
257    posix.write(cls.tuns[netid].fileno(), str(ra))
258
259  @classmethod
260  def _RunSetupCommands(cls, netid, is_add):
261    for version in [4, 6]:
262      # Find out how to configure things.
263      iface = cls.GetInterfaceName(netid)
264      ifindex = cls.ifindices[netid]
265      macaddr = cls.RouterMacAddress(netid)
266      router = cls._RouterAddress(netid, version)
267      table = cls._TableForNetid(netid)
268
269      # Set up routing rules.
270      if HAVE_UID_ROUTING:
271        start, end = cls.UidRangeForNetid(netid)
272        cls.iproute.UidRangeRule(version, is_add, start, end, table,
273                                 cls.PRIORITY_UID)
274      cls.iproute.OifRule(version, is_add, iface, table, cls.PRIORITY_OIF)
275      cls.iproute.FwmarkRule(version, is_add, netid, table,
276                             cls.PRIORITY_FWMARK)
277
278      # Configure routing and addressing.
279      #
280      # IPv6 uses autoconf for everything, except if per-device autoconf routing
281      # tables are not supported, in which case the default route (only) is
282      # configured manually. For IPv4 we have to manually configure addresses,
283      # routes, and neighbour cache entries (since we don't reply to ARP or ND).
284      #
285      # Since deleting addresses also causes routes to be deleted, we need to
286      # be careful with ordering or the delete commands will fail with ENOENT.
287      do_routing = (version == 4 or cls.AUTOCONF_TABLE_OFFSET is None)
288      if is_add:
289        if version == 4:
290          cls.iproute.AddAddress(cls._MyIPv4Address(netid), 24, ifindex)
291          cls.iproute.AddNeighbour(version, router, macaddr, ifindex)
292        if do_routing:
293          cls.iproute.AddRoute(version, table, "default", 0, router, ifindex)
294          if version == 6:
295            cls.iproute.AddRoute(version, table,
296                                 cls.IPv6Prefix(netid), 64, None, ifindex)
297      else:
298        if do_routing:
299          cls.iproute.DelRoute(version, table, "default", 0, router, ifindex)
300          if version == 6:
301            cls.iproute.DelRoute(version, table,
302                                 cls.IPv6Prefix(netid), 64, None, ifindex)
303        if version == 4:
304          cls.iproute.DelNeighbour(version, router, macaddr, ifindex)
305          cls.iproute.DelAddress(cls._MyIPv4Address(netid), 24, ifindex)
306
307  @classmethod
308  def SetDefaultNetwork(cls, netid):
309    table = cls._TableForNetid(netid) if netid else None
310    for version in [4, 6]:
311      is_add = table is not None
312      cls.iproute.DefaultRule(version, is_add, table, cls.PRIORITY_DEFAULT)
313
314  @classmethod
315  def ClearDefaultNetwork(cls):
316    cls.SetDefaultNetwork(None)
317
318  @classmethod
319  def GetSysctl(cls, sysctl):
320    return open(sysctl, "r").read()
321
322  @classmethod
323  def SetSysctl(cls, sysctl, value):
324    # Only save each sysctl value the first time we set it. This is so we can
325    # set it to arbitrary values multiple times and still write it back
326    # correctly at the end.
327    if sysctl not in cls.saved_sysctls:
328      cls.saved_sysctls[sysctl] = cls.GetSysctl(sysctl)
329    open(sysctl, "w").write(str(value) + "\n")
330
331  @classmethod
332  def SetIPv6SysctlOnAllIfaces(cls, sysctl, value):
333    for netid in cls.tuns:
334      iface = cls.GetInterfaceName(netid)
335      name = "/proc/sys/net/ipv6/conf/%s/%s" % (iface, sysctl)
336      cls.SetSysctl(name, value)
337
338  @classmethod
339  def _RestoreSysctls(cls):
340    for sysctl, value in cls.saved_sysctls.iteritems():
341      try:
342        open(sysctl, "w").write(value)
343      except IOError:
344        pass
345
346  @classmethod
347  def _ICMPRatelimitFilename(cls, version):
348    return "/proc/sys/net/" + {4: "ipv4/icmp_ratelimit",
349                               6: "ipv6/icmp/ratelimit"}[version]
350
351  @classmethod
352  def _SetICMPRatelimit(cls, version, limit):
353    cls.SetSysctl(cls._ICMPRatelimitFilename(version), limit)
354
355  @classmethod
356  def setUpClass(cls):
357    # This is per-class setup instead of per-testcase setup because shelling out
358    # to ip and iptables is slow, and because routing configuration doesn't
359    # change during the test.
360    cls.iproute = iproute.IPRoute()
361    cls.tuns = {}
362    cls.ifindices = {}
363    if HAVE_AUTOCONF_TABLE:
364      cls.SetSysctl(AUTOCONF_TABLE_SYSCTL, -1000)
365      cls.AUTOCONF_TABLE_OFFSET = -1000
366    else:
367      cls.AUTOCONF_TABLE_OFFSET = None
368
369    # Disable ICMP rate limits. These will be restored by _RestoreSysctls.
370    for version in [4, 6]:
371      cls._SetICMPRatelimit(version, 0)
372
373    for netid in cls.NETIDS:
374      cls.tuns[netid] = cls.CreateTunInterface(netid)
375      iface = cls.GetInterfaceName(netid)
376      cls.ifindices[netid] = net_test.GetInterfaceIndex(iface)
377
378      cls.SendRA(netid)
379      cls._RunSetupCommands(netid, True)
380
381    for version in [4, 6]:
382      cls.iproute.UnreachableRule(version, True, 1000)
383
384    # Uncomment to look around at interface and rule configuration while
385    # running in the background. (Once the test finishes running, all the
386    # interfaces and rules are gone.)
387    # time.sleep(30)
388
389  @classmethod
390  def tearDownClass(cls):
391    for version in [4, 6]:
392      try:
393        cls.iproute.UnreachableRule(version, False, 1000)
394      except IOError:
395        pass
396
397    for netid in cls.tuns:
398      cls._RunSetupCommands(netid, False)
399      cls.tuns[netid].close()
400    cls._RestoreSysctls()
401
402  def setUp(self):
403    self.ClearTunQueues()
404
405  def SetSocketMark(self, s, netid):
406    if netid is None:
407      netid = 0
408    s.setsockopt(SOL_SOCKET, net_test.SO_MARK, netid)
409
410  def GetSocketMark(self, s):
411    return s.getsockopt(SOL_SOCKET, net_test.SO_MARK)
412
413  def ClearSocketMark(self, s):
414    self.SetSocketMark(s, 0)
415
416  def BindToDevice(self, s, iface):
417    if not iface:
418      iface = ""
419    s.setsockopt(SOL_SOCKET, SO_BINDTODEVICE, iface)
420
421  def SetUnicastInterface(self, s, ifindex):
422    # Otherwise, Python thinks it's a 1-byte option.
423    ifindex = struct.pack("!I", ifindex)
424
425    # Always set the IPv4 interface, because it will be used even on IPv6
426    # sockets if the destination address is a mapped address.
427    s.setsockopt(net_test.SOL_IP, IP_UNICAST_IF, ifindex)
428    if s.family == AF_INET6:
429      s.setsockopt(net_test.SOL_IPV6, IPV6_UNICAST_IF, ifindex)
430
431  def GetRemoteAddress(self, version):
432    return {4: self.IPV4_ADDR,
433            5: "::ffff:" + self.IPV4_ADDR,
434            6: self.IPV6_ADDR}[version]
435
436  def SelectInterface(self, s, netid, mode):
437    if mode == "uid":
438      raise ValueError("Can't change UID on an existing socket")
439    elif mode == "mark":
440      self.SetSocketMark(s, netid)
441    elif mode == "oif":
442      iface = self.GetInterfaceName(netid) if netid else ""
443      self.BindToDevice(s, iface)
444    elif mode == "ucast_oif":
445      self.SetUnicastInterface(s, self.ifindices.get(netid, 0))
446    else:
447      raise ValueError("Unknown interface selection mode %s" % mode)
448
449  def BuildSocket(self, version, constructor, netid, routing_mode):
450    s = constructor(self.GetProtocolFamily(version))
451
452    if routing_mode not in [None, "uid"]:
453      self.SelectInterface(s, netid, routing_mode)
454    elif routing_mode == "uid":
455      os.fchown(s.fileno(), self.UidForNetid(netid), -1)
456
457    return s
458
459  def SendOnNetid(self, version, s, dstaddr, dstport, netid, payload, cmsgs):
460    if netid is not None:
461      pktinfo = MakePktInfo(version, None, self.ifindices[netid])
462      cmsg_level, cmsg_name = {
463          4: (net_test.SOL_IP, IP_PKTINFO),
464          6: (net_test.SOL_IPV6, IPV6_PKTINFO)}[version]
465      cmsgs.append((cmsg_level, cmsg_name, pktinfo))
466    csocket.Sendmsg(s, (dstaddr, dstport), payload, cmsgs, csocket.MSG_CONFIRM)
467
468  def ReceiveEtherPacketOn(self, netid, packet):
469    posix.write(self.tuns[netid].fileno(), str(packet))
470
471  def ReceivePacketOn(self, netid, ip_packet):
472    routermac = self.RouterMacAddress(netid)
473    mymac = self.MyMacAddress(netid)
474    packet = scapy.Ether(src=routermac, dst=mymac) / ip_packet
475    self.ReceiveEtherPacketOn(netid, packet)
476
477  def ReadAllPacketsOn(self, netid, include_multicast=False):
478    packets = []
479    while True:
480      try:
481        packet = posix.read(self.tuns[netid].fileno(), 4096)
482        if not packet:
483          break
484        ether = scapy.Ether(packet)
485        # Multicast frames are frames where the first byte of the destination
486        # MAC address has 1 in the least-significant bit.
487        if include_multicast or not int(ether.dst.split(":")[0], 16) & 0x1:
488          packets.append(ether.payload)
489      except OSError, e:
490        # EAGAIN means there are no more packets waiting.
491        if re.match(e.message, os.strerror(errno.EAGAIN)):
492          break
493        # Anything else is unexpected.
494        else:
495          raise e
496    return packets
497
498  def ClearTunQueues(self):
499    # Keep reading packets on all netids until we get no packets on any of them.
500    waiting = None
501    while waiting != 0:
502      waiting = sum(len(self.ReadAllPacketsOn(netid)) for netid in self.NETIDS)
503
504  def assertPacketMatches(self, expected, actual):
505    # The expected packet is just a rough sketch of the packet we expect to
506    # receive. For example, it doesn't contain fields we can't predict, such as
507    # initial TCP sequence numbers, or that depend on the host implementation
508    # and settings, such as TCP options. To check whether the packet matches
509    # what we expect, instead of just checking all the known fields one by one,
510    # we blank out fields in the actual packet and then compare the whole
511    # packets to each other as strings. Because we modify the actual packet,
512    # make a copy here.
513    actual = actual.copy()
514
515    # Blank out IPv4 fields that we can't predict, like ID and the DF bit.
516    actualip = actual.getlayer("IP")
517    expectedip = expected.getlayer("IP")
518    if actualip and expectedip:
519      actualip.id = expectedip.id
520      actualip.flags &= 5
521      actualip.chksum = None  # Change the header, recalculate the checksum.
522
523    # Blank out the flow label, since new kernels randomize it by default.
524    actualipv6 = actual.getlayer("IPv6")
525    expectedipv6 = expected.getlayer("IPv6")
526    if actualipv6 and expectedipv6:
527      actualipv6.fl = expectedipv6.fl
528
529    # Blank out UDP fields that we can't predict (e.g., the source port for
530    # kernel-originated packets).
531    actualudp = actual.getlayer("UDP")
532    expectedudp = expected.getlayer("UDP")
533    if actualudp and expectedudp:
534      if expectedudp.sport is None:
535        actualudp.sport = None
536        actualudp.chksum = None
537
538    # Since the TCP code below messes with options, recalculate the length.
539    if actualip:
540      actualip.len = None
541    if actualipv6:
542      actualipv6.plen = None
543
544    # Blank out TCP fields that we can't predict.
545    actualtcp = actual.getlayer("TCP")
546    expectedtcp = expected.getlayer("TCP")
547    if actualtcp and expectedtcp:
548      actualtcp.dataofs = expectedtcp.dataofs
549      actualtcp.options = expectedtcp.options
550      actualtcp.window = expectedtcp.window
551      if expectedtcp.sport is None:
552        actualtcp.sport = None
553      if expectedtcp.seq is None:
554        actualtcp.seq = None
555      if expectedtcp.ack is None:
556        actualtcp.ack = None
557      actualtcp.chksum = None
558
559    # Serialize the packet so that expected packet fields that are only set when
560    # a packet is serialized e.g., the checksum) are filled in.
561    expected_real = expected.__class__(str(expected))
562    actual_real = actual.__class__(str(actual))
563    # repr() can be expensive. Call it only if the test is going to fail and we
564    # want to see the error.
565    if expected_real != actual_real:
566      self.assertEquals(repr(expected_real), repr(actual_real))
567
568  def PacketMatches(self, expected, actual):
569    try:
570      self.assertPacketMatches(expected, actual)
571      return True
572    except AssertionError:
573      return False
574
575  def ExpectNoPacketsOn(self, netid, msg):
576    packets = self.ReadAllPacketsOn(netid)
577    if packets:
578      firstpacket = repr(packets[0])
579    else:
580      firstpacket = ""
581    self.assertFalse(packets, msg + ": unexpected packet: " + firstpacket)
582
583  def ExpectPacketOn(self, netid, msg, expected):
584    # To avoid confusion due to lots of ICMPv6 ND going on all the time, drop
585    # multicast packets unless the packet we expect to see is a multicast
586    # packet. For now the only tests that use this are IPv6.
587    ipv6 = expected.getlayer("IPv6")
588    if ipv6 and ipv6.dst.startswith("ff"):
589      include_multicast = True
590    else:
591      include_multicast = False
592
593    packets = self.ReadAllPacketsOn(netid, include_multicast=include_multicast)
594    self.assertTrue(packets, msg + ": received no packets")
595
596    # If we receive a packet that matches what we expected, return it.
597    for packet in packets:
598      if self.PacketMatches(expected, packet):
599        return packet
600
601    # None of the packets matched. Call assertPacketMatches to output a diff
602    # between the expected packet and the last packet we received. In theory,
603    # we'd output a diff to the packet that's the best match for what we
604    # expected, but this is good enough for now.
605    try:
606      self.assertPacketMatches(expected, packets[-1])
607    except Exception, e:
608      raise UnexpectedPacketError(
609          "%s: diff with last packet:\n%s" % (msg, e.message))
610
611  def Combinations(self, version):
612    """Produces a list of combinations to test."""
613    combinations = []
614
615    # Check packets addressed to the IP addresses of all our interfaces...
616    for dest_ip_netid in self.tuns:
617      ip_if = self.GetInterfaceName(dest_ip_netid)
618      myaddr = self.MyAddress(version, dest_ip_netid)
619      remoteaddr = self.GetRemoteAddress(version)
620
621      # ... coming in on all our interfaces.
622      for netid in self.tuns:
623        iif = self.GetInterfaceName(netid)
624        combinations.append((netid, iif, ip_if, myaddr, remoteaddr))
625
626    return combinations
627
628  def _FormatMessage(self, iif, ip_if, extra, desc, reply_desc):
629    msg = "Receiving %s on %s to %s IP, %s" % (desc, iif, ip_if, extra)
630    if reply_desc:
631      msg += ": Expecting %s on %s" % (reply_desc, iif)
632    else:
633      msg += ": Expecting no packets on %s" % iif
634    return msg
635
636  def _ReceiveAndExpectResponse(self, netid, packet, reply, msg):
637    self.ReceivePacketOn(netid, packet)
638    if reply:
639      return self.ExpectPacketOn(netid, msg, reply)
640    else:
641      self.ExpectNoPacketsOn(netid, msg)
642      return None
643