[simulation] allow specify local host (#9925)

This commit adds a argument `-L`/`--local-host` to simulation platform
to specify the source IP address for packets simulating 15.4
frames. This allows the simulation packets being transmitted over
different network interfaces, so that the simulation can run on
different hosts.

This can be used to enable multiple emulation devices(e.g. Android
Virtual Device) communicating to each other over emulated Thread
radio.

The argument accepts either an IPv4 address or a network interface
name. In the latter case, the first found IPv4 address on that
interface will be used will be used.
diff --git a/examples/platforms/simulation/simul_utils.c b/examples/platforms/simulation/simul_utils.c
index 7b55406..36b69af 100644
--- a/examples/platforms/simulation/simul_utils.c
+++ b/examples/platforms/simulation/simul_utils.c
@@ -36,6 +36,8 @@
 #define UTILS_SOCKET_LOCAL_HOST_ADDR "127.0.0.1"
 #define UTILS_SOCKET_GROUP_ADDR "224.0.0.116"
 
+const char *gLocalHost = UTILS_SOCKET_LOCAL_HOST_ADDR;
+
 void utilsAddFdToFdSet(int aFd, fd_set *aFdSet, int *aMaxFd)
 {
     otEXPECT(aFd >= 0);
@@ -75,7 +77,7 @@
     memset(&sockaddr, 0, sizeof(sockaddr));
     sockaddr.sin_family      = AF_INET;
     sockaddr.sin_port        = htons(aSocket->mPort);
-    sockaddr.sin_addr.s_addr = inet_addr(UTILS_SOCKET_LOCAL_HOST_ADDR);
+    sockaddr.sin_addr.s_addr = inet_addr(gLocalHost);
 
     rval = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &sockaddr.sin_addr, sizeof(sockaddr.sin_addr));
     otEXPECT_ACTION(rval != -1, perror("setsockopt(TxFd, IP_MULTICAST_IF)"));
@@ -103,7 +105,7 @@
     memset(&mreq, 0, sizeof(mreq));
     inet_pton(AF_INET, UTILS_SOCKET_GROUP_ADDR, &mreq.imr_multiaddr);
 
-    mreq.imr_address.s_addr = inet_addr(UTILS_SOCKET_LOCAL_HOST_ADDR);
+    mreq.imr_address.s_addr = inet_addr(gLocalHost);
 
     rval = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreq.imr_address, sizeof(mreq.imr_address));
     otEXPECT_ACTION(rval != -1, perror("setsockopt(RxFd, IP_MULTICAST_IF)"));
diff --git a/examples/platforms/simulation/simul_utils.h b/examples/platforms/simulation/simul_utils.h
index 39496d5..0d70f2d 100644
--- a/examples/platforms/simulation/simul_utils.h
+++ b/examples/platforms/simulation/simul_utils.h
@@ -46,6 +46,8 @@
     uint16_t mPort;        ///< The port number used by this node
 } utilsSocket;
 
