[dbus] enhance external route support (#399)

* Add ExternalRoute structure
* Add ExternalRouteTable property
* Enhance tests for the external route.
diff --git a/src/dbus/client/thread_api_dbus.cpp b/src/dbus/client/thread_api_dbus.cpp
index 208d729..c4e5cb3 100644
--- a/src/dbus/client/thread_api_dbus.cpp
+++ b/src/dbus/client/thread_api_dbus.cpp
@@ -348,9 +348,9 @@
     return CallDBusMethodSync(OTBR_DBUS_REMOVE_ON_MESH_PREFIX_METHOD, std::tie(aPrefix));
 }
 
-ClientError ThreadApiDBus::AddExternalRoute(const Ip6Prefix &aPrefix, int8_t aPreference, bool aStable)
+ClientError ThreadApiDBus::AddExternalRoute(const ExternalRoute &aExternalRoute)
 {
-    return CallDBusMethodSync(OTBR_DBUS_ADD_EXTERNAL_ROUTE_METHOD, std::tie(aPrefix, aPreference, aStable));
+    return CallDBusMethodSync(OTBR_DBUS_ADD_EXTERNAL_ROUTE_METHOD, std::tie(aExternalRoute));
 }
 
 ClientError ThreadApiDBus::RemoveExternalRoute(const Ip6Prefix &aPrefix)
@@ -504,6 +504,11 @@
     return GetProperty(OTBR_DBUS_PROPERTY_RADIO_TX_POWER, aTxPower);
 }
 
