Merge "TetherController: Fix a memory and fd leak"
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
new file mode 100644
index 0000000..4302129
--- /dev/null
+++ b/bpf_progs/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2019 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.
+//
+
+//
+// bpf kernel programs
+//
+bpf {
+    name: "netd.o",
+    srcs: ["netd.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    include_dirs: [
+        "system/netd/libnetdbpf/include",
+        "system/netd/libnetdutils/include",
+    ],
+}
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
new file mode 100644
index 0000000..38782f9
--- /dev/null
+++ b/bpf_progs/netd.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include "netd.h"
+#include <linux/bpf.h>
+
+SEC("cgroupskb/ingress/stats")
+int bpf_cgroup_ingress(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, BPF_INGRESS);
+}
+
+SEC("cgroupskb/egress/stats")
+int bpf_cgroup_egress(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, BPF_EGRESS);
+}
+
+SEC("skfilter/egress/xtbpf")
+int xt_bpf_egress_prog(struct __sk_buff* skb) {
+    uint32_t key = skb->ifindex;
+    bpf_update_stats(skb, &iface_stats_map, BPF_EGRESS, &key);
+    return BPF_MATCH;
+}
+
+SEC("skfilter/ingress/xtbpf")
+int xt_bpf_ingress_prog(struct __sk_buff* skb) {
+    uint32_t key = skb->ifindex;
+    bpf_update_stats(skb, &iface_stats_map, BPF_INGRESS, &key);
+    return BPF_MATCH;
+}
+
+SEC("skfilter/whitelist/xtbpf")
+int xt_bpf_whitelist_prog(struct __sk_buff* skb) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    if (is_system_uid(sock_uid)) return BPF_MATCH;
+    uint8_t* whitelistMatch = bpf_map_lookup_elem(&uid_owner_map, &sock_uid);
+    if (whitelistMatch) return *whitelistMatch & HAPPY_BOX_MATCH;
+    return BPF_NOMATCH;
+}
+
+SEC("skfilter/blacklist/xtbpf")
+int xt_bpf_blacklist_prog(struct __sk_buff* skb) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    uint8_t* blacklistMatch = bpf_map_lookup_elem(&uid_owner_map, &sock_uid);
+    if (blacklistMatch) return *blacklistMatch & PENALTY_BOX_MATCH;
+    return BPF_NOMATCH;
+}
+
+struct bpf_map_def SEC("maps") uid_permission_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(uint8_t),
+        .max_entries = UID_OWNER_MAP_SIZE,
+};
+
+SEC("cgroupsock/inet/creat")
+int inet_socket_create(struct bpf_sock* sk) {
+    uint64_t gid_uid = bpf_get_current_uid_gid();
+    /*
+     * A given app is guaranteed to have the same app ID in all the profiles in
+     * which it is installed, and install permission is granted to app for all
+     * user at install time so we only check the appId part of a request uid at
+     * run time. See UserHandle#isSameApp for detail.
+     */
+    uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+    uint8_t* internetPermission = bpf_map_lookup_elem(&uid_permission_map, &appId);
+    if (internetPermission) return *internetPermission & ALLOW_SOCK_CREATE;
+    return NO_PERMISSION;
+}
+
+char _license[] SEC("license") = "Apache 2.0";
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
new file mode 100644
index 0000000..3c36807
--- /dev/null
+++ b/bpf_progs/netd.h
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+/*
+ * This h file together with netd.c is used for compiling the eBPF kernel
+ * program.
+ */
+
+#include <bpf_helpers.h>
+#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "netdbpf/bpf_shared.h"
+
+struct uid_tag {
+    uint32_t uid;
+    uint32_t tag;
+};
+
+struct stats_key {
+    uint32_t uid;
+    uint32_t tag;
+    uint32_t counterSet;
+    uint32_t ifaceIndex;
+};
+
+struct stats_value {
+    uint64_t rxPackets;
+    uint64_t rxBytes;
+    uint64_t txPackets;
+    uint64_t txBytes;
+};
+
+struct IfaceValue {
+    char name[IFNAMSIZ];
+};
+
+// This is defined for cgroup bpf filter only.
+#define BPF_PASS 1
+#define BPF_DROP 0
+
+// This is used for xt_bpf program only.
+#define BPF_NOMATCH 0
+#define BPF_MATCH 1
+
+#define BPF_EGRESS 0
+#define BPF_INGRESS 1
+
+#define IP_PROTO_OFF offsetof(struct iphdr, protocol)
+#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr)
+#define IPPROTO_IHL_OFF 0
+#define TCP_FLAG_OFF 13
+#define RST_OFFSET 2
+
+struct bpf_map_def SEC("maps") cookie_tag_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint64_t),
+        .value_size = sizeof(struct uid_tag),
+        .max_entries = COOKIE_UID_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") uid_counterset_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(uint8_t),
+        .max_entries = UID_COUNTERSET_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") app_uid_stats_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(struct stats_value),
+        .max_entries = APP_STATS_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") stats_map_A = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(struct stats_key),
+        .value_size = sizeof(struct stats_value),
+        .max_entries = STATS_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") stats_map_B = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(struct stats_key),
+        .value_size = sizeof(struct stats_value),
+        .max_entries = STATS_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") iface_stats_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(struct stats_value),
+        .max_entries = IFACE_STATS_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") configuration_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(uint8_t),
+        .max_entries = CONFIGURATION_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") uid_owner_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(uint8_t),
+        .max_entries = UID_OWNER_MAP_SIZE,
+};
+
+struct bpf_map_def SEC("maps") iface_index_name_map = {
+        .type = BPF_MAP_TYPE_HASH,
+        .key_size = sizeof(uint32_t),
+        .value_size = sizeof(struct IfaceValue),
+        .max_entries = IFACE_INDEX_NAME_MAP_SIZE,
+};
+
+static __always_inline int is_system_uid(uint32_t uid) {
+    return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+}
+
+static __always_inline inline void bpf_update_stats(struct __sk_buff* skb, struct bpf_map_def* map,
+                                                    int direction, void* key) {
+    struct stats_value* value;
+    value = bpf_map_lookup_elem(map, key);
+    if (!value) {
+        struct stats_value newValue = {};
+        bpf_map_update_elem(map, key, &newValue, BPF_NOEXIST);
+        value = bpf_map_lookup_elem(map, key);
+    }
+    if (value) {
+        if (direction == BPF_EGRESS) {
+            __sync_fetch_and_add(&value->txPackets, 1);
+            __sync_fetch_and_add(&value->txBytes, skb->len);
+        } else if (direction == BPF_INGRESS) {
+            __sync_fetch_and_add(&value->rxPackets, 1);
+            __sync_fetch_and_add(&value->rxBytes, skb->len);
+        }
+    }
+}
+
+static inline bool skip_owner_match(struct __sk_buff* skb) {
+    int offset = -1;
+    int ret = 0;
+    if (skb->protocol == ETH_P_IP) {
+        offset = IP_PROTO_OFF;
+        uint8_t proto, ihl;
+        uint16_t flag;
+        ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+        if (!ret) {
+            if (proto == IPPROTO_ESP) {
+                return true;
+            } else if (proto == IPPROTO_TCP) {
+                ret = bpf_skb_load_bytes(skb, IPPROTO_IHL_OFF, &ihl, 1);
+                ihl = ihl & 0x0F;
+                ret = bpf_skb_load_bytes(skb, ihl * 4 + TCP_FLAG_OFF, &flag, 1);
+                if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+                    return true;
+                }
+            }
+        }
+    } else if (skb->protocol == ETH_P_IPV6) {
+        offset = IPV6_PROTO_OFF;
+        uint8_t proto;
+        ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+        if (!ret) {
+            if (proto == IPPROTO_ESP) {
+                return true;
+            } else if (proto == IPPROTO_TCP) {
+                uint16_t flag;
+                ret = bpf_skb_load_bytes(skb, sizeof(struct ipv6hdr) + TCP_FLAG_OFF, &flag, 1);
+                if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
+static __always_inline BpfConfig getConfig(uint32_t configKey) {
+    uint32_t mapSettingKey = configKey;
+    BpfConfig* config = bpf_map_lookup_elem(&configuration_map, &mapSettingKey);
+    if (!config) {
+        // Couldn't read configuration entry. Assume everything is disabled.
+        return DEFAULT_CONFIG;
+    }
+    return *config;
+}
+
+static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid) {
+    if (skip_owner_match(skb)) return BPF_PASS;
+
+    if ((uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID)) return BPF_PASS;
+
+    BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
+    if (!enabledRules) {
+        return BPF_PASS;
+    }
+
+    uint8_t* uidEntry = bpf_map_lookup_elem(&uid_owner_map, &uid);
+    uint8_t uidRules = uidEntry ? *uidEntry : 0;
+    if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
+        return BPF_DROP;
+    }
+    if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
+        return BPF_DROP;
+    }
+    if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
+        return BPF_DROP;
+    }
+    return BPF_PASS;
+}
+
+static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
+                                                            void* key, uint8_t selectedMap) {
+    if (selectedMap == SELECT_MAP_A) {
+        bpf_update_stats(skb, &stats_map_A, direction, key);
+    } else if (selectedMap == SELECT_MAP_B) {
+        bpf_update_stats(skb, &stats_map_B, direction, key);
+    }
+}
+
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, int direction) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    int match = bpf_owner_match(skb, sock_uid);
+    if ((direction == BPF_EGRESS) && (match == BPF_DROP)) {
+        // If an outbound packet is going to be dropped, we do not count that
+        // traffic.
+        return match;
+    }
+
+    uint64_t cookie = bpf_get_socket_cookie(skb);
+    struct uid_tag* utag = bpf_map_lookup_elem(&cookie_tag_map, &cookie);
+    uint32_t uid, tag;
+    if (utag) {
+        uid = utag->uid;
+        tag = utag->tag;
+    } else {
+        uid = sock_uid;
+        tag = 0;
+    }
+
+    struct stats_key key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
+
+    uint8_t* counterSet = bpf_map_lookup_elem(&uid_counterset_map, &uid);
+    if (counterSet) key.counterSet = (uint32_t)*counterSet;
+
+    uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+    uint8_t* selectedMap = bpf_map_lookup_elem(&configuration_map, &mapSettingKey);
+    if (!selectedMap) {
+        return match;
+    }
+
+    if (tag) {
+        update_stats_with_config(skb, direction, &key, *selectedMap);
+    }
+
+    key.tag = 0;
+    update_stats_with_config(skb, direction, &key, *selectedMap);
+    bpf_update_stats(skb, &app_uid_stats_map, direction, &uid);
+    return match;
+}
diff --git a/libnetdbpf/BpfNetworkStatsTest.cpp b/libnetdbpf/BpfNetworkStatsTest.cpp
index 0d17677..8f95787 100644
--- a/libnetdbpf/BpfNetworkStatsTest.cpp
+++ b/libnetdbpf/BpfNetworkStatsTest.cpp
@@ -38,11 +38,6 @@
 #include "bpf/BpfUtils.h"
 #include "netdbpf/BpfNetworkStats.h"
 
