[srp-client] support saving selected server info (by auto-start) (#6672)

This commit adds support for a new feature in SRP client which allows
it to save the selected server info (by the auto-start feature) in
non-volatile settings. On SRP client restart (e.g., due to a device
reset) the client will select the same server when searching to
discover and pick one from the Thread Network Data service entries.
Config `OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE` can
be used to enable/disable this feature.

The server info is saved only after the host info is successfully
registered with the server and if it is selected by auto-start from a
network data SRP *unicast* service entry.

This commit also adds `test_srp_client_save_server_info.py` test-case
which verifies the behavior of the new feature.
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index dbf1fd4..c782221 100644
--- a/include/openthread/instance.h
+++ b/include/openthread/instance.h
@@ -53,7 +53,7 @@
  * @note This number versions both OpenThread platform and user APIs.
  *
  */
-#define OPENTHREAD_API_VERSION (118)
+#define OPENTHREAD_API_VERSION (119)
 
 /**
  * @addtogroup api-instance
diff --git a/include/openthread/platform/settings.h b/include/openthread/platform/settings.h
index cb51993..56a0623 100644
--- a/include/openthread/platform/settings.h
+++ b/include/openthread/platform/settings.h
@@ -71,6 +71,7 @@
     OT_SETTINGS_KEY_OMR_PREFIX           = 0x0009, ///< Off-mesh routable (OMR) prefix.
     OT_SETTINGS_KEY_ON_LINK_PREFIX       = 0x000a, ///< On-link prefix for infrastructure link.
     OT_SETTINGS_KEY_SRP_ECDSA_KEY        = 0x000b, ///< SRP client ECDSA public/private key pair.
+    OT_SETTINGS_KEY_SRP_CLIENT_INFO      = 0x000c, ///< The SRP client info (selected SRP server address).
 };
 
 /**
diff --git a/src/core/common/settings.cpp b/src/core/common/settings.cpp
index 77f64ef..4a61374 100644
--- a/src/core/common/settings.cpp
+++ b/src/core/common/settings.cpp
@@ -91,6 +91,14 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+void SettingsBase::LogSrpClientInfo(const char *aAction, const SrpClientInfo &aSrpClientInfo) const
+{
+    otLogInfoCore("Non-volatile: %s SrpClientInfo {Server:[%s]:%u}", aAction,
+                  aSrpClientInfo.GetServerAddress().ToString().AsCString(), aSrpClientInfo.GetServerPort());
+}
+#endif
+
 #endif // #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO)
 
 #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_WARN)
@@ -571,6 +579,55 @@
     return error;
 }
 
+#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+
+Error Settings::SaveSrpClientInfo(const SrpClientInfo &aSrpClientInfo)
+{
+    Error         error = kErrorNone;
+    SrpClientInfo prevInfo;
+    uint16_t      length = sizeof(SrpClientInfo);
+
+    if ((Read(kKeySrpClientInfo, &prevInfo, length) == kErrorNone) && (length == sizeof(SrpClientInfo)) &&
+        (prevInfo == aSrpClientInfo))
+    {
+        LogSrpClientInfo("Re-saved", aSrpClientInfo);
+        ExitNow();
+    }
+
+    SuccessOrExit(error = Save(kKeySrpClientInfo, &aSrpClientInfo, sizeof(SrpClientInfo)));
+    LogSrpClientInfo("Saved", aSrpClientInfo);
+
+exit:
+    LogFailure(error, "saving SrpClientInfo", /* aIsDelete */ false);
+    return error;
+}
+
+Error Settings::ReadSrpClientInfo(SrpClientInfo &aSrpClientInfo) const
+{
+    Error    error;
+    uint16_t length = sizeof(SrpClientInfo);
+
+    aSrpClientInfo.Init();
+    SuccessOrExit(error = Read(kKeySrpClientInfo, &aSrpClientInfo, length));
+    LogSrpClientInfo("Read", aSrpClientInfo);
+
+exit:
+    return error;
+}
+
+Error Settings::DeleteSrpClientInfo(void)
+{
+    Error error;
+
+    SuccessOrExit(error = Delete(kKeySrpClientInfo));
+    otLogInfoCore("Non-volatile: Deleted SrpClientInfo");
+
+exit:
+    LogFailure(error, "deleting SrpClientInfo", true);
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
 
 Error Settings::Read(Key aKey, void *aBuffer, uint16_t &aSize) const
diff --git a/src/core/common/settings.hpp b/src/core/common/settings.hpp
index e9b098c..2e0e1f5 100644
--- a/src/core/common/settings.hpp
+++ b/src/core/common/settings.hpp
@@ -590,6 +590,57 @@
     } OT_TOOL_PACKED_END;
 
     /**
+     * This structure represents the SRP client info (selected server address).
+     *
+     */
+    OT_TOOL_PACKED_BEGIN
+    class SrpClientInfo : public Equatable<SrpClientInfo>, private Clearable<SrpClientInfo>
+    {
+    public:
+        /**
+         * This method initializes the `SrpClientInfo` object.
+         *
+         */
+        void Init(void) { Clear(); }
+
+        /**
+         * This method returns the server IPv6 address.
+         *
+         * @returns The server IPv6 address.
+         *
+         */
+        const Ip6::Address &GetServerAddress(void) const { return mServerAddress; }
+
+        /**
+         * This method sets the server IPv6 address.
+         *
+         * @param[in] aAddress  The server IPv6 address.
+         *
+         */
+        void SetServerAddress(const Ip6::Address &aAddress) { mServerAddress = aAddress; }
+
+        /**
+         * This method returns the server port number.
+         *
+         * @returns The server port number.
+         *
+         */
+        uint16_t GetServerPort(void) const { return Encoding::LittleEndian::HostSwap16(mServerPort); }
+
+        /**
+         * This method sets the server port number.
+         *
+         * @param[in] aPort  The server port number.
+         *
+         */
+        void SetServerPort(uint16_t aPort) { mServerPort = Encoding::LittleEndian::HostSwap16(aPort); }
+
+    private:
+        Ip6::Address mServerAddress;
+        uint16_t     mServerPort; // (in little-endian encoding)
+    } OT_TOOL_PACKED_END;
+
+    /**
      * This enumeration defines the keys of settings.
      *
      */