+ClientError ThreadApiDBus::GetExternalRoutes(std::vector<ExternalRoute> &aExternalRoutes)
+{
+    return GetProperty(OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES, aExternalRoutes);
+}
+
 std::string ThreadApiDBus::GetInterfaceName(void)
 {
     return mInterfaceName;
diff --git a/src/dbus/client/thread_api_dbus.hpp b/src/dbus/client/thread_api_dbus.hpp
index 4c92fbd..b38bf52 100644
--- a/src/dbus/client/thread_api_dbus.hpp
+++ b/src/dbus/client/thread_api_dbus.hpp
@@ -210,16 +210,14 @@
     /**
      * This method adds an external route.
      *
-     * @param[in]   aPrefix         The route prefix.
-     * @param[in]   aPreference     The route preference.
-     * @param[in]   aStable         Whether or not the route is stable.
+     * @param[in]   aExternalroute  The external route config
      *
      * @retval ERROR_NONE successfully performed the dbus function call
      * @retval ERROR_DBUS dbus encode/decode error
      * @retval ...        OpenThread defined error value otherwise
      *
      */
-    ClientError AddExternalRoute(const Ip6Prefix &aPrefix, int8_t aPerference, bool aStable);
+    ClientError AddExternalRoute(const ExternalRoute &aExternalRoute);
 
     /**
      * This method removes an external route.
@@ -571,6 +569,18 @@
     ClientError GetRadioTxPower(int8_t &aTxPower);
 
     /**
+     * This method gets the external route table
+     *
+     * @param[out]  aExternalRoutes   The external route table
+     *
+     * @retval ERROR_NONE successfully performed the dbus function call
+     * @retval ERROR_DBUS dbus encode/decode error
+     * @retval ...        OpenThread defined error value otherwise
+     *
+     */
+    ClientError GetExternalRoutes(std::vector<ExternalRoute> &aExternalRoutes);
+
+    /**
      * This method returns the network interface name the client is bound to.
      *
      * @returns The network interface name.
diff --git a/src/dbus/common/constants.hpp b/src/dbus/common/constants.hpp
index 1258c75..13e6c91 100644
--- a/src/dbus/common/constants.hpp
+++ b/src/dbus/common/constants.hpp
@@ -77,6 +77,7 @@
 #define OTBR_DBUS_PROPERTY_PARTITION_ID_PROEPRTY "PartitionID"
 #define OTBR_DBUS_PROPERTY_INSTANT_RSSI "InstantRssi"
 #define OTBR_DBUS_PROPERTY_RADIO_TX_POWER "RadioTxPower"
+#define OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES "ExternalRoutes"
 
 #define OTBR_ROLE_NAME_DISABLED "disabled"
 #define OTBR_ROLE_NAME_DETACHED "detached"
diff --git a/src/dbus/common/dbus_message_helper.hpp b/src/dbus/common/dbus_message_helper.hpp
index 7ffde1e..4f2a42f 100644
--- a/src/dbus/common/dbus_message_helper.hpp
+++ b/src/dbus/common/dbus_message_helper.hpp
@@ -52,6 +52,8 @@
 otbrError DBusMessageExtract(DBusMessageIter *aIter, LinkModeConfig &aConfig);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, const Ip6Prefix &aPrefix);
 otbrError DBusMessageExtract(DBusMessageIter *aIter, Ip6Prefix &aPrefix);
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const ExternalRoute &aRoute);
+otbrError DBusMessageExtract(DBusMessageIter *aIter, ExternalRoute &aRoute);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, const OnMeshPrefix &aPrefix);
 otbrError DBusMessageExtract(DBusMessageIter *aIter, OnMeshPrefix &aPrefix);
 otbrError DBusMessageEncode(DBusMessageIter *aIter, const MacCounters &aCounters);
@@ -101,10 +103,22 @@
 
 template <> struct DBusTypeTrait<Ip6Prefix>
 {
-    // struct of { arrray of bytes, byte}
+    // struct of {array of bytes, byte}
     static constexpr const char *TYPE_AS_STRING = "(ayy)";
 };
 
+template <> struct DBusTypeTrait<ExternalRoute>
+{
+    // struct of {{array of bytes, byte}, uint16, byte, bool, bool}
+    static constexpr const char *TYPE_AS_STRING = "((ayy)qybb)";
+};
+
+template <> struct DBusTypeTrait<std::vector<ExternalRoute>>
+{
+    // array of {{array of bytes, byte}, uint16, byte, bool, bool}
+    static constexpr const char *TYPE_AS_STRING = "a((ayy)qybb)";
+};
+
 template <> struct DBusTypeTrait<LeaderData>
 {
     // struct of { uint32, byte, byte, byte, byte }
diff --git a/src/dbus/common/dbus_message_helper_openthread.cpp b/src/dbus/common/dbus_message_helper_openthread.cpp
index 8cae00f..c02830b 100644
--- a/src/dbus/common/dbus_message_helper_openthread.cpp
+++ b/src/dbus/common/dbus_message_helper_openthread.cpp
@@ -146,12 +146,11 @@
     VerifyOrExit(dbus_message_iter_open_container(aIter, DBUS_TYPE_STRUCT, nullptr, &sub), error = OTBR_ERROR_DBUS);
     VerifyOrExit(aPrefix.mPrefix.size() <= OTBR_IP6_PREFIX_SIZE, error = OTBR_ERROR_DBUS);
 
-    SuccessOrExit(DBusMessageEncode(&sub, aPrefix.mPrefix));
-    SuccessOrExit(DBusMessageEncode(&sub, aPrefix.mLength));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aPrefix.mPrefix));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aPrefix.mLength));
 
-    VerifyOrExit(dbus_message_iter_close_container(aIter, &sub));
+    VerifyOrExit(dbus_message_iter_close_container(aIter, &sub), error = OTBR_ERROR_DBUS);
 
-    error = OTBR_ERROR_NONE;
 exit:
     return error;
 }
@@ -167,6 +166,44 @@
     SuccessOrExit(error = DBusMessageExtract(&sub, aPrefix.mLength));
 
     dbus_message_iter_next(aIter);
+
+exit:
+    return error;
+}
+
+otbrError DBusMessageEncode(DBusMessageIter *aIter, const ExternalRoute &aRoute)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+
+    VerifyOrExit(dbus_message_iter_open_container(aIter, DBUS_TYPE_STRUCT, nullptr, &sub), error = OTBR_ERROR_DBUS);
+
+    SuccessOrExit(error = DBusMessageEncode(&sub, aRoute.mPrefix));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aRoute.mRloc16));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aRoute.mPreference));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aRoute.mStable));
+    SuccessOrExit(error = DBusMessageEncode(&sub, aRoute.mNextHopIsThisDevice));
+
+    VerifyOrExit(dbus_message_iter_close_container(aIter, &sub), error = OTBR_ERROR_DBUS);
+
+exit:
+    return error;
+}
+
+otbrError DBusMessageExtract(DBusMessageIter *aIter, ExternalRoute &aRoute)
+{
+    DBusMessageIter sub;
+    otbrError       error = OTBR_ERROR_NONE;
+
+    dbus_message_iter_recurse(aIter, &sub);
+    SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mPrefix));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mRloc16));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mPreference));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mStable));
+    SuccessOrExit(error = DBusMessageExtract(&sub, aRoute.mNextHopIsThisDevice));
+
+    dbus_message_iter_next(aIter);
+
 exit:
     return error;
 }
diff --git a/src/dbus/common/types.hpp b/src/dbus/common/types.hpp
index 5ca31a5..5cebae2 100644
--- a/src/dbus/common/types.hpp
+++ b/src/dbus/common/types.hpp
@@ -127,6 +127,39 @@
     bool mStable;
 };
 
+struct ExternalRoute
+{
+    /**
+     * The IPv6 prefix.
+     */
+    Ip6Prefix mPrefix;
+
+    /**
+     * The Rloc associated with the external route entry.
+     *
+     * This value is ignored when adding an external route. For any added route, the device's Rloc is used.
+     */
+    uint16_t mRloc16;
+
+    /**
+     * A 2-bit signed integer indicating router preference as defined in RFC 4191.
+     */
+    int8_t mPreference;
+
+    /**
+     * TRUE, if this configuration is considered Stable Network Data.  FALSE, otherwise.
+     */
+    bool mStable;
+
+    /**
+     * TRUE if the external route entry's next hop is this device itself (i.e., the route was added earlier by this
+     * device). FALSE otherwise.
+     *
+     * This value is ignored when adding an external route. For any added route the next hop is this device.
+     */
+    bool mNextHopIsThisDevice;
+};
+
 /**
  * This structure represents the MAC layer counters.
  *
diff --git a/src/dbus/server/dbus_thread_object.cpp b/src/dbus/server/dbus_thread_object.cpp
index 07609b3..a066a15 100644
--- a/src/dbus/server/dbus_thread_object.cpp
+++ b/src/dbus/server/dbus_thread_object.cpp
@@ -183,6 +183,8 @@
                                std::bind(&DBusThreadObject::GetInstantRssiHandler, this, _1));
     RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_RADIO_TX_POWER,
                                std::bind(&DBusThreadObject::GetRadioTxPowerHandler, this, _1));
+    RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES,
+                               std::bind(&DBusThreadObject::GetExternalRoutesHandler, this, _1));
 
     return error;
 }
@@ -373,24 +375,25 @@
 void DBusThreadObject::AddExternalRouteHandler(DBusRequest &aRequest)
 {
     auto                  threadHelper = mNcp->GetThreadHelper();
-    Ip6Prefix             routePrefix;
-    int8_t                preference;
-    bool                  stable;
-    auto                  args  = std::tie(routePrefix, preference, stable);
+    ExternalRoute         route;
+    auto                  args  = std::tie(route);
     otError               error = OT_ERROR_NONE;
-    otExternalRouteConfig route;
-    otIp6Prefix &         prefix = route.mPrefix;
+    otExternalRouteConfig otRoute;
+    otIp6Prefix &         prefix = otRoute.mPrefix;
 
     VerifyOrExit(DBusMessageToTuple(*aRequest.GetMessage(), args) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS);
 
     // size is guaranteed by parsing
-    std::copy(routePrefix.mPrefix.begin(), routePrefix.mPrefix.end(), &prefix.mPrefix.mFields.m8[0]);
-    prefix.mLength    = routePrefix.mLength;
-    route.mPreference = preference;
-    route.mStable     = stable;
+    std::copy(route.mPrefix.mPrefix.begin(), route.mPrefix.mPrefix.end(), &prefix.mPrefix.mFields.m8[0]);
+    prefix.mLength      = route.mPrefix.mLength;
+    otRoute.mPreference = route.mPreference;
+    otRoute.mStable     = route.mStable;
 
-    SuccessOrExit(error = otBorderRouterAddRoute(threadHelper->GetInstance(), &route));
-    SuccessOrExit(error = otBorderRouterRegister(threadHelper->GetInstance()));
+    SuccessOrExit(error = otBorderRouterAddRoute(threadHelper->GetInstance(), &otRoute));
+    if (route.mStable)
+    {
+        SuccessOrExit(error = otBorderRouterRegister(threadHelper->GetInstance()));
+    }
 
 exit:
     aRequest.ReplyOtResult(error);
@@ -906,5 +909,37 @@
     return error;
 }
 
+otError DBusThreadObject::GetExternalRoutesHandler(DBusMessageIter &aIter)
+{
+    auto                       threadHelper = mNcp->GetThreadHelper();
+    otError                    error        = OT_ERROR_NONE;
+    otNetworkDataIterator      iter         = OT_NETWORK_DATA_ITERATOR_INIT;
+    otExternalRouteConfig      config;
+    std::vector<ExternalRoute> externalRouteTable;
+
+    while (otNetDataGetNextRoute(threadHelper->GetInstance(), &iter, &config) == OT_ERROR_NONE)
+    {
+        ExternalRoute route;
+
+        route.mPrefix.mPrefix      = std::vector<uint8_t>(&config.mPrefix.mPrefix.mFields.m8[0],
+                                                     &config.mPrefix.mPrefix.mFields.m8[OTBR_IP6_PREFIX_SIZE]);
+        route.mPrefix.mLength      = config.mPrefix.mLength;
+        route.mRloc16              = config.mRloc16;
+        route.mPreference          = config.mPreference;
+        route.mStable              = config.mStable;
+        route.mNextHopIsThisDevice = config.mNextHopIsThisDevice;
+        externalRouteTable.push_back(route);
+    }
+
+    printf("Encode size %zu\n", externalRouteTable.size());
+
+    VerifyOrExit(DBusMessageEncodeToVariant(&aIter, externalRouteTable) == OTBR_ERROR_NONE,
+                 error = OT_ERROR_INVALID_ARGS);
+
+exit:
+    printf("error %d\n", error);
+    return error;
+}
+
 } // namespace DBus
 } // namespace otbr
diff --git a/src/dbus/server/dbus_thread_object.hpp b/src/dbus/server/dbus_thread_object.hpp
index d0dd873..1427248 100644
--- a/src/dbus/server/dbus_thread_object.hpp
+++ b/src/dbus/server/dbus_thread_object.hpp
@@ -109,6 +109,7 @@
     otError GetPartitionIDHandler(DBusMessageIter &aIter);
     otError GetInstantRssiHandler(DBusMessageIter &aIter);
     otError GetRadioTxPowerHandler(DBusMessageIter &aIter);
+    otError GetExternalRoutesHandler(DBusMessageIter &aIter);
 
     void ReplyScanResult(DBusRequest &aRequest, otError aError, const std::vector<otActiveScanResult> &aResult);
 
diff --git a/src/dbus/server/instropect.xml b/src/dbus/server/instropect.xml
index a721e30..d44a089 100644
--- a/src/dbus/server/instropect.xml
+++ b/src/dbus/server/instropect.xml
@@ -57,13 +57,17 @@
     <method name="AddExternalRoute">
       <!--
         struct {
-          uint8[] prefix_bytes
-          uint8 prefix_length
+          struct {
+            uint8[] prefix_bytes
+            uint8 prefix_length
+          }
+          uint16 rloc
+          uint8 preference
+          bool stable
+          bool next_hop_is_self
         }
       -->
-      <arg name="prefix" type="(ayy)"/>
-      <arg name="preference" type="y"/>
-      <arg name="stable" type="b"/>
+      <arg name="prefix" type="((ayy)qybb)"/>
     </method>
 
     <method name="RemoveExternalRoute">
@@ -317,6 +321,22 @@
     <property name="RadioTxPower" type="y" access="read">
       <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
     </property>
+
+    <!--
+      struct {
+        struct {
+          uint8[] prefix_bytes
+          uint8 prefix_length
+        }
+        uint16 rloc
+        uint8 preference
+        bool stable
+        bool next_hop_is_self
+      }
+    -->
+    <property name="ExternalRoutes" type="((ayy)qybb)" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
+    </property>
   </interface>
 
   <interface name="org.freedesktop.DBus.Properties">
diff --git a/tests/dbus/test_dbus_client.cpp b/tests/dbus/test_dbus_client.cpp
index 0567ffd..9c25467 100644
--- a/tests/dbus/test_dbus_client.cpp
+++ b/tests/dbus/test_dbus_client.cpp
@@ -28,6 +28,7 @@
 
 #include <assert.h>
 #include <stdio.h>
+#include <string.h>
 
 #include <memory>
 
@@ -39,7 +40,10 @@
 using otbr::DBus::ActiveScanResult;
 using otbr::DBus::ClientError;
 using otbr::DBus::DeviceRole;
+using otbr::DBus::ExternalRoute;
+using otbr::DBus::Ip6Prefix;
 using otbr::DBus::LinkModeConfig;
+using otbr::DBus::OnMeshPrefix;
 using otbr::DBus::ThreadApiDBus;
 
 struct DBusConnectionDeleter
@@ -49,6 +53,33 @@
 
 using UniqueDBusConnection = std::unique_ptr<DBusConnection, DBusConnectionDeleter>;
 
+static bool operator==(const otbr::DBus::Ip6Prefix &aLhs, const otbr::DBus::Ip6Prefix &aRhs)
+{
+    bool prefixDataEquality = (aLhs.mPrefix.size() == aRhs.mPrefix.size()) &&
+                              (memcmp(&aLhs.mPrefix[0], &aRhs.mPrefix[0], aLhs.mPrefix.size()) == 0);
+
+    return prefixDataEquality && aLhs.mLength == aRhs.mLength;
+}
+
+static void CheckExternalRoute(ThreadApiDBus *aApi, const Ip6Prefix &aPrefix)
+{
+    ExternalRoute              route;
+    std::vector<ExternalRoute> externalRouteTable;
+
+    route.mPrefix     = aPrefix;
+    route.mStable     = true;
+    route.mPreference = 0;
+
+    assert(aApi->AddExternalRoute(route) == OTBR_ERROR_NONE);
+    assert(aApi->GetExternalRoutes(externalRouteTable) == OTBR_ERROR_NONE);
+    assert(externalRouteTable.size() == 1);
+    assert(externalRouteTable[0].mPrefix == aPrefix);
+    assert(externalRouteTable[0].mPreference == 0);
+    assert(externalRouteTable[0].mStable);
+    assert(externalRouteTable[0].mNextHopIsThisDevice);
+    assert(aApi->RemoveExternalRoute(aPrefix) == OTBR_ERROR_NONE);
+}
+
 int main()
 {
     DBusError                      error;
@@ -101,8 +132,8 @@
                 std::vector<otbr::DBus::ChildInfo>    childTable;
                 std::vector<otbr::DBus::NeighborInfo> neighborTable;
                 uint32_t                              partitionId;
-                otbr::DBus::Ip6Prefix                 prefix;
-                otbr::DBus::OnMeshPrefix              onMeshPrefix;
+                Ip6Prefix                             prefix;
+                OnMeshPrefix                          onMeshPrefix;
 
                 prefix.mPrefix = {0xfd, 0xcd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
                 prefix.mLength = 64;
@@ -125,8 +156,7 @@
                 assert(api->GetPartitionId(partitionId) == OTBR_ERROR_NONE);
                 assert(api->GetInstantRssi(rssi) == OTBR_ERROR_NONE);
                 assert(api->GetRadioTxPower(txPower) == OTBR_ERROR_NONE);
-                assert(api->AddExternalRoute(prefix, 0, true) == OTBR_ERROR_NONE);
-                assert(api->RemoveExternalRoute(prefix) == OTBR_ERROR_NONE);
+                CheckExternalRoute(api.get(), prefix);
                 assert(api->AddOnMeshPrefix(onMeshPrefix) == OTBR_ERROR_NONE);
                 assert(api->RemoveOnMeshPrefix(onMeshPrefix.mPrefix) == OTBR_ERROR_NONE);
                 api->FactoryReset(nullptr);
diff --git a/tests/unit/test_dbus_message.cpp b/tests/unit/test_dbus_message.cpp
index f3db941..9b9b469 100644
--- a/tests/unit/test_dbus_message.cpp
+++ b/tests/unit/test_dbus_message.cpp
@@ -105,6 +105,20 @@
            aLhs.mIsNative == aRhs.mIsNative && aLhs.mIsJoinable == aRhs.mIsJoinable;
 }
 
+bool operator==(const otbr::DBus::Ip6Prefix &aLhs, const otbr::DBus::Ip6Prefix &aRhs)
+{
+    bool prefixDataEquality = (aLhs.mPrefix.size() == aRhs.mPrefix.size()) &&
+                              (memcmp(&aLhs.mPrefix[0], &aRhs.mPrefix[0], aLhs.mPrefix.size()) == 0);
+
+    return prefixDataEquality && aLhs.mLength == aRhs.mLength;
+}
+
+bool operator==(const otbr::DBus::ExternalRoute &aLhs, const otbr::DBus::ExternalRoute &aRhs)
+{
+    return aLhs.mPrefix == aRhs.mPrefix && aLhs.mRloc16 == aRhs.mRloc16 && aLhs.mPreference == aRhs.mPreference &&
+           aLhs.mStable == aRhs.mStable && aLhs.mNextHopIsThisDevice == aRhs.mNextHopIsThisDevice;
+}
+
 inline otbrError DBusMessageEncode(DBusMessageIter *aIter, const TestStruct &aValue)
 {
     otbrError       error = OTBR_ERROR_DBUS;
@@ -295,3 +309,21 @@
 
     dbus_message_unref(msg);
 }
+
+TEST(DBusMessage, TestOtbrExternalRoute)
+{
+    DBusMessage *                                 msg = dbus_message_new(DBUS_MESSAGE_TYPE_METHOD_RETURN);
+    tuple<std::vector<otbr::DBus::ExternalRoute>> setVals(
+        {{otbr::DBus::Ip6Prefix({{0xfa, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, 64}), uint16_t(0xfc00), 1, true,
+          true}});
+    tuple<std::vector<otbr::DBus::ExternalRoute>> getVals;
+
+    CHECK(msg != NULL);
+
+    CHECK(TupleToDBusMessage(*msg, setVals) == OTBR_ERROR_NONE);
+    CHECK(DBusMessageToTuple(*msg, getVals) == OTBR_ERROR_NONE);
+
+    CHECK(std::get<0>(setVals)[0] == std::get<0>(getVals)[0]);
+
+    dbus_message_unref(msg);
+}