-using ::testing::_;
-using ::testing::ByMove;
-using ::testing::Invoke;
-using ::testing::Return;
-using ::testing::StrictMock;
 using ::testing::Test;
 
 namespace android {
diff --git a/server/MDnsSdListener.cpp b/server/MDnsSdListener.cpp
index f96df1a..83726c3 100644
--- a/server/MDnsSdListener.cpp
+++ b/server/MDnsSdListener.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include "MDnsSdListener.h"
+
 #include <arpa/inet.h>
 #include <dirent.h>
 #include <errno.h>
@@ -21,12 +23,12 @@
 #include <netdb.h>
 #include <netinet/in.h>
 #include <pthread.h>
+#include <resolv.h>
 #include <stdlib.h>
+#include <string.h>
 #include <sys/poll.h>
 #include <sys/socket.h>
 #include <sys/types.h>
-#include <string.h>
-#include <resolv.h>
 
 #define LOG_TAG "MDnsDS"
 #define DBG 1
@@ -36,7 +38,6 @@
 #include <log/log.h>
 #include <sysutils/SocketClient.h>
 
-#include "MDnsSdListener.h"
 #include "ResponseCode.h"
 #include "thread_util.h"
 
@@ -524,7 +525,6 @@
     mPollRefs = nullptr;
     mPollSize = 10;
     socketpair(AF_LOCAL, SOCK_STREAM, 0, mCtrlSocketPair);
-    pthread_mutex_init(&mHeadMutex, nullptr);
 
     const int rval = ::android::net::threadLaunch(this);
     if (rval != 0) {
@@ -554,35 +554,27 @@
 }
 
 int MDnsSdListener::Monitor::startService() {
-    int result = 0;
     char property_value[PROPERTY_VALUE_MAX];
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     property_get(MDNS_SERVICE_STATUS, property_value, "");
     if (strcmp("running", property_value) != 0) {
         ALOGD("Starting MDNSD");
         property_set("ctl.start", MDNS_SERVICE_NAME);
         wait_for_property(MDNS_SERVICE_STATUS, "running", 5);
-        result = -1;
-    } else {
-        result = 0;
+        return -1;
     }
-    pthread_mutex_unlock(&mHeadMutex);
-    return result;
+    return 0;
 }
 
 int MDnsSdListener::Monitor::stopService() {
-    int result = 0;
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     if (mHead == nullptr) {
         ALOGD("Stopping MDNSD");
         property_set("ctl.stop", MDNS_SERVICE_NAME);
         wait_for_property(MDNS_SERVICE_STATUS, "stopped", 5);
-        result = -1;
-    } else {
-        result = 0;
+        return -1;
     }
-    pthread_mutex_unlock(&mHeadMutex);
-    return result;
+    return 0;
 }
 
 void MDnsSdListener::Monitor::run() {
@@ -612,9 +604,8 @@
                         ALOGD("Monitor found [%d].revents = %d - calling ProcessResults",
                                 i, mPollFds[i].revents);
                     }
-                    pthread_mutex_lock(&mHeadMutex);
+                    std::lock_guard guard(mMutex);
                     DNSServiceProcessResult(*(mPollRefs[i]));
-                    pthread_mutex_unlock(&mHeadMutex);
                     mPollFds[i].revents = 0;
                 }
             }
