Merge "Add NATT keepalive resources and methods into IpSecService"
am: c345527430

Change-Id: I97e7451959d619f42d68a5c29cca410bf0740852
diff --git a/core/java/android/net/IIpSecService.aidl b/core/java/android/net/IIpSecService.aidl
index d6774d4..b5a2a16 100644
--- a/core/java/android/net/IIpSecService.aidl
+++ b/core/java/android/net/IIpSecService.aidl
@@ -72,4 +72,8 @@
             int tunnelResourceId, int direction, int transformResourceId, in String callingPackage);
 
     void removeTransportModeTransforms(in ParcelFileDescriptor socket);
+
+    int lockEncapSocketForNattKeepalive(int encapSocketResourceId, int requesterUid);
+
+    void releaseNattKeepalive(int nattKeepaliveResourceId, int ownerUid);
 }
diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java
index 2055b64..c1f5255 100644
--- a/services/core/java/com/android/server/IpSecService.java
+++ b/services/core/java/com/android/server/IpSecService.java
@@ -28,8 +28,10 @@
 import static com.android.internal.util.Preconditions.checkNotNull;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.IIpSecService;
 import android.net.INetd;
 import android.net.IpSecAlgorithm;
@@ -42,6 +44,7 @@
 import android.net.IpSecUdpEncapResponse;
 import android.net.LinkAddress;
 import android.net.Network;
+import android.net.NetworkStack;
 import android.net.NetworkUtils;
 import android.net.TrafficStats;
 import android.net.util.NetdService;
