Bug 960397 - Part 2: Multicast option support for UDPSocket. r=mayhemer
authorJ. Ryan Stinnett <jryans@gmail.com>
Tue, 29 Apr 2014 21:35:00 +0200
changeset 181348 6888e03eb62724a66c8864b15a40c0d86a28f88e
parent 181347 72ea2bcb65f3a66f6d52d86f03d5b13e9896dbeb
child 181349 d05f273a2d1d73487f882840054c752497e7f864
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersmayhemer
bugs960397
milestone32.0a1
Bug 960397 - Part 2: Multicast option support for UDPSocket. r=mayhemer
netwerk/base/src/nsUDPSocket.cpp
netwerk/base/src/nsUDPSocket.h
netwerk/test/TestUDPSocket.cpp
netwerk/test/unit/test_udp_multicast.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/base/src/nsUDPSocket.cpp
+++ b/netwerk/base/src/nsUDPSocket.cpp
@@ -60,16 +60,36 @@ ResolveHost(const nsACString &host, nsID
 
   nsCOMPtr<nsICancelable> tmpOutstanding;
   return dns->AsyncResolve(host, 0, listener, nullptr,
                            getter_AddRefs(tmpOutstanding));
 
 }
 
 //-----------------------------------------------------------------------------
+
+class SetSocketOptionRunnable : public nsRunnable
+{
+public:
+  SetSocketOptionRunnable(nsUDPSocket* aSocket, const PRSocketOptionData& aOpt)
+    : mSocket(aSocket)
+    , mOpt(aOpt)
+  {}
+
+  NS_IMETHOD Run()
+  {
+    return mSocket->SetSocketOption(mOpt);
+  }
+
+private:
+  nsRefPtr<nsUDPSocket> mSocket;
+  PRSocketOptionData    mOpt;
+};
+
+//-----------------------------------------------------------------------------
 // nsUDPOutputStream impl
 //-----------------------------------------------------------------------------
 NS_IMPL_ISUPPORTS(nsUDPOutputStream, nsIOutputStream)
 
 nsUDPOutputStream::nsUDPOutputStream(nsUDPSocket* aSocket,
                                      PRFileDesc* aFD,
                                      PRNetAddr& aPrClientAddr)
   : mSocket(aSocket)
@@ -919,8 +939,253 @@ nsUDPSocket::SendWithAddress(const NetAd
 
     nsresult rv = mSts->Dispatch(new SendRequestRunnable(this, *aAddr, fallibleArray),
                                  NS_DISPATCH_NORMAL);
     NS_ENSURE_SUCCESS(rv, rv);
     *_retval = aDataLength;
   }
   return NS_OK;
 }