@@ -606,6 +657,7 @@
         kKeyOmrPrefix         = OT_SETTINGS_KEY_OMR_PREFIX,
         kKeyOnLinkPrefix      = OT_SETTINGS_KEY_ON_LINK_PREFIX,
         kKeySrpEcdsaKey       = OT_SETTINGS_KEY_SRP_ECDSA_KEY,
+        kKeySrpClientInfo     = OT_SETTINGS_KEY_SRP_CLIENT_INFO,
     };
 
 protected:
@@ -624,6 +676,7 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     void LogPrefix(const char *aAction, const char *aPrefixName, const Ip6::Prefix &aOmrPrefix) const;
 #endif
+    void LogSrpClientInfo(const char *aAction, const SrpClientInfo &aSrpClientInfo) const;
 #else // (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO) && (OPENTHREAD_CONFIG_LOG_UTIL != 0)
     void LogNetworkInfo(const char *, const NetworkInfo &) const {}
     void LogParentInfo(const char *, const ParentInfo &) const {}
@@ -634,6 +687,7 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     void LogPrefix(const char *, const char *, const Ip6::Prefix &) const {}
 #endif
+    void LogSrpClientInfo(const char *, const SrpClientInfo &) const {}
 #endif // (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO) && (OPENTHREAD_CONFIG_LOG_UTIL != 0)
 
 #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_WARN) && (OPENTHREAD_CONFIG_LOG_UTIL != 0)
@@ -1092,6 +1146,41 @@
      *
      */
     Error DeleteSrpKey(void);
+
+#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+    /**
+     * This method saves SRP client info.
+     *
+     * @param[in] aSrpClientInfo      The `SrpClientInfo` to save.
+     *
+     * @retval kErrorNone             Successfully saved the information in settings.
+     * @retval kErrorNotImplemented   The platform does not implement settings functionality.
+     *
+     */
+    Error SaveSrpClientInfo(const SrpClientInfo &aSrpClientInfo);
+
+    /**
+     * This method reads SRP client info.
+     *
+     * @param[out] aSrpClientInfo     A reference to a `SrpClientInfo` to output the read content.
+     *
+     * @retval kErrorNone             Successfully read the information.
+     * @retval kErrorNotFound         No corresponding value in the setting store.
+     * @retval kErrorNotImplemented   The platform does not implement settings functionality.
+     *
+     */
+    Error ReadSrpClientInfo(SrpClientInfo &aSrpClientInfo) const;
+
+    /**
+     * This method deletes SRP client info from settings.
+     *
+     * @retval kErrorNone            Successfully deleted the value.
+     * @retval kErrorNotImplemented  The platform does not implement settings functionality.
+     *
+     */
+    Error DeleteSrpClientInfo(void);
+
+#endif // OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
 
 private:
diff --git a/src/core/config/srp_client.h b/src/core/config/srp_client.h
index 21123a1..43fe02d 100644
--- a/src/core/config/srp_client.h
+++ b/src/core/config/srp_client.h
@@ -51,7 +51,7 @@
  * Define to 1 to enable SRP Client auto-start feature and its APIs.
  *
  * When enabled, the SRP client can be configured to automatically start when it detects the presence of an SRP server
- *  (by monitoring the Thread Network Data for SRP Server Service entries).
+ * (by monitoring the Thread Network Data for SRP Server Service entries).
  *
  */
 #ifndef OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
