Merge "Move DeviceInfoUtilsTest from NetworkStack to frameworks/lib/net"
diff --git a/common/device/com/android/net/module/util/BpfMap.java b/common/device/com/android/net/module/util/BpfMap.java
index f1420c2..0ee862a 100644
--- a/common/device/com/android/net/module/util/BpfMap.java
+++ b/common/device/com/android/net/module/util/BpfMap.java
@@ -18,6 +18,7 @@
 import static android.system.OsConstants.EEXIST;
 import static android.system.OsConstants.ENOENT;
 
+import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
 
 import androidx.annotation.NonNull;
@@ -26,6 +27,7 @@
 
 import com.android.net.module.util.Struct;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.NoSuchElementException;
@@ -57,7 +59,7 @@
     private static final int BPF_NOEXIST = 1;
     private static final int BPF_EXIST = 2;
 
-    private final int mMapFd;
+    private final ParcelFileDescriptor mMapFd;
     private final Class<K> mKeyClass;
     private final Class<V> mValueClass;
     private final int mKeySize;
@@ -72,8 +74,7 @@
      */
     public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
             final Class<V> value) throws ErrnoException, NullPointerException {
-        mMapFd = bpfFdGet(path, flag);
-
+        mMapFd = ParcelFileDescriptor.adoptFd(bpfFdGet(path, flag));
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -85,10 +86,11 @@
      * The derived class implements an internal mocked map. It need to implement all functions
      * which are related with the native BPF map because the BPF map handler is not initialized.
      * See BpfCoordinatorTest#TestBpfMap.
+     * TODO: remove once TestBpfMap derive from IBpfMap.
      */
     @VisibleForTesting
     protected BpfMap(final Class<K> key, final Class<V> value) {
-        mMapFd = -1;
+        mMapFd = ParcelFileDescriptor.adoptFd(-1 /*invalid*/);  // unused
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -101,7 +103,7 @@
      */
     @Override
     public void updateEntry(K key, V value) throws ErrnoException {
-        writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+        writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
     }
 
     /**
@@ -112,7 +114,7 @@
     public void insertEntry(K key, V value)
             throws ErrnoException, IllegalStateException {
         try {
-            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
         } catch (ErrnoException e) {
             if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
 
@@ -128,7 +130,7 @@
     public void replaceEntry(K key, V value)
             throws ErrnoException, NoSuchElementException {
         try {
-            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
         } catch (ErrnoException e) {
             if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
 
@@ -146,13 +148,13 @@
     public boolean insertOrReplaceEntry(K key, V value)
             throws ErrnoException {
         try {
-            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
             return true;   /* insert succeeded */
         } catch (ErrnoException e) {
             if (e.errno != EEXIST) throw e;
         }
         try {
-            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
             return false;   /* replace succeeded */
         } catch (ErrnoException e) {
             if (e.errno != ENOENT) throw e;
@@ -169,7 +171,7 @@
     /** Remove existing key from eBpf map. Return false if map was not modified. */
     @Override
     public boolean deleteEntry(K key) throws ErrnoException {
-        return deleteMapEntry(mMapFd, key.writeToBytes());
+        return deleteMapEntry(mMapFd.getFd(), key.writeToBytes());
     }
 
     /** Returns {@code true} if this map contains no elements. */
@@ -202,7 +204,7 @@
 
     private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
         byte[] nextKey = new byte[mKeySize];
-        if (getNextMapKey(mMapFd, key, nextKey)) return nextKey;
+        if (getNextMapKey(mMapFd.getFd(), key, nextKey)) return nextKey;
 
         return null;
     }
@@ -237,7 +239,7 @@
 
     private byte[] getRawValue(final byte[] key) throws ErrnoException {
         byte[] value = new byte[mValueSize];
-        if (findMapEntry(mMapFd, key, value)) return value;
+        if (findMapEntry(mMapFd.getFd(), key, value)) return value;
 
         return null;
     }
@@ -262,8 +264,8 @@
     }
 
     @Override