+extern const char *gLocalHost; ///< Local host address to use for sockets
+
 /**
  * Adds a file descriptor (FD) to a given FD set.
  *
diff --git a/examples/platforms/simulation/system.c b/examples/platforms/simulation/system.c
index bb648b5..07af589 100644
--- a/examples/platforms/simulation/system.c
+++ b/examples/platforms/simulation/system.c
@@ -36,19 +36,27 @@
 
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
 
+#include <arpa/inet.h>
 #include <assert.h>
 #include <errno.h>
 #include <getopt.h>
+#include <ifaddrs.h>
 #include <libgen.h>
+#include <netinet/in.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
 
 #include <openthread/tasklet.h>
 #include <openthread/platform/alarm-milli.h>
 #include <openthread/platform/radio.h>
 
+#include "simul_utils.h"
+#include "utils/code_utils.h"
+
 uint32_t gNodeId = 1;
 
 extern bool        gPlatformPseudoResetWasRequested;
@@ -71,6 +79,7 @@
 {
     OT_SIM_OPT_HELP               = 'h',
     OT_SIM_OPT_ENABLE_ENERGY_SCAN = 'E',
+    OT_SIM_OPT_LOCAL_HOST         = 'L',
     OT_SIM_OPT_SLEEP_TO_TX        = 't',
     OT_SIM_OPT_TIME_SPEED         = 's',
     OT_SIM_OPT_LOG_FILE           = 'l',
@@ -96,6 +105,56 @@
     exit(aExitCode);
 }
 
+static const char *GetLocalHostAddress(const char *aLocalHost)
+{
+    struct ifaddrs *ifaddr;
+    static char     ipstr[INET_ADDRSTRLEN] = {0};
+    const char     *rval                   = NULL;
+
+    {
+        struct in_addr addr;
+
+        otEXPECT_ACTION(inet_aton(aLocalHost, &addr) == 0, rval = aLocalHost);
+    }
+
+    if (getifaddrs(&ifaddr) == -1)
+    {
+        perror("getifaddrs");
+        exit(EXIT_FAILURE);
+    }
+
+    for (struct ifaddrs *ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
+    {
+        if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET)
+        {
+            continue;
+        }
+
+        if (strcmp(ifa->ifa_name, aLocalHost) == 0)
+        {
+            struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr;
+
+            if (inet_ntop(AF_INET, &addr->sin_addr, ipstr, sizeof(ipstr)))
+            {
+                break;
+            }
+        }
+    }
+
+    freeifaddrs(ifaddr);
+
+    if (ipstr[0] == '\0')
+    {
+        fprintf(stderr, "Local host address not found!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    rval = ipstr;
+
+exit:
+    return rval;
+}
+
 void otSysInit(int aArgCount, char *aArgVector[])
 {
     char    *endptr;
@@ -106,6 +165,7 @@
         {"enable-energy-scan", no_argument, 0, OT_SIM_OPT_ENABLE_ENERGY_SCAN},
         {"sleep-to-tx", no_argument, 0, OT_SIM_OPT_SLEEP_TO_TX},
         {"time-speed", required_argument, 0, OT_SIM_OPT_TIME_SPEED},
+        {"local-host", required_argument, 0, OT_SIM_OPT_LOCAL_HOST},
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
         {"log-file", required_argument, 0, OT_SIM_OPT_LOG_FILE},
 #endif
@@ -113,9 +173,9 @@
     };
 
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
-    static const char options[] = "Ehts:l:";
+    static const char options[] = "Ehts:L:l:";
 #else
-    static const char options[] = "Ehts:";
+    static const char options[] = "Ehts:L:";
 #endif
 
     if (gPlatformPseudoResetWasRequested)
@@ -149,6 +209,10 @@
         case OT_SIM_OPT_SLEEP_TO_TX:
             gRadioCaps |= OT_RADIO_CAPS_SLEEP_TO_TX;
             break;
+        case OT_SIM_OPT_LOCAL_HOST:
+            gLocalHost = GetLocalHostAddress(optarg);
+            fprintf(stderr, "Simulate on %s\n", gLocalHost);
+            break;
         case OT_SIM_OPT_TIME_SPEED:
             speedUpFactor = (uint32_t)strtol(optarg, &endptr, 10);
             if (*endptr != '\0' || speedUpFactor == 0)
diff --git a/tests/scripts/expect/posix-rcp-local-host.exp b/tests/scripts/expect/posix-rcp-local-host.exp
new file mode 100755
index 0000000..e8c8201
--- /dev/null
+++ b/tests/scripts/expect/posix-rcp-local-host.exp
@@ -0,0 +1,44 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2024, 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.
+#
+
+source "tests/scripts/expect/_common.exp"
+
+spawn_node 1 rcp "spinel+hdlc+uart://$::env(OT_SIMULATION_APPS)/ncp/ot-rcp?forkpty-arg=-Llo&forkpty-arg=1"
+send "factoryreset\n"
+wait_for "state" "disabled"
+setup_default_network
+attach
+
+spawn_node 2 rcp "spinel+hdlc+uart://$::env(OT_SIMULATION_APPS)/ncp/ot-rcp?forkpty-arg=--local-host=127.0.0.1&forkpty-arg=2"
+send "factoryreset\n"
+wait_for "state" "disabled"
+setup_default_network
+attach child
+
+dispose_all