// // NodeList.cpp // libraries/networking/src // // Created by Stephen Birarda on 2/15/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include "AccountManager.h" #include "AddressManager.h" #include "Assignment.h" #include "HifiSockAddr.h" #include "JSONBreakableMarshal.h" #include "NodeList.h" #include "PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" #include "NetworkLogging.h" NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned short dtlsListenPort) : LimitedNodeList(socketListenPort, dtlsListenPort), _ownerType(newOwnerType), _nodeTypesOfInterest(), _domainHandler(this), _numNoReplyDomainCheckIns(0), _assignmentServerSocket() { static bool firstCall = true; if (firstCall) { NodeType::init(); // register the SharedNodePointer meta-type for signals/slots qRegisterMetaType(); firstCall = false; } auto addressManager = DependencyManager::get(); // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, &_domainHandler, &DomainHandler::setHostnameAndPort); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); // handle a request for a path change from the AddressManager connect(addressManager.data(), &AddressManager::pathChangeRequired, this, &NodeList::handleDSPathQuery); // in case we don't know how to talk to DS when a path change is requested // fire off any pending DS path query when we get socket information connect(&_domainHandler, &DomainHandler::completedSocketDiscovery, this, &NodeList::sendPendingDSPathQuery); // send a domain server check in immediately once the DS socket is known connect(&_domainHandler, &DomainHandler::completedSocketDiscovery, this, &NodeList::sendDomainServerCheckIn); // send a domain server check in immediately if there is a public socket change connect(this, &LimitedNodeList::publicSockAddrChanged, this, &NodeList::sendDomainServerCheckIn); // clear our NodeList when the domain changes connect(&_domainHandler, &DomainHandler::disconnectedFromDomain, this, &NodeList::reset); // send an ICE heartbeat as soon as we get ice server information connect(&_domainHandler, &DomainHandler::iceSocketAndIDReceived, this, &NodeList::handleICEConnectionToDomainServer); // handle ping timeout from DomainHandler to establish a connection with auto networked domain-server connect(&_domainHandler.getICEPeer(), &NetworkPeer::pingTimerTimeout, this, &NodeList::pingPunchForDomainServer); // send a ping punch immediately connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); // clear out NodeList when login is finished connect(&AccountManager::getInstance(), &AccountManager::loginComplete , this, &NodeList::reset); // clear our NodeList when logout is requested connect(&AccountManager::getInstance(), &AccountManager::logoutComplete , this, &NodeList::reset); // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); // we definitely want STUN to update our public socket, so call the LNL to kick that off startSTUNPublicSocketUpdate(); } qint64 NodeList::sendStats(const QJsonObject& statsObject, const HifiSockAddr& destination) { QByteArray statsPacket(MAX_PACKET_SIZE, 0); int numBytesForPacketHeader = populatePacketHeader(statsPacket, PacketTypeNodeJsonStats); // get a QStringList using JSONBreakableMarshal QStringList statsStringList = JSONBreakableMarshal::toStringList(statsObject, ""); int numBytesWritten = numBytesForPacketHeader; // enumerate the resulting strings - pack them and send off packets once we hit MTU size foreach(const QString& statsItem, statsStringList) { QByteArray utf8String = statsItem.toUtf8(); utf8String.append('\0'); if (numBytesWritten + utf8String.size() > MAX_PACKET_SIZE) { // send off the current packet since the next string will make us too big statsPacket.resize(numBytesWritten); writeUnverifiedDatagram(statsPacket, destination); // reset the number of bytes written to the size of our packet header numBytesWritten = numBytesForPacketHeader; } // write this string into the stats packet statsPacket.replace(numBytesWritten, utf8String.size(), utf8String); // keep track of the number of bytes we have written numBytesWritten += utf8String.size(); } if (numBytesWritten > numBytesForPacketHeader) { // always send the last packet, if it has data statsPacket.resize(numBytesWritten); writeUnverifiedDatagram(statsPacket, destination); } // enumerate the resulting strings, breaking them into MTU sized packets return 0; } qint64 NodeList::sendStatsToDomainServer(const QJsonObject& statsObject) { return sendStats(statsObject, _domainHandler.getSockAddr()); } void NodeList::timePingReply(const QByteArray& packet, const SharedNodePointer& sendingNode) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); quint8 pingType; quint64 ourOriginalTime, othersReplyTime; packetStream >> pingType >> ourOriginalTime >> othersReplyTime; quint64 now = usecTimestampNow(); int pingTime = now - ourOriginalTime; int oneWayFlightTime = pingTime / 2; // half of the ping is our one way flight // The other node's expected time should be our original time plus the one way flight time // anything other than that is clock skew quint64 othersExprectedReply = ourOriginalTime + oneWayFlightTime; int clockSkew = othersReplyTime - othersExprectedReply; sendingNode->setPingMs(pingTime / 1000); sendingNode->updateClockSkewUsec(clockSkew); const bool wantDebug = false; if (wantDebug) { qCDebug(networking) << "PING_REPLY from node " << *sendingNode << "\n" << " now: " << now << "\n" << " ourTime: " << ourOriginalTime << "\n" << " pingTime: " << pingTime << "\n" << " oneWayFlightTime: " << oneWayFlightTime << "\n" << " othersReplyTime: " << othersReplyTime << "\n" << " othersExprectedReply: " << othersExprectedReply << "\n" << " clockSkew: " << clockSkew << "\n" << " average clockSkew: " << sendingNode->getClockSkewUsec(); } } void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteArray& packet) { PacketType packetType = packetTypeForPacket(packet); switch (packetType) { case PacketTypeDomainList: case PacketTypeDomainServerAddedNode: { if (!_domainHandler.getSockAddr().isNull()) { // only process a packet from domain-server if we're talking to a domain // TODO: how do we make sure this is actually the domain we want the list from (DTLS probably) if (packetType == PacketTypeDomainList) { processDomainServerList(packet); } else if (packetType == PacketTypeDomainServerAddedNode) { processDomainServerAddedNode(packet); } } break; } case PacketTypeDomainServerRequireDTLS: { _domainHandler.parseDTLSRequirementPacket(packet); break; } case PacketTypeIceServerPeerInformation: { if (!_domainHandler.getICEPeer().hasSockets()) { _domainHandler.processICEResponsePacket(packet); } break; } case PacketTypePing: { // send back a reply SharedNodePointer matchingNode = sendingNodeForPacket(packet); if (matchingNode) { matchingNode->setLastHeardMicrostamp(usecTimestampNow()); QByteArray replyPacket = constructPingReplyPacket(packet); writeDatagram(replyPacket, matchingNode, senderSockAddr); // If we don't have a symmetric socket for this node and this socket doesn't match // what we have for public and local then set it as the symmetric. // This allows a server on a reachable port to communicate with nodes on symmetric NATs if (matchingNode->getSymmetricSocket().isNull()) { if (senderSockAddr != matchingNode->getLocalSocket() && senderSockAddr != matchingNode->getPublicSocket()) { matchingNode->setSymmetricSocket(senderSockAddr); } } } break; } case PacketTypePingReply: { SharedNodePointer sendingNode = sendingNodeForPacket(packet); if (sendingNode) { sendingNode->setLastHeardMicrostamp(usecTimestampNow()); // activate the appropriate socket for this node, if not yet updated activateSocketFromNodeCommunication(packet, sendingNode); // set the ping time for this node for stat collection timePingReply(packet, sendingNode); } break; } case PacketTypeUnverifiedPing: { // send back a reply QByteArray replyPacket = constructPingReplyPacket(packet, _domainHandler.getICEClientID()); writeUnverifiedDatagram(replyPacket, senderSockAddr); break; } case PacketTypeUnverifiedPingReply: { qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr; if (_domainHandler.getIP().isNull()) { // for now we're unsafely assuming this came back from the domain if (senderSockAddr == _domainHandler.getICEPeer().getLocalSocket()) { qCDebug(networking) << "Connecting to domain using local socket"; _domainHandler.activateICELocalSocket(); } else if (senderSockAddr == _domainHandler.getICEPeer().getPublicSocket()) { qCDebug(networking) << "Conecting to domain using public socket"; _domainHandler.activateICEPublicSocket(); } else { qCDebug(networking) << "Reply does not match either local or public socket for domain. Will not connect."; } } } case PacketTypeStunResponse: { // a STUN packet begins with 00, we've checked the second zero with packetVersionMatch // pass it along so it can be processed into our public address and port processSTUNResponse(packet); break; } case PacketTypeDomainServerPathResponse: { handleDSPathQueryResponse(packet); break; } default: LimitedNodeList::processNodeData(senderSockAddr, packet); break; } } void NodeList::reset() { LimitedNodeList::reset(); _numNoReplyDomainCheckIns = 0; // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); if (sender() != &_domainHandler) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); } // if we setup the DTLS socket, also disconnect from the DTLS socket readyRead() so it can handle handshaking if (_dtlsSocket) { disconnect(_dtlsSocket, 0, this, 0); } } void NodeList::addNodeTypeToInterestSet(NodeType_t nodeTypeToAdd) { _nodeTypesOfInterest << nodeTypeToAdd; } void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes) { _nodeTypesOfInterest.unite(setOfNodeTypes); } void NodeList::sendDomainServerCheckIn() { if (_publicSockAddr.isNull()) { // we don't know our public socket and we need to send it to the domain server qCDebug(networking) << "Waiting for inital public socket from STUN. Will not send domain-server check in."; } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) { qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in."; handleICEConnectionToDomainServer(); } else if (!_domainHandler.getIP().isNull()) { bool isUsingDTLS = false; PacketType domainPacketType = !_domainHandler.isConnected() ? PacketTypeDomainConnectRequest : PacketTypeDomainListRequest; if (!_domainHandler.isConnected()) { qCDebug(networking) << "Sending connect request to domain-server at" << _domainHandler.getHostname(); // is this our localhost domain-server? // if so we need to make sure we have an up-to-date local port in case it restarted if (_domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost || _domainHandler.getHostname() == "localhost") { quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT; getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, domainPort); qCDebug(networking) << "Local domain-server port read from shared memory (or default) is" << domainPort; _domainHandler.setPort(domainPort); } } // construct the DS check in packet QUuid packetUUID = _sessionUUID; if (domainPacketType == PacketTypeDomainConnectRequest) { if (!_domainHandler.getAssignmentUUID().isNull()) { // this is a connect request and we're an assigned node // so set our packetUUID as the assignment UUID packetUUID = _domainHandler.getAssignmentUUID(); } else if (_domainHandler.requiresICE()) { // this is a connect request and we're an interface client // that used ice to discover the DS // so send our ICE client UUID with the connect request packetUUID = _domainHandler.getICEClientID(); } } QByteArray domainServerPacket = byteArrayWithUUIDPopulatedHeader(domainPacketType, packetUUID); QDataStream packetStream(&domainServerPacket, QIODevice::Append); // pack our data to send to the domain-server packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); // if this is a connect request, and we can present a username signature, send it along if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = AccountManager::getInstance().getAccountInfo(); packetStream << accountInfo.getUsername(); const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature(); if (!usernameSignature.isEmpty()) { qCDebug(networking) << "Including username signature in domain connect request."; packetStream << usernameSignature; } } flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn); if (!isUsingDTLS) { writeUnverifiedDatagram(domainServerPacket, _domainHandler.getSockAddr()); } if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS // so emit our signal that says that emit limitOfSilentDomainCheckInsReached(); } // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } } void NodeList::handleDSPathQuery(const QString& newPath) { if (_domainHandler.isSocketKnown()) { // if we have a DS socket we assume it will get this packet and send if off right away sendDSPathQuery(newPath); } else { // otherwise we make it pending so that it can be sent once a connection is established _domainHandler.setPendingPath(newPath); } } void NodeList::sendPendingDSPathQuery() { QString pendingPath = _domainHandler.getPendingPath(); if (!pendingPath.isEmpty()) { qCDebug(networking) << "Attemping to send pending query to DS for path" << pendingPath; // this is a slot triggered if we just established a network link with a DS and want to send a path query sendDSPathQuery(_domainHandler.getPendingPath()); // clear whatever the pending path was _domainHandler.clearPendingPath(); } } void NodeList::sendDSPathQuery(const QString& newPath) { // only send a path query if we know who our DS is or is going to be if (_domainHandler.isSocketKnown()) { // construct the path query packet QByteArray pathQueryPacket = byteArrayWithPopulatedHeader(PacketTypeDomainServerPathQuery); // get the UTF8 representation of path query QByteArray pathQueryUTF8 = newPath.toUtf8(); // get the size of the UTF8 representation of the desired path quint16 numPathBytes = pathQueryUTF8.size(); if (pathQueryPacket.size() + numPathBytes + sizeof(numPathBytes) < MAX_PACKET_SIZE) { // append the size of the path to the query packet pathQueryPacket.append(reinterpret_cast(&numPathBytes), sizeof(numPathBytes)); // append the path itself to the query packet pathQueryPacket.append(pathQueryUTF8); qCDebug(networking) << "Sending a path query packet for path" << newPath << "to domain-server at" << _domainHandler.getSockAddr(); // send off the path query writeUnverifiedDatagram(pathQueryPacket, _domainHandler.getSockAddr()); } else { qCDebug(networking) << "Path" << newPath << "would make PacketTypeDomainServerPathQuery packet > MAX_PACKET_SIZE." << "Will not send query."; } } } void NodeList::handleDSPathQueryResponse(const QByteArray& packet) { // This is a response to a path query we theoretically made. // In the future we may want to check that this was actually from our DS and for a query we actually made. int numHeaderBytes = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainServerPathResponse); const char* startPosition = packet.data() + numHeaderBytes; const char* currentPosition = startPosition; // figure out how many bytes the path query is qint16 numPathBytes; memcpy(&numPathBytes, currentPosition, sizeof(numPathBytes)); currentPosition += sizeof(numPathBytes); // make sure it is safe to pull the path if (numPathBytes <= packet.size() - numHeaderBytes - (currentPosition - startPosition)) { // pull the path from the packet QString pathQuery = QString::fromUtf8(currentPosition, numPathBytes); currentPosition += numPathBytes; // figure out how many bytes the viewpoint is qint16 numViewpointBytes; memcpy(&numViewpointBytes, currentPosition, sizeof(numViewpointBytes)); currentPosition += sizeof(numViewpointBytes); // make sure it is safe to pull the viewpoint if (numViewpointBytes <= packet.size() - numHeaderBytes - (currentPosition - startPosition)) { // pull the viewpoint from the packet QString viewpoint = QString::fromUtf8(currentPosition, numViewpointBytes); // Hand it off to the AddressManager so it can handle it as a relative viewpoint if (DependencyManager::get()->goToViewpoint(viewpoint)) { qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery; } else { qCDebug(networking) << "Could not go to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery; } } } } void NodeList::handleICEConnectionToDomainServer() { // if we're still waiting to get sockets we want to ping for the domain-server // then send another heartbeat now if (!_domainHandler.getICEPeer().hasSockets()) { _domainHandler.getICEPeer().resetConnectionAttempts(); flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendICEServerQuery); LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), _domainHandler.getICEDomainID()); } } void NodeList::pingPunchForDomainServer() { // make sure if we're here that we actually still need to ping the domain-server if (_domainHandler.getIP().isNull() && _domainHandler.getICEPeer().hasSockets()) { // check if we've hit the number of pings we'll send to the DS before we consider it a fail const int NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET = 2000 / UDP_PUNCH_PING_INTERVAL_MS; if (_domainHandler.getICEPeer().getConnectionAttempts() > 0 && _domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat _domainHandler.getICEPeer().softReset(); handleICEConnectionToDomainServer(); } qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendPingsToDS); // send the ping packet to the local and public sockets for this node QByteArray localPingPacket = constructPingPacket(PingType::Local, false, _domainHandler.getICEClientID()); writeUnverifiedDatagram(localPingPacket, _domainHandler.getICEPeer().getLocalSocket()); QByteArray publicPingPacket = constructPingPacket(PingType::Public, false, _domainHandler.getICEClientID()); writeUnverifiedDatagram(publicPingPacket, _domainHandler.getICEPeer().getPublicSocket()); _domainHandler.getICEPeer().incrementConnectionAttempts(); } } int NodeList::processDomainServerList(const QByteArray& packet) { // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); // if this was the first domain-server list from this domain, we've now connected if (!_domainHandler.isConnected()) { _domainHandler.setUUID(uuidFromPacketHeader(packet)); _domainHandler.setIsConnected(true); } int readNodes = 0; QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); // pull our owner UUID from the packet, it's always the first thing QUuid newUUID; packetStream >> newUUID; setSessionUUID(newUUID); bool thisNodeCanAdjustLocks; packetStream >> thisNodeCanAdjustLocks; setThisNodeCanAdjustLocks(thisNodeCanAdjustLocks); bool thisNodeCanRez; packetStream >> thisNodeCanRez; setThisNodeCanRez(thisNodeCanRez); // pull each node in the packet while (packetStream.device()->pos() < packet.size()) { parseNodeFromPacketStream(packetStream); } return readNodes; } void NodeList::processDomainServerAddedNode(const QByteArray& packet) { // setup a QDataStream, skip the header QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); // use our shared method to pull out the new node parseNodeFromPacketStream(packetStream); } void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { // setup variables to read into from QDataStream qint8 nodeType; QUuid nodeUUID, connectionUUID; HifiSockAddr nodePublicSocket, nodeLocalSocket; bool canAdjustLocks; bool canRez; packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> canAdjustLocks >> canRez; // if the public socket address is 0 then it's reachable at the same IP // as the domain server if (nodePublicSocket.getAddress().isNull()) { nodePublicSocket.setAddress(_domainHandler.getIP()); } packetStream >> connectionUUID; SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, nodeLocalSocket, canAdjustLocks, canRez, connectionUUID); } void NodeList::sendAssignment(Assignment& assignment) { PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand ? PacketTypeCreateAssignment : PacketTypeRequestAssignment; QByteArray packet = byteArrayWithPopulatedHeader(assignmentPacketType); QDataStream packetStream(&packet, QIODevice::Append); packetStream << assignment; _nodeSocket.writeDatagram(packet, _assignmentServerSocket.getAddress(), _assignmentServerSocket.getPort()); } void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) { if (node->getType() == NodeType::AudioMixer) { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendAudioPing); } // every second we're trying to ping this node and we're not getting anywhere - debug that out const int NUM_DEBUG_CONNECTION_ATTEMPTS = 1000 / (UDP_PUNCH_PING_INTERVAL_MS); if (node->getConnectionAttempts() > 0 && node->getConnectionAttempts() % NUM_DEBUG_CONNECTION_ATTEMPTS == 0) { qCDebug(networking) << "No response to UDP hole punch pings for node" << node->getUUID() << "in last second."; } // send the ping packet to the local and public sockets for this node QByteArray localPingPacket = constructPingPacket(PingType::Local); writeDatagram(localPingPacket, node, node->getLocalSocket()); QByteArray publicPingPacket = constructPingPacket(PingType::Public); writeDatagram(publicPingPacket, node, node->getPublicSocket()); if (!node->getSymmetricSocket().isNull()) { QByteArray symmetricPingPacket = constructPingPacket(PingType::Symmetric); writeDatagram(symmetricPingPacket, node, node->getSymmetricSocket()); } node->incrementConnectionAttempts(); } void NodeList::startNodeHolePunch(const SharedNodePointer& node) { // connect to the correct signal on this node so we know when to ping it connect(node.data(), &Node::pingTimerTimeout, this, &NodeList::handleNodePingTimeout); // start the ping timer for this node node->startPingTimer(); // ping this node immediately pingPunchForInactiveNode(node); } void NodeList::handleNodePingTimeout() { SharedNodePointer senderNode = nodeWithUUID(qobject_cast(sender())->getUUID()); if (senderNode && !senderNode->getActiveSocket()) { pingPunchForInactiveNode(senderNode); } } void NodeList::activateSocketFromNodeCommunication(const QByteArray& packet, const SharedNodePointer& sendingNode) { // deconstruct this ping packet to see if it is a public or local reply QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); quint8 pingType; packetStream >> pingType; // if this is a local or public ping then we can activate a socket // we do nothing with agnostic pings, those are simply for timing if (pingType == PingType::Local && sendingNode->getActiveSocket() != &sendingNode->getLocalSocket()) { sendingNode->activateLocalSocket(); } else if (pingType == PingType::Public && !sendingNode->getActiveSocket()) { sendingNode->activatePublicSocket(); } else if (pingType == PingType::Symmetric && !sendingNode->getActiveSocket()) { sendingNode->activateSymmetricSocket(); } if (sendingNode->getType() == NodeType::AudioMixer) { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SetAudioMixerSocket); } }