Make public Network#bindSocket(FileDescriptor).

Code that uses android.system.Os to create sockets as FileDescriptors
should be able to bind them to networks.  Note that FileDescriptors
could already be marked as "protected from VPNs" via
NetworkUtils#protectFromVpn(), but heretofore were not easily bound
to any particular network.

Bug: 21449922
Change-Id: I4bb86db5d95d5a55bb2d7e245848d11eaa351e65
diff --git a/api/current.txt b/api/current.txt
index 85c9ccd..0228d49 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -18329,6 +18329,7 @@
   public class Network implements android.os.Parcelable {
     method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
     method public void bindSocket(java.net.Socket) throws java.io.IOException;
+    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
     method public int describeContents();
     method public java.net.InetAddress[] getAllByName(java.lang.String) throws java.net.UnknownHostException;
     method public java.net.InetAddress getByName(java.lang.String) throws java.net.UnknownHostException;
diff --git a/api/system-current.txt b/api/system-current.txt
index e416fe8..0d7a5b3 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -19815,6 +19815,7 @@
   public class Network implements android.os.Parcelable {
     method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
     method public void bindSocket(java.net.Socket) throws java.io.IOException;
+    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
     method public int describeContents();
     method public java.net.InetAddress[] getAllByName(java.lang.String) throws java.net.UnknownHostException;
     method public java.net.InetAddress getByName(java.lang.String) throws java.net.UnknownHostException;
diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java
index 754c6b3..9628bae 100644
--- a/core/java/android/net/Network.java
+++ b/core/java/android/net/Network.java
@@ -19,6 +19,8 @@
 import android.os.Parcelable;
 import android.os.Parcel;
 import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -64,7 +66,7 @@
     // maybeInitHttpClient() must be called prior to reading either variable.
     private volatile ConnectionPool mConnectionPool = null;
     private volatile com.android.okhttp.internal.Network mNetwork = null;
-    private Object mLock = new Object();
+    private final Object mLock = new Object();
 
     // Default connection pool values. These are evaluated at startup, just
     // like the OkHttp code. Also like the OkHttp code, we will throw parse
@@ -300,14 +302,10 @@
      * connected.
      */
     public void bindSocket(DatagramSocket socket) throws IOException {
-        // Apparently, the kernel doesn't update a connected UDP socket's routing upon mark changes.
-        if (socket.isConnected()) {
-            throw new SocketException("Socket is connected");
-        }
         // Query a property of the underlying socket to ensure that the socket's file descriptor
         // exists, is available to bind to a network and is not closed.
         socket.getReuseAddress();
-        bindSocketFd(socket.getFileDescriptor$());
+        bindSocket(socket.getFileDescriptor$());
     }
 
     /**
@@ -316,18 +314,38 @@
      * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
      */
     public void bindSocket(Socket socket) throws IOException {
-        // Apparently, the kernel doesn't update a connected TCP socket's routing upon mark changes.
-        if (socket.isConnected()) {
-            throw new SocketException("Socket is connected");
-        }
         // Query a property of the underlying socket to ensure that the socket's file descriptor
         // exists, is available to bind to a network and is not closed.
         socket.getReuseAddress();
-        bindSocketFd(socket.getFileDescriptor$());
+        bindSocket(socket.getFileDescriptor$());
     }
 
-    private void bindSocketFd(FileDescriptor fd) throws IOException {
-        int err = NetworkUtils.bindSocketToNetwork(fd.getInt$(), netId);
+    /**
+     * Binds the specified {@link FileDescriptor} to this {@code Network}. All data traffic on the
+     * socket represented by this file descriptor will be sent on this {@code Network},
+     * irrespective of any process-wide network binding set by
+     * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
+     */
+    public void bindSocket(FileDescriptor fd) throws IOException {
+        try {
+            final SocketAddress peer = Os.getpeername(fd);
+            final InetAddress inetPeer = ((InetSocketAddress) peer).getAddress();
+            if (!inetPeer.isAnyLocalAddress()) {
+                // Apparently, the kernel doesn't update a connected UDP socket's
+                // routing upon mark changes.
+                throw new SocketException("Socket is connected");
+            }
+        } catch (ErrnoException e) {
+            // getpeername() failed.
+            if (e.errno != OsConstants.ENOTCONN) {
+                throw e.rethrowAsSocketException();
+            }
+        } catch (ClassCastException e) {
+            // Wasn't an InetSocketAddress.
+            throw new SocketException("Only AF_INET/AF_INET6 sockets supported");
+        }
+
+        final int err = NetworkUtils.bindSocketToNetwork(fd.getInt$(), netId);
         if (err != 0) {
             // bindSocketToNetwork returns negative errno.
             throw new ErrnoException("Binding socket to network " + netId, -err)
diff --git a/core/tests/coretests/src/android/net/NetworkTest.java b/core/tests/coretests/src/android/net/NetworkTest.java
new file mode 100644
index 0000000..b0ecb049
--- /dev/null
+++ b/core/tests/coretests/src/android/net/NetworkTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.Network;
+import android.test.suitebuilder.annotation.SmallTest;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.net.SocketException;
+import junit.framework.TestCase;
+
+public class NetworkTest extends TestCase {
+    final Network mNetwork = new Network(99);
+
+    @SmallTest
+    public void testBindSocketOfInvalidFdThrows() throws Exception {
+
+        final FileDescriptor fd = new FileDescriptor();
+        assertFalse(fd.valid());
+
+        try {
+            mNetwork.bindSocket(fd);
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @SmallTest
+    public void testBindSocketOfNonSocketFdThrows() throws Exception {
+        final File devNull = new File("/dev/null");
+        assertTrue(devNull.canRead());
+
+        final FileInputStream fis = new FileInputStream(devNull);
+        assertTrue(null != fis.getFD());
+        assertTrue(fis.getFD().valid());
+
+        try {
+            mNetwork.bindSocket(fis.getFD());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @SmallTest
+    public void testBindSocketOfConnectedDatagramSocketThrows() throws Exception {
+        final DatagramSocket mDgramSocket = new DatagramSocket(0, (InetAddress) Inet6Address.ANY);
+        mDgramSocket.connect((InetAddress) Inet6Address.LOOPBACK, 53);
+        assertTrue(mDgramSocket.isConnected());
+
+        try {
+            mNetwork.bindSocket(mDgramSocket);
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @SmallTest
+    public void testBindSocketOfLocalSocketThrows() throws Exception {
+        final LocalSocket mLocalClient = new LocalSocket();
+        mLocalClient.bind(new LocalSocketAddress("testClient"));
+        assertTrue(mLocalClient.getFileDescriptor().valid());
+
+        try {
+            mNetwork.bindSocket(mLocalClient.getFileDescriptor());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+
+        final LocalServerSocket mLocalServer = new LocalServerSocket("testServer");
+        mLocalClient.connect(mLocalServer.getLocalSocketAddress());
+        assertTrue(mLocalClient.isConnected());
+
+        try {
+            mNetwork.bindSocket(mLocalClient.getFileDescriptor());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+}