STUN, TURN and ICE research on WebRTC

Be prepared to look at the WebRTC source code and take a deep dive into ICE for the protocol formats in these articles.

These three articles are the best ICE articles I have read so far:
P2P Communication Standard Protocol (1) STUN
P2P Communication Standard Protocol (2) TURN
P2P Communication Standard Protocol (3) ICE

This can be used as a supplement:
Detailed explanation of P2P technology (3): Detailed explanation of STUN, TURN and ICE of P2P technology


First learn the basics of the above article, and then start to analyze the process of WebRTC creating PeerConnection until connecting Stun and Turn:

bool PeerConnection::InitializePortAllocator_n(
...
  if (ParseIceServers(configuration.servers, &stun_servers, &turn_servers) !=
...
port_allocator_->SetConfiguration(
      stun_servers, turn_servers, configuration.ice_candidate_pool_size,
      configuration.prune_turn_ports, configuration.turn_customizer,
      configuration.stun_candidate_keepalive_interval);
...
}
bool PortAllocator::SetConfiguration(
...
  stun_servers_ = stun_servers;
  turn_servers_ = turn_servers;
...
// If |candidate_pool_size_| is greater than the number of pooled sessions,
  // create new sessions.
  while (static_cast<int>(pooled_sessions_.size()) < candidate_pool_size_) {
    PortAllocatorSession* pooled_session = CreateSessionInternal("", 0, "", "");
    pooled_session->StartGettingPorts();
    pooled_sessions_.push_back(
        std::unique_ptr<PortAllocatorSession>(pooled_session));
  }
  return true;
}

PeerConnection creates port_allocator_ during initialization, and calls PortAllocator::SetConfiguration to store stun_servers and turn_servers.
And called BasicPortAllocatorSession::StartGettingPorts()


void BasicPortAllocatorSession::StartGettingPorts() {
...
network_thread_->Post(RTC_FROM_HERE, this, MSG_CONFIG_START);
...
}
void BasicPortAllocatorSession::OnMessage(rtc::Message *message) {
  switch (message->message_id) {
  case MSG_CONFIG_START:
    RTC_DCHECK(rtc::Thread::Current() == network_thread_);
    GetPortConfigurations();
...
}

void BasicPortAllocatorSession::GetPortConfigurations() {
  PortConfiguration* config = new PortConfiguration(allocator_->stun_servers(),
                                                    username(),
                                                    password());

  for (const RelayServerConfig& turn_server : allocator_->turn_servers()) {
    config->AddRelay(turn_server);
  }
  ConfigReady(config);
}

void BasicPortAllocatorSession::ConfigReady(PortConfiguration* config) {
  network_thread_->Post(RTC_FROM_HERE, this, MSG_CONFIG_READY, config);
}
void BasicPortAllocatorSession::OnMessage(rtc::Message *message) {
  switch (message->message_id) {
...
  case MSG_CONFIG_READY:
    RTC_DCHECK(rtc::Thread::Current() == network_thread_);
    OnConfigReady(static_cast<PortConfiguration*>(message->pdata));
    break;
...
}
// Adds a configuration to the list.
void BasicPortAllocatorSession::OnConfigReady(PortConfiguration* config) {
  if (config) {
    configs_.push_back(config);
  }

  AllocatePorts();
}

// For each network, see if we have a sequence that covers it already.  If not,
// create a new sequence to create the appropriate ports.
void BasicPortAllocatorSession::DoAllocate(bool disable_equivalent) {
...
 AllocationSequence* sequence =
          new AllocationSequence(this, networks[i], config, sequence_flags);
      sequence->SignalPortAllocationComplete.connect(
          this, &BasicPortAllocatorSession::OnPortAllocationComplete);
      sequence->Init();
      sequence->Start();
      sequences_.push_back(sequence);
...
}

Pass in PortConfiguration *config all the way, create AllocationSequence* sequence, and call the Start() method


void AllocationSequence::Start() {
  state_ = kRunning;
  session_->network_thread()->Post(RTC_FROM_HERE, this, MSG_ALLOCATION_PHASE);
  // Take a snapshot of the best IP, so that when DisableEquivalentPhases is
  // called next time, we enable all phases if the best IP has since changed.
  previous_best_ip_ = network_->GetBestIP();
}
void AllocationSequence::OnMessage(rtc::Message* msg) {
  RTC_DCHECK(rtc::Thread::Current() == session_->network_thread());
  RTC_DCHECK(msg->message_id == MSG_ALLOCATION_PHASE);

  const char* const PHASE_NAMES[kNumPhases] = {"Udp", "Relay", "Tcp"};

  // Perform all of the phases in the current step.
  RTC_LOG(LS_INFO) << network_->ToString()
                   << ": Allocation Phase=" << PHASE_NAMES[phase_];

  switch (phase_) {
    case PHASE_UDP:
      CreateUDPPorts();
      CreateStunPorts();
      break;

    case PHASE_RELAY:
      CreateRelayPorts();
      break;

    case PHASE_TCP:
      CreateTCPPorts();
      state_ = kCompleted;
      break;

    default:
      RTC_NOTREACHED();
  }

  if (state() == kRunning) {
    ++phase_;
    session_->network_thread()->PostDelayed(RTC_FROM_HERE,
                                            session_->allocator()->step_delay(),
                                            this, MSG_ALLOCATION_PHASE);
  } else {
    // If all phases in AllocationSequence are completed, no allocation
    // steps needed further. Canceling  pending signal.
    session_->network_thread()->Clear(this, MSG_ALLOCATION_PHASE);
    SignalPortAllocationComplete(this);
  }
}

phase_ defaults to 0, which is PHASE_UDP, and then as long as state() == kRunning, ++phase will continue to send MSG_ALLOCATION_PHASE messages to delay the callback itself, that is, CreateRelayPorts(); and CreateTCPPorts(); will be called. The delay time is session_->allocator()->step_delay(), which is set to kMinimumStepDelay, which means that it is executed only at 50 millisecond intervals.

// As per RFC 5245 Appendix B.1, STUN transactions need to be paced at certain
// internal. Less than 20ms is not acceptable. We choose 50ms as our default.
const uint32_t kMinimumStepDelay = 50;

Someone commented in the code that this interval is too short, resulting in too many STUN connections at the same time:

  // Delay between different candidate gathering phases (UDP, TURN, TCP).
  // Defaults to 1 second, but PeerConnection sets it to 50ms.
  // TODO(deadbeef): Get rid of this. Its purpose is to avoid sending too many
  // STUN transactions at once, but that's already happening if you configure
  // multiple STUN servers or have multiple network interfaces. We should
  // implement some global pacing logic instead if that's our goal.
  uint32_t step_delay() const { return step_delay_; }
  void set_step_delay(uint32_t delay) { step_delay_ = delay; }

CreateUDPPorts();UDPPort is created and called session_->AddAllocatedPort(port, this, true);.
CreateStunPorts();StunPort is created, also called session_->AddAllocatedPort(port, this, true);.
StunPort inherits from UDPPort.
The type_ of UDPPort is LOCAL_PORT_TYPE, and the type of StunPort is STUN_PORT_TYPE.

I don't know why we need to create UDPPort and StunPort for the time being. Logically, a UDPPort or StunPort is enough.
Added: Take a closer look at CreateStunPorts():

void AllocationSequence::CreateStunPorts() {
...
 if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET)) {
    return;
  }
...

And PORTALLOCATOR_ENABLE_SHARED_SOCKET is set when PeerConnection is initialized, so CreateStunPorts() will not be executed, only UDPPort is valid.


void BasicPortAllocatorSession::AddAllocatedPort(Port* port,
                                                 AllocationSequence * seq,
                                                 bool prepare_address) {
  if (!port)
    return;

  RTC_LOG(LS_INFO) << "Adding allocated port for " << content_name();
  port->set_content_name(content_name());
  port->set_component(component());
  port->set_generation(generation());
  if (allocator_->proxy().type != rtc::PROXY_NONE)
    port->set_proxy(allocator_->user_agent(), allocator_->proxy());
  port->set_send_retransmit_count_attribute(
      (flags() & PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE) != 0);

  PortData data(port, seq);
  ports_.push_back(data);

  port->SignalCandidateReady.connect(
      this, &BasicPortAllocatorSession::OnCandidateReady);
  port->SignalPortComplete.connect(this,
      &BasicPortAllocatorSession::OnPortComplete);
  port->SignalDestroyed.connect(this,
      &BasicPortAllocatorSession::OnPortDestroyed);
  port->SignalPortError.connect(
      this, &BasicPortAllocatorSession::OnPortError);
  RTC_LOG(LS_INFO) << port->ToString()
                   << ": Added port to allocator";

  if (prepare_address)
    port->PrepareAddress();
}

PrepareAddress() of UDPPort and StunPort both call UDPPort::SendStunBindingRequests()


void UDPPort::SendStunBindingRequests() {
  // We will keep pinging the stun server to make sure our NAT pin-hole stays
  // open until the deadline (specified in SendStunBindingRequest).
  RTC_DCHECK(requests_.empty());

  for (ServerAddresses::const_iterator it = server_addresses_.begin();
       it != server_addresses_.end(); ++it) {
    SendStunBindingRequest(*it);
  }
}

void UDPPort::SendStunBindingRequest(const rtc::SocketAddress& stun_addr) {
...
      requests_.Send(
          new StunBindingRequest(this, stun_addr, rtc::TimeMillis()));
...
}

StunBindingRequest inherits from StunRequest, which has one inside StunMessage* msg_,
StunMessage encapsulates the Stun client protocol.
The source code is compared with the STUN protocol format written by STUN of the P2P communication standard protocol (1) .

The first outgoing message has a type_ of STUN_BINDING_REQUEST and a value of 0x0001.


About STUN Message Type is decomposed into the following structure

 0                 1
 2  3  4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+

The bits shown are from the most significant bit M11 to the least significant bit M0, and M11 to M0 represent the 12-bit encoding of the method. The two bits C1 and C0 represent the code of the class. For example, for the binding method,
0b00 means request, 0b01 means indication, 0b10 means success response, and 0b11 means error response. Each method may correspond to a different transmission category.

The above 0b is a binary prefix (corresponding to 0x in hexadecimal), so 0b00, that is, C1C0 is all 0, and 0b11 means C1==1, C0==1.
So there are only 4 categories at most, and you can know which category is by looking at C1C0.

RFC5389文档说明:
For example, a Binding request has class=0b00 (request) and
method=0b000000000001 (Binding) and is encoded into the first 16 bits
as 0x0001. A Binding response has class=0b10 (success response) and
method=0b000000000001, and is encoded into the first 16 bits as
0x0101.


bool StunMessage::Write(ByteBufferWriter* buf) const {
  buf->WriteUInt16(type_);
  buf->WriteUInt16(length_);
  if (!IsLegacy())
    buf->WriteUInt32(stun_magic_cookie_);
  buf->WriteString(transaction_id_);

  for (const auto& attr : attrs_) {
    buf->WriteUInt16(attr->type());
    buf->WriteUInt16(static_cast<uint16_t>(attr->length()));
    if (!attr->Write(buf)) {
      return false;
    }
  }

  return true;
}

StunMessage::Write encapsulates the packet to be sent.

StunRequest::StunRequest(){
...
msg_->SetTransactionID(
      rtc::CreateRandomString(kStunTransactionIdLength));
...
}

transaction_id_ is the transaction ID, and it can be seen that the value of transaction_id_ is a random string.
All STUN attributes are written after transaction_id_ is written.
The base class of the STUN attribute is StunAttribute, which derives several classes.
All derived classes are listed in StunAttribute::Create.

StunAttribute* StunAttribute::Create(StunAttributeValueType value_type,
                                     uint16_t type,
                                     uint16_t length,
                                     StunMessage* owner) {
  switch (value_type) {
    case STUN_VALUE_ADDRESS:
      return new StunAddressAttribute(type, length);
    case STUN_VALUE_XOR_ADDRESS:
      return new StunXorAddressAttribute(type, length, owner);
    case STUN_VALUE_UINT32:
      return new StunUInt32Attribute(type);
    case STUN_VALUE_UINT64:
      return new StunUInt64Attribute(type);
    case STUN_VALUE_BYTE_STRING:
      return new StunByteStringAttribute(type, length);
    case STUN_VALUE_ERROR_CODE:
      return new StunErrorCodeAttribute(type, length);
    case STUN_VALUE_UINT16_LIST:
      return new StunUInt16ListAttribute(type, length);
    default:
      return NULL;
  }
}

UDPPort::OnReadPacketHandle STUN messages returned by the server.
Call the base class method to Port::OnReadPacketverify that if it is a valid STUN return packet, unpack it to IceMessage.
IceMessage is derived from StunMessage.


In fact, in the process of viewing the code of the STUN client, it can be seen that there is not only the STUN client function, but also the ICE function. When the Offer-Answer on both sides of the client obtains the sdp and exchanges the Candidate, the ICE function will be enabled, and the two sides will use the STUN protocol to get through the NAT, which has nothing to do with the external STUN server.


Let's continue to analyze the TURN client code, starting with CreateRelayPorts():
void AllocationSequence::CreateRelayPorts() {
...
  for (RelayServerConfig& relay : config_->relays) {
    if (relay.type == RELAY_GTURN) {
      CreateGturnPort(relay);
    } else if (relay.type == RELAY_TURN) {
      CreateTurnPort(relay);
    } else {
      RTC_NOTREACHED();
    }
  }
}

CreateGturnPort is for google's own TURN service, and CreateTurnPort is for the standard TURN service. The following only analyzes CreateTurnPort.

void AllocationSequence::CreateTurnPort(const RelayServerConfig& config) {
  PortList::const_iterator relay_port;
  for (relay_port = config.ports.begin();
       relay_port != config.ports.end(); ++relay_port) {
    // Skip UDP connections to relay servers if it's disallowed.
    if (IsFlagSet(PORTALLOCATOR_DISABLE_UDP_RELAY) &&
        relay_port->proto == PROTO_UDP) {
      continue;
    }

    // Do not create a port if the server address family is known and does
    // not match the local IP address family.
    int server_ip_family = relay_port->address.ipaddr().family();
    int local_ip_family = network_->GetBestIP().family();
    if (server_ip_family != AF_UNSPEC && server_ip_family != local_ip_family) {
      RTC_LOG(LS_INFO)
          << "Server and local address families are not compatible. "
             "Server address: " << relay_port->address.ipaddr().ToString()
          << " Local address: " << network_->GetBestIP().ToString();
      continue;
    }

    CreateRelayPortArgs args;
    args.network_thread = session_->network_thread();
    args.socket_factory = session_->socket_factory();
    args.network = network_;
    args.username = session_->username();
    args.password = session_->password();
    args.server_address = &(*relay_port);
    args.config = &config;
    args.origin = session_->allocator()->origin();
    args.turn_customizer = session_->allocator()->turn_customizer();

    std::unique_ptr<cricket::Port> port;
    // Shared socket mode must be enabled only for UDP based ports. Hence
    // don't pass shared socket for ports which will create TCP sockets.
    // TODO(mallinath) - Enable shared socket mode for TURN ports. Disabled
    // due to webrtc bug https://code.google.com/p/webrtc/issues/detail?id=3537
    if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) &&
        relay_port->proto == PROTO_UDP && udp_socket_) {
      port = session_->allocator()->relay_port_factory()->Create(
          args, udp_socket_.get());

      if (!port) {
        RTC_LOG(LS_WARNING)
            << "Failed to create relay port with "
            << args.server_address->address.ToString();
        continue;
      }

      relay_ports_.push_back(port.get());
      // Listen to the port destroyed signal, to allow AllocationSequence to
      // remove entrt from it's map.
      port->SignalDestroyed.connect(this, &AllocationSequence::OnPortDestroyed);
    } else {
      port = session_->allocator()->relay_port_factory()->Create(
          args,
          session_->allocator()->min_port(),
          session_->allocator()->max_port());

      if (!port) {
        RTC_LOG(LS_WARNING)
            << "Failed to create relay port with "
            << args.server_address->address.ToString();
        continue;
      }
    }
    RTC_DCHECK(port != NULL);
    session_->AddAllocatedPort(port.release(), this, true);
  }
}

