Process DSCP QoS events for policies

New events to handle adding and removing of DSCP QoS policies.
Async indication sends status back to client if the policy
has been added, failed, or if the policy limit has been
reached.

Bug: 202871011
Change-Id: I7988d22ae625ad0dd415927d2943de4a749e6fb8
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 145eade..d2c0e18 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -126,6 +126,7 @@
 import android.net.ConnectivitySettingsManager;
 import android.net.DataStallReportParcelable;
 import android.net.DnsResolverServiceManager;
+import android.net.DscpPolicy;
 import android.net.ICaptivePortal;
 import android.net.IConnectivityDiagnosticsCallback;
 import android.net.IConnectivityManager;
@@ -220,6 +221,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.sysprop.NetworkProperties;
+import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -250,6 +252,7 @@
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
+import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
@@ -389,6 +392,7 @@
     protected IDnsResolver mDnsResolver;
     @VisibleForTesting
     protected INetd mNetd;
+    private DscpPolicyTracker mDscpPolicyTracker = null;
     private NetworkStatsManager mStatsManager;
     private NetworkPolicyManager mPolicyManager;
     private final NetdCallback mNetdCallback;
@@ -1489,6 +1493,12 @@
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
                 new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
                 mLingerDelayMs, mQosCallbackTracker, mDeps);
+
+        try {
+            mDscpPolicyTracker = new DscpPolicyTracker();
+        } catch (ErrnoException e) {
+            loge("Unable to create DscpPolicyTracker");
+        }
     }
 
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
@@ -3406,6 +3416,25 @@
                     nai.setLingerDuration((int) arg.second);
                     break;
                 }