@@ -81,6 +81,22 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+ *
+ * Define to 1 to enable SRP client feature to save the selected server in non-volatile settings.
+ *
+ * When enabled, the SRP client will save the selected server info by auto-start feature in the non-volatile settings
+ * and on a client restart (e.g., due to a device reset) it will select the same server when searching to discover and
+ * pick one from the Thread Network Data service entries. The server info is saved only after the host info is
+ * successfully registered with the server and if it is selected by auto-start from a network data SRP *unicast*
+ * service entry.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+#define OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE 1
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE
  *
  * Specifies the default requested lease interval (in seconds). Set to two hours.
diff --git a/src/core/net/srp_client.cpp b/src/core/net/srp_client.cpp
index d4874a0..65f7abc 100644
--- a/src/core/net/srp_client.cpp
+++ b/src/core/net/srp_client.cpp
@@ -542,12 +542,36 @@
 
 void Client::ChangeHostAndServiceStates(const ItemState *aNewStates)
 {
+#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+    ItemState oldHostState = mHostInfo.GetState();
+#endif
+
     mHostInfo.SetState(aNewStates[mHostInfo.GetState()]);
 
     for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext())
     {
         service->SetState(aNewStates[service->GetState()]);
     }
+
+#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+    if (mAutoStartModeEnabled && mAutoStartDidSelectServer && (oldHostState != kRegistered) &&
+        (mHostInfo.GetState() == kRegistered))
+    {
+        if (mAutoStartIsUsingAnycastAddress)
+        {
+            IgnoreError(Get<Settings>().DeleteSrpClientInfo());
+        }
+        else
+        {
+            Settings::SrpClientInfo info;
+
+            info.SetServerAddress(GetServerAddress().GetAddress());
+            info.SetServerPort(GetServerAddress().GetPort());
+
+            IgnoreError(Get<Settings>().SaveSrpClientInfo(info));
+        }
+    }
+#endif
 }
 
 void Client::InvokeCallback(Error aError) const
@@ -1515,6 +1539,10 @@
     Ip6::SockAddr                             serverSockAddr;
     bool                                      serverIsAnycast = false;
     NetworkData::Service::DnsSrpAnycast::Info anycastInfo;
+#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+    Settings::SrpClientInfo savedInfo;
+    bool                    hasSavedServerInfo = false;
+#endif
 
     VerifyOrExit(mAutoStartModeEnabled);
 
@@ -1535,6 +1563,13 @@
 
     // Now `IsRunning()` implies `mAutoStartDidSelectServer`.
 