@@ -645,7 +636,7 @@
     if (VDBG) {
         ALOGD("MDnsSdListener::Monitor poll rescanning - size=%d, live=%d", mPollSize, mLiveCount);
     }
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     Element **prevPtr = &mHead;
     int i = 1;
     if (mPollSize <= mLiveCount) {
@@ -690,7 +681,7 @@
             prevPtr = &((*prevPtr)->mNext);
         }
     }
-    pthread_mutex_unlock(&mHeadMutex);
+
     return i;
 }
 
@@ -700,53 +691,45 @@
         return nullptr;
     }
     Element *e = new Element(id, context);
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     e->mNext = mHead;
     mHead = e;
-    pthread_mutex_unlock(&mHeadMutex);
     return &(e->mRef);
 }
 
 DNSServiceRef *MDnsSdListener::Monitor::lookupServiceRef(int id) {
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     Element *cur = mHead;
     while (cur != nullptr) {
         if (cur->mId == id) {
             DNSServiceRef *result = &(cur->mRef);
-            pthread_mutex_unlock(&mHeadMutex);
             return result;
         }
         cur = cur->mNext;
     }
-    pthread_mutex_unlock(&mHeadMutex);
     return nullptr;
 }
 
 void MDnsSdListener::Monitor::startMonitoring(int id) {
     if (VDBG) ALOGD("startMonitoring %d", id);
-    pthread_mutex_lock(&mHeadMutex);
-    Element *cur = mHead;
-    while (cur != nullptr) {
+    std::lock_guard guard(mMutex);
+    for (Element* cur = mHead; cur != nullptr; cur = cur->mNext) {
         if (cur->mId == id) {
             if (DBG_RESCAN) ALOGD("marking %p as ready to be added", cur);
             mLiveCount++;
             cur->mReady = 1;
-            pthread_mutex_unlock(&mHeadMutex);
             write(mCtrlSocketPair[1], RESCAN, 1);  // trigger a rescan for a fresh poll
             if (VDBG) ALOGD("triggering rescan");
             return;
         }
-        cur = cur->mNext;
     }
-    pthread_mutex_unlock(&mHeadMutex);
 }
 
 void MDnsSdListener::Monitor::freeServiceRef(int id) {
     if (VDBG) ALOGD("freeServiceRef %d", id);
-    pthread_mutex_lock(&mHeadMutex);
-    Element **prevPtr = &mHead;
-    Element *cur;
-    while (*prevPtr != nullptr) {
+    std::lock_guard guard(mMutex);
+    Element* cur;
+    for (Element** prevPtr = &mHead; *prevPtr != nullptr; prevPtr = &(cur->mNext)) {
         cur = *prevPtr;
         if (cur->mId == id) {
             if (DBG_RESCAN) ALOGD("marking %p as ready to be removed", cur);
@@ -760,16 +743,12 @@
                 *prevPtr = cur->mNext;
                 delete cur;
             }
-            pthread_mutex_unlock(&mHeadMutex);
             return;
         }
-        prevPtr = &(cur->mNext);
     }
-    pthread_mutex_unlock(&mHeadMutex);
 }
 
 void MDnsSdListener::Monitor::deallocateServiceRef(DNSServiceRef* ref) {
-    pthread_mutex_lock(&mHeadMutex);
+    std::lock_guard guard(mMutex);
     DNSServiceRefDeallocate(*ref);
-    pthread_mutex_unlock(&mHeadMutex);
 }
diff --git a/server/MDnsSdListener.h b/server/MDnsSdListener.h
index adec855..47ddc28 100644
--- a/server/MDnsSdListener.h
+++ b/server/MDnsSdListener.h
@@ -17,10 +17,10 @@
 #ifndef _MDNSSDLISTENER_H__
 #define _MDNSSDLISTENER_H__
 
-#include <pthread.h>
-
+#include <android-base/thread_annotations.h>
 #include <dns_sd.h>
 #include <sysutils/FrameworkListener.h>
+#include <mutex>
 
 #include "NetdCommand.h"
 
@@ -93,13 +93,13 @@
             Context *mContext;
             int mReady = 0;
         };
-        Element *mHead;
+        Element* mHead GUARDED_BY(mMutex);
         int mLiveCount;
         struct pollfd *mPollFds;
         DNSServiceRef **mPollRefs;
         int mPollSize;
         int mCtrlSocketPair[2];
-        pthread_mutex_t mHeadMutex;
+        std::mutex mMutex;
     };
 
     class Handler : public NetdCommand {