[netstack] Support sending `IPV6_PKTINFO` cmsg

Fixed: 102222
Change-Id: I9f6c303c06b10affde5adc11102a044e9d51417e
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/691224
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Fuchsia-Auto-Submit: Ghanan Gowripalan <ghanan@google.com>
Reviewed-by: Nick Brown <nickbrow@google.com>
Reviewed-by: Tamir Duberstein <tamird@google.com>
Reviewed-by: Bruno Dal Bo <brunodalbo@google.com>
API-Review: Bruno Dal Bo <brunodalbo@google.com>
diff --git a/sdk/fidl/fuchsia.posix.socket/socket.fidl b/sdk/fidl/fuchsia.posix.socket/socket.fidl
index 8d134fb..00e4e56 100644
--- a/sdk/fidl/fuchsia.posix.socket/socket.fidl
+++ b/sdk/fidl/fuchsia.posix.socket/socket.fidl
@@ -533,6 +533,25 @@
         /// The Hop Limit value to set in the IPv6 header of an outgoing
         /// packet.
         2: hoplimit uint8;
+
+        /// Information controlling the local interface and/or address used when
+        /// sending an IPv6 packet.
+        //
+        // This is a structure instead of a table as it is meant to match
+        // `in6_pktinfo` which is not expected to grow.
+        3: pktinfo @generated_name("Ipv6PktInfoSendControlData") struct {
+            /// The interface index from which the IPv6 packet should be sent.
+            ///
+            /// 0 indicates that the local interface is unspecified and the
+            /// stack may choose an appropriate interface.
+            iface fuchsia.net.interface_id;
+            /// The source address from which the IPv6 packet should be sent.
+            ///
+            /// All zeroes indicates that the local address is unspecified and
+            /// the stack may choose an appropriate address (i.e. the local
+            /// address to which the socket is bound).
+            local_addr fuchsia.net.Ipv6Address;
+        };
     };
 };
 
diff --git a/sdk/lib/fdio/socket.cc b/sdk/lib/fdio/socket.cc
index ce94bac..ae660a4 100644
--- a/sdk/lib/fdio/socket.cc
+++ b/sdk/lib/fdio/socket.cc
@@ -295,8 +295,9 @@
   }
 }
 