-    public void close() throws ErrnoException {
-        closeMap(mMapFd);
+    public void close() throws IOException {
+        mMapFd.close();
     }
 
     /**
@@ -281,8 +283,6 @@
         }
     }
 
-    private static native int closeMap(int fd) throws ErrnoException;
-
     private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
 
     private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
diff --git a/common/framework/com/android/net/module/util/PerUidCounter.java b/common/framework/com/android/net/module/util/PerUidCounter.java
new file mode 100644
index 0000000..0b2de7a
--- /dev/null
+++ b/common/framework/com/android/net/module/util/PerUidCounter.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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 com.android.net.module.util;
+
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Keeps track of the counters under different uid, fire exception if the counter
+ * exceeded the specified maximum value.
+ *
+ * @hide
+ */
+public class PerUidCounter {
+    private final int mMaxCountPerUid;
+
+    // Map from UID to count that UID has filed.
+    @VisibleForTesting
+    @GuardedBy("mUidToCount")
+    final SparseIntArray mUidToCount = new SparseIntArray();
+
+    /**
+     * Constructor
+     *
+     * @param maxCountPerUid the maximum count per uid allowed
+     */
+    public PerUidCounter(final int maxCountPerUid) {
+        if (maxCountPerUid <= 0) {
+            throw new IllegalArgumentException("Maximum counter value must be positive");
+        }
+        mMaxCountPerUid = maxCountPerUid;
+    }
+
+    /**
+     * Increments the count of the given uid.  Throws an exception if the number
+     * of the counter for the uid exceeds the value of maxCounterPerUid which is the value
+     * passed into the constructor. see: {@link #PerUidCounter(int)}.
+     *
+     * @throws IllegalStateException if the number of counter for the uid exceed
+     *         the allowed number.
+     *
+     * @param uid the uid that the counter was made under
+     */
+    public void incrementCountOrThrow(final int uid) {
+        incrementCountOrThrow(uid, 1 /* numToIncrement */);
+    }
+
+    public synchronized void incrementCountOrThrow(final int uid, final int numToIncrement) {
+        if (numToIncrement <= 0) {
+            throw new IllegalArgumentException("Increment count must be positive");
+        }
+        final long newCount = ((long) mUidToCount.get(uid, 0)) + numToIncrement;
+        if (newCount > mMaxCountPerUid) {
+            throw new IllegalStateException("Uid " + uid + " exceeded its allowed limit");
+        }
+        // Since the count cannot be greater than Integer.MAX_VALUE here since mMaxCountPerUid
+        // is an integer, it is safe to cast to int.
+        mUidToCount.put(uid, (int) newCount);
+    }
+
+    /**
+     * Decrements the count of the given uid. Throws an exception if the number
+     * of the counter goes below zero.
+     *
+     * @throws IllegalStateException if the number of counter for the uid goes below
+     *         zero.
+     *
+     * @param uid the uid that the count was made under
+     */
+    public void decrementCountOrThrow(final int uid) {
+        decrementCountOrThrow(uid, 1 /* numToDecrement */);
+    }
+
+    public synchronized void decrementCountOrThrow(final int uid, final int numToDecrement) {
+        if (numToDecrement <= 0) {
+            throw new IllegalArgumentException("Decrement count must be positive");
+        }
+        final int newCount = mUidToCount.get(uid, 0) - numToDecrement;
+        if (newCount < 0) {
+            throw new IllegalStateException("BUG: too small count " + newCount + " for UID " + uid);
+        } else if (newCount == 0) {
+            mUidToCount.delete(uid);
+        } else {
+            mUidToCount.put(uid, newCount);
+        }
+    }
+}
diff --git a/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index e25e17d..e3f48e5 100644
--- a/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -27,15 +27,6 @@
 
 namespace android {
 
-static jint com_android_net_module_util_BpfMap_closeMap(JNIEnv *env, jobject clazz,
-        jint fd) {
-    int ret = close(fd);
-
-    if (ret) jniThrowErrnoException(env, "closeMap", errno);
-
-    return ret;
-}
-
 static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
         jstring path, jint mode) {
     ScopedUtfChars pathname(env, path);
@@ -112,8 +103,6 @@
  */
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    { "closeMap", "(I)I",
-        (void*) com_android_net_module_util_BpfMap_closeMap },
     { "bpfFdGet", "(Ljava/lang/String;I)I",
         (void*) com_android_net_module_util_BpfMap_bpfFdGet },
     { "writeToMapEntry", "(I[B[BI)V",
diff --git a/common/netd/libnetdutils/InternetAddressesTest.cpp b/common/netd/libnetdutils/InternetAddressesTest.cpp
index f75fa76..9e37d11 100644
--- a/common/netd/libnetdutils/InternetAddressesTest.cpp
+++ b/common/netd/libnetdutils/InternetAddressesTest.cpp
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include <android-base/macros.h>
+#include <fmt/format.h>
 #include <gtest/gtest.h>
 
 #include "netdutils/InternetAddresses.h"
@@ -438,6 +439,192 @@
     }
 }
 
+TEST(IPPrefixTest, containsPrefix) {
+    const struct {
+        const char* prefix;
+        const char* otherPrefix;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, other={}, expect={}", prefix, otherPrefix, expected);
+        }
+    } testExpectations[] = {
+            {"192.0.0.0/8", "192.0.0.0/8", true},
+            {"192.1.0.0/16", "192.1.0.0/16", true},
+            {"192.1.2.0/24", "192.1.2.0/24", true},
+            {"192.1.2.3/32", "192.1.2.3/32", true},
+            {"0.0.0.0/0", "192.0.0.0/8", true},
+            {"0.0.0.0/0", "192.1.0.0/16", true},
+            {"0.0.0.0/0", "192.1.2.0/24", true},
+            {"0.0.0.0/0", "192.1.2.3/32", true},
+            {"192.0.0.0/8", "192.1.0.0/16", true},
+            {"192.0.0.0/8", "192.1.2.0/24", true},
+            {"192.0.0.0/8", "192.1.2.5/32", true},
+            {"192.1.0.0/16", "192.1.2.0/24", true},
+            {"192.1.0.0/16", "192.1.3.6/32", true},
+            {"192.5.6.0/24", "192.5.6.7/32", true},
+            {"192.1.2.3/32", "192.1.2.0/24", false},
+            {"192.1.2.3/32", "192.1.0.0/16", false},
+            {"192.1.2.3/32", "192.0.0.0/8", false},
+            {"192.1.2.3/32", "0.0.0.0/0", false},
+            {"192.1.2.0/24", "192.1.0.0/16", false},
+            {"192.1.2.0/24", "192.0.0.0/8", false},
+            {"192.1.2.0/24", "0.0.0.0/0", false},
+            {"192.9.0.0/16", "192.0.0.0/8", false},
+            {"192.9.0.0/16", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "191.0.0.0/8", false},
+            {"191.0.0.0/8", "192.0.0.0/8", false},
+            {"192.8.0.0/16", "192.7.0.0/16", false},
+            {"192.7.0.0/16", "192.8.0.0/16", false},
+            {"192.8.6.0/24", "192.7.5.0/24", false},
+            {"192.7.5.0/24", "192.8.6.0/24", false},
+            {"192.8.6.100/32", "192.8.6.200/32", false},
+            {"192.8.6.200/32", "192.8.6.100/32", false},
+            {"192.0.0.0/8", "192.0.0.0/12", true},
+            {"192.0.0.0/12", "192.0.0.0/8", false},
+            {"2001::/16", "2001::/16", true},
+            {"2001:db8::/32", "2001:db8::/32", true},
+            {"2001:db8:cafe::/48", "2001:db8:cafe::/48", true},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:d00d::/64", true},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001:db8:cafe:d00d:fec0:de::/96", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001:db8::cafe:0:1/128", "2001:db8::cafe:0:1/128", true},
+            {"2001::/16", "2001:db8::/32", true},
+            {"2001::/16", "2001:db8:cafe::/48", true},
+            {"2001::/16", "2001:db8:cafe:d00d::/64", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"::/0", "2001::/16", true},
+            {"::/0", "2001:db8::/32", true},
+            {"::/0", "2001:db8:cafe::/48", true},
+            {"::/0", "2001:db8:cafe:d00d::/64", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"2001:db8::dd/128", "2001::/16", false},
+            {"2001:db8::dd/128", "2001:db8::/32", false},
+            {"2001:db8::dd/128", "2001:db8:cafe::/48", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de::/96", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db7::/32", "2001:db8::/32", false},
+            {"2001:db8::/32", "2001:db7::/32", false},
+            {"2001:db8:caff::/48", "2001:db8:cafe::/48", false},
+            {"2001:db8:cafe::/48", "2001:db8:caff::/48", false},
+            {"2001:db8:cafe:a00d::/64", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:a00d::/64", false},
+            {"2001:db8:cafe:d00d:fec1::/80", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec1::/80", false},
+            {"2001:db8:cafe:d00d:fec0:dd::/96", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "2001:db8:cafe:d00d:fec0:dd::/96", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"2001:db8::cafe:0:123/128", "2001:db8::cafe:0:456/128", false},
+            {"2001:db8::cafe:0:456/128", "2001:db8::cafe:0:123/128", false},
+            {"2001:db8::/32", "2001:db8::/64", true},
+            {"2001:db8::/64", "2001:db8::/32", false},
+            {"::/0", "0.0.0.0/0", false},
+            {"::/0", "1.0.0.0/8", false},
+            {"::/0", "1.2.0.0/16", false},
+            {"::/0", "1.2.3.0/24", false},
+            {"::/0", "1.2.3.4/32", false},
+            {"2001::/16", "1.2.3.4/32", false},
+            {"2001::db8::/32", "1.2.3.4/32", false},
+            {"2001:db8:cafe::/48", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d::/64", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "1.2.3.4/32", false},
+            {"0.0.0.0/0", "::/0", false},
+            {"0.0.0.0/0", "2001::/16", false},
+            {"0.0.0.0/0", "2001::db8::/32", false},
+            {"0.0.0.0/0", "2001:db8:cafe::/48", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d::/64", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.4/32", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.0/24", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.0.0/16", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.0.0.0/8", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPPrefix b = IPPrefix::forString(expectation.otherPrefix);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
+TEST(IPPrefixTest, containsAddress) {
+    const struct {
+        const char* prefix;
+        const char* address;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, address={}, expect={}", prefix, address, expected);
+        }
+    } testExpectations[] = {
+        {"0.0.0.0/0", "255.255.255.255", true},
+        {"0.0.0.0/0", "1.2.3.4", true},
+        {"0.0.0.0/0", "1.2.3.0", true},
+        {"0.0.0.0/0", "1.2.0.0", true},
+        {"0.0.0.0/0", "1.0.0.0", true},
+        {"0.0.0.0/0", "0.0.0.0", true},
+        {"0.0.0.0/0", "2001:4868:4860::8888", false},
+        {"0.0.0.0/0", "::/0", false},
+        {"192.0.2.0/23", "192.0.2.0", true},
+        {"192.0.2.0/23", "192.0.2.43", true},
+        {"192.0.2.0/23", "192.0.3.21", true},
+        {"192.0.2.0/23", "192.0.0.21", false},
+        {"192.0.2.0/23", "8.8.8.8", false},
+        {"192.0.2.0/23", "2001:4868:4860::8888", false},
+        {"192.0.2.0/23", "::/0", false},
+        {"1.2.3.4/32", "1.2.3.4", true},
+        {"1.2.3.4/32", "1.2.3.5", false},
+        {"10.0.0.0/8", "10.2.0.0", true},
+        {"10.0.0.0/8", "10.2.3.5", true},
+        {"10.0.0.0/8", "10.0.0.0", true},
+        {"10.0.0.0/8", "10.255.255.254", true},
+        {"10.0.0.0/8", "11.0.0.0", false},
+        {"::/0", "2001:db8:f000::ace:d00c", true},
+        {"::/0", "2002:db8:f00::ace:d00d", true},
+        {"::/0", "2001:db7:f00::ace:d00e", true},
+        {"::/0", "2001:db8:f01::bad:d00d", true},
+        {"::/0", "::", true},
+        {"::/0", "0.0.0.0", false},
+        {"::/0", "1.2.3.4", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00c", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00e", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::bad:d00d", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:4868:4860::8888", false},
+        {"2001:db8:f00::ace:d00d/127", "8.8.8.8", false},
+        {"2001:db8:f00::ace:d00d/127", "0.0.0.0", false},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00c", false},
+        {"2001::/16", "2001::", true},
+        {"2001::/16", "2001:db8:f00::ace:d00d", true},
+        {"2001::/16", "2001:db8:f00::bad:d00d", true},
+        {"2001::/16", "2001::abc", true},
+        {"2001::/16", "2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
+        {"2001::/16", "2000::", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPAddress b = IPAddress::forString(expectation.address);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
 TEST(IPPrefixTest, GamutOfOperators) {
     const std::vector<OperatorExpectation<IPPrefix>> kExpectations{
             {EQ, IPPrefix(), IPPrefix()},
diff --git a/common/netd/libnetdutils/include/netdutils/InternetAddresses.h b/common/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d5cbe2b..d10cec7 100644
--- a/common/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/common/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -221,6 +221,12 @@
     in_addr addr4() const noexcept { return mData.ip.v4; }
     in6_addr addr6() const noexcept { return mData.ip.v6; }
     constexpr int length() const noexcept { return mData.cidrlen; }
+    bool contains(const IPPrefix& other) {
+        return length() <= other.length() && IPPrefix(other.ip(), length()).ip() == ip();
+    }
+    bool contains(const IPAddress& other) {
+        return IPPrefix(other, length()).ip() == ip();
+    }
 
     bool isUninitialized() const noexcept;
     std::string toString() const noexcept;
diff --git a/common/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt b/common/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
new file mode 100644
index 0000000..0f2d52a
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 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 com.android.net.module.util
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PerUidCounterTest {
+    private val UID_A = 1000
+    private val UID_B = 1001
+
+    @Test
+    fun testCounterMaximum() {
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(-1)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(0)
+        }
+
+        val largeMaxCounter = PerUidCounter(Integer.MAX_VALUE)
+        largeMaxCounter.incrementCountOrThrow(UID_A, Integer.MAX_VALUE)
+        assertFailsWith<IllegalStateException> {
+            largeMaxCounter.incrementCountOrThrow(UID_A)
+        }
+    }
+
+    @Test
+    fun testIncrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the increment count cannot be zero.
+        assertFailsWith<IllegalArgumentException> {
+            counter.incrementCountOrThrow(UID_A, 0)
+        }
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B, 2)
+        counter.incrementCountOrThrow(UID_B)
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_A)
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_B)
+        }
+
+        // Verify exception can be triggered again.
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A, 3)
+        }
+    }
+
+    @Test
+    fun testDecrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the decrement count cannot be zero.
+        assertFailsWith<IllegalArgumentException> {
+            counter.decrementCountOrThrow(UID_A, 0)
+        }
+
+        // Verify the count cannot go below zero.
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, 5)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, Integer.MAX_VALUE)
+        }
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B)
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, 3)
+        }
+        counter.decrementCountOrThrow(UID_A)
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+    }
+}
\ No newline at end of file