+
+nsresult
+nsUDPSocket::SetSocketOption(const PRSocketOptionData& aOpt)
+{
+  bool onSTSThread = false;
+  mSts->IsOnCurrentThread(&onSTSThread);
+
+  if (!onSTSThread) {
+    // Dispatch to STS thread and re-enter this method there
+    nsCOMPtr<nsIRunnable> runnable = new SetSocketOptionRunnable(this, aOpt);
+    nsresult rv = mSts->Dispatch(runnable, NS_DISPATCH_NORMAL);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+    return NS_OK;
+  }
+
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  if (PR_SetSocketOption(mFD, &aOpt) != PR_SUCCESS) {
+    SOCKET_LOG(("nsUDPSocket::SetSocketOption [this=%p] failed for type %d, "
+      "error %d\n", this, aOpt.option, PR_GetError()));
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::JoinMulticast(const nsACString& aAddr, const nsACString& aIface)
+{
+  if (NS_WARN_IF(aAddr.IsEmpty())) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prAddr;
+  if (PR_StringToNetAddr(aAddr.BeginReading(), &prAddr) != PR_SUCCESS) {
+    return NS_ERROR_FAILURE;
+  }
+
+  PRNetAddr prIface;
+  if (aIface.IsEmpty()) {
+    PR_InitializeNetAddr(PR_IpAddrAny, 0, &prIface);
+  } else {
+    if (PR_StringToNetAddr(aIface.BeginReading(), &prIface) != PR_SUCCESS) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  return JoinMulticastInternal(prAddr, prIface);
+}
+
+NS_IMETHODIMP
+nsUDPSocket::JoinMulticastAddr(const NetAddr aAddr, const NetAddr* aIface)
+{
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prAddr;
+  NetAddrToPRNetAddr(&aAddr, &prAddr);
+
+  PRNetAddr prIface;
+  if (!aIface) {
+    PR_InitializeNetAddr(PR_IpAddrAny, 0, &prIface);
+  } else {
+    NetAddrToPRNetAddr(aIface, &prIface);
+  }
+
+  return JoinMulticastInternal(prAddr, prIface);
+}
+
+nsresult
+nsUDPSocket::JoinMulticastInternal(const PRNetAddr& aAddr,
+                                   const PRNetAddr& aIface)
+{
+  PRSocketOptionData opt;
+
+  opt.option = PR_SockOpt_AddMember;
+  opt.value.add_member.mcaddr = aAddr;
+  opt.value.add_member.ifaddr = aIface;
+
+  nsresult rv = SetSocketOption(opt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::LeaveMulticast(const nsACString& aAddr, const nsACString& aIface)
+{
+  if (NS_WARN_IF(aAddr.IsEmpty())) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prAddr;
+  if (PR_StringToNetAddr(aAddr.BeginReading(), &prAddr) != PR_SUCCESS) {
+    return NS_ERROR_FAILURE;
+  }
+
+  PRNetAddr prIface;
+  if (aIface.IsEmpty()) {
+    PR_InitializeNetAddr(PR_IpAddrAny, 0, &prIface);
+  } else {
+    if (PR_StringToNetAddr(aIface.BeginReading(), &prIface) != PR_SUCCESS) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  return LeaveMulticastInternal(prAddr, prIface);
+}
+
+NS_IMETHODIMP
+nsUDPSocket::LeaveMulticastAddr(const NetAddr aAddr, const NetAddr* aIface)
+{
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prAddr;
+  NetAddrToPRNetAddr(&aAddr, &prAddr);
+
+  PRNetAddr prIface;
+  if (!aIface) {
+    PR_InitializeNetAddr(PR_IpAddrAny, 0, &prIface);
+  } else {
+    NetAddrToPRNetAddr(aIface, &prIface);
+  }
+
+  return LeaveMulticastInternal(prAddr, prIface);
+}
+
+nsresult
+nsUDPSocket::LeaveMulticastInternal(const PRNetAddr& aAddr,
+                                    const PRNetAddr& aIface)
+{
+  PRSocketOptionData opt;
+
+  opt.option = PR_SockOpt_DropMember;
+  opt.value.drop_member.mcaddr = aAddr;
+  opt.value.drop_member.ifaddr = aIface;
+
+  nsresult rv = SetSocketOption(opt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::GetMulticastLoopback(bool* aLoopback)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::SetMulticastLoopback(bool aLoopback)
+{
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRSocketOptionData opt;
+
+  opt.option = PR_SockOpt_McastLoopback;
+  opt.value.mcast_loopback = aLoopback;
+
+  nsresult rv = SetSocketOption(opt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::GetMulticastInterface(nsACString& aIface)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::GetMulticastInterfaceAddr(NetAddr* aIface)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsUDPSocket::SetMulticastInterface(const nsACString& aIface)
+{
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prIface;
+  if (aIface.IsEmpty()) {
+    PR_InitializeNetAddr(PR_IpAddrAny, 0, &prIface);
+  } else {
+    if (PR_StringToNetAddr(aIface.BeginReading(), &prIface) != PR_SUCCESS) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  return SetMulticastInterfaceInternal(prIface);
+}
+
+NS_IMETHODIMP
+nsUDPSocket::SetMulticastInterfaceAddr(NetAddr aIface)
+{
+  if (NS_WARN_IF(!mFD)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRNetAddr prIface;
+  NetAddrToPRNetAddr(&aIface, &prIface);
+
+  return SetMulticastInterfaceInternal(prIface);
+}
+
+nsresult
+nsUDPSocket::SetMulticastInterfaceInternal(const PRNetAddr& aIface)
+{
+  PRSocketOptionData opt;
+
+  opt.option = PR_SockOpt_McastInterface;
+  opt.value.mcast_if = aIface;
+
+  nsresult rv = SetSocketOption(opt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
--- a/netwerk/base/src/nsUDPSocket.h
+++ b/netwerk/base/src/nsUDPSocket.h
@@ -38,16 +38,24 @@ public:
 
 private:
   void OnMsgClose();
   void OnMsgAttach();
 
   // try attaching our socket (mFD) to the STS's poll list.
   nsresult TryAttach();
 
+  friend class SetSocketOptionRunnable;
+  nsresult SetSocketOption(const PRSocketOptionData& aOpt);
+  nsresult JoinMulticastInternal(const PRNetAddr& aAddr,
+                                 const PRNetAddr& aIface);
+  nsresult LeaveMulticastInternal(const PRNetAddr& aAddr,
+                                  const PRNetAddr& aIface);
+  nsresult SetMulticastInterfaceInternal(const PRNetAddr& aIface);
+
   // lock protects access to mListener;
   // so mListener is not cleared while being used/locked.
   mozilla::Mutex                       mLock;
   PRFileDesc                           *mFD;
   mozilla::net::NetAddr                mAddr;
   nsCOMPtr<nsIUDPSocketListener>       mListener;
   nsCOMPtr<nsIEventTarget>             mListenerTarget;
   bool                                 mAttached;
--- a/netwerk/test/TestUDPSocket.cpp
+++ b/netwerk/test/TestUDPSocket.cpp
@@ -5,21 +5,23 @@
 #include "TestCommon.h"
 #include "TestHarness.h"
 #include "nsIUDPSocket.h"
 #include "nsISocketTransportService.h"
 #include "nsISocketTransport.h"
 #include "nsIOutputStream.h"
 #include "nsIInputStream.h"
 #include "nsINetAddr.h"
+#include "nsITimer.h"
 #include "mozilla/net/DNS.h"
 #include "prerror.h"
 
 #define REQUEST  0x68656c6f
 #define RESPONSE 0x6f6c6568
+#define MULTICAST_TIMEOUT 2000
 
 #define EXPECT_SUCCESS(rv, ...) \
   PR_BEGIN_MACRO \
   if (NS_FAILED(rv)) { \
     fail(__VA_ARGS__); \
     return false; \
   } \
   PR_END_MACRO
@@ -39,16 +41,17 @@
     fail(__VA_ARGS__); \
     return false; \
   } \
   PR_END_MACRO
 
 enum TestPhase {
   TEST_OUTPUT_STREAM,
   TEST_SEND_API,
+  TEST_MULTICAST,
   TEST_NONE
 };
 
 static TestPhase phase = TEST_NONE;
 
 static bool CheckMessageContent(nsIUDPMessage *aMessage, uint32_t aExpectedContent)
 {
   nsCString data;
@@ -190,32 +193,67 @@ UDPServerListener::OnPacketReceived(nsIU
     mResult = outstream->Write((const char*)&data, sizeof(uint32_t), &count);
 
     if (mResult == NS_OK && count == sizeof(uint32_t)) {
       passed("Response written");
     } else {
       fail("Response written");
     }
     return NS_OK;
+  } else if (TEST_MULTICAST == phase && CheckMessageContent(message, REQUEST)) {
+    mResult = NS_OK;
   } else if (TEST_SEND_API != phase || !CheckMessageContent(message, RESPONSE)) {
     mResult = NS_ERROR_FAILURE;
   }
 
   // Notify thread
   QuitPumpingEvents();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 UDPServerListener::OnStopListening(nsIUDPSocket*, nsresult)
 {
   QuitPumpingEvents();
   return NS_OK;
 }
 
+/**
+ * Multicast timer callback: detects delivery failure
+ */
+class MulticastTimerCallback : public nsITimerCallback
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSITIMERCALLBACK
+
+  virtual ~MulticastTimerCallback();
+
+  nsresult mResult;
+};
+
+NS_IMPL_ISUPPORTS(MulticastTimerCallback, nsITimerCallback)
+
+MulticastTimerCallback::~MulticastTimerCallback()
+{
+}
+
+NS_IMETHODIMP
+MulticastTimerCallback::Notify(nsITimer* timer)
+{
+  if (TEST_MULTICAST != phase) {
+    return NS_OK;
+  }
+  // Multicast ping failed
+  printf("Multicast ping timeout expired\n");
+  mResult = NS_ERROR_FAILURE;
+  QuitPumpingEvents();
+  return NS_OK;
+}
+
 /**** Main ****/
 int
 main(int32_t argc, char *argv[])
 {
   nsresult rv;
   ScopedXPCOM xpcom("UDP ServerSocket");
   if (xpcom.failed())
     return -1;
@@ -225,26 +263,26 @@ main(int32_t argc, char *argv[])
   server = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv);
   NS_ENSURE_SUCCESS(rv, -1);
   client = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv);
   NS_ENSURE_SUCCESS(rv, -1);
 
   // Create UDPServerListener to process UDP packets
   nsRefPtr<UDPServerListener> serverListener = new UDPServerListener();
 
-  // Bind server socket to 127.0.0.1
-  rv = server->Init(0, true);
+  // Bind server socket to 0.0.0.0
+  rv = server->Init(0, false);
   NS_ENSURE_SUCCESS(rv, -1);
   int32_t serverPort;
   server->GetPort(&serverPort);
   server->AsyncListen(serverListener);
 
   // Bind clinet on arbitrary port
   nsRefPtr<UDPClientListener> clientListener = new UDPClientListener();
-  client->Init(0, true);
+  client->Init(0, false);
   client->AsyncListen(clientListener);
 
   // Write data to server
   uint32_t count;
   const uint32_t data = REQUEST;
 
   phase = TEST_OUTPUT_STREAM;
   rv = client->Send(NS_LITERAL_CSTRING("127.0.0.1"), serverPort, (uint8_t*)&data, sizeof(uint32_t), &count);
@@ -257,30 +295,170 @@ main(int32_t argc, char *argv[])
   NS_ENSURE_SUCCESS(serverListener->mResult, -1);
 
   // Read response from server
   NS_ENSURE_SUCCESS(clientListener->mResult, -1);
 
   mozilla::net::NetAddr clientAddr;
   rv = client->GetAddress(&clientAddr);
   NS_ENSURE_SUCCESS(rv, -1);
+  // The client address is 0.0.0.0, but Windows won't receive packets there, so
+  // use 127.0.0.1 explicitly
+  clientAddr.inet.ip = PR_htonl(127 << 24 | 1);
 
   phase = TEST_SEND_API;
   rv = server->SendWithAddress(&clientAddr, (uint8_t*)&data, sizeof(uint32_t), &count);
   NS_ENSURE_SUCCESS(rv, -1);
   REQUIRE_EQUAL(count, sizeof(uint32_t), "Error");
   passed("Request written by SendWithAddress");
 
   // Wait for server
   PumpEvents();
   NS_ENSURE_SUCCESS(serverListener->mResult, -1);
 
   // Read response from server
   NS_ENSURE_SUCCESS(clientListener->mResult, -1);
 
+  // Setup timer to detect multicast failure
+  nsCOMPtr<nsITimer> timer = do_CreateInstance("@mozilla.org/timer;1");
+  if (NS_WARN_IF(!timer)) {
+    return -1;
+  }
+  nsCOMPtr<MulticastTimerCallback> timerCb = new MulticastTimerCallback();
+
+  // The following multicast tests using multiple sockets require a firewall
+  // exception on Windows XP before they pass.  For now, we'll skip them here.
+  // Later versions of Windows don't seem to have this issue.
+#ifdef XP_WIN
+  OSVERSIONINFO OsVersion;
+  OsVersion.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
+  GetVersionEx(&OsVersion);
+  if (OsVersion.dwMajorVersion == 5 && OsVersion.dwMinorVersion == 1) {
+    goto close;
+  }
+#endif
+
+  // Join multicast group
+  printf("Joining multicast group\n");
+  phase = TEST_MULTICAST;
+  mozilla::net::NetAddr multicastAddr;
+  multicastAddr.inet.family = AF_INET;
+  multicastAddr.inet.ip = PR_htonl(224 << 24 | 255);
+  multicastAddr.inet.port = PR_htons(serverPort);
+  rv = server->JoinMulticastAddr(multicastAddr, nullptr);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+
+  // Send multicast ping
+  timerCb->mResult = NS_OK;
+  timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+  rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+  REQUIRE_EQUAL(count, sizeof(uint32_t), "Error");
+  passed("Multicast ping written by SendWithAddress");
+
+  // Wait for server to receive successfully
+  PumpEvents();
+  if (NS_WARN_IF(NS_FAILED(serverListener->mResult))) {
+    return -1;
+  }
+  if (NS_WARN_IF(NS_FAILED(timerCb->mResult))) {
+    return -1;
+  }
+  timer->Cancel();
+  passed("Server received ping successfully");
+
+  // Disable multicast loopback
+  printf("Disable multicast loopback\n");
+  client->SetMulticastLoopback(false);
+  server->SetMulticastLoopback(false);
+
+  // Send multicast ping
+  timerCb->mResult = NS_OK;
+  timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+  rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+  REQUIRE_EQUAL(count, sizeof(uint32_t), "Error");
+  passed("Multicast ping written by SendWithAddress");
+
+  // Wait for server to fail to receive
+  PumpEvents();
+  if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) {
+    return -1;
+  }
+  timer->Cancel();
+  passed("Server failed to receive ping correctly");
+
+  // Reset state
+  client->SetMulticastLoopback(true);
+  server->SetMulticastLoopback(true);
+
+  // Change multicast interface
+  printf("Changing multicast interface\n");
+  mozilla::net::NetAddr loopbackAddr;
+  loopbackAddr.inet.family = AF_INET;
+  loopbackAddr.inet.ip = PR_htonl(INADDR_LOOPBACK);
+  client->SetMulticastInterfaceAddr(loopbackAddr);
+
+  // Send multicast ping
+  timerCb->mResult = NS_OK;
+  timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+  rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+  REQUIRE_EQUAL(count, sizeof(uint32_t), "Error");
+  passed("Multicast ping written by SendWithAddress");
+
+  // Wait for server to fail to receive
+  PumpEvents();
+  if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) {
+    return -1;
+  }
+  timer->Cancel();
+  passed("Server failed to receive ping correctly");
+
+  // Reset state
+  mozilla::net::NetAddr anyAddr;
+  anyAddr.inet.family = AF_INET;
+  anyAddr.inet.ip = PR_htonl(INADDR_ANY);
+  client->SetMulticastInterfaceAddr(anyAddr);
+
+  // Leave multicast group
+  printf("Leave multicast group\n");
+  rv = server->LeaveMulticastAddr(multicastAddr, nullptr);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+
+  // Send multicast ping
+  timerCb->mResult = NS_OK;
+  timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+  rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return -1;
+  }
+  REQUIRE_EQUAL(count, sizeof(uint32_t), "Error");
+  passed("Multicast ping written by SendWithAddress");
+
+  // Wait for server to fail to receive
+  PumpEvents();
+  if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) {
+    return -1;
+  }
+  timer->Cancel();
+  passed("Server failed to receive ping correctly");
+  goto close;
+
+close:
   // Close server
   printf("*** Attempting to close server ...\n");
   server->Close();
   client->Close();
   PumpEvents();
   passed("Server closed");
 
   return 0; // failure is a non-zero return
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_udp_multicast.js
@@ -0,0 +1,108 @@
+// Bug 960397: UDP multicast options
+
+const { Constructor: CC } = Components;
+
+const UDPSocket = CC("@mozilla.org/network/udp-socket;1",
+                     "nsIUDPSocket",
+                     "init");
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+
+const ADDRESS = "224.0.0.255";
+const TIMEOUT = 2000;
+
+const ua = Cc["@mozilla.org/network/protocol;1?name=http"]
+           .getService(Ci.nsIHttpProtocolHandler).userAgent;
+const isWinXP = ua.indexOf("Windows NT 5.1") != -1;
+
+let gConverter;
+
+function run_test() {
+  setup();
+  run_next_test();
+}
+
+function setup() {
+  gConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+               createInstance(Ci.nsIScriptableUnicodeConverter);
+  gConverter.charset = "utf8";
+}
+
+function createSocketAndJoin() {
+  let socket = new UDPSocket(-1, false);
+  socket.joinMulticast(ADDRESS);
+  return socket;
+}
+
+function sendPing(socket) {
+  let ping = "ping";
+  let rawPing = gConverter.convertToByteArray(ping);
+
+  let deferred = promise.defer();
+
+  socket.asyncListen({
+    onPacketReceived: function(s, message) {
+      do_print("Received on port " + socket.port);
+      do_check_eq(message.data, ping);
+      socket.close();
+      deferred.resolve(message.data);
+    },
+    onStopListening: function(socket, status) {}
+  });
+
+  do_print("Multicast send to port " + socket.port);
+  socket.send(ADDRESS, socket.port, rawPing, rawPing.length);
+
+  // Timers are bad, but it seems like the only way to test *not* getting a
+  // packet.
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback(() => {
+    socket.close();
+    deferred.reject();
+  }, TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
+
+  return deferred.promise;
+}
+
+add_test(() => {
+  do_print("Joining multicast group");
+  let socket = createSocketAndJoin();
+  sendPing(socket).then(
+    run_next_test,
+    () => do_throw("Joined group, but no packet received")
+  );
+});
+
+add_test(() => {
+  do_print("Disabling multicast loopback");
+  let socket = createSocketAndJoin();
+  socket.multicastLoopback = false;
+  sendPing(socket).then(
+    () => do_throw("Loopback disabled, but still got a packet"),
+    run_next_test
+  );
+});
+
+// The following multicast interface test doesn't work on Windows XP, as it
+// appears to allow packets no matter what address is given, so we'll skip the
+// test there.
+if (!isWinXP) {
+  add_test(() => {
+    do_print("Changing multicast interface");
+    let socket = createSocketAndJoin();
+    socket.multicastInterface = "127.0.0.1";
+    sendPing(socket).then(
+      () => do_throw("Changed interface, but still got a packet"),
+      run_next_test
+    );
+  });
+}
+
+add_test(() => {
+  do_print("Leaving multicast group");
+  let socket = createSocketAndJoin();
+  socket.leaveMulticast(ADDRESS);
+  sendPing(socket).then(
+    () => do_throw("Left group, but still got a packet"),
+    run_next_test
+  );
+});
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -315,8 +315,9 @@ skip-if = os == "android"
 [test_referrer.js]
 [test_seer.js]
 # Android version detection w/in gecko does not work right on infra, so we just
 # disable this test on all android versions, even though it's enabled on 2.3+ in
 # the wild.
 skip-if = os == "android"
 [test_signature_extraction.js]
 run-if = os == "win"
+[test_udp_multicast.js]