diff --git a/remoting/client/chromoting_client.cc b/remoting/client/chromoting_client.cc
index 2d308c92c47a8..f87d9e8dccd74 100644
--- a/remoting/client/chromoting_client.cc
+++ b/remoting/client/chromoting_client.cc
@@ -119,6 +119,13 @@ void ChromotingClient::SetPairingResponse(
   user_interface_->SetPairingResponse(pairing_response);
 }
 
+void ChromotingClient::DeliverHostMessage(
+    const protocol::ExtensionMessage& message) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  user_interface_->DeliverHostMessage(message);
+}
+
 void ChromotingClient::InjectClipboardEvent(
     const protocol::ClipboardEvent& event) {
   DCHECK(task_runner_->BelongsToCurrentThread());
diff --git a/remoting/client/chromoting_client.h b/remoting/client/chromoting_client.h
index 625a4d8fd2e2e..58c8050d2771f 100644
--- a/remoting/client/chromoting_client.h
+++ b/remoting/client/chromoting_client.h
@@ -67,6 +67,8 @@ class ChromotingClient : public protocol::ConnectionToHost::HostEventCallback,
       const protocol::Capabilities& capabilities) OVERRIDE;
   virtual void SetPairingResponse(
       const protocol::PairingResponse& pairing_response) OVERRIDE;
+  virtual void DeliverHostMessage(
+      const protocol::ExtensionMessage& message) OVERRIDE;
 
   // ClipboardStub implementation for receiving clipboard data from host.
   virtual void InjectClipboardEvent(
diff --git a/remoting/client/client_user_interface.h b/remoting/client/client_user_interface.h
index 9779835cc9715..88d3e79699158 100644
--- a/remoting/client/client_user_interface.h
+++ b/remoting/client/client_user_interface.h
@@ -42,6 +42,10 @@ class ClientUserInterface {
   virtual void SetPairingResponse(
       const protocol::PairingResponse& pairing_response) = 0;
 
+  // Deliver an extension message from the host to the client.
+  virtual void DeliverHostMessage(
+      const protocol::ExtensionMessage& message) = 0;
+
   // Get the view's ClipboardStub implementation.
   virtual protocol::ClipboardStub* GetClipboardStub() = 0;
 
diff --git a/remoting/client/jni/chromoting_jni_instance.cc b/remoting/client/jni/chromoting_jni_instance.cc
index b68aab9f2c62b..067e954bca05e 100644
--- a/remoting/client/jni/chromoting_jni_instance.cc
+++ b/remoting/client/jni/chromoting_jni_instance.cc
@@ -176,6 +176,11 @@ void ChromotingJniInstance::SetPairingResponse(
                  response.shared_secret()));
 }
 
+void ChromotingJniInstance::DeliverHostMessage(
+    const protocol::ExtensionMessage& message) {
+  NOTIMPLEMENTED();
+}
+
 protocol::ClipboardStub* ChromotingJniInstance::GetClipboardStub() {
   return this;
 }
diff --git a/remoting/client/jni/chromoting_jni_instance.h b/remoting/client/jni/chromoting_jni_instance.h
index a8684bf4b8220..d066d6fe7dbbe 100644
--- a/remoting/client/jni/chromoting_jni_instance.h
+++ b/remoting/client/jni/chromoting_jni_instance.h
@@ -78,6 +78,8 @@ class ChromotingJniInstance
   virtual void SetCapabilities(const std::string& capabilities) OVERRIDE;
   virtual void SetPairingResponse(
       const protocol::PairingResponse& response) OVERRIDE;
+  virtual void DeliverHostMessage(
+      const protocol::ExtensionMessage& message) OVERRIDE;
   virtual protocol::ClipboardStub* GetClipboardStub() OVERRIDE;
   virtual protocol::CursorShapeStub* GetCursorShapeStub() OVERRIDE;
   virtual scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>