+                case NetworkAgent.EVENT_ADD_DSCP_POLICY: {
+                    DscpPolicy policy = (DscpPolicy) arg.second;
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.addDscpPolicy(nai, policy);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_REMOVE_DSCP_POLICY: {
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.removeDscpPolicy(nai, (int) arg.second);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                    }
+                    break;
+                }
             }
         }
 
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
new file mode 100644
index 0000000..0512a44
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2021 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.server.connectivity;
+
+import static android.net.DscpPolicy.STATUS_DELETED;
+import static android.net.DscpPolicy.STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+import static android.net.DscpPolicy.STATUS_POLICY_NOT_FOUND;
+import static android.net.DscpPolicy.STATUS_SUCCESS;
+import static android.system.OsConstants.ETH_P_ALL;
+
+import android.annotation.NonNull;
+import android.net.DscpPolicy;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.TcUtils;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * DscpPolicyTracker has a single entry point from ConnectivityService handler.
+ * This guarantees that all code runs on the same thread and no locking is needed.
+ */
+public class DscpPolicyTracker {
+    static {
+        System.loadLibrary("com_android_connectivity_com_android_net_module_util_jni");
+    }
+
+    // After tethering and clat priorities.
+    static final short PRIO_DSCP = 5;
+
+    private static final String TAG = DscpPolicyTracker.class.getSimpleName();
+    private static final String PROG_PATH =
+            "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+    // Name is "map + *.o + map_name + map". Can probably shorten this
+    private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
+            "dscp_policy_ipv4_dscp_policies");
+    private static final String IPV6_POLICY_MAP_PATH = makeMapPath(
+            "dscp_policy_ipv6_dscp_policies");
+    private static final int MAX_POLICIES = 16;
+
+    private static String makeMapPath(String which) {
+        return "/sys/fs/bpf/map_" + which + "_map";
+    }
+
+    private Set<String> mAttachedIfaces;
+
+    private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
+    private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
+    private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+    public DscpPolicyTracker() throws ErrnoException {
+        mAttachedIfaces = new HashSet<String>();
+
+        mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+        mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
+                BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+        mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
+                BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+    }
+
+    private int getFirstFreeIndex() {
+        for (int i = 0; i < MAX_POLICIES; i++) {
+            if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+        }
+        return MAX_POLICIES;
+    }
+
+    private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
+        try {
+            nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Failed update policy status: ", e);
+        }
+    }
+
+    private boolean matchesIpv4(DscpPolicy policy) {
+        return ((policy.getDestinationAddress() == null
+                       || policy.getDestinationAddress() instanceof Inet4Address)
+            && (policy.getSourceAddress() == null
+                        || policy.getSourceAddress() instanceof Inet4Address));
+    }
+
+    private boolean matchesIpv6(DscpPolicy policy) {
+        return ((policy.getDestinationAddress() == null
+                       || policy.getDestinationAddress() instanceof Inet6Address)
+            && (policy.getSourceAddress() == null
+                        || policy.getSourceAddress() instanceof Inet6Address));
+    }
+
+    private int addDscpPolicyInternal(DscpPolicy policy) {
+        // If there is no existing policy with a matching ID, and we are already at
+        // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
+        final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
+        if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
+            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        }
+
+        // Currently all classifiers are supported, if any are removed return
+        // STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+        // and for any other generic error STATUS_REQUEST_DECLINED
+
+        int addIndex = 0;
+        // If a policy with a matching ID exists, replace it, otherwise use the next free
+        // index for the policy.
+        if (existingIndex != -1) {
+            addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
+        } else {
+            addIndex = getFirstFreeIndex();
+        }
+
+        try {
+            mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
+
+            // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
+            // are both null or if they are both instances of Inet6Address.
+            if (matchesIpv4(policy)) {
+                mBpfDscpIpv4Policies.insertOrReplaceEntry(
+                        new Struct.U32(addIndex),
+                        new DscpPolicyValue(policy.getSourceAddress(),
+                            policy.getDestinationAddress(),
+                            policy.getSourcePort(), policy.getDestinationPortRange(),
+                            (short) policy.getProtocol(), (short) policy.getDscpValue()));
+            }
+
+            // Add v6 policy to mBpfDscpIpv6Policies if source and destination address
+            // are both null or if they are both instances of Inet6Address.
+            if (matchesIpv6(policy)) {
+                mBpfDscpIpv6Policies.insertOrReplaceEntry(
+                        new Struct.U32(addIndex),
+                        new DscpPolicyValue(policy.getSourceAddress(),
+                                policy.getDestinationAddress(),
+                                policy.getSourcePort(), policy.getDestinationPortRange(),
+                                (short) policy.getProtocol(), (short) policy.getDscpValue()));
+            }
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to insert policy into map: ", e);
+            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        }
+
+        return STATUS_SUCCESS;
+    }
+
+    /**
+     * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface
+     * if not already attached. Response will be sent back to nai with status.
+     *
+     * STATUS_SUCCESS - if policy was added successfully
+     * STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     */
+    public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            if (!attachProgram(nai.linkProperties.getInterfaceName())) {
+                Log.e(TAG, "Unable to attach program");
+                sendStatus(nai, policy.getPolicyId(), STATUS_INSUFFICIENT_PROCESSING_RESOURCES);
+                return;
+            }
+        }
+
+        int status = addDscpPolicyInternal(policy);
+        sendStatus(nai, policy.getPolicyId(), status);
+    }
+
+    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+        int status = STATUS_POLICY_NOT_FOUND;
+        try {
+            mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+            mBpfDscpIpv6Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+            status = STATUS_DELETED;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to delete policy from map: ", e);
+        }
+
+        sendStatus(nai, policyId, status);
+    }
+
+    /**
+     * Remove specified DSCP policy and detach program if no other policies are active.
+     */
+    public void removeDscpPolicy(NetworkAgentInfo nai, int policyId) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            // Nothing to remove since program is not attached. Send update back for policy id.
+            sendStatus(nai, policyId, STATUS_POLICY_NOT_FOUND);
+            return;
+        }
+
+        if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
+            removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
+            mPolicyIdToBpfMapIndex.delete(policyId);
+        }
+
+        // TODO: detach should only occur if no more policies are present on the nai's iface.
+        if (mPolicyIdToBpfMapIndex.size() == 0) {
+            detachProgram(nai.linkProperties.getInterfaceName());
+        }
+    }
+
+    /**
+     * Remove all DSCP policies and detach program.
+     */
+    // TODO: Remove all should only remove policies from corresponding nai iface.
+    public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            // Nothing to remove since program is not attached. Send update for policy
+            // id 0. The status update must contain a policy ID, and 0 is an invalid id.
+            sendStatus(nai, 0, STATUS_SUCCESS);
+            return;
+        }
+
+        for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
+            removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
+                    mPolicyIdToBpfMapIndex.valueAt(i));
+        }
+        mPolicyIdToBpfMapIndex.clear();
+
+        // Can detach program since no policies are active.
+        detachProgram(nai.linkProperties.getInterfaceName());
+    }
+
+    /**
+     * Attach BPF program
+     */
+    private boolean attachProgram(@NonNull String iface) {
+        // TODO: attach needs to be per iface not program.
+
+        try {
+            NetworkInterface netIface = NetworkInterface.getByName(iface);
+            TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
+                    PROG_PATH);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
+            return false;
+        }
+        mAttachedIfaces.add(iface);
+        return true;
+    }
+
+    /**
+     * Detach BPF program
+     */
+    public void detachProgram(@NonNull String iface) {
+        try {
+            NetworkInterface netIface = NetworkInterface.getByName(iface);
+            if (netIface != null) {
+                TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
+        }
+        mAttachedIfaces.remove(iface);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
new file mode 100644
index 0000000..cb40306
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 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.server.connectivity;
+
+import android.util.Log;
+import android.util.Range;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/** Value type for DSCP setting and rewriting to DSCP policy BPF maps. */
+public class DscpPolicyValue extends Struct {
+    private static final String TAG = DscpPolicyValue.class.getSimpleName();
+
+    // TODO: add the interface index.
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 1, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 2, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 3, type = Type.UBE16)
+    public final int dstPortStart;
+
+    @Field(order = 4, type = Type.UBE16)
+    public final int dstPortEnd;
+
+    @Field(order = 5, type = Type.U8)
+    public final short proto;
+
+    @Field(order = 6, type = Type.U8)
+    public final short dscp;
+
+    @Field(order = 7, type = Type.U8, padding = 3)
+    public final short mask;
+
+    private static final int SRC_IP_MASK = 0x1;
+    private static final int DST_IP_MASK = 0x02;
+    private static final int SRC_PORT_MASK = 0x4;
+    private static final int DST_PORT_MASK = 0x8;
+    private static final int PROTO_MASK = 0x10;
+
+    private boolean ipEmpty(final byte[] ip) {
+        for (int i = 0; i < ip.length; i++) {
+            if (ip[i] != 0) return false;
+        }
+        return true;
+    }
+
+    private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
+        final byte[] addr6 = new byte[16];
+        if (ia != null) {
+            final byte[] addr4 = ia.getAddress();
+            addr6[10] = (byte) 0xff;
+            addr6[11] = (byte) 0xff;
+            addr6[12] = addr4[0];
+            addr6[13] = addr4[1];
+            addr6[14] = addr4[2];
+            addr6[15] = addr4[3];
+        }
+        return addr6;
+    }
+
+    private byte[] toAddressField(InetAddress addr) {
+        if (addr == null) {
+            return EMPTY_ADDRESS_FIELD;
+        } else if (addr instanceof Inet4Address) {
+            return toIpv4MappedAddressBytes(addr);
+        } else {
+            return addr.getAddress();
+        }
+    }
+
+    private static final byte[] EMPTY_ADDRESS_FIELD =
+            InetAddress.parseNumericAddress("::").getAddress();
+
+    private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPortStart, final short proto, final short dscp) {
+        short mask = 0;
+        if (src46 != EMPTY_ADDRESS_FIELD) {
+            mask |= SRC_IP_MASK;
+        }
+        if (dst46 != EMPTY_ADDRESS_FIELD) {
+            mask |=  DST_IP_MASK;
+        }
+        if (srcPort != -1) {
+            mask |=  SRC_PORT_MASK;
+        }
+        if (dstPortStart != -1 && dstPortEnd != -1) {
+            mask |=  DST_PORT_MASK;
+        }
+        if (proto != -1) {
+            mask |=  PROTO_MASK;
+        }
+        return mask;
+    }
+
+    // This constructor is necessary for BpfMap#getValue since all values must be
+    // in the constructor.
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+            final int dstPortStart, final int dstPortEnd, final short proto,
+            final short dscp) {
+        this.src46 = toAddressField(src46);
+        this.dst46 = toAddressField(dst46);
+
+        // These params need to be stored as 0 because uints are used in BpfMap.
+        // If they are -1 BpfMap write will throw errors.
+        this.srcPort = srcPort != -1 ? srcPort : 0;
+        this.dstPortStart = dstPortStart != -1 ? dstPortStart : 0;
+        this.dstPortEnd = dstPortEnd != -1 ? dstPortEnd : 0;
+        this.proto = proto != -1 ? proto : 0;
+
+        this.dscp = dscp;
+        // Use member variables for IP since byte[] is needed and api variables for everything else
+        // so -1 is passed into mask if parameter is not present.
+        this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+    }
+
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+            final Range<Integer> dstPort, final short proto,
+            final short dscp) {
+        this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+                dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
+    }
+
+    public static final DscpPolicyValue NONE = new DscpPolicyValue(
+            null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+            -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
+            (short) 0 /* dscp */);
+
+    @Override
+    public String toString() {
+        String srcIpString = "empty";
+        String dstIpString = "empty";
+
+        // Separate try/catch for IP's so it's easier to debug.
+        try {
+            srcIpString = InetAddress.getByAddress(src46).getHostAddress();
+        }  catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid SRC IP address", e);
+        }
+
+        try {
+            dstIpString = InetAddress.getByAddress(src46).getHostAddress();
+        }  catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid DST IP address", e);
+        }
+
+        try {
+            return String.format(
+                    "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
+                    + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
+                    dstPortEnd, proto, dscp);
+        } catch (IllegalArgumentException e) {
+            return String.format("String format error: " + e);
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index b7f3ed9..1a7248a 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.net.CaptivePortalData;
+import android.net.DscpPolicy;
 import android.net.IDnsResolver;
 import android.net.INetd;
 import android.net.INetworkAgent;
@@ -700,6 +701,24 @@
             mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
                     new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
         }
+
+        @Override
+        public void sendAddDscpPolicy(final DscpPolicy policy) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_ADD_DSCP_POLICY,
+                    new Pair<>(NetworkAgentInfo.this, policy)).sendToTarget();
+        }
+
+        @Override
+        public void sendRemoveDscpPolicy(final int policyId) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_DSCP_POLICY,
+                    new Pair<>(NetworkAgentInfo.this, policyId)).sendToTarget();
+        }
+
+        @Override
+        public void sendRemoveAllDscpPolicies() {
+            mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES,
+                    new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+        }
     }
 
     /**