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