-int16_t ParseIpv6LevelControlMessage(fsocket::wire::Ipv6SendControlData& fidl_ipv6, int type,
-                                     const void* data, socklen_t data_len) {
+int16_t ParseIpv6LevelControlMessage(fsocket::wire::Ipv6SendControlData& fidl_ipv6,
+                                     fidl::AnyArena& allocator, int type, const void* data,
+                                     socklen_t data_len) {
   switch (type) {
     case IPV6_HOPLIMIT: {
       int hoplimit;
@@ -315,6 +316,21 @@
       }
       return 0;
     }
+    case IPV6_PKTINFO: {
+      in6_pktinfo pktinfo;
+      if (data_len != sizeof(pktinfo)) {
+        return EINVAL;
+      }
+      memcpy(&pktinfo, data, sizeof(pktinfo));
+      fsocket::wire::Ipv6PktInfoSendControlData fidl_pktinfo{
+          .iface = static_cast<uint64_t>(pktinfo.ipi6_ifindex),
+      };
+      static_assert(sizeof(pktinfo.ipi6_addr) == sizeof(fidl_pktinfo.local_addr.addr),
+                    "mismatch between size of FIDL and in6_pktinfo IPv6 addresses");
+      memcpy(fidl_pktinfo.local_addr.addr.data(), &pktinfo.ipi6_addr, sizeof(pktinfo.ipi6_addr));
+      fidl_ipv6.set_pktinfo(allocator, fidl_pktinfo);
+      return 0;
+    }
     default:
       // TODO(https://fxbug.dev/88984): Validate unsupported SOL_IPV6 control messages.
       return 0;
@@ -350,7 +366,7 @@
       if (!fidl_net.has_ipv6()) {
         fidl_net.set_ipv6(allocator, fsocket::wire::Ipv6SendControlData(allocator));
       }
-      return ParseIpv6LevelControlMessage(fidl_net.ipv6(), type, data, data_len);
+      return ParseIpv6LevelControlMessage(fidl_net.ipv6(), allocator, type, data, data_len);
     default:
       return 0;
   }
diff --git a/src/connectivity/network/netstack/fuchsia_posix_socket.go b/src/connectivity/network/netstack/fuchsia_posix_socket.go
index 8a722fc..5e5bbba 100644
--- a/src/connectivity/network/netstack/fuchsia_posix_socket.go
+++ b/src/connectivity/network/netstack/fuchsia_posix_socket.go
@@ -1943,10 +1943,18 @@
 	if in.HasIpv6() {
 		inIpv6 := in.GetIpv6()
 		if inIpv6.HasHoplimit() {
-			hoplimit := inIpv6.GetHoplimit()
-			out.HopLimit = hoplimit
+			out.HopLimit = inIpv6.GetHoplimit()
 			out.HasHopLimit = true
 		}
+
+		if inIpv6.HasPktinfo() {
+			pktInfo := inIpv6.GetPktinfo()
+			out.IPv6PacketInfo = tcpip.IPv6PacketInfo{
+				NIC:  tcpip.NICID(pktInfo.Iface),
+				Addr: toTcpIpAddressDroppingUnspecifiedv6(pktInfo.LocalAddr),
+			}
+			out.HasIPv6PacketInfo = true
+		}
 	}
 
 	return 0
diff --git a/src/connectivity/network/tests/multi_nic/BUILD.gn b/src/connectivity/network/tests/multi_nic/BUILD.gn
index 55139c9..7112cb0 100644
--- a/src/connectivity/network/tests/multi_nic/BUILD.gn
+++ b/src/connectivity/network/tests/multi_nic/BUILD.gn
@@ -7,7 +7,10 @@
 
 test("bin") {
   output_name = "multi_nic_test_client"
-  sources = [ "main.cc" ]
+  sources = [
+    "main.cc",
+    "socket_test.cc",
+  ]
 
   deps = [
     "//src/connectivity/network/testing/netemul/sync-manager/fidl:sync_llcpp",
diff --git a/src/connectivity/network/tests/multi_nic/constants.h b/src/connectivity/network/tests/multi_nic/constants.h
new file mode 100644
index 0000000..eeeb1a7
--- /dev/null
+++ b/src/connectivity/network/tests/multi_nic/constants.h
@@ -0,0 +1,23 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_CONNECTIVITY_NETWORK_TESTS_MULTI_NIC_CONSTANTS_H_
+#define SRC_CONNECTIVITY_NETWORK_TESTS_MULTI_NIC_CONSTANTS_H_
+
+#include <stdint.h>
+
+constexpr char kClientNic1Name[] = "client-ep-1";
+constexpr char kClientNic2Name[] = "client-ep-2";
+
+constexpr char kClientIpv4Addr1[] = "192.168.0.1";
+constexpr char kClientIpv4Addr2[] = "192.168.0.2";
+constexpr char kServerIpv4Addr[] = "192.168.0.254";
+
+constexpr char kClientIpv6Addr1[] = "a::1";
+constexpr char kClientIpv6Addr2[] = "a::2";
+constexpr char kServerIpv6Addr[] = "a::ffff";
+
+constexpr uint16_t kServerPort = 1234;
+
+#endif  // SRC_CONNECTIVITY_NETWORK_TESTS_MULTI_NIC_CONSTANTS_H_
diff --git a/src/connectivity/network/tests/multi_nic/main.cc b/src/connectivity/network/tests/multi_nic/main.cc
index d880e0d..b5a27a3 100644
--- a/src/connectivity/network/tests/multi_nic/main.cc
+++ b/src/connectivity/network/tests/multi_nic/main.cc
@@ -12,20 +12,14 @@
 #include <fbl/unique_fd.h>
 #include <gtest/gtest.h>
 
+#include "constants.h"
+
 namespace {
 
 constexpr char kBusName[] = "test-bus";
 constexpr char kTestClientName[] = "client";
 constexpr char kTestServerName[] = "server";
 
-constexpr char kClientIpv4Addr1[] = "192.168.0.1";
-constexpr char kClientIpv4Addr2[] = "192.168.0.2";
-constexpr char kServerIpv4Addr[] = "192.168.0.254";
-constexpr char kClientIpv6Addr1[] = "a::1";
-constexpr char kClientIpv6Addr2[] = "a::2";
-constexpr char kServerIpv6Addr[] = "a::ffff";
-constexpr uint16_t kServerPort = 1234;
-
 void TestUdpPing(int domain, const sockaddr* bind_addr, socklen_t bind_addr_len,
                  const sockaddr* connect_addr, socklen_t connect_addr_len) {
   fbl::unique_fd s;
diff --git a/src/connectivity/network/tests/multi_nic/socket_test.cc b/src/connectivity/network/tests/multi_nic/socket_test.cc
new file mode 100644
index 0000000..458bbf4
--- /dev/null
+++ b/src/connectivity/network/tests/multi_nic/socket_test.cc
@@ -0,0 +1,259 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include <algorithm>
+
+#include <fbl/unique_fd.h>
+#include <gtest/gtest.h>
+
+#include "constants.h"
+
+namespace {
+
+struct SendIpv6PacketInfoSuccessTestCase {
+  std::string test_name;
+  std::optional<std::string> send_local_addr_str;
+  std::optional<std::string> send_local_if_str;
+  std::string expected_recv_addr_str;
+};
+
+class SendIpv6PacketInfoSuccessTest
+    : public testing::TestWithParam<SendIpv6PacketInfoSuccessTestCase> {};
+
+TEST_P(SendIpv6PacketInfoSuccessTest, SendAndRecv) {
+  fbl::unique_fd s;
+  ASSERT_TRUE(s = fbl::unique_fd(socket(AF_INET6, SOCK_DGRAM, 0))) << strerror(errno);
+
+  constexpr int kOne = 1;
+  ASSERT_EQ(setsockopt(s.get(), SOL_IPV6, IPV6_RECVPKTINFO, &kOne, sizeof(kOne)), 0)
+      << strerror(errno);
+
+  const sockaddr_in6 bind_addr{
+      .sin6_family = AF_INET6,
+  };
+  ASSERT_EQ(bind(s.get(), reinterpret_cast<const sockaddr*>(&bind_addr), sizeof(bind_addr)), 0)
+      << strerror(errno);
+
+  sockaddr_in6 to_addr = {
+      .sin6_family = AF_INET6,
+      .sin6_port = htons(kServerPort),
+  };
+  ASSERT_EQ(inet_pton(to_addr.sin6_family, kServerIpv6Addr, &to_addr.sin6_addr), 1);
+
+  const auto [test_name, send_local_addr_str, send_local_if_str, expected_recv_addr_str] =
+      GetParam();
+  in6_addr expected_recv_addr;
+  ASSERT_EQ(inet_pton(AF_INET6, expected_recv_addr_str.c_str(), &expected_recv_addr), 1);
+  in6_pktinfo send_pktinfo = {};
+  if (send_local_addr_str.has_value()) {
+    ASSERT_EQ(inet_pton(AF_INET6, send_local_addr_str->c_str(), &send_pktinfo.ipi6_addr), 1);
+  }
+  if (send_local_if_str.has_value()) {
+    send_pktinfo.ipi6_ifindex = if_nametoindex(send_local_if_str->c_str());
+  }
+
+  char send_buf[] = "hello";
+  iovec send_iovec = {
+      .iov_base = send_buf,
+      .iov_len = sizeof(send_buf),
+  };
+  char send_control[CMSG_SPACE(sizeof(send_pktinfo)) + 1];
+  msghdr send_msghdr = {
+      .msg_name = &to_addr,
+      .msg_namelen = sizeof(to_addr),
+      .msg_iov = &send_iovec,
+      .msg_iovlen = 1,
+      .msg_control = send_control,
+      .msg_controllen = sizeof(send_control),
+  };
+  cmsghdr* cmsg = CMSG_FIRSTHDR(&send_msghdr);
+  ASSERT_NE(cmsg, nullptr);
+  cmsg->cmsg_len = CMSG_LEN(sizeof(send_pktinfo));
+  cmsg->cmsg_level = SOL_IPV6;
+  cmsg->cmsg_type = IPV6_PKTINFO;
+  memcpy(CMSG_DATA(cmsg), &send_pktinfo, sizeof(send_pktinfo));
+  ASSERT_EQ(sendmsg(s.get(), &send_msghdr, 0), static_cast<ssize_t>(sizeof(send_buf)))
+      << strerror(errno);
+
+  constexpr char kExpectedRecvBuf[] = "Response: hello";
+  in6_pktinfo recv_pktinfo;
+  char recv_buf[sizeof(kExpectedRecvBuf) + 1];
+  iovec recv_iovec = {
+      .iov_base = recv_buf,
+      .iov_len = sizeof(recv_buf),
+  };
+  char recv_control[CMSG_SPACE(sizeof(recv_pktinfo)) + 1];
+  msghdr recv_msghdr = {
+      .msg_iov = &recv_iovec,
+      .msg_iovlen = 1,
+      .msg_control = recv_control,
+      .msg_controllen = sizeof(recv_control),
+  };
+
+  ASSERT_EQ(recvmsg(s.get(), &recv_msghdr, 0), static_cast<ssize_t>(sizeof(kExpectedRecvBuf)))
+      << strerror(errno);
+  EXPECT_EQ(std::string_view(kExpectedRecvBuf, sizeof(kExpectedRecvBuf)),
+            std::string_view(recv_buf, sizeof(kExpectedRecvBuf)));
+  cmsg = CMSG_FIRSTHDR(&recv_msghdr);
+  ASSERT_NE(cmsg, nullptr);
+  EXPECT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(recv_pktinfo)));
+  EXPECT_EQ(cmsg->cmsg_level, SOL_IPV6);
+  EXPECT_EQ(cmsg->cmsg_type, IPV6_PKTINFO);
+  memcpy(&recv_pktinfo, CMSG_DATA(cmsg), sizeof(recv_pktinfo));
+  EXPECT_EQ(memcmp(&recv_pktinfo.ipi6_addr, &expected_recv_addr, sizeof(expected_recv_addr)), 0);
+}
+
+INSTANTIATE_TEST_SUITE_P(SocketTests, SendIpv6PacketInfoSuccessTest,
+                         testing::Values(
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC1 local address",
+                                 .send_local_addr_str = kClientIpv6Addr1,
+                                 .expected_recv_addr_str = kClientIpv6Addr1,
+                             },
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC1 local interface",
+                                 .send_local_if_str = kClientNic1Name,
+                                 .expected_recv_addr_str = kClientIpv6Addr1,
+                             },
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC1 local address and interface",
+                                 .send_local_addr_str = kClientIpv6Addr1,
+                                 .send_local_if_str = kClientNic1Name,
+                                 .expected_recv_addr_str = kClientIpv6Addr1,
+                             },
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC2 local address",
+                                 .send_local_addr_str = kClientIpv6Addr2,
+                                 .expected_recv_addr_str = kClientIpv6Addr2,
+                             },
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC2 local interface",
+                                 .send_local_if_str = kClientNic2Name,
+                                 .expected_recv_addr_str = kClientIpv6Addr2,
+                             },
+                             SendIpv6PacketInfoSuccessTestCase{
+                                 .test_name = "NIC2 local address and interface",
+                                 .send_local_addr_str = kClientIpv6Addr2,
+                                 .send_local_if_str = kClientNic2Name,
+                                 .expected_recv_addr_str = kClientIpv6Addr2,
+                             }),
+                         [](const testing::TestParamInfo<SendIpv6PacketInfoSuccessTestCase>& info) {
+                           std::string test_name(info.param.test_name);
+                           std::replace(test_name.begin(), test_name.end(), ' ', '_');
+                           return test_name;
+                         });
+
+struct SendIpv6PacketInfoFailureTestCase {
+  std::string test_name;
+  std::string bind_addr_str;
+  std::optional<std::string> bind_to_device;
+  std::optional<std::string> send_local_addr_str;
+  std::optional<std::string> send_local_if_str;
+  ssize_t expected_errno;
+};
+
+class SendIpv6PacketInfoFailureTest
+    : public testing::TestWithParam<SendIpv6PacketInfoFailureTestCase> {};
+
+TEST_P(SendIpv6PacketInfoFailureTest, CheckError) {
+  fbl::unique_fd s;
+  ASSERT_TRUE(s = fbl::unique_fd(socket(AF_INET6, SOCK_DGRAM, 0))) << strerror(errno);
+
+  const auto [test_name, bind_addr_str, bind_to_device, send_local_addr_str, send_local_if_str,
+              expected_errno] = GetParam();
+
+  if (bind_to_device.has_value()) {
+    ASSERT_EQ(setsockopt(s.get(), SOL_SOCKET, SO_BINDTODEVICE, bind_to_device->c_str(),
+                         static_cast<socklen_t>(bind_to_device->size())),
+              0)
+        << strerror(errno);
+  }
+
+  sockaddr_in6 bind_addr{
+      .sin6_family = AF_INET6,
+  };
+  ASSERT_EQ(inet_pton(bind_addr.sin6_family, bind_addr_str.c_str(), &bind_addr.sin6_addr), 1);
+  ASSERT_EQ(bind(s.get(), reinterpret_cast<const sockaddr*>(&bind_addr), sizeof(bind_addr)), 0)
+      << strerror(errno);
+
+  sockaddr_in6 to_addr = {
+      .sin6_family = AF_INET6,
+      .sin6_port = htons(kServerPort),
+  };
+  ASSERT_EQ(inet_pton(to_addr.sin6_family, kServerIpv6Addr, &to_addr.sin6_addr), 1);
+
+  in6_pktinfo send_pktinfo = {};
+  if (send_local_addr_str.has_value()) {
+    ASSERT_EQ(inet_pton(AF_INET6, send_local_addr_str->c_str(), &send_pktinfo.ipi6_addr), 1);
+  }
+  if (send_local_if_str.has_value()) {
+    send_pktinfo.ipi6_ifindex = if_nametoindex(send_local_if_str->c_str());
+  }
+
+  char send_buf[] = "hello";
+  iovec send_iovec = {
+      .iov_base = send_buf,
+      .iov_len = sizeof(send_buf),
+  };
+  char send_control[CMSG_SPACE(sizeof(send_pktinfo)) + 1];
+  msghdr send_msghdr = {
+      .msg_name = &to_addr,
+      .msg_namelen = sizeof(to_addr),
+      .msg_iov = &send_iovec,
+      .msg_iovlen = 1,
+      .msg_control = send_control,
+      .msg_controllen = sizeof(send_control),
+  };
+  cmsghdr* cmsg = CMSG_FIRSTHDR(&send_msghdr);
+  ASSERT_NE(cmsg, nullptr);
+  cmsg->cmsg_len = CMSG_LEN(sizeof(send_pktinfo));
+  cmsg->cmsg_level = SOL_IPV6;
+  cmsg->cmsg_type = IPV6_PKTINFO;
+  memcpy(CMSG_DATA(cmsg), &send_pktinfo, sizeof(send_pktinfo));
+  ASSERT_EQ(sendmsg(s.get(), &send_msghdr, 0), -1);
+  EXPECT_EQ(errno, expected_errno) << strerror(errno);
+}
+
+constexpr char kIpv6UnspecifiedAddr[] = "::";
+
+INSTANTIATE_TEST_SUITE_P(SocketTests, SendIpv6PacketInfoFailureTest,
+                         testing::Values(
+                             SendIpv6PacketInfoFailureTestCase{
+                                 .test_name = "Local interface and bound device mismatch",
+                                 .bind_addr_str = kIpv6UnspecifiedAddr,
+                                 .bind_to_device = kClientNic2Name,
+                                 .send_local_if_str = kClientNic1Name,
+                                 .expected_errno = EHOSTUNREACH,
+                             },
+                             SendIpv6PacketInfoFailureTestCase{
+                                 .test_name = "Local address and bound device mismatch",
+                                 .bind_addr_str = kIpv6UnspecifiedAddr,
+                                 .bind_to_device = kClientNic2Name,
+                                 .send_local_addr_str = kClientIpv6Addr1,
+                                 .expected_errno = EADDRNOTAVAIL,
+                             },
+                             SendIpv6PacketInfoFailureTestCase{
+                                 .test_name = "Local addr and interface mismatch",
+                                 .bind_addr_str = kIpv6UnspecifiedAddr,
+                                 .send_local_addr_str = kClientIpv6Addr2,
+                                 .send_local_if_str = kClientNic1Name,
+                                 .expected_errno = EADDRNOTAVAIL,
+                             },
+                             SendIpv6PacketInfoFailureTestCase{
+                                 .test_name = "Bound address and local interface mismatch",
+                                 .bind_addr_str = kClientIpv6Addr1,
+                                 .send_local_if_str = kClientNic2Name,
+                                 .expected_errno = EADDRNOTAVAIL}),
+                         [](const testing::TestParamInfo<SendIpv6PacketInfoFailureTestCase>& info) {
+                           std::string test_name(info.param.test_name);
+                           std::replace(test_name.begin(), test_name.end(), ' ', '_');
+                           return test_name;
+                         });
+
+}  // namespace