ncsid: Import from gBMC

This is the initial code drop from gBMC.

Google-Bug-Id: 179618516
Upstream: 1e71af914bc8c54d8b91d0a1cf377e2696713c2f
Change-Id: Ic653e8271dacd205e04f2bc713071ef2ec5936a4
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/ncsid/test/iface_test.cpp b/ncsid/test/iface_test.cpp
new file mode 100644
index 0000000..70eef05
--- /dev/null
+++ b/ncsid/test/iface_test.cpp
@@ -0,0 +1,24 @@
+#include "net_iface_mock.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestIFace, TestGetIndex)
+{
+    mock::IFace iface_mock;
+
+    constexpr int test_index = 5;
+    iface_mock.index = test_index;
+
+    EXPECT_EQ(test_index, iface_mock.get_index());
+}
+
+TEST(TestIFace, TestSetClearFlags)
+{
+    mock::IFace iface_mock;
+
+    const short new_flags = 0xab;
+    iface_mock.set_sock_flags(0, new_flags);
+    EXPECT_EQ(new_flags, new_flags & iface_mock.flags);
+    iface_mock.clear_sock_flags(0, 0xa0);
+    EXPECT_EQ(0xb, new_flags & iface_mock.flags);
+}
diff --git a/ncsid/test/meson.build b/ncsid/test/meson.build
new file mode 100644
index 0000000..169b33f
--- /dev/null
+++ b/ncsid/test/meson.build
@@ -0,0 +1,67 @@
+gtest = dependency('gtest', main: true, disabler: true, required: false)
+gmock = dependency('gmock', disabler: true, required: false)
+if not gtest.found() or not gmock.found()
+  gtest_proj = import('cmake').subproject(
+    'googletest',
+    cmake_options: [
+      '-DCMAKE_CXX_FLAGS=-Wno-pedantic',
+    ],
+    required: false)
+  if gtest_proj.found()
+    gtest = declare_dependency(
+      dependencies: [
+        dependency('threads'),
+        gtest_proj.dependency('gtest'),
+        gtest_proj.dependency('gtest_main'),
+      ])
+    gmock = gtest_proj.dependency('gmock')
+  else
+    assert(not build_tests.enabled(), 'Googletest is required')
+  endif
+endif
+
+tests = [
+  'iface_test',
+  'sock_test',
+  #'ncsi_test',  # TODO: Re-enable when fixed
+]
+
+ncsid_test_headers = include_directories('.')
+
+ncsid_test_lib = static_library(
+  'ncsid_test',
+  [
+    'net_iface_mock.cpp',
+    'nic_mock.cpp',
+  ],
+  include_directories: ncsid_test_headers,
+  implicit_include_directories: false,
+  dependencies: ncsid)
+
+ncsid_test = declare_dependency(
+  dependencies: ncsid,
+  include_directories: ncsid_test_headers,
+  link_with: ncsid_test_lib)
+
+foreach t : tests
+  test(t, executable(t.underscorify(), t + '.cpp',
+                     implicit_include_directories: false,
+                     dependencies: [gtest, gmock, ncsid_test]))
+endforeach
+
+script_tests = [
+  'normalize_ip_test',
+  'normalize_mac_test',
+]
+
+script_env = environment()
+script_deps = []
+script_env.set('NORMALIZE_IP', normalize_ip.full_path())
+script_deps += normalize_ip
+script_env.set('NORMALIZE_MAC', normalize_mac.full_path())
+script_deps += normalize_mac
+
+foreach st : script_tests
+  test(st, find_program('bash'), args: files(st + '.sh'),
+    protocol: 'tap', env: script_env, depends: script_deps)
+endforeach
diff --git a/ncsid/test/ncsi_test.cpp b/ncsid/test/ncsi_test.cpp
new file mode 100644
index 0000000..9808da6
--- /dev/null
+++ b/ncsid/test/ncsi_test.cpp
@@ -0,0 +1,240 @@
+#include "net_iface_mock.h"
+#include "nic_mock.h"
+#include "platforms/nemora/portable/default_addresses.h"
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/ncsi_fsm.h"
+#include "platforms/nemora/portable/net_types.h"
+
+#include <ncsi_state_machine.h>
+#include <net_config.h>
+#include <net_sockio.h>
+#include <netinet/ether.h>
+#include <netinet/in.h>
+
+#include <gmock/gmock.h>
+
+namespace
+{
+
+constexpr uint32_t ETHER_NCSI = 0x88f8;
+
+class MockConfig : public net::ConfigBase
+{
+  public:
+    int get_mac_addr(mac_addr_t* mac) override
+    {
+        std::memcpy(mac, &mac_addr, sizeof(mac_addr_t));
+
+        return 0;
+    }
+
+    int set_mac_addr(const mac_addr_t& mac) override
+    {
+        mac_addr = mac;
+
+        return 0;
+    }
+
+    int set_nic_hostless(bool is_hostless) override
+    {
+        is_nic_hostless = is_hostless;
+
+        return 0;
+    }
+
+    mac_addr_t mac_addr;
+    bool is_nic_hostless = true;
+};
+
+class NICConnection : public net::SockIO
+{
+  public:
+    int write(const void* buf, size_t len) override
+    {
+        conseq_reads = 0;
+        ++n_writes;
+        std::memcpy(last_write.data, buf, len);
+        last_write.len = len;
+        const auto* hdr = reinterpret_cast<const struct ether_header*>(buf);
+        if (ETHER_NCSI == ntohs(hdr->ether_type))
+        {
+            ++n_handles;
+            next_read.len = nic_mock.handle_request(last_write, &next_read);
+        }
+
+        return len;
+    }
+
+    int recv(void* buf, size_t maxlen) override
+    {
+        ++n_reads;
+        ++conseq_reads;
+
+        if (read_timeout > 0)
+        {
+            if (conseq_reads > read_timeout)
+            {
+                return 0;
+            }
+        }
+
+        if (maxlen < next_read.len)
+        {
+            ++n_read_errs;
+            return 0;
+        }
+
+        std::memcpy(buf, next_read.data, next_read.len);
+
+        return next_read.len;
+    }
+
+    mock::NIC nic_mock{false, 2};
+    int n_writes = 0;
+    int n_reads = 0;
+    int n_handles = 0;
+    int n_read_errs = 0;
+
+    // Max number of consequitive reads without writes.
+    int read_timeout = -1;
+    int conseq_reads = 0;
+
+    ncsi_buf_t last_write = {};
+    ncsi_buf_t next_read = {};
+};
+
+} // namespace
+
+class TestNcsi : public testing::Test
+{
+  public:
+    void SetUp() override
+    {
+        ncsi_sm.set_sockio(&ncsi_sock);
+        ncsi_sm.set_net_config(&net_config_mock);
+        ncsi_sm.set_retest_delay(0);
+        ncsi_sock.nic_mock.set_mac(nic_mac);
+        ncsi_sock.nic_mock.set_hostless(true);
+        ncsi_sock.read_timeout = 10;
+    }
+
+  protected:
+    void ExpectFiltersNotConfigured()
+    {
+        for (uint8_t i = 0; i < ncsi_sock.nic_mock.get_channel_count(); ++i)
+        {
+            EXPECT_FALSE(ncsi_sock.nic_mock.is_filter_configured(i));
+        }
+    }
+
+    void ExpectFiltersConfigured()
+    {
+        // Check that filters are configured on all channels.
+        for (uint8_t i = 0; i < ncsi_sock.nic_mock.get_channel_count(); ++i)
+        {
+            EXPECT_TRUE(ncsi_sock.nic_mock.is_filter_configured(i));
+            const ncsi_oem_filter_t& ch_filter =
+                ncsi_sock.nic_mock.get_filter(i);
+
+            for (unsigned i = 0; i < sizeof(nic_mac.octet); ++i)
+            {
+                EXPECT_EQ(nic_mac.octet[i], ch_filter.mac[i]);
+            }
+
+            EXPECT_EQ(ch_filter.ip, 0);
+            const uint16_t filter_port = ntohs(ch_filter.port);
+            EXPECT_EQ(filter_port, DEFAULT_ADDRESSES_RX_PORT);
+        }
+    }
+
+    MockConfig net_config_mock;
+    NICConnection ncsi_sock;
+    ncsi::StateMachine ncsi_sm;
+    const mac_addr_t nic_mac = {{0xde, 0xca, 0xfb, 0xad, 0x01, 0x02}};
+
+    // Number of states in each state machine
+    static constexpr int l2_num_states = 26;
+    static constexpr int l3l4_num_states = 2;
+    static constexpr int test_num_states = 9;
+
+    // Total number of states in all three state machines.
+    static constexpr int total_num_states =
+        l2_num_states + l3l4_num_states + test_num_states;
+};
+
+TEST_F(TestNcsi, TestMACAddrPropagation)
+{
+    ncsi_sm.run(total_num_states);
+    EXPECT_EQ(ncsi_sock.n_read_errs, 0);
+    EXPECT_EQ(ncsi_sock.n_handles, ncsi_sock.n_writes);
+    EXPECT_EQ(0, std::memcmp(nic_mac.octet, net_config_mock.mac_addr.octet,
+                             sizeof(nic_mac.octet)));
+
+    // Since network is not configured, the filters should not be configured
+    // either.
+    ExpectFiltersNotConfigured();
+}
+
+TEST_F(TestNcsi, TestFilterConfiguration)
+{
+    ncsi_sm.run(total_num_states);
+    EXPECT_EQ(ncsi_sock.n_read_errs, 0);
+    EXPECT_EQ(ncsi_sock.n_handles, ncsi_sock.n_writes);
+
+    ExpectFiltersConfigured();
+}
+
+TEST_F(TestNcsi, TestFilterReset)
+{
+    ncsi_sm.run(total_num_states);
+    EXPECT_EQ(ncsi_sock.n_read_errs, 0);
+    EXPECT_EQ(ncsi_sock.n_handles, ncsi_sock.n_writes);
+
+    // Since network is not configured, the filters should not be configured
+    // either.
+    ExpectFiltersNotConfigured();
+
+    ncsi_sm.run(total_num_states);
+
+    ExpectFiltersConfigured();
+}
+
+TEST_F(TestNcsi, TestRetest)
+{
+    ncsi_sm.run(total_num_states + test_num_states);
+
+    // Verify that the test state machine was stepped through twice,
+    // by counting how many times the last command of the state machine
+    // has been executed.
+    const uint8_t last_test_command = NCSI_GET_LINK_STATUS;
+    const auto& cmd_log = ncsi_sock.nic_mock.get_command_log();
+    int num_test_runs = 0;
+    for (const auto& ncsi_frame : cmd_log)
+    {
+        if (ncsi_frame.get_control_packet_type() == last_test_command)
+        {
+            ++num_test_runs;
+        }
+    }
+
+    EXPECT_EQ(num_test_runs, 2);
+}
+
+TEST_F(TestNcsi, TestHostlessSwitch)
+{
+    // By default the NIC is in hostless mode.
+    // Verify that net config flag changes after FSM run.
+    net_config_mock.is_nic_hostless = false;
+    ncsi_sm.run(total_num_states);
+    EXPECT_EQ(ncsi_sock.n_read_errs, 0);
+    EXPECT_EQ(ncsi_sock.n_handles, ncsi_sock.n_writes);
+    EXPECT_TRUE(net_config_mock.is_nic_hostless);
+
+    // Now disable the hostless mode and verify that net config
+    // flag changes to false.
+    ncsi_sock.nic_mock.set_hostless(false);
+    ncsi_sm.run(total_num_states);
+    EXPECT_EQ(ncsi_sock.n_read_errs, 0);
+    EXPECT_EQ(ncsi_sock.n_handles, ncsi_sock.n_writes);
+    EXPECT_FALSE(net_config_mock.is_nic_hostless);
+}
diff --git a/ncsid/test/net_iface_mock.cpp b/ncsid/test/net_iface_mock.cpp
new file mode 100644
index 0000000..4c72ef2
--- /dev/null
+++ b/ncsid/test/net_iface_mock.cpp
@@ -0,0 +1,38 @@
+#include "net_iface_mock.h"
+
+namespace mock
+{
+
+int IFace::bind_sock(int sockfd, struct sockaddr_ll*) const
+{
+    bound_socks.push_back(sockfd);
+    return 0;
+}
+
+int IFace::ioctl_sock(int, int request, struct ifreq* ifr) const
+{
+    return ioctl(request, ifr);
+}
+
+int IFace::ioctl(int request, struct ifreq* ifr) const
+{
+    int ret = 0;
+    switch (request)
+    {
+        case SIOCGIFINDEX:
+            ifr->ifr_ifindex = index;
+            break;
+        case SIOCGIFFLAGS:
+            ifr->ifr_flags = flags;
+            break;
+        case SIOCSIFFLAGS:
+            flags = ifr->ifr_flags;
+            break;
+        default:
+            ret = -1;
+    }
+
+    return ret;
+}
+
+} // namespace mock
diff --git a/ncsid/test/net_iface_mock.h b/ncsid/test/net_iface_mock.h
new file mode 100644
index 0000000..5fcbcc9
--- /dev/null
+++ b/ncsid/test/net_iface_mock.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <net_iface.h>
+
+#include <vector>
+
+namespace mock
+{
+
+class IFace : public net::IFaceBase
+{
+  public:
+    IFace() : net::IFaceBase("mock0")
+    {}
+    explicit IFace(const std::string& name) : net::IFaceBase(name)
+    {}
+    int bind_sock(int sockfd, struct sockaddr_ll* saddr) const override;
+
+    mutable std::vector<int> bound_socks;
+    int index;
+    mutable short flags = 0;
+
+  private:
+    int ioctl_sock(int sockfd, int request, struct ifreq* ifr) const override;
+    int ioctl(int request, struct ifreq* ifr) const override;
+};
+
+} // namespace mock
diff --git a/ncsid/test/nic_mock.cpp b/ncsid/test/nic_mock.cpp
new file mode 100644
index 0000000..8996125
--- /dev/null
+++ b/ncsid/test/nic_mock.cpp
@@ -0,0 +1,337 @@
+#include "nic_mock.h"
+
+#include "platforms/nemora/portable/ncsi.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <stdexcept>
+
+namespace mock
+{
+
+bool NCSIFrame::parse_ethernet_frame(const ncsi_buf_t& ncsi_buf)
+{
+    std::memcpy(&dst_mac_, ncsi_buf.data, sizeof(dst_mac_));
+    std::memcpy(&src_mac_, ncsi_buf.data + sizeof(dst_mac_), sizeof(src_mac_));
+    // The constant defined in a way that assumes big-endian platform, so we are
+    // just going to calculate it here properly.
+    const uint8_t et_hi = *(ncsi_buf.data + 2 * sizeof(mac_addr_t));
+    const uint8_t et_lo = *(ncsi_buf.data + 2 * sizeof(mac_addr_t) + 1);
+    ethertype_ = (et_hi << 8) + et_lo;
+
+    if (ethertype_ != NCSI_ETHERTYPE)
+    {
+        return false;
+    }
+
+    // This code parses the NC-SI command, according to spec and
+    // as defined in platforms/nemora/portable/ncsi.h
+    // It takes some shortcuts to only retrieve the data we are interested in,
+    // such as using offsetof ot get to a particular field.
+    control_packet_type_ =
+        *(ncsi_buf.data + offsetof(ncsi_header_t, control_packet_type));
+    channel_id_ = *(ncsi_buf.data + offsetof(ncsi_header_t, channel_id));
+
+    size_t payload_offset = sizeof(ncsi_header_t);
+    if (control_packet_type_ & NCSI_RESPONSE)
+    {
+        is_response_ = true;
+        control_packet_type_ &= ~NCSI_RESPONSE;
+        std::memcpy(&response_code_, ncsi_buf.data + payload_offset,
+                    sizeof(response_code_));
+        response_code_ = ntohs(response_code_);
+        std::memcpy(&reason_code_,
+                    ncsi_buf.data + payload_offset + sizeof(reason_code_),
+                    sizeof(reason_code_));
+        reason_code_ = ntohs(reason_code_);
+        payload_offset += sizeof(reason_code_) + sizeof(response_code_);
+    }
+
+    if (control_packet_type_ == NCSI_OEM_COMMAND)
+    {
+        std::memcpy(&manufacturer_id_, ncsi_buf.data + payload_offset,
+                    sizeof(manufacturer_id_));
+        manufacturer_id_ = ntohl(manufacturer_id_);
+        // Number of reserved bytes after manufacturer_id_ = 3
+        oem_command_ =
+            *(ncsi_buf.data + payload_offset + sizeof(manufacturer_id_) + 3);
+        payload_offset += sizeof(ncsi_oem_extension_header_t);
+    }
+
+    packet_raw_ =
+        std::vector<uint8_t>(ncsi_buf.data, ncsi_buf.data + ncsi_buf.len);
+    // TODO: Verify payload length.
+
+    return true;
+}
+
+uint32_t NIC::handle_request(const ncsi_buf_t& request_buf,
+                             ncsi_buf_t* response_buf)
+{
+    const ncsi_header_t* ncsi_header =
+        reinterpret_cast<const ncsi_header_t*>(request_buf.data);
+
+    NCSIFrame request_frame;
+    request_frame.parse_ethernet_frame(request_buf);
+    save_frame_to_log(request_frame);
+
+    uint32_t response_size;
+    if (is_loopback_)
+    {
+        std::memcpy(response_buf, &request_buf, sizeof(request_buf));
+        response_size = request_buf.len;
+    }
+    else if (std::find(simple_commands_.begin(), simple_commands_.end(),
+                       ncsi_header->control_packet_type) !=
+             simple_commands_.end())
+    {
+        // Simple Response
+        response_size =
+            ncsi_build_simple_ack(request_buf.data, response_buf->data);
+    }
+    else
+    {
+        // Not-so-Simple Response
+        switch (ncsi_header->control_packet_type)
+        {
+            case NCSI_GET_VERSION_ID:
+                response_size = ncsi_build_version_id_ack(
+                    request_buf.data, response_buf->data, &version_);
+                break;
+            case NCSI_GET_CAPABILITIES:
+                response_size = sizeof(ncsi_capabilities_response_t);
+                {
+                    ncsi_capabilities_response_t response;
+                    ncsi_build_response_header(
+                        request_buf.data, reinterpret_cast<uint8_t*>(&response),
+                        0, 0, response_size - sizeof(ncsi_header_t));
+                    response.channel_count = channel_count_;
+                    std::memcpy(response_buf->data, &response,
+                                sizeof(response));
+                }
+                break;
+            case NCSI_GET_PASSTHROUGH_STATISTICS:
+                if (is_legacy_)
+                {
+                    response_size = ncsi_build_pt_stats_legacy_ack(
+                        request_buf.data, response_buf->data, &stats_legacy_);
+                }
+                else
+                {
+                    response_size = ncsi_build_pt_stats_ack(
+                        request_buf.data, response_buf->data, &stats_);
+                }
+                break;
+            case NCSI_GET_LINK_STATUS:
+                response_size = ncsi_build_link_status_ack(
+                    request_buf.data, response_buf->data, &link_status_);
+                break;
+            case NCSI_OEM_COMMAND:
+                response_size = handle_oem_request(request_buf, response_buf);
+                break;
+            default:
+                response_size = ncsi_build_simple_nack(
+                    request_buf.data, response_buf->data, 1, 1);
+                break;
+        }
+    }
+
+    response_buf->len = response_size;
+
+    return response_size;
+}
+
+uint32_t NIC::handle_oem_request(const ncsi_buf_t& request_buf,
+                                 ncsi_buf_t* response_buf)
+{
+    const ncsi_oem_simple_cmd_t* oem_cmd =
+        reinterpret_cast<const ncsi_oem_simple_cmd_t*>(request_buf.data);
+    uint32_t response_size;
+    switch (oem_cmd->oem_header.oem_cmd)
+    {
+        case NCSI_OEM_COMMAND_GET_HOST_MAC:
+            response_size = ncsi_build_oem_get_mac_ack(
+                request_buf.data, response_buf->data, &mac_);
+            break;
+        case NCSI_OEM_COMMAND_SET_FILTER:
+        {
+            const ncsi_oem_set_filter_cmd_t* cmd =
+                reinterpret_cast<const ncsi_oem_set_filter_cmd_t*>(
+                    request_buf.data);
+            if (set_filter(cmd->hdr.channel_id, cmd->filter))
+            {
+                response_size = ncsi_build_oem_simple_ack(request_buf.data,
+                                                          response_buf->data);
+            }
+            else
+            {
+                response_size = ncsi_build_simple_nack(
+                    request_buf.data, response_buf->data, 3, 4);
+            }
+        }
+        break;
+        case NCSI_OEM_COMMAND_ECHO:
+            response_size =
+                ncsi_build_oem_echo_ack(request_buf.data, response_buf->data);
+            break;
+        case NCSI_OEM_COMMAND_GET_FILTER:
+        {
+            const ncsi_simple_command_t* cmd =
+                reinterpret_cast<const ncsi_simple_command_t*>(
+                    request_buf.data);
+            if (cmd->hdr.channel_id == 0)
+            {
+                response_size = ncsi_build_oem_get_filter_ack(
+                    request_buf.data, response_buf->data, &ch0_filter_);
+            }
+            else if (cmd->hdr.channel_id == 1)
+            {
+                response_size = ncsi_build_oem_get_filter_ack(
+                    request_buf.data, response_buf->data, &ch1_filter_);
+            }
+            else
+            {
+                response_size = ncsi_build_simple_nack(
+                    request_buf.data, response_buf->data, 3, 4);
+            }
+        }
+        break;
+        default:
+            response_size = ncsi_build_simple_nack(request_buf.data,
+                                                   response_buf->data, 1, 2);
+            break;
+    }
+
+    return response_size;
+}
+
+bool NIC::is_filter_configured(uint8_t channel) const
+{
+    if (channel == 0)
+    {
+        return is_ch0_filter_configured_;
+    }
+    else if (channel == 1)
+    {
+        return is_ch1_filter_configured_;
+    }
+
+    throw std::invalid_argument("Unsupported channel");
+}
+
+bool NIC::set_filter(uint8_t channel, const ncsi_oem_filter_t& filter)
+{
+    ncsi_oem_filter_t* nic_filter;
+    if (channel == 0)
+    {
+        nic_filter = &ch0_filter_;
+        is_ch0_filter_configured_ = true;
+    }
+    else if (channel == 1)
+    {
+        nic_filter = &ch1_filter_;
+        is_ch1_filter_configured_ = true;
+    }
+    else
+    {
+        throw std::invalid_argument("Unsupported channel");
+    }
+
+    std::memcpy(nic_filter->mac, filter.mac, MAC_ADDR_SIZE);
+    nic_filter->ip = 0;
+    nic_filter->port = filter.port;
+    return true;
+}
+
+const ncsi_oem_filter_t& NIC::get_filter(uint8_t channel) const
+{
+    if (channel == 0)
+    {
+        return ch0_filter_;
+    }
+    else if (channel == 1)
+    {
+        return ch1_filter_;
+    }
+
+    throw std::invalid_argument("Unsupported channel");
+}
+
+void NIC::set_hostless(bool is_hostless)
+{
+    auto set_flag_op = [](uint8_t lhs, uint8_t rhs) -> auto
+    {
+        return lhs | rhs;
+    };
+
+    auto clear_flag_op = [](uint8_t lhs, uint8_t rhs) -> auto
+    {
+        return lhs & ~rhs;
+    };
+
+    auto flag_op = is_hostless ? set_flag_op : clear_flag_op;
+
+    if (channel_count_ > 0)
+    {
+        ch0_filter_.flags =
+            flag_op(ch0_filter_.flags, NCSI_OEM_FILTER_FLAGS_HOSTLESS);
+    }
+
+    if (channel_count_ > 1)
+    {
+        ch1_filter_.flags =
+            flag_op(ch1_filter_.flags, NCSI_OEM_FILTER_FLAGS_HOSTLESS);
+    }
+}
+
+void NIC::toggle_hostless()
+{
+    if (channel_count_ > 0)
+    {
+        ch0_filter_.flags ^= NCSI_OEM_FILTER_FLAGS_HOSTLESS;
+    }
+
+    if (channel_count_ > 1)
+    {
+        ch1_filter_.flags ^= NCSI_OEM_FILTER_FLAGS_HOSTLESS;
+    }
+}
+
+bool NIC::is_hostless()
+{
+    return ch0_filter_.flags & NCSI_OEM_FILTER_FLAGS_HOSTLESS;
+}
+
+void NIC::save_frame_to_log(const NCSIFrame& frame)
+{
+    if (cmd_log_.size() >= max_log_size_)
+    {
+        cmd_log_.erase(cmd_log_.begin());
+    }
+
+    cmd_log_.push_back(frame);
+}
+
+const std::vector<uint8_t> NIC::simple_commands_ = {
+    NCSI_CLEAR_INITIAL_STATE,
+    NCSI_SELECT_PACKAGE,
+    NCSI_DESELECT_PACKAGE,
+    NCSI_ENABLE_CHANNEL,
+    NCSI_DISABLE_CHANNEL,
+    NCSI_RESET_CHANNEL,
+    NCSI_ENABLE_CHANNEL_NETWORK_TX,
+    NCSI_DISABLE_CHANNEL_NETWORK_TX,
+    NCSI_AEN_ENABLE,
+    NCSI_SET_LINK,
+    NCSI_SET_VLAN_FILTER,
+    NCSI_ENABLE_VLAN,
+    NCSI_DISABLE_VLAN,
+    NCSI_SET_MAC_ADDRESS,
+    NCSI_ENABLE_BROADCAST_FILTER,
+    NCSI_DISABLE_BROADCAST_FILTER,
+    NCSI_ENABLE_GLOBAL_MULTICAST_FILTER,
+    NCSI_DISABLE_GLOBAL_MULTICAST_FILTER,
+    NCSI_SET_NCSI_FLOW_CONTROL,
+};
+
+} // namespace mock
diff --git a/ncsid/test/nic_mock.h b/ncsid/test/nic_mock.h
new file mode 100644
index 0000000..97a6737
--- /dev/null
+++ b/ncsid/test/nic_mock.h
@@ -0,0 +1,222 @@
+#pragma once
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/ncsi_fsm.h"
+#include "platforms/nemora/portable/ncsi_server.h"
+
+#include <netinet/in.h>
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+namespace mock
+{
+
+class NCSIFrame
+{
+  public:
+    mac_addr_t get_dst_mac() const
+    {
+        return dst_mac_;
+    }
+
+    mac_addr_t get_src_mac() const
+    {
+        return src_mac_;
+    }
+
+    uint16_t get_ethertype() const
+    {
+        return ethertype_;
+    }
+
+    bool is_ncsi() const
+    {
+        return ethertype_ == NCSI_ETHERTYPE;
+    }
+
+    uint8_t get_control_packet_type() const
+    {
+        return control_packet_type_;
+    }
+
+    void set_conrol_packet_type(uint8_t control_packet_type)
+    {
+        control_packet_type_ = control_packet_type;
+    }
+
+    bool is_oem_command() const
+    {
+        return control_packet_type_ == NCSI_OEM_COMMAND;
+    }
+
+    uint8_t get_channel_id() const
+    {
+        return channel_id_;
+    }
+
+    void set_channel_id(uint8_t channel_id)
+    {
+        channel_id_ = channel_id;
+    }
+
+    uint8_t get_oem_command() const
+    {
+        return oem_command_;
+    }
+
+    void set_oem_command(uint8_t oem_command)
+    {
+        set_conrol_packet_type(NCSI_OEM_COMMAND);
+        oem_command_ = oem_command;
+    }
+
+    uint32_t get_manufacturer_id() const
+    {
+        return manufacturer_id_;
+    }
+
+    std::vector<uint8_t>::size_type get_size() const
+    {
+        return packet_raw_.size();
+    }
+
+    bool is_response() const
+    {
+        return is_response_;
+    }
+
+    uint16_t get_response_code() const
+    {
+        return response_code_;
+    }
+
+    uint16_t get_reason_code() const
+    {
+        return reason_code_;
+    }
+
+    bool parse_ethernet_frame(const ncsi_buf_t& ncsi_buf);
+
+  private:
+    mac_addr_t dst_mac_;
+    mac_addr_t src_mac_;
+    uint16_t ethertype_ = NCSI_ETHERTYPE;
+    uint8_t control_packet_type_;
+    uint8_t channel_id_;
+    uint8_t oem_command_;
+    uint32_t manufacturer_id_;
+    uint16_t response_code_ = 0;
+    uint16_t reason_code_ = 0;
+    bool is_response_ = false;
+    std::vector<uint8_t> packet_raw_;
+};
+
+class NIC
+{
+  public:
+    explicit NIC(bool legacy = false, uint8_t channel_count = 1) :
+        channel_count_{channel_count}
+    {
+        if (legacy)
+        {
+            version_.firmware_version = htonl(0x08000000);
+        }
+        else
+        {
+            version_.firmware_version = 0xabcdef12;
+        }
+
+        is_legacy_ = legacy;
+
+        set_link_up();
+    }
+
+    void set_link_up()
+    {
+        link_status_.link_status |= htonl(NCSI_LINK_STATUS_UP);
+    }
+
+    void set_mac(const mac_addr_t& mac)
+    {
+        mac_ = mac;
+    }
+
+    mac_addr_t get_mac() const
+    {
+        return mac_;
+    }
+
+    uint8_t get_channel_count() const
+    {
+        return channel_count_;
+    }
+
+    // ????? NICs with Google firmware version ????
+    bool is_legacy() const
+    {
+        return is_legacy_;
+    }
+
+    uint32_t handle_request(const ncsi_buf_t& request_buf,
+                            ncsi_buf_t* response_buf);
+
+    const std::vector<NCSIFrame>& get_command_log() const
+    {
+        return cmd_log_;
+    }
+
+    bool set_filter(uint8_t channel, const ncsi_oem_filter_t& filter);
+    const ncsi_oem_filter_t& get_filter(uint8_t channel) const;
+
+    void set_hostless(bool is_hostless);
+    void toggle_hostless();
+    bool is_hostless();
+
+    // The NIC itself does not really have a loopback. This is used to emulate
+    // the *absence* of NIC and loopback plug inserted.
+    void set_loopback()
+    {
+        is_loopback_ = true;
+    }
+
+    void reset_loopback()
+    {
+        is_loopback_ = false;
+    }
+
+    bool is_filter_configured(uint8_t channel) const;
+
+  private:
+    static const std::vector<uint8_t> simple_commands_;
+
+    uint32_t handle_oem_request(const ncsi_buf_t& request_buf,
+                                ncsi_buf_t* response_buf);
+
+    void save_frame_to_log(const NCSIFrame& frame);
+
+    ncsi_version_id_t version_;
+    ncsi_oem_filter_t ch0_filter_;
+    ncsi_oem_filter_t ch1_filter_;
+    bool is_ch0_filter_configured_ = false;
+    bool is_ch1_filter_configured_ = false;
+    uint8_t channel_count_;
+    mac_addr_t mac_ = {{0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba}};
+    std::vector<NCSIFrame> cmd_log_;
+
+    /* If used in a continuous loop, cmd_log_ may grow too big over time.
+     * This constant determines how many (most recent) commands will be kept. */
+    const uint32_t max_log_size_ = 1000;
+
+    bool is_legacy_;
+    bool is_loopback_ = false;
+
+    // TODO: populate stats somehow.
+    ncsi_passthrough_stats_t stats_;
+    ncsi_passthrough_stats_legacy_t stats_legacy_;
+
+    ncsi_link_status_t link_status_;
+};
+
+} // namespace mock
diff --git a/ncsid/test/normalize_ip_test.sh b/ncsid/test/normalize_ip_test.sh
new file mode 100755
index 0000000..cb520ef
--- /dev/null
+++ b/ncsid/test/normalize_ip_test.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+TEST_DIR="$(dirname "${BASH_SOURCE[0]}")"
+source "$TEST_DIR"/test_lib.sh
+
+TestNormalizeIPInvalidArgs() {
+  ! "$NORMALIZE_IP"
+  ! "$NORMALIZE_IP" '192.168.10.1' 'extra'
+}
+
+TestNormalizeIPBadIP() {
+  ! "$NORMALIZE_IP" '0f0.100.595.444'
+  ! "$NORMALIZE_IP" 'fx80::1'
+}
+
+TestNormalizeIPv4() {
+  StrEq "$("$NORMALIZE_IP" '192.168.10.1')" '192.168.10.1'
+  StrEq "$("$NORMALIZE_IP" '1.1.1.1')" '1.1.1.1'
+}
+
+TestNormalizeIPv6() {
+  StrEq "$("$NORMALIZE_IP" 'fe80:00B1::0000:1')" 'fe80:b1::1'
+}
+
+TESTS+=(
+  TestNormalizeIPInvalidArgs
+  TestNormalizeIPBadIP
+  TestNormalizeIPv4
+  TestNormalizeIPv6
+)
+
+return 0 2>/dev/null
+TestAnythingMain
diff --git a/ncsid/test/normalize_mac_test.sh b/ncsid/test/normalize_mac_test.sh
new file mode 100755
index 0000000..4c94570
--- /dev/null
+++ b/ncsid/test/normalize_mac_test.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+TEST_DIR="$(dirname "${BASH_SOURCE[0]}")"
+source "$TEST_DIR"/test_lib.sh
+
+TestNormalizeMACInvalidArgs() {
+  ! "$NORMALIZE_MAC"
+  ! "$NORMALIZE_MAC" '0:0:0:0:0:0' 'extra'
+}
+
+TestNormalizeMACBadMAC() {
+  ! "$NORMALIZE_MAC" '0:0'
+  ! "$NORMALIZE_MAC" '0:0:0:0:0:0:0'
+  ! "$NORMALIZE_MAC" '1ff:0:0:0:0'
+}
+
+TestNormalizeMACSuccess() {
+  StrEq "$("$NORMALIZE_MAC" '0:0:0:0:0:0')" '00:00:00:00:00:00'
+  StrEq "$("$NORMALIZE_MAC" 'ff:0f:0:0:11:1')" 'ff:0f:00:00:11:01'
+  StrEq "$("$NORMALIZE_MAC" '0:0:0:0:0:ff')" "$("$NORMALIZE_MAC" '0:0:0:0:0:FF')"
+}
+
+TESTS+=(
+  TestNormalizeMACInvalidArgs
+  TestNormalizeMACBadMAC
+  TestNormalizeMACSuccess
+)
+
+return 0 2>/dev/null
+TestAnythingMain
diff --git a/ncsid/test/sock_test.cpp b/ncsid/test/sock_test.cpp
new file mode 100644
index 0000000..2ff2638
--- /dev/null
+++ b/ncsid/test/sock_test.cpp
@@ -0,0 +1,21 @@
+#include "ncsi_sockio.h"
+#include "net_iface_mock.h"
+
+#include <gmock/gmock.h>
+
+TEST(TestSockIO, TestBind)
+{
+    mock::IFace iface_mock;
+    constexpr int test_index = 5;
+    iface_mock.index = test_index;
+
+    // This needs to be negative so that ncsi::SockIO
+    // won't try to close the socket upon desctrution.
+    constexpr int sock_fake_fd = -10;
+    ncsi::SockIO ncsi_sock(sock_fake_fd);
+
+    ncsi_sock.bind_to_iface(iface_mock);
+    EXPECT_THAT(iface_mock.bound_socks.size(), testing::Ge(0));
+    EXPECT_THAT(iface_mock.bound_socks, testing::Contains(sock_fake_fd));
+    EXPECT_EQ(iface_mock.flags & IFF_PROMISC, IFF_PROMISC);
+}
diff --git a/ncsid/test/test_lib.sh b/ncsid/test/test_lib.sh
new file mode 100644
index 0000000..9e6c882
--- /dev/null
+++ b/ncsid/test/test_lib.sh
@@ -0,0 +1,32 @@
+# Compares two strings and prints out an error message if they are not equal
+StrEq() {
+  if [ "$1" != "$2" ]; then
+    echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} Mismatched strings" >&2
+    echo "  Expected: $2" >&2
+    echo "  Got:      $1" >&2
+    exit 1
+  fi
+}
+
+TESTS=()
+
+# Runs tests and emits output specified by the Test Anything Protocol
+# https://testanything.org/
+TestAnythingMain() {
+  set -o nounset
+  set -o errexit
+  set -o pipefail
+
+  echo "TAP version 13"
+  echo "1..${#TESTS[@]}"
+
+  local i
+  for ((i=0; i <${#TESTS[@]}; ++i)); do
+    local t="${TESTS[i]}"
+    local tap_i=$((i + 1))
+    if ! "$t"; then
+      printf "not "
+    fi
+    printf "ok %d - %s\n" "$tap_i" "$t"
+  done
+}