+#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+    if (!IsRunning())
+    {
+        hasSavedServerInfo = (Get<Settings>().ReadSrpClientInfo(savedInfo) == kErrorNone);
+    }
+#endif
+
     if (Get<NetworkData::Service::Manager>().FindPreferredDnsSrpAnycastInfo(anycastInfo) == kErrorNone)
     {
         if (IsRunning() && mAutoStartIsUsingAnycastAddress && (mServerSequenceNumber == anycastInfo.mSequenceNumber) &&
@@ -1564,6 +1599,19 @@
                 ExitNow();
             }
 
+#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
+            if (hasSavedServerInfo && (unicastInfo.mSockAddr.GetAddress() == savedInfo.GetServerAddress()) &&
+                (unicastInfo.mSockAddr.GetPort() == savedInfo.GetServerPort()))
+            {
+                // Stop the search if we see a match for the previously
+                // saved server info in the network data entries.
+
+                serverSockAddr  = unicastInfo.mSockAddr;
+                serverIsAnycast = false;
+                break;
+            }
+#endif
+
             numServers++;
 
             // Choose a server randomly (with uniform distribution) from
diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am
index fa9de8e..85f8faf 100644
--- a/tests/scripts/thread-cert/Makefile.am
+++ b/tests/scripts/thread-cert/Makefile.am
@@ -176,6 +176,7 @@
     test_router_reattach.py                                          \
     test_service.py                                                  \
     test_srp_auto_start_mode.py                                      \
+    test_srp_client_save_server_info.py                              \
     test_srp_lease.py                                                \
     test_srp_name_conflicts.py                                       \
     test_srp_register_single_service.py                              \
@@ -233,6 +234,7 @@
     test_router_reattach.py                                          \
     test_service.py                                                  \
     test_srp_auto_start_mode.py                                      \
+    test_srp_client_save_server_info.py                              \
     test_srp_lease.py                                                \
     test_srp_name_conflicts.py                                       \
     test_srp_register_single_service.py                              \
diff --git a/tests/scripts/thread-cert/test_srp_client_save_server_info.py b/tests/scripts/thread-cert/test_srp_client_save_server_info.py
new file mode 100755
index 0000000..f555966
--- /dev/null
+++ b/tests/scripts/thread-cert/test_srp_client_save_server_info.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2021, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+#
+
+import ipaddress
+import unittest
+
+import command
+import thread_cert
+
+# Test description:
+#   This test verifies SRP client behavior to save the selected server address info (unicast) by
+#   auto-start feature.
+#
+# Topology:
+#
+#   4 node (SRP client as leader, 3 SRP servers).
+#
+
+CLIENT = 1
+SERVER1 = 2
+SERVER2 = 3
+SERVER3 = 4
+
+WAIT_TIME = 5
+MAX_ITER = 5
+
+
+class SrpAutoStartMode(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    SUPPORT_NCP = False
+
+    TOPOLOGY = {
+        CLIENT: {
+            'name': 'SRP_CLIENT',
+            'masterkey': '00112233445566778899aabbccddeeff',
+            'mode': 'rdn',
+        },
+        SERVER1: {
+            'name': 'SRP_SERVER1',
+            'masterkey': '00112233445566778899aabbccddeeff',
+            'mode': 'rn',
+        },
+        SERVER2: {
+            'name': 'SRP_SERVER2',
+            'masterkey': '00112233445566778899aabbccddeeff',
+            'mode': 'rn',
+        },
+        SERVER3: {
+            'name': 'SRP_SERVER3',
+            'masterkey': '00112233445566778899aabbccddeeff',
+            'mode': 'rn',
+        },
+    }
+
+    def test(self):
+        client = self.nodes[CLIENT]
+        server1 = self.nodes[SERVER1]
+        server2 = self.nodes[SERVER2]
+        server3 = self.nodes[SERVER3]
+
+        # Start the server & client devices.
+
+        client.start()
+        self.simulator.go(WAIT_TIME)
+        self.assertEqual(client.get_state(), 'leader')
+
+        for node in [server1, server2, server3]:
+            node.start()
+            self.simulator.go(WAIT_TIME)
+            self.assertEqual(node.get_state(), 'child')
+
+        # Enable auto start mode on client and check that server1 is used.
+
+        server1.srp_server_set_enabled(True)
+
+        client.srp_client_set_host_name('host')
+        client.srp_client_set_host_address('2001::1')
+        client.srp_client_add_service('my-service', '_ipps._tcp', 12345)
+
+        self.assertEqual(client.srp_client_get_state(), 'Disabled')
+        client.srp_client_enable_auto_start_mode()
+        self.assertEqual(client.srp_client_get_auto_start_mode(), 'Enabled')
+        self.simulator.go(WAIT_TIME)
+
+        self.assertEqual(client.srp_client_get_state(), 'Enabled')
+        self.assertTrue(server1.has_ipaddr(client.srp_client_get_server_address()))
+        self.assertEqual(client.srp_client_get_host_state(), 'Registered')
+
+        # Enable server2 and server3 and check that server1 is still used.
+
+        server2.srp_server_set_enabled(True)
+        server3.srp_server_set_enabled(True)
+        self.simulator.go(WAIT_TIME)
+        self.assertTrue(server1.has_ipaddr(client.srp_client_get_server_address()))
+
+        # Stop and restart the client (multiple times) and verify that
+        # server1 is always picked.
+
+        for iter in range(0, MAX_ITER):
+            client.srp_client_stop()
+            client.srp_client_enable_auto_start_mode()
+            self.simulator.go(WAIT_TIME)
+            self.assertTrue(server1.has_ipaddr(client.srp_client_get_server_address()))
+            self.assertEqual(client.srp_client_get_host_state(), 'Registered')
+
+        # Stop the client, then stop server1 and restart client and
+        # verify that server1 is no longer used.
+
+        client.srp_client_stop()
+        server1.srp_server_set_enabled(False)
+        self.simulator.go(WAIT_TIME)
+
+        client.srp_client_enable_auto_start_mode()
+        self.simulator.go(WAIT_TIME)
+        server_address = client.srp_client_get_server_address()
+        self.assertFalse(server1.has_ipaddr(server_address))
+        self.assertTrue(server2.has_ipaddr(server_address) or server3.has_ipaddr(server_address))
+        self.assertEqual(client.srp_client_get_host_state(), 'Registered')
+
+        # Start back server1, then restart client and verify that now we remain
+        # with the new saved server info.
+
+        server1.srp_server_set_enabled(True)
+        self.simulator.go(WAIT_TIME)
+
+        for iter in range(0, MAX_ITER):
+            client.srp_client_stop()
+            self.simulator.go(WAIT_TIME)
+            client.srp_client_enable_auto_start_mode()
+            self.simulator.go(WAIT_TIME)
+            self.assertEqual(client.srp_client_get_server_address(), server_address)
+            self.assertEqual(client.srp_client_get_host_state(), 'Registered')
+
+
+if __name__ == '__main__':
+    unittest.main()