diff --git a/remoting/client/plugin/chromoting_instance.cc b/remoting/client/plugin/chromoting_instance.cc
index 8be169c9d2278..e22452a42168a 100644
--- a/remoting/client/plugin/chromoting_instance.cc
+++ b/remoting/client/plugin/chromoting_instance.cc
@@ -144,7 +144,7 @@ logging::LogMessageHandlerFunction g_logging_old_handler = NULL;
 const char ChromotingInstance::kApiFeatures[] =
     "highQualityScaling injectKeyEvent sendClipboardItem remapKey trapKey "
     "notifyClientDimensions notifyClientResolution pauseVideo pauseAudio "
-    "asyncPin thirdPartyAuth pinlessAuth";
+    "asyncPin thirdPartyAuth pinlessAuth extensionMessage";
 
 const char ChromotingInstance::kRequestedCapabilities[] = "";
 const char ChromotingInstance::kSupportedCapabilities[] = "desktopShape";
@@ -433,6 +433,13 @@ void ChromotingInstance::HandleMessage(const pp::Var& message) {
       return;
     }
     RequestPairing(client_name);
+  } else if (method == "extensionMessage") {
+    std::string type, message;
+    if (!data->GetString("type", &type) || !data->GetString("data", &message)) {
+      LOG(ERROR) << "Invalid extensionMessage.";
+      return;
+    }
+    SendClientMessage(type, message);
   }
 }
 
@@ -537,6 +544,14 @@ void ChromotingInstance::SetPairingResponse(
   PostChromotingMessage("pairingResponse", data.Pass());
 }
 
