All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.lastbamboo.common.ice.IceStunConnectivityCheckerImpl Maven / Gradle / Ivy

package org.lastbamboo.common.ice;

import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Map;

import org.apache.commons.id.uuid.UUID;
import org.lastbamboo.common.ice.candidate.IceCandidate;
import org.lastbamboo.common.ice.candidate.IceCandidatePair;
import org.lastbamboo.common.ice.candidate.IceCandidatePairState;
import org.lastbamboo.common.stun.client.StunClientMessageVisitor;
import org.littleshoot.stun.stack.message.BindingErrorResponse;
import org.littleshoot.stun.stack.message.BindingRequest;
import org.littleshoot.stun.stack.message.BindingSuccessResponse;
import org.littleshoot.stun.stack.message.StunMessage;
import org.littleshoot.stun.stack.message.attributes.StunAttribute;
import org.littleshoot.stun.stack.message.attributes.StunAttributeType;
import org.littleshoot.stun.stack.message.attributes.ice.IceControlledAttribute;
import org.littleshoot.stun.stack.message.attributes.ice.IceControllingAttribute;
import org.littleshoot.stun.stack.transaction.StunTransactionTracker;
import org.littleshoot.mina.common.IoSession;
import org.littleshoot.mina.common.TransportType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Processes STUN connectivity checks for ICE. See the 
 * "7.2.  STUN Server Procedures" section at:

* * http://tools.ietf.org/html/rfc5245#section-7.2 * * @param The type STUN message visitor methods return. */ public final class IceStunConnectivityCheckerImpl extends StunClientMessageVisitor { private final Logger m_log = LoggerFactory.getLogger(getClass()); private final IceAgent m_agent; private final IceMediaStream m_iceMediaStream; private final IoSession m_ioSession; private final ExistingSessionIceCandidatePairFactory m_candidatePairFactory; private final IceBindingRequestTracker m_bindingRequestTracker; /** * Creates a new message visitor for the specified session. * * @param agent The top-level ICE agent. * @param session The IO session to perform the checks over. * @param transactionTracker The class that keeps track of STUN * transactions for the checks. * @param checkerFactory The factory for creating new classes for * performing connectivity checks. * @param bindingRequestTracker Tracks Binding Requests we've already * processed. */ public IceStunConnectivityCheckerImpl( final IceAgent agent, final IoSession session, final StunTransactionTracker transactionTracker, final IceStunCheckerFactory checkerFactory, final IceBindingRequestTracker bindingRequestTracker) { super (transactionTracker); m_agent = agent; m_iceMediaStream = (IceMediaStream) session.getAttribute( IceMediaStream.class.getSimpleName()); m_ioSession = session; m_bindingRequestTracker = bindingRequestTracker; m_candidatePairFactory = new ExistingSessionIceCandidatePairFactoryImpl(checkerFactory); } @Override public T visitBindingRequest(final BindingRequest request) { m_log.debug("Visiting Binding Request message: {}", request); if (this.m_ioSession.isClosing() || !this.m_ioSession.isConnected()) { m_log.info("Ignoring binding request for closed session"); return null; } // This is not standard. Most STUN implementations will implement // transaction state machines and will therefore filter out duplicate // Binding Requests. This is not required, however, so we add this // check to avoid more processing if we are sent duplicate requests // for transactions. if (this.m_bindingRequestTracker.recentlyProcessed(request)) { m_log.debug("We've recently processed the request -- ignoring " + "duplicate"); final StunMessage response = this.m_bindingRequestTracker.getResponse(request); if (response != null) { // We need to resend the response again for the same reason // we send requests multiple times: UDP packets can get // dropped!! m_log.info("Writing same response again"); this.m_ioSession.write(response); } else { m_log.warn("Received dup request before mapping response?"); } return null; } this.m_bindingRequestTracker.add(request); // We occasionally see requests from ourselves in tests where the // NAT has funky hairpinning support. We check if the request is one // we just sent and ignore it if so. if (fromOurselves(this.m_agent, request)) { m_log.error("Received a request from us on: {}", this.m_ioSession); return null; } m_log.debug("Not from ourselves..."); // We need to check ICE controlling and controlled roles for conflicts. // This implements: // 7.2.1.1. Detecting and Repairing Role Conflicts final IceRoleChecker checker = new IceRoleCheckerImpl(); final BindingErrorResponse errorResponse = checker.checkAndRepairRoles(request, this.m_agent, this.m_ioSession); m_log.debug("Checked role conflict..."); if (errorResponse != null) { // This can happen in the rare case that there's a role conflict. this.m_log.debug("Sending error response..."); this.m_ioSession.write(errorResponse); this.m_bindingRequestTracker.addResponse(request, errorResponse); } else { // We now implement the remaining sections 7.2.1 following 7.2.1.1 // since we're returning a success response. m_log.debug("Processing no role conflict..."); processNoRoleConflict(request); } return null; } /** * Process the typical case where the {@link BindingRequest} did not * create a role conflict. * * @param binding The {@link BindingRequest}. */ private void processNoRoleConflict(final BindingRequest binding) { if (binding.getAttributes().containsKey( StunAttributeType.ICE_USE_CANDIDATE)) { m_log.debug("GOT BINDING REQUEST WITH USE CANDIDATE"); } final InetSocketAddress localAddress = (InetSocketAddress) this.m_ioSession.getLocalAddress(); /* // We need to implement ICE section 7.2.1.2 here for TURN // candidates. We need to translate the remote address as seen on // the IoSession (which will be the address of the TURN server) // to the address of the remote host - the peer itself. This is // available in the REMOTE-ADDRESS attribute of a Data Indication // message. final InetSocketAddress remoteAddress; final TurnStunMessageMapper mapper = (TurnStunMessageMapper) this.m_ioSession.getAttribute( "REMOTE_ADDRESS_MAP"); if (mapper != null) { remoteAddress = mapper.get(binding); m_log.debug("Using stored remote address (TURN CONNECTION): {}", remoteAddress); } else { // If there's no remote address map, then it's not a TURN check. // This is the "normal" case, so just keep going. remoteAddress = (InetSocketAddress) this.m_ioSession.getRemoteAddress(); m_log.debug("Using normal remote address: {}", remoteAddress); } */ final InetSocketAddress remoteAddress = (InetSocketAddress) this.m_ioSession.getRemoteAddress(); m_log.debug("Using normal remote address: {}", remoteAddress); // TODO: This should include other attributes!! final UUID transactionId = binding.getTransactionId(); final StunMessage response = new BindingSuccessResponse(transactionId.getRawBytes(), remoteAddress); // We write the response as soon as possible. m_log.debug("Writing success response..."); this.m_ioSession.write(response); // We need to remember our response to use it for duplicate requests. this.m_bindingRequestTracker.addResponse(binding, response); // Check to see if the remote address matches the address of // any remote candidates we know about. If it does not, it's a // new peer reflexive address. See ICE section 7.2.1.3 final TransportType type = this.m_ioSession.getTransportType(); final boolean isUdp = type.isConnectionless(); final IceCandidate remoteCandidate; if (!m_iceMediaStream.hasRemoteCandidate(remoteAddress, isUdp)) { m_log.debug("New remote candidate..."); remoteCandidate = m_iceMediaStream.addRemotePeerReflexive( binding, localAddress, remoteAddress, isUdp); m_log.debug("Added peer reflexive remote candidate: {}", remoteCandidate); } else { remoteCandidate = this.m_iceMediaStream.getRemoteCandidate(remoteAddress, isUdp); } final IceCandidate localCandidate = this.m_iceMediaStream.getLocalCandidate(localAddress, isUdp); m_log.debug("Using existing local candidate: {}", localCandidate); if (localCandidate == null) { m_log.warn("Could not create local candidate."); return; } if (remoteCandidate == null) { // There should always be a remote candidate at this point // because the peer reflexive check above should have added it // if it wasn't already there. m_log.warn("Could not find remote candidate."); return; } // 7.2.1.4. Triggered Checks final IceCandidatePair existingPair = this.m_iceMediaStream.getPair(localAddress, remoteAddress, localCandidate.isUdp()); final IceCandidatePair computedPair; if (existingPair != null) { m_log.debug("Found existing pair"); computedPair = existingPair; /* if (computedPair.getIoSession() == null) { m_log.error("Should have a session for pair: {}", computedPair); throw new NullPointerException("Null session!!!"); } */ // This is the case where the new pair is already on the // check list. See ICE section 7.2.1.4. Triggered Checks final IceCandidatePairState state = computedPair.getState(); switch (state) { case WAITING: // Fall through. case FROZEN: m_log.debug("Adding triggered check for previously " + "frozen or waiting pair:\n{}",existingPair); this.m_iceMediaStream.addTriggeredPair(existingPair); break; case IN_PROGRESS: // We need to cancel the in-progress transaction. // This just means we won't re-submit requests and will // not treat the lack of response as a failure. // The following code doesn't seem to be necessary and // broke NAT traversal in at least one scenario when // trying to traverse a NAT from OSX to Windows, ethernet // to wireless. /* m_log.debug("Canceling in progress transaction..."); existingPair.cancelStunTransaction(); // Add the pair to the triggered check queue. existingPair.setState(IceCandidatePairState.WAITING); this.m_iceMediaStream.addTriggeredPair(existingPair); */ m_log.info("Pair is IN PROGRESS...nominating on success"); break; case FAILED: existingPair.setState(IceCandidatePairState.WAITING); this.m_iceMediaStream.addTriggeredPair(existingPair); break; case SUCCEEDED: // Nothing more to do. m_log.debug("Pair has already been checked and SUCCEEDED"); break; } } else { m_log.debug("Creating new candidate pair."); computedPair = this.m_candidatePairFactory.newUdpPair( localCandidate, remoteCandidate, this.m_ioSession); // Continue with the rest of ICE section 7.2.1.4, // "Triggered Checks" // * The pair is inserted into the check list based on its priority. // * Its state is set to Waiting. // * The pair is enqueued into the triggered check queue. // // TODO: The remote candidate needs a username fragment and // password. We don't implement this yet. This is the // description of what we need to do: // "The username fragment for the remote candidate is equal to // the part after the colon of the USERNAME in the Binding // Request that was just received. Using that username // fragment, the agent can check the SDP messages received // from its peer (there may be more than one in cases of // forking), and find this username fragment. The // corresponding password is then selected." // Add the pair the normal check list, set its state to waiting, // and add a triggered check. this.m_iceMediaStream.addPair(computedPair); computedPair.setState(IceCandidatePairState.WAITING); this.m_iceMediaStream.addTriggeredPair(computedPair); // TODO: We should be handling the username fragment and // password for the triggered check. } // 7.2.1.5. Updating the Nominated Flag // If the ICE USE CANDIDATE attribute is set, and we're in the // controlled role, we need to deal with nominating the pair. if (binding.getAttributes().containsKey( StunAttributeType.ICE_USE_CANDIDATE) && !this.m_agent.isControlling()) { // Just record the fact that the controlling agent told us // to use this pair. computedPair.useCandidate(); final IceCandidatePairState state = computedPair.getState(); switch (state) { // This is an optimization that allows us to nominate now // when we've already previously done a successful check // on this pair. In that case, we didn't add a triggered // check above, and we can just nominate now. case SUCCEEDED: m_log.debug("Nominating pair on controlled agent:\n{}", computedPair); computedPair.nominate(); // Note this typically means the controlled agent will be // the first to use a nominated pair for media, as the // controlling agent won't use the nominated pair until // it receives the successful binding response. m_agent.onNominatedPair(computedPair, this.m_iceMediaStream); break; case IN_PROGRESS: // Section 7.2.1.5: /* If the state of this pair is In-Progress, if its check produces a successful result, the resulting valid pair has its nominated flag set when the response arrives. This may end ICE processing for this media stream when it arrives; see Section 8. */ computedPair.nominateOnSuccess(); break; case WAITING: // No action. break; case FROZEN: // No action. break; case FAILED: // No action. break; } } } private boolean fromOurselves(final IceAgent agent, final BindingRequest request) { final Map remoteAttributes = request.getAttributes(); final byte[] remoteTieBreaker; if (remoteAttributes.containsKey(StunAttributeType.ICE_CONTROLLED)) { final IceControlledAttribute attribute = (IceControlledAttribute) remoteAttributes.get( StunAttributeType.ICE_CONTROLLED); remoteTieBreaker = attribute.getTieBreaker(); } else { final IceControllingAttribute attribute = (IceControllingAttribute) remoteAttributes.get( StunAttributeType.ICE_CONTROLLING); if (attribute == null) { // This can often happen during tests. If it happens in // production, though, this will get sent to our servers. m_log.error("No controlling attribute"); return false; } remoteTieBreaker = attribute.getTieBreaker(); } final byte[] localTieBreaker = agent.getTieBreaker().toByteArray(); return Arrays.equals(localTieBreaker, remoteTieBreaker); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy