agent: implement support for RFC7675 - Consent Freshness
authorMatthew Waters <matthew@centricular.com>
Tue, 15 Sep 2020 04:54:00 +0000 (14:54 +1000)
committerOlivier Crête <olivier.crete@collabora.com>
Thu, 10 Dec 2020 16:27:53 +0000 (11:27 -0500)
Specified in https://tools.ietf.org/html/rfc7675

RFC 7675 is a slight modification of the existing keepalive connection
checks that could be enabled manually or were used with the GOOGLE
compatibility mode.

Slight differences from the existing keepalive connection checks
include:
- an additional consent expiry timer instead of relying on all binding
  requests to succeed.
- 403: 'Forbidden' stun error-code which revokes consent with immediate
  effect.

16 files changed:
README
agent/agent-priv.h
agent/agent.c
agent/agent.h
agent/component.c
agent/component.h
agent/conncheck.c
agent/conncheck.h
docs/reference/libnice/libnice-docs.xml
docs/reference/libnice/libnice-sections.txt
nice/libnice.sym
stun/stunagent.c
stun/stunagent.h
stun/stunmessage.h
tests/meson.build
tests/test-consent.c [new file with mode: 0644]

diff --git a/README b/README
index b307b9b..c1a5deb 100644 (file)
--- a/README
+++ b/README
@@ -61,6 +61,8 @@ ICE
 STUN
   http://tools.ietf.org/html/rfc3489 (old)
   http://tools.ietf.org/html/rfc5389
+STUN Consent Freshness RFC
+  https://tools.ietf.org/html/rfc7675
 TURN 
   http://tools.ietf.org/html/rfc5766
 RTP
index 8c08b28..1e9387c 100644 (file)
@@ -106,6 +106,10 @@ nice_input_message_iter_compare (const NiceInputMessageIter *a,
 
 #define NICE_AGENT_TIMER_TA_DEFAULT 20      /* timer Ta, msecs (impl. defined) */
 #define NICE_AGENT_TIMER_TR_DEFAULT 25000   /* timer Tr, msecs (impl. defined) */
+#define NICE_AGENT_TIMER_CONSENT_DEFAULT 5000    /* msec timer consent freshness connchecks (RFC 7675) */
+#define NICE_AGENT_TIMER_CONSENT_TIMEOUT 10000   /* msec timer for consent checks to timeout and assume consent lost (RFC 7675) */
+#define NICE_AGENT_TIMER_MIN_CONSENT_INTERVAL 4000  /* msec timer minimum for consent lost requests (RFC 7675) */
+#define NICE_AGENT_TIMER_KEEPALIVE_TIMEOUT 50000    /* msec timer for keepalive (without consent checks) to timeout and assume conection lost */
 #define NICE_AGENT_MAX_CONNECTIVITY_CHECKS_DEFAULT 100 /* see RFC 8445 6.1.2.5 */
 
 
@@ -184,6 +188,8 @@ struct _NiceAgent
   guint conncheck_ongoing_idle_delay; /* ongoing delay before timer stop */
   gboolean controlling_mode;          /* controlling mode used by the
                                          conncheck */
+  gboolean consent_freshness;         /* rfc 7675 consent freshness with
+                                         connchecks */
   /* XXX: add pointer to internal data struct for ABI-safe extensions */
 };
 
index 50ea756..2145755 100644 (file)
@@ -123,6 +123,7 @@ enum
   PROP_ICE_TRICKLE,
   PROP_SUPPORT_RENOMINATION,
   PROP_IDLE_TIMEOUT,
+  PROP_CONSENT_FRESHNESS,
 };
 
 
@@ -773,6 +774,8 @@ nice_agent_class_init (NiceAgentClass *klass)
    * This is always enabled if the compatibility mode is
    * %NICE_COMPATIBILITY_GOOGLE.
    *
+   * This is always enabled if the 'consent-freshness' property is %TRUE
+   *
    * Since: 0.1.8
    */
    g_object_class_install_property (gobject_class, PROP_KEEPALIVE_CONNCHECK,
@@ -890,6 +893,28 @@ nice_agent_class_init (NiceAgentClass *klass)
         FALSE,
         G_PARAM_READWRITE));
 
+   /**
+    * NiceAgent:consent-freshness
+    *
+    * Whether to perform periodic consent freshness checks as specified in
+    * RFC 7675.  When %TRUE, the agent will periodically send binding requests
+    * to the peer to maintain the consent to send with the peer.  On receipt
+    * of any authenticated error response, a component will immediately move
+    * to the failed state.
+    *
+    * Setting this property to %TRUE implies that 'keepalive-conncheck' should
+    * be %TRUE as well.
+    *
+    * Since: 0.1.20
+    */
+   g_object_class_install_property (gobject_class, PROP_CONSENT_FRESHNESS,
+      g_param_spec_boolean (
+        "consent-freshness",
+        "Consent Freshness",
+        "Whether to perform the consent freshness checks as specified in RFC 7675",
+        FALSE,
+        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
   /* install signals */
 
   /**
@@ -1314,6 +1339,7 @@ nice_agent_new_full (GMainContext *ctx,
       "full-mode", (flags & NICE_AGENT_OPTION_LITE_MODE) ? FALSE : TRUE,
       "ice-trickle", (flags & NICE_AGENT_OPTION_ICE_TRICKLE) ? TRUE : FALSE,
       "support-renomination", (flags & NICE_AGENT_OPTION_SUPPORT_RENOMINATION) ? TRUE : FALSE,
+      "consent-freshness", (flags & NICE_AGENT_OPTION_CONSENT_FRESHNESS) ? TRUE : FALSE,
       NULL);
 
   return agent;
@@ -1438,7 +1464,7 @@ nice_agent_get_property (
       break;
 
     case PROP_KEEPALIVE_CONNCHECK:
-      if (agent->compatibility == NICE_COMPATIBILITY_GOOGLE)
+      if (agent->compatibility == NICE_COMPATIBILITY_GOOGLE || agent->consent_freshness)
         g_value_set_boolean (value, TRUE);
       else
         g_value_set_boolean (value, agent->keepalive_conncheck);
@@ -1464,6 +1490,10 @@ nice_agent_get_property (
       g_value_set_boolean (value, agent->use_ice_trickle);
       break;
 
+    case PROP_CONSENT_FRESHNESS:
+      g_value_set_boolean (value, agent->consent_freshness);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     }
@@ -1502,9 +1532,14 @@ nice_agent_init_stun_agent (NiceAgent *agent, StunAgent *stun_agent)
         STUN_AGENT_USAGE_USE_FINGERPRINT |
         STUN_AGENT_USAGE_NO_ALIGNED_ATTRIBUTES);
   } else {
+    StunAgentUsageFlags stun_usage = 0;
+
+    if (agent->consent_freshness)
+      stun_usage |= STUN_AGENT_USAGE_CONSENT_FRESHNESS;
+
     stun_agent_init (stun_agent, STUN_ALL_KNOWN_ATTRIBUTES,
         STUN_COMPATIBILITY_RFC5389,
-        STUN_AGENT_USAGE_SHORT_TERM_CREDENTIALS |
+        stun_usage | STUN_AGENT_USAGE_SHORT_TERM_CREDENTIALS |
         STUN_AGENT_USAGE_USE_FINGERPRINT);
   }
   stun_agent_set_software (stun_agent, agent->software_attribute);
@@ -1677,6 +1712,10 @@ nice_agent_set_property (
       agent->use_ice_trickle = g_value_get_boolean (value);
       break;
 
+    case PROP_CONSENT_FRESHNESS:
+      agent->consent_freshness = g_value_get_boolean (value);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     }
@@ -5187,6 +5226,13 @@ nice_agent_send_messages_nonblocking_internal (
     goto done;
   }
 
+  if (component->selected_pair.local != NULL &&
+        !component->selected_pair.remote_consent.have) {
+    g_set_error (&child_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
+                 "Consent to send has been revoked by the peer");
+    goto done;
+  }
+
   /* FIXME: Cancellation isn’t yet supported, but it doesn’t matter because
    * we only deal with non-blocking writes. */
   if (component->selected_pair.local != NULL) {
@@ -5965,6 +6011,8 @@ nice_agent_set_selected_pair (
       NICE_COMPONENT_STATE_READY);
 
   /* step: set the selected pair */
+  /* XXX: assume we have consent to send to this selected remote address */
+  pair.remote_consent.have = TRUE;
   nice_component_update_selected_pair (agent, component, &pair);
   agent_signal_new_selected_pair (agent, stream_id, component_id,
       (NiceCandidate *) pair.local, (NiceCandidate *) pair.remote);
@@ -7103,3 +7151,28 @@ nice_agent_get_sockets (NiceAgent *agent, guint stream_id, guint component_id)
 
   return array;
 }
+
+NICEAPI_EXPORT gboolean
+nice_agent_consent_lost (
+    NiceAgent *agent,
+    guint stream_id,
+    guint component_id)
+{
+  gboolean result = FALSE;
+  NiceComponent *component;
+
+  agent_lock (agent);
+  if (!agent->consent_freshness) {
+    g_warning ("Agent %p: Attempt made to signal consent lost for "
+        "stream/component %u/%u but RFC7675/consent-freshness is not enabled "
+        "for this agent. Ignoring request", agent, stream_id, component_id);
+  } else if (agent_find_component (agent, stream_id, component_id, NULL, &component)) {
+    nice_debug ("Agent %p: local consent lost for stream/component %u/%u", agent,
+        component->stream_id, component->id);
+    component->have_local_consent = FALSE;
+    result = TRUE;
+  }
+  agent_unlock_and_emit (agent);
+
+  return result;
+}
index 1164138..980b760 100644 (file)
@@ -407,6 +407,7 @@ typedef enum
  * @NICE_AGENT_OPTION_ICE_TRICKLE: Enable ICE trickle mode
  * @NICE_AGENT_OPTION_SUPPORT_RENOMINATION: Enable renomination triggered by NOMINATION STUN attribute
  * proposed here: https://tools.ietf.org/html/draft-thatcher-ice-renomination-00
+ * @NICE_AGENT_OPTION_CONSENT_FRESHNESS: Enable RFC 7675 consent freshness support. (Since: 0.1.20)
  *
  * These are options that can be passed to nice_agent_new_full(). They set
  * various properties on the agent. Not including them sets the property to
@@ -420,6 +421,7 @@ typedef enum {
   NICE_AGENT_OPTION_LITE_MODE = 1 << 2,
   NICE_AGENT_OPTION_ICE_TRICKLE = 1 << 3,
   NICE_AGENT_OPTION_SUPPORT_RENOMINATION = 1 << 4,
+  NICE_AGENT_OPTION_CONSENT_FRESHNESS = 1 << 5,
 } NiceAgentOption;
 
 /**
@@ -919,6 +921,9 @@ nice_agent_get_remote_candidates (
  * "ICE Restarts"), as well as when reacting (spec section 9.2.1.1.
  * "Detecting ICE Restart") to a restart.
  *
+ * If consent-freshness has been enabled on @agent, as specified in RFC7675
+ * then restarting streams will restore the local consent.
+ *
  * Returns: %TRUE on success %FALSE on error
  **/
 gboolean
@@ -938,6 +943,9 @@ nice_agent_restart (
  * Unlike nice_agent_restart(), this applies to a single stream. It also
  * does not generate a new tie breaker.
  *
+ * If consent-freshness has been enabled on @agent, as specified in RFC7675
+ * then restart @stream_id will restore the local consent for that stream.
+ *
  * Returns: %TRUE on success %FALSE on error
  *
  * Since: 0.1.6
@@ -1660,6 +1668,34 @@ nice_agent_peer_candidate_gathering_done (
     guint stream_id);
 
 /**
+ * nice_agent_consent_lost:
+ * @agent: The #NiceAgent Object
+ * @stream_id: The ID of the stream
+ * @component_id: The ID of the component
+ *
+ * Notifies the agent that consent to receive has been revoked.  This will
+ * cause the component to fail with 403 'Forbidden' all incoming STUN binding
+ * requests as specified in RFC 7675.
+ *
+ * A stream with a component in the consent-lost state can be reused by
+ * performing an ice restart with nice_agent_restart() or
+ * nice_agent_restart_stream().
+ *
+ * Calling the function only has an effect when @agent has been created with
+ * @NICE_AGENT_OPTION_CONSENT_FRESHNESS.
+ *
+ * Returns: %FALSE if the stream or component could not be found or consent
+ *     freshness is not enabled, %TRUE otherwise
+ *
+ * Since: 0.1.20
+ */
+gboolean
+nice_agent_consent_lost (
+    NiceAgent *agent,
+    guint stream_id,
+    guint component_id);
+
+/**
  * nice_agent_close_async:
  * @agent: The #NiceAgent object
  * @callback: (nullable): A callback that will be called when the closing is
index 13c4e4b..5e86b15 100644 (file)
@@ -307,10 +307,10 @@ nice_component_clean_turn_servers (NiceAgent *agent, NiceComponent *cmp)
 static void
 nice_component_clear_selected_pair (NiceComponent *component)
 {
-  if (component->selected_pair.keepalive.tick_source != NULL) {
-    g_source_destroy (component->selected_pair.keepalive.tick_source);
-    g_source_unref (component->selected_pair.keepalive.tick_source);
-    component->selected_pair.keepalive.tick_source = NULL;
+  if (component->selected_pair.remote_consent.tick_source != NULL) {
+    g_source_destroy (component->selected_pair.remote_consent.tick_source);
+    g_source_unref (component->selected_pair.remote_consent.tick_source);
+    component->selected_pair.remote_consent.tick_source = NULL;
   }
 
   memset (&component->selected_pair, 0, sizeof(CandidatePair));
@@ -458,6 +458,8 @@ nice_component_restart (NiceComponent *cmp)
   /* Reset the priority to 0 to make sure we get a new pair */
   cmp->selected_pair.priority = 0;
 
+  cmp->have_local_consent = TRUE;
+
   /* note: component state managed by agent */
 }
 
@@ -499,6 +501,7 @@ nice_component_update_selected_pair (NiceAgent *agent, NiceComponent *component,
   component->selected_pair.remote = pair->remote;
   component->selected_pair.priority = pair->priority;
   component->selected_pair.stun_priority = pair->stun_priority;
+  component->selected_pair.remote_consent.have = pair->remote_consent.have;
 
   nice_component_add_valid_candidate (agent, component,
       (NiceCandidate *) pair->remote);
@@ -580,6 +583,7 @@ nice_component_set_selected_remote_candidate (NiceComponent *component,
   component->selected_pair.local = (NiceCandidateImpl *) local;
   component->selected_pair.remote = (NiceCandidateImpl *) remote;
   component->selected_pair.priority = priority;
+  component->selected_pair.remote_consent.have = TRUE;
 
   /* Get into fallback mode where packets from any source is accepted once
    * this has been called. This is the expected behavior of pre-ICE SIP.
@@ -1107,6 +1111,8 @@ nice_component_init (NiceComponent *component)
 
   g_queue_init (&component->queued_tcp_packets);
   g_queue_init (&component->incoming_checks);
+
+  component->have_local_consent = TRUE;
 }
 
 static void
index 2e6c496..788e539 100644 (file)
@@ -65,17 +65,23 @@ G_BEGIN_DECLS
 
 typedef struct _CandidatePair CandidatePair;
 typedef struct _CandidatePairKeepalive CandidatePairKeepalive;
+typedef struct _CandidatePairConsentCheck CandidatePairConsentCheck;
 typedef struct _IncomingCheck IncomingCheck;
 
 struct _CandidatePairKeepalive
 {
   guint64 next_tick;    /* next tick timestamp */
-  GSource *tick_source;
   guint stream_id;
   guint component_id;
   StunTimer timer;
-  uint8_t stun_buffer[STUN_MAX_MESSAGE_SIZE_IPV6];
-  StunMessage stun_message;
+};
+
+struct _CandidatePairConsentCheck
+{
+  GSource *tick_source;
+  gboolean have;
+  guint64 last_received;        /* g_get_monotonic_time() of last remote
+                                   consent received */
 };
 
 struct _CandidatePair
@@ -85,6 +91,7 @@ struct _CandidatePair
   guint64 priority;           /* candidate pair priority */
   guint32 stun_priority;
   CandidatePairKeepalive keepalive;
+  CandidatePairConsentCheck remote_consent;
 };
 
 struct _IncomingCheck
@@ -224,6 +231,8 @@ struct _NiceComponent {
    * ACKs on. The messages are dequeued to the pseudo-TCP socket once a selected
    * UDP socket is available. This is only used for reliable Components. */
   GQueue queued_tcp_packets;
+
+  gboolean have_local_consent;
 };
 
 typedef struct {
index 680787d..522c980 100644 (file)
@@ -1234,66 +1234,43 @@ static gboolean priv_conn_check_tick_agent_locked (NiceAgent *agent,
   return TRUE;
 }
 
-static gboolean priv_conn_keepalive_retransmissions_tick_agent_locked (
+static gboolean priv_conn_remote_consent_tick_agent_locked (
     NiceAgent *agent, gpointer pointer)
 {
   CandidatePair *pair = (CandidatePair *) pointer;
+  guint64 consent_timeout = 0;
+  guint64 now;
 
-  g_source_destroy (pair->keepalive.tick_source);
-  g_source_unref (pair->keepalive.tick_source);
-  pair->keepalive.tick_source = NULL;
-
-  switch (stun_timer_refresh (&pair->keepalive.timer)) {
-    case STUN_USAGE_TIMER_RETURN_TIMEOUT:
-      {
-        /* Time out */
-        StunTransactionId id;
-        NiceComponent *component;
-
-        if (!agent_find_component (agent,
-                pair->keepalive.stream_id, pair->keepalive.component_id,
-                NULL, &component)) {
-          nice_debug ("Could not find stream or component in"
-              " priv_conn_keepalive_retransmissions_tick");
-          return FALSE;
-        }
-
-        stun_message_id (&pair->keepalive.stun_message, id);
-        stun_agent_forget_transaction (&component->stun_agent, id);
-        pair->keepalive.stun_message.buffer = NULL;
-
-        if (agent->media_after_tick) {
-          nice_debug ("Agent %p : Keepalive conncheck timed out!! "
-              "but media was received. Suspecting keepalive lost because of "
-              "network bottleneck", agent);
-        } else {
-          nice_debug ("Agent %p : Keepalive conncheck timed out!! "
-              "peer probably lost connection", agent);
-          agent_signal_component_state_change (agent,
-              pair->keepalive.stream_id, pair->keepalive.component_id,
-              NICE_COMPONENT_STATE_FAILED);
-        }
-        break;
-      }
-    case STUN_USAGE_TIMER_RETURN_RETRANSMIT:
-      /* Retransmit */
-      agent_socket_send (pair->local->sockptr, &pair->remote->c.addr,
-          stun_message_length (&pair->keepalive.stun_message),
-          (gchar *)pair->keepalive.stun_buffer);
+  if (pair->remote_consent.tick_source) {
+    g_source_destroy (pair->remote_consent.tick_source);
+    g_source_unref (pair->remote_consent.tick_source);
+  }
+  pair->remote_consent.tick_source = NULL;
 
-      nice_debug ("Agent %p : Retransmitting keepalive conncheck",
-          agent);
+  if (agent->consent_freshness) {
+    consent_timeout = NICE_AGENT_TIMER_CONSENT_TIMEOUT * 1000;
+  } else {
+    consent_timeout = NICE_AGENT_TIMER_KEEPALIVE_TIMEOUT* 1000;
+  }
 
-      G_GNUC_FALLTHROUGH;
-    case STUN_USAGE_TIMER_RETURN_SUCCESS:
-      agent_timeout_add_with_context (agent,
-          &pair->keepalive.tick_source,
-          "Pair keepalive", stun_timer_remainder (&pair->keepalive.timer),
-          priv_conn_keepalive_retransmissions_tick_agent_locked, pair);
-      break;
-    default:
-      g_assert_not_reached();
-      break;
+  now = g_get_monotonic_time();
+  if (now - pair->remote_consent.last_received > consent_timeout) {
+    guint64 time_since = now - pair->remote_consent.last_received;
+    pair->remote_consent.have = FALSE;
+    nice_debug ("Agent %p : pair %p consent for stream/component %u/%u timed "
+         "out! -> FAILED.  Last consent received: %" G_GUINT64_FORMAT ".%" G_GUINT64_FORMAT "s ago",
+        agent, pair, pair->keepalive.stream_id, pair->keepalive.component_id,
+        time_since / G_USEC_PER_SEC, time_since % G_USEC_PER_SEC);
+    agent_signal_component_state_change (agent, pair->keepalive.stream_id,
+        pair->keepalive.component_id, NICE_COMPONENT_STATE_FAILED);
+  } else {
+    guint64 delay = (consent_timeout - now - pair->remote_consent.last_received) / 1000;
+    nice_debug ("Agent %p : pair %p rechecking consent in %" G_GUINT64_FORMAT ".%03" G_GUINT64_FORMAT "s",
+        agent, pair, delay / 1000, delay % 1000);
+    agent_timeout_add_with_context (agent,
+        &pair->remote_consent.tick_source,
+        "Pair remote consent", delay,
+        priv_conn_remote_consent_tick_agent_locked, pair);
   }
 
   return FALSE;
@@ -1400,12 +1377,17 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
   guint64 next_timer_tick;
 
   now = g_get_monotonic_time ();
-  min_next_tick = now + 1000 * NICE_AGENT_TIMER_TR_DEFAULT;
+  if (agent->consent_freshness) {
+    min_next_tick = now + 1000 * NICE_AGENT_TIMER_MIN_CONSENT_INTERVAL;
+  } else {
+    min_next_tick = now + 1000 * NICE_AGENT_TIMER_TR_DEFAULT;
+  }
 
   /* case 1: session established and media flowing
    *         (ref ICE sect 11 "Keepalives" RFC-8445)
-   * TODO: keepalives should be send only when no packet has been sent
-   * on that pair in the last Tr seconds, and not unconditionally.
+   * TODO: without RFC 7675 (consent freshness), keepalives should be sent
+   * only when no packet has been sent on that pair in the last Tr seconds,
+   * and not unconditionally.
    */
   for (i = agent->streams; i; i = i->next) {
 
@@ -1417,7 +1399,7 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
 
         /* Disable keepalive checks on TCP candidates unless explicitly enabled */
         if (p->local->c.transport != NICE_CANDIDATE_TRANSPORT_UDP &&
-            !agent->keepalive_conncheck)
+            !NICE_AGENT_DO_KEEPALIVE_CONNCHECKS (agent))
           continue;
 
         if (p->keepalive.next_tick) {
@@ -1427,8 +1409,7 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
             continue;
         }
 
-        if (agent->compatibility == NICE_COMPATIBILITY_GOOGLE ||
-            agent->keepalive_conncheck) {
+        if (NICE_AGENT_DO_KEEPALIVE_CONNCHECKS (agent)) {
           uint8_t uname[NICE_STREAM_MAX_UNAME];
           size_t uname_len =
               priv_create_username (agent, agent_find_stream (agent, stream->id),
@@ -1438,29 +1419,24 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
           size_t password_len = priv_get_password (agent,
               agent_find_stream (agent, stream->id),
               (NiceCandidate *) p->remote, &password);
+          uint8_t stun_buffer[STUN_MAX_MESSAGE_SIZE_IPV6];
+          StunMessage stun_message;
 
-          if (p->keepalive.stun_message.buffer != NULL) {
-            nice_debug ("Agent %p: Keepalive for s%u:c%u still"
-                " retransmitting, not restarting", agent, stream->id,
-                component->id);
-            continue;
-          }
-
-          if (nice_debug_is_enabled ()) {
-            gchar tmpbuf[INET6_ADDRSTRLEN];
-            nice_address_to_string (&p->remote->c.addr, tmpbuf);
-            nice_debug ("Agent %p : Keepalive STUN-CC REQ to '%s:%u', "
-                "(c-id:%u), username='%.*s' (%" G_GSIZE_FORMAT "), "
-                "password='%.*s' (%" G_GSIZE_FORMAT "), priority=%08x.",
-                agent, tmpbuf, nice_address_get_port (&p->remote->c.addr),
-                component->id, (int) uname_len, uname, uname_len,
-                (int) password_len, password, password_len,
-                p->stun_priority);
-          }
           if (uname_len > 0) {
+            if (nice_debug_is_enabled ()) {
+              gchar tmpbuf[INET6_ADDRSTRLEN];
+              nice_address_to_string (&p->remote->c.addr, tmpbuf);
+              nice_debug ("Agent %p : Keepalive STUN-CC REQ to '%s:%u', "
+                  "(c-id:%u), username='%.*s' (%" G_GSIZE_FORMAT "), "
+                  "password='%.*s' (%" G_GSIZE_FORMAT "), priority=%08x.",
+                  agent, tmpbuf, nice_address_get_port (&p->remote->c.addr),
+                  component->id, (int) uname_len, uname, uname_len,
+                  (int) password_len, password, password_len,
+                  p->stun_priority);
+            }
+
             buf_len = stun_usage_ice_conncheck_create (&component->stun_agent,
-                &p->keepalive.stun_message, p->keepalive.stun_buffer,
-                sizeof(p->keepalive.stun_buffer),
+                &stun_message, stun_buffer, sizeof(stun_buffer),
                 uname, uname_len, password, password_len,
                 agent->controlling_mode, agent->controlling_mode,
                 p->stun_priority,
@@ -1469,27 +1445,31 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
                 agent_to_ice_compatibility (agent));
 
             nice_debug ("Agent %p: conncheck created %zd - %p",
-                agent, buf_len, p->keepalive.stun_message.buffer);
+                agent, buf_len, stun_message.buffer);
 
             if (buf_len > 0) {
-              stun_timer_start (&p->keepalive.timer,
-                  agent->stun_initial_timeout,
-                  agent->stun_max_retransmissions);
+              /* random range over 0.8 -> 1.2 as specified in RFC7675 */
+              double modifier = g_random_double() * 0.4 + 0.8;
+              guint64 delay = 1000 * MAX((guint64) ((NICE_AGENT_TIMER_CONSENT_DEFAULT) * modifier),
+                  NICE_AGENT_TIMER_MIN_CONSENT_INTERVAL);
+
+              p->keepalive.stream_id = stream->id;
+              p->keepalive.component_id = component->id;
+              p->keepalive.next_tick = now + delay;
+
+              if (p->remote_consent.have) {
+                if (p->remote_consent.last_received == 0) {
+                  p->remote_consent.last_received = g_get_monotonic_time();
+                }
+
+                priv_conn_remote_consent_tick_agent_locked (agent, p);
+              }
 
               agent->media_after_tick = FALSE;
 
               /* send the conncheck */
               agent_socket_send (p->local->sockptr, &p->remote->c.addr,
-                  buf_len, (gchar *)p->keepalive.stun_buffer);
-
-              p->keepalive.stream_id = stream->id;
-              p->keepalive.component_id = component->id;
-              p->keepalive.next_tick = now + 1000 * NICE_AGENT_TIMER_TR_DEFAULT;
-
-              agent_timeout_add_with_context (agent,
-                  &p->keepalive.tick_source, "Pair keepalive",
-                  stun_timer_remainder (&p->keepalive.timer),
-                  priv_conn_keepalive_retransmissions_tick_agent_locked, p);
+                  buf_len, (gchar *) stun_buffer);
 
               next_timer_tick = now + agent->timer_ta * 1000;
               goto done;
@@ -1498,18 +1478,20 @@ static gboolean priv_conn_keepalive_tick_unlocked (NiceAgent *agent)
             }
           }
         } else {
+          uint8_t stun_buffer[STUN_MAX_MESSAGE_SIZE_IPV6];
+          StunMessage stun_message;
+
           buf_len = stun_usage_bind_keepalive (&component->stun_agent,
-              &p->keepalive.stun_message, p->keepalive.stun_buffer,
-              sizeof(p->keepalive.stun_buffer));
+              &stun_message, stun_buffer, sizeof(stun_buffer));
 
           if (buf_len > 0) {
             agent_socket_send (p->local->sockptr, &p->remote->c.addr, buf_len,
-                (gchar *)p->keepalive.stun_buffer);
+                (gchar *) stun_buffer);
 
             p->keepalive.next_tick = now + 1000 * NICE_AGENT_TIMER_TR_DEFAULT;
 
             if (agent->compatibility == NICE_COMPATIBILITY_OC2007R2) {
-              ms_ice2_legacy_conncheck_send (&p->keepalive.stun_message,
+              ms_ice2_legacy_conncheck_send (&stun_message,
                   p->local->sockptr, &p->remote->c.addr);
             }
 
@@ -2082,6 +2064,7 @@ conn_check_update_selected_pair (NiceAgent *agent, NiceComponent *component,
     cpair.remote = (NiceCandidateImpl *) pair->remote;
     cpair.priority = pair->priority;
     cpair.stun_priority = pair->stun_priority;
+    cpair.remote_consent.have = TRUE;
 
     nice_component_update_selected_pair (agent, component, &cpair);
 
@@ -4291,27 +4274,13 @@ static gboolean priv_map_reply_to_relay_remove (NiceAgent *agent,
 static gboolean priv_map_reply_to_keepalive_conncheck (NiceAgent *agent,
     NiceComponent *component, StunMessage *resp)
 {
-  StunTransactionId conncheck_id;
-  StunTransactionId response_id;
-  stun_message_id (resp, response_id);
-
-  if (component->selected_pair.keepalive.stun_message.buffer) {
-      stun_message_id (&component->selected_pair.keepalive.stun_message,
-          conncheck_id);
-      if (memcmp (conncheck_id, response_id, sizeof(StunTransactionId)) == 0) {
-        nice_debug ("Agent %p : Keepalive for selected pair received.",
-            agent);
-        if (component->selected_pair.keepalive.tick_source) {
-          g_source_destroy (component->selected_pair.keepalive.tick_source);
-          g_source_unref (component->selected_pair.keepalive.tick_source);
-          component->selected_pair.keepalive.tick_source = NULL;
-        }
-        component->selected_pair.keepalive.stun_message.buffer = NULL;
-        return TRUE;
-      }
+  nice_debug ("Agent %p : Keepalive for selected pair %p received.",
+      agent, &component->selected_pair);
+  if (agent->consent_freshness) {
+    guint64 now = g_get_monotonic_time();
+    component->selected_pair.remote_consent.last_received = now;
   }
-
-  return FALSE;
+  return TRUE;
 }
 
 
@@ -4602,6 +4571,41 @@ gboolean conn_check_handle_inbound_stun (NiceAgent *agent, NiceStream *stream,
     return TRUE;
   }
 
+  if (valid == STUN_VALIDATION_FORBIDDEN) {
+    CandidatePair *pair = &component->selected_pair;
+    gchar tmpbuf[INET6_ADDRSTRLEN];
+    nice_address_to_string (from, tmpbuf);
+    nice_debug ("Agent %p : received 403: 'Forbidden' for %u/%u (stream/component) from [%s]:%u",
+        agent, stream->id, component->id, tmpbuf, nice_address_get_port (from));
+
+    for (i = stream->conncheck_list; i; i = i->next) {
+      CandidateCheckPair *p = i->data;
+
+      if (nice_address_equal (from, &p->remote->addr)) {
+        candidate_check_pair_fail (stream, agent, p);
+      }
+    }
+
+    /* if the pair was selected, it is no longer useful */
+    if (nice_address_equal (from, &pair->remote->c.addr)) {
+      pair->remote_consent.have = FALSE;
+      nice_debug ("Agent %p : pair %p lost consent for %u/%u (stream/component)",
+          agent, pair, stream->id, component->id);
+
+      /* explicit revocation received, we don't need to time out anymore */
+      if (pair->remote_consent.tick_source) {
+        g_source_destroy (pair->remote_consent.tick_source);
+        g_source_unref (pair->remote_consent.tick_source);
+        pair->remote_consent.tick_source = NULL;
+      }
+
+      agent_signal_component_state_change (agent, stream->id, component->id,
+          NICE_COMPONENT_STATE_FAILED);
+    }
+
+    return TRUE;
+  }
+
   username = (uint8_t *) stun_message_find (&req, STUN_ATTRIBUTE_USERNAME,
                                            &username_len);
 
@@ -4733,6 +4737,22 @@ gboolean conn_check_handle_inbound_stun (NiceAgent *agent, NiceStream *stream,
       }
     }
 
+    if (!component->have_local_consent) {
+      /* RFC 7675: return forbidden to all authenticated requests if we should
+       * signal lost consent */
+      nice_debug("Agent %p : returning FORBIDDEN on stream/component %u/%u "
+          "for lost local consent", agent, stream->id, component->id);
+      if (stun_agent_init_error (&component->stun_agent, &msg, rbuf, rbuf_len,
+              &req, STUN_ERROR_FORBIDDEN)) {
+        rbuf_len = stun_agent_finish_message (&component->stun_agent, &msg, NULL, 0);
+        if (rbuf_len > 0 && agent->compatibility != NICE_COMPATIBILITY_MSN &&
+              agent->compatibility != NICE_COMPATIBILITY_OC2007) {
+          agent_socket_send (nicesock, from, rbuf_len, (const gchar*) rbuf);
+        }
+        return TRUE;
+      }
+    }
+
     rbuf_len = sizeof (rbuf);
     res = stun_usage_ice_conncheck_create_reply (&component->stun_agent, &req,
         &msg, rbuf, &rbuf_len, &sockaddr.storage, sizeof (sockaddr),
index 50fa0b1..358ad28 100644 (file)
 
 #define NICE_CANDIDATE_PAIR_MAX_FOUNDATION        NICE_CANDIDATE_MAX_FOUNDATION*2
 
+/* A helper macro to test whether connection checks should continue to be
+ * performed after a component has successfully connected */
+#define NICE_AGENT_DO_KEEPALIVE_CONNCHECKS(obj) \
+  ((obj)->consent_freshness || (obj)->keepalive_conncheck || (obj)->compatibility == NICE_COMPATIBILITY_GOOGLE)
+
 /**
  * NiceCheckState:
  * @NICE_CHECK_WAITING: Waiting to be scheduled.
index bcca5f4..c4741dc 100644 (file)
       <title>Index of new symbols in 0.1.18</title>
       <xi:include href="xml/api-index-0.1.18.xml"><xi:fallback/></xi:include>
     </index>
+    <index role="0.1.20">
+      <title>Index of new symbols in 0.1.20</title>
+      <xi:include href="xml/api-index-0.1.20.xml"><xi:fallback/></xi:include>
+    </index>
     <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
   </part>
 </book>
index f5c13d7..5c13bce 100644 (file)
@@ -57,6 +57,7 @@ nice_agent_get_selected_socket
 nice_agent_get_sockets
 nice_agent_get_component_state
 nice_agent_close_async
+nice_agent_consent_lost
 nice_component_state_to_string
 <SUBSECTION Standard>
 NICE_AGENT
index 007be55..1362691 100644 (file)
@@ -18,6 +18,7 @@ nice_address_to_string
 nice_agent_add_local_address
 nice_agent_add_stream
 nice_agent_close_async
+nice_agent_consent_lost
 nice_agent_recv
 nice_agent_recv_messages
 nice_agent_recv_nonblocking
index 26adb9f..e873e55 100644 (file)
@@ -337,6 +337,13 @@ StunValidationStatus stun_agent_validate (StunAgent *agent, StunMessage *msg,
     }
   }
 
+  if (agent->usage_flags & STUN_AGENT_USAGE_CONSENT_FRESHNESS &&
+      stun_message_get_class (msg) == STUN_ERROR) {
+    stun_message_find_error (msg, &error_code);
+    if (error_code == STUN_ERROR_FORBIDDEN) {
+      return STUN_VALIDATION_FORBIDDEN;
+    }
+  }
 
   if (sent_id_idx != -1 && sent_id_idx < STUN_AGENT_MAX_SAVED_IDS) {
     agent->sent_ids[sent_id_idx].valid = FALSE;
index 95e89fd..fba972e 100644 (file)
@@ -123,6 +123,9 @@ typedef enum {
  * @STUN_VALIDATION_UNKNOWN_ATTRIBUTE: The message is valid but contains one
  * or more unknown comprehension attributes. This is a response, or error,
  * or indication message and no error response should be sent
+ * @STUN_VALIDATION_FORBIDDEN: The message response is valid and indicates
+ * the peer responded with the error code 403 'Forbidden'.  No response
+ * should be sent.
  *
  * This enum is used as the return value of stun_agent_validate() and represents
  * the status result of the validation of a STUN message.
@@ -137,6 +140,7 @@ typedef enum {
   STUN_VALIDATION_UNMATCHED_RESPONSE,
   STUN_VALIDATION_UNKNOWN_REQUEST_ATTRIBUTE,
   STUN_VALIDATION_UNKNOWN_ATTRIBUTE,
+  STUN_VALIDATION_FORBIDDEN,
 } StunValidationStatus;
 
 /**
@@ -168,6 +172,9 @@ typedef enum {
  * @STUN_AGENT_USAGE_NO_ALIGNED_ATTRIBUTES: The agent should not assume STUN
  * attributes are aligned on 32-bit boundaries when parsing messages and also
  * do not add padding when creating messages.
+ * @STUN_AGENT_USAGE_CONSENT_FRESHNESS: The agent should expect and use
+ * the %STUN_VALIDATION_FORBIDDEN return value from ERROR-CODE responses and
+ * abort all transactions accordingly.
  *
  * This enum defines a bitflag usages for a #StunAgent and they will define how
  * the agent should behave, independently of the compatibility mode it uses.
@@ -183,6 +190,7 @@ typedef enum {
   STUN_AGENT_USAGE_NO_INDICATION_AUTH        = (1 << 5),
   STUN_AGENT_USAGE_FORCE_VALIDATER           = (1 << 6),
   STUN_AGENT_USAGE_NO_ALIGNED_ATTRIBUTES     = (1 << 7),
+  STUN_AGENT_USAGE_CONSENT_FRESHNESS         = (1 << 8),
 } StunAgentUsageFlags;
 
 
index 0ac9977..cfa5ca2 100644 (file)
@@ -414,6 +414,8 @@ typedef uint8_t StunTransactionId[STUN_MESSAGE_TRANS_ID_LEN];
  * "Bad Request" error as defined in RFC5389
  * @STUN_ERROR_UNAUTHORIZED: The ERROR-CODE value for the
  * "Unauthorized" error as defined in RFC5389
+ * @STUN_ERROR_FORBIDDEN: The ERROR-CODE value for the
+ * "Forbidden" error as defined in RFC7675
  * @STUN_ERROR_UNKNOWN_ATTRIBUTE: The ERROR-CODE value for the
  * "Unknown Attribute" error as defined in RFC5389
  * @STUN_ERROR_ALLOCATION_MISMATCH:The ERROR-CODE value for the
@@ -457,6 +459,7 @@ typedef enum
   STUN_ERROR_TRY_ALTERNATE=300,      /* RFC5389 */
   STUN_ERROR_BAD_REQUEST=400,      /* RFC5389 */
   STUN_ERROR_UNAUTHORIZED=401,      /* RFC5389 */
+  STUN_ERROR_FORBIDDEN=403,      /* RFC7675 */
   STUN_ERROR_UNKNOWN_ATTRIBUTE=420,    /* RFC5389 */
   STUN_ERROR_ALLOCATION_MISMATCH=437,   /* TURN-12 */
   STUN_ERROR_STALE_NONCE=438,      /* RFC5389 */
index 65ac23a..685d11d 100644 (file)
@@ -28,7 +28,8 @@ nice_tests = [
   'test-drop-invalid',
   'test-nomination',
   'test-interfaces',
-  'test-set-port-range'
+  'test-set-port-range',
+  'test-consent',
 ]
 
 if cc.has_header('arpa/inet.h')
diff --git a/tests/test-consent.c b/tests/test-consent.c
new file mode 100644 (file)
index 0000000..8159c7e
--- /dev/null
@@ -0,0 +1,520 @@
+/*
+ * This file is part of the Nice GLib ICE library.
+ *
+ * (C) 2007 Nokia Corporation. All rights reserved.
+ *  Contact: Kai Vehmanen
+ * (C) 2020 Matthew Waters <matthew@centricular.com>
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Nice GLib ICE library.
+ *
+ * The Initial Developers of the Original Code are Collabora Ltd and Nokia
+ * Corporation. All Rights Reserved.
+ *
+ * Contributors:
+ *   Kai Vehmanen, Nokia
+ *   Matthew Waters, Centricular
+ *
+ * Alternatively, the contents of this file may be used under the terms of the
+ * the GNU Lesser General Public License Version 2.1 (the "LGPL"), in which
+ * case the provisions of LGPL are applicable instead of those above. If you
+ * wish to allow use of your version of this file only under the terms of the
+ * LGPL and not to allow others to use your version of this file under the
+ * MPL, indicate your decision by deleting the provisions above and replace
+ * them with the notice and other provisions required by the LGPL. If you do
+ * not delete the provisions above, a recipient may use your version of this
+ * file under either the MPL or the LGPL.
+ */
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include "agent.h"
+#include "agent-priv.h" /* for testing purposes */
+
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include <io.h>
+#endif
+
+static NiceComponentState global_lagent_state = NICE_COMPONENT_STATE_LAST;
+static NiceComponentState global_ragent_state = NICE_COMPONENT_STATE_LAST;
+static guint global_components_ready = 0;
+static guint global_components_ready_exit = 0;
+static guint global_components_failed = 0;
+static guint global_components_failed_exit = 0;
+static GMainLoop *global_mainloop = NULL;
+static gboolean global_lagent_gathering_done = FALSE;
+static gboolean global_ragent_gathering_done = FALSE;
+static gboolean global_lagent_ibr_received = FALSE;
+static gboolean global_ragent_ibr_received = FALSE;
+static int global_lagent_cands = 0;
+static int global_ragent_cands = 0;
+static gint global_ragent_read = 0;
+static gint global_ragent_read_exit = 0;
+
+static void priv_print_global_status (void)
+{
+  g_debug ("\tgathering_done=%d", global_lagent_gathering_done && global_ragent_gathering_done);
+  g_debug ("\tlstate=%d", global_lagent_state);
+  g_debug ("\trstate=%d", global_ragent_state);
+}
+
+static gboolean timer_cb (gpointer pointer)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, pointer);
+
+  /* signal status via a global variable */
+
+  /* note: should not be reached, abort */
+  g_debug ("ERROR: test has got stuck, aborting...");
+  exit (-1);
+
+}
+
+static void cb_nice_recv (NiceAgent *agent, guint stream_id, guint component_id, guint len, gchar *buf, gpointer user_data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, user_data);
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent; (void)stream_id; (void)component_id; (void)buf;
+
+  if ((intptr_t)user_data == 2) {
+    global_ragent_read += len;
+
+    g_debug ("test-consent: read %u/%u", global_ragent_read, global_ragent_read_exit);
+
+    if (global_ragent_read == global_ragent_read_exit) {
+      g_debug ("test-consent: quit mainloop: read enough data");
+      g_main_loop_quit (global_mainloop);
+    }
+  }
+}
+
+static void cb_candidate_gathering_done(NiceAgent *agent, guint stream_id, gpointer data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, data);
+
+  if ((intptr_t)data == 1)
+    global_lagent_gathering_done = TRUE;
+  else if ((intptr_t)data == 2)
+    global_ragent_gathering_done = TRUE;
+
+  if (global_lagent_gathering_done &&
+      global_ragent_gathering_done) {
+    g_debug ("test-consent: quit mainloop: gathering completed");
+    g_main_loop_quit (global_mainloop);
+  }
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent;
+}
+
+static void cb_component_state_changed (NiceAgent *agent, guint stream_id, guint component_id, guint state, gpointer data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, data);
+
+  if ((intptr_t)data == 1)
+    global_lagent_state = state;
+  else if ((intptr_t)data == 2)
+    global_ragent_state = state;
+  
+  if (state == NICE_COMPONENT_STATE_READY)
+    global_components_ready++;
+  if (state == NICE_COMPONENT_STATE_FAILED)
+    global_components_failed++;
+
+  g_debug ("test-consent: READY %u/%u FAILED %u/%u.", global_components_ready, global_components_ready_exit, global_components_failed, global_components_failed_exit);
+
+  /* signal status via a global variable */
+  if (global_components_ready == global_components_ready_exit) {
+    g_debug ("test-consent: quit mainloop: components ready");
+    g_main_loop_quit (global_mainloop); 
+    return;
+  }
+
+  /* signal status via a global variable */
+  if (global_components_failed == global_components_failed_exit) {
+    g_debug ("test-consent: quit mainloop: components failed");
+    g_main_loop_quit (global_mainloop); 
+    return;
+  }
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent; (void)stream_id; (void)data; (void)component_id;
+}
+
+static void cb_new_selected_pair(NiceAgent *agent, guint stream_id, guint component_id, 
+                                gchar *lfoundation, gchar* rfoundation, gpointer data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, data);
+
+  if ((intptr_t)data == 1)
+    ++global_lagent_cands;
+  else if ((intptr_t)data == 2)
+    ++global_ragent_cands;
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent; (void)stream_id; (void)component_id; (void)lfoundation; (void)rfoundation;
+}
+
+static void cb_new_candidate(NiceAgent *agent, guint stream_id, guint component_id, 
+                            gchar *foundation, gpointer data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, data);
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent; (void)stream_id; (void)data; (void)component_id; (void)foundation;
+}
+
+static void cb_initial_binding_request_received(NiceAgent *agent, guint stream_id, gpointer data)
+{
+  g_debug ("test-consent:%s: %p", G_STRFUNC, data);
+
+  if ((intptr_t)data == 1)
+    global_lagent_ibr_received = TRUE;
+  else if ((intptr_t)data == 2)
+    global_ragent_ibr_received = TRUE;
+
+  /* XXX: dear compiler, these are for you: */
+  (void)agent; (void)stream_id; (void)data;
+}
+
+static void priv_get_local_addr (NiceAgent *agent, guint stream_id, guint component_id, NiceAddress *dstaddr)
+{
+  GSList *cands, *i;
+  cands = nice_agent_get_local_candidates(agent, stream_id, component_id);
+  for (i = cands; i; i = i->next) {
+    NiceCandidate *cand = i->data;
+    if (cand) {
+      g_assert (dstaddr);
+      *dstaddr = cand->addr;
+    }
+  }
+  for (i = cands; i; i = i->next)
+    nice_candidate_free ((NiceCandidate *) i->data);
+  g_slist_free (cands);
+}
+
+static int run_consent_test (NiceAgent *lagent, NiceAgent *ragent, NiceAddress *baseaddr)
+{
+  NiceAddress laddr, raddr, laddr_rtcp, raddr_rtcp;   
+  NiceCandidate cdes;
+  GSList *cands;
+  guint ls_id, rs_id;
+
+  /* XXX: dear compiler, these are for you: */
+  (void)baseaddr;
+
+  memset (&cdes, 0, sizeof(NiceCandidate));
+  cdes.priority = 10000;
+  strcpy (cdes.foundation, "1");
+  cdes.type = NICE_CANDIDATE_TYPE_HOST;
+  cdes.transport = NICE_CANDIDATE_TRANSPORT_UDP;
+
+  /* step: initialize variables modified by the callbacks */
+  global_components_ready = 0;
+  global_components_ready_exit = 4;
+  global_components_failed = 0;
+  global_components_failed_exit = 4;
+  global_lagent_gathering_done = FALSE;
+  global_ragent_gathering_done = FALSE;
+  global_lagent_ibr_received =
+    global_ragent_ibr_received = FALSE;
+  global_lagent_cands = 
+    global_ragent_cands = 0;
+  global_ragent_read_exit = 32;
+
+  g_object_set (G_OBJECT (lagent), "controlling-mode", TRUE,
+      "consent-freshness", TRUE, NULL);
+  g_object_set (G_OBJECT (ragent), "controlling-mode", FALSE,
+      "consent-freshness", TRUE, NULL);
+
+  /* step: add one stream, with RTP+RTCP components, to each agent */
+  ls_id = nice_agent_add_stream (lagent, 2);
+  rs_id = nice_agent_add_stream (ragent, 2);
+  g_assert_cmpuint (ls_id, >, 0);
+  g_assert_cmpuint (rs_id, >, 0);
+
+  nice_agent_gather_candidates (lagent, ls_id);
+  nice_agent_gather_candidates (ragent, rs_id);
+
+  /* step: attach to mainloop (needed to register the fds) */
+  nice_agent_attach_recv (lagent, ls_id, NICE_COMPONENT_TYPE_RTP,
+      g_main_loop_get_context (global_mainloop), cb_nice_recv, (gpointer)1);
+  nice_agent_attach_recv (lagent, ls_id, NICE_COMPONENT_TYPE_RTCP,
+      g_main_loop_get_context (global_mainloop), cb_nice_recv, (gpointer)1);
+  nice_agent_attach_recv (ragent, rs_id, NICE_COMPONENT_TYPE_RTP,
+      g_main_loop_get_context (global_mainloop), cb_nice_recv, (gpointer)2);
+  nice_agent_attach_recv (ragent, rs_id, NICE_COMPONENT_TYPE_RTCP,
+      g_main_loop_get_context (global_mainloop), cb_nice_recv, (gpointer)2);
+
+  /* step: run mainloop until local candidates are ready 
+   *       (see timer_cb() above) */
+  if (global_lagent_gathering_done != TRUE ||
+      global_ragent_gathering_done != TRUE) {
+    g_debug ("test-consent: Added streams, running mainloop until 'candidate-gathering-done'...");
+    g_main_loop_run (global_mainloop);
+    g_assert (global_lagent_gathering_done == TRUE);
+    g_assert (global_ragent_gathering_done == TRUE);
+  }
+
+  /* step: find out the local candidates of each agent */
+
+  priv_get_local_addr (ragent, rs_id, NICE_COMPONENT_TYPE_RTP, &raddr);
+  g_debug ("test-consent: local RTP port R %u",
+           nice_address_get_port (&raddr));
+
+  priv_get_local_addr (lagent, ls_id, NICE_COMPONENT_TYPE_RTP, &laddr);
+  g_debug ("test-consent: local RTP port L %u",
+           nice_address_get_port (&laddr));
+
+  priv_get_local_addr (ragent, rs_id, NICE_COMPONENT_TYPE_RTCP, &raddr_rtcp);
+  g_debug ("test-consent: local RTCP port R %u",
+           nice_address_get_port (&raddr_rtcp));
+
+  priv_get_local_addr (lagent, ls_id, NICE_COMPONENT_TYPE_RTCP, &laddr_rtcp);
+  g_debug ("test-consent: local RTCP port L %u",
+           nice_address_get_port (&laddr_rtcp));
+
+  /* step: pass the remote candidates to agents  */
+  cands = g_slist_append (NULL, &cdes);
+  {
+      gchar *ufrag = NULL, *password = NULL;
+      nice_agent_get_local_credentials(lagent, ls_id, &ufrag, &password);
+      nice_agent_set_remote_credentials (ragent,
+                                        rs_id, ufrag, password);
+      g_free (ufrag);
+      g_free (password);
+      nice_agent_get_local_credentials(ragent, rs_id, &ufrag, &password);
+      nice_agent_set_remote_credentials (lagent,
+                                        ls_id, ufrag, password);
+      g_free (ufrag);
+      g_free (password);
+  }
+  cdes.component_id = NICE_COMPONENT_TYPE_RTP;
+  cdes.addr = raddr;
+  nice_agent_set_remote_candidates (lagent, ls_id, NICE_COMPONENT_TYPE_RTP, cands);
+  cdes.addr = laddr;
+  nice_agent_set_remote_candidates (ragent, rs_id, NICE_COMPONENT_TYPE_RTP, cands);
+  cdes.component_id = NICE_COMPONENT_TYPE_RTCP;
+  cdes.addr = raddr_rtcp;
+  nice_agent_set_remote_candidates (lagent, ls_id, NICE_COMPONENT_TYPE_RTCP, cands);
+  cdes.addr = laddr_rtcp;
+  nice_agent_set_remote_candidates (ragent, rs_id, NICE_COMPONENT_TYPE_RTCP, cands);
+
+  g_debug ("test-consent: Set properties, next running mainloop until connectivity checks succeed...");
+
+  /* step: run the mainloop until connectivity checks succeed 
+   *       (see timer_cb() above) */
+  g_main_loop_run (global_mainloop);
+
+  /* note: verify that STUN binding requests were sent */
+  g_assert (global_lagent_ibr_received == TRUE);
+  g_assert (global_ragent_ibr_received == TRUE);
+  /* note: verify that correct number of local candidates were reported */
+  g_assert_cmpint (global_lagent_cands, ==, 2);
+  g_assert_cmpint (global_ragent_cands, ==, 2);
+  /* note: verify that agents are in correct state */
+  g_assert_cmpint (global_lagent_state, ==, NICE_COMPONENT_STATE_READY);
+  g_assert_cmpint (global_ragent_state, ==, NICE_COMPONENT_STATE_READY);
+
+  /* step: send a new test packet from L ot R */
+  global_ragent_read = 0;
+  g_assert_cmpint (nice_agent_send (lagent, ls_id, 1, 16, "1234567812345678"), ==, 16);
+
+  global_components_ready = 0;
+  global_components_failed = 0;
+
+  /* step: synthesize marking the components as consent failed */
+  g_assert (nice_agent_consent_lost (ragent, rs_id, NICE_COMPONENT_TYPE_RTP));
+  g_assert (nice_agent_consent_lost (ragent, rs_id, NICE_COMPONENT_TYPE_RTCP));
+
+  /* step: synthesize marking the components as consent failed */
+  g_assert (nice_agent_consent_lost (lagent, rs_id, NICE_COMPONENT_TYPE_RTP));
+  g_assert (nice_agent_consent_lost (lagent, rs_id, NICE_COMPONENT_TYPE_RTCP));
+
+  /* transition to failed will take roughly 4-6 seconds as that's the pacing
+   * of the consent connection checks */
+  g_debug ("test-consent: run loop: consent lost: waiting for components to transition to failed...");
+  g_main_loop_run (global_mainloop);
+
+  /* note: verify that ragent is in correct state after consent lost */
+  g_assert_cmpint (global_ragent_state, ==, NICE_COMPONENT_STATE_FAILED);
+
+  /* note: verify that lagent is in correct state after consent lost */
+  g_assert_cmpint (global_lagent_state, ==, NICE_COMPONENT_STATE_FAILED);
+
+  /* note: verify that all 4 components failed */
+  g_assert_cmpint (global_components_failed, ==, 4);
+
+  /* send another packet after consent lost before ice restart that will fail */
+  g_assert_cmpint (nice_agent_send (lagent, ls_id, 1, 16, "1234567812345678"), ==, -1);
+
+  g_debug ("test-consent: ICE restart...");
+  /* restart the agent to gather new credentials and clear the consent-lost */
+  nice_agent_restart (ragent);
+  nice_agent_restart (lagent);
+
+  {
+      gchar *ufrag = NULL, *password = NULL;
+      nice_agent_get_local_credentials(lagent, ls_id, &ufrag, &password);
+      nice_agent_set_remote_credentials (ragent,
+                                        rs_id, ufrag, password);
+      g_free (ufrag);
+      g_free (password);
+      nice_agent_get_local_credentials(ragent, rs_id, &ufrag, &password);
+      nice_agent_set_remote_credentials (lagent,
+                                        ls_id, ufrag, password);
+      g_free (ufrag);
+      g_free (password);
+  }
+
+  /* step: reset state variables */
+  global_lagent_ibr_received = FALSE;
+  global_ragent_ibr_received = FALSE;
+  global_components_ready = 0;
+  global_components_failed = 0;
+
+  /* step: exchange remote candidates */
+  cdes.component_id = NICE_COMPONENT_TYPE_RTP;
+  cdes.addr = raddr;
+  nice_agent_set_remote_candidates (lagent, ls_id, NICE_COMPONENT_TYPE_RTP, cands);
+  cdes.addr = laddr;
+  nice_agent_set_remote_candidates (ragent, rs_id, NICE_COMPONENT_TYPE_RTP, cands);
+  cdes.component_id = NICE_COMPONENT_TYPE_RTCP;
+  cdes.addr = raddr_rtcp;
+  nice_agent_set_remote_candidates (lagent, ls_id, NICE_COMPONENT_TYPE_RTCP, cands);
+  cdes.addr = laddr_rtcp;
+  nice_agent_set_remote_candidates (ragent, rs_id, NICE_COMPONENT_TYPE_RTCP, cands);
+
+  g_debug ("test-consent: run main loop after ICE restart...");
+  g_main_loop_run (global_mainloop);
+
+  /* note: verify binding requests were resent after restart */
+  g_assert (global_lagent_ibr_received == TRUE);
+  g_assert (global_ragent_ibr_received == TRUE);
+
+  /* send another packet after consent lost and after ice restart that will succeed */
+  g_assert_cmpint (nice_agent_send (lagent, ls_id, 1, 16, "1234567812345678"), ==, 16);
+
+  global_components_ready = 0;
+  global_components_failed = 0;
+
+  g_debug ("test-consent: run main loop to send packet after ICE restart ...");
+  g_main_loop_run (global_mainloop);
+
+  /* note: verify that payload was succesfully received */
+  g_assert_cmpint (global_ragent_read, ==, 32);
+  g_debug ("test-consent: Ran mainloop, removing streams...");
+
+  /* step: clean up resources and exit */
+
+  g_slist_free (cands);
+  nice_agent_remove_stream (lagent, ls_id);
+  nice_agent_remove_stream (ragent, rs_id);
+
+  return 0;
+}
+
+int main (void)
+{
+  NiceAgent *lagent, *ragent;      /* agent's L and R */
+  NiceAddress baseaddr;
+  int result;
+  guint timer_id;
+  const char *stun_server = NULL, *stun_server_port = NULL;
+
+#ifdef G_OS_WIN32
+  WSADATA w;
+
+  WSAStartup(0x0202, &w);
+#endif
+
+  global_mainloop = g_main_loop_new (NULL, FALSE);
+
+  /* Note: impl limits ...
+   * - no multi-stream support
+   * - no IPv6 support
+   */
+
+
+  /* step: create the agents L and R */
+  lagent = nice_agent_new_full (g_main_loop_get_context (global_mainloop), NICE_COMPATIBILITY_RFC5245, NICE_AGENT_OPTION_CONSENT_FRESHNESS);
+  ragent = nice_agent_new_full (g_main_loop_get_context (global_mainloop), NICE_COMPATIBILITY_RFC5245, NICE_AGENT_OPTION_CONSENT_FRESHNESS);
+  g_object_set (G_OBJECT (lagent), "ice-tcp", FALSE,  NULL);
+  g_object_set (G_OBJECT (ragent), "ice-tcp", FALSE,  NULL);
+
+  g_object_set (G_OBJECT (lagent), "upnp", FALSE, NULL);
+  g_object_set (G_OBJECT (ragent), "upnp", FALSE, NULL);
+
+  /* step: add a timer to catch state changes triggered by signals */
+  timer_id = g_timeout_add (30000, timer_cb, NULL);
+
+  /* step: specify which local interface to use */
+  if (!nice_address_set_from_string (&baseaddr, "127.0.0.1"))
+    g_assert_not_reached ();
+  nice_agent_add_local_address (lagent, &baseaddr);
+  nice_agent_add_local_address (ragent, &baseaddr);
+
+  g_signal_connect (G_OBJECT (lagent), "candidate-gathering-done", 
+                   G_CALLBACK (cb_candidate_gathering_done), (gpointer)1);
+  g_signal_connect (G_OBJECT (ragent), "candidate-gathering-done", 
+                   G_CALLBACK (cb_candidate_gathering_done), (gpointer)2);
+  g_signal_connect (G_OBJECT (lagent), "component-state-changed", 
+                   G_CALLBACK (cb_component_state_changed), (gpointer)1);
+  g_signal_connect (G_OBJECT (ragent), "component-state-changed", 
+                   G_CALLBACK (cb_component_state_changed), (gpointer)2);
+  g_signal_connect (G_OBJECT (lagent), "new-selected-pair", 
+                   G_CALLBACK (cb_new_selected_pair), (gpointer)1);
+  g_signal_connect (G_OBJECT (ragent), "new-selected-pair", 
+                   G_CALLBACK (cb_new_selected_pair), (gpointer)2);
+  g_signal_connect (G_OBJECT (lagent), "new-candidate", 
+                   G_CALLBACK (cb_new_candidate), (gpointer)1);
+  g_signal_connect (G_OBJECT (ragent), "new-candidate", 
+                   G_CALLBACK (cb_new_candidate), (gpointer)2);
+  g_signal_connect (G_OBJECT (lagent), "initial-binding-request-received", 
+                   G_CALLBACK (cb_initial_binding_request_received), (gpointer)1);
+  g_signal_connect (G_OBJECT (ragent), "initial-binding-request-received", 
+                   G_CALLBACK (cb_initial_binding_request_received), (gpointer)2);
+
+  stun_server = getenv ("NICE_STUN_SERVER");
+  stun_server_port = getenv ("NICE_STUN_SERVER_PORT");
+  if (stun_server) {
+    g_object_set (G_OBJECT (lagent), "stun-server", stun_server,  NULL);
+    g_object_set (G_OBJECT (lagent), "stun-server-port", atoi (stun_server_port),  NULL);
+    g_object_set (G_OBJECT (ragent), "stun-server", stun_server,  NULL);
+    g_object_set (G_OBJECT (ragent), "stun-server-port", atoi (stun_server_port),  NULL);
+  }
+
+  /* step: run test the first time */
+  g_debug ("test-consent: TEST STARTS / consent test");
+  result = run_consent_test (lagent, ragent, &baseaddr);
+  priv_print_global_status ();
+  g_assert_cmpint (result, ==, 0);
+  g_assert_cmpint (global_lagent_state, ==, NICE_COMPONENT_STATE_READY);
+  g_assert_cmpint (global_ragent_state, ==, NICE_COMPONENT_STATE_READY);
+
+  g_object_unref (lagent);
+  g_object_unref (ragent);
+
+
+  g_main_loop_unref (global_mainloop);
+  global_mainloop = NULL;
+
+  g_source_remove (timer_id);
+#ifdef G_OS_WIN32
+  WSACleanup();
+#endif
+  return result;
+}