| // Copyright 2017 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 <zircon/compiler.h> |
| #include <zircon/device/device.h> |
| #include <zircon/device/ethernet.h> |
| #include <zircon/device/ethertap.h> |
| #include <zircon/status.h> |
| #include <zircon/types.h> |
| #include <zx/fifo.h> |
| #include <zx/socket.h> |
| #include <zx/time.h> |
| #include <zx/vmar.h> |
| #include <zx/vmo.h> |
| #include <fdio/watcher.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/intrusive_single_list.h> |
| #include <fbl/type_support.h> |
| #include <fbl/unique_ptr.h> |
| #include <fbl/vector.h> |
| #include <unittest/unittest.h> |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| namespace { |
| |
| const char kEthernetDir[] = "/dev/class/ethernet"; |
| const char kTapctl[] = "/dev/misc/tapctl"; |
| const char kTapDevName[] = "test"; |
| const uint8_t kTapMac[] = { 0x12, 0x20, 0x30, 0x40, 0x50, 0x60 }; |
| |
| const char* mxstrerror(zx_status_t status) { |
| return zx_status_get_string(status); |
| } |
| |
| zx_status_t CreateEthertap(uint32_t mtu, zx::socket* sock) { |
| if (sock == nullptr) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| int ctlfd = open(kTapctl, O_RDONLY); |
| if (ctlfd < 0) { |
| fprintf(stderr, "could not open %s: %s\n", kTapctl, strerror(errno)); |
| return ZX_ERR_IO; |
| } |
| auto closer = fbl::MakeAutoCall([ctlfd]() { close(ctlfd); }); |
| |
| ethertap_ioctl_config_t config = {}; |
| strlcpy(config.name, kTapDevName, ETHERTAP_MAX_NAME_LEN); |
| // Uncomment this to trace ETHERTAP events |
| //config.options = ETHERTAP_OPT_TRACE; |
| config.mtu = mtu; |
| memcpy(config.mac, kTapMac, 6); |
| |
| ssize_t rc = ioctl_ethertap_config(ctlfd, &config, sock->reset_and_get_address()); |
| if (rc < 0) { |
| zx_status_t status = static_cast<zx_status_t>(rc); |
| fprintf(stderr, "could not configure ethertap device: %s\n", mxstrerror(status)); |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t WatchCb(int dirfd, int event, const char* fn, void* cookie) { |
| if (event != WATCH_EVENT_ADD_FILE) return ZX_OK; |
| if (!strcmp(fn, ".") || !strcmp(fn, "..")) return ZX_OK; |
| |
| int devfd = openat(dirfd, fn, O_RDONLY); |
| if (devfd < 0) { |
| return ZX_OK; |
| } |
| auto closer = fbl::MakeAutoCall([devfd]() { close(devfd); }); |
| |
| // See if this device is our ethertap device |
| eth_info_t info; |
| ssize_t rc = ioctl_ethernet_get_info(devfd, &info); |
| if (rc < 0) { |
| zx_status_t status = static_cast<zx_status_t>(rc); |
| fprintf(stderr, "could not get ethernet info for %s/%s: %s\n", kEthernetDir, fn, |
| mxstrerror(status)); |
| // Return ZX_OK to keep watching for devices. |
| return ZX_OK; |
| } |
| if (!(info.features & ETH_FEATURE_SYNTH)) { |
| // Not a match, keep looking. |
| return ZX_OK; |
| } |
| |
| // Found it! |
| // TODO(tkilbourn): this might not be the test device we created; need a robust way of getting |
| // the name of the tap device to check. Note that ioctl_device_get_device_name just returns |
| // "ethernet" since that's the child of the tap device that we've opened here. |
| auto fd = reinterpret_cast<int*>(cookie); |
| *fd = devfd; |
| closer.cancel(); |
| return ZX_ERR_STOP; |
| } |
| |
| zx_status_t OpenEthertapDev(int* fd) { |
| if (fd == nullptr) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| int ethdir = open(kEthernetDir, O_RDONLY); |
| if (ethdir < 0) { |
| fprintf(stderr, "could not open %s: %s\n", kEthernetDir, strerror(errno)); |
| return ZX_ERR_IO; |
| } |
| |
| zx_status_t status = fdio_watch_directory(ethdir, WatchCb, zx::deadline_after(ZX_SEC(2)), |
| reinterpret_cast<void*>(fd)); |
| if (status == ZX_ERR_STOP) { |
| return ZX_OK; |
| } else { |
| return status; |
| } |
| } |
| |
| struct FifoEntry : public fbl::SinglyLinkedListable<fbl::unique_ptr<FifoEntry>> { |
| eth_fifo_entry_t e; |
| }; |
| |
| class EthernetClient { |
| public: |
| explicit EthernetClient(int fd) : fd_(fd) {} |
| ~EthernetClient() { |
| if (mapped_ > 0) { |
| zx::vmar::root_self().unmap(mapped_, vmo_size_); |
| } |
| close(fd_); |
| } |
| |
| zx_status_t Register(const char* name, uint32_t nbufs, uint16_t bufsize) { |
| ssize_t rc = ioctl_ethernet_set_client_name(fd_, name, strlen(name) + 1); |
| if (rc < 0) { |
| return static_cast<zx_status_t>(rc); |
| } |
| |
| eth_fifos_t fifos; |
| rc = ioctl_ethernet_get_fifos(fd_, &fifos); |
| if (rc < 0) { |
| return static_cast<zx_status_t>(rc); |
| } |
| |
| tx_.reset(fifos.tx_fifo); |
| rx_.reset(fifos.rx_fifo); |
| tx_depth_ = fifos.tx_depth; |
| rx_depth_ = fifos.rx_depth; |
| |
| nbufs_ = nbufs; |
| bufsize_ = bufsize; |
| |
| vmo_size_ = 2 * nbufs_ * bufsize_; |
| zx_status_t status = zx::vmo::create(vmo_size_, 0u, &buf_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = zx::vmar::root_self().map(0, buf_, 0, vmo_size_, |
| ZX_VM_FLAG_PERM_READ | ZX_VM_FLAG_PERM_WRITE, |
| &mapped_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| zx::vmo buf_copy; |
| status = buf_.duplicate(ZX_RIGHT_SAME_RIGHTS, &buf_copy); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| zx_handle_t bufh = buf_copy.release(); |
| rc = ioctl_ethernet_set_iobuf(fd_, &bufh); |
| if (rc < 0) { |
| return static_cast<zx_status_t>(rc); |
| } |
| |
| uint32_t idx = 0; |
| for (; idx < nbufs; idx++) { |
| eth_fifo_entry_t entry = { |
| .offset = idx * bufsize_, |
| .length = bufsize_, |
| .flags = 0, |
| .cookie = nullptr, |
| }; |
| uint32_t actual; |
| status = rx_.write(&entry, sizeof(entry), &actual); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| |
| for (; idx < 2 * nbufs; idx++) { |
| auto entry = fbl::unique_ptr<FifoEntry>(new FifoEntry); |
| entry->e.offset = idx * bufsize_; |
| entry->e.length = bufsize_; |
| entry->e.flags = 0; |
| entry->e.cookie = reinterpret_cast<uint8_t*>(mapped_) + entry->e.offset; |
| tx_available_.push_front(fbl::move(entry)); |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Start() { |
| ssize_t rc = ioctl_ethernet_start(fd_); |
| return rc < 0 ? static_cast<zx_status_t>(rc) : ZX_OK; |
| } |
| |
| zx_status_t Stop() { |
| ssize_t rc = ioctl_ethernet_stop(fd_); |
| return rc < 0 ? static_cast<zx_status_t>(rc) : ZX_OK; |
| } |
| |
| zx_status_t GetStatus(uint32_t* eth_status) { |
| ssize_t rc = ioctl_ethernet_get_status(fd_, eth_status); |
| return rc < 0 ? static_cast<zx_status_t>(rc) : ZX_OK; |
| } |
| |
| zx::fifo* tx_fifo() { return &tx_; } |
| zx::fifo* rx_fifo() { return &rx_; } |
| uint32_t tx_depth() { return tx_depth_; } |
| uint32_t rx_depth() { return rx_depth_; } |
| |
| uint8_t* GetRxBuffer(uint32_t offset) { |
| return reinterpret_cast<uint8_t*>(mapped_) + offset; |
| } |
| |
| eth_fifo_entry_t* GetTxBuffer() { |
| auto entry_ptr = tx_available_.pop_front(); |
| eth_fifo_entry_t* entry = nullptr; |
| if (entry_ptr != nullptr) { |
| entry = &entry_ptr->e; |
| tx_pending_.push_front(fbl::move(entry_ptr)); |
| } |
| return entry; |
| } |
| |
| void ReturnTxBuffer(eth_fifo_entry_t* entry) { |
| auto entry_ptr = tx_pending_.erase_if( |
| [entry](const FifoEntry& tx_entry) { return tx_entry.e.cookie == entry->cookie; }); |
| if (entry_ptr != nullptr) { |
| tx_available_.push_front(fbl::move(entry_ptr)); |
| } |
| } |
| |
| private: |
| int fd_; |
| |
| uint64_t vmo_size_ = 0; |
| zx::vmo buf_; |
| uintptr_t mapped_ = 0; |
| uint32_t nbufs_ = 0; |
| uint16_t bufsize_ = 0; |
| |
| zx::fifo tx_; |
| zx::fifo rx_; |
| uint32_t tx_depth_ = 0; |
| uint32_t rx_depth_ = 0; |
| |
| using FifoEntryPtr = fbl::unique_ptr<FifoEntry>; |
| fbl::SinglyLinkedList<FifoEntryPtr> tx_available_; |
| fbl::SinglyLinkedList<FifoEntryPtr> tx_pending_; |
| }; |
| |
| } // namespace |
| |
| static bool EthernetStartTest() { |
| // Create the ethertap device |
| zx::socket sock; |
| ASSERT_EQ(ZX_OK, CreateEthertap(1500, &sock)); |
| |
| // Open the ethernet device |
| int devfd = -1; |
| ASSERT_EQ(ZX_OK, OpenEthertapDev(&devfd)); |
| ASSERT_GE(devfd, 0); |
| |
| // Set up an ethernet client |
| EthernetClient client(devfd); |
| ASSERT_EQ(ZX_OK, client.Register(kTapDevName, 32, 2048)); |
| |
| // Verify no signals asserted on the rx fifo |
| zx_signals_t obs; |
| client.rx_fifo()->wait_one(ETH_SIGNAL_STATUS, 0, &obs); |
| EXPECT_FALSE(obs & ETH_SIGNAL_STATUS); |
| |
| // Start the ethernet client |
| EXPECT_EQ(ZX_OK, client.Start()); |
| |
| // Default link status should be OFFLINE |
| uint32_t eth_status = 0; |
| EXPECT_EQ(ZX_OK, client.GetStatus(ð_status)); |
| EXPECT_EQ(0, eth_status); |
| |
| // Set the link status to online and verify |
| sock.signal_peer(0, ETHERTAP_SIGNAL_ONLINE); |
| |
| EXPECT_EQ(ZX_OK, |
| client.rx_fifo()->wait_one(ETH_SIGNAL_STATUS, zx::deadline_after(ZX_MSEC(10)), &obs)); |
| EXPECT_TRUE(obs & ETH_SIGNAL_STATUS); |
| |
| EXPECT_EQ(ZX_OK, client.GetStatus(ð_status)); |
| EXPECT_EQ(ETH_STATUS_ONLINE, eth_status); |
| |
| // Shutdown the ethernet client |
| EXPECT_EQ(ZX_OK, client.Stop()); |
| |
| // Clean up the ethertap device |
| sock.reset(); |
| |
| return true; |
| } |
| |
| static bool EthernetLinkStatusTest() { |
| // Create the ethertap device |
| zx::socket sock; |
| ASSERT_EQ(ZX_OK, CreateEthertap(1500, &sock)); |
| |
| // Set the link status to online |
| sock.signal_peer(0, ETHERTAP_SIGNAL_ONLINE); |
| |
| // Open the ethernet device |
| int devfd = -1; |
| ASSERT_EQ(ZX_OK, OpenEthertapDev(&devfd)); |
| ASSERT_GE(devfd, 0); |
| |
| // Set up an ethernet client |
| EthernetClient client(devfd); |
| ASSERT_EQ(ZX_OK, client.Register(kTapDevName, 32, 2048)); |
| |
| // Start the ethernet client |
| EXPECT_EQ(ZX_OK, client.Start()); |
| |
| // Link status should be ONLINE since we set it before starting the client |
| uint32_t eth_status = 0; |
| EXPECT_EQ(ZX_OK, client.GetStatus(ð_status)); |
| EXPECT_EQ(ETH_STATUS_ONLINE, eth_status); |
| |
| // Now the device goes offline |
| sock.signal_peer(0, ETHERTAP_SIGNAL_OFFLINE); |
| |
| // Verify the link status |
| zx_signals_t obs; |
| EXPECT_EQ(ZX_OK, |
| client.rx_fifo()->wait_one(ETH_SIGNAL_STATUS, zx::deadline_after(ZX_MSEC(10)), &obs)); |
| EXPECT_TRUE(obs & ETH_SIGNAL_STATUS); |
| |
| EXPECT_EQ(ZX_OK, client.GetStatus(ð_status)); |
| EXPECT_EQ(0, eth_status); |
| |
| // Shutdown the ethernet client |
| EXPECT_EQ(ZX_OK, client.Stop()); |
| |
| // Clean up the ethertap device |
| sock.reset(); |
| |
| return true; |
| } |
| |
| static bool EthernetDataTest_Send() { |
| // Set up the tap device and the ethernet client |
| zx::socket sock; |
| ASSERT_EQ(ZX_OK, CreateEthertap(1500, &sock)); |
| |
| int devfd = -1; |
| ASSERT_EQ(ZX_OK, OpenEthertapDev(&devfd)); |
| ASSERT_GE(devfd, 0); |
| |
| EthernetClient client(devfd); |
| ASSERT_EQ(ZX_OK, client.Register(kTapDevName, 32, 2048)); |
| ASSERT_EQ(ZX_OK, client.Start()); |
| |
| sock.signal_peer(0, ETHERTAP_SIGNAL_ONLINE); |
| |
| // Ensure that the fifo is writable |
| zx_signals_t obs; |
| EXPECT_EQ(ZX_OK, client.tx_fifo()->wait_one(ZX_FIFO_WRITABLE, 0, &obs)); |
| ASSERT_TRUE(obs & ZX_FIFO_WRITABLE); |
| |
| // Grab an available TX fifo entry |
| auto entry = client.GetTxBuffer(); |
| ASSERT_TRUE(entry != nullptr); |
| |
| // Populate some data |
| uint8_t* buf = static_cast<uint8_t*>(entry->cookie); |
| for (int i = 0; i < 32; i++) { |
| buf[i] = static_cast<uint8_t>(i & 0xff); |
| } |
| entry->length = 32; |
| |
| // Write to the TX fifo |
| uint32_t actual = 0; |
| ASSERT_EQ(ZX_OK, client.tx_fifo()->write(entry, sizeof(eth_fifo_entry_t), &actual)); |
| EXPECT_EQ(1u, actual); |
| |
| // The socket should be readable |
| EXPECT_EQ(ZX_OK, sock.wait_one(ZX_SOCKET_READABLE, zx::deadline_after(ZX_MSEC(10)), &obs)); |
| ASSERT_TRUE(obs & ZX_SOCKET_READABLE); |
| |
| // Read the data from the socket, which should match what was written to the fifo |
| uint8_t read_buf[32]; |
| size_t actual_sz = 0; |
| EXPECT_EQ(ZX_OK, sock.read(0u, static_cast<void*>(read_buf), 32, &actual_sz)); |
| ASSERT_EQ(32, actual_sz); |
| EXPECT_BYTES_EQ(buf, read_buf, 32, ""); |
| |
| // Now the TX completion entry should be available to read from the TX fifo |
| EXPECT_EQ(ZX_OK, |
| client.tx_fifo()->wait_one(ZX_FIFO_READABLE, zx::deadline_after(ZX_MSEC(10)), &obs)); |
| ASSERT_TRUE(obs & ZX_FIFO_READABLE); |
| |
| eth_fifo_entry_t return_entry; |
| ASSERT_EQ(ZX_OK, client.tx_fifo()->read(&return_entry, sizeof(eth_fifo_entry_t), &actual)); |
| EXPECT_EQ(1u, actual); |
| |
| // Check the flags on the returned entry |
| EXPECT_TRUE(return_entry.flags & ETH_FIFO_TX_OK); |
| return_entry.flags = 0; |
| |
| // Verify the bytes from the rest of the entry match what we wrote |
| auto expected_entry = reinterpret_cast<uint8_t*>(entry); |
| auto actual_entry = reinterpret_cast<uint8_t*>(&return_entry); |
| EXPECT_BYTES_EQ(expected_entry, actual_entry, sizeof(eth_fifo_entry_t), ""); |
| |
| // Return the buffer to our client; the client destructor will make sure no TXs are still |
| // pending at the end of te test. |
| client.ReturnTxBuffer(&return_entry); |
| |
| // Shutdown the client and cleanup the tap device |
| EXPECT_EQ(ZX_OK, client.Stop()); |
| sock.reset(); |
| |
| return true; |
| } |
| |
| static bool EthernetDataTest_Recv() { |
| // Set up the tap device and the ethernet client |
| zx::socket sock; |
| ASSERT_EQ(ZX_OK, CreateEthertap(1500, &sock)); |
| |
| int devfd = -1; |
| ASSERT_EQ(ZX_OK, OpenEthertapDev(&devfd)); |
| ASSERT_GE(devfd, 0); |
| |
| EthernetClient client(devfd); |
| ASSERT_EQ(ZX_OK, client.Register(kTapDevName, 32, 2048)); |
| ASSERT_EQ(ZX_OK, client.Start()); |
| |
| sock.signal_peer(0, ETHERTAP_SIGNAL_ONLINE); |
| |
| // The socket should be writable |
| zx_signals_t obs; |
| EXPECT_EQ(ZX_OK, sock.wait_one(ZX_SOCKET_WRITABLE, 0, &obs)); |
| ASSERT_TRUE(obs & ZX_SOCKET_WRITABLE); |
| |
| // Send a buffer through the socket |
| uint8_t buf[32]; |
| for (int i = 0; i < 32; i++) { |
| buf[i] = static_cast<uint8_t>(i & 0xff); |
| } |
| size_t actual = 0; |
| EXPECT_EQ(ZX_OK, sock.write(0, static_cast<void*>(buf), 32, &actual)); |
| EXPECT_EQ(32, actual); |
| |
| // The fifo should be readable |
| EXPECT_EQ(ZX_OK, |
| client.rx_fifo()->wait_one(ZX_FIFO_READABLE, zx::deadline_after(ZX_MSEC(10)), &obs)); |
| ASSERT_TRUE(obs & ZX_FIFO_READABLE); |
| |
| // Read the RX fifo |
| eth_fifo_entry_t entry; |
| uint32_t actual_entries = 0; |
| EXPECT_EQ(ZX_OK, client.rx_fifo()->read(&entry, sizeof(eth_fifo_entry_t), &actual_entries)); |
| EXPECT_EQ(1, actual_entries); |
| |
| // Check the bytes in the VMO compared to what we sent through the socket |
| auto return_buf = client.GetRxBuffer(entry.offset); |
| EXPECT_BYTES_EQ(buf, return_buf, entry.length, ""); |
| |
| // RX fifo should be readable, and we can return the buffer to the driver |
| EXPECT_EQ(ZX_OK, client.rx_fifo()->wait_one(ZX_FIFO_WRITABLE, 0, &obs)); |
| ASSERT_TRUE(obs & ZX_FIFO_WRITABLE); |
| |
| entry.length = 2048; |
| EXPECT_EQ(ZX_OK, client.rx_fifo()->write(&entry, sizeof(eth_fifo_entry_t), &actual_entries)); |
| EXPECT_EQ(1, actual_entries); |
| |
| // Shutdown the client and cleanup the tap device |
| EXPECT_EQ(ZX_OK, client.Stop()); |
| sock.reset(); |
| |
| return true; |
| } |
| |
| BEGIN_TEST_CASE(EthernetSetupTests) |
| RUN_TEST_MEDIUM(EthernetStartTest) |
| RUN_TEST_MEDIUM(EthernetLinkStatusTest) |
| END_TEST_CASE(EthernetSetupTests) |
| |
| BEGIN_TEST_CASE(EthernetDataTests) |
| RUN_TEST_MEDIUM(EthernetDataTest_Send) |
| RUN_TEST_MEDIUM(EthernetDataTest_Recv) |
| END_TEST_CASE(EthernetDataTests) |
| |
| int main(int argc, char* argv[]) { |
| bool success = unittest_run_all_tests(argc, argv); |
| return success ? 0 : -1; |
| } |