def send_multicast_beacons(self, interfaces, beacon_type='solicitation', verbose=False): """Sends out multicast beacons on each interface in `interfaces`. :param interfaces: The output of `get_all_interfaces_definition()`. :param beacon_type: Type of beacon to send. (Default: 'solicitation'.) :param verbose: If True, will log the payload of each beacon sent. """ for ifname, ifdata in interfaces.items(): log.msg("Sending multicast beacon on '%s'." % ifname) if not ifdata['enabled']: continue remote = interface_info_to_beacon_remote_payload(ifname, ifdata) # We'll make slight adjustments to the beacon payload depending # on the configured source subnet (if any), but the basic payload # is ready. payload = {"remote": remote} links = ifdata["links"] if len(links) == 0: # No configured links, so try sending out a link-local IPv6 # multicast beacon. beacon = create_beacon_payload(beacon_type, payload) if verbose: log.msg("Beacon payload:\n%s" % pformat(beacon.payload)) self.send_multicast_beacon(ifdata["index"], beacon) continue sent_ipv6 = False for link in ifdata["links"]: subnet = link["address"] remote['subnet'] = subnet address = subnet.split("/")[0] beacon = create_beacon_payload(beacon_type, payload) if verbose: log.msg("Beacon payload:\n%s" % pformat(beacon.payload)) if ':' not in address: # An IPv4 socket requires the source address to be the # IPv4 address assigned to the interface. self.send_multicast_beacon(address, beacon) else: # An IPv6 socket requires the source address to be the # interface index. self.send_multicast_beacon(ifdata["index"], beacon) sent_ipv6 = True if not sent_ipv6: remote['subnet'] = None beacon = create_beacon_payload(beacon_type, payload) self.send_multicast_beacon(ifdata["index"], beacon)
def do_beaconing(args, interfaces=None): """Sends out beacons based on the given arguments, and waits for replies. :param args: The command-line arguments. :param interfaces: The interfaces to send out beacons on. Must be the result of `get_all_interfaces_definition()`. """ if args.source is None: source_ip = '::' else: source_ip = args.source protocol = BeaconingSocketProtocol(reactor, process_incoming=True, debug=True, interface=source_ip, port=args.port, interfaces=interfaces) if args.destination is None: destination_ip = "::ffff:" + BEACON_IPV4_MULTICAST elif ':' not in args.destination: destination_ip = "::ffff:" + args.destination else: destination_ip = args.destination if "224.0.0.118" in destination_ip: protocol.send_multicast_beacons(interfaces, verbose=args.verbose) else: log.msg("Sending unicast beacon to '%s'..." % destination_ip) beacon = create_beacon_payload("solicitation") protocol.send_beacon(beacon, (destination_ip, BEACON_PORT)) reactor.callLater(args.timeout, lambda: reactor.stop()) reactor.run() return protocol
def test__succeeds_when_shared_secret_present(self): self.write_secret() beacon = create_beacon_payload("solicitation", payload={}) self.assertThat(beacon.type, Equals("solicitation")) self.assertThat( beacon.payload["type"], Equals(BEACON_TYPES["solicitation"]) )
def test__creates_packet_that_can_decode(self): self.write_secret() random_type = random.choice(list(BEACON_TYPES.keys())) random_key = factory.make_string(prefix="_") random_value = factory.make_string() packet_bytes, _, _, _ = create_beacon_payload( random_type, payload={random_key: random_value}) decrypted = read_beacon_payload(packet_bytes) self.assertThat(decrypted.type, Equals(random_type)) self.assertThat(decrypted.payload[random_key], Equals(random_value))
def test__supplements_data_and_returns_complete_data(self): self.write_secret() random_type = random.choice(list(BEACON_TYPES.keys())) random_key = factory.make_string(prefix="_") random_value = factory.make_string() beacon = create_beacon_payload(random_type, payload={random_key: random_value}) # Ensure a valid UUID was added. self.assertIsNotNone(UUID(beacon.payload['uuid'])) self.assertThat(beacon.type, Equals(random_type)) # The type is replicated here for authentication purposes. self.assertThat(beacon.payload['type'], Equals(BEACON_TYPES[random_type])) self.assertThat(beacon.payload[random_key], Equals(random_value))
def test__send_multicast_beacon_sets_ipv4_source(self): # Note: Always use a random port for testing. (port=0) protocol = BeaconingSocketProtocol(reactor, port=0, process_incoming=True, loopback=True, interface="::", debug=False) self.assertThat(protocol.listen_port, Not(Is(None))) listen_port = protocol.listen_port._realPortNumber self.write_secret() beacon = create_beacon_payload("advertisement", {}) protocol.send_multicast_beacon("127.0.0.1", beacon, port=listen_port) # Verify that we received the packet. yield wait_for_rx_packets(protocol, 1) yield protocol.stopProtocol()
def test__sends_and_receives_unicast_beacons(self): # Note: Always use a random port for testing. (port=0) logger = self.useFixture(TwistedLoggerFixture()) protocol = BeaconingSocketProtocol( reactor, port=0, process_incoming=True, loopback=True, interface="::", debug=True, ) self.assertThat(protocol.listen_port, Not(Is(None))) listen_port = protocol.listen_port._realPortNumber self.write_secret() beacon = create_beacon_payload("solicitation", {}) rx_uuid = beacon.payload["uuid"] destination = random.choice(["::ffff:127.0.0.1", "::1"]) protocol.send_beacon(beacon, (destination, listen_port)) # Pretend we didn't send this packet. Otherwise we won't reply to it. # We have to do this now, before the reactor runs again. transmitted = protocol.tx_queue.pop(rx_uuid, None) # Since we've instructed the protocol to loop back packets for testing, # it should have sent a multicast solicitation, received it back, sent # an advertisement, then received it back. So we'll wait for two # packets to be sent. yield wait_for_rx_packets(protocol, 2) # Grab the beacon we know we transmitted and then received. received = protocol.rx_queue.pop(rx_uuid, None) self.assertThat(transmitted, Equals(beacon)) self.assertThat(received[0].json["payload"]["uuid"], Equals(rx_uuid)) # Grab the subsequent packets from the queues. transmitted = protocol.tx_queue.popitem()[1] received = protocol.rx_queue.popitem()[1] # We should have received a second packet to ack the first beacon. self.assertThat(received[0].json["payload"]["acks"], Equals(rx_uuid)) # We should have transmitted an advertisement in response to the # solicitation. self.assertThat(transmitted.type, Equals("advertisement")) # This tests that the post gets closed properly; otherwise the test # suite will complain about things left in the reactor. yield protocol.stopProtocol() # In debug mode, the logger should have printed each packet. self.assertThat( logger.output, DocTestMatches("...Beacon received:...Own beacon received:..."), )
def beaconReceived(self, beacon_json): """Called whenever a beacon is received. This method is responsible for updating the `tx_queue` and `rx_queue` data structures, and determining if the incoming beacon is meaningful for determining network topology. :param beacon_json: The normalized beacon JSON, which can come either from the external tcpdump-based process, or from the sockets layer (with less information about the received packet). """ rx_uuid = beacon_json.get('payload', {}).get("uuid") if rx_uuid is None: if self.debug is True: log.msg("Rejecting incoming beacon: no UUID found: \n%s" % (pformat(beacon_json))) return own_beacon = False if self.tx_queue.get(rx_uuid): own_beacon = True is_dup = self.remember_beacon_and_check_duplicate(rx_uuid, beacon_json) if self.debug is True: log.msg("%s %sreceived:\n%s" % ("Own beacon" if own_beacon else "Beacon", "(duplicate) " if is_dup else "", beacon_json)) # From what we know so far, we can infer some facts about the network. # (1) If we received our own beacon, that means the interface we sent # the packet out on is on the same fabric as the interface that # received it. # (2) If we receive a duplicate beacon on two different interfaces, # that means those two interfaces are on the same fabric. reply_ip = beacon_json['source_ip'] reply_port = beacon_json['source_port'] if ':' not in reply_ip: # Since we opened an IPv6-compatible socket, need IPv6 syntax # here to send to IPv4 addresses. reply_ip = '::ffff:' + reply_ip reply_address = (reply_ip, reply_port) beacon_type = beacon_json['type'] if beacon_type == "solicitation": receive_interface_info = self.get_receive_interface_info( beacon_json) payload = {"interface": receive_interface_info, "acks": rx_uuid} reply = create_beacon_payload("advertisement", payload) self.send_beacon(reply, reply_address)
def test__send_multicast_beacon_sets_ipv6_source(self): # Due to issues beyond my control, this test doesn't do what I expected # it to do. But it's still useful for code coverage (to make sure no # blatant exceptions occur in the IPv6 path). # self.skipTest( # "IPv6 loopback multicast isn't working, for whatever reason.") # Since we can't test IPv6 multicast on the loopback interface, another # method can be used to verify that it's working: # (1) sudo tcpdump -i <physical-interface> 'udp and port == 5240' # (2) bin/maas-rack send-beacons -p 5240 # Verifying IPv6 (and IPv4) multicast group join behavior can be # validated by doing something like: # (1) bin/maas-rack send-beacons -t 600 # (the high timeout will cause it to wait for 10 minutes) # (2) ip maddr show | egrep 'ff02::15a|224.0.0.118|$' # The expected result from command (2) will be that 'egrep' will # highlight the MAAS multicast groups in red text. Any Ethernet # interface with an assigned IPv4 address should have joined the # 224.0.0.118 group. All Ethernet interfaces should have joined the # 'ff02::15a' group. # Note: Always use a random port for testing. (port=0) protocol = BeaconingSocketProtocol( reactor, port=0, process_incoming=True, loopback=True, interface="::", debug=False, ) self.assertThat(protocol.listen_port, Not(Is(None))) listen_port = protocol.listen_port._realPortNumber self.write_secret() beacon = create_beacon_payload("advertisement", {}) # The loopback interface ifindex should always be 1; this is saying # to send an IPv6 multicast on ifIndex == 1. protocol.send_multicast_beacon(1, beacon, port=listen_port) # Instead of skipping the test, just don't expect to receive anything. # yield wait_for_rx_packets(protocol, 1) yield protocol.stopProtocol()
def test__returns_beaconpayload_namedtuple(self): beacon = create_beacon_payload("solicitation") self.assertThat(beacon.bytes, IsInstance(bytes)) self.assertThat(beacon.payload, Is(None)) self.assertThat(beacon.type, Equals("solicitation")) self.assertThat(beacon.version, Equals(1))
def test__requires_maas_shared_secret_for_inner_data_payload(self): with ExpectedException(MissingSharedSecret, ".*shared secret not found.*"): create_beacon_payload("solicitation", payload={})
def test__is_valid__succeeds_for_valid_payload(self): beacon = create_beacon_payload("solicitation") beacon_packet = BeaconingPacket(beacon.bytes) self.assertTrue(beacon_packet.valid)