@@ -74,6 +77,8 @@
 import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -175,6 +180,14 @@
     }
 
     /**
+     * Sentinel value placeholder for a real binder in RefcountedResources.
+     *
+     * <p>Used for cases where there the allocating party is a system service, and thus is expected
+     * to track the resource lifecycles instead of IpSecService.
+     */
+    private static final Binder DUMMY_BINDER = new Binder();
+
+    /**
      * RefcountedResource manages references and dependencies in an exclusively acyclic graph.
      *
      * <p>RefcountedResource implements both explicit and implicit resource management. Creating a
@@ -188,24 +201,42 @@
      */
     @VisibleForTesting
     public class RefcountedResource<T extends IResource> implements IBinder.DeathRecipient {
-        private final T mResource;
-        private final List<RefcountedResource> mChildren;
-        int mRefCount = 1; // starts at 1 for user's reference.
-        IBinder mBinder;
 
-        RefcountedResource(T resource, IBinder binder, RefcountedResource... children) {
+        @NonNull private final T mResource;
+        @NonNull private final List<RefcountedResource> mChildren;
+        int mRefCount = 1; // starts at 1 for user's reference.
+
+        /*
+         * This field can take one of three states:
+         * 1. null, when the object has been released by the user (or the user's binder dies)
+         * 2. DUMMY_BINDER, when the refcounted resource is allocated from another system service
+         *     and the other system service manages the lifecycle of the resource instead of
+         *     IpSecService.
+         * 3. an actual binder, to ensure IpSecService can cleanup after users that forget to close
+         *     their resources.
+         */
+        @Nullable IBinder mBinder;
+
+        RefcountedResource(@NonNull T resource, @NonNull RefcountedResource... children) {
+            this(resource, DUMMY_BINDER, children);
+        }
+
+        RefcountedResource(
+                @NonNull T resource,
+                @NonNull IBinder binder,
+                @NonNull RefcountedResource... children) {
             synchronized (IpSecService.this) {
                 this.mResource = resource;
-                this.mChildren = new ArrayList<>(children.length);
                 this.mBinder = binder;
-
+                this.mChildren = Collections.unmodifiableList(Arrays.asList(children));
                 for (RefcountedResource child : children) {
-                    mChildren.add(child);
                     child.mRefCount++;
                 }
 
                 try {
-                    mBinder.linkToDeath(this, 0);
+                    if (mBinder != DUMMY_BINDER) {
+                        mBinder.linkToDeath(this, 0);
+                    }
                 } catch (RemoteException e) {
                     binderDied();
                     e.rethrowFromSystemServer();
@@ -252,11 +283,12 @@
                 return;
             }
 
-            mBinder.unlinkToDeath(this, 0);
+            if (mBinder != DUMMY_BINDER) {
+                mBinder.unlinkToDeath(this, 0);
+            }
             mBinder = null;
 
             mResource.invalidate();
-
             releaseReference();
         }
 
@@ -381,6 +413,8 @@
                 new RefcountedResourceArray<>(EncapSocketRecord.class.getSimpleName());
         final RefcountedResourceArray<TunnelInterfaceRecord> mTunnelInterfaceRecords =
                 new RefcountedResourceArray<>(TunnelInterfaceRecord.class.getSimpleName());
+        final RefcountedResourceArray<NattKeepaliveRecord> mNattKeepaliveRecords =
+                new RefcountedResourceArray<>(NattKeepaliveRecord.class.getSimpleName());
 
         /**
          * Trackers for quotas for each of the OwnedResource types.
@@ -394,6 +428,8 @@
         final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
         final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
         final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
+        final ResourceTracker mNattKeepaliveQuotaTracker =
+                new ResourceTracker(MAX_NUM_ENCAP_SOCKETS); // Max 1 NATT keepalive per encap socket
         final ResourceTracker mTunnelQuotaTracker = new ResourceTracker(MAX_NUM_TUNNEL_INTERFACES);
 
         void removeSpiRecord(int resourceId) {
@@ -412,6 +448,10 @@
             mEncapSocketRecords.remove(resourceId);
         }
 
+        void removeNattKeepaliveRecord(int resourceId) {
+            mNattKeepaliveRecords.remove(resourceId);
+        }
+
         @Override
         public String toString() {
             return new StringBuilder()
@@ -421,6 +461,8 @@
                     .append(mTransformQuotaTracker)
                     .append(", mSocketQuotaTracker=")
                     .append(mSocketQuotaTracker)
+                    .append(", mNattKeepaliveQuotaTracker=")
+                    .append(mNattKeepaliveQuotaTracker)
                     .append(", mTunnelQuotaTracker=")
                     .append(mTunnelQuotaTracker)
                     .append(", mSpiRecords=")
@@ -429,6 +471,8 @@
                     .append(mTransformRecords)
                     .append(", mEncapSocketRecords=")
                     .append(mEncapSocketRecords)
+                    .append(", mNattKeepaliveRecords=")
+                    .append(mNattKeepaliveRecords)
                     .append(", mTunnelInterfaceRecords=")
                     .append(mTunnelInterfaceRecords)
                     .append("}")
@@ -573,6 +617,11 @@
             mArray.remove(key);
         }
 
+        @VisibleForTesting
+        int size() {
+            return mArray.size();
+        }
+
         @Override
         public String toString() {
             return mArray.toString();
@@ -984,6 +1033,50 @@
     }
 
     /**
+     * Tracks a NATT-keepalive instance
+     *
+     * <p>This class ensures that while a NATT-keepalive is active, the UDP encap socket that it is
+     * supporting will stay open until the NATT-keepalive is finished. NATT-keepalive offload
+     * lifecycles will be managed by ConnectivityService, which will validate that the UDP Encap
+     * socket is owned by the requester, and take a reference to it via this NattKeepaliveRecord
+     *
+     * <p>It shall be the responsibility of the caller to ensure that instances of an EncapSocket do
+     * not spawn multiple instances of NATT keepalives (and thereby register duplicate records)
+     */
+    private final class NattKeepaliveRecord extends OwnedResourceRecord {
+        NattKeepaliveRecord(int resourceId) {
+            super(resourceId);
+        }
+
+        @Override
+        @GuardedBy("IpSecService.this")
+        public void freeUnderlyingResources() {
+            Log.d(TAG, "Natt Keepalive released: " + mResourceId);
+
+            getResourceTracker().give();
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mNattKeepaliveQuotaTracker;
+        }
+
+        @Override
+        public void invalidate() {
+            getUserRecord().removeNattKeepaliveRecord(mResourceId);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{super=")
+                    .append(super.toString())
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    /**
      * Constructs a new IpSecService instance
      *
      * @param context Binder context for this service
@@ -1818,6 +1911,57 @@
         }
     }
 
+    private void verifyNetworkStackCaller() {
+        if (mContext.checkCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+                        != PackageManager.PERMISSION_GRANTED
+                && mContext.checkCallingOrSelfPermission(android.Manifest.permission.NETWORK_STACK)
+                        != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Requires permission NETWORK_STACK or MAINLINE_NETWORK_STACK");
+        }
+    }
+
+    /**
+     * Validates that a provided UID owns the encapSocket, and creates a NATT keepalive record
+     *
+     * <p>For system server use only. Caller must have NETWORK_STACK permission
+     *
+     * @param encapSocketResourceId resource identifier of the encap socket record
+     * @param ownerUid the UID of the caller. Used to verify ownership.
+     * @return
+     */
+    public synchronized int lockEncapSocketForNattKeepalive(
+            int encapSocketResourceId, int ownerUid) {
+        verifyNetworkStackCaller();
+
+        // Verify ownership. Will throw IllegalArgumentException if the UID specified does not
+        // own the specified UDP encapsulation socket
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(ownerUid);
+        RefcountedResource<EncapSocketRecord> refcountedSocketRecord =
+                userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(encapSocketResourceId);
+
+        // Build NattKeepaliveRecord
+        final int resourceId = mNextResourceId++;
+        userRecord.mNattKeepaliveRecords.put(
+                resourceId,
+                new RefcountedResource<NattKeepaliveRecord>(
+                        new NattKeepaliveRecord(resourceId), refcountedSocketRecord));
+
+        return resourceId;
+    }
+
+    /**
+     * Release a previously allocated NattKeepalive that has been registered with the system server
+     */
+    @Override
+    public synchronized void releaseNattKeepalive(int nattKeepaliveResourceId, int ownerUid)
+            throws RemoteException {
+        verifyNetworkStackCaller();
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(ownerUid);
+        releaseResource(userRecord.mNattKeepaliveRecords, nattKeepaliveResourceId);
+    }
+
     @Override
     protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
diff --git a/tests/net/java/com/android/server/IpSecServiceTest.java b/tests/net/java/com/android/server/IpSecServiceTest.java
index 4a35015..6b5a220 100644
--- a/tests/net/java/com/android/server/IpSecServiceTest.java
+++ b/tests/net/java/com/android/server/IpSecServiceTest.java
@@ -118,6 +118,7 @@
     INetd mMockNetd;
     IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig;
     IpSecService mIpSecService;
+    int mUid = Os.getuid();
 
     @Before
     public void setUp() throws Exception {
@@ -665,4 +666,99 @@
         mIpSecService.releaseNetId(releasedNetId);
         assertEquals(releasedNetId, mIpSecService.reserveNetId());
     }
+
+    @Test
+    public void testLockEncapSocketForNattKeepalive() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+
+        // Verify no NATT keepalive records upon startup
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+
+        int nattKeepaliveResourceId =
+                mIpSecService.lockEncapSocketForNattKeepalive(udpEncapResp.resourceId, mUid);
+
+        // Validate response, and record was added
+        assertNotEquals(IpSecManager.INVALID_RESOURCE_ID, nattKeepaliveResourceId);
+        assertEquals(1, userRecord.mNattKeepaliveRecords.size());
+
+        // Validate keepalive can be released and removed.
+        mIpSecService.releaseNattKeepalive(nattKeepaliveResourceId, mUid);
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+    }
+
+    @Test
+    public void testLockEncapSocketForNattKeepaliveInvalidUid() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+
+        // Verify no NATT keepalive records upon startup
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+
+        try {
+            int nattKeepaliveResourceId =
+                    mIpSecService.lockEncapSocketForNattKeepalive(
+                            udpEncapResp.resourceId, mUid + 1);
+            fail("Expected SecurityException for invalid user");
+        } catch (SecurityException expected) {
+        }
+
+        // Validate keepalive was not added to lists
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+    }
+
+    @Test
+    public void testLockEncapSocketForNattKeepaliveInvalidResourceId() throws Exception {
+        // Verify no NATT keepalive records upon startup
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+
+        try {
+            int nattKeepaliveResourceId =
+                    mIpSecService.lockEncapSocketForNattKeepalive(12345, mUid);
+            fail("Expected IllegalArgumentException for invalid resource ID");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Validate keepalive was not added to lists
+        assertEquals(0, userRecord.mNattKeepaliveRecords.size());
+    }
+
+    @Test
+    public void testEncapSocketReleasedBeforeKeepaliveReleased() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+
+        // Get encap socket record, verify initial starting refcount.
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        IpSecService.RefcountedResource encapSocketRefcountedRecord =
+                userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
+                        udpEncapResp.resourceId);
+        assertEquals(1, encapSocketRefcountedRecord.mRefCount);
+
+        // Verify that the reference was added
+        int nattKeepaliveResourceId =
+                mIpSecService.lockEncapSocketForNattKeepalive(udpEncapResp.resourceId, mUid);
+        assertNotEquals(IpSecManager.INVALID_RESOURCE_ID, nattKeepaliveResourceId);
+        assertEquals(2, encapSocketRefcountedRecord.mRefCount);
+
+        // Close UDP encap socket, but expect the refcountedRecord to still have a reference.
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        assertEquals(1, encapSocketRefcountedRecord.mRefCount);
+
+        // Verify UDP encap socket cleaned up once reference is removed. Expect -1 if cleanup
+        // was properly completed.
+        mIpSecService.releaseNattKeepalive(nattKeepaliveResourceId, mUid);
+        assertEquals(-1, encapSocketRefcountedRecord.mRefCount);
+    }
 }