Mainly in:

 port = session_->allocator()->relay_port_factory()->Create(
          args, udp_socket_.get());
std::unique_ptr<Port> TurnPortFactory::Create(
    const CreateRelayPortArgs& args,
    int min_port,
    int max_port) {

  TurnPort* port = TurnPort::Create(
      args.network_thread,
      args.socket_factory,
      args.network,
      min_port,
      max_port,
      args.username,
      args.password,
      *args.server_address,
      args.config->credentials,
      args.config->priority,
      args.origin,
      args.config->tls_alpn_protocols,
      args.config->tls_elliptic_curves,
      args.turn_customizer);
  port->SetTlsCertPolicy(args.config->tls_cert_policy);
  return std::unique_ptr<Port>(port);
}

TurnPortFactory creates TurnPort, also AddAllocatedPort, then TurnPort::PrepareAddress()

void TurnPort::PrepareAddress() {
  if (credentials_.username.empty() ||
      credentials_.password.empty()) {
    RTC_LOG(LS_ERROR) << "Allocation can't be started without setting the"
                         " TURN server credentials for the user.";
    OnAllocateError();
    return;
  }

  if (!server_address_.address.port()) {
    // We will set default TURN port, if no port is set in the address.
    server_address_.address.SetPort(TURN_DEFAULT_PORT);
  }

  if (server_address_.address.IsUnresolvedIP()) {
    ResolveTurnAddress(server_address_.address);
  } else {
    // If protocol family of server address doesn't match with local, return.
    if (!IsCompatibleAddress(server_address_.address)) {
      RTC_LOG(LS_ERROR) << "IP address family does not match. server: "
                        << server_address_.address.family()
                        << " local: " << Network()->GetBestIP().family();
      OnAllocateError();
      return;
    }

    // Insert the current address to prevent redirection pingpong.
    attempted_server_addresses_.insert(server_address_.address);

    RTC_LOG(LS_INFO) << ToString()
                     << ": Trying to connect to TURN server via "
                     << ProtoToString(server_address_.proto) << " @ "
                     << server_address_.address.ToSensitiveString();
    if (!CreateTurnClientSocket()) {
      RTC_LOG(LS_ERROR) << "Failed to create TURN client socket";
      OnAllocateError();
      return;
    }
    if (server_address_.proto == PROTO_UDP) {
      // If its UDP, send AllocateRequest now.
      // For TCP and TLS AllcateRequest will be sent by OnSocketConnect.
      SendRequest(new TurnAllocateRequest(this), 0);
    }
  }
}

It can be seen that username and password are required, which is the so-called long-term credentials must be enabled in coTurn to support WebRTC.

TurnAllocateRequest is also derived from StunRequest, but its internal msg_ is TurnMessage.
After watching TurnMessage for a long time, it turns out that the current WebRTC version does not support turn oauth authentication, and the W3C WebRTC 1.0: Real-time Communication Between Browsers is only a standard draft, and it is not fully implemented, WTF.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325672001&siteId=291194637