+void ChromotingInstance::DeliverHostMessage(
+    const protocol::ExtensionMessage& message) {
+  scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
+  data->SetString("type", message.type());
+  data->SetString("data", message.data());
+  PostChromotingMessage("extensionMessage", data.Pass());
+}
+
 void ChromotingInstance::FetchSecretFromDialog(
     bool pairing_supported,
     const protocol::SecretFetchedCallback& secret_fetched_callback) {
@@ -839,6 +854,17 @@ void ChromotingInstance::RequestPairing(const std::string& client_name) {
   host_connection_->host_stub()->RequestPairing(pairing_request);
 }
 
+void ChromotingInstance::SendClientMessage(const std::string& type,
+                                           const std::string& data) {
+  if (!IsConnected()) {
+    return;
+  }
+  protocol::ExtensionMessage message;
+  message.set_type(type);
+  message.set_data(data);
+  host_connection_->host_stub()->DeliverClientMessage(message);
+}
+
 ChromotingStats* ChromotingInstance::GetStats() {
   if (!client_.get())
     return NULL;
diff --git a/remoting/client/plugin/chromoting_instance.h b/remoting/client/plugin/chromoting_instance.h
index d7fd4481f3422..23c4c4996c9dc 100644
--- a/remoting/client/plugin/chromoting_instance.h
+++ b/remoting/client/plugin/chromoting_instance.h
@@ -120,6 +120,8 @@ class ChromotingInstance :
   virtual void SetCapabilities(const std::string& capabilities) OVERRIDE;
   virtual void SetPairingResponse(
       const protocol::PairingResponse& pairing_response) OVERRIDE;
+  virtual void DeliverHostMessage(
+      const protocol::ExtensionMessage& message) OVERRIDE;
   virtual protocol::ClipboardStub* GetClipboardStub() OVERRIDE;
   virtual protocol::CursorShapeStub* GetCursorShapeStub() OVERRIDE;
   virtual scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>
@@ -198,6 +200,7 @@ class ChromotingInstance :
   void OnThirdPartyTokenFetched(const std::string& token,
                                 const std::string& shared_secret);
   void RequestPairing(const std::string& client_name);
+  void SendClientMessage(const std::string& type, const std::string& data);
 
   // Helper method to post messages to the webapp.
   void PostChromotingMessage(const std::string& method,
diff --git a/remoting/host/client_session.cc b/remoting/host/client_session.cc
index 86bbe7ffa1280..933ded3e4748c 100644
--- a/remoting/host/client_session.cc
+++ b/remoting/host/client_session.cc
@@ -187,6 +187,13 @@ void ClientSession::RequestPairing(
   }
 }
 
+void ClientSession::DeliverClientMessage(
+    const protocol::ExtensionMessage& message) {
+  // No messages are currently supported.
+  LOG(INFO) << "Unexpected message received: "
+            << message.type() << ": " << message.data();
+}
+
 void ClientSession::OnConnectionAuthenticated(
     protocol::ConnectionToClient* connection) {
   DCHECK(CalledOnValidThread());
diff --git a/remoting/host/client_session.h b/remoting/host/client_session.h
index f023abd8ddd72..474e13b38a022 100644
--- a/remoting/host/client_session.h
+++ b/remoting/host/client_session.h
@@ -111,6 +111,8 @@ class ClientSession
       const protocol::Capabilities& capabilities) OVERRIDE;
   virtual void RequestPairing(
       const remoting::protocol::PairingRequest& pairing_request) OVERRIDE;
+  virtual void DeliverClientMessage(
+      const protocol::ExtensionMessage& message) OVERRIDE;
 
   // protocol::ConnectionToClient::EventHandler interface.
   virtual void OnConnectionAuthenticated(
diff --git a/remoting/proto/control.proto b/remoting/proto/control.proto
index 888c0b01b9010..2d2baf620b9b0 100644
--- a/remoting/proto/control.proto
+++ b/remoting/proto/control.proto
@@ -66,3 +66,14 @@ message PairingResponse {
   // Shared secret for this client.
   optional string shared_secret = 2;
 }
+
+message ExtensionMessage {
+  // The message type. This is used to dispatch the message to the correct
+  // recipient.
+  optional string type = 1;
+
+  // String-encoded message data. The client and host must agree on the encoding
+  // for each message type; different message types need not shared the same
+  // encoding.
+  optional string data = 2;
+}
\ No newline at end of file
diff --git a/remoting/proto/internal.proto b/remoting/proto/internal.proto
index 219e3ef799c14..6ae35f4e3c279 100644
--- a/remoting/proto/internal.proto
+++ b/remoting/proto/internal.proto
@@ -24,6 +24,7 @@ message ControlMessage {
   optional Capabilities capabilities = 6;
   optional PairingRequest pairing_request = 7;
   optional PairingResponse pairing_response = 8;
+  optional ExtensionMessage extension_message = 9;
 }
 
 // Defines an event message on the event channel.
diff --git a/remoting/protocol/client_control_dispatcher.cc b/remoting/protocol/client_control_dispatcher.cc
index 8bb918efcb58f..a42c3f628d8b0 100644
--- a/remoting/protocol/client_control_dispatcher.cc
+++ b/remoting/protocol/client_control_dispatcher.cc
@@ -74,6 +74,13 @@ void ClientControlDispatcher::RequestPairing(
   writer_.Write(SerializeAndFrameMessage(message), base::Closure());
 }
 
+void ClientControlDispatcher::DeliverClientMessage(
+    const ExtensionMessage& message) {
+  ControlMessage control_message;
+  control_message.mutable_extension_message()->CopyFrom(message);
+  writer_.Write(SerializeAndFrameMessage(control_message), base::Closure());
+}
+
 void ClientControlDispatcher::OnMessageReceived(
     scoped_ptr<ControlMessage> message, const base::Closure& done_task) {
   DCHECK(client_stub_);
@@ -88,6 +95,8 @@ void ClientControlDispatcher::OnMessageReceived(
     client_stub_->SetCursorShape(message->cursor_shape());
   } else if (message->has_pairing_response()) {
     client_stub_->SetPairingResponse(message->pairing_response());
+  } else if (message->has_extension_message()) {
+    client_stub_->DeliverHostMessage(message->extension_message());
   } else {
     LOG(WARNING) << "Unknown control message received.";
   }
diff --git a/remoting/protocol/client_control_dispatcher.h b/remoting/protocol/client_control_dispatcher.h
index b2a0bfa5a9af2..556ce72190d24 100644
--- a/remoting/protocol/client_control_dispatcher.h
+++ b/remoting/protocol/client_control_dispatcher.h
@@ -40,6 +40,7 @@ class ClientControlDispatcher : public ChannelDispatcherBase,
   virtual void ControlAudio(const AudioControl& audio_control) OVERRIDE;
   virtual void SetCapabilities(const Capabilities& capabilities) OVERRIDE;
   virtual void RequestPairing(const PairingRequest& pairing_request) OVERRIDE;
+  virtual void DeliverClientMessage(const ExtensionMessage& message) OVERRIDE;
 
   // Sets the ClientStub that will be called for each incoming control
   // message. |client_stub| must outlive this object.
diff --git a/remoting/protocol/client_stub.h b/remoting/protocol/client_stub.h
index 4507ba7f821ad..d57f948d35ea3 100644
--- a/remoting/protocol/client_stub.h
+++ b/remoting/protocol/client_stub.h
@@ -18,6 +18,7 @@ namespace remoting {
 namespace protocol {
 
 class Capabilities;
+class ExtensionMessage;
 class PairingResponse;
 
 class ClientStub : public ClipboardStub,
@@ -32,6 +33,9 @@ class ClientStub : public ClipboardStub,
   // Passes a pairing response message to the client.
   virtual void SetPairingResponse(const PairingResponse& pairing_response) = 0;
 
+  // Deliver an extension message from the host to the client.
+  virtual void DeliverHostMessage(const ExtensionMessage& message) = 0;
+
  private:
   DISALLOW_COPY_AND_ASSIGN(ClientStub);
 };
diff --git a/remoting/protocol/host_control_dispatcher.cc b/remoting/protocol/host_control_dispatcher.cc
index b979e36060d89..26f09fc3e55e7 100644
--- a/remoting/protocol/host_control_dispatcher.cc
+++ b/remoting/protocol/host_control_dispatcher.cc
@@ -46,6 +46,13 @@ void HostControlDispatcher::SetPairingResponse(
   writer_.Write(SerializeAndFrameMessage(message), base::Closure());
 }
 
+void HostControlDispatcher::DeliverHostMessage(
+    const ExtensionMessage& message) {
+  ControlMessage control_message;
+  control_message.mutable_extension_message()->CopyFrom(message);
+  writer_.Write(SerializeAndFrameMessage(control_message), base::Closure());
+}
+
 void HostControlDispatcher::InjectClipboardEvent(const ClipboardEvent& event) {
   ControlMessage message;
   message.mutable_clipboard_event()->CopyFrom(event);
@@ -78,6 +85,8 @@ void HostControlDispatcher::OnMessageReceived(
     host_stub_->SetCapabilities(message->capabilities());
   } else if (message->has_pairing_request()) {
     host_stub_->RequestPairing(message->pairing_request());
+  } else if (message->has_extension_message()) {
+    host_stub_->DeliverClientMessage(message->extension_message());
   } else {
     LOG(WARNING) << "Unknown control message received.";
   }
diff --git a/remoting/protocol/host_control_dispatcher.h b/remoting/protocol/host_control_dispatcher.h
index 4620be10b877f..82aa7938f5e8a 100644
--- a/remoting/protocol/host_control_dispatcher.h
+++ b/remoting/protocol/host_control_dispatcher.h
@@ -37,6 +37,8 @@ class HostControlDispatcher : public ChannelDispatcherBase,
   virtual void SetCapabilities(const Capabilities& capabilities) OVERRIDE;
   virtual void SetPairingResponse(
       const PairingResponse& pairing_response) OVERRIDE;
+  virtual void DeliverHostMessage(
+      const ExtensionMessage& message) OVERRIDE;
 
   // ClipboardStub implementation for sending clipboard data to client.
   virtual void InjectClipboardEvent(const ClipboardEvent& event) OVERRIDE;
diff --git a/remoting/protocol/host_stub.h b/remoting/protocol/host_stub.h
index 46d75346bd62d..cf9fa0b95c3fb 100644
--- a/remoting/protocol/host_stub.h
+++ b/remoting/protocol/host_stub.h
@@ -17,6 +17,7 @@ namespace protocol {
 class AudioControl;
 class Capabilities;
 class ClientResolution;
+class ExtensionMessage;
 class PairingResponse;
 class PairingRequest;
 class VideoControl;
@@ -43,6 +44,9 @@ class HostStub {
   // Requests pairing between the host and client for PIN-less authentication.
   virtual void RequestPairing(const PairingRequest& pairing_request) = 0;
 
+  // Deliver an extension message from the client to the host.
+  virtual void DeliverClientMessage(const ExtensionMessage& message) = 0;
+
  protected:
   virtual ~HostStub() {}
 
diff --git a/remoting/protocol/protocol_mock_objects.h b/remoting/protocol/protocol_mock_objects.h
index 74435dbb486a5..16bb8663d7fdf 100644
--- a/remoting/protocol/protocol_mock_objects.h
+++ b/remoting/protocol/protocol_mock_objects.h
@@ -114,6 +114,7 @@ class MockHostStub : public HostStub {
   MOCK_METHOD1(SetCapabilities, void(const Capabilities& capabilities));
   MOCK_METHOD1(RequestPairing,
                void(const PairingRequest& pairing_request));
+  MOCK_METHOD1(DeliverClientMessage, void(const ExtensionMessage& message));
 
  private:
   DISALLOW_COPY_AND_ASSIGN(MockHostStub);
@@ -128,6 +129,7 @@ class MockClientStub : public ClientStub {
   MOCK_METHOD1(SetCapabilities, void(const Capabilities& capabilities));
   MOCK_METHOD1(SetPairingResponse,
                void(const PairingResponse& pairing_response));
+  MOCK_METHOD1(DeliverHostMessage, void(const ExtensionMessage& message));
 
   // ClipboardStub mock implementation.
   MOCK_METHOD1(InjectClipboardEvent, void(const ClipboardEvent& event));
diff --git a/remoting/webapp/client_plugin.js b/remoting/webapp/client_plugin.js
index e98b345825da2..247d06821576d 100644
--- a/remoting/webapp/client_plugin.js
+++ b/remoting/webapp/client_plugin.js
@@ -67,7 +67,8 @@ remoting.ClientPlugin.Feature = {
   SEND_CLIPBOARD_ITEM: 'sendClipboardItem',
   THIRD_PARTY_AUTH: 'thirdPartyAuth',
   TRAP_KEY: 'trapKey',
-  PINLESS_AUTH: 'pinlessAuth'
+  PINLESS_AUTH: 'pinlessAuth',
+  EXTENSION_MESSAGE: 'extensionMessage'
 };
 
 /**
diff --git a/remoting/webapp/client_plugin_async.js b/remoting/webapp/client_plugin_async.js
index f457d159fe466..ac82c9f914177 100644
--- a/remoting/webapp/client_plugin_async.js
+++ b/remoting/webapp/client_plugin_async.js
@@ -313,6 +313,10 @@ remoting.ClientPluginAsync.prototype.handleMessage_ = function(messageStr) {
       return;
     }
     this.onPairingComplete_(clientId, sharedSecret);
+  } else if (message.method == 'extensionMessage') {
+    // No messages currently supported.
+    console.log('Unexpected message received: ' +
+                message.data.type + ': ' + message.data.data);
   }
 };
 
@@ -606,6 +610,23 @@ remoting.ClientPluginAsync.prototype.requestPairing =
       { method: 'requestPairing', data: { clientName: clientName } }));
 };
 
+/**
+ * Send an extension message to the host.
+ *
+ * @param {string} type The message type.
+ * @param {Object} message The message payload.
+ */
+remoting.ClientPluginAsync.prototype.sendClientMessage =
+    function(type, message) {
+  if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
+    return;
+  }
+  this.plugin.postMessage(JSON.stringify(
+    { method: 'extensionMessage',
+      data: { type: type, data: JSON.stringify(message) } }));
+
+};
+
 /**
  * If we haven't yet received a "hello" message from the plugin, change its
  * size so that the user can confirm it if click-to-